diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index bc30926d5..8410edcab 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -8,6 +8,11 @@ import useSWR, { useSWRConfig } from "swr"; import { Send, Loader2, ImagePlus, Paperclip, X, Plus, History, LayoutGrid, Maximize2, RefreshCw, Globe, Box, ArrowUp, Video, Sparkles, Check, ChevronDown, ChevronUp, AlertCircle, CheckCircle2, Brain, ThumbsUp, ThumbsDown } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; import { AlertDialog, AlertDialogAction, @@ -232,6 +237,24 @@ function normalizeCanvasSectionTitle(line: string): string { return normalized; } +function shouldSplitCanvasAnalysisAfterLine(line: string): boolean { + const normalized = String(line || "") + .replace(/^[-*]\s*/, "") + .trim(); + if (!normalized) return false; + + if ( + /^(现在我将|接下来|下一步|我将|现在开始|下面我|随后|Then\b|Next\b|Now\b)/i.test( + normalized + ) + ) { + return true; + } + + // 模型徽标行(例如:◈ Nano Banana Pro) + return /^[◈◇◆🔹⬡✦•·]\s*[A-Za-z][A-Za-z0-9 .:+\-_/()]{2,}\s*$/i.test(normalized); +} + function extractCanvasImageAnalysisSection(content: string): CanvasAnalysisSection | null { const lines = String(content || "").split(/\r?\n/); let startIndex = -1; @@ -260,6 +283,10 @@ function extractCanvasImageAnalysisSection(content: string): CanvasAnalysisSecti let endIndex = lines.length; for (let i = startIndex + 1; i < lines.length; i += 1) { + if (shouldSplitCanvasAnalysisAfterLine(lines[i])) { + endIndex = i; + break; + } const title = normalizeCanvasSectionTitle(lines[i]); if (!title || title.startsWith(CANVAS_IMAGE_ANALYSIS_TITLE)) continue; if (CANVAS_AGENT_SECTION_TITLES.has(title)) { @@ -954,6 +981,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF const [lastImageModelId, setLastImageModelId] = useState(null); const [lastVideoModelId, setLastVideoModelId] = useState(null); const [deepThinkingEnabled, setDeepThinkingEnabled] = useState(false); + const [analysisCollapseState, setAnalysisCollapseState] = useState>({}); const [insufficientPointsState, setInsufficientPointsState] = useState<{ balance: number; expectedCost: number; @@ -4959,6 +4987,8 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF const textWithoutImages = stripMediaMarkup(rawText); const analysisSection = extractCanvasImageAnalysisSection(textWithoutImages); const plainText = normalizeCanvasAgentText(textWithoutImages); + const analysisToggleKey = `${message.id}-${i}-analysis`; + const analysisOpen = analysisCollapseState[analysisToggleKey] ?? true; const renderPlainText = (text: string, key: string) => (
-
+ + setAnalysisCollapseState((prev) => ({ + ...prev, + [analysisToggleKey]: open, + })) + } + > + Q. @@ -4985,12 +5024,23 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF 图片分析 - -
-
+ + + {analysisSection.analysis} -
-
+ + {analysisSection.after ? renderPlainText(analysisSection.after, `after-${i}`) : null} diff --git a/components/message.tsx b/components/message.tsx index d55c14077..35f01ae57 100644 --- a/components/message.tsx +++ b/components/message.tsx @@ -137,29 +137,36 @@ function filterVideoContent(text: string): string { } /** - * 过滤 Agent 里“模型路由说明”这类冗余展示,避免在对话里暴露模型名称行 + * 规范化 Agent 路由文本: + * - 保留“模型名”展示 + * - 去掉原始路由日志行(Routed to / Using model) */ function filterAgentRoutingNoise(text: string): string { if (!text) return text; let filtered = text; + let routedModelName = ""; - // 1) 去掉“现在我将使用 XXX ...”这类过渡句 - filtered = filtered.replace(/^现在我将使用.+$/gim, ""); - - // 2) 去掉独立的模型名称行(常见为带图标的单行) + // 提取并移除“路由日志行”,稍后转成用户可见的模型名行 filtered = filtered.replace( - /^\s*[◈◇◆🔹⬡✦•·]\s*[A-Za-z][A-Za-z0-9 .:+\-_/()]{2,}\s*$/gm, - "", + /^\s*(?:Routed to|Using model)\s*[::]\s*([^\n(]+).*$/gim, + (_match, modelName: string) => { + if (!routedModelName) { + routedModelName = String(modelName || "").trim(); + } + return ""; + } ); - // 3) 去掉纯模型路由标识行(不带图标) - filtered = filtered.replace( - /^\s*(?:Routed to|Using model|模型|Model)\s*[::].*$/gim, - "", + // 如果文本里没有现成的模型展示行,则补一行友好的模型名 + const hasModelBadgeLine = /^\s*[◈◇◆🔹⬡✦•·]\s*[A-Za-z][A-Za-z0-9 .:+\-_/()]{2,}\s*$/im.test( + filtered ); + if (routedModelName && !hasModelBadgeLine) { + filtered = `◈ ${routedModelName}\n\n${filtered}`; + } - // 4) 清理多余空行 + // 清理多余空行 filtered = filtered.replace(/\n{3,}/g, "\n\n").trim(); return filtered;