Spaces:
Sleeping
Sleeping
| import type { Message, MessageFile } from "$lib/types/Message"; | |
| import { MessageRole } from "$lib/types/Message"; | |
| import { addChildren } from "$lib/utils/tree/addChildren"; | |
| import { addSibling } from "$lib/utils/tree/addSibling"; | |
| import type { WriteMessageContext, WriteMessageParams } from "$lib/types/MessageContext"; | |
| export class ConversationTreeManager { | |
| constructor(private ctx: WriteMessageContext) {} | |
| public prepareMessageForWrite( | |
| params: WriteMessageParams, | |
| base64Files: MessageFile[] = [] | |
| ): { | |
| messageToWriteToId: string; | |
| navigateToMessageId: string | null; | |
| } { | |
| const { | |
| prompt, | |
| messageId = this.ctx.messagesPath.at(-1)?.id ?? undefined, | |
| isRetry = false, | |
| isContinue = false, | |
| personaId, | |
| } = params; | |
| let messageToWriteToId: string | undefined; | |
| let navigateToMessageId: string | null = null; | |
| if (isContinue && messageId) { | |
| const msg = this.ctx.messages.find((m) => m.id === messageId); | |
| if ((msg?.children?.length ?? 0) > 0) { | |
| throw new Error("Can only continue the last message"); | |
| } | |
| messageToWriteToId = messageId; | |
| } else if (isRetry && messageId) { | |
| const messageToRetry = this.ctx.messages.find((m) => m.id === messageId); | |
| if (!messageToRetry) { | |
| throw new Error("Message not found"); | |
| } | |
| if (messageToRetry.from === MessageRole.User && prompt !== undefined) { | |
| const newUserMessageId = addSibling( | |
| { | |
| messages: this.ctx.messages, | |
| rootMessageId: this.ctx.data.rootMessageId, | |
| }, | |
| { | |
| from: MessageRole.User, | |
| content: prompt, | |
| files: messageToRetry.files, | |
| ...(messageToRetry.branchedFrom && { | |
| branchedFrom: messageToRetry.branchedFrom, | |
| }), | |
| }, | |
| messageId | |
| ); | |
| const targetPersonaId = | |
| this.ctx.branchState?.personaId || messageToRetry.branchedFrom?.personaId; | |
| const initialResponses: Message["personaResponses"] = []; | |
| if (targetPersonaId) { | |
| const persona = this.ctx.settings.personas?.find((p) => p.id === targetPersonaId); | |
| initialResponses.push({ | |
| personaId: targetPersonaId, | |
| personaName: this.ctx.branchState?.personaName || persona?.name || targetPersonaId, | |
| content: "", | |
| }); | |
| } | |
| messageToWriteToId = addChildren( | |
| { | |
| messages: this.ctx.messages, | |
| rootMessageId: this.ctx.data.rootMessageId, | |
| }, | |
| { | |
| from: MessageRole.Assistant, | |
| content: "", | |
| personaResponses: initialResponses, | |
| ...((this.ctx.branchState || messageToRetry.branchedFrom) && { | |
| branchedFrom: this.ctx.branchState | |
| ? { | |
| messageId: this.ctx.branchState.messageId, | |
| personaId: this.ctx.branchState.personaId, | |
| } | |
| : messageToRetry.branchedFrom, | |
| }), | |
| }, | |
| newUserMessageId | |
| ); | |
| if (messageToRetry.branchedFrom && !this.ctx.branchState) { | |
| const persona = this.ctx.settings.personas?.find( | |
| (p) => p.id === messageToRetry.branchedFrom?.personaId | |
| ); | |
| this.ctx.updateBranchState({ | |
| messageId: messageToRetry.branchedFrom.messageId, | |
| personaId: messageToRetry.branchedFrom.personaId, | |
| personaName: persona?.name || messageToRetry.branchedFrom.personaId, | |
| }); | |
| navigateToMessageId = newUserMessageId; | |
| } | |
| this.ctx.onMessageCreated?.(messageToWriteToId); | |
| } else if (messageToRetry.from === MessageRole.User && prompt === undefined) { | |
| messageToWriteToId = addChildren( | |
| { | |
| messages: this.ctx.messages, | |
| rootMessageId: this.ctx.data.rootMessageId, | |
| }, | |
| { | |
| from: MessageRole.Assistant, | |
| content: "", | |
| personaResponses: [], | |
| ...(this.ctx.branchState && { | |
| branchedFrom: { | |
| messageId: this.ctx.branchState.messageId, | |
| personaId: this.ctx.branchState.personaId, | |
| }, | |
| }), | |
| }, | |
| messageId | |
| ); | |
| navigateToMessageId = messageToWriteToId; | |
| this.ctx.onMessageCreated?.(messageToWriteToId); | |
| } else if (messageToRetry.from === MessageRole.Assistant) { | |
| let initialPersonaResponses: Message["personaResponses"] = []; | |
| if (personaId && messageToRetry.personaResponses) { | |
| initialPersonaResponses = messageToRetry.personaResponses.map((p) => { | |
| if (p.personaId === personaId) { | |
| return { | |
| ...p, | |
| content: "", | |
| interrupted: undefined, | |
| reasoning: undefined, | |
| updates: undefined, | |
| routerMetadata: undefined, | |
| }; | |
| } | |
| // Defensive copy using structuredClone | |
| return structuredClone(p); | |
| }); | |
| } | |
| messageToWriteToId = addSibling( | |
| { | |
| messages: this.ctx.messages, | |
| rootMessageId: this.ctx.data.rootMessageId, | |
| }, | |
| { | |
| from: MessageRole.Assistant, | |
| content: "", | |
| personaResponses: initialPersonaResponses, | |
| ...((this.ctx.branchState || messageToRetry.branchedFrom) && { | |
| branchedFrom: this.ctx.branchState | |
| ? { | |
| messageId: this.ctx.branchState.messageId, | |
| personaId: this.ctx.branchState.personaId, | |
| } | |
| : messageToRetry.branchedFrom, | |
| }), | |
| }, | |
| messageId | |
| ); | |
| if (messageToRetry.branchedFrom && !this.ctx.branchState) { | |
| const persona = this.ctx.settings.personas?.find( | |
| (p) => p.id === messageToRetry.branchedFrom?.personaId | |
| ); | |
| this.ctx.updateBranchState({ | |
| messageId: messageToRetry.branchedFrom.messageId, | |
| personaId: messageToRetry.branchedFrom.personaId, | |
| personaName: persona?.name || messageToRetry.branchedFrom.personaId, | |
| }); | |
| navigateToMessageId = messageToWriteToId; | |
| } | |
| this.ctx.onMessageCreated?.(messageToWriteToId); | |
| } | |
| } else { | |
| // New message | |
| const newUserMessageId = addChildren( | |
| { | |
| messages: this.ctx.messages, | |
| rootMessageId: this.ctx.data.rootMessageId, | |
| }, | |
| { | |
| from: MessageRole.User, | |
| content: prompt ?? "", | |
| files: base64Files, | |
| ...(this.ctx.branchState && { | |
| branchedFrom: { | |
| messageId: this.ctx.branchState.messageId, | |
| personaId: this.ctx.branchState.personaId, | |
| }, | |
| }), | |
| }, | |
| messageId | |
| ); | |
| if (!this.ctx.data.rootMessageId) { | |
| this.ctx.data.rootMessageId = newUserMessageId; | |
| } | |
| messageToWriteToId = addChildren( | |
| { | |
| messages: this.ctx.messages, | |
| rootMessageId: this.ctx.data.rootMessageId, | |
| }, | |
| { | |
| from: MessageRole.Assistant, | |
| content: "", | |
| personaResponses: [], | |
| ...(this.ctx.branchState && { | |
| branchedFrom: { | |
| messageId: this.ctx.branchState.messageId, | |
| personaId: this.ctx.branchState.personaId, | |
| }, | |
| }), | |
| }, | |
| newUserMessageId | |
| ); | |
| this.ctx.onMessageCreated?.(messageToWriteToId); | |
| } | |
| if (!messageToWriteToId) { | |
| throw new Error("Failed to determine message ID to write to"); | |
| } | |
| return { messageToWriteToId, navigateToMessageId }; | |
| } | |
| /** | |
| * Safely updates a message ID in the tree, ensuring parent linkage is maintained. | |
| */ | |
| public syncMessageId(oldId: string, newId: string): void { | |
| const message = this.ctx.messages.find((m) => m.id === oldId || m.id === newId); | |
| if (!message) return; | |
| // If ID is already updated, just return | |
| if (message.id === newId) return; | |
| message.id = newId; | |
| if (message.ancestors && message.ancestors.length > 0) { | |
| const parentId = message.ancestors[message.ancestors.length - 1]; | |
| const parent = this.ctx.messages.find((m) => m.id === parentId); | |
| if (parent) { | |
| if (!parent.children) parent.children = []; | |
| const childIndex = parent.children.indexOf(oldId); | |
| if (childIndex !== -1) { | |
| parent.children[childIndex] = newId; | |
| } else { | |
| // Fallback: append if not found | |
| console.warn( | |
| `[TreeManager] Parent ${parentId} missing child ${oldId}, appending ${newId}` | |
| ); | |
| parent.children.push(newId); | |
| } | |
| } else { | |
| console.error( | |
| `[TreeManager] Parent ${parentId} not found for message ${oldId} -> ${newId}` | |
| ); | |
| } | |
| } | |
| console.debug(`[TreeManager] Synced message ID: ${oldId} -> ${newId}`); | |
| } | |
| } | |