This commit is contained in:
fengxiangdebaozi committed 2026-08-19 21:24:58 +08:00
commit 8e77e2da1e
3 files changed
+196 -111

No files matched your search

@@ -15,7 +15,7 @@ import {
SquareTerminalIcon, SquareTerminalIcon,
WrenchIcon, WrenchIcon,
} from "lucide-react"; } from "lucide-react";
import { useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState, type AnchorHTMLAttributes } from "react";
import { import {
ChainOfThought, ChainOfThought,
@@ -58,6 +58,7 @@ import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
import { useLocalSettings } from "@/core/settings"; import { useLocalSettings } from "@/core/settings";
import { useSkills } from "@/core/skills/hooks"; import { useSkills } from "@/core/skills/hooks";
import { import {
applyInlineCitations,
buildSkillDisplayMap, buildSkillDisplayMap,
findDisplayConfigForTool, findDisplayConfigForTool,
isDisplaySamplePayload, isDisplaySamplePayload,
@@ -71,6 +72,7 @@ import { env } from "@/env";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useArtifactsOptional } from "../artifacts"; import { useArtifactsOptional } from "../artifacts";
import { CitationLink } from "../citations/citation-link";
import { FlipDisplay } from "../flip-display"; import { FlipDisplay } from "../flip-display";
import { Tooltip } from "../tooltip"; import { Tooltip } from "../tooltip";
import { import {
@@ -79,15 +81,18 @@ import {
} from "../skill-display-sources"; } from "../skill-display-sources";
import { MarkdownContent } from "./markdown-content"; import { MarkdownContent } from "./markdown-content";
import { SingleRoundRagCitationDialog } from "./message-list-item";
export function MessageGroup({ export function MessageGroup({
className, className,
messages, messages,
isLoading = false, isLoading = false,
displaySources = [],
}: { }: {
className?: string; className?: string;
messages: Message[]; messages: Message[];
isLoading?: boolean; isLoading?: boolean;
displaySources?: RuntimeDisplaySource[];
}) { }) {
const { t } = useI18n(); const { t } = useI18n();
const [showAbove, setShowAbove] = useState( const [showAbove, setShowAbove] = useState(
@@ -124,11 +129,29 @@ export function MessageGroup({
} }
}, [lastToolCallStep, steps]); }, [lastToolCallStep, steps]);
const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading); const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading);
const [selectedCitation, setSelectedCitation] =
useState<RuntimeDisplaySource | null>(null);
const handleCitationClick = useCallback(
(referenceId: string) => {
const referenceIndex = Number(referenceId);
if (!Number.isInteger(referenceIndex) || referenceIndex < 1) return;
const source =
displaySources.find((item) => item.index === referenceIndex) ??
displaySources[referenceIndex - 1];
if (source) setSelectedCitation(source);
},
[displaySources],
);
const reasoningId =
lastReasoningStep?.id ??
messages.find((message) => message.type === "ai")?.id;
return ( return (
<ChainOfThought <>
className={cn("w-full gap-2 rounded-lg border p-0.5", className)} <ChainOfThought
open={true} className={cn("w-full gap-2 rounded-lg border p-0.5", className)}
> data-reference-batch-id={reasoningId}
open={true}
>
{aboveLastToolCallSteps.length > 0 && ( {aboveLastToolCallSteps.length > 0 && (
<Button <Button
key="above" key="above"
@@ -163,10 +186,12 @@ export function MessageGroup({
<ChainOfThoughtStep <ChainOfThoughtStep
key={step.id} key={step.id}
label={ label={
<MarkdownContent <StepReasoningMarkdown
content={step.reasoning ?? ""} content={step.reasoning ?? ""}
displaySources={displaySources}
isLoading={isLoading} isLoading={isLoading}
rehypePlugins={rehypePlugins} rehypePlugins={rehypePlugins}
onCitationClick={handleCitationClick}
/> />
} }
></ChainOfThoughtStep> ></ChainOfThoughtStep>
@@ -223,10 +248,12 @@ export function MessageGroup({
<ChainOfThoughtStep <ChainOfThoughtStep
key={lastReasoningStep.id} key={lastReasoningStep.id}
label={ label={
<MarkdownContent <StepReasoningMarkdown
content={lastReasoningStep.reasoning ?? ""} content={lastReasoningStep.reasoning ?? ""}
displaySources={displaySources}
isLoading={isLoading} isLoading={isLoading}
rehypePlugins={rehypePlugins} rehypePlugins={rehypePlugins}
onCitationClick={handleCitationClick}
/> />
} }
></ChainOfThoughtStep> ></ChainOfThoughtStep>
@@ -234,7 +261,60 @@ export function MessageGroup({
)} )}
</> </>
)} )}
</ChainOfThought> </ChainOfThought>
<SingleRoundRagCitationDialog
source={selectedCitation}
onOpenChange={(open) => {
if (!open) setSelectedCitation(null);
}}
/>
</>
);
}
function StepReasoningMarkdown({
content,
displaySources,
isLoading,
rehypePlugins,
onCitationClick,
}: {
content: string;
displaySources: RuntimeDisplaySource[];
isLoading: boolean;
rehypePlugins: ReturnType<typeof useRehypeSplitWordsIntoSpans>;
onCitationClick: (referenceId: string) => void;
}) {
const markdown = useMemo(
() =>
applyInlineCitations(content, displaySources, { appendSummary: false }),
[content, displaySources],
);
const components = useMemo(() => {
if (displaySources.length === 0) return undefined;
return {
a: ({ href, ...props }: AnchorHTMLAttributes<HTMLAnchorElement>) => {
if (href?.startsWith("#reference-")) {
return (
<CitationLink
href={href}
{...props}
onReferenceClick={onCitationClick}
/>
);
}
return <a href={href} {...props} />;
},
};
}, [displaySources.length, onCitationClick]);
return (
<MarkdownContent
content={markdown}
isLoading={isLoading}
rehypePlugins={rehypePlugins}
components={components}
/>
); );
} }
@@ -695,13 +695,6 @@ function MessageContent_({
const withCitations = applyInlineCitations(rawContent ?? '', displaySources); const withCitations = applyInlineCitations(rawContent ?? '', displaySources);
return stripIncompleteCitationMarker(withCitations); return stripIncompleteCitationMarker(withCitations);
}, [rawContent, isHuman, displaySources]); }, [rawContent, isHuman, displaySources]);
const reasoningToDisplay = useMemo(() => {
if (!reasoningContent) return '';
const withCitations = applyInlineCitations(reasoningContent, displaySources, {
appendSummary: false,
});
return stripIncompleteCitationMarker(withCitations);
}, [reasoningContent, displaySources]);
const markdownRehypePlugins = useMemo( const markdownRehypePlugins = useMemo(
() => [...rehypePlugins, [rehypeKatex, { output: 'html' as const }]], () => [...rehypePlugins, [rehypeKatex, { output: 'html' as const }]],
[rehypePlugins], [rehypePlugins],
@@ -735,6 +728,23 @@ function MessageContent_({
); );
} }
// Reasoning-only AI message (no main response content yet)
if (!isHuman && reasoningContent && !rawContent && !multiRoundTrace) {
return (
<AIElementMessageContent className={className}>
<Reasoning isStreaming={isLoading}>
<ReasoningTrigger />
<ReasoningContent>{reasoningContent}</ReasoningContent>
</Reasoning>
<MessageTokenUsage
enabled={tokenUsageEnabled}
isLoading={isLoading}
message={message}
/>
</AIElementMessageContent>
);
}
if (isHuman) { if (isHuman) {
// Only the user's own question carries a delete affordance, and only once // Only the user's own question carries a delete affordance, and only once
// it exists server-side (real, non-optimistic id). Deleting it cascades to // it exists server-side (real, non-optimistic id). Deleting it cascades to
@@ -770,24 +780,6 @@ function MessageContent_({
<AIElementMessageContent className={className}> <AIElementMessageContent className={className}>
{filesList} {filesList}
<div data-reference-batch-id={message.id} className="contents"> <div data-reference-batch-id={message.id} className="contents">
{reasoningToDisplay && !multiRoundTrace && (
<Reasoning isStreaming={isLoading && !contentToDisplay}>
<ReasoningTrigger />
<ReasoningContent>
{isSingleRoundRag || displaySources.length > 0 ? (
<MarkdownContent
content={reasoningToDisplay}
isLoading={isLoading}
rehypePlugins={markdownRehypePlugins}
components={components}
className="text-muted-foreground text-sm"
/>
) : (
reasoningToDisplay
)}
</ReasoningContent>
</Reasoning>
)}
{multiRoundTrace ? ( {multiRoundTrace ? (
<MultiRoundRagContent <MultiRoundRagContent
trace={multiRoundTrace} trace={multiRoundTrace}
@@ -797,7 +789,7 @@ function MessageContent_({
rehypePlugins={markdownRehypePlugins} rehypePlugins={markdownRehypePlugins}
components={components} components={components}
/> />
) : contentToDisplay ? ( ) : (
<AssistantDisplayContent <AssistantDisplayContent
content={contentToDisplay} content={contentToDisplay}
displaySources={displaySources} displaySources={displaySources}
@@ -806,7 +798,7 @@ function MessageContent_({
rehypePlugins={markdownRehypePlugins} rehypePlugins={markdownRehypePlugins}
components={components} components={components}
/> />
) : null} )}
</div> </div>
{(isSingleRoundRag || isMultiRoundRag) && displaySources.length > 0 && ( {(isSingleRoundRag || isMultiRoundRag) && displaySources.length > 0 && (
<FrontendRagSourcesPanel <FrontendRagSourcesPanel
@@ -905,7 +897,7 @@ function FrontendRagSourcesPanel({
); );
} }
function SingleRoundRagCitationDialog({ export function SingleRoundRagCitationDialog({
source, source,
onOpenChange, onOpenChange,
}: { }: {
@@ -11,8 +11,8 @@ import {
} from "@/components/ai-elements/conversation"; } from "@/components/ai-elements/conversation";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { rememberAskTimes, useChatAskTimes } from "@/core/chat-timestamps"; import { rememberAskTimes, useChatAskTimes } from "@/core/chat-timestamps";
import { useDocumentRewritePresentation } from "@/core/document-rewrite/context"; import { useDocumentRewritePresentation } from "@/core/document-rewrite/context";
import { useI18n } from "@/core/i18n/hooks"; import { useI18n } from "@/core/i18n/hooks";
import { import {
extractContentFromMessage, extractContentFromMessage,
@@ -46,8 +46,8 @@ import { getQuestionAnchorId } from "../question-navigator";
import { StreamingIndicator } from "../streaming-indicator"; import { StreamingIndicator } from "../streaming-indicator";
import { ReferenceBatchesContext, ThreadContext } from "./context"; import { ReferenceBatchesContext, ThreadContext } from "./context";
import { MarkdownContent } from "./markdown-content"; import { MarkdownContent } from "./markdown-content";
import { DocumentRewriteTimeline } from "./document-rewrite-timeline"; import { DocumentRewriteTimeline } from "./document-rewrite-timeline";
import { MessageGroup } from "./message-group"; import { MessageGroup } from "./message-group";
import { MessageListItem } from "./message-list-item"; import { MessageListItem } from "./message-list-item";
import { import {
@@ -112,10 +112,10 @@ export function MessageList({
onSetupWritingApprove, onSetupWritingApprove,
onSetupWritingReject, onSetupWritingReject,
onSetupWritingStart, onSetupWritingStart,
researchApprovalSlot, researchApprovalSlot,
onOpenArtifact, onOpenArtifact,
onDownloadArtifact, onDownloadArtifact,
}: { }: {
className?: string; className?: string;
contentClassName?: string; contentClassName?: string;
fitMessageRowsToContent?: boolean; fitMessageRowsToContent?: boolean;
@@ -138,23 +138,23 @@ export function MessageList({
/** 审批卡「直接开始写作」回调:应用配置(含已确认大纲)并立即启动写作流程(仅写作页提供,模板编辑等场景缺省隐藏该按钮)。 */ /** 审批卡「直接开始写作」回调:应用配置(含已确认大纲)并立即启动写作流程(仅写作页提供,模板编辑等场景缺省隐藏该按钮)。 */
onSetupWritingStart?: (payload: SetupWritingApprovalPayload) => void | Promise<void>; onSetupWritingStart?: (payload: SetupWritingApprovalPayload) => void | Promise<void>;
/** /**
* 深度研究的交互卡插槽(报告配置、计划确认、失败重试):workbench 向 * 深度研究的交互卡插槽(报告配置、计划确认、失败重试):workbench 向
* displayThread 注入带 `additional_kwargs.setup_research_approval` 的虚拟 ai * displayThread 注入带 `additional_kwargs.setup_research_approval` 的虚拟 ai
* 消息。撰写步骤和完成摘要不走此插槽,而是分别复用原生 tool-call 步骤条和 * 消息。撰写步骤和完成摘要不走此插槽,而是分别复用原生 tool-call 步骤条和
* assistant 消息行;不传该 slot 时普通对话/AI 写作零行为变化。 * assistant 消息行;不传该 slot 时普通对话/AI 写作零行为变化。
*/ */
researchApprovalSlot?: (message: Message) => React.ReactNode; researchApprovalSlot?: (message: Message) => React.ReactNode;
onOpenArtifact?: (filepath: string) => void; onOpenArtifact?: (filepath: string) => void;
/** Host override for downloading virtual/session-owned files in a present-files card. */ /** Host override for downloading virtual/session-owned files in a present-files card. */
onDownloadArtifact?: (filepath: string) => void | Promise<void>; onDownloadArtifact?: (filepath: string) => void | Promise<void>;
}) { }) {
const baseRehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading); const baseRehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading);
const rehypePlugins = useMemo( const rehypePlugins = useMemo(
() => (extraRehypePlugins?.length ? [...baseRehypePlugins, ...extraRehypePlugins] : baseRehypePlugins), () => (extraRehypePlugins?.length ? [...baseRehypePlugins, ...extraRehypePlugins] : baseRehypePlugins),
[baseRehypePlugins, extraRehypePlugins], [baseRehypePlugins, extraRehypePlugins],
); );
const updateSubtask = useUpdateSubtask(); const updateSubtask = useUpdateSubtask();
const documentRewritePresentation = useDocumentRewritePresentation(); const documentRewritePresentation = useDocumentRewritePresentation();
const { t } = useI18n(); const { t } = useI18n();
const threadContext = useContext(ThreadContext); const threadContext = useContext(ThreadContext);
// Read-only contexts (public share, mock previews) must not offer deletion. // Read-only contexts (public share, mock previews) must not offer deletion.
@@ -194,7 +194,7 @@ export function MessageList({
: ({ ...message, content: override } as Message); : ({ ...message, content: override } as Message);
}); });
}, [messageContentOverrides, thread.messages]); }, [messageContentOverrides, thread.messages]);
const messages = useMemo( const messages = useMemo(
() => { () => {
const externalHidden = externallyHiddenMessageIds ?? new Set<string>(); const externalHidden = externallyHiddenMessageIds ?? new Set<string>();
return hiddenMessageIds.size === 0 && externalHidden.size === 0 return hiddenMessageIds.size === 0 && externalHidden.size === 0
@@ -205,9 +205,9 @@ export function MessageList({
(!hiddenMessageIds.has(m.id) && !externalHidden.has(m.id)), (!hiddenMessageIds.has(m.id) && !externalHidden.has(m.id)),
); );
}, },
[allMessages, externallyHiddenMessageIds, hiddenMessageIds], [allMessages, externallyHiddenMessageIds, hiddenMessageIds],
); );
const documentRewriteItems = documentRewritePresentation?.forThread(threadId) ?? []; const documentRewriteItems = documentRewritePresentation?.forThread(threadId) ?? [];
const askTimes = useChatAskTimes(); const askTimes = useChatAskTimes();
@@ -255,15 +255,15 @@ export function MessageList({
const [settings] = useLocalSettings(); const [settings] = useLocalSettings();
const ragModeEnabled = const ragModeEnabled =
settings.display.ragModeEnabled || isThreadRagModeLocked(threadId); settings.display.ragModeEnabled || isThreadRagModeLocked(threadId);
const hasSingleRoundRag = useMemo( const hasSingleRoundRag = useMemo(
() => () =>
messages.some( messages.some(
(message) => (message) =>
isSingleRoundRagMessage(message) || isMultiRoundRagMessage(message), isSingleRoundRagMessage(message) || isMultiRoundRagMessage(message),
), ),
[messages], [messages],
); );
const referencesEnabled = ragModeEnabled || hasSingleRoundRag; const referencesEnabled = ragModeEnabled || hasSingleRoundRag;
const { skills } = useSkills(); const { skills } = useSkills();
const skillDisplayMap = useMemo( const skillDisplayMap = useMemo(
() => (referencesEnabled ? buildSkillDisplayMap(skills) : new Map()), () => (referencesEnabled ? buildSkillDisplayMap(skills) : new Map()),
@@ -322,6 +322,32 @@ export function MessageList({
return map; return map;
}, [thread.messages]); }, [thread.messages]);
const resolveDisplaySources = useCallback(
(msg: Message | undefined): RuntimeDisplaySource[] => {
if (!referencesEnabled || !msg || msg.type !== "ai") return [];
const batchSources =
referenceBatches.find((batch) => batch.assistant_message_id === msg.id)
?.sources ?? [];
const frontendRagSources = msg.id
? frontendRagSourcesByAiId.get(msg.id) ?? []
: [];
if (batchSources.length > 0) return batchSources;
if (frontendRagSources.length > 0) return frontendRagSources;
return collectRuntimeDisplaySources(messages, skillDisplayMap, {
afterMessageId: findPreviousHumanMessageId(messages, msg.id),
beforeMessageId: msg.id,
includeBeforeMessage: true,
});
},
[
frontendRagSourcesByAiId,
messages,
referenceBatches,
referencesEnabled,
skillDisplayMap,
],
);
// Track which clarification messages are still awaiting an answer. // Track which clarification messages are still awaiting an answer.
// Pending = clarification tool messages that appear after the last human message // Pending = clarification tool messages that appear after the last human message
// (no user reply has been sent for them yet). // (no user reply has been sent for them yet).
@@ -436,47 +462,29 @@ export function MessageList({
className={cn("flex size-full flex-col justify-center", className)} className={cn("flex size-full flex-col justify-center", className)}
> >
<ScrollToBottomOnUserMessage threadId={threadId} messages={messages} /> <ScrollToBottomOnUserMessage threadId={threadId} messages={messages} />
<ConversationContent <ConversationContent
className={cn( className={cn(
"mx-auto w-full max-w-(--container-width-lg)", "mx-auto w-full max-w-(--container-width-lg)",
hasSingleRoundRag ? "gap-4 pt-4" : "gap-8 pt-12", hasSingleRoundRag ? "gap-4 pt-4" : "gap-8 pt-12",
contentClassName, contentClassName,
)} )}
> >
<MessageMetricsProvider messages={messages} isLoading={thread.isLoading}> <MessageMetricsProvider messages={messages} isLoading={thread.isLoading}>
{groupMessages(messages, (group) => { {groupMessages(messages, (group) => {
if (group.type === "human" || group.type === "assistant") { if (group.type === "human" || group.type === "assistant") {
return group.messages.map((msg) => { return group.messages.map((msg) => {
// 深度研究的报告摘要由独立 SSE 作业驱动;把它接入既有 assistant 流式状态, // 深度研究的报告摘要由独立 SSE 作业驱动;把它接入既有 assistant 流式状态,
// 便于 Streamdown 在每次 summary_delta 到达时立即更新该普通消息行。 // 便于 Streamdown 在每次 summary_delta 到达时立即更新该普通消息行。
const messageIsLoading = const messageIsLoading =
thread.isLoading || thread.isLoading ||
msg.additional_kwargs?.deep_research_summary_streaming === true || msg.additional_kwargs?.deep_research_summary_streaming === true ||
msg.additional_kwargs?.deep_research_follow_up_streaming === true; msg.additional_kwargs?.deep_research_follow_up_streaming === true;
const isAssistantMessage = const isAssistantMessage =
referencesEnabled && referencesEnabled &&
group.type === "assistant" && group.type === "assistant" &&
msg.type === "ai"; msg.type === "ai";
const batchSources = isAssistantMessage
? referenceBatches.find(
(batch) => batch.assistant_message_id === msg.id,
)?.sources ?? []
: [];
// 单轮/多轮 RAG 的参考资料来自隐藏的 tool 消息,优先于通用扫描。
const frontendRagSources =
isAssistantMessage && msg.id
? frontendRagSourcesByAiId.get(msg.id) ?? []
: [];
const displaySources = isAssistantMessage const displaySources = isAssistantMessage
? batchSources.length > 0 ? resolveDisplaySources(msg)
? batchSources
: frontendRagSources.length > 0
? frontendRagSources
: collectRuntimeDisplaySources(messages, skillDisplayMap, {
afterMessageId: findPreviousHumanMessageId(messages, msg.id),
beforeMessageId: msg.id,
includeBeforeMessage: true,
})
: []; : [];
return ( return (
<MessageListItem <MessageListItem
@@ -486,9 +494,9 @@ export function MessageList({
msg.type === "human" msg.type === "human"
? getQuestionAnchorId(msg, messages.indexOf(msg)) ? getQuestionAnchorId(msg, messages.indexOf(msg))
: undefined : undefined
} }
message={msg} message={msg}
isLoading={messageIsLoading} isLoading={messageIsLoading}
threadId={threadId} threadId={threadId}
tokenUsageEnabled={tokenUsageEnabled} tokenUsageEnabled={tokenUsageEnabled}
displaySources={displaySources} displaySources={displaySources}
@@ -584,8 +592,8 @@ export function MessageList({
</div> </div>
) : null; ) : null;
} else if (group.type === "assistant:research-approval") { } else if (group.type === "assistant:research-approval") {
// 深度研究交互卡(配置/计划确认/失败重试):宿主不传渲染函数时该 // 深度研究交互卡(配置/计划确认/失败重试):宿主不传渲染函数时该
// 分组不渲染。撰写步骤和完成摘要会经过普通的 processing/assistant 分组。 // 分组不渲染。撰写步骤和完成摘要会经过普通的 processing/assistant 分组。
const message = group.messages[0]; const message = group.messages[0];
return researchApprovalSlot && message ? ( return researchApprovalSlot && message ? (
<div key={group.id} className="w-full pb-2"> <div key={group.id} className="w-full pb-2">
@@ -611,11 +619,11 @@ export function MessageList({
/> />
)} )}
<ArtifactFileList <ArtifactFileList
files={files} files={files}
threadId={threadId} threadId={threadId}
onOpenArtifact={onOpenArtifact} onOpenArtifact={onOpenArtifact}
onDownloadArtifact={onDownloadArtifact} onDownloadArtifact={onDownloadArtifact}
/> />
<MessageTokenUsageList <MessageTokenUsageList
enabled={tokenUsageEnabled} enabled={tokenUsageEnabled}
isLoading={thread.isLoading} isLoading={thread.isLoading}
@@ -683,6 +691,7 @@ export function MessageList({
key={"thinking-group-" + message.id} key={"thinking-group-" + message.id}
messages={[message]} messages={[message]}
isLoading={thread.isLoading} isLoading={thread.isLoading}
displaySources={resolveDisplaySources(message)}
/>, />,
); );
} }
@@ -726,11 +735,15 @@ export function MessageList({
message.type === "ai" && message.type === "ai" &&
(hasToolCalls(message) ? true : !hasContent(message)), (hasToolCalls(message) ? true : !hasContent(message)),
); );
const processingAi = [...group.messages]
.reverse()
.find((message) => message.type === "ai");
return ( return (
<div key={"group-" + group.id} className="w-full"> <div key={"group-" + group.id} className="w-full">
<MessageGroup <MessageGroup
messages={group.messages} messages={group.messages}
isLoading={thread.isLoading} isLoading={thread.isLoading}
displaySources={resolveDisplaySources(processingAi)}
/> />
<MessageTokenUsageList <MessageTokenUsageList
enabled={tokenUsageEnabled} enabled={tokenUsageEnabled}
@@ -739,11 +752,11 @@ export function MessageList({
/> />
</div> </div>
); );
})} })}
{documentRewriteItems.map((item) => ( {documentRewriteItems.map((item) => (
<DocumentRewriteTimeline key={item.id} item={item} /> <DocumentRewriteTimeline key={item.id} item={item} />
))} ))}
{thread.isLoading && <StreamingIndicator className="my-4" />} {thread.isLoading && <StreamingIndicator className="my-4" />}
<div style={{ height: `${paddingBottom}px` }} /> <div style={{ height: `${paddingBottom}px` }} />
</MessageMetricsProvider> </MessageMetricsProvider>
</ConversationContent> </ConversationContent>