feat(ui): 优化流式输出工具调用渲染
- 添加 tool_start/tool_end WebSocket 事件支持 - 流式消息复用 ChatMessage 组件渲染工具调用卡片 - 修复 AI SDK v5 格式兼容问题(input/output 字段) - 修复会话恢复时 tool-result 格式错误 - 放宽 ToolState schema 中 input 字段类型为 unknown
This commit is contained in:
@@ -25,10 +25,12 @@ import type { Message, ToolCallInfo, ToolCallStatus, ToolMessagePart } from '../
|
||||
|
||||
interface ChatMessageProps {
|
||||
message: Message;
|
||||
/** 是否为流式输出中(显示打字光标) */
|
||||
isStreaming?: boolean;
|
||||
}
|
||||
|
||||
export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
||||
({ message }, ref) => {
|
||||
({ message, isStreaming = false }, ref) => {
|
||||
const isUser = message.role === 'user';
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
@@ -42,18 +44,39 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
||||
const renderContent = () => {
|
||||
// 优先使用 parts 数组(保持原始顺序)
|
||||
if (message.parts && message.parts.length > 0) {
|
||||
// 查找最后一个文本 part 的索引(用于显示打字光标)
|
||||
let lastTextPartIndex = -1;
|
||||
if (isStreaming) {
|
||||
for (let i = message.parts.length - 1; i >= 0; i--) {
|
||||
if (message.parts[i].type === 'text') {
|
||||
lastTextPartIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="message-content text-fg-secondary space-y-3">
|
||||
{message.parts.map((part) => {
|
||||
{message.parts.map((part, index) => {
|
||||
switch (part.type) {
|
||||
case 'text':
|
||||
if (!part.text) return null;
|
||||
if (!part.text && index !== lastTextPartIndex) return null;
|
||||
return isUser ? (
|
||||
<div key={part.id}>
|
||||
<FileMentionText text={part.text} />
|
||||
</div>
|
||||
) : (
|
||||
<Markdown key={part.id} content={part.text} />
|
||||
<div key={part.id}>
|
||||
<Markdown content={part.text} />
|
||||
{/* 流式输出时在最后一个文本末尾显示打字光标 */}
|
||||
{isStreaming && index === lastTextPartIndex && (
|
||||
<motion.span
|
||||
animate={{ opacity: [1, 0] }}
|
||||
transition={{ duration: 0.8, repeat: Infinity, repeatType: 'reverse' }}
|
||||
className="inline-block w-2 h-4 bg-primary-400 ml-1 rounded-sm align-middle"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
case 'tool':
|
||||
return <ToolPartItem key={part.id} part={part} />;
|
||||
|
||||
Reference in New Issue
Block a user