feat: improve image analysis collapse and keep model badge visible
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-02-21 02:24:03 +08:00
parent 9a1cbc4d15
commit 7cc489a510
2 changed files with 76 additions and 19 deletions

View File

@ -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<string | null>(null);
const [lastVideoModelId, setLastVideoModelId] = useState<string | null>(null);
const [deepThinkingEnabled, setDeepThinkingEnabled] = useState(false);
const [analysisCollapseState, setAnalysisCollapseState] = useState<Record<string, boolean>>({});
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) => (
<div
key={key}
@ -4975,8 +5005,17 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
{analysisSection.before
? renderPlainText(analysisSection.before, `before-${i}`)
: null}
<div className="my-2 rounded-[20px] bg-[#f4f5f7] px-5 py-4">
<div className="flex items-center justify-between gap-3">
<Collapsible
className="my-2 rounded-[20px] bg-[#f4f5f7] px-5 py-4"
open={analysisOpen}
onOpenChange={(open) =>
setAnalysisCollapseState((prev) => ({
...prev,
[analysisToggleKey]: open,
}))
}
>
<CollapsibleTrigger className="flex w-full items-center justify-between gap-3 text-left">
<span className="inline-flex items-center gap-2">
<span className="inline-flex h-6 min-w-6 items-center justify-center rounded-full bg-white text-[11px] font-semibold text-[#98a2b3] shadow-[inset_0_0_0_1px_rgba(148,163,184,0.45)]">
Q.
@ -4985,12 +5024,23 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
图片分析
</span>
</span>
<ChevronUp className="h-4 w-4 text-[#9aa1ad]" />
</div>
<div className="pt-3 text-[15px] font-medium leading-7 text-[#2f3640]">
<ChevronDown
className={cn(
"h-4 w-4 text-[#9aa1ad] transition-transform duration-200",
analysisOpen ? "rotate-180" : "rotate-0"
)}
/>
</CollapsibleTrigger>
<CollapsibleContent
className={cn(
"pt-3 text-[15px] font-medium leading-7 text-[#2f3640]",
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-1 data-[state=open]:slide-in-from-top-1",
"outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in"
)}
>
{analysisSection.analysis}
</div>
</div>
</CollapsibleContent>
</Collapsible>
{analysisSection.after
? renderPlainText(analysisSection.after, `after-${i}`)
: null}

View File

@ -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;