diff --git a/src/components/insights/coach-panel/message-thread.tsx b/src/components/insights/coach-panel/message-thread.tsx index add50de79..54c1880c0 100644 --- a/src/components/insights/coach-panel/message-thread.tsx +++ b/src/components/insights/coach-panel/message-thread.tsx @@ -26,7 +26,6 @@ import { ABOUT_ME_FIELD_MAX_CHARS } from "@/lib/validations/about-me"; import { SourceChips } from "./source-chips"; import { ReminderSuggestionCard } from "./reminder-suggestion-card"; import { StreamedProse } from "./streamed-prose"; -import { ThinkingDisclosure } from "./thinking-disclosure"; import { MessageTokenFooter } from "./message-token-footer"; import type { CoachConversationDetailDTO, @@ -470,11 +469,8 @@ export function MessageThread({ providerType={streaming.inProgress ? "streaming" : null} inProgress={streaming.inProgress} errorCode={streaming.errorCode} - // v1.18.9 — live word-fade + the elapsed-time thinking - // disclosure + the just-landed token footer. + // v1.18.9 — live word-fade + the just-landed token footer. streaming - startedAt={streaming.startedAt} - reasoning={streaming.reasoning} usage={streaming.usage} /> @@ -513,17 +509,6 @@ interface ChatBubbleProps { * settled plain text. */ streaming?: boolean; - /** - * v1.18.9 — send-anchored timestamp (epoch ms) for the streaming turn, - * feeding the thinking disclosure's elapsed timer. Null on persisted - * bubbles. - */ - startedAt?: number | null; - /** - * v1.18.9 — reasoning-summary text from the additive `reasoning` SSE - * frame, shown inside the thinking disclosure when present. - */ - reasoning?: string; /** * v1.18.9 — per-turn token usage for the just-finished streaming bubble * (from `done.usage`). Persisted bubbles read `tokensUsed` / `model` @@ -548,8 +533,6 @@ function ChatBubble({ errorCode, messageId, streaming, - startedAt, - reasoning, usage, tokensUsed, model, @@ -678,28 +661,16 @@ function ChatBubble({ )}
- {/* v1.18.9 — the single thinking indicator (live elapsed-time line, - then an auto-collapsed past-tense disclosure). Renders only for - the live streaming turn — persisted bubbles pass no `startedAt`, - so the disclosure returns null and history stays settled. This - replaces the old dots-plus-word combo: there is - never a three-dots indicator AND a "Thinking" word at once. */} - {streaming && startedAt != null && ( - - )} - {/* The prose bubble. Skipped while the live turn is still thinking - with no prose (the disclosure carries the live state above), so - the user never sees an empty bubble under the indicator. The - `` fallback below only fires when there is no live - disclosure (e.g. a defensive null `startedAt`). */} - {(content || - safeError || - (inProgress && !(streaming && startedAt != null))) && ( + {/* v1.19.1 (C3) — the live turn shows the classic typing animation + (three pulsing dots) while it is still thinking with no prose + yet, restoring the writing/typing indicator the maintainer + prefers over the "Denke nach / Nachgedacht" reasoning disclosure. + The dots render inside the prose bubble below; the disclosure is + retired. Persisted bubbles never stream, so history stays settled. */} + {/* The prose bubble. While the live turn is still thinking with no + prose the bubble carries the typing animation; once tokens land + it swaps to the streamed prose without a layout jump. */} + {(content || safeError || inProgress) && (
`), which showed - * three bouncing dots AND the word "Thinking…" at the same time — the - * exact pairing the maintainer disliked. This is a single, calm control: - * - * - While the turn is thinking (streaming, no prose yet): a live - * "Thinking… N s" line with a small spinner. `role="status"` so a - * screen reader announces "thinking" once, not on every tick. - * - Once the first token lands OR the turn settles: it relabels to the - * past tense ("Thought for N s") and auto-collapses into a quiet - * `
` disclosure. Expanding reveals the reasoning summary when - * a reasoning-capable provider supplied one (the additive `reasoning` - * SSE frame), otherwise a one-line factual fallback — never fabricated - * chain-of-thought. - * - * The elapsed time is computed from the send-anchored `startedAt` - * timestamp: it ticks live while thinking and freezes the instant the - * first token (or the settled state) arrives. - */ -export interface ThinkingDisclosureProps { - /** - * Epoch-ms timestamp the turn started (the moment the send fired). Null - * disables the elapsed timer (the component renders nothing). - */ - startedAt: number | null; - /** True until the turn settles (the `done`/`error` frame closed it). */ - inProgress: boolean; - /** - * Whether any assistant prose has streamed in yet. Drives the live → - * past-tense relabel + auto-collapse: the moment the answer starts, the - * "Thinking…" line freezes and folds away. - */ - hasContent: boolean; - /** - * Optional reasoning-summary text from the additive `reasoning` SSE - * frame, rendered inside the expanded disclosure when present. - */ - reasoning?: string; -} - -export function ThinkingDisclosure({ - startedAt, - inProgress, - hasContent, - reasoning, -}: ThinkingDisclosureProps) { - const { t } = useTranslations(); - const panelId = useId(); - const [open, setOpen] = useState(false); - - // The turn is "thinking" only while it is in progress with no prose yet. - const thinking = inProgress && hasContent === false && startedAt !== null; - - // `elapsed` ticks once a second while thinking; the interval stops the - // moment `thinking` flips false, so the last value it set is the frozen - // "thought for N s" figure — no render-time ref, no sync setState in the - // effect body (the first tick lands on the interval, not inline). - const [elapsed, setElapsed] = useState(0); - useEffect(() => { - if (!thinking || startedAt === null) return; - const tick = () => - setElapsed(Math.max(0, Math.round((Date.now() - startedAt) / 1000))); - tick(); - const handle = window.setInterval(tick, 1000); - return () => window.clearInterval(handle); - }, [thinking, startedAt]); - - if (startedAt === null) return null; - - // ── Live "thinking" line (single indicator, no dots-plus-word) ── - if (thinking) { - return ( -
-
- ); - } - - // ── Settled: past-tense collapsible disclosure (auto-collapsed) ── - const reasoningText = reasoning?.trim() ?? ""; - return ( -
setOpen((e.target as HTMLDetailsElement).open)} - className="group inline-block" - > - - -

- {reasoningText || t("insights.coach.thinkingDetail")} -

-
- ); -}