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 { useEffect, useRef, useState, useCallback, useMemo } from "react";
import { useSession } from "next-auth/react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { createPortal } from "react-dom";
import type { DetailUiProtocol } from "@/lib/detail-ui-protocol";
import type { DetailUiScreenReviewPatch } from "@/lib/detail-ui-protocol";
@ -1209,6 +1210,16 @@ interface CanvasChatSession {
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({
chatId,
initialMessages = [],
@ -1234,6 +1245,13 @@ export function CanvasChatPanel({
);
const effectiveAgentEnabled = agentEnabled && !isVideoWorkspace;
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 [attachments, setAttachments] = useState<PickedObjectAttachment[]>([]);
const [selectedModelId, setSelectedModelId] = useState<string>(
@ -1299,8 +1317,10 @@ export function CanvasChatPanel({
});
const [detailModeMessageStartIndex, setDetailModeMessageStartIndex] = useState(0);
const canvasId = useMemo(() => toRawCanvasChatId(chatId), [chatId]);
const [activeChatId, setActiveChatId] = useState<string>(chatId);
const activeChatIdRef = useRef<string>(chatId);
const [activeChatId, setActiveChatId] = useState<string>(
() => requestedChatSessionId || chatId
);
const activeChatIdRef = useRef<string>(requestedChatSessionId || chatId);
const inFlightChatIdRef = useRef<string | null>(null);
const inFlightRawChatIdRef = useRef<string | null>(null);
const [chatSessions, setChatSessions] = useState<CanvasChatSession[]>([]);
@ -1334,10 +1354,29 @@ export function CanvasChatPanel({
const pendingLaunchRef = useRef<CanvasLaunchPayload | null>(null);
const [launchAutoSendTick, setLaunchAutoSendTick] = useState(0);
const hasInitializedChatStateRef = useRef(false);
const [loadedChatSessionStorageKey, setLoadedChatSessionStorageKey] = useState<string | null>(
null
);
const hasInitializedVideoWorkspaceRef = useRef(false);
const sessionMenuRef = useRef<HTMLDivElement>(null);
const historyScrollTimerRef = useRef<ReturnType<typeof setTimeout> | 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);
@ -1657,10 +1696,20 @@ export function CanvasChatPanel({
createdAt: 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 {
const key = getSessionStorageKey(canvasId, chatScope);
const saved = localStorage.getItem(key);
const saved = localStorage.getItem(sessionStorageKey);
const parsed = saved ? (JSON.parse(saved) as CanvasChatSession[]) : [];
const sanitized = parsed
@ -1672,21 +1721,33 @@ export function CanvasChatPanel({
updatedAt: Number(session.updatedAt) || now,
}));
const hasCurrent = sanitized.some((session) => session.id === rootChatId);
const nextSessions = hasCurrent
? sanitized
: [fallbackSession, ...sanitized];
let nextSessions = sanitized;
const requiredSessions = requestedSession
? [fallbackSession, requestedSession]
: [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);
setChatSessions(sorted);
setActiveChatId(rootChatId);
localStorage.setItem(key, JSON.stringify(sorted));
setActiveChatId(initialChatId);
localStorage.setItem(sessionStorageKey, JSON.stringify(sorted));
setLoadedChatSessionStorageKey(sessionStorageKey);
} catch (error) {
console.error("[CanvasChat] Failed to load chat sessions:", error);
setChatSessions([fallbackSession]);
setActiveChatId(rootChatId);
setChatSessions(
requestedSession ? [requestedSession, fallbackSession] : [fallbackSession]
);
setActiveChatId(initialChatId);
setLoadedChatSessionStorageKey(sessionStorageKey);
}
}, [canvasId, chatId, chatScope]);
}, [canvasId, chatId, chatScope, requestedChatSessionId]);
useEffect(() => {
if (!isSessionMenuOpen) return;
@ -1700,15 +1761,18 @@ export function CanvasChatPanel({
}, [isSessionMenuOpen]);
useEffect(() => {
const sessionStorageKey = getSessionStorageKey(canvasId, chatScope);
if (loadedChatSessionStorageKey !== sessionStorageKey) return;
try {
localStorage.setItem(
getSessionStorageKey(canvasId, chatScope),
sessionStorageKey,
JSON.stringify(chatSessions)
);
} catch (error) {
console.error("[CanvasChat] Failed to save chat sessions:", error);
}
}, [canvasId, chatScope, chatSessions]);
}, [canvasId, chatScope, chatSessions, loadedChatSessionStorageKey]);
// Canvas store
const addNode = useCanvasStore((state) => state.addNode);
@ -10075,8 +10139,9 @@ export function CanvasChatPanel({
releaseSendGuard(activeChatId);
setChatSessions((prevSessions) => [newSession, ...prevSessions]);
setActiveChatId(newChatSessionId);
replaceChatSessionQuery(newChatSessionId);
toast.success("已新建聊天窗口");
}, [activeChatId, chatSessions.length, releaseSendGuard]);
}, [activeChatId, chatSessions.length, releaseSendGuard, replaceChatSessionQuery]);
const switchToSession = useCallback((sessionId: string) => {
if (!sessionId || sessionId === activeChatId) {
@ -10086,9 +10151,10 @@ export function CanvasChatPanel({
forceHistoryHydrationRef.current = toRawCanvasChatId(sessionId);
releaseSendGuard(activeChatId);
setActiveChatId(sessionId);
replaceChatSessionQuery(sessionId);
setIsSessionMenuOpen(false);
toast.success("已切换聊天窗口");
}, [activeChatId, releaseSendGuard]);
}, [activeChatId, releaseSendGuard, replaceChatSessionQuery]);
const activeSessionTitle = useMemo(() => {
const current = chatSessions.find((session) => session.id === activeChatId);