import { useEffect, useLayoutEffect, useRef, useState, type ReactElement } from 'react' import type { SessionDiscussionSource, SessionHistoryRequest, SessionHistoryResult, SessionHistoryTurn, } from '../session-history.ts' import type { GraphViewInjected } from './GraphView.tsx' import type { SessionGraphKey } from './locales.ts' import styles from './GraphView.module.css' import { useKnowledge } from './Knowledge.tsx' import { useResearchReuse } from './ResearchReuse.tsx' import { loadWorkingPosition, saveWorkingPosition } from './working-position.ts' /** The Selected Session's explicitly opened discussion reader. */ export function SessionHistory({ sessionId, anchorSeq, highlightSeq, source, retainedSource, workingKey, onUnavailable, read, t }: { readonly sessionId: string readonly anchorSeq?: number readonly highlightSeq?: number readonly source?: SessionDiscussionSource /** Topic provenance is a fallback; an explicitly opened source still wins. */ readonly retainedSource?: SessionDiscussionSource readonly workingKey?: string | undefined readonly onUnavailable?: (() => void) | undefined readonly read: GraphViewInjected['readSessionHistory'] readonly t: (key: SessionGraphKey, params?: Record) => string }): ReactElement { const knowledge = useKnowledge() const reuse = useResearchReuse() const [result, setResult] = useState(() => { const excerpt = source ?? retainedSource return excerpt === undefined ? undefined : { kind: 'excerpt', sessionId, turns: excerpt.turns, hasEarlier: false, hasLater: false, } }) const element = useRef(null) const initialScroll = useRef(anchorSeq === undefined && source === undefined ? loadWorkingPosition(workingKey).historyScroll?.[sessionId] : undefined) const unavailable = useRef(onUnavailable) unavailable.current = onUnavailable const [request, setRequest] = useState(() => { if (source !== undefined) return { sessionId, source } if (anchorSeq !== undefined) return { sessionId, anchorSeq } const position = loadWorkingPosition(workingKey) const range = position.historyRange?.[sessionId] const anchor = position.history?.[sessionId] if (range !== undefined && range.startSeq === anchor) return { sessionId, range } if (anchor !== undefined) return { sessionId, anchorSeq: anchor } return { sessionId, ...(retainedSource === undefined ? {} : { source: retainedSource }) } }) const [loading, setLoading] = useState(true) const [selection, setSelection] = useState() const [failed, setFailed] = useState(false) const [canceled, setCanceled] = useState(false) const [incompleteRange, setIncompleteRange] = useState(false) const activeRead = useRef() const loadedTurns = useRef(new Map()) useEffect(() => { const controller = new AbortController() activeRead.current = controller setLoading(true) setFailed(false) setCanceled(false) void read(request, controller.signal).then(value => { if (controller.signal.aborted) return if (value.kind === 'original') { for (const turn of value.turns) loadedTurns.current.set(turn.startSeq, turn) const anchor = value.turns[0]?.startSeq const position = loadWorkingPosition(workingKey) const history = { ...position.history } const historyRange = { ...position.historyRange } const endSeq = value.turns.at(-1)?.endSeq if (anchor === undefined) delete history[sessionId] else history[sessionId] = anchor if ((request.source !== undefined || request.range !== undefined) && anchor !== undefined && endSeq != null) { historyRange[sessionId] = { startSeq: anchor, endSeq } } else delete historyRange[sessionId] saveWorkingPosition(workingKey, { history, historyRange }) } else if (value.kind === 'unavailable' && (request.anchorSeq !== undefined || request.range !== undefined) && workingKey !== undefined) { const history = { ...loadWorkingPosition(workingKey).history } const historyRange = { ...loadWorkingPosition(workingKey).historyRange } const historyScroll = { ...loadWorkingPosition(workingKey).historyScroll } delete history[sessionId] delete historyRange[sessionId] delete historyScroll[sessionId] saveWorkingPosition(workingKey, { history, historyRange, historyScroll }) unavailable.current?.() } setResult(value) setLoading(false) }).catch(() => { if (controller.signal.aborted) return setFailed(true) setLoading(false) }) return () => { controller.abort() } }, [request, read]) useLayoutEffect(() => { if (loading || result?.kind !== 'original') return const panel = element.current?.closest('[data-working-scroll]') if (panel == null) return if (initialScroll.current !== undefined) { panel.scrollTop = initialScroll.current initialScroll.current = undefined } const remember = (): void => { saveWorkingPosition(workingKey, { historyScroll: { ...loadWorkingPosition(workingKey).historyScroll, [sessionId]: panel.scrollTop } }) } panel.addEventListener('scroll', remember, { passive: true }) return () => { panel.removeEventListener('scroll', remember) } }, [workingKey, sessionId, result, loading]) const select = (turn: SessionHistoryTurn): void => { if (turn.endSeq === null || loading || result?.kind !== 'original') return setIncompleteRange(false) if (selection !== undefined && turn.startSeq >= selection.startSeq && turn.endSeq <= selection.endSeq) { setSelection(undefined) return } const startSeq = Math.min(selection?.startSeq ?? turn.startSeq, turn.startSeq) const endSeq = Math.max(selection?.endSeq ?? turn.endSeq, turn.endSeq) const turns = [...loadedTurns.current.values()] .filter(item => item.startSeq >= startSeq && item.startSeq <= endSeq) .sort((a, b) => a.startSeq - b.startSeq) if (turns.some((item, index) => { const previous = turns[index - 1] return item.endSeq === null || (previous !== undefined && item.turn !== previous.turn + 1) })) { setIncompleteRange(true) return } setSelection({ startSeq, endSeq, turns }) } return (
{sessionId}

{t('history.scope')}

{loading ?

{t('history.loading')}

: null} {canceled ?

{t('history.canceled')}

: null} {failed ?

{t('history.error')}

: null} {!loading && !failed && result?.kind === 'unavailable' ?

{t('history.unavailable')}

: null} {result?.kind === 'excerpt' ? (
{t('history.excerpt')}{loading ? null :

{t('history.excerptHint')}

}
) : null} {!loading && !failed && result?.kind === 'original' && result.turns.length === 0 ?

{t('history.empty')}

: null} {!loading && (failed || canceled || result?.kind === 'unavailable' || result?.kind === 'excerpt') ? ( ) : null}
{selection === undefined ?

{t('history.selectHint')}

: (
{t('history.selected', { first: selection.turns[0]?.turn, last: selection.turns.at(-1)?.turn })}
{t('history.boundary', { start: selection.startSeq, end: selection.endSeq })}
{knowledge === undefined ? null : } {knowledge === undefined ? null : } {reuse === undefined ? null : <>{selection.turns.length !== 1 ?

{t('reuse.singleTurn')}

: null}}
)} {incompleteRange ?

{t('history.incompleteRange')}

: null} {result?.turns.map(turn => (

{t('history.turn', { turn: turn.turn })}

{turn.endSeq === null ?

{t('history.unfinished')}

: null} {turn.messages.map(message => (
{t(message.role === 'user' ? 'history.user' : 'history.assistant')}

{message.text}

))}
))}
) }