fix: persist canvas chat session selection
This commit is contained in:
parent
8a2483ad7d
commit
f2ed508cb4
@ -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,10 +1354,29 @@ 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);
|
||||||
@ -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);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user