refactor: optimize component rendering with memoization and improve state management

- Added memoization to ChatHistoryPanel, ChatMessageList, and TaskBoard components to prevent unnecessary re-renders.
- Refactored HomePage to utilize useMemo for derived state calculations, enhancing performance.
- Updated main.tsx to conditionally render React.StrictMode based on the environment.
- Improved chat and channel store hooks to allow for selector functions, enhancing flexibility in state selection.
- Enhanced streaming message handling in chat store to manage pending deltas more effectively.
- Refactored LoginPage to include animated decorations for improved user experience.
- Implemented lazy loading for routes in the router to optimize initial load time.
This commit is contained in:
duanshuwen
2026-04-18 11:05:49 +08:00
parent 85d92b937f
commit dfa4388087
12 changed files with 533 additions and 189 deletions

View File

@@ -197,6 +197,7 @@ export const channelStore = {
removeSelectedChannel,
};
export function useChannelStore(): ChannelStoreState {
return useSyncExternalStore(channelStore.subscribe, channelStore.getSnapshot, channelStore.getSnapshot);
export function useChannelStore<T = ChannelStoreState>(selector?: (state: ChannelStoreState) => T): T {
const select = selector ?? ((current: ChannelStoreState) => current as unknown as T);
return useSyncExternalStore(subscribe, () => select(getSnapshot()), () => select(getSnapshot()));
}

View File

@@ -63,6 +63,9 @@ const listeners = new Set<() => void>();
const historyLoadInFlight = new Map<string, Promise<void>>();
const lastHistoryLoadAtBySession = new Map<string, number>();
const chatEventDedupe = new Map<string, number>();
let pendingStreamingDelta = '';
let pendingStreamingRunId: string | null = null;
let streamingFlushHandle: number | null = null;
let gatewaySubscribed = false;
let loadSessionsInFlight: Promise<void> | null = null;
@@ -98,6 +101,79 @@ function patchState(patch: Partial<ChatStoreState>): ChatStoreState {
return state;
}
function requestFrame(callback: () => void): number {
if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') {
return window.requestAnimationFrame(() => callback());
}
return window.setTimeout(callback, 16);
}
function cancelFrame(handle: number): void {
if (typeof window !== 'undefined' && typeof window.cancelAnimationFrame === 'function') {
window.cancelAnimationFrame(handle);
return;
}
window.clearTimeout(handle);
}
function applyPendingStreamingDelta(): void {
if (!pendingStreamingDelta) return;
const previousContent = state.streamingMessage ? extractText(state.streamingMessage) : '';
const nextContent = previousContent + pendingStreamingDelta;
const nextRunId = pendingStreamingRunId;
pendingStreamingDelta = '';
pendingStreamingRunId = null;
patchState({
sending: true,
error: null,
activeRunId: nextRunId ?? state.activeRunId,
streamingMessage: {
role: 'assistant',
content: nextContent,
timestamp: Date.now(),
id: state.streamingMessage?.id || `stream-${nextRunId || Date.now()}`,
},
});
}
function flushPendingStreamingDelta(): void {
if (streamingFlushHandle !== null) {
cancelFrame(streamingFlushHandle);
streamingFlushHandle = null;
}
applyPendingStreamingDelta();
}
function resetPendingStreamingDelta(): void {
if (streamingFlushHandle !== null) {
cancelFrame(streamingFlushHandle);
streamingFlushHandle = null;
}
pendingStreamingDelta = '';
pendingStreamingRunId = null;
}
function queueStreamingDelta(delta: string, runId?: string): void {
if (!delta) return;
pendingStreamingDelta += delta;
pendingStreamingRunId = runId ?? pendingStreamingRunId;
if (streamingFlushHandle !== null) return;
streamingFlushHandle = requestFrame(() => {
streamingFlushHandle = null;
applyPendingStreamingDelta();
});
}
function getAgentIdFromSessionKey(sessionKey: string): string {
const parsed = parseSessionKey(normalizeAgentSessionKey(sessionKey));
if (parsed.isAgentSession) return parsed.agentId;
@@ -527,6 +603,8 @@ async function loadHistory(sessionKey = state.currentSessionKey, quiet = false):
}
async function newSession(agentId = state.currentAgentId): Promise<void> {
resetPendingStreamingDelta();
const leavingEmpty =
!state.currentSessionKey.endsWith(':main') &&
state.messages.length === 0 &&
@@ -560,6 +638,7 @@ async function newSession(agentId = state.currentAgentId): Promise<void> {
function switchSession(sessionKey: string): void {
if (sessionKey === state.currentSessionKey) return;
resetPendingStreamingDelta();
patchState(buildSessionSwitchPatch(state, sessionKey));
void loadHistory(sessionKey);
}
@@ -575,6 +654,10 @@ function selectAgent(agentId: string): void {
}
async function deleteSession(sessionKey: string): Promise<void> {
if (sessionKey === state.currentSessionKey) {
resetPendingStreamingDelta();
}
try {
await gatewayRpc('session.delete', { sessionKey });
} catch {
@@ -724,6 +807,7 @@ async function sendMessage(text: string, attachments: StagedAttachment[] = []):
async function abortRun(): Promise<void> {
const sessionKey = state.currentSessionKey;
resetPendingStreamingDelta();
patchState({
sending: false,
@@ -749,21 +833,12 @@ async function handleGatewayEvent(event: GatewayEvent): Promise<void> {
switch (event.type) {
case 'chat:delta': {
const previousContent = state.streamingMessage ? extractText(state.streamingMessage) : '';
patchState({
sending: true,
error: null,
activeRunId: typeof event.runId === 'string' ? event.runId : state.activeRunId,
streamingMessage: {
role: 'assistant',
content: previousContent + event.delta,
timestamp: Date.now(),
id: state.streamingMessage?.id || `stream-${event.runId || Date.now()}`,
},
});
queueStreamingDelta(event.delta, typeof event.runId === 'string' ? event.runId : undefined);
break;
}
case 'chat:final': {
flushPendingStreamingDelta();
const composedMessage = state.streamingMessage && typeof event.message.content === 'string'
? {
...event.message,
@@ -806,6 +881,8 @@ async function handleGatewayEvent(event: GatewayEvent): Promise<void> {
break;
}
case 'chat:error': {
flushPendingStreamingDelta();
if (state.streamingMessage && !state.messages.some((message) => message.id === state.streamingMessage?.id)) {
patchState({
messages: [
@@ -827,9 +904,11 @@ async function handleGatewayEvent(event: GatewayEvent): Promise<void> {
pendingFinal: false,
lastUserMessageAt: null,
});
resetPendingStreamingDelta();
break;
}
case 'chat:aborted': {
resetPendingStreamingDelta();
patchState({
sending: false,
activeRunId: null,
@@ -902,6 +981,7 @@ export const chatStore = {
stageAttachmentFiles,
};
export function useChatStore(): ChatStoreState {
return useSyncExternalStore(chatStore.subscribe, chatStore.getSnapshot, chatStore.getSnapshot);
export function useChatStore<T = ChatStoreState>(selector?: (state: ChatStoreState) => T): T {
const select = selector ?? ((current: ChatStoreState) => current as unknown as T);
return useSyncExternalStore(subscribe, () => select(getSnapshot()), () => select(getSnapshot()));
}

View File

@@ -141,6 +141,7 @@ export const modelsStore = {
resolveProviderAccountId,
};
export function useModelsStore(): ModelsStoreState {
return useSyncExternalStore(modelsStore.subscribe, modelsStore.getSnapshot, modelsStore.getSnapshot);
export function useModelsStore<T = ModelsStoreState>(selector?: (state: ModelsStoreState) => T): T {
const select = selector ?? ((current: ModelsStoreState) => current as unknown as T);
return useSyncExternalStore(subscribe, () => select(getSnapshot()), () => select(getSnapshot()));
}

View File

@@ -354,8 +354,9 @@ export const taskStore = {
removeTask,
};
export function useTaskStore(): TaskStoreState {
return useSyncExternalStore(taskStore.subscribe, taskStore.getSnapshot, taskStore.getSnapshot);
export function useTaskStore<T = TaskStoreState>(selector?: (state: TaskStoreState) => T): T {
const select = selector ?? ((current: TaskStoreState) => current as unknown as T);
return useSyncExternalStore(subscribe, () => select(getSnapshot()), () => select(getSnapshot()));
}
export function getPendingTasks(tasks = state.tasks): Task[] {