import { Texts } from "./texts.js"; import { cacheConfiguration, sameCacheConfiguration } from "./cache.js"; import { configDefinition, configValues } from "./annotator-config.js"; import { presentAnnotations } from "./annotation-display.js"; import { Options } from "./controls.js"; import { Choice, SegmentedControl, useCommandControl } from "@quixos/camino-react/ui"; import { renderAnnotations, type AnnotationRender } from "./render-hooks.js"; import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { referenceOf, useCaminoQuery, useInterface, useLiveControl, useMutationFeedback, type ExactReference, } from "@quixos/camino-react"; import { interfaces, queries, type Modules } from "./gen/client.js"; import { useVocabulary, VocabularyLabel } from "./vocabulary.js"; import { useCachedAnnotations } from "./cached-results.js"; import { extensionActions, WordMenu, type ReaderContext } from "./extensions.js"; import { createReaderExtensions } from "./reader-extensions.js"; import type { CanvasContextTarget } from "./canvas/Surface.js"; import { Surface } from "./canvas/Surface.js"; import { copyText, selectionRange, type Selection } from "./canvas/selection.js"; import { intersects, type Annotation, type Range } from "./canvas/types.js"; import type { ReadingWindow } from "./canvas/pages.js"; import type { ProcessingRegion } from "./canvas/processing.js"; import { useProcessing } from "./processing.js"; import { HoverIntent, processorMatches, selectSuccessfulAnnotationJobs } from "./work.js"; import { ReaderSettings } from "./editor.js"; import { Importer, SpeechSettings, Playback, Rewrite } from "./sources.js"; import { accepted, asInterface, errorMessage, type Catalog, type DocNode, type FeatureNode, type Root, type ReaderNode, } from "./model.js"; import { readingStyle } from "./style.js"; import { usePageViews } from "./page-views.js"; import type { NavigationValues } from "./model.js"; export const Reader: Modules["Reader"] = ({ subject, navigation, input }) => { const app = useRef(null); useLayoutEffect(() => { const resize = () => { if (app.current) app.current.style.setProperty("--reader-top", Math.max(0, app.current.getBoundingClientRect().top) + "px"); }; resize(); const observer = new ResizeObserver(resize); observer.observe(document.documentElement); window.addEventListener("resize", resize); return () => { observer.disconnect(); window.removeEventListener("resize", resize); }; }, []); const root = input.body.root, library = subject; const nav = useInterface(interfaces.ReadingNavigation, navigation.reference), ops = useInterface( { catalog: interfaces.LanguageReader.catalog, ensureReaders: interfaces.LanguageReader.ensureReaders, ensureVocabulary: interfaces.LanguageReader.ensureVocabulary, }, library, ); const feedback = useMutationFeedback(); const ui = input.nav.root.ui; const panel = ui.panel, page = ui.page; const setUI = (changes: Partial) => { if (nav.status === "ready") feedback.track(nav.value.ui.set({ ...ui, ...changes })); }; const setPanel = (panel: string) => setUI({ panel }); const setPage = (page: string) => setUI({ page }); const [catalog, setCatalog] = useState([]), [error, setError] = useState(""), [pendingDocument, setPendingDocument] = useState(null); const chosen = input.nav.root.document ?? root.documents.entries.find((e) => !e.node.archived)?.node; const renders = useMemo( () => Object.fromEntries(catalog.map((item) => [item.key, configDefinition(item.config).renders])), [catalog], ); const revisions = useMemo( () => Object.fromEntries(catalog.map((item) => [item.key, item.processorRevision])), [catalog, root.inferenceProfile], ); const revisionAliases = useMemo( () => Object.fromEntries( catalog.map((item) => [item.key, configDefinition(item.config).cache?.previousRevisions ?? []]), ), [catalog], ); const computations = useMemo( () => Object.fromEntries( root.features.entries.map(({ node: feature }) => { const entry = catalog.find((item) => item.key === feature.data.processor); const definition = configDefinition(entry?.config ?? "{}"); const options = { ...Object.fromEntries(definition.fields.map((field) => [field.key, field.default])), ...configValues(feature.options), }; return [ referenceOf(feature).object, cacheConfiguration( definition.cache?.identity ?? feature.data.processor, feature.data.scope, feature.data.generation, options, definition.cache?.inputs ?? [], ), ]; }), ), [catalog, root.features.entries], ); const pendingAuthors = root.tasks.entries .map((e) => e.node) .filter( (t) => t.data.kind === "customize" && ["queued", "running", "waiting_input"].includes( t.thread?.active?.progress.status ?? t.request?.progress.status ?? t.data.status, ), ); const selected = pendingDocument ? asInterface(pendingDocument, "document") : chosen ? asInterface(referenceOf(chosen), "document") : undefined; useEffect(() => { if (pendingDocument?.object === input.nav.root.document?._camino.ref.object) setPendingDocument(null); }, [pendingDocument?.object, input.nav.root.document?._camino.ref.object]); const contentQuery = useCaminoQuery(queries.DocumentContentView, { enabled: !!selected, root: selected, input: {}, admission: { query: queries.DocumentContentInputs, input: {} }, }); const contentReady = contentQuery.status === "ready" && referenceOf(contentQuery.data.root).object === selected?.object && (!pendingDocument || input.nav.root.document?._camino.ref.object === pendingDocument.object); useEffect(() => { let closed = false; if (ops.status === "ready") void ops.value.catalog .call(null) .then((value) => { if (!closed) setCatalog(value); }) .catch((error) => { if (!closed) setError(errorMessage(error)); }); return () => { closed = true; }; }, [ops.status, ops.status === "ready" ? ops.value : null]); const open = (ref: ExactReference) => { if (nav.status !== "ready") return; setPendingDocument(ref); const ticket = nav.value.open.call(asInterface(ref, "document")); feedback.track(ticket); void ticket.settled.then( (outcome) => { if (outcome.status !== "accepted") setPendingDocument((current) => (current?.object === ref.object ? null : current)); }, () => setPendingDocument((current) => (current?.object === ref.object ? null : current)), ); setUI({ panel: "", page: "reading" }); }; useEffect(() => { if (ops.status === "ready" && !root.defaultReader) void accepted(ops.value.ensureReaders.call(null)).catch((e) => setError(errorMessage(e))); }, [ops.status, root.defaultReader]); useEffect(() => { if (ops.status === "ready" && !root.defaultVocabulary) void accepted(ops.value.ensureVocabulary.call(null)).catch((e) => setError(errorMessage(e))); }, [ops.status, root.defaultVocabulary]); const selectedDoc = contentQuery.status === "ready" && contentReady ? contentQuery.data.root : null; const activeReader = root.readers.entries .map((e) => e.node) .find( (r) => referenceOf(r).object === (selectedDoc?.reader?._camino.ref.object ?? root.defaultReader?._camino.ref.object), ); const openReaderSettings = () => { if (nav.status !== "ready") return; feedback.track( nav.value.selectSettings.call({ reader: selectedDoc?.reader ? asInterface(referenceOf(selectedDoc.reader), "reader") : root.defaultReader ? asInterface(referenceOf(root.defaultReader), "reader") : null, feature: null, section: "general", open: true, document: null, }), ); }; return (
{pendingAuthors.length > 0 && ( )} {page === "library" ? ( setPanel("import")} speech={() => setPanel("speech")} /> ) : contentQuery.status === "ready" && contentReady ? ( setPage("library")} /> ) : (

{selected ? "Opening text…" : "No text selected"}

{!selected && }
)} {(error || feedback.message || contentQuery.status === "error") && (

{error || feedback.message || "Unable to open this document."}

)}
{panel === "import" && ( setPanel("")} added={(ref) => { setPendingDocument(ref); setUI({ page: "reading", panel: "" }); }} /> )} {panel === "speech" && setPanel("")} />} {panel === "features" && ( setPanel("")} /> )}
); }; function ReadingDocument({ document: contentDoc, library, root, revisions, revisionAliases, computations, renders, reader, onOpen, navigation, cursor, readingView, annotationTarget, viewports, onAnnotations, onLibrary, panel, setPanel, }: { document: DocNode; library: ExactReference; root: Root; revisions: Record; revisionAliases: Record; computations: Record; renders: Record; reader?: ReaderNode; onOpen: (ref: ExactReference) => void; navigation: ExactReference; cursor: { anchor: number; focus: number; size: number }; readingView: { displayMode: string; position: number }; annotationTarget: NavigationValues["annotationTarget"]; viewports: NavigationValues["viewports"]; panel: string; setPanel: (panel: string) => void; onAnnotations: () => void; onLibrary: () => void; }) { const cachedAnnotations = useCachedAnnotations(referenceOf(contentDoc)); const annotationsReady = cachedAnnotations.ready; const doc = useMemo( () => ({ ...contentDoc, jobs: cachedAnnotations.ready ? { entries: cachedAnnotations.entries } : contentDoc.jobs, }), [contentDoc, cachedAnnotations.ready, cachedAnnotations.entries], ); const ops = useInterface({ prepareVariant: interfaces.ReadingDocument.prepareVariant }, referenceOf(doc)), nav = useInterface(interfaces.ReadingNavigation, navigation), feedback = useMutationFeedback(); const [selection, setSelection] = useState({ anchor: cursor.anchor, focus: cursor.focus }), [hover, setHover] = useState(null), [window, setWindow] = useState({ visible: { start: 0, end: 0 }, nearby: { start: 0, end: 0 } }), [message, setMessage] = useState(""), [noteHover, setNoteHover] = useState<{ owner: string | null; range: Range } | null>(null), [menu, setMenu] = useState(null); const focusControl = useLiveControl({ key: navigation.object + "/annotation-target", operation: interfaces.ReadingNavigation.annotationTarget.set, reference: navigation, }); const [earlyFocus, setEarlyFocus] = useState<{ target: Range | null } | null>(null); const focus = earlyFocus ? earlyFocus.target : focusControl.value === undefined ? annotationTarget : focusControl.value; const setFocus = (target: Range | null) => { if (focusControl.ready) focusControl.request(target); else setEarlyFocus({ target }); }; useEffect(() => { if (focusControl.ready && earlyFocus) { focusControl.request(earlyFocus.target); setEarlyFocus(null); } }, [focusControl.ready, earlyFocus]); const pageViews = usePageViews(navigation, viewports, readingView.displayMode === "pages"); const rewrite = panel === "rewrite", setRewrite = (open: boolean) => setPanel(open ? "rewrite" : ""); const cursorTimer = useRef | null>(null), hoverIntent = useRef(new HoverIntent()), viewTimer = useRef | null>(null); const viewRef = useRef(readingView); viewRef.current = readingView; const [surfacePosition, setSurfacePosition] = useState(readingView.position); const viewEchoes = useRef(new Set()), previousMode = useRef(readingView.displayMode); useLayoutEffect(() => { // Delayed publication of our own save must not undo newer local movement. // Positions written by another shared view still move this viewport. const modeChanged = previousMode.current !== readingView.displayMode; previousMode.current = readingView.displayMode; if (!viewEchoes.current.delete(readingView.position) || modeChanged) setSurfacePosition(readingView.position); }, [readingView.position, readingView.displayMode]); const viewWrite = useRef(null), modeWrite = useRef(null); function saveReading(value: typeof readingView) { if (nav.status !== "ready") throw Error("Reading navigation is not ready"); const ticket = nav.value.reading.set(value, { requires: viewWrite.current ? [viewWrite.current] : [] }); if (value.position !== viewRef.current.position) viewEchoes.current.add(value.position); viewWrite.current = ticket.operationId; void ticket.settled.then( (result) => { if (result.status !== "accepted") viewEchoes.current.delete(value.position); if (result.status !== "accepted" && viewWrite.current === ticket.operationId) viewWrite.current = null; if (modeWrite.current === ticket.operationId) modeWrite.current = null; }, () => { viewEchoes.current.delete(value.position); if (viewWrite.current === ticket.operationId) viewWrite.current = null; if (modeWrite.current === ticket.operationId) modeWrite.current = null; }, ); return ticket; } const features = useMemo(() => { const members = new Set(reader?.features.entries.map((e) => referenceOf(e.node).object) ?? []); return root.features.entries.map((e) => e.node).filter((f) => members.has(referenceOf(f).object)); }, [root.features.entries, reader]), jobs = useMemo(() => doc.jobs.entries.map(({ node }) => node), [doc.jobs.entries]); const enabled = features.filter((feature) => feature.data.enabled); const displays = enabled.flatMap((feature) => (renders[feature.data.processor] ?? [{ style: feature.data.style, reveal: feature.data.reveal }]).map((render) => ({ ...feature, data: { ...feature.data, style: render.style, reveal: render.reveal }, })), ); const model = useProcessing({ document: doc, annotationsReady, features, revisions, revisionAliases, computations, renders, window, error: setMessage, }); const currentSection = window.visible; useEffect(() => { if (focus) void model.focus(focus, "click"); }, [focus?.start, focus?.end, model.ready]); const viewControl = useCommandControl({ key: navigation.object + "/mode", value: readingView.displayMode, submit: nav.status === "ready" ? (mode: string) => { if (viewTimer.current) clearTimeout(viewTimer.current); setSurfacePosition(window.visible.start); const ticket = saveReading({ displayMode: mode, position: window.visible.start }); modeWrite.current = ticket.operationId; return ticket; } : null, }); const onWindow = useCallback( (next: ReadingWindow) => { setWindow((previous) => (JSON.stringify(previous) === JSON.stringify(next) ? previous : next)); if (viewTimer.current) clearTimeout(viewTimer.current); if (nav.status === "ready" && next.visible.start !== viewRef.current.position) viewTimer.current = setTimeout(() => { if (nav.status === "ready" && !modeWrite.current) feedback.track(saveReading({ displayMode: viewRef.current.displayMode, position: next.visible.start })); }, 250); }, [nav.status, nav.status === "ready" ? nav.value : null], ); const selectedRange = selectionRange(selection), selectedText = doc.data.text.slice(selectedRange.start, selectedRange.end), activeRange = displays.some((f) => f.data.style === "sidebar" && f.data.reveal === "hover") ? (hover ?? focus) : focus; useEffect(() => { if (!cursorTimer.current) { setSelection({ anchor: cursor.anchor, focus: cursor.focus }); } }, [cursor.anchor, cursor.focus]); useEffect( () => () => { if (cursorTimer.current) clearTimeout(cursorTimer.current); hoverIntent.current.cancel(); if (viewTimer.current) clearTimeout(viewTimer.current); }, [], ); function updateSelection(value: Selection) { setSelection(value); if (cursorTimer.current) clearTimeout(cursorTimer.current); cursorTimer.current = setTimeout(() => { cursorTimer.current = null; if (nav.status === "ready") feedback.track(nav.value.cursor.set({ ...value, size: cursor.size })); }, 180); } const request = model.request; const vocabularyRef = reader?.vocabulary ? referenceOf(reader.vocabulary) : root.defaultVocabulary ? referenceOf(root.defaultVocabulary) : null; const vocabulary = useVocabulary(vocabularyRef); const vocabularyAPI = useInterface( { remember: interfaces.ReadingVocabulary.remember }, vocabularyRef ? asInterface(vocabularyRef, "vocabulary") : undefined, !!vocabularyRef, ); const [savingWord, setSavingWord] = useState(null); const extensions = useMemo( () => createReaderExtensions({ vocabulary: vocabularyRef, name: vocabulary.node?.name ?? "Vocabulary", known: vocabulary.known, busy: savingWord, ready: vocabularyAPI.status === "ready", report: (error) => setMessage(errorMessage(error)), async remember(word) { if (vocabularyAPI.status !== "ready" || savingWord) return; setSavingWord(word); try { await accepted(vocabularyAPI.value.remember.call(word)); } finally { setSavingWord(null); } }, }), [vocabularyRef?.object, vocabulary.node?.name, vocabulary.known, vocabularyAPI, savingWord], ); const annotationMap = useMemo(() => { const result: Annotation[] = [], seen = new Set(); for (const feature of enabled) for (const job of selectSuccessfulAnnotationJobs( jobs, (job) => processorMatches(job.data, revisions[feature.data.processor], revisionAliases[feature.data.processor]) && job.data.sourceRevision === doc.data.sourceRevision && sameCacheConfiguration(computations[referenceOf(feature).object], job.configuration), )) { let items: { start: number; end: number; text: string; tag: string; color?: string; fontFamily?: string; fontSize?: number; }[]; try { items = JSON.parse(job.data.output).items; } catch { continue; } if (!Array.isArray(items)) continue; for (const [index, item] of presentAnnotations(feature.data.processor, items, configValues(feature.options), { source: doc.data.text, vocabulary: vocabulary.known, }).entries()) { const featureId = referenceOf(feature).object, key = `${featureId}:${item.start}:${item.end}`; if (seen.has(key)) continue; seen.add(key); result.push( ...renderAnnotations( { ...item, activation: { start: job.data.start, end: job.data.end }, id: `${featureId}:${job.data.key}:${index}`, feature: featureId, // Broader context sits above word labels. Graph relation order // does not express lane order and can differ between workspaces. priority: (feature.data.scope === "word" ? 0 : feature.data.scope === "sentence" ? 1 : 2) * features.length + features.indexOf(feature), style: item.style ?? (feature.data.style as Annotation["style"]), reveal: item.reveal ?? (feature.data.reveal as Annotation["reveal"]), color: item.color ?? feature.data.color, }, doc.data.text, item.style ? undefined : renders[feature.data.processor], ), ); } } return result; }, [jobs, features, revisions, revisionAliases, doc.data.sourceRevision, computations, renders, vocabulary.known]); const visible = useCallback( (id: string) => { const annotation = annotationMap.find((item) => item.id === id), feature = features.find((feature) => referenceOf(feature).object === annotation?.feature); if (!annotation || !feature) return false; const reveal = annotation.reveal ?? feature.data.reveal; if (reveal === "click" && feature.data.generation === "focus" && feature.data.scope !== "word") return ( !!focus && (model.clickScopes[referenceOf(feature).object] ?? []).some( (scope) => annotation.activation?.start === scope.start && annotation.activation?.end === scope.end, ) ); const range = reveal === "hover" ? hover : reveal === "click" ? focus : null; return reveal === "always" || (!!range && intersects(annotation, range)); }, [annotationMap, features, focus, hover, model.clickScopes], ); function choose(range: Range) { setFocus(range); setHover(null); hoverIntent.current.cancel(); model.cancelHover(); } function hoverWord(range: Range | null) { if (hover?.start === range?.start && hover?.end === range?.end) return; setHover(range); hoverIntent.current.cancel(); model.cancelHover(); if (range) hoverIntent.current.schedule(() => { void model.focus(range, "hover"); }); } const context: ReaderContext = { library, document: referenceOf(doc), sourceRevision: doc.data.sourceRevision, text: doc.data.text, reader, target: selectedRange.end > selectedRange.start ? selectedRange : focus, targetKind: selectedRange.end > selectedRange.start ? "selection" : "word", annotations: annotationMap, highlight: useCallback((range: Range | null) => setNoteHover(range ? { owner: null, range } : null), []), }; const wordActions = extensionActions(extensions, context); const sidebarExtensions = extensions.filter( (extension) => extension.Sidebar && (extension.sidebarVisible?.(context) ?? true), ); const sidebar = annotationMap .filter((item) => visible(item.id) && item.style === "sidebar" && intersects(item, activeRange ?? currentSection)) .slice(0, 12); const sidebarJobs = displays .filter((feature) => feature.data.style === "sidebar") .flatMap((feature) => jobs .filter( (job) => job.data.kind === "annotation" && processorMatches(job.data, revisions[feature.data.processor], revisionAliases[feature.data.processor]) && sameCacheConfiguration(job.configuration, computations[referenceOf(feature).object]) && job.data.sourceRevision === doc.data.sourceRevision && intersects( job.data, (feature.data.reveal === "hover" ? hover : feature.data.reveal === "click" ? focus : currentSection) ?? { start: 0, end: 0, }, ), ) .map((job) => ({ feature, job })), ); const pendingSidebars = displays .filter((feature) => feature.data.style === "sidebar") .flatMap((feature) => model.pending .filter( (item) => computations[referenceOf(item.feature).object] === computations[referenceOf(feature).object] && intersects( item.range, (feature.data.reveal === "hover" ? hover : feature.data.reveal === "click" ? focus : currentSection) ?? { start: 0, end: 0, }, ), ) .map((item) => ({ feature, range: item.range })), ); const hoveredPanelPresent = !noteHover?.owner || sidebar.some((item) => noteHover?.owner === "annotation:" + item.id) || pendingSidebars.some( ({ feature, range }) => noteHover?.owner === "pending:" + referenceOf(feature).object + ":" + range.start, ) || sidebarJobs.some( ({ feature, job }) => job.data.status !== "ready" && noteHover?.owner === "job:" + referenceOf(feature).object + ":" + referenceOf(job).object, ); useLayoutEffect(() => { if (!hoveredPanelPresent) setNoteHover(null); }, [hoveredPanelPresent]); const processing = useMemo(() => { const scopes: ProcessingRegion[] = []; for (const job of jobs) { const feature = enabled.find( (f) => sameCacheConfiguration(computations[referenceOf(f).object], job.configuration) && processorMatches(job.data, revisions[f.data.processor], revisionAliases[f.data.processor]), ); if ( !feature || job.data.kind !== "annotation" || job.data.status !== "queued" || job.data.sourceRevision !== doc.data.sourceRevision || !processorMatches(job.data, revisions[feature.data.processor], revisionAliases[feature.data.processor]) ) continue; scopes.push({ feature: referenceOf(feature).object, range: { start: job.data.start, end: job.data.end }, label: `Preparing ${feature.data.name}`, color: feature.data.color, }); } return scopes; }, [jobs, features, revisions, root.inferenceProfile, doc.data.sourceRevision, computations]); const previousLayoutAnnotations = useRef([]); const layoutAnnotations = useMemo(() => { const next = annotationMap.filter( (a) => !["overhead", "replace", "bold", "italic"].includes(a.style) || visible(a.id), ); const previous = previousLayoutAnnotations.current; if (next.length === previous.length && next.every((annotation, index) => annotation === previous[index])) return previous; previousLayoutAnnotations.current = next; return next; }, [annotationMap, visible]); const pendingTooltips = displays .filter((feature) => feature.data.style === "tooltip") .flatMap((feature) => { const range = feature.data.reveal === "hover" ? hover : focus; if ( !range || annotationMap.some((item) => item.feature === referenceOf(feature).object && intersects(item, range)) ) return []; const waiting = model.pending.some( (item) => item.feature.data.processor === feature.data.processor && intersects(item.range, range), ) || jobs.some( (job) => job.data.status === "queued" && job.data.sourceRevision === doc.data.sourceRevision && job.configuration === computations[referenceOf(feature).object] && processorMatches(job.data, revisions[feature.data.processor], revisionAliases[feature.data.processor]) && intersects(job.data, range), ); return waiting ? [ { ...range, id: "pending:" + referenceOf(feature).object, feature: referenceOf(feature).object, style: "tooltip" as const, text: "Preparing…", color: feature.data.color, label: feature.data.name, trigger: feature.data.reveal as "hover" | "click", }, ] : []; }); const variant = useMemo(() => { try { const value = JSON.parse(doc.adaptation); return Array.isArray(value.sections) ? (value as { source: string; sections: { start: number; end: number; text?: string }[] }) : null; } catch { return null; } }, [doc.adaptation]); const next = variant?.sections.find((s) => s.text === undefined), variantJob = next ? [...jobs] .sort((a, b) => b.data.createdAt.localeCompare(a.data.createdAt)) .find((j) => j.data.kind === "variant" && j.data.start === next.start && j.data.end === next.end) : undefined; const preparingVariant = useRef(false); const prepareVariant = useCallback( (retry = false) => { if (ops.status !== "ready" || preparingVariant.current) return; preparingVariant.current = true; void accepted(ops.value.prepareVariant.call({ createdAt: new Date().toISOString(), retry })) .catch((e) => setMessage(errorMessage(e))) .finally(() => { preparingVariant.current = false; }); }, [ops.status, ops.status === "ready" ? ops.value : null], ); useEffect(() => { if (next && !variantJob && (!doc.data.text || window.nearby.end >= doc.data.text.length - 1200)) prepareVariant(); }, [next?.start, variantJob, doc.data.text.length, window.nearby.end, prepareVariant]); return ( <>

{doc.data.title}

{doc.data.url && ( Original source ↗ )}
{ void viewControl.request(value); }} />
{vocabulary.continuation} {cachedAnnotations.continuation}
item.style === "tooltip") .map((item) => { const feature = features.find((f) => referenceOf(f).object === item.feature)!; return { ...item, label: feature.data.name, trigger: (item.reveal ?? feature.data.reveal) as "hover" | "click", }; }), ...pendingTooltips, ]} onDismissTooltip={() => { setFocus(null); hoverWord(null); }} processing={[ ...processing, ...model.pending.map(({ feature, range }) => ({ feature: referenceOf(feature).object, range, label: `Preparing ${feature.data.name}`, color: feature.data.color, })), ]} /> {!annotationsReady && enabled.length > 0 && (
Loading annotations…
)} {variant && next && (
{variantJob && ["failed", "skipped", "uncertain"].includes(variantJob.data.status) ? variantJob.data.error || "Adaptation paused" : "Preparing more text…"}{" "} · {variant.sections.filter((s) => s.text !== undefined).length} of {variant.sections.length} sections {variantJob && ["failed", "skipped", "uncertain"].includes(variantJob.data.status) && ( )}
)} {doc.canonical && (
Adapted from {doc.canonical.data.title}. Sections are prepared as you read.
)}
{(displays.some((feature) => feature.data.style === "sidebar") || sidebarExtensions.length > 0) && ( )}
{processing.length > 0 && (

{processing.map((item) => item.label).join(". ")}

)} {(message || feedback.message) && (

{message || feedback.message}

)} {menu && ( setMenu(null)} report={(error) => setMessage(errorMessage(error))} /> )} {rewrite && ( setRewrite(false)} /> )} ); }