fix detail workspace layout gap by filtering non-renderable chat messages
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-19 11:51:23 +08:00
parent 8aba32d35d
commit 9d2b21e1ef

View File

@ -3010,7 +3010,69 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
);
const shouldShowImageLoading = hasGenerationStart && !hasGenerationEnd && !!currentModel;
const loadingImageCount = imageParams.imageCount || 1;
const isDetailEmptyState = isDetailGenerationMode && visibleMessages.length === 0;
const renderableVisibleMessages = useMemo(() => {
return visibleMessages.reduce<ChatMessage[]>((acc, message) => {
const rawParts = (message.parts || []) as any[];
const cleanedParts = rawParts.map((part: any) => {
if (part.type === "text" && part.text) {
return {
...part,
text: String(part.text)
.replace(/\n*<!-- GENERATING_IMAGE -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_START -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_END -->\n*/g, "")
.trim(),
};
}
return part;
});
const fallbackContent =
typeof (message as any).content === "string"
? String((message as any).content).trim()
: "";
if (cleanedParts.length === 0 && fallbackContent) {
cleanedParts.push({ type: "text", text: fallbackContent } as any);
}
let hasContent = false;
for (const part of cleanedParts) {
if (part?.type === "text" && part?.text && String(part.text).trim().length > 0) {
hasContent = true;
break;
}
const fileUrl =
typeof part?.url === "string" && part.url.trim()
? part.url
: typeof part?.data === "string" && part.data.trim()
? part.data
: "";
if ((part?.type === "file" || part?.type === "image") && fileUrl) {
hasContent = true;
break;
}
}
const attachmentsList = (message as any).attachments || [];
if (!hasContent && attachmentsList.length > 0) {
hasContent = true;
}
if (!hasContent) {
return acc;
}
acc.push({ ...message, parts: cleanedParts } as ChatMessage);
return acc;
}, []);
}, [visibleMessages]);
const isDetailEmptyState =
isDetailGenerationMode &&
renderableVisibleMessages.length === 0 &&
!isLoading &&
!shouldShowImageLoading;
const renderModelSelectContent = () => (
<SelectContent className="w-[238px] max-h-[340px] overflow-hidden rounded-[18px] border border-[#cfd4dc] bg-[#f3f4f6] p-1.5 shadow-[0_8px_20px_rgba(15,23,42,0.13)]">
@ -3170,19 +3232,19 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
{/* 消息列表 */}
{isDetailEmptyState ? (
<div className="h-16 shrink-0" />
<div className="h-3 shrink-0" />
) : (
<div
key={`${activeChatId}-${messageViewportKey}`}
className={cn(
"flex-1 min-h-0 overflow-y-auto overflow-x-hidden p-4",
isDetailGenerationMode && visibleMessages.length === 0
isDetailGenerationMode && renderableVisibleMessages.length === 0
? "pt-10"
: "pt-4"
)}
>
<div className="space-y-6 overflow-x-hidden pb-24 pr-1">
{!isDetailGenerationMode && visibleMessages.length === 0 && (
{!isDetailGenerationMode && renderableVisibleMessages.length === 0 && (
<div className={cn("flex flex-col h-full", isDetailGenerationMode ? "pt-0" : "pt-4")}>
{/* Greeting */}
<div className={cn(isDetailGenerationMode ? "mb-4" : "mb-8")}>
@ -3219,66 +3281,9 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
</div>
)}
{visibleMessages.map((message) => {
// 🆕 清理消息中的内部标记并检查是否有实际内容
const rawParts = message.parts || [];
const cleanedParts = rawParts.map((part: any) => {
if (part.type === 'text' && part.text) {
return {
...part,
text: part.text
.replace(/\n*<!-- GENERATING_IMAGE -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_START -->\n*/g, "")
.replace(/\n*<!-- IMAGE_GENERATION_END -->\n*/g, "")
.trim()
};
}
return part;
});
const fallbackContent =
typeof (message as any).content === "string"
? (message as any).content.trim()
: "";
if (cleanedParts.length === 0 && fallbackContent) {
cleanedParts.push({ type: "text", text: fallbackContent } as any);
}
// 检查清理后是否有实际内容(更严格的检查)
let hasContent = false;
for (const part of cleanedParts) {
if (part.type === 'text' && part.text && part.text.trim().length > 0) {
hasContent = true;
break;
}
const fileUrl =
typeof part?.url === "string" && part.url.trim()
? part.url
: typeof part?.data === "string" && part.data.trim()
? part.data
: "";
if ((part.type === 'file' || part.type === 'image') && fileUrl) {
hasContent = true;
break;
}
}
// 同时检查附件
const attachmentsList = (message as any).attachments || [];
if (attachmentsList.length > 0) {
hasContent = true;
}
// 没有内容则不渲染(包括空 parts 的情况)
if (!hasContent) {
console.log('[CanvasChat] 跳过空消息:', message.id);
return null;
}
// 使用清理后的消息
const cleanedMessage = { ...message, parts: cleanedParts };
{renderableVisibleMessages.map((message) => {
const assistantTextParts = message.role === "assistant"
? cleanedParts.filter((part: any) => part.type === "text" && part.text)
? (message.parts || []).filter((part: any) => part.type === "text" && part.text)
: [];
const hasAssistantImages = assistantTextParts.some((part: any) =>
extractImageUrls(part.text || "").length > 0
@ -3335,7 +3340,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
{(() => {
const renderedUserParts: React.ReactNode[] = [];
cleanedMessage.parts?.forEach((part: any, idx: number) => {
message.parts?.forEach((part: any, idx: number) => {
const fileUrl =
typeof part?.url === "string" && part.url.trim()
? part.url
@ -3372,7 +3377,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
});
if (
!cleanedMessage.parts?.some((p: any) => p.type === 'file') &&
!message.parts?.some((p: any) => p.type === 'file') &&
(message as any).attachments?.length > 0
) {
(message as any).attachments.forEach((att: any, idx: number) => {
@ -3395,7 +3400,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
if (renderedUserParts.length === 0) {
const fallbackText =
extractUserTextPrompt(cleanedMessage as any) ||
extractUserTextPrompt(message as any) ||
(isVideoModel ? "已发送视频生成请求" : "已发送请求");
renderedUserParts.push(
<span key="fallback-text" className="text-sm text-foreground">
@ -3420,7 +3425,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
>
{/* 消息内容 */}
<div className="w-full min-w-0 whitespace-pre-wrap break-words [overflow-wrap:anywhere] leading-relaxed">
{cleanedMessage.parts?.map((part: any, i: number) => {
{message.parts?.map((part: any, i: number) => {
if (part.type === 'text') {
// 提取并显示图片/视频
const rawText = part.text || '';
@ -3568,7 +3573,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
className={cn(
"mx-3 overflow-y-auto rounded-2xl border border-black/10 bg-white/95 p-3 pr-2",
"mt-3",
visibleMessages.length === 0 ? "max-h-[62vh]" : "max-h-[54vh]"
renderableVisibleMessages.length === 0 ? "max-h-[62vh]" : "max-h-[54vh]"
)}
>
<div className="flex items-start justify-between gap-3">