重构聊天会话管理,支持会话历史和存储
This commit is contained in:
@@ -9,16 +9,23 @@ import type {
|
||||
BranchGroup,
|
||||
BranchTransition,
|
||||
ChatProgress,
|
||||
ChatSessionSummary,
|
||||
LoadedChatState,
|
||||
Message,
|
||||
PersistedChatState,
|
||||
} from "../GlobalChatbox.types";
|
||||
import {
|
||||
CHAT_STORAGE_KEY,
|
||||
cloneBranchGroups,
|
||||
cloneMessages,
|
||||
createId,
|
||||
getInitialChatState,
|
||||
} from "../GlobalChatbox.utils";
|
||||
import {
|
||||
createEmptyChatSession,
|
||||
deleteChatSession,
|
||||
listChatSessions,
|
||||
loadActiveChatState,
|
||||
loadChatSessionById,
|
||||
saveActiveChatState,
|
||||
} from "../chatStorage";
|
||||
|
||||
type UseAgentChatSessionOptions = {
|
||||
onToolCall: (
|
||||
@@ -88,24 +95,20 @@ export const useAgentChatSession = ({
|
||||
onToolCall,
|
||||
onBeforeSend,
|
||||
}: UseAgentChatSessionOptions) => {
|
||||
const initialChatStateRef = useRef<PersistedChatState | null>(null);
|
||||
if (initialChatStateRef.current === null) {
|
||||
initialChatStateRef.current = getInitialChatState();
|
||||
}
|
||||
const storageSessionIdRef = useRef<string | undefined>(undefined);
|
||||
const hydrationCompletedRef = useRef(false);
|
||||
const hydrationNonceRef = useRef(0);
|
||||
|
||||
const [messages, setMessages] = useState<Message[]>(
|
||||
initialChatStateRef.current.messages,
|
||||
);
|
||||
const [sessionId, setSessionId] = useState<string | undefined>(
|
||||
initialChatStateRef.current.sessionId,
|
||||
);
|
||||
const [branchGroups, setBranchGroups] = useState<BranchGroup[]>(
|
||||
initialChatStateRef.current.branchGroups ?? [],
|
||||
);
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [sessionTitle, setSessionTitle] = useState<string | undefined>(undefined);
|
||||
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
|
||||
const [branchGroups, setBranchGroups] = useState<BranchGroup[]>([]);
|
||||
const [chatSessions, setChatSessions] = useState<ChatSessionSummary[]>([]);
|
||||
const [branchTransition, setBranchTransition] = useState<BranchTransition | null>(null);
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
const [isHydrating, setIsHydrating] = useState(true);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
const sessionIdRef = useRef<string | undefined>(initialChatStateRef.current.sessionId);
|
||||
const sessionIdRef = useRef<string | undefined>(undefined);
|
||||
const cancelPromiseRef = useRef<Promise<void> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -113,13 +116,74 @@ export const useAgentChatSession = ({
|
||||
}, [sessionId]);
|
||||
|
||||
useEffect(() => {
|
||||
const state: PersistedChatState = { messages, sessionId, branchGroups };
|
||||
try {
|
||||
window.localStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify(state));
|
||||
} catch (error) {
|
||||
console.error("[GlobalChatbox] Failed to persist chat state:", error);
|
||||
}
|
||||
}, [branchGroups, messages, sessionId]);
|
||||
let cancelled = false;
|
||||
|
||||
const hydrate = async () => {
|
||||
try {
|
||||
const [loadedState, sessions] = await Promise.all([
|
||||
loadActiveChatState(),
|
||||
listChatSessions(),
|
||||
]);
|
||||
if (cancelled) return;
|
||||
|
||||
storageSessionIdRef.current = loadedState.storageSessionId;
|
||||
sessionIdRef.current = loadedState.sessionId;
|
||||
hydrationCompletedRef.current = true;
|
||||
hydrationNonceRef.current += 1;
|
||||
|
||||
setMessages(loadedState.messages);
|
||||
setSessionTitle(loadedState.title);
|
||||
setSessionId(loadedState.sessionId);
|
||||
setBranchGroups(loadedState.branchGroups);
|
||||
setChatSessions(sessions);
|
||||
} catch (error) {
|
||||
console.error("[GlobalChatbox] Failed to hydrate chat state:", error);
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setIsHydrating(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void hydrate();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isHydrating || !hydrationCompletedRef.current) return;
|
||||
|
||||
const currentHydrationNonce = hydrationNonceRef.current;
|
||||
const persistTimer = window.setTimeout(() => {
|
||||
const state: LoadedChatState = {
|
||||
storageSessionId: storageSessionIdRef.current,
|
||||
title: sessionTitle,
|
||||
messages,
|
||||
sessionId,
|
||||
branchGroups,
|
||||
};
|
||||
|
||||
void saveActiveChatState(state)
|
||||
.then((storageSessionId) => {
|
||||
if (hydrationNonceRef.current !== currentHydrationNonce) return;
|
||||
storageSessionIdRef.current = storageSessionId;
|
||||
return listChatSessions();
|
||||
})
|
||||
.then((sessions) => {
|
||||
if (!sessions || hydrationNonceRef.current !== currentHydrationNonce) return;
|
||||
setChatSessions(sessions);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("[GlobalChatbox] Failed to persist chat state:", error);
|
||||
});
|
||||
}, 150);
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(persistTimer);
|
||||
};
|
||||
}, [branchGroups, isHydrating, messages, sessionId, sessionTitle]);
|
||||
|
||||
useEffect(() => {
|
||||
setBranchGroups((prev) => {
|
||||
@@ -182,7 +246,7 @@ export const useAgentChatSession = ({
|
||||
assistantMessage,
|
||||
}: PromptRunOptions) => {
|
||||
const prompt = rawPrompt.trim();
|
||||
if (!prompt || isStreaming) return;
|
||||
if (!prompt || isStreaming || isHydrating) return;
|
||||
|
||||
await cancelPromiseRef.current?.catch(() => undefined);
|
||||
onBeforeSend?.();
|
||||
@@ -240,6 +304,11 @@ export const useAgentChatSession = ({
|
||||
assistantMessageId: nextAssistantMessage.id,
|
||||
appendArtifact,
|
||||
});
|
||||
} else if (event.type === "session_title") {
|
||||
const nextTitle = event.title.trim();
|
||||
if (nextTitle) {
|
||||
setSessionTitle(nextTitle);
|
||||
}
|
||||
} else if (event.type === "done") {
|
||||
setMessages((prev) =>
|
||||
prev.map((message) => {
|
||||
@@ -321,7 +390,7 @@ export const useAgentChatSession = ({
|
||||
setIsStreaming(false);
|
||||
}
|
||||
},
|
||||
[appendArtifact, isStreaming, messages, onBeforeSend, onToolCall],
|
||||
[appendArtifact, isHydrating, isStreaming, messages, onBeforeSend, onToolCall],
|
||||
);
|
||||
|
||||
const abort = useCallback(() => {
|
||||
@@ -356,13 +425,115 @@ export const useAgentChatSession = ({
|
||||
cancelPromiseRef.current = trackedCancelPromise;
|
||||
}
|
||||
setMessages([]);
|
||||
setSessionTitle(undefined);
|
||||
setBranchGroups([]);
|
||||
setBranchTransition(null);
|
||||
setSessionId(undefined);
|
||||
sessionIdRef.current = undefined;
|
||||
storageSessionIdRef.current = undefined;
|
||||
setIsStreaming(false);
|
||||
}, []);
|
||||
|
||||
const createSession = useCallback(async () => {
|
||||
if (isHydrating || isStreaming) return;
|
||||
|
||||
const controller = abortRef.current;
|
||||
controller?.abort();
|
||||
setBranchTransition(null);
|
||||
|
||||
const newState = await createEmptyChatSession();
|
||||
const sessions = await listChatSessions();
|
||||
|
||||
hydrationNonceRef.current += 1;
|
||||
storageSessionIdRef.current = newState.storageSessionId;
|
||||
sessionIdRef.current = newState.sessionId;
|
||||
setMessages(newState.messages);
|
||||
setSessionTitle(newState.title);
|
||||
setSessionId(newState.sessionId);
|
||||
setBranchGroups(newState.branchGroups);
|
||||
setChatSessions(sessions);
|
||||
setIsStreaming(false);
|
||||
}, [isHydrating, isStreaming]);
|
||||
|
||||
const switchSession = useCallback(
|
||||
async (nextStorageSessionId: string) => {
|
||||
if (isHydrating || isStreaming || storageSessionIdRef.current === nextStorageSessionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsHydrating(true);
|
||||
try {
|
||||
const [nextState, sessions] = await Promise.all([
|
||||
loadChatSessionById(nextStorageSessionId),
|
||||
listChatSessions(),
|
||||
]);
|
||||
|
||||
hydrationNonceRef.current += 1;
|
||||
storageSessionIdRef.current = nextState.storageSessionId;
|
||||
sessionIdRef.current = nextState.sessionId;
|
||||
setBranchTransition(null);
|
||||
setMessages(nextState.messages);
|
||||
setSessionTitle(nextState.title);
|
||||
setSessionId(nextState.sessionId);
|
||||
setBranchGroups(nextState.branchGroups);
|
||||
setChatSessions(sessions);
|
||||
} catch (error) {
|
||||
console.error("[GlobalChatbox] Failed to switch chat session:", error);
|
||||
} finally {
|
||||
setIsHydrating(false);
|
||||
}
|
||||
},
|
||||
[isHydrating, isStreaming],
|
||||
);
|
||||
|
||||
const removeSession = useCallback(
|
||||
async (targetStorageSessionId: string) => {
|
||||
if (isHydrating || isStreaming) return;
|
||||
|
||||
try {
|
||||
const nextActiveSessionId = await deleteChatSession(targetStorageSessionId);
|
||||
const sessions = await listChatSessions();
|
||||
setChatSessions(sessions);
|
||||
|
||||
if (storageSessionIdRef.current !== targetStorageSessionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!nextActiveSessionId) {
|
||||
hydrationNonceRef.current += 1;
|
||||
storageSessionIdRef.current = undefined;
|
||||
sessionIdRef.current = undefined;
|
||||
setBranchTransition(null);
|
||||
setMessages([]);
|
||||
setSessionTitle(undefined);
|
||||
setSessionId(undefined);
|
||||
setBranchGroups([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsHydrating(true);
|
||||
const [nextState, sessionsAfterDelete] = await Promise.all([
|
||||
loadChatSessionById(nextActiveSessionId),
|
||||
listChatSessions(),
|
||||
]);
|
||||
hydrationNonceRef.current += 1;
|
||||
storageSessionIdRef.current = nextState.storageSessionId;
|
||||
sessionIdRef.current = nextState.sessionId;
|
||||
setBranchTransition(null);
|
||||
setMessages(nextState.messages);
|
||||
setSessionTitle(nextState.title);
|
||||
setSessionId(nextState.sessionId);
|
||||
setBranchGroups(nextState.branchGroups);
|
||||
setChatSessions(sessionsAfterDelete);
|
||||
} catch (error) {
|
||||
console.error("[GlobalChatbox] Failed to delete chat session:", error);
|
||||
} finally {
|
||||
setIsHydrating(false);
|
||||
}
|
||||
},
|
||||
[isHydrating, isStreaming],
|
||||
);
|
||||
|
||||
const sendPrompt = useCallback(
|
||||
async (rawPrompt: string) => {
|
||||
await runPrompt({ prompt: rawPrompt });
|
||||
@@ -371,7 +542,7 @@ export const useAgentChatSession = ({
|
||||
);
|
||||
|
||||
const regenerate = useCallback(async () => {
|
||||
if (isStreaming || messages.length === 0) return;
|
||||
if (isHydrating || isStreaming || messages.length === 0) return;
|
||||
|
||||
let lastUserIndex = messages.length - 1;
|
||||
while (lastUserIndex >= 0 && messages[lastUserIndex].role !== "user") {
|
||||
@@ -400,11 +571,11 @@ export const useAgentChatSession = ({
|
||||
userMessage: nextUserMessage,
|
||||
assistantMessage: nextAssistantMessage,
|
||||
});
|
||||
}, [isStreaming, messages, runPrompt]);
|
||||
}, [isHydrating, isStreaming, messages, runPrompt]);
|
||||
|
||||
const editAndResubmit = useCallback(
|
||||
async (messageId: string, newContent: string) => {
|
||||
if (isStreaming) return;
|
||||
if (isHydrating || isStreaming) return;
|
||||
|
||||
const trimmedContent = newContent.trim();
|
||||
if (!trimmedContent) return;
|
||||
@@ -483,12 +654,12 @@ export const useAgentChatSession = ({
|
||||
assistantMessage: nextAssistantMessage,
|
||||
});
|
||||
},
|
||||
[isStreaming, messages, runPrompt],
|
||||
[isHydrating, isStreaming, messages, runPrompt],
|
||||
);
|
||||
|
||||
const cycleBranch = useCallback(
|
||||
(rootMessageId: string, direction: -1 | 1) => {
|
||||
if (isStreaming) return;
|
||||
if (isHydrating || isStreaming) return;
|
||||
|
||||
setBranchGroups((prev) => {
|
||||
const next = cloneBranchGroups(prev);
|
||||
@@ -519,13 +690,16 @@ export const useAgentChatSession = ({
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[isStreaming, messages],
|
||||
[isHydrating, isStreaming, messages],
|
||||
);
|
||||
|
||||
return {
|
||||
messages,
|
||||
chatSessions,
|
||||
activeStorageSessionId: storageSessionIdRef.current,
|
||||
branchGroups,
|
||||
branchTransition,
|
||||
isHydrating,
|
||||
isStreaming,
|
||||
sessionId,
|
||||
sendPrompt,
|
||||
@@ -533,6 +707,9 @@ export const useAgentChatSession = ({
|
||||
editAndResubmit,
|
||||
cycleBranch,
|
||||
abort,
|
||||
createSession,
|
||||
reset,
|
||||
removeSession,
|
||||
switchSession,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user