import { useEffect, useMemo, useRef, useState, type ReactElement } from 'react' import type { SessionListState } from '@deepseek-ai/dsh-api-session-controller/client' import type { WorkspaceSnapshot } from '@deepseek-ai/dsh-api-workspace-controller/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' import type { ResearchTopic, ResearchTopicSnapshot } from '../research-topic.ts' import type { GraphViewInjected } from './GraphView.tsx' import type { SessionGraphNode } from './graph-model.ts' import type { KnowledgeCard } from '../knowledge.ts' import type { LayoutState } from './layout-store.ts' import type { SessionGraphKey } from './locales.ts' import { deriveTopicGraph } from './graph-model.ts' import { layoutSessionGraph } from './layout.ts' import { GraphCanvas } from './GraphCanvas.tsx' import { SessionHistory } from './SessionHistory.tsx' import styles from './GraphView.module.css' import { useKnowledge } from './Knowledge.tsx' import { withKnowledgeCards } from './knowledge-graph.ts' import { loadWorkingPosition, saveWorkingPosition, workingPositionKey } from './working-position.ts' type Translate = (key: SessionGraphKey, params?: Record) => string export interface TopicGraphContext { readonly workingKey: string readonly sessions: SessionListState readonly workspaces: WorkspaceSnapshot readonly pendingInteractions: ReadonlyMap readonly viewedId: SessionId readonly actions: GraphViewInjected } /** Each mounted topic owns its cancellable metadata read and source inspector. */ export function TopicGraph({ topic, context, arrangement, onArrange, remove, busy, t }: { readonly topic: ResearchTopic readonly context: TopicGraphContext readonly arrangement: LayoutState readonly onArrange: (state: LayoutState) => void readonly remove: (sessionId: string) => void readonly busy: boolean readonly t: Translate }): ReactElement { const knowledge = useKnowledge()! const workingKey = workingPositionKey(context.actions.hostId, context.workingKey, topic.topicId) const [cards, setCards] = useState([]) const [snapshot, setSnapshot] = useState() const [phase, setPhase] = useState<'loading' | 'ready' | 'error' | 'canceled'>('loading') const [revision, setRevision] = useState(0) const active = useRef() const membership = JSON.stringify(topic.references.map(reference => reference.sessionId)) const read = context.actions.topics.read const searchCards = knowledge.api.search useEffect(() => { const controller = new AbortController() active.current = controller setPhase('loading') setSnapshot(undefined) void Promise.all([read({ topicId: topic.topicId }, controller.signal), searchCards({ query: '', topicId: topic.topicId }, controller.signal)]).then(([value, found]) => { if (controller.signal.aborted) return setSnapshot(value) setCards(found) setPhase('ready') }, () => { if (!controller.signal.aborted) setPhase('error') }) return () => { controller.abort() } }, [topic.topicId, membership, read, searchCards, revision, knowledge.refresh]) const graph = useMemo(() => { if (snapshot === undefined) return undefined const archived = new Set(context.workspaces.archivedSessionIds) const workspaceTitles = new Map(context.workspaces.items.map(workspace => [String(workspace.workspaceId), workspace.title])) const sources = snapshot.sources.map(source => ({ ...source, archived: context.workspaces.phase === 'ready' ? archived.has(source.sessionId) : source.archived, ...(source.workspace === undefined ? {} : { workspace: { ...source.workspace, title: workspaceTitles.get(source.workspace.id) || source.workspace.title, } }), })) return withKnowledgeCards(deriveTopicGraph({ sources, topic }, context.sessions, context.viewedId, context.pendingInteractions), cards, context.sessions, archived) }, [snapshot, topic, context.sessions, context.workspaces, context.viewedId, context.pendingInteractions, cards]) const laid = useMemo(() => graph === undefined ? undefined : layoutSessionGraph(graph), [graph]) const open = (id: SessionId): void => { const node = graph?.nodes.get(id) const source = node?.kind === 'knowledge' ? undefined : node?.topicSource if (source?.status === 'listed' && !source.archived) context.actions.openSession(id) } return
{cards.length > 0 ? {t('knowledge.sourceRelation')} : null}
{phase === 'loading' ?
{t('topic.loading')}
: null} {phase === 'error' || phase === 'canceled' ?
{t(phase === 'error' ? 'topic.readError' : 'topic.canceled')}
: null} {phase !== 'ready' || graph === undefined || laid === undefined ? null : <> {graph.nodes.size === 0 ?

{t('topic.noReferences')}

: null} node === undefined ? null : node.kind === 'knowledge' ? : reference.sessionId === node.id) ? () => { remove(node.id) } : undefined} busy={busy} onClose={onClose} onUnavailable={onUnavailable} t={t} /> }} />}
} function TopicSourcePanel({ node, read, open, remove, busy, onClose, onUnavailable, workingKey, t }: { readonly onUnavailable: () => void readonly workingKey: string readonly node: SessionGraphNode readonly read: GraphViewInjected['readSessionHistory'] readonly open: GraphViewInjected['openSession'] readonly remove: (() => void) | undefined readonly busy: boolean readonly onClose: () => void readonly t: Translate }): ReactElement { const [reading, setReading] = useState(() => loadWorkingPosition(workingKey).selected === node.id && loadWorkingPosition(workingKey).tab === 'history') useEffect(() => { saveWorkingPosition(workingKey, { tab: reading ? 'history' : 'digest' }) }, [workingKey, reading]) const source = node.topicSource return }