2025-12-07 19:32:53 +02:00
|
|
|
import React from 'react';
|
2026-01-03 13:39:59 +02:00
|
|
|
import type { Part } from '@opencode-ai/sdk/v2';
|
2025-12-07 19:32:53 +02:00
|
|
|
import { MarkdownRenderer } from '../../MarkdownRenderer';
|
|
|
|
|
import type { StreamPhase } from '../types';
|
|
|
|
|
import type { ContentChangeReason } from '@/hooks/useChatScrollManager';
|
2026-03-12 23:45:45 +02:00
|
|
|
import { useStreamingTextThrottle } from '../../hooks/useStreamingTextThrottle';
|
|
|
|
|
import { resolveAssistantDisplayText, shouldRenderAssistantText } from './assistantTextVisibility';
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
type PartWithText = Part & { text?: string; content?: string; value?: string; time?: { start?: number; end?: number } };
|
|
|
|
|
|
|
|
|
|
interface AssistantTextPartProps {
|
|
|
|
|
part: Part;
|
|
|
|
|
messageId: string;
|
|
|
|
|
streamPhase: StreamPhase;
|
2026-03-12 23:45:45 +02:00
|
|
|
chatRenderMode?: 'sorted' | 'live';
|
2025-12-07 19:32:53 +02:00
|
|
|
onContentChange?: (reason?: ContentChangeReason, messageId?: string) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const AssistantTextPart: React.FC<AssistantTextPartProps> = ({
|
|
|
|
|
part,
|
|
|
|
|
messageId,
|
|
|
|
|
streamPhase,
|
2026-03-12 23:45:45 +02:00
|
|
|
chatRenderMode = 'live',
|
2025-12-07 19:32:53 +02:00
|
|
|
}) => {
|
|
|
|
|
const partWithText = part as PartWithText;
|
2026-03-12 23:45:45 +02:00
|
|
|
const rawText = typeof partWithText.text === 'string' ? partWithText.text : '';
|
|
|
|
|
const contentText = typeof partWithText.content === 'string' ? partWithText.content : '';
|
|
|
|
|
const valueText = typeof partWithText.value === 'string' ? partWithText.value : '';
|
|
|
|
|
const textContent = [rawText, contentText, valueText].reduce((best, candidate) => {
|
|
|
|
|
return candidate.length > best.length ? candidate : best;
|
|
|
|
|
}, '');
|
2025-12-07 19:32:53 +02:00
|
|
|
const isStreamingPhase = streamPhase === 'streaming';
|
|
|
|
|
const isCooldownPhase = streamPhase === 'cooldown';
|
2026-03-12 23:45:45 +02:00
|
|
|
const isStreaming = chatRenderMode === 'live' && (isStreamingPhase || isCooldownPhase);
|
2025-12-07 19:32:53 +02:00
|
|
|
|
2026-03-12 23:45:45 +02:00
|
|
|
const throttledTextContent = useStreamingTextThrottle({
|
|
|
|
|
text: textContent,
|
|
|
|
|
isStreaming,
|
|
|
|
|
identityKey: `${messageId}:${part.id ?? 'text'}`,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const displayTextContent = resolveAssistantDisplayText({
|
|
|
|
|
textContent,
|
|
|
|
|
throttledTextContent,
|
|
|
|
|
isStreaming,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const lastDisplayLengthRef = React.useRef(0);
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (!isStreaming || typeof window === 'undefined') {
|
|
|
|
|
lastDisplayLengthRef.current = displayTextContent.length;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const debugEnabled = window.localStorage.getItem('openchamber_stream_debug') === '1';
|
|
|
|
|
if (!debugEnabled) {
|
|
|
|
|
lastDisplayLengthRef.current = displayTextContent.length;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (displayTextContent.length < lastDisplayLengthRef.current) {
|
|
|
|
|
console.info('[STREAM-TRACE] render_shrink', {
|
|
|
|
|
messageId,
|
|
|
|
|
partId: part.id,
|
|
|
|
|
rawTextLen: rawText.length,
|
|
|
|
|
contentLen: contentText.length,
|
|
|
|
|
valueLen: valueText.length,
|
|
|
|
|
chosenLen: textContent.length,
|
|
|
|
|
throttledLen: throttledTextContent.length,
|
|
|
|
|
displayLen: displayTextContent.length,
|
|
|
|
|
prevDisplayLen: lastDisplayLengthRef.current,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
lastDisplayLengthRef.current = displayTextContent.length;
|
|
|
|
|
}, [contentText.length, displayTextContent.length, isStreaming, messageId, part.id, rawText.length, textContent.length, throttledTextContent.length, valueText.length]);
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
const time = partWithText.time;
|
2026-03-12 23:45:45 +02:00
|
|
|
const isFinalized = Boolean(time && typeof time.end !== 'undefined');
|
2025-12-07 19:32:53 +02:00
|
|
|
|
2026-03-12 23:45:45 +02:00
|
|
|
const isRenderableTextPart = part.type === 'text' || part.type === 'reasoning';
|
|
|
|
|
if (!isRenderableTextPart) {
|
2025-12-07 19:32:53 +02:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 23:45:45 +02:00
|
|
|
if (!shouldRenderAssistantText({
|
|
|
|
|
displayTextContent,
|
|
|
|
|
isFinalized,
|
|
|
|
|
})) {
|
2025-12-07 19:32:53 +02:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2026-03-13 14:57:30 +02:00
|
|
|
<div
|
|
|
|
|
className={`group/assistant-text relative break-words ${chatRenderMode === 'live' ? 'my-1' : ''}`}
|
|
|
|
|
key={part.id || `${messageId}-text`}
|
|
|
|
|
>
|
2025-12-07 19:32:53 +02:00
|
|
|
<MarkdownRenderer
|
2026-03-12 23:45:45 +02:00
|
|
|
content={displayTextContent}
|
2025-12-07 19:32:53 +02:00
|
|
|
part={part}
|
|
|
|
|
messageId={messageId}
|
2026-03-12 23:45:45 +02:00
|
|
|
isAnimated={false}
|
|
|
|
|
isStreaming={isStreaming}
|
|
|
|
|
disableStreamAnimation={chatRenderMode === 'sorted'}
|
|
|
|
|
variant={part.type === 'reasoning' ? 'reasoning' : 'assistant'}
|
2025-12-07 19:32:53 +02:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default AssistantTextPart;
|