fix(canvas): restore assistant result image parsing in canvas
This commit is contained in:
parent
507615e984
commit
c2ba727ad0
@ -5,7 +5,7 @@ import { DefaultChatTransport } from "ai";
|
||||
import { useEffect, useRef, useState, useCallback, useMemo } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
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, Eye, Trash2 } from "lucide-react";
|
||||
import { Send, Loader2, ImagePlus, Paperclip, X, Plus, History, LayoutGrid, Maximize2, RefreshCw, Globe, Box, ArrowUp, Video, Sparkles, Check, ChevronDown, ChevronUp, AlertCircle, CheckCircle2, Brain, ThumbsUp, ThumbsDown, Eye, Trash2, Download } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
@ -39,7 +39,11 @@ import { toast } from "sonner";
|
||||
import { useCanvasStore } from "@/store/canvas";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { fetcher, fetchWithErrorHandlers, generateUUID } from "@/lib/utils";
|
||||
import { extractImageUrlsFromText } from "@/lib/image-url";
|
||||
import {
|
||||
extractImageUrlsFromText,
|
||||
extractOriginalImageUrlFromAlt,
|
||||
inferOriginalImageUrl,
|
||||
} from "@/lib/image-url";
|
||||
import type { Attachment, ChatMessage } from "@/lib/types";
|
||||
import { type CanvasLaunchPayload, getCanvasLaunchStorageKey } from "@/lib/canvas-launch";
|
||||
import { ImageParamsBar, type ImageGenerationParams } from "@/components/image-params-bar";
|
||||
@ -152,6 +156,43 @@ function normalizeMediaUrl(url: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeImageIdentityUrl(url: string): string {
|
||||
const raw = String(url || "").trim();
|
||||
if (!raw) return "";
|
||||
const canonical = inferOriginalImageUrl(raw);
|
||||
return normalizeMediaUrl(canonical || raw);
|
||||
}
|
||||
|
||||
function buildDownloadFilename(url: string): string {
|
||||
const fallback = `canvas-image-${Date.now()}.png`;
|
||||
const raw = String(url || "").trim();
|
||||
if (!raw) return fallback;
|
||||
|
||||
try {
|
||||
const pathname = new URL(raw).pathname || "";
|
||||
const candidate = pathname.split("/").pop() || "";
|
||||
if (candidate) return candidate;
|
||||
} catch {
|
||||
const candidate = raw.split("/").pop()?.split("?")[0] || "";
|
||||
if (candidate) return candidate;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function getAssistantImageNodeIdentityKey(node: any): string {
|
||||
if (!node || node.type !== "image") return "";
|
||||
const data = ((node.data as any) || {}) as Record<string, any>;
|
||||
const metadata = ((data.metadata as any) || {}) as Record<string, any>;
|
||||
const sourceMessageId = String(metadata.sourceMessageId || "").trim();
|
||||
if (!sourceMessageId) return "";
|
||||
const sourceImageKey = normalizeImageIdentityUrl(
|
||||
String(metadata.sourceImageKey || data.src || "").trim()
|
||||
);
|
||||
if (!sourceImageKey) return "";
|
||||
return `${sourceMessageId}::${sourceImageKey}`;
|
||||
}
|
||||
|
||||
function collectMessageImageUrlKeys(message: ChatMessage | undefined): Set<string> {
|
||||
const urlKeys = new Set<string>();
|
||||
if (!message) return urlKeys;
|
||||
@ -198,6 +239,43 @@ function getNearestUserImageUrlKeys(messages: ChatMessage[], assistantIndex: num
|
||||
}
|
||||
|
||||
function extractAssistantImageUrls(text: string, referenceUrlKeys?: Set<string>): string[] {
|
||||
const markdownPattern = /!\[([^\]]*)\]\((https?:\/\/[^\s)]+)\)/gim;
|
||||
const markdownUrls: string[] = [];
|
||||
const seenMarkdownUrls = new Set<string>();
|
||||
let match: RegExpExecArray | null = null;
|
||||
|
||||
while ((match = markdownPattern.exec(text)) !== null) {
|
||||
const altText = String(match[1] || "");
|
||||
const markdownSrc = String(match[2] || "").trim();
|
||||
if (!markdownSrc) continue;
|
||||
|
||||
const originalFromAlt = extractOriginalImageUrlFromAlt(altText, markdownSrc);
|
||||
const candidates = [originalFromAlt, inferOriginalImageUrl(markdownSrc), markdownSrc];
|
||||
let selected = "";
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const normalized = normalizeMediaUrl(candidate);
|
||||
if (!normalized) continue;
|
||||
if (referenceUrlKeys?.size && referenceUrlKeys.has(normalized)) continue;
|
||||
selected = candidate;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!selected) {
|
||||
selected = candidates.find((candidate) => normalizeMediaUrl(candidate)) || "";
|
||||
}
|
||||
if (!selected) continue;
|
||||
|
||||
const dedupeKey = normalizeMediaUrl(selected);
|
||||
if (!dedupeKey || seenMarkdownUrls.has(dedupeKey)) continue;
|
||||
seenMarkdownUrls.add(dedupeKey);
|
||||
markdownUrls.push(selected);
|
||||
}
|
||||
|
||||
if (markdownUrls.length > 0) {
|
||||
return markdownUrls;
|
||||
}
|
||||
|
||||
const urls = extractImageUrls(text);
|
||||
if (!referenceUrlKeys || referenceUrlKeys.size === 0) return urls;
|
||||
|
||||
@ -1343,6 +1421,27 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
const videoParamsRef = useRef(videoParams);
|
||||
const deepThinkingEnabledRef = useRef(deepThinkingEnabled);
|
||||
const imageAutoTitleCacheRef = useRef<Map<string, string>>(new Map());
|
||||
const sendGuardRef = useRef<{
|
||||
inFlight: boolean;
|
||||
signature: string;
|
||||
timestamp: number;
|
||||
}>({
|
||||
inFlight: false,
|
||||
signature: "",
|
||||
timestamp: 0,
|
||||
});
|
||||
const releaseSendGuard = useCallback(() => {
|
||||
sendGuardRef.current.inFlight = false;
|
||||
}, []);
|
||||
|
||||
const shouldBlockDuplicateSend = useCallback((signature: string) => {
|
||||
const now = Date.now();
|
||||
const previous = sendGuardRef.current;
|
||||
return (
|
||||
previous.signature === signature &&
|
||||
now - previous.timestamp < 4500
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleTypewriterActiveChange = useCallback((typingKey: string, active: boolean) => {
|
||||
setActiveTypingSections((prev) => {
|
||||
@ -1547,6 +1646,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
|
||||
// Canvas store
|
||||
const addNode = useCanvasStore((state) => state.addNode);
|
||||
const deleteNode = useCanvasStore((state) => state.deleteNode);
|
||||
const updateNode = useCanvasStore((state) => state.updateNode);
|
||||
const nodes = useCanvasStore((state) => state.nodes);
|
||||
const projectTitle = useCanvasStore((state) => state.projectTitle);
|
||||
@ -1563,6 +1663,40 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
const voteSWRKey = rawChatId ? `/api/vote?chatId=${rawChatId}` : null;
|
||||
const { mutate } = useSWRConfig();
|
||||
const [votingMessageId, setVotingMessageId] = useState<string | null>(null);
|
||||
const handleDownloadOriginalImage = useCallback(async (imageUrl: string) => {
|
||||
const raw = String(imageUrl || "").trim();
|
||||
if (!raw) {
|
||||
toast.error("图片地址无效");
|
||||
return;
|
||||
}
|
||||
|
||||
const filename = buildDownloadFilename(raw);
|
||||
try {
|
||||
const response = await fetch(raw);
|
||||
if (!response.ok) {
|
||||
throw new Error(`download failed: ${response.status}`);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const blobUrl = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = blobUrl;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(blobUrl);
|
||||
return;
|
||||
} catch {
|
||||
const link = document.createElement("a");
|
||||
link.href = raw;
|
||||
link.target = "_blank";
|
||||
link.rel = "noopener noreferrer";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
toast.message("已打开原图,可右键另存为");
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 从 API 加载历史消息
|
||||
const { data: historyData } = useSWR<{
|
||||
@ -2730,6 +2864,12 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "ready") {
|
||||
releaseSendGuard();
|
||||
}
|
||||
}, [releaseSendGuard, status]);
|
||||
|
||||
useEffect(() => {
|
||||
const launchPayload = pendingLaunchRef.current;
|
||||
if (!launchPayload?.autoSend) return;
|
||||
@ -2898,7 +3038,23 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
const existingImageUrlKeys = new Set(
|
||||
nodes
|
||||
.filter((n) => n.type === "image" && (n.data as any)?.src)
|
||||
.map((n) => normalizeMediaUrl((n.data as any).src))
|
||||
.map((n) => normalizeImageIdentityUrl((n.data as any).src))
|
||||
.filter(Boolean)
|
||||
);
|
||||
const existingMessageImageKeys = new Set(
|
||||
nodes
|
||||
.filter((n) => n.type === "image")
|
||||
.map((n) => {
|
||||
const data = ((n.data as any) || {}) as Record<string, any>;
|
||||
const metadata = ((data.metadata as any) || {}) as Record<string, any>;
|
||||
const sourceMessageId = String(metadata.sourceMessageId || "").trim();
|
||||
if (!sourceMessageId) return "";
|
||||
const sourceImageKey = normalizeImageIdentityUrl(
|
||||
String(metadata.sourceImageKey || data.src || "").trim()
|
||||
);
|
||||
if (!sourceImageKey) return "";
|
||||
return `${sourceMessageId}::${sourceImageKey}`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
);
|
||||
const existingVideoUrlKeys = new Set(
|
||||
@ -2916,7 +3072,8 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
}
|
||||
|
||||
const content = message.parts?.map((p: any) => p.text || "").join("") || "";
|
||||
const imageUrls = extractImageUrls(content);
|
||||
const nearestUserImageKeys = getNearestUserImageUrlKeys(messages, messageIndex);
|
||||
const imageUrls = extractAssistantImageUrls(content, nearestUserImageKeys);
|
||||
const videoUrls = extractVideoUrls(content);
|
||||
if (imageUrls.length === 0 && videoUrls.length === 0) continue;
|
||||
|
||||
@ -2926,16 +3083,52 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
let addedImageCount = 0;
|
||||
let addedVideoCount = 0;
|
||||
|
||||
const newImageUrls = imageUrls.filter((url) => {
|
||||
const key = normalizeMediaUrl(url);
|
||||
if (!key || existingImageUrlKeys.has(key)) return false;
|
||||
existingImageUrlKeys.add(key);
|
||||
return true;
|
||||
});
|
||||
const newImageEntries = imageUrls.reduce<Array<{ url: string; sourceImageKey: string }>>(
|
||||
(acc, url) => {
|
||||
const sourceImageKey = normalizeImageIdentityUrl(url);
|
||||
if (!sourceImageKey || existingImageUrlKeys.has(sourceImageKey)) {
|
||||
return acc;
|
||||
}
|
||||
const messageImageKey = `${message.id}::${sourceImageKey}`;
|
||||
if (existingMessageImageKeys.has(messageImageKey)) {
|
||||
return acc;
|
||||
}
|
||||
existingImageUrlKeys.add(sourceImageKey);
|
||||
existingMessageImageKeys.add(messageImageKey);
|
||||
acc.push({ url, sourceImageKey });
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
for (let index = 0; index < newImageUrls.length; index += 1) {
|
||||
for (let index = 0; index < newImageEntries.length; index += 1) {
|
||||
if (cancelled) return;
|
||||
const url = newImageUrls[index];
|
||||
const imageEntry = newImageEntries[index];
|
||||
const url = imageEntry.url;
|
||||
const messageImageKey = `${message.id}::${imageEntry.sourceImageKey}`;
|
||||
const hasDuplicateInStore = useCanvasStore
|
||||
.getState()
|
||||
.nodes.some((node) => {
|
||||
if (node.type !== "image") return false;
|
||||
const data = ((node.data as any) || {}) as Record<string, any>;
|
||||
const metadata = ((data.metadata as any) || {}) as Record<string, any>;
|
||||
const sourceMessageId = String(metadata.sourceMessageId || "").trim();
|
||||
const sourceImageKey = normalizeImageIdentityUrl(
|
||||
String(metadata.sourceImageKey || data.src || "").trim()
|
||||
);
|
||||
if (!sourceImageKey) return false;
|
||||
const nodeMessageImageKey = sourceMessageId
|
||||
? `${sourceMessageId}::${sourceImageKey}`
|
||||
: "";
|
||||
return (
|
||||
nodeMessageImageKey === messageImageKey ||
|
||||
sourceImageKey === imageEntry.sourceImageKey
|
||||
);
|
||||
});
|
||||
if (hasDuplicateInStore) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const dimensions = await getImageDimensions(url);
|
||||
const existingNodes = nodes.length + addedImageCount + addedVideoCount;
|
||||
const spacing = 20;
|
||||
@ -2956,6 +3149,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
originalHeight: dimensions.originalHeight,
|
||||
generationPrompt: generationPrompt || undefined,
|
||||
sourceMessageId: message.id,
|
||||
sourceImageKey: imageEntry.sourceImageKey,
|
||||
},
|
||||
},
|
||||
connections: [],
|
||||
@ -3009,6 +3203,31 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
};
|
||||
}, [messages, addNode, nodes, autoNameGeneratedImageNode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (nodes.length < 2) return;
|
||||
|
||||
const seenKeys = new Set<string>();
|
||||
const duplicateNodeIds: string[] = [];
|
||||
|
||||
for (const node of nodes) {
|
||||
const identityKey = getAssistantImageNodeIdentityKey(node);
|
||||
if (!identityKey) continue;
|
||||
if (seenKeys.has(identityKey)) {
|
||||
duplicateNodeIds.push(node.id);
|
||||
continue;
|
||||
}
|
||||
seenKeys.add(identityKey);
|
||||
}
|
||||
|
||||
if (duplicateNodeIds.length === 0) return;
|
||||
|
||||
duplicateNodeIds.forEach((nodeId) => deleteNode(nodeId));
|
||||
console.warn(
|
||||
"[CanvasChat] Removed duplicate assistant image nodes:",
|
||||
duplicateNodeIds.length
|
||||
);
|
||||
}, [deleteNode, nodes]);
|
||||
|
||||
// 为历史节点补全可编辑的原始提示词(旧数据里可能只有“对话生成”占位文案)
|
||||
useEffect(() => {
|
||||
if (!messages.length || !nodes.length) return;
|
||||
@ -4966,8 +5185,21 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
|
||||
// 处理发送(图片已在上传时添加到画布,这里只发送消息)
|
||||
const handleSend = useCallback(async () => {
|
||||
if (sendGuardRef.current.inFlight || status === "streaming" || status === "submitted") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDetailGenerationMode && aiDetailGenerationEnabled) {
|
||||
await handleDetailGeneration();
|
||||
sendGuardRef.current = {
|
||||
inFlight: true,
|
||||
signature: `detail:${activeChatId}:${Date.now()}`,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
try {
|
||||
await handleDetailGeneration();
|
||||
} finally {
|
||||
releaseSendGuard();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -5097,11 +5329,41 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
currentModelIdRef.current = effectiveModelId;
|
||||
}
|
||||
|
||||
// 使用与 multimodal-input 相同的格式发送消息
|
||||
sendMessage({
|
||||
role: "user",
|
||||
parts: parts,
|
||||
const normalizedAttachmentKeys = effectiveAttachments
|
||||
.map((attachment) => normalizeMediaUrl(attachment.url))
|
||||
.filter(Boolean)
|
||||
.sort();
|
||||
const sendSignature = JSON.stringify({
|
||||
chatId: activeChatId,
|
||||
model: effectiveModelId,
|
||||
text: textContent,
|
||||
attachments: normalizedAttachmentKeys,
|
||||
mode: isVideoModel ? "video" : "image",
|
||||
});
|
||||
|
||||
if (shouldBlockDuplicateSend(sendSignature)) {
|
||||
toast.message("检测到重复发送,已自动忽略");
|
||||
return;
|
||||
}
|
||||
|
||||
sendGuardRef.current = {
|
||||
inFlight: true,
|
||||
signature: sendSignature,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// 使用与 multimodal-input 相同的格式发送消息
|
||||
try {
|
||||
sendMessage({
|
||||
role: "user",
|
||||
parts: parts,
|
||||
});
|
||||
} catch (error) {
|
||||
releaseSendGuard();
|
||||
console.error("[CanvasChat] sendMessage failed:", error);
|
||||
toast.error("发送失败,请重试");
|
||||
return;
|
||||
}
|
||||
hasHydratedHistoryRef.current = true;
|
||||
|
||||
// 清空状态
|
||||
@ -5118,6 +5380,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
console.log('[CanvasChat] 已清空输入框和选中状态');
|
||||
}
|
||||
}, [
|
||||
activeChatId,
|
||||
input,
|
||||
attachments,
|
||||
selectedModelId,
|
||||
@ -5133,6 +5396,10 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
aiDetailGenerationEnabled,
|
||||
handleDetailGeneration,
|
||||
ensureImagePointsBeforeSend,
|
||||
isVideoModel,
|
||||
releaseSendGuard,
|
||||
shouldBlockDuplicateSend,
|
||||
status,
|
||||
]);
|
||||
|
||||
const maybeAutoSetCanvasTitle = useCallback(async (firstImageUrl: string | null) => {
|
||||
@ -5671,10 +5938,10 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
}, [activeSessionTitle, projectTitle]);
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full min-h-0 flex-col bg-background/95 backdrop-blur-sm">
|
||||
<div className="relative flex h-full min-h-0 flex-col bg-white dark:bg-zinc-950">
|
||||
{/* 全局遮罩提示 - 当处于画布选图模式时显示 */}
|
||||
{detailSlotPickerIndex !== null && (
|
||||
<div className="absolute inset-0 z-50 flex flex-col items-center justify-center bg-background/90 backdrop-blur-sm animate-in fade-in duration-200">
|
||||
<div className="absolute inset-0 z-50 flex flex-col items-center justify-center bg-white dark:bg-zinc-950 animate-in fade-in duration-200">
|
||||
<div className="flex flex-col items-center gap-4 p-6 text-center">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 text-primary ring-4 ring-primary/5 animate-pulse">
|
||||
<ImagePlus className="h-6 w-6" />
|
||||
@ -5737,7 +6004,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
<History className="h-4 w-4" />
|
||||
</Button>
|
||||
{isSessionMenuOpen && (
|
||||
<div className="absolute right-0 top-10 w-64 rounded-xl border bg-background/95 backdrop-blur-sm shadow-xl p-2">
|
||||
<div className="absolute right-0 top-10 w-64 rounded-xl border bg-white dark:bg-zinc-950 shadow-xl p-2">
|
||||
<div className="px-2 py-1 text-xs text-muted-foreground">聊天窗口记录</div>
|
||||
<div className="max-h-56 overflow-y-auto space-y-1">
|
||||
{chatSessions.length === 0 ? (
|
||||
@ -6187,6 +6454,19 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
setFullscreenPreview({ url });
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute bottom-2 right-2 inline-flex items-center gap-1 rounded-md bg-black/70 px-2 py-1 text-[11px] font-medium text-white hover:bg-black/80"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
void handleDownloadOriginalImage(url);
|
||||
}}
|
||||
title="下载原图"
|
||||
>
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
原图
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -6310,6 +6590,18 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="absolute top-4 right-16 inline-flex items-center gap-1 rounded-full bg-white/10 px-3 py-2 text-sm text-white hover:bg-white/20 transition-colors"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
void handleDownloadOriginalImage(fullscreenPreview.url);
|
||||
}}
|
||||
aria-label="下载原图"
|
||||
title="下载原图"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
下载原图
|
||||
</button>
|
||||
|
||||
<img
|
||||
src={fullscreenPreview.url}
|
||||
@ -6326,7 +6618,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
)}
|
||||
|
||||
{/* 底部输入区域 - 混合输入(图片标签+文字可自由混合) */}
|
||||
<div className="p-4 bg-background/95 backdrop-blur-sm border-t">
|
||||
<div className="p-4 bg-white dark:bg-zinc-950 border-t">
|
||||
<div
|
||||
className="relative flex flex-col bg-card rounded-3xl border transition-all duration-200 focus-within:border-primary/50 focus-within:bg-background"
|
||||
data-canvas-chat-input-anchor="true"
|
||||
@ -6334,7 +6626,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
{isDetailGenerationMode && (
|
||||
<div
|
||||
className={cn(
|
||||
"order-3 mx-3 mb-11 overflow-y-auto rounded-2xl border border-black/10 bg-white/95 p-3 pr-2",
|
||||
"order-3 mx-3 mb-11 overflow-y-auto rounded-2xl border border-black/10 bg-white p-3 pr-2",
|
||||
"mt-1",
|
||||
renderableVisibleMessages.length === 0 ? "max-h-[62vh]" : "max-h-[54vh]"
|
||||
)}
|
||||
@ -7066,7 +7358,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
ref={textareaRef as any}
|
||||
contentEditable={!isSubmitting}
|
||||
suppressContentEditableWarning
|
||||
className="min-h-[40px] max-h-[120px] overflow-y-auto text-sm outline-none empty:before:content-[attr(data-placeholder)] empty:before:text-muted-foreground/50"
|
||||
className="min-h-[40px] max-h-[120px] overflow-y-auto text-sm outline-none empty:before:content-[attr(data-placeholder)] empty:before:text-[#9aa1ad]"
|
||||
data-placeholder={
|
||||
isDetailGenerationMode
|
||||
? "可选补充:风格、情绪氛围、禁用元素、期望生成张数(如:生成8张)"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user