fix: persist canvas chat session selection

This commit is contained in:
134 2026-06-23 23:07:21 +08:00
parent 8a2483ad7d
commit f2ed508cb4

View File

@ -4,6 +4,7 @@ import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai"; import { DefaultChatTransport } from "ai";
import { useEffect, useRef, useState, useCallback, useMemo } from "react"; import { useEffect, useRef, useState, useCallback, useMemo } from "react";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import type { DetailUiProtocol } from "@/lib/detail-ui-protocol"; import type { DetailUiProtocol } from "@/lib/detail-ui-protocol";
import type { DetailUiScreenReviewPatch } from "@/lib/detail-ui-protocol"; import type { DetailUiScreenReviewPatch } from "@/lib/detail-ui-protocol";
@ -1209,6 +1210,16 @@ interface CanvasChatSession {
updatedAt: number; updatedAt: number;
} }
const CANVAS_CHAT_QUERY_PARAM = "chat";
function normalizeCanvasChatSessionId(value?: string | null) {
const raw = String(value || "").trim();
if (!raw) return null;
const rawUuid = raw.startsWith("canvas-") ? raw.slice("canvas-".length) : raw;
return isValidChatUuid(rawUuid) ? `canvas-${rawUuid}` : null;
}
export function CanvasChatPanel({ export function CanvasChatPanel({
chatId, chatId,
initialMessages = [], initialMessages = [],
@ -1234,6 +1245,13 @@ export function CanvasChatPanel({
); );
const effectiveAgentEnabled = agentEnabled && !isVideoWorkspace; const effectiveAgentEnabled = agentEnabled && !isVideoWorkspace;
const { data: session } = useSession(); const { data: session } = useSession();
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const requestedChatSessionId = useMemo(
() => normalizeCanvasChatSessionId(searchParams.get(CANVAS_CHAT_QUERY_PARAM)),
[searchParams]
);
const [input, setInput] = useState(""); const [input, setInput] = useState("");
const [attachments, setAttachments] = useState<PickedObjectAttachment[]>([]); const [attachments, setAttachments] = useState<PickedObjectAttachment[]>([]);
const [selectedModelId, setSelectedModelId] = useState<string>( const [selectedModelId, setSelectedModelId] = useState<string>(
@ -1299,8 +1317,10 @@ export function CanvasChatPanel({
}); });
const [detailModeMessageStartIndex, setDetailModeMessageStartIndex] = useState(0); const [detailModeMessageStartIndex, setDetailModeMessageStartIndex] = useState(0);
const canvasId = useMemo(() => toRawCanvasChatId(chatId), [chatId]); const canvasId = useMemo(() => toRawCanvasChatId(chatId), [chatId]);
const [activeChatId, setActiveChatId] = useState<string>(chatId); const [activeChatId, setActiveChatId] = useState<string>(
const activeChatIdRef = useRef<string>(chatId); () => requestedChatSessionId || chatId
);
const activeChatIdRef = useRef<string>(requestedChatSessionId || chatId);
const inFlightChatIdRef = useRef<string | null>(null); const inFlightChatIdRef = useRef<string | null>(null);
const inFlightRawChatIdRef = useRef<string | null>(null); const inFlightRawChatIdRef = useRef<string | null>(null);
const [chatSessions, setChatSessions] = useState<CanvasChatSession[]>([]); const [chatSessions, setChatSessions] = useState<CanvasChatSession[]>([]);
@ -1334,11 +1354,30 @@ export function CanvasChatPanel({
const pendingLaunchRef = useRef<CanvasLaunchPayload | null>(null); const pendingLaunchRef = useRef<CanvasLaunchPayload | null>(null);
const [launchAutoSendTick, setLaunchAutoSendTick] = useState(0); const [launchAutoSendTick, setLaunchAutoSendTick] = useState(0);
const hasInitializedChatStateRef = useRef(false); const hasInitializedChatStateRef = useRef(false);
const [loadedChatSessionStorageKey, setLoadedChatSessionStorageKey] = useState<string | null>(
null
);
const hasInitializedVideoWorkspaceRef = useRef(false); const hasInitializedVideoWorkspaceRef = useRef(false);
const sessionMenuRef = useRef<HTMLDivElement>(null); const sessionMenuRef = useRef<HTMLDivElement>(null);
const historyScrollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); const historyScrollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const forceHistoryHydrationRef = useRef<string | null>(null); const forceHistoryHydrationRef = useRef<string | null>(null);
const replaceChatSessionQuery = useCallback(
(sessionId: string) => {
const normalized = normalizeCanvasChatSessionId(sessionId);
if (!normalized || typeof window === "undefined") return;
const params = new URLSearchParams(window.location.search);
params.set(CANVAS_CHAT_QUERY_PARAM, normalized);
const query = params.toString();
const hash = window.location.hash || "";
router.replace(`${pathname}${query ? `?${query}` : ""}${hash}`, {
scroll: false,
});
},
[pathname, router]
);
// 🆕 标签悬停预览状态 // 🆕 标签悬停预览状态
const [tagPreview, setTagPreview] = useState<TagPreviewInfo | null>(null); const [tagPreview, setTagPreview] = useState<TagPreviewInfo | null>(null);
const previewTimeoutRef = useRef<NodeJS.Timeout | null>(null); const previewTimeoutRef = useRef<NodeJS.Timeout | null>(null);
@ -1657,10 +1696,20 @@ export function CanvasChatPanel({
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
}; };
const requestedSession: CanvasChatSession | null =
requestedChatSessionId && requestedChatSessionId !== rootChatId
? {
id: requestedChatSessionId,
title: "新对话",
createdAt: now,
updatedAt: now,
}
: null;
const initialChatId = requestedChatSessionId || rootChatId;
const sessionStorageKey = getSessionStorageKey(canvasId, chatScope);
try { try {
const key = getSessionStorageKey(canvasId, chatScope); const saved = localStorage.getItem(sessionStorageKey);
const saved = localStorage.getItem(key);
const parsed = saved ? (JSON.parse(saved) as CanvasChatSession[]) : []; const parsed = saved ? (JSON.parse(saved) as CanvasChatSession[]) : [];
const sanitized = parsed const sanitized = parsed
@ -1672,21 +1721,33 @@ export function CanvasChatPanel({
updatedAt: Number(session.updatedAt) || now, updatedAt: Number(session.updatedAt) || now,
})); }));
const hasCurrent = sanitized.some((session) => session.id === rootChatId); let nextSessions = sanitized;
const nextSessions = hasCurrent const requiredSessions = requestedSession
? sanitized ? [fallbackSession, requestedSession]
: [fallbackSession, ...sanitized]; : [fallbackSession];
for (const requiredSession of requiredSessions) {
const hasSession = nextSessions.some(
(session) => session.id === requiredSession.id
);
if (!hasSession) {
nextSessions = [requiredSession, ...nextSessions];
}
}
const sorted = [...nextSessions].sort((a, b) => b.updatedAt - a.updatedAt); const sorted = [...nextSessions].sort((a, b) => b.updatedAt - a.updatedAt);
setChatSessions(sorted); setChatSessions(sorted);
setActiveChatId(rootChatId); setActiveChatId(initialChatId);
localStorage.setItem(key, JSON.stringify(sorted)); localStorage.setItem(sessionStorageKey, JSON.stringify(sorted));
setLoadedChatSessionStorageKey(sessionStorageKey);
} catch (error) { } catch (error) {
console.error("[CanvasChat] Failed to load chat sessions:", error); console.error("[CanvasChat] Failed to load chat sessions:", error);
setChatSessions([fallbackSession]); setChatSessions(
setActiveChatId(rootChatId); requestedSession ? [requestedSession, fallbackSession] : [fallbackSession]
);
setActiveChatId(initialChatId);
setLoadedChatSessionStorageKey(sessionStorageKey);
} }
}, [canvasId, chatId, chatScope]); }, [canvasId, chatId, chatScope, requestedChatSessionId]);
useEffect(() => { useEffect(() => {
if (!isSessionMenuOpen) return; if (!isSessionMenuOpen) return;
@ -1700,15 +1761,18 @@ export function CanvasChatPanel({
}, [isSessionMenuOpen]); }, [isSessionMenuOpen]);
useEffect(() => { useEffect(() => {
const sessionStorageKey = getSessionStorageKey(canvasId, chatScope);
if (loadedChatSessionStorageKey !== sessionStorageKey) return;
try { try {
localStorage.setItem( localStorage.setItem(
getSessionStorageKey(canvasId, chatScope), sessionStorageKey,
JSON.stringify(chatSessions) JSON.stringify(chatSessions)
); );
} catch (error) { } catch (error) {
console.error("[CanvasChat] Failed to save chat sessions:", error); console.error("[CanvasChat] Failed to save chat sessions:", error);
} }
}, [canvasId, chatScope, chatSessions]); }, [canvasId, chatScope, chatSessions, loadedChatSessionStorageKey]);
// Canvas store // Canvas store
const addNode = useCanvasStore((state) => state.addNode); const addNode = useCanvasStore((state) => state.addNode);
@ -10075,8 +10139,9 @@ export function CanvasChatPanel({
releaseSendGuard(activeChatId); releaseSendGuard(activeChatId);
setChatSessions((prevSessions) => [newSession, ...prevSessions]); setChatSessions((prevSessions) => [newSession, ...prevSessions]);
setActiveChatId(newChatSessionId); setActiveChatId(newChatSessionId);
replaceChatSessionQuery(newChatSessionId);
toast.success("已新建聊天窗口"); toast.success("已新建聊天窗口");
}, [activeChatId, chatSessions.length, releaseSendGuard]); }, [activeChatId, chatSessions.length, releaseSendGuard, replaceChatSessionQuery]);
const switchToSession = useCallback((sessionId: string) => { const switchToSession = useCallback((sessionId: string) => {
if (!sessionId || sessionId === activeChatId) { if (!sessionId || sessionId === activeChatId) {
@ -10086,9 +10151,10 @@ export function CanvasChatPanel({
forceHistoryHydrationRef.current = toRawCanvasChatId(sessionId); forceHistoryHydrationRef.current = toRawCanvasChatId(sessionId);
releaseSendGuard(activeChatId); releaseSendGuard(activeChatId);
setActiveChatId(sessionId); setActiveChatId(sessionId);
replaceChatSessionQuery(sessionId);
setIsSessionMenuOpen(false); setIsSessionMenuOpen(false);
toast.success("已切换聊天窗口"); toast.success("已切换聊天窗口");
}, [activeChatId, releaseSendGuard]); }, [activeChatId, releaseSendGuard, replaceChatSessionQuery]);
const activeSessionTitle = useMemo(() => { const activeSessionTitle = useMemo(() => {
const current = chatSessions.find((session) => session.id === activeChatId); const current = chatSessions.find((session) => session.id === activeChatId);