d2l-ui / components /chat /Thread.tsx
Berkkirik's picture
fix: streaming race — keep Thinking until first token, fall back to JSON when backend doesn't honour stream=true
a686c43
"use client";
import { useEffect, useRef } from "react";
import { useChatStore } from "@/lib/chatStore";
import type { SourceDoc } from "@/lib/types";
import { AssistantMessage, ErrorMessage, UserMessage } from "./Message";
import { Thinking } from "./Thinking";
export function Thread({
onOpenSource,
}: {
onOpenSource?: (doc: SourceDoc, index: number) => void;
}) {
const conv = useChatStore((s) =>
s.activeId ? s.conversations[s.activeId] : null
);
const ref = useRef<HTMLDivElement | null>(null);
// Auto-scroll to bottom on new turns / when streaming tokens arrive
const turns = conv?.turns ?? [];
const lastTurn = turns[turns.length - 1];
useEffect(() => {
const el = ref.current;
if (!el) return;
el.scrollTo({ top: el.scrollHeight, behavior: "smooth" });
}, [
turns.length,
lastTurn?.pending,
lastTurn?.response?.answer?.length,
]);
if (!conv || conv.turns.length === 0) return null;
return (
<div ref={ref} className="flex-1 overflow-y-auto min-h-0">
<div className="min-h-full flex flex-col justify-end max-w-[920px] mx-auto px-4 sm:px-6 py-8 space-y-6">
{conv.turns.map((t) => {
const hasAnswer = Boolean(t.response?.answer);
const showThinking = t.pending && !hasAnswer && !t.error;
const showAnswer = !!t.response && (hasAnswer || !t.pending);
return (
<div key={t.id} className="space-y-4">
<UserMessage text={t.question} />
{showThinking && <Thinking />}
{t.error && <ErrorMessage turn={t} />}
{showAnswer && (
<AssistantMessage turn={t} onOpenSource={onOpenSource} />
)}
</div>
);
})}
</div>
</div>
);
}