feat: release chat and mindspace UI updates

This commit is contained in:
john
2026-06-27 21:57:21 +08:00
parent c924b2bb77
commit 8264e71f9e
24 changed files with 2042 additions and 209 deletions
+73 -53
View File
@@ -52,6 +52,7 @@ import {
import { buildAbsoluteAssetImageUrl } from '../utils/mindspaceCards';
import {
buildUserMessage,
mergeConversationSnapshot,
normalizeConversationMessages,
getDisplayText,
getToolConfirmation,
@@ -60,7 +61,12 @@ import {
isRelayServerErrorMessage,
pushMessage,
} from '../utils/message';
import { prependUnique, shouldShowNewChatTitle, sortAndTrim, touchSession } from '../utils/sessions';
import {
mergeSessionLists,
prependUnique,
shouldShowNewChatTitle,
touchSession,
} from '../utils/sessions';
const INSUFFICIENT_BALANCE_NOTICE = '余额不足,请充值后继续使用';
@@ -338,7 +344,7 @@ export function useTKMindChat(
setSessionsLoading(true);
try {
const items = await listSessions();
setSessions(sortAndTrim(items));
setSessions((prev) => mergeSessionLists(prev, items));
} catch (err) {
if (err instanceof ApiError && err.status === 0) {
setError('网络不可用,无法刷新历史列表');
@@ -385,6 +391,7 @@ export function useTKMindChat(
const syncSessionMessages = useCallback(async (sessionId: string) => {
try {
const detail = await loadSessionDetail(sessionId);
setSessions((prev) => prependUnique(prev, detail.session));
if (sessionRef.current?.id !== sessionId) return;
messagesRef.current = detail.messages;
setMessages(detail.messages);
@@ -467,12 +474,14 @@ export function useTKMindChat(
}
return;
}
case 'UpdateConversation':
messagesRef.current = normalizeConversationMessages(
case 'UpdateConversation': {
const snapshot = normalizeConversationMessages(
event.conversation.filter((m) => m.metadata?.userVisible),
);
messagesRef.current = mergeConversationSnapshot(messagesRef.current, snapshot);
setMessages(messagesRef.current);
return;
}
case 'Error':
setError(event.error);
setChatState('error');
@@ -519,11 +528,22 @@ export function useTKMindChat(
sessionId,
(event) => {
const rid = activeRequestId.current;
if (rid) processEvent(event, rid, sessionId);
else if (event.type === 'ActiveRequests' && event.request_ids.length > 0) {
activeRequestId.current = event.request_ids[0];
setChatState('streaming');
if (event.type === 'ActiveRequests') {
if (!rid && event.request_ids.length > 0) {
// SSE reconnected while agent was running — adopt the active request.
activeRequestId.current = event.request_ids[0];
setChatState('streaming');
} else if (rid && !event.request_ids.includes(rid)) {
// Our request is no longer active — agent finished while SSE was paused.
// Sync the final state from the server.
activeRequestId.current = null;
setChatState('idle');
setPendingTool(null);
void syncSessionMessages(sessionId);
}
return;
}
if (rid) processEvent(event, rid, sessionId);
},
(err) => {
if (err instanceof ApiError && err.status === 401) return;
@@ -531,6 +551,8 @@ export function useTKMindChat(
notifyInsufficientBalance();
return;
}
// SSE 断连时如果 agent 还在跑,不打断 chatState,等重连后事件恢复
if (activeRequestId.current) return;
setError(err.message);
},
{
@@ -643,7 +665,6 @@ export function useTKMindChat(
setError(null);
const previousSessionId = readStoredSessionId(userRef.current?.id);
let sessionId: string | null = null;
let staleSession = false;
if (previousSessionId) {
@@ -667,20 +688,12 @@ export function useTKMindChat(
}
}
const freshSession = await startSession();
sessionId = freshSession.id;
writeStoredSessionId(userRef.current?.id, sessionId);
if (staleSession) {
setNotice('上次会话已失效,已为你新建对话');
}
if (cancelled) return;
await connectSessionRef.current(sessionId, {
skipResume: true,
seedSession: freshSession,
});
if (cancelled) return;
void loadProjectMemory(sessionId, false);
setChatState('idle');
void refreshSessions();
} catch (err) {
if (!cancelled) {
@@ -745,7 +758,7 @@ export function useTKMindChat(
imageUrls?: string[],
) => {
const normalizedImageUrls = (imageUrls ?? []).filter((value) => typeof value === 'string' && value.trim());
if (!session || (!text.trim() && normalizedImageUrls.length === 0)) return;
if (!text.trim() && normalizedImageUrls.length === 0) return;
if (chatState === 'streaming' || chatState === 'loading' || chatState === 'connecting') return;
const trimmed = text.trim();
@@ -764,15 +777,41 @@ export function useTKMindChat(
activeRequestId.current = requestId;
messagesRef.current = [...messagesRef.current, userMessage];
setMessages(messagesRef.current);
setSessions((prev) => touchSession(prev, session.id, 1));
setChatState('streaming');
setError(null);
setPendingTool(null);
let activeSessionId = session?.id ?? null;
if (!activeSessionId) {
try {
const created = await startSession();
activeSessionId = created.id;
writeStoredSessionId(userRef.current?.id, created.id);
setSession(created);
setSessions((prev) => prependUnique(prev, created));
subscribeToSession(created.id);
void ensureProvider(created.id);
void loadProjectMemory(created.id, false);
void refreshSessions();
} catch (err) {
if (err instanceof ApiError && err.status === 402) {
notifyInsufficientBalance();
} else {
setError(err instanceof Error ? err.message : String(err));
setChatState('error');
}
activeRequestId.current = null;
return;
}
} else {
setSessions((prev) => touchSession(prev, activeSessionId!, 1));
}
try {
await sendReply(session.id, requestId, userMessage);
await sendReply(activeSessionId, requestId, userMessage);
} catch (err) {
setSessions((prev) => touchSession(prev, session.id, -1));
if (session) setSessions((prev) => touchSession(prev, activeSessionId!, -1));
if (err instanceof ApiError && err.status === 402) {
notifyInsufficientBalance();
} else {
@@ -782,7 +821,7 @@ export function useTKMindChat(
activeRequestId.current = null;
}
},
[notifyInsufficientBalance, session, chatState, grantedSkills],
[notifyInsufficientBalance, session, chatState, grantedSkills, subscribeToSession, ensureProvider, loadProjectMemory, refreshSessions],
);
const stop = useCallback(async () => {
@@ -808,37 +847,18 @@ export function useTKMindChat(
const newSession = useCallback(() => {
if (chatState === 'streaming') return;
resetSessionView();
connectTokenRef.current += 1;
unsubscribeRef.current?.();
unsubscribeRef.current = null;
clearStoredSessionId(userRef.current?.id);
activeRequestId.current = null;
messagesRef.current = [];
setMessages([]);
setSession(null);
const token = connectTokenRef.current;
void (async () => {
try {
const created = await startSession();
if (token !== connectTokenRef.current) return;
writeStoredSessionId(userRef.current?.id, created.id);
setSession(created);
setSessions((prev) => prependUnique(prev, created));
void loadProjectMemory(created.id, false);
void refreshSessions();
await ensureProvider(created.id);
if (token !== connectTokenRef.current) return;
await connectSession(created.id, { showLoading: false });
} catch (err) {
if (token !== connectTokenRef.current) return;
if (err instanceof ApiError && err.status === 402) {
notifyInsufficientBalance();
return;
}
setError(err instanceof Error ? err.message : String(err));
setChatState('error');
}
})();
}, [chatState, connectSession, ensureProvider, loadProjectMemory, notifyInsufficientBalance, refreshSessions, resetSessionView]);
setError(null);
setPendingTool(null);
setChatState('idle');
}, [chatState]);
const refreshProjectMemory = useCallback(async () => {
if (!session || chatState === 'streaming') return;