Thread onShowPopup from MessageBody through AssistantTextPart into the markdown renderer so clicking a rendered mermaid diagram in assistant messages opens the existing pan/zoom fullscreen preview dialog.
111 lines
4.2 KiB
TypeScript
111 lines
4.2 KiB
TypeScript
import React from 'react';
|
|
import type { Part } from '@opencode-ai/sdk/v2';
|
|
import { MarkdownRenderer } from '../../MarkdownRenderer';
|
|
import type { StreamPhase, ToolPopupContent } from '../types';
|
|
import type { ContentChangeReason } from '@/hooks/useChatAutoFollow';
|
|
import { useStreamingTextThrottle } from '../../hooks/useStreamingTextThrottle';
|
|
import { resolveAssistantDisplayText, shouldRenderAssistantText } from './assistantTextVisibility';
|
|
import { streamPerfCount, streamPerfObserve } from '@/stores/utils/streamDebug';
|
|
import { GeneratedJsonResultCard } from './GeneratedJsonResultCard';
|
|
import { parseGeneratedJsonResult } from './generatedJsonResult';
|
|
|
|
type PartWithText = Part & { text?: string; content?: string; value?: string; time?: { start?: number; end?: number } };
|
|
|
|
interface AssistantTextPartProps {
|
|
part: Part;
|
|
sessionId?: string;
|
|
messageId: string;
|
|
streamPhase: StreamPhase;
|
|
chatRenderMode?: 'sorted' | 'live';
|
|
onContentChange?: (reason?: ContentChangeReason, messageId?: string) => void;
|
|
onShowPopup?: (content: ToolPopupContent) => void;
|
|
}
|
|
|
|
const AssistantTextPart: React.FC<AssistantTextPartProps> = ({
|
|
part,
|
|
messageId,
|
|
streamPhase,
|
|
chatRenderMode = 'live',
|
|
onShowPopup,
|
|
}) => {
|
|
// Use part directly from props — parent provides the latest version from the store.
|
|
// No store subscription here to avoid re-render cascade from unrelated delta events.
|
|
const partWithText = part as PartWithText;
|
|
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;
|
|
}, '');
|
|
const isStreamingPhase = streamPhase === 'streaming';
|
|
const isCooldownPhase = streamPhase === 'cooldown';
|
|
const isStreaming = chatRenderMode === 'live' && (isStreamingPhase || isCooldownPhase);
|
|
|
|
streamPerfCount('ui.assistant_text_part.render');
|
|
if (isStreaming) {
|
|
streamPerfCount('ui.assistant_text_part.render.streaming');
|
|
}
|
|
|
|
const throttledTextContent = useStreamingTextThrottle({
|
|
text: textContent,
|
|
isStreaming,
|
|
identityKey: `${messageId}:${part.id ?? 'text'}`,
|
|
});
|
|
|
|
const displayTextContent = resolveAssistantDisplayText({
|
|
textContent,
|
|
throttledTextContent,
|
|
isStreaming,
|
|
});
|
|
|
|
streamPerfObserve('ui.assistant_text_part.display_len', displayTextContent.length);
|
|
|
|
const time = partWithText.time;
|
|
const isFinalized = Boolean(time && typeof time.end !== 'undefined');
|
|
|
|
const isRenderableTextPart = part.type === 'text' || part.type === 'reasoning';
|
|
if (!isRenderableTextPart) {
|
|
return null;
|
|
}
|
|
|
|
if (!shouldRenderAssistantText({
|
|
displayTextContent,
|
|
isFinalized,
|
|
})) {
|
|
return null;
|
|
}
|
|
|
|
const generatedResult = !isStreaming && isFinalized ? parseGeneratedJsonResult(displayTextContent) : null;
|
|
if (generatedResult) {
|
|
return (
|
|
<div
|
|
className={`group/assistant-text relative break-words ${chatRenderMode === 'live' ? 'my-1' : ''}`}
|
|
key={part.id || `${messageId}-text`}
|
|
>
|
|
<GeneratedJsonResultCard result={generatedResult} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={`group/assistant-text relative break-words ${chatRenderMode === 'live' ? 'my-1' : ''}`}
|
|
key={part.id || `${messageId}-text`}
|
|
>
|
|
<MarkdownRenderer
|
|
content={displayTextContent}
|
|
part={part}
|
|
messageId={messageId}
|
|
isAnimated={false}
|
|
isStreaming={isStreaming}
|
|
disableStreamAnimation={chatRenderMode === 'sorted'}
|
|
variant={part.type === 'reasoning' ? 'reasoning' : 'assistant'}
|
|
enableFileReferences={isFinalized}
|
|
onShowPopup={onShowPopup}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default React.memo(AssistantTextPart);
|