fix: stabilize ai design history switching
This commit is contained in:
@@ -65,6 +65,8 @@ type ImageWorkspaceState = {
|
||||
deletingWorkspaceId: string | null;
|
||||
tasks: DesignGenerationTask[];
|
||||
pendingTurn: PendingDesignTurn | null;
|
||||
loadingOlderMessages: boolean;
|
||||
olderMessagesError: string | null;
|
||||
taskStreamState: ImageWorkspaceTaskStreamState;
|
||||
error: string | null;
|
||||
load: () => Promise<DesignWorkspaceBootstrap | null>;
|
||||
@@ -76,6 +78,7 @@ type ImageWorkspaceState = {
|
||||
selectConversation: (conversationId: string) => Promise<void>;
|
||||
refreshWorkspace: () => Promise<DesignWorkspace | null>;
|
||||
refreshConversation: () => Promise<DesignConversation | null>;
|
||||
loadOlderMessages: () => Promise<DesignConversation | null>;
|
||||
refreshTasks: () => Promise<DesignGenerationTask[]>;
|
||||
connectTaskStream: () => void;
|
||||
markTaskStreamNeedsReconciliation: () => void;
|
||||
@@ -101,7 +104,7 @@ const CONFIRMED_TASK_RECONCILE_INTERVAL_MS = 250;
|
||||
let activeTaskEventSource: EventSource | null = null;
|
||||
let activeTaskEventWorkspaceId: string | null = null;
|
||||
let activeTaskEventConversationId: string | null = null;
|
||||
let taskStreamGeneration = 0;
|
||||
const pendingTaskEventConnections = new Map<string, symbol>();
|
||||
let taskFallbackTimer: ReturnType<typeof setInterval> | null = null;
|
||||
const taskEventRevisions = new Map<string, number>();
|
||||
let workspaceLoadGeneration = 0;
|
||||
@@ -126,13 +129,16 @@ function taskRevisionKey(workspaceId: string, taskId: string): string {
|
||||
return `${workspaceId}:${taskId}`;
|
||||
}
|
||||
|
||||
function taskStreamKey(workspaceId: string, conversationId: string): string {
|
||||
return `${workspaceId}:${conversationId}`;
|
||||
}
|
||||
|
||||
function stopFallbackPolling(): void {
|
||||
if (taskFallbackTimer !== null) clearInterval(taskFallbackTimer);
|
||||
taskFallbackTimer = null;
|
||||
}
|
||||
|
||||
function closeTaskEventSource(): void {
|
||||
taskStreamGeneration += 1;
|
||||
stopFallbackPolling();
|
||||
if (activeTaskEventSource) {
|
||||
activeTaskEventSource.onopen = null;
|
||||
@@ -345,33 +351,35 @@ function mergePendingUserMessage(
|
||||
return { ...conversation, messages };
|
||||
}
|
||||
|
||||
function mergeUserMessagesFromCurrentConversation(
|
||||
function mergeConversationMessagePages(
|
||||
current: DesignConversation | null,
|
||||
incoming: DesignConversation,
|
||||
source: 'latest' | 'older',
|
||||
): DesignConversation {
|
||||
if (!current
|
||||
|| current.conversationId !== incoming.conversationId
|
||||
|| current.workspaceId !== incoming.workspaceId) {
|
||||
return incoming;
|
||||
}
|
||||
const missingUserMessages = current.messages.filter((message) => (
|
||||
message.role === 'user'
|
||||
&& !incoming.messages.some((candidate) => (
|
||||
candidate.role === 'user'
|
||||
&& candidate.turnRevision === message.turnRevision
|
||||
&& candidate.text === message.text
|
||||
))
|
||||
));
|
||||
if (missingUserMessages.length === 0) return incoming;
|
||||
|
||||
const messages = [...incoming.messages];
|
||||
for (const userMessage of missingUserMessages) {
|
||||
const insertionIndex = messages.findIndex(
|
||||
(message) => message.turnRevision >= userMessage.turnRevision,
|
||||
);
|
||||
messages.splice(insertionIndex < 0 ? messages.length : insertionIndex, 0, userMessage);
|
||||
const messagesById = new Map<string, DesignMessage>();
|
||||
const messageSources = source === 'older'
|
||||
? [incoming.messages, current.messages]
|
||||
: [current.messages, incoming.messages];
|
||||
for (const messages of messageSources) {
|
||||
for (const message of messages) messagesById.set(message.id, message);
|
||||
}
|
||||
return { ...incoming, messages };
|
||||
const messages = [...messagesById.values()].sort((left, right) => (
|
||||
left.turnRevision - right.turnRevision
|
||||
|| (left.role === right.role ? left.id.localeCompare(right.id) : left.role === 'user' ? -1 : 1)
|
||||
|| left.createdAt.localeCompare(right.createdAt)
|
||||
));
|
||||
return {
|
||||
...incoming,
|
||||
messages,
|
||||
messagePage: source === 'older'
|
||||
? incoming.messagePage
|
||||
: current.messagePage ?? incoming.messagePage,
|
||||
};
|
||||
}
|
||||
|
||||
function replaceGenerationQuote(
|
||||
@@ -473,13 +481,20 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
&& activeTaskEventConversationId === conversationId
|
||||
&& get().taskStreamState !== 'idle') return;
|
||||
closeTaskEventSource();
|
||||
const generation = taskStreamGeneration;
|
||||
activeTaskEventWorkspaceId = workspaceId;
|
||||
activeTaskEventConversationId = conversationId;
|
||||
set({ taskStreamState: 'connecting' });
|
||||
const connectionKey = taskStreamKey(workspaceId, conversationId);
|
||||
if (pendingTaskEventConnections.has(connectionKey)) return;
|
||||
const connectionAttempt = Symbol(connectionKey);
|
||||
pendingTaskEventConnections.set(connectionKey, connectionAttempt);
|
||||
void openImageWorkspaceTaskEvents(workspaceId, conversationId).then((source) => {
|
||||
if (generation !== taskStreamGeneration
|
||||
|| get().activeWorkspaceId !== workspaceId
|
||||
if (pendingTaskEventConnections.get(connectionKey) !== connectionAttempt) {
|
||||
source.close();
|
||||
return;
|
||||
}
|
||||
pendingTaskEventConnections.delete(connectionKey);
|
||||
if (get().activeWorkspaceId !== workspaceId
|
||||
|| get().activeConversationId !== conversationId) {
|
||||
source.close();
|
||||
return;
|
||||
@@ -518,9 +533,10 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
|| snapshot.conversation.turnRevision >= state.conversation.turnRevision
|
||||
? snapshot.conversation
|
||||
: state.conversation;
|
||||
const resolvedConversation = mergeUserMessagesFromCurrentConversation(
|
||||
const resolvedConversation = mergeConversationMessagePages(
|
||||
state.conversation,
|
||||
mergePendingUserMessage(conversation, state.pendingTurn),
|
||||
'latest',
|
||||
);
|
||||
const workspace = upsertConversation(
|
||||
state.workspace
|
||||
@@ -607,8 +623,9 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
startFallbackPolling();
|
||||
};
|
||||
}).catch(() => {
|
||||
if (generation !== taskStreamGeneration
|
||||
|| get().activeWorkspaceId !== workspaceId
|
||||
if (pendingTaskEventConnections.get(connectionKey) !== connectionAttempt) return;
|
||||
pendingTaskEventConnections.delete(connectionKey);
|
||||
if (get().activeWorkspaceId !== workspaceId
|
||||
|| get().activeConversationId !== conversationId) return;
|
||||
set({ taskStreamState: 'degraded' });
|
||||
startFallbackPolling();
|
||||
@@ -668,7 +685,10 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
return workspace;
|
||||
};
|
||||
|
||||
const applyConversation = (conversation: DesignConversation): DesignConversation => {
|
||||
const applyConversation = (
|
||||
conversation: DesignConversation,
|
||||
source: 'latest' | 'older' = 'latest',
|
||||
): DesignConversation => {
|
||||
let applied = conversation;
|
||||
set((state) => {
|
||||
if (state.conversation?.conversationId === conversation.conversationId
|
||||
@@ -678,9 +698,10 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
applied = state.conversation;
|
||||
return state;
|
||||
}
|
||||
const resolvedConversation = mergeUserMessagesFromCurrentConversation(
|
||||
const resolvedConversation = mergeConversationMessagePages(
|
||||
state.conversation,
|
||||
mergePendingUserMessage(conversation, state.pendingTurn),
|
||||
source,
|
||||
);
|
||||
applied = resolvedConversation;
|
||||
const pendingTurn = state.pendingTurn?.workspaceId === resolvedConversation.workspaceId
|
||||
@@ -693,6 +714,8 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
activeConversationId: resolvedConversation.conversationId,
|
||||
conversation: resolvedConversation,
|
||||
pendingTurn,
|
||||
loadingOlderMessages: false,
|
||||
olderMessagesError: null,
|
||||
error: null,
|
||||
};
|
||||
});
|
||||
@@ -711,9 +734,6 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
)
|
||||
? currentConversationId
|
||||
: workspace.conversations[0]?.conversationId ?? null;
|
||||
const tasks = await fetchImageWorkspaceTasks(workspaceId);
|
||||
if (loadGeneration !== workspaceLoadGeneration
|
||||
|| get().activeWorkspaceId !== workspaceId) return;
|
||||
set((state) => ({
|
||||
status: 'ready',
|
||||
bootstrap: upsertSummary(state.bootstrap, workspace),
|
||||
@@ -721,9 +741,17 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
activeConversationId: conversationId,
|
||||
workspace,
|
||||
conversation: null,
|
||||
tasks: sortTasks(tasks),
|
||||
tasks: [],
|
||||
pendingTurn: null,
|
||||
loadingOlderMessages: false,
|
||||
olderMessagesError: null,
|
||||
error: null,
|
||||
}));
|
||||
void fetchImageWorkspaceTasks(workspaceId).then((tasks) => {
|
||||
if (loadGeneration !== workspaceLoadGeneration
|
||||
|| get().activeWorkspaceId !== workspaceId) return;
|
||||
set({ tasks: sortTasks(tasks) });
|
||||
}).catch(() => undefined);
|
||||
if (!conversationId) return;
|
||||
const selectionGeneration = ++conversationSelectionGeneration;
|
||||
const conversation = await fetchImageWorkspaceConversation(workspaceId, conversationId);
|
||||
@@ -760,6 +788,8 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
deletingWorkspaceId: null,
|
||||
tasks: [],
|
||||
pendingTurn: null,
|
||||
loadingOlderMessages: false,
|
||||
olderMessagesError: null,
|
||||
taskStreamState: 'idle',
|
||||
error: null,
|
||||
|
||||
@@ -861,6 +891,8 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
activeConversationId: conversation.conversationId,
|
||||
conversation: null,
|
||||
pendingTurn: null,
|
||||
loadingOlderMessages: false,
|
||||
olderMessagesError: null,
|
||||
taskStreamState: 'idle',
|
||||
error: null,
|
||||
});
|
||||
@@ -962,6 +994,8 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
creatingConversation: false,
|
||||
tasks: [],
|
||||
pendingTurn: null,
|
||||
loadingOlderMessages: false,
|
||||
olderMessagesError: null,
|
||||
taskStreamState: 'idle',
|
||||
error: null,
|
||||
});
|
||||
@@ -990,6 +1024,8 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
activeConversationId: conversationId,
|
||||
conversation: null,
|
||||
pendingTurn: null,
|
||||
loadingOlderMessages: false,
|
||||
olderMessagesError: null,
|
||||
taskStreamState: 'idle',
|
||||
error: null,
|
||||
});
|
||||
@@ -1047,6 +1083,35 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
}
|
||||
},
|
||||
|
||||
loadOlderMessages: async () => {
|
||||
const workspaceId = get().activeWorkspaceId;
|
||||
const conversation = get().conversation;
|
||||
const before = conversation?.messagePage?.nextBefore;
|
||||
if (!workspaceId || !conversation || !conversation.messagePage?.hasOlder || !before) {
|
||||
return conversation ?? null;
|
||||
}
|
||||
const conversationId = conversation.conversationId;
|
||||
const selectionGeneration = conversationSelectionGeneration;
|
||||
set({ loadingOlderMessages: true, olderMessagesError: null });
|
||||
try {
|
||||
const older = await fetchImageWorkspaceConversation(workspaceId, conversationId, before);
|
||||
if (selectionGeneration !== conversationSelectionGeneration
|
||||
|| get().activeWorkspaceId !== workspaceId
|
||||
|| get().activeConversationId !== conversationId) return null;
|
||||
return applyConversation(older, 'older');
|
||||
} catch (error) {
|
||||
if (selectionGeneration === conversationSelectionGeneration
|
||||
&& get().activeWorkspaceId === workspaceId
|
||||
&& get().activeConversationId === conversationId) {
|
||||
set({
|
||||
loadingOlderMessages: false,
|
||||
olderMessagesError: handleRequestError(error),
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
refreshTasks: async () => {
|
||||
const workspaceId = get().activeWorkspaceId;
|
||||
if (!workspaceId) {
|
||||
@@ -1306,6 +1371,7 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
generationQuoteUpdateSequence += 1;
|
||||
reconcileTaskStateOnNextConnect = false;
|
||||
stopTaskStream();
|
||||
pendingTaskEventConnections.clear();
|
||||
set({
|
||||
status: 'idle',
|
||||
bootstrap: null,
|
||||
@@ -1317,6 +1383,8 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
deletingWorkspaceId: null,
|
||||
tasks: [],
|
||||
pendingTurn: null,
|
||||
loadingOlderMessages: false,
|
||||
olderMessagesError: null,
|
||||
taskStreamState: 'idle',
|
||||
error: null,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user