carbon-tokenization / frontend /src /editor /BibliographyView.tsx
tfrere's picture
tfrere HF Staff
refactor: store pre-computed attrs in citation/bibliography nodes
905f030
Raw
History Blame Contribute Delete
3.53 kB
import { NodeViewWrapper } from "@tiptap/react";
import { useEffect, useState, useCallback, useRef } from "react";
import type { NodeViewProps } from "@tiptap/react";
export function BibliographyView({ editor, node, getPos }: NodeViewProps) {
const [html, setHtml] = useState("");
const [count, setCount] = useState(0);
const [style, setStyle] = useState("apa");
const cacheRef = useRef("");
const timerRef = useRef<ReturnType<typeof setTimeout>>();
// Sync style from collaborative Y.Map("settings")
useEffect(() => {
const settingsMap = (editor.storage.citation as any)?.settingsMap;
if (!settingsMap) return;
const sync = () => {
const val = settingsMap.get("citationStyle");
if (val && val !== style) setStyle(val as string);
};
sync();
settingsMap.observe(sync);
return () => settingsMap.unobserve(sync);
}, [editor]);
const refresh = useCallback(() => {
const citationsMap = (editor.storage.citation as any)?.citationsMap;
if (!citationsMap) return;
const keys = new Set<string>();
editor.state.doc.descendants((node) => {
if (node.type.name === "citation" && node.attrs.key) {
keys.add(node.attrs.key as string);
}
});
const entries: any[] = [];
for (const key of keys) {
const entry = citationsMap.get(key);
if (entry) entries.push({ ...entry, id: key });
}
setCount(entries.length);
if (!entries.length) {
setHtml("");
cacheRef.current = "";
return;
}
const cacheKey = entries
.map((e) => e.id)
.sort()
.join(",") + `:${style}`;
if (cacheKey === cacheRef.current) return;
cacheRef.current = cacheKey;
fetch("/api/citations/format", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ entries, style }),
})
.then((r) => r.json())
.then((data) => {
if (data.html) {
setHtml(data.html);
// Persist rendered HTML into node attrs for server-side generation
const pos = getPos();
if (typeof pos === "number" && node.attrs.renderedHtml !== data.html) {
editor.commands.command(({ tr }) => {
tr.setNodeMarkup(pos, undefined, { ...node.attrs, renderedHtml: data.html });
return true;
});
}
}
})
.catch(console.error);
}, [editor, style]);
useEffect(() => {
const debouncedRefresh = () => {
clearTimeout(timerRef.current);
timerRef.current = setTimeout(refresh, 500);
};
refresh();
const citationsMap = (editor.storage.citation as any)?.citationsMap;
if (citationsMap) citationsMap.observe(debouncedRefresh);
const onUpdate = () => debouncedRefresh();
editor.on("update", onUpdate);
return () => {
clearTimeout(timerRef.current);
if (citationsMap) citationsMap.unobserve(debouncedRefresh);
editor.off("update", onUpdate);
};
}, [editor, refresh]);
return (
<NodeViewWrapper>
<div className="bibliography-block" contentEditable={false}>
<h2 className="bibliography-title">References</h2>
{count === 0 ? (
<p className="bibliography-empty">
No citations yet. Use /Citation to add references.
</p>
) : (
<div
className="bibliography-content"
dangerouslySetInnerHTML={{ __html: html }}
/>
)}
</div>
</NodeViewWrapper>
);
}