diff --git a/app/api/canvas/clawd-chat/route.ts b/app/api/canvas/clawd-chat/route.ts index 14c458aaa..750c074a2 100644 --- a/app/api/canvas/clawd-chat/route.ts +++ b/app/api/canvas/clawd-chat/route.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { auth } from "@/app/(auth)/auth"; import { getChatById, + getMessagesByChatId, getSystemSetting, saveChat, saveMessages, @@ -91,7 +92,6 @@ export async function POST(request: Request) { const userMessageId = parsed.data.userMessageId || generateUUID(); const assistantMessageId = generateUUID(); - const now = new Date(); const conversationId = createConversationId(session.user.id, parsed.data.id); const endpoint = `${config.baseUrl}/api/chat`; @@ -159,13 +159,28 @@ export async function POST(request: Request) { ); } + const existingMessages = await getMessagesByChatId({ id: parsed.data.id }).catch( + () => [] + ); + const latestMessageTimestamp = existingMessages.reduce((latest, item) => { + const timestamp = + item.createdAt instanceof Date + ? item.createdAt.getTime() + : Date.parse(String(item.createdAt || "")); + return Number.isFinite(timestamp) ? Math.max(latest, timestamp) : latest; + }, 0); + const userCreatedAt = new Date( + Math.max(Date.now(), latestMessageTimestamp + 1) + ); + const assistantCreatedAt = new Date(userCreatedAt.getTime() + 1); + const userMessage = { id: userMessageId, chatId: parsed.data.id, role: "user", parts: [{ type: "text", text: parsed.data.message }], attachments: [], - createdAt: now, + createdAt: userCreatedAt, videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, @@ -178,7 +193,7 @@ export async function POST(request: Request) { role: "assistant", parts: [{ type: "text", text: answer }], attachments: [], - createdAt: new Date(now.getTime() + 1), + createdAt: assistantCreatedAt, videoStorageStatus: null, temporaryVideoUrl: null, permanentVideoUrl: null, @@ -199,12 +214,14 @@ export async function POST(request: Request) { id: userMessage.id, role: "user", parts: userMessage.parts, + createdAt: userMessage.createdAt.toISOString(), metadata: { createdAt: userMessage.createdAt.toISOString() }, }, { id: assistantMessage.id, role: "assistant", parts: assistantMessage.parts, + createdAt: assistantMessage.createdAt.toISOString(), metadata: { createdAt: assistantMessage.createdAt.toISOString() }, }, ], diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index e3daa4029..93accbac3 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -3367,6 +3367,23 @@ export function CanvasChatPanel({ : isPresetAgentSelection ? getToolbarItem("chatModePresetAgent").label : "Agent"; + const getComposerModeMenuItemClass = useCallback( + (mode: CanvasComposerMode) => + cn( + "h-11 cursor-pointer rounded-[12px] px-3 text-[16px] font-normal outline-none transition-colors focus:text-[#1f2937] dark:focus:text-white [&_svg]:!size-5", + isVideoWorkspace + ? "text-foreground focus:bg-background/80" + : "text-[#2e3238] focus:bg-[#eef0f3] dark:text-zinc-100 dark:focus:bg-white/12", + activeComposerMode === mode + ? isVideoWorkspace + ? "bg-background text-foreground" + : "bg-[#eef0f3] text-[#1f2937] dark:bg-white/12 dark:text-white" + : isVideoWorkspace + ? "hover:bg-background/80" + : "hover:bg-[#f5f6f7] dark:hover:bg-white/8" + ), + [activeComposerMode, isVideoWorkspace] + ); const shouldMaskAgentRuntimeModelName = activeComposerMode === "agent" || selectedModelId === "auto"; const getAgentSafeModelName = useCallback( @@ -8536,11 +8553,19 @@ export function CanvasChatPanel({ } const userMessageId = generateUUID(); + const latestMessageTimestamp = messages.reduce( + (latest, message) => Math.max(latest, getMessageTimestamp(message) ?? 0), + 0 + ); + const optimisticCreatedAt = new Date( + Math.max(Date.now(), latestMessageTimestamp + 1) + ); const optimisticUserMessage: ChatMessage = { id: userMessageId, role: "user", parts: [{ type: "text", text: textContent }], - metadata: { createdAt: new Date().toISOString() }, + createdAt: optimisticCreatedAt, + metadata: { createdAt: optimisticCreatedAt.toISOString() }, } as ChatMessage; setSendGuard(activeChatId, { @@ -8555,6 +8580,14 @@ export function CanvasChatPanel({ optimisticUserMessage, ]) ); + setInput(""); + setAttachments([]); + clearSelection(); + + const editableDiv = textareaRef.current as HTMLDivElement | null; + if (editableDiv) { + editableDiv.innerHTML = ""; + } try { const response = await fetch("/api/canvas/clawd-chat", { @@ -8594,17 +8627,19 @@ export function CanvasChatPanel({ void mutate(buildHistoryMessagesUrl(rawChatId, { cacheBust: true })); } hasHydratedHistoryRef.current = true; - setInput(""); - setAttachments([]); - clearSelection(); - - const editableDiv = textareaRef.current as HTMLDivElement | null; - if (editableDiv) { - editableDiv.innerHTML = ""; - } } catch (error) { console.error("[CanvasChat] Clawd request failed:", error); toast.error(error instanceof Error ? error.message : "Clawd 请求失败"); + const currentEditorText = (editableDiv?.textContent || "") + .replace(/\u200b/g, " ") + .replace(/\s+/g, " ") + .trim(); + if (!currentEditorText) { + setInput(textContent); + if (editableDiv) { + editableDiv.textContent = textContent; + } + } } finally { setIsClawdSubmitting(false); releaseSendGuard(activeChatId); @@ -12448,7 +12483,7 @@ export function CanvasChatPanel({ side="top" sideOffset={10} className={cn( - "w-[200px] rounded-[18px] border border-[#d8dce3] bg-white p-2 text-[#20242a] shadow-[0_22px_54px_rgba(15,23,42,0.16)]", + "w-[200px] space-y-1 rounded-[18px] border border-[#d8dce3] bg-white p-2 text-[#20242a] shadow-[0_22px_54px_rgba(15,23,42,0.16)]", isVideoWorkspace ? "border border-border/60 bg-background/95 shadow-[0_14px_30px_-20px_rgba(15,23,42,0.35)] backdrop-blur-xl" : "dark:border-white/10 dark:bg-zinc-900/95 dark:text-zinc-100" @@ -12457,19 +12492,7 @@ export function CanvasChatPanel({ {effectiveAgentEnabled ? ( handleSelectGenerationMode("agent")} - className={cn( - "h-11 cursor-pointer rounded-[12px] px-3 text-[16px] font-normal [&_svg]:!size-5", - isVideoWorkspace - ? "text-foreground" - : "text-[#2e3238] dark:text-zinc-100", - activeComposerMode === "agent" - ? isVideoWorkspace - ? "bg-background" - : "bg-transparent dark:bg-white/10" - : isVideoWorkspace - ? "hover:bg-background/80" - : "hover:bg-[#f5f6f7] dark:hover:bg-white/8" - )} + className={getComposerModeMenuItemClass("agent")} > handleSelectGenerationMode("clawd")} - className={cn( - "h-11 cursor-pointer rounded-[12px] px-3 text-[16px] font-normal [&_svg]:!size-5", - isVideoWorkspace - ? "text-foreground" - : "text-[#2e3238] dark:text-zinc-100", - activeComposerMode === "clawd" - ? isVideoWorkspace - ? "bg-background" - : "bg-transparent dark:bg-white/10" - : isVideoWorkspace - ? "hover:bg-background/80" - : "hover:bg-[#f5f6f7] dark:hover:bg-white/8" - )} + className={getComposerModeMenuItemClass("clawd")} > Clawd模式 @@ -12511,19 +12522,7 @@ export function CanvasChatPanel({ {canUseDetailGeneration ? ( handleSelectGenerationMode("detail")} - className={cn( - "h-11 cursor-pointer rounded-[12px] px-3 text-[16px] font-normal [&_svg]:!size-5", - isVideoWorkspace - ? "text-foreground" - : "text-[#2e3238] dark:text-zinc-100", - activeComposerMode === "detail" - ? isVideoWorkspace - ? "bg-background" - : "bg-transparent dark:bg-white/10" - : isVideoWorkspace - ? "hover:bg-background/80" - : "hover:bg-[#f5f6f7] dark:hover:bg-white/8" - )} + className={getComposerModeMenuItemClass("detail")} > handleSelectGenerationMode("image")} - className={cn( - "h-11 cursor-pointer rounded-[12px] px-3 text-[16px] font-normal [&_svg]:!size-5", - isVideoWorkspace - ? "text-foreground" - : "text-[#2e3238] dark:text-zinc-100", - activeComposerMode === "image" - ? isVideoWorkspace - ? "bg-background" - : "bg-transparent dark:bg-white/10" - : isVideoWorkspace - ? "hover:bg-background/80" - : "hover:bg-[#f5f6f7] dark:hover:bg-white/8" - )} + className={getComposerModeMenuItemClass("image")} > handleSelectGenerationMode("video")} - className={cn( - "h-11 cursor-pointer rounded-[12px] px-3 text-[16px] font-normal [&_svg]:!size-5", - isVideoWorkspace - ? "text-foreground" - : "text-[#2e3238] dark:text-zinc-100", - activeComposerMode === "video" - ? isVideoWorkspace - ? "bg-background" - : "bg-transparent dark:bg-white/10" - : isVideoWorkspace - ? "hover:bg-background/80" - : "hover:bg-[#f5f6f7] dark:hover:bg-white/8" - )} + className={getComposerModeMenuItemClass("video")} > ) + : null; const rawValue = (message as any).createdAt ?? (message as any).created_at ?? (message as any).timestamp ?? + metadata?.createdAt ?? + metadata?.created_at ?? + metadata?.timestamp ?? null; if (rawValue instanceof Date) {