fix(canvas): dedupe vote row and show model badge
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:34:24 +08:00
parent 7cc489a510
commit 1d293f180e

View File

@ -158,6 +158,46 @@ function stripMediaMarkup(text: string): string {
.trim();
}
function extractCanvasModelName(text: string): string {
const source = String(text || "");
if (!source) return "";
const badgeMatch = source.match(
/^\s*[◈◇◆🔹⬡✦•·]\s*([A-Za-z][A-Za-z0-9 .:+\-_/()]{2,})\s*$/im
);
if (badgeMatch?.[1]) {
return badgeMatch[1].trim();
}
const routedMatch = source.match(
/^\s*(?:Routed to|Using model)\s*[::]\s*([^\n(]+).*$/im
);
if (routedMatch?.[1]) {
return routedMatch[1].trim();
}
return "";
}
function stripCanvasModelRoutingLines(text: string): string {
return String(text || "")
.replace(/^\s*(?:Routed to|Using model)\s*[::].*$/gim, "")
.replace(/^\s*[◈◇◆🔹⬡✦•·]\s*[A-Za-z][A-Za-z0-9 .:+\-_/()]{2,}\s*$/gim, "")
.replace(/\n{3,}/g, "\n\n")
.trim();
}
function messageHasAssistantMedia(message: ChatMessage | undefined): boolean {
if (!message || message.role !== "assistant") return false;
const textParts = (message.parts || []).filter(
(part: any) => part.type === "text" && part.text
);
return textParts.some((part: any) => {
const text = String(part.text || "");
return extractImageUrls(text).length > 0 || extractVideoUrls(text).length > 0;
});
}
function getMessageTimestamp(message: ChatMessage): number | null {
const rawValue =
(message as any).createdAt ??
@ -4840,19 +4880,32 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
const assistantTextParts = message.role === "assistant"
? (message.parts || []).filter((part: any) => part.type === "text" && part.text)
: [];
const assistantModelName =
assistantTextParts
.map((part: any) => extractCanvasModelName(part.text || ""))
.find((name) => Boolean(name)) || "";
const hasAssistantImages = assistantTextParts.some((part: any) =>
extractImageUrls(part.text || "").length > 0
);
const hasAssistantVideos = assistantTextParts.some((part: any) =>
extractVideoUrls(part.text || "").length > 0
);
const hasAssistantText = assistantTextParts.some((part: any) => {
return stripMediaMarkup(part.text).length > 0;
});
const hasAssistantMedia = hasAssistantImages || hasAssistantVideos;
const hasAssistantText = assistantTextParts.some((part: any) =>
stripCanvasModelRoutingLines(stripMediaMarkup(part.text || "")).length > 0
);
const isImageOnlyAssistant =
message.role === "assistant" &&
(hasAssistantImages || hasAssistantVideos) &&
hasAssistantMedia &&
!hasAssistantText;
const nextMessage = renderableVisibleMessages[index + 1] as ChatMessage | undefined;
const nextAssistantHasMedia = messageHasAssistantMedia(nextMessage);
const hideVoteActions =
message.role === "assistant" &&
!hasAssistantMedia &&
nextAssistantHasMedia;
const showVoteActions = message.role === "assistant" && !hideVoteActions;
const showModelMeta = hideVoteActions && Boolean(assistantModelName);
const messageTimestamp = getMessageTimestamp(message);
const prevMessageTimestamp =
@ -4984,7 +5037,9 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
const rawText = part.text || '';
const imageUrls = extractImageUrls(rawText);
const videoUrls = extractVideoUrls(rawText);
const textWithoutImages = stripMediaMarkup(rawText);
const textWithoutImages = stripCanvasModelRoutingLines(
stripMediaMarkup(rawText)
);
const analysisSection = extractCanvasImageAnalysisSection(textWithoutImages);
const plainText = normalizeCanvasAgentText(textWithoutImages);
const analysisToggleKey = `${message.id}-${i}-analysis`;
@ -5109,31 +5164,40 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
})}
</div>
</div>
<div className="mt-1 flex items-center gap-1.5 text-[#8f96a3]">
<button
type="button"
className={cn(
"inline-flex h-7 w-7 items-center justify-center rounded-md transition-colors hover:bg-black/5",
currentVote?.isUpvoted && "text-[#22a06b] bg-emerald-50/80"
)}
title="点赞"
disabled={isVotingCurrentMessage}
onClick={() => void handleAssistantVote(message.id, "up")}
>
<ThumbsUp className="h-4 w-4" />
</button>
<button
type="button"
className={cn(
"inline-flex h-7 w-7 items-center justify-center rounded-md transition-colors hover:bg-black/5",
currentVote && !currentVote.isUpvoted && "text-[#d94848] bg-rose-50/80"
)}
title="点踩"
disabled={isVotingCurrentMessage}
onClick={() => void handleAssistantVote(message.id, "down")}
>
<ThumbsDown className="h-4 w-4" />
</button>
<div className="mt-1 flex items-center gap-2 text-[#8f96a3]">
{showModelMeta ? (
<span className="text-[12px] font-medium tracking-tight text-[#8d94a2]">
◈ {assistantModelName}
</span>
) : null}
{showVoteActions ? (
<>
<button
type="button"
className={cn(
"inline-flex h-7 w-7 items-center justify-center rounded-md transition-colors hover:bg-black/5",
currentVote?.isUpvoted && "text-[#22a06b] bg-emerald-50/80"
)}
title="点赞"
disabled={isVotingCurrentMessage}
onClick={() => void handleAssistantVote(message.id, "up")}
>
<ThumbsUp className="h-4 w-4" />
</button>
<button
type="button"
className={cn(
"inline-flex h-7 w-7 items-center justify-center rounded-md transition-colors hover:bg-black/5",
currentVote && !currentVote.isUpvoted && "text-[#d94848] bg-rose-50/80"
)}
title="点踩"
disabled={isVotingCurrentMessage}
onClick={() => void handleAssistantVote(message.id, "down")}
>
<ThumbsDown className="h-4 w-4" />
</button>
</>
) : null}
</div>
</>
)}
@ -5156,7 +5220,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
<div className="flex w-full justify-start">
<div className="flex items-center gap-2 text-[#8e97a5]">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-[16px]">思考中</span>
<span className="text-[16px]">分析中</span>
</div>
</div>
)}