diff --git a/app/api/canvas/clawd-chat/history/route.ts b/app/api/canvas/clawd-chat/history/route.ts index 97bbf669d..28d829e49 100644 --- a/app/api/canvas/clawd-chat/history/route.ts +++ b/app/api/canvas/clawd-chat/history/route.ts @@ -25,7 +25,7 @@ export async function GET(request: Request) { const url = new URL(request.url); const limit = Math.max( 1, - Math.min(100, Math.round(Number(url.searchParams.get("limit") || 50))) + Math.min(500, Math.round(Number(url.searchParams.get("limit") || 100))) ); const chats = await getClawdChatsByUserId({ diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index ca46bb4b5..51f7259ee 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -271,11 +271,14 @@ import { isGenericCanvasPrompt, isValidChatHttpUrl, isValidChatUuid, + mergeClawdHistoryIntoCanvasSessions, MAX_CHAT_AUTO_CONTEXT_LENGTH, MAX_CHAT_HIDDEN_PROMPT_LENGTH, MAX_CHAT_TEXT_PART_LENGTH, normalizeChatAttachmentName, normalizeChatMediaType, + type CanvasChatSessionRecord, + type ClawdChatHistoryRecord, type PickedObjectAttachment, sanitizeChatAttachment, toRawCanvasChatId, @@ -3008,23 +3011,11 @@ type MessageVote = { isUpvoted: boolean; }; -interface CanvasChatSession { - id: string; - title: string; +type CanvasChatSession = Omit & { mode: CanvasComposerMode; - createdAt: number; - updatedAt: number; - clawdSkillTagId?: string | null; - clawdSkillTagPromptApplied?: boolean; -} +}; -interface ClawdHistoryItem { - id: string; - title: string; - createdAt: string | Date; - updatedAt: string | Date; - messageCount: number; -} +type ClawdHistoryItem = ClawdChatHistoryRecord; const CANVAS_CHAT_QUERY_PARAM = "chat"; const CANVAS_IMAGE_NODE_MANUAL_UPLOAD_EVENT = "canvas:image-node-manual-upload"; @@ -5978,18 +5969,26 @@ export function CanvasChatPanel({ activeComposerMode ); const clawdHistorySWRKey = - isClawdMode && isClawdHistoryMenuOpen - ? "/api/canvas/clawd-chat/history?limit=80" + isClawdMode && session?.user?.id + ? "/api/canvas/clawd-chat/history?limit=500" : null; const { data: clawdHistoryData, + error: clawdHistoryError, isLoading: isClawdHistoryLoading, mutate: mutateClawdHistory, } = useSWR<{ chats: ClawdHistoryItem[] }>(clawdHistorySWRKey, fetcher, { - revalidateOnFocus: false, - revalidateOnReconnect: false, + revalidateOnFocus: true, + revalidateOnReconnect: true, dedupingInterval: 5000, }); + useEffect(() => { + const persistedChats = clawdHistoryData?.chats; + if (!persistedChats) return; + updateChatSessions((previousSessions) => + mergeClawdHistoryIntoCanvasSessions(previousSessions, persistedChats) + ); + }, [clawdHistoryData?.chats, updateChatSessions]); useEffect(() => { if (activeChatSessionMode !== "clawd") return; if (!isClawdModeEnabled) return; @@ -15523,6 +15522,14 @@ export function CanvasChatPanel({ 正在加载 Claw 历史 + ) : clawdHistoryError ? ( + ) : (clawdHistoryData?.chats || []).length === 0 ? (
暂无 Claw 历史 diff --git a/components/canvas/chat-panel/OpenClawInteractiveModelViewer.tsx b/components/canvas/chat-panel/OpenClawInteractiveModelViewer.tsx index cc8117a9f..a1fcea2ed 100644 --- a/components/canvas/chat-panel/OpenClawInteractiveModelViewer.tsx +++ b/components/canvas/chat-panel/OpenClawInteractiveModelViewer.tsx @@ -38,10 +38,6 @@ type AnimationState = { starts: Map; targets: Map; easing: string; - cameraStart: THREE.Vector3; - cameraTarget: THREE.Vector3; - controlsTargetStart: THREE.Vector3; - controlsTargetEnd: THREE.Vector3; }; type DragState = { @@ -199,51 +195,6 @@ function ModelInteractionScene({ const currentAmountRef = useRef(0); const selectionHelperRef = useRef(null); - const getCameraFrame = ( - starts: Map, - targets: Map - ) => { - for (const [nodeName, target] of targets) { - entriesRef.current.get(nodeName)?.node.position.copy(target); - } - scene.updateMatrixWorld(true); - const bounds = new THREE.Box3().setFromObject(scene); - for (const [nodeName, start] of starts) { - entriesRef.current.get(nodeName)?.node.position.copy(start); - } - scene.updateMatrixWorld(true); - - const controlsTargetStart = controlsRef.current?.target?.clone?.() || - new THREE.Vector3(); - if (bounds.isEmpty()) { - return { - cameraTarget: camera.position.clone(), - controlsTargetEnd: controlsTargetStart.clone(), - }; - } - const sphere = bounds.getBoundingSphere(new THREE.Sphere()); - const perspectiveCamera = camera as THREE.PerspectiveCamera; - const verticalHalfFov = THREE.MathUtils.degToRad(perspectiveCamera.fov) / 2; - const horizontalHalfFov = Math.atan( - Math.tan(verticalHalfFov) * perspectiveCamera.aspect - ); - const limitingHalfFov = Math.max( - 0.1, - Math.min(verticalHalfFov, horizontalHalfFov) - ); - const distance = Math.max( - 1.2, - (sphere.radius / Math.sin(limitingHalfFov)) * 1.12 - ); - const direction = camera.position.clone().sub(controlsTargetStart); - if (direction.lengthSq() < 0.0001) direction.set(1, 0.75, 1); - direction.normalize(); - return { - cameraTarget: sphere.center.clone().addScaledVector(direction, distance), - controlsTargetEnd: sphere.center.clone(), - }; - }; - useEffect(() => { const partsByNodeName = new Map( manifest.parts.map((part) => [part.nodeName, part]) @@ -306,19 +257,11 @@ function ModelInteractionScene({ starts.set(nodeName, entry.node.position.clone()); targets.set(nodeName, getPartTargetPosition(entry, manifest, amount)); } - const cameraFrame = getCameraFrame(starts, targets); if (!transformRequest.animate) { animationRef.current = null; for (const [nodeName, target] of targets) { entriesRef.current.get(nodeName)?.node.position.copy(target); } - camera.position.copy(cameraFrame.cameraTarget); - if (controlsRef.current) { - controlsRef.current.target.copy(cameraFrame.controlsTargetEnd); - controlsRef.current.update(); - } else { - camera.lookAt(cameraFrame.controlsTargetEnd); - } return; } const targetState = amount >= 0.5 ? "exploded" : "assembled"; @@ -331,13 +274,8 @@ function ModelInteractionScene({ starts, targets, easing: action?.easing || "easeInOutCubic", - cameraStart: camera.position.clone(), - cameraTarget: cameraFrame.cameraTarget, - controlsTargetStart: - controlsRef.current?.target?.clone?.() || new THREE.Vector3(), - controlsTargetEnd: cameraFrame.controlsTargetEnd, }; - }, [camera, manifest, scene, transformRequest]); + }, [manifest, transformRequest]); useEffect(() => { if (resetAllToken <= 0) return; @@ -507,21 +445,6 @@ function ModelInteractionScene({ const start = animation.starts.get(nodeName); if (entry && start) entry.node.position.lerpVectors(start, target, progress); } - camera.position.lerpVectors( - animation.cameraStart, - animation.cameraTarget, - progress - ); - if (controlsRef.current) { - controlsRef.current.target.lerpVectors( - animation.controlsTargetStart, - animation.controlsTargetEnd, - progress - ); - controlsRef.current.update(); - } else { - camera.lookAt(animation.controlsTargetEnd); - } if (rawProgress >= 1) animationRef.current = null; }); diff --git a/components/canvas/chat-panel/OpenClawModel3DPreview.tsx b/components/canvas/chat-panel/OpenClawModel3DPreview.tsx index 547d83018..9ef95ead6 100644 --- a/components/canvas/chat-panel/OpenClawModel3DPreview.tsx +++ b/components/canvas/chat-panel/OpenClawModel3DPreview.tsx @@ -12,7 +12,14 @@ import { Undo2, X, } from "lucide-react"; -import { useCallback, useEffect, useId, useMemo, useState } from "react"; +import { + useCallback, + useEffect, + useId, + useMemo, + useRef, + useState, +} from "react"; import { buildOpenClawArtifactProxyUrl, getOpenClaw3DArtifactVariant, @@ -24,6 +31,11 @@ import { type OpenClawModelInteractionManifest, type OpenClawModelInteractionPart, } from "@/lib/openclaw-chat/model-interaction"; +import { + clearOpenClaw3DViewerSession, + readOpenClaw3DViewerSession, + writeOpenClaw3DViewerSession, +} from "@/lib/openclaw-chat/viewer-session"; const Model3DViewer = dynamic( () => @@ -70,6 +82,14 @@ function supportsWebGl() { } } +function getViewerSessionStorage() { + try { + return window.sessionStorage; + } catch { + return null; + } +} + function formatDimensions(part: OpenClawModelInteractionPart) { const dimensions = part.dimensionsMm; if (!dimensions) return null; @@ -110,12 +130,23 @@ export function OpenClawModel3DPreview({ durationMs: 700, nonce: 0, }); + const hasRestoredSessionRef = useRef(false); + const restoredSelectedNodeNameRef = useRef(null); const closeViewer = useCallback(() => { setIsActive(false); setSelectedPart(null); setMatchState({ matched: 0, total: 0 }); - }, []); + if (typeof window !== "undefined") { + const storage = getViewerSessionStorage(); + if (!storage) return; + clearOpenClaw3DViewerSession({ + storage, + chatId, + messageId, + }); + } + }, [chatId, messageId]); const handleMatchChange = useCallback((matched: number, total: number) => { setMatchState((current) => current.matched === matched && current.total === total @@ -124,6 +155,63 @@ export function OpenClawModel3DPreview({ ); }, []); + useEffect(() => { + if (hasRestoredSessionRef.current || !model) return; + hasRestoredSessionRef.current = true; + const storage = getViewerSessionStorage(); + if (!storage) return; + const session = readOpenClaw3DViewerSession({ + storage, + chatId, + }); + if (session?.messageId !== messageId) return; + if (!supportsWebGl()) { + setWebGlUnavailable(true); + clearOpenClaw3DViewerSession({ + storage, + chatId, + messageId, + }); + return; + } + + restoredSelectedNodeNameRef.current = session.selectedNodeName; + setExplodeAmount(session.explodeAmount); + setTransformRequest((current) => ({ + actionId: null, + amount: session.explodeAmount, + animate: false, + durationMs: model.viewer?.transitionDurationMs || 700, + nonce: current.nonce + 1, + })); + window.dispatchEvent( + new CustomEvent(ACTIVE_VIEWER_EVENT, { detail: viewerId }) + ); + setIsActive(true); + }, [chatId, messageId, model, viewerId]); + + useEffect(() => { + if (!isActive) return; + const storage = getViewerSessionStorage(); + if (!storage) return; + writeOpenClaw3DViewerSession({ + storage, + chatId, + messageId, + explodeAmount, + selectedNodeName: selectedPart?.nodeName || null, + }); + }, [chatId, explodeAmount, isActive, messageId, selectedPart?.nodeName]); + + useEffect(() => { + const nodeName = restoredSelectedNodeNameRef.current; + if (!manifest || !nodeName) return; + restoredSelectedNodeNameRef.current = null; + setSelectedPart( + manifest.parts.find((part) => part.nodeName === nodeName) || null + ); + }, [manifest]); + useEffect(() => { const handleOtherViewerActivated = (event: Event) => { const activeId = (event as CustomEvent).detail; @@ -348,37 +436,55 @@ export function OpenClawModel3DPreview({
- {canExplode ? ( - - ) : null} - {canAssemble ? ( - + {canAssemble ? ( + + ) : null} + {canExplode ? ( + + ) : null} +
) : null} {selectedPart && canResetParts ? (