import { useEffect, useRef, useState, type ReactElement } from 'react' import { useBridge } from '@jaspers-ai/sdk' import { isCalculation, type Passage } from './chat' import { Markdown } from './MarkdownView' import type { Outline } from './research' // A cited passage in its document: the document's sections in order, the cited one open and drawn // as Markdown with the passage marked and scrolled to, the rest open on a click. A 10-K is hundreds of // thousands of characters, so a section is read only when it is opened; what was read is kept for // the life of the view. A cited calculation is no document: the window shows its lines, worked out. interface Opened extends Outline { /** The section open first, and its text. */ open: number | undefined text: string } const documents = new Map>() const sectionTexts = new Map>() /** A passage's id is `#`. */ export function placeOf(passage: Passage): { doc: string; chunk: number | null } { const hash = passage.id.lastIndexOf('#') if (hash === -1) return { doc: passage.id, chunk: null } const chunk = Number(passage.id.slice(hash + 1)) return { doc: passage.id.slice(0, hash), chunk: Number.isInteger(chunk) ? chunk : null } } export function DocWindow({ passage, pinned, onPin, onClose, onEnter, onLeave, onLink, }: { passage: Passage pinned: boolean onPin: () => void onClose: () => void onEnter: () => void onLeave: () => void onLink: (href: string) => void }): ReactElement { const bridge = useBridge() const [opened, setOpened] = useState(null) const [failure, setFailure] = useState(null) const [texts, setTexts] = useState>(new Map()) const body = useRef(null) const { doc, chunk } = placeOf(passage) const calculation = isCalculation(passage) useEffect(() => { setOpened(null) setFailure(null) if (calculation) return const key = passage.id if (!documents.has(key)) { documents.set( key, bridge.runSource('agents/document', chunk === null ? { doc } : { doc, chunk }).then((result) => { if (result.kind !== 'text') throw new Error('The document did not come back.') return JSON.parse(result.text) as Opened }), ) } let live = true documents.get(key)!.then( (got) => { if (!live) return setOpened(got) setTexts(new Map(got.open === undefined ? [] : [[got.open, got.text]])) }, (err: unknown) => { documents.delete(key) if (live) setFailure(err instanceof Error ? err.message : String(err)) }, ) return () => { live = false } }, [bridge, passage.id, doc, chunk, calculation]) // The passage in view once its section is drawn. Braces: scrollIntoView returns a promise here. useEffect(() => { body.current?.querySelector('.ag-mark')?.scrollIntoView({ block: 'center' }) }, [opened]) const toggle = (seq: number): void => { if (texts.has(seq)) { const next = new Map(texts) next.delete(seq) setTexts(next) return } setTexts(new Map(texts).set(seq, null)) const key = `${doc}/${seq}` if (!sectionTexts.has(key)) { sectionTexts.set( key, bridge.runSource('agents/document-section', { doc, seq }).then((r) => (r.kind === 'text' ? r.text : '')), ) } sectionTexts.get(key)!.then( (text) => setTexts((now) => (now.has(seq) ? new Map(now).set(seq, text) : now)), (err: unknown) => { sectionTexts.delete(key) setFailure(err instanceof Error ? err.message : String(err)) }, ) } const url = opened?.doc.url ?? passage.url return (
[{passage.ref}] {passage.label} {url && ( )}
{calculation &&
{passage.text}
} {failure &&
{failure}
} {!calculation && !opened && !failure &&
Opening the document…
} {opened?.sections.map((section) => { const text = texts.get(section.seq) const open = texts.has(section.seq) return (
{open && (text === null || text === undefined ? (
Reading…
) : ( ))}
) })}
) }