()
| 85 | } |
| 86 | |
| 87 | export function ChatApp() { |
| 88 | const [session, setSession] = useState<ChatSession>(() => createSession()); |
| 89 | const [sessionList, setSessionList] = useState< |
| 90 | Awaited<ReturnType<typeof listSessions>> |
| 91 | >([]); |
| 92 | const [hydrated, setHydrated] = useState(false); |
| 93 | const [draft, setDraft] = useState(""); |
| 94 | const [busy, setBusy] = useState(false); |
| 95 | const [isReplay, setIsReplay] = useState(false); |
| 96 | const [replaySlice, setReplaySlice] = useState<ChatMessage[] | null>(null); |
| 97 | const [replayTargetId, setReplayTargetId] = useState<string | null>(null); |
| 98 | const [replaySettings, setReplaySettings] = useState<ReplaySettings>({ |
| 99 | delayMs: 16, |
| 100 | chunkSize: 0, |
| 101 | }); |
| 102 | const [error, setError] = useState<string | null>(null); |
| 103 | |
| 104 | const msgsRef = useRef<HTMLDivElement>(null); |
| 105 | const btsRef = useRef<HTMLPreElement>(null); |
| 106 | const rawTextRef = useRef(""); |
| 107 | const rawReasoningRef = useRef(""); |
| 108 | const rawContentRef = useRef(""); |
| 109 | const abortRef = useRef<AbortController | null>(null); |
| 110 | const msgsScrollFollowRef = useRef(createStreamScrollFollow()); |
| 111 | const btsScrollFollowRef = useRef(createStreamScrollFollow()); |
| 112 | const saveTimerRef = useRef<number | null>(null); |
| 113 | |
| 114 | const [rawStats, setRawStats] = useState({ chars: 0, chunks: 0 }); |
| 115 | const [rawPreviewId, setRawPreviewId] = useState<string | null>(null); |
| 116 | |
| 117 | const streaming = busy || isReplay; |
| 118 | |
| 119 | const refreshSessions = useCallback(async () => { |
| 120 | setSessionList(await listSessions()); |
| 121 | }, []); |
| 122 | |
| 123 | const paintRawStream = useCallback((reasoning: string, content: string) => { |
| 124 | rawReasoningRef.current = reasoning; |
| 125 | rawContentRef.current = content; |
| 126 | rawTextRef.current = formatRawStream(reasoning, content); |
| 127 | if (btsRef.current) { |
| 128 | btsRef.current.textContent = rawTextRef.current; |
| 129 | } |
| 130 | setRawStats((prev) => ({ |
| 131 | ...prev, |
| 132 | chars: rawTextRef.current.length, |
| 133 | })); |
| 134 | }, []); |
| 135 | |
| 136 | const appendStreamPart = useCallback((part: StreamPart) => { |
| 137 | if (!part.delta) { |
| 138 | return; |
| 139 | } |
| 140 | |
| 141 | if (part.kind === "reasoning") { |
| 142 | rawReasoningRef.current += part.delta; |
| 143 | } else { |
| 144 | rawContentRef.current += part.delta; |
nothing calls this directly
no test coverage detected