"use client"; import type { UseChatHelpers } from "@ai-sdk/react"; import equal from "fast-deep-equal"; import { memo, useState } from "react"; import type { Vote } from "@/lib/db/schema"; import type { ChatMessage } from "@/lib/types"; import { cn, sanitizeText } from "@/lib/utils"; import { useDataStream } from "./data-stream-provider"; import { DocumentToolResult } from "./document"; import { DocumentPreview } from "./document-preview"; import { MessageContent } from "./elements/message"; import { Response } from "./elements/response"; import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput, } from "./elements/tool"; import { SparklesIcon } from "./icons"; import { MessageActions } from "./message-actions"; import { MessageEditor } from "./message-editor"; import { MessageReasoning } from "./message-reasoning"; import { PreviewAttachment } from "./preview-attachment"; import { Weather } from "./weather"; import { ImageProcessingLoading } from "./image-processing-loading"; /** * 解析文本中的标注内容,返回文本片段数组 * 格式:「图1: 物体」 -> 标注文本(需要高亮) */ function parseAnnotations(text: string): Array<{ type: 'normal' | 'annotation'; content: string }> { if (!text) { return [{ type: 'normal', content: text }]; } const parts: Array<{ type: 'normal' | 'annotation'; content: string }> = []; let lastIndex = 0; const regex = /「([^」]+)」/g; let match; while ((match = regex.exec(text)) !== null) { // 添加标注之前的普通文本 if (match.index > lastIndex) { parts.push({ type: 'normal', content: text.slice(lastIndex, match.index) }); } // 添加标注文本(移除「」标记) parts.push({ type: 'annotation', content: match[1] }); lastIndex = match.index + match[0].length; } // 添加最后的普通文本 if (lastIndex < text.length) { parts.push({ type: 'normal', content: text.slice(lastIndex) }); } // 如果没有找到标注,返回整个文本 if (parts.length === 0) { return [{ type: 'normal', content: text }]; } return parts; } /** * 过滤掉视频生成过程中的 JSON 和进度信息,只保留真正的视频内容 * 注意:只对包含特定关键词的内容进行过滤,避免误伤正常消息 */ function filterVideoContent(text: string): string { if (!text) { return text; } // 只对包含视频生成相关关键词的内容进行过滤 const hasVideoKeywords = text.includes("进度") || text.includes("任务正在") || text.includes("视频生成") || text.includes("[视频]") || /\{[^}]*"prompt"[^}]*\}/.test(text) || text.includes(".mp4"); // 如果不包含视频相关关键词,直接返回原文本(避免误伤正常消息) if (!hasVideoKeywords) { return text; } let filtered = text; // 1. 提取视频 URL let extractedVideoUrl: string | null = null; // 匹配 Markdown 链接 [text](url.mp4) 或 [text](url) const mdLinkMatch = filtered.match(/\[.*?\]\((https?:\/\/[^)]+\.mp4)\)/); if (mdLinkMatch) { extractedVideoUrl = mdLinkMatch[1]; } else { // 匹配纯文本 URL const urlMatch = filtered.match(/(https?:\/\/[^\s]+\.mp4)/); if (urlMatch) { extractedVideoUrl = urlMatch[1]; } } // 2. 移除 JSON 格式的内容 (匹配多行 json 代码块或单行 json) filtered = filtered.replace(/```json[\s\S]*?```/g, ""); filtered = filtered.replace(/\{[^}]*"prompt"[^}]*\}/g, ""); filtered = filtered.replace(/\{[^}]*"mode"[^}]*\}/g, ""); // 3. 移除进度信息和提示行 filtered = filtered.replace(/^[🏃⏱️🕐🔄⌛].*$/gm, ""); // 表情符号开头的行 filtered = filtered.replace(/^>.*?(进度|任务|队列).*?$/gm, ""); // 引用块开头的进度 filtered = filtered.replace(/^.*?进度[::]\s*\d+(\.\d+)?%.*$/gm, ""); // 纯文本进度 filtered = filtered.replace(/^.*?任务正在.*?$/gm, ""); // 任务提示 // 4. 移除 "视频生成成功" 提示行(避免重复) filtered = filtered.replace(/^>.*?视频生成成功.*?$/gm, ""); filtered = filtered.replace(/^[✅❌].*?视频.*?(成功|失败).*?$/gm, ""); // 清理多余空行 filtered = filtered.replace(/\n{3,}/g, "\n\n").trim(); // 5. 如果提取到了视频 URL 且当前没有 video 标签,则追加视频组件 if (extractedVideoUrl && !filtered.includes("\n\n[下载视频](${extractedVideoUrl})`; // 如果过滤后为空,直接返回视频组件;否则追加在后面 if (!filtered) { filtered = videoComponent; } else { filtered += videoComponent; } } return filtered; } const PurePreviewMessage = ({ chatId, message, vote, isLoading, setMessages, regenerate, sendMessage, isReadonly, requiresScrollPadding: _requiresScrollPadding, onImageEdit, onDrawingComplete, onImageEnhance, onImageRmbg, onImageExtend, onImageAngleEdit, onImageWatermarkRemove, isProcessingEnhance, isProcessingRmbg, isProcessingExtend, isProcessingAngleEdit, isProcessingWatermarkRemove, }: { chatId: string; message: ChatMessage; vote: Vote | undefined; isLoading: boolean; setMessages: UseChatHelpers["setMessages"]; regenerate: UseChatHelpers["regenerate"]; sendMessage: UseChatHelpers["sendMessage"]; isReadonly: boolean; requiresScrollPadding: boolean; onImageEdit?: (imageUrl: string) => void; onDrawingComplete?: (annotatedImageUrl: string) => void; onImageEnhance?: (imageUrl: string) => void; onImageRmbg?: (imageUrl: string) => void; onImageExtend?: (imageUrl: string) => void; onImageAngleEdit?: (imageUrl: string) => void; onImageWatermarkRemove?: (imageUrl: string) => void; isProcessingEnhance?: boolean; isProcessingRmbg?: boolean; isProcessingExtend?: boolean; isProcessingAngleEdit?: boolean; isProcessingWatermarkRemove?: boolean; }) => { const [mode, setMode] = useState<"view" | "edit">("view"); const attachmentsFromMessage = message.parts.filter( (part) => part.type === "file" ); useDataStream(); return (
{message.role === "assistant" && message.parts.some((part) => part.type === "text" && part.text && part.text.trim().length > 0) && (
)}
p.type === "text" && p.text?.trim() ), "w-full": (message.role === "assistant" && message.parts?.some( (p) => p.type === "text" && p.text?.trim() )) || mode === "edit", "max-w-[calc(100%-2.5rem)] sm:max-w-[min(fit-content,80%)]": message.role === "user" && mode !== "edit", })} > {attachmentsFromMessage.length > 0 && (
{attachmentsFromMessage.map((attachment) => ( ))}
)} {message.parts?.map((part, index) => { const { type } = part; const key = `message-${message.id}-part-${index}`; if (type === "reasoning" && part.text?.trim().length > 0) { return ( ); } if (type === "text") { if (mode === "view") { // 检查是否是图片处理的加载消息 if (part.text === "正在变清晰..." || part.text === "正在抠图..." || part.text === "正在图像拓展..." || part.text === "正在去水印...") { let type: "enhance" | "rmbg" | "extend" | "watermark"; if (part.text === "正在变清晰...") { type = "enhance"; } else if (part.text === "正在抠图...") { type = "rmbg"; } else if (part.text === "正在去水印...") { type = "watermark"; } else { type = "extend"; } return ( ); } // 应用视频内容过滤 const filteredText = filterVideoContent(part.text); const textParts = parseAnnotations(filteredText); return (
{textParts.map((part, partIndex) => { if (part.type === 'annotation') { return ( {part.content} ); } // 使用处理状态作为 key 的一部分,确保状态变化时 Streamdown 重新渲染 const processingKey = `${isProcessingEnhance}-${isProcessingRmbg}-${isProcessingExtend}-${isProcessingAngleEdit}`; return ( {sanitizeText(part.content)} ); })}
); } if (mode === "edit") { return (
); } } if (type === "tool-getWeather") { const { toolCallId, state } = part; return ( {state === "input-available" && ( )} {state === "output-available" && ( } /> )} ); } if (type === "tool-createDocument") { const { toolCallId } = part; if (part.output && "error" in part.output) { return (
创建文档失败:{String(part.output.error)}
); } return ( ); } if (type === "tool-updateDocument") { const { toolCallId } = part; if (part.output && "error" in part.output) { return (
更新文档失败:{String(part.output.error)}
); } return (
); } if (type === "tool-requestSuggestions") { const { toolCallId, state } = part; return ( {state === "input-available" && ( )} {state === "output-available" && ( 错误:{String(part.output.error)}
) : ( ) } /> )} ); } return null; })} {!isReadonly && ( )}
); }; export const PreviewMessage = memo( PurePreviewMessage, (prevProps, nextProps) => { if (prevProps.isLoading !== nextProps.isLoading) { return false; } if (prevProps.message.id !== nextProps.message.id) { return false; } if (prevProps.requiresScrollPadding !== nextProps.requiresScrollPadding) { return false; } if (!equal(prevProps.message.parts, nextProps.message.parts)) { return false; } if (!equal(prevProps.vote, nextProps.vote)) { return false; } return false; } ); export const ThinkingMessage = () => { return (
{/* 增强的头像动画 - 添加多层脉动效果 */}
{/* 外层脉动光晕 */}
{/* 中层脉动光晕 */}
{/* 头像容器 */}
{/* 简洁明了的文本 + 动态点 */}
AI 正在思考中
); };