Pre-MCP UI and architecture cleanup (#19689)
This commit is contained in:
@@ -1,54 +1,38 @@
|
||||
import { browser } from '$app/environment';
|
||||
/**
|
||||
* conversationsStore - Reactive State Store for Conversations
|
||||
*
|
||||
* Manages conversation lifecycle, persistence, navigation.
|
||||
*
|
||||
* **Architecture & Relationships:**
|
||||
* - **DatabaseService**: Stateless IndexedDB layer
|
||||
* - **conversationsStore** (this): Reactive state + business logic
|
||||
* - **chatStore**: Chat-specific state (streaming, loading)
|
||||
*
|
||||
* **Key Responsibilities:**
|
||||
* - Conversation CRUD (create, load, delete)
|
||||
* - Message management and tree navigation
|
||||
* - Import/Export functionality
|
||||
* - Title management with confirmation
|
||||
*
|
||||
* @see DatabaseService in services/database.ts for IndexedDB operations
|
||||
*/
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { DatabaseService } from '$lib/services/database.service';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import { filterByLeafNodeId, findLeafNode } from '$lib/utils';
|
||||
import { AttachmentType } from '$lib/enums';
|
||||
import { MessageRole } from '$lib/enums';
|
||||
|
||||
/**
|
||||
* conversationsStore - Persistent conversation data and lifecycle management
|
||||
*
|
||||
* **Terminology - Chat vs Conversation:**
|
||||
* - **Chat**: The active interaction space with the Chat Completions API. Represents the
|
||||
* real-time streaming session, loading states, and UI visualization of AI communication.
|
||||
* Managed by chatStore, a "chat" is ephemeral and exists during active AI interactions.
|
||||
* - **Conversation**: The persistent database entity storing all messages and metadata.
|
||||
* A "conversation" survives across sessions, page reloads, and browser restarts.
|
||||
* It contains the complete message history, branching structure, and conversation metadata.
|
||||
*
|
||||
* This store manages all conversation-level data and operations including creation, loading,
|
||||
* deletion, and navigation. It maintains the list of conversations and the currently active
|
||||
* conversation with its message history, providing reactive state for UI components.
|
||||
*
|
||||
* **Architecture & Relationships:**
|
||||
* - **conversationsStore** (this class): Persistent conversation data management
|
||||
* - Manages conversation list and active conversation state
|
||||
* - Handles conversation CRUD operations via DatabaseService
|
||||
* - Maintains active message array for current conversation
|
||||
* - Coordinates branching navigation (currNode tracking)
|
||||
*
|
||||
* - **chatStore**: Uses conversation data as context for active AI streaming
|
||||
* - **DatabaseService**: Low-level IndexedDB storage for conversations and messages
|
||||
*
|
||||
* **Key Features:**
|
||||
* - **Conversation Lifecycle**: Create, load, update, delete conversations
|
||||
* - **Message Management**: Active message array with branching support
|
||||
* - **Import/Export**: JSON-based conversation backup and restore
|
||||
* - **Branch Navigation**: Navigate between message tree branches
|
||||
* - **Title Management**: Auto-update titles with confirmation dialogs
|
||||
* - **Reactive State**: Svelte 5 runes for automatic UI updates
|
||||
*
|
||||
* **State Properties:**
|
||||
* - `conversations`: All conversations sorted by last modified
|
||||
* - `activeConversation`: Currently viewed conversation
|
||||
* - `activeMessages`: Messages in current conversation path
|
||||
* - `isInitialized`: Store initialization status
|
||||
*/
|
||||
class ConversationsStore {
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// State
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
/**
|
||||
*
|
||||
*
|
||||
* State
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
/** List of all conversations */
|
||||
conversations = $state<DatabaseConversation[]>([]);
|
||||
@@ -65,102 +49,110 @@ class ConversationsStore {
|
||||
/** Callback for title update confirmation dialog */
|
||||
titleUpdateConfirmationCallback?: (currentTitle: string, newTitle: string) => Promise<boolean>;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Modalities
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
/**
|
||||
*
|
||||
*
|
||||
* Lifecycle
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Modalities used in the active conversation.
|
||||
* Computed from attachments in activeMessages.
|
||||
* Used to filter available models - models must support all used modalities.
|
||||
* Initialize the store by loading conversations from database.
|
||||
* Must be called once after app startup.
|
||||
*/
|
||||
usedModalities: ModelModalities = $derived.by(() => {
|
||||
return this.calculateModalitiesFromMessages(this.activeMessages);
|
||||
});
|
||||
async init(): Promise<void> {
|
||||
if (!browser) return;
|
||||
if (this.isInitialized) return;
|
||||
|
||||
/**
|
||||
* Calculate modalities from a list of messages.
|
||||
* Helper method used by both usedModalities and getModalitiesUpToMessage.
|
||||
*/
|
||||
private calculateModalitiesFromMessages(messages: DatabaseMessage[]): ModelModalities {
|
||||
const modalities: ModelModalities = { vision: false, audio: false };
|
||||
|
||||
for (const message of messages) {
|
||||
if (!message.extra) continue;
|
||||
|
||||
for (const extra of message.extra) {
|
||||
if (extra.type === AttachmentType.IMAGE) {
|
||||
modalities.vision = true;
|
||||
}
|
||||
|
||||
// PDF only requires vision if processed as images
|
||||
if (extra.type === AttachmentType.PDF) {
|
||||
const pdfExtra = extra as DatabaseMessageExtraPdfFile;
|
||||
|
||||
if (pdfExtra.processedAsImages) {
|
||||
modalities.vision = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (extra.type === AttachmentType.AUDIO) {
|
||||
modalities.audio = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (modalities.vision && modalities.audio) break;
|
||||
}
|
||||
|
||||
return modalities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get modalities used in messages BEFORE the specified message.
|
||||
* Used for regeneration - only consider context that was available when generating this message.
|
||||
*/
|
||||
getModalitiesUpToMessage(messageId: string): ModelModalities {
|
||||
const messageIndex = this.activeMessages.findIndex((m) => m.id === messageId);
|
||||
|
||||
if (messageIndex === -1) {
|
||||
return this.usedModalities;
|
||||
}
|
||||
|
||||
const messagesBefore = this.activeMessages.slice(0, messageIndex);
|
||||
return this.calculateModalitiesFromMessages(messagesBefore);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
if (browser) {
|
||||
this.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Lifecycle
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Initializes the conversations store by loading conversations from the database
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
try {
|
||||
await this.loadConversations();
|
||||
this.isInitialized = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize conversations store:', error);
|
||||
console.error('Failed to initialize conversations:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for init() for backward compatibility.
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
return this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* Message Array Operations
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Adds a message to the active messages array
|
||||
*/
|
||||
addMessageToActive(message: DatabaseMessage): void {
|
||||
this.activeMessages.push(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a message at a specific index in active messages
|
||||
*/
|
||||
updateMessageAtIndex(index: number, updates: Partial<DatabaseMessage>): void {
|
||||
if (index !== -1 && this.activeMessages[index]) {
|
||||
this.activeMessages[index] = { ...this.activeMessages[index], ...updates };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the index of a message in active messages
|
||||
*/
|
||||
findMessageIndex(messageId: string): number {
|
||||
return this.activeMessages.findIndex((m) => m.id === messageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes messages from active messages starting at an index
|
||||
*/
|
||||
sliceActiveMessages(startIndex: number): void {
|
||||
this.activeMessages = this.activeMessages.slice(0, startIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a message from active messages by index
|
||||
*/
|
||||
removeMessageAtIndex(index: number): DatabaseMessage | undefined {
|
||||
if (index !== -1) {
|
||||
return this.activeMessages.splice(index, 1)[0];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the callback function for title update confirmations
|
||||
*/
|
||||
setTitleUpdateConfirmationCallback(
|
||||
callback: (currentTitle: string, newTitle: string) => Promise<boolean>
|
||||
): void {
|
||||
this.titleUpdateConfirmationCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* Conversation CRUD
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Loads all conversations from the database
|
||||
*/
|
||||
async loadConversations(): Promise<void> {
|
||||
this.conversations = await DatabaseService.getAllConversations();
|
||||
const conversations = await DatabaseService.getAllConversations();
|
||||
this.conversations = conversations;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Conversation CRUD
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Creates a new conversation and navigates to it
|
||||
* @param name - Optional name for the conversation
|
||||
@@ -170,7 +162,7 @@ class ConversationsStore {
|
||||
const conversationName = name || `Chat ${new Date().toLocaleString()}`;
|
||||
const conversation = await DatabaseService.createConversation(conversationName);
|
||||
|
||||
this.conversations.unshift(conversation);
|
||||
this.conversations = [conversation, ...this.conversations];
|
||||
this.activeConversation = conversation;
|
||||
this.activeMessages = [];
|
||||
|
||||
@@ -196,13 +188,15 @@ class ConversationsStore {
|
||||
|
||||
if (conversation.currNode) {
|
||||
const allMessages = await DatabaseService.getConversationMessages(convId);
|
||||
this.activeMessages = filterByLeafNodeId(
|
||||
const filteredMessages = filterByLeafNodeId(
|
||||
allMessages,
|
||||
conversation.currNode,
|
||||
false
|
||||
) as DatabaseMessage[];
|
||||
this.activeMessages = filteredMessages;
|
||||
} else {
|
||||
this.activeMessages = await DatabaseService.getConversationMessages(convId);
|
||||
const messages = await DatabaseService.getConversationMessages(convId);
|
||||
this.activeMessages = messages;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -213,169 +207,11 @@ class ConversationsStore {
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the active conversation and messages
|
||||
* Used when navigating away from chat or starting fresh
|
||||
* Clears the active conversation and messages.
|
||||
*/
|
||||
clearActiveConversation(): void {
|
||||
this.activeConversation = null;
|
||||
this.activeMessages = [];
|
||||
// Active processing conversation is now managed by chatStore
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Message Management
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Refreshes active messages based on currNode after branch navigation
|
||||
*/
|
||||
async refreshActiveMessages(): Promise<void> {
|
||||
if (!this.activeConversation) return;
|
||||
|
||||
const allMessages = await DatabaseService.getConversationMessages(this.activeConversation.id);
|
||||
|
||||
if (allMessages.length === 0) {
|
||||
this.activeMessages = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const leafNodeId =
|
||||
this.activeConversation.currNode ||
|
||||
allMessages.reduce((latest: DatabaseMessage, msg: DatabaseMessage) =>
|
||||
msg.timestamp > latest.timestamp ? msg : latest
|
||||
).id;
|
||||
|
||||
const currentPath = filterByLeafNodeId(allMessages, leafNodeId, false) as DatabaseMessage[];
|
||||
|
||||
this.activeMessages.length = 0;
|
||||
this.activeMessages.push(...currentPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the name of a conversation
|
||||
* @param convId - The conversation ID to update
|
||||
* @param name - The new name for the conversation
|
||||
*/
|
||||
async updateConversationName(convId: string, name: string): Promise<void> {
|
||||
try {
|
||||
await DatabaseService.updateConversation(convId, { name });
|
||||
|
||||
const convIndex = this.conversations.findIndex((c) => c.id === convId);
|
||||
|
||||
if (convIndex !== -1) {
|
||||
this.conversations[convIndex].name = name;
|
||||
}
|
||||
|
||||
if (this.activeConversation?.id === convId) {
|
||||
this.activeConversation.name = name;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update conversation name:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates conversation title with optional confirmation dialog based on settings
|
||||
* @param convId - The conversation ID to update
|
||||
* @param newTitle - The new title content
|
||||
* @param onConfirmationNeeded - Callback when user confirmation is needed
|
||||
* @returns True if title was updated, false if cancelled
|
||||
*/
|
||||
async updateConversationTitleWithConfirmation(
|
||||
convId: string,
|
||||
newTitle: string,
|
||||
onConfirmationNeeded?: (currentTitle: string, newTitle: string) => Promise<boolean>
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const currentConfig = config();
|
||||
|
||||
if (currentConfig.askForTitleConfirmation && onConfirmationNeeded) {
|
||||
const conversation = await DatabaseService.getConversation(convId);
|
||||
if (!conversation) return false;
|
||||
|
||||
const shouldUpdate = await onConfirmationNeeded(conversation.name, newTitle);
|
||||
if (!shouldUpdate) return false;
|
||||
}
|
||||
|
||||
await this.updateConversationName(convId, newTitle);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to update conversation title with confirmation:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Navigation
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Updates the current node of the active conversation
|
||||
* @param nodeId - The new current node ID
|
||||
*/
|
||||
async updateCurrentNode(nodeId: string): Promise<void> {
|
||||
if (!this.activeConversation) return;
|
||||
|
||||
await DatabaseService.updateCurrentNode(this.activeConversation.id, nodeId);
|
||||
this.activeConversation.currNode = nodeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates conversation lastModified timestamp and moves it to top of list
|
||||
*/
|
||||
updateConversationTimestamp(): void {
|
||||
if (!this.activeConversation) return;
|
||||
|
||||
const chatIndex = this.conversations.findIndex((c) => c.id === this.activeConversation!.id);
|
||||
|
||||
if (chatIndex !== -1) {
|
||||
this.conversations[chatIndex].lastModified = Date.now();
|
||||
const updatedConv = this.conversations.splice(chatIndex, 1)[0];
|
||||
this.conversations.unshift(updatedConv);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to a specific sibling branch by updating currNode and refreshing messages
|
||||
* @param siblingId - The sibling message ID to navigate to
|
||||
*/
|
||||
async navigateToSibling(siblingId: string): Promise<void> {
|
||||
if (!this.activeConversation) return;
|
||||
|
||||
const allMessages = await DatabaseService.getConversationMessages(this.activeConversation.id);
|
||||
const rootMessage = allMessages.find(
|
||||
(m: DatabaseMessage) => m.type === 'root' && m.parent === null
|
||||
);
|
||||
const currentFirstUserMessage = this.activeMessages.find(
|
||||
(m: DatabaseMessage) => m.role === 'user' && m.parent === rootMessage?.id
|
||||
);
|
||||
|
||||
const currentLeafNodeId = findLeafNode(allMessages, siblingId);
|
||||
|
||||
await DatabaseService.updateCurrentNode(this.activeConversation.id, currentLeafNodeId);
|
||||
this.activeConversation.currNode = currentLeafNodeId;
|
||||
await this.refreshActiveMessages();
|
||||
|
||||
// Only show title dialog if we're navigating between different first user message siblings
|
||||
if (rootMessage && this.activeMessages.length > 0) {
|
||||
const newFirstUserMessage = this.activeMessages.find(
|
||||
(m: DatabaseMessage) => m.role === 'user' && m.parent === rootMessage.id
|
||||
);
|
||||
|
||||
if (
|
||||
newFirstUserMessage &&
|
||||
newFirstUserMessage.content.trim() &&
|
||||
(!currentFirstUserMessage ||
|
||||
newFirstUserMessage.id !== currentFirstUserMessage.id ||
|
||||
newFirstUserMessage.content.trim() !== currentFirstUserMessage.content.trim())
|
||||
) {
|
||||
await this.updateConversationTitleWithConfirmation(
|
||||
this.activeConversation.id,
|
||||
newFirstUserMessage.content.trim(),
|
||||
this.titleUpdateConfirmationCallback
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -420,12 +256,192 @@ class ConversationsStore {
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Import/Export
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
/**
|
||||
*
|
||||
*
|
||||
* Message Management
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Downloads a conversation as JSON file
|
||||
* Refreshes active messages based on currNode after branch navigation.
|
||||
*/
|
||||
async refreshActiveMessages(): Promise<void> {
|
||||
if (!this.activeConversation) return;
|
||||
|
||||
const allMessages = await DatabaseService.getConversationMessages(this.activeConversation.id);
|
||||
|
||||
if (allMessages.length === 0) {
|
||||
this.activeMessages = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const leafNodeId =
|
||||
this.activeConversation.currNode ||
|
||||
allMessages.reduce((latest, msg) => (msg.timestamp > latest.timestamp ? msg : latest)).id;
|
||||
|
||||
const currentPath = filterByLeafNodeId(allMessages, leafNodeId, false) as DatabaseMessage[];
|
||||
|
||||
this.activeMessages = currentPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all messages for a specific conversation
|
||||
* @param convId - The conversation ID
|
||||
* @returns Array of messages
|
||||
*/
|
||||
async getConversationMessages(convId: string): Promise<DatabaseMessage[]> {
|
||||
return await DatabaseService.getConversationMessages(convId);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* Title Management
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Updates the name of a conversation.
|
||||
* @param convId - The conversation ID to update
|
||||
* @param name - The new name for the conversation
|
||||
*/
|
||||
async updateConversationName(convId: string, name: string): Promise<void> {
|
||||
try {
|
||||
await DatabaseService.updateConversation(convId, { name });
|
||||
|
||||
const convIndex = this.conversations.findIndex((c) => c.id === convId);
|
||||
|
||||
if (convIndex !== -1) {
|
||||
this.conversations[convIndex].name = name;
|
||||
this.conversations = [...this.conversations];
|
||||
}
|
||||
|
||||
if (this.activeConversation?.id === convId) {
|
||||
this.activeConversation = { ...this.activeConversation, name };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update conversation name:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates conversation title with optional confirmation dialog based on settings
|
||||
* @param convId - The conversation ID to update
|
||||
* @param newTitle - The new title content
|
||||
* @returns True if title was updated, false if cancelled
|
||||
*/
|
||||
async updateConversationTitleWithConfirmation(
|
||||
convId: string,
|
||||
newTitle: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const currentConfig = config();
|
||||
|
||||
if (currentConfig.askForTitleConfirmation && this.titleUpdateConfirmationCallback) {
|
||||
const conversation = await DatabaseService.getConversation(convId);
|
||||
if (!conversation) return false;
|
||||
|
||||
const shouldUpdate = await this.titleUpdateConfirmationCallback(
|
||||
conversation.name,
|
||||
newTitle
|
||||
);
|
||||
if (!shouldUpdate) return false;
|
||||
}
|
||||
|
||||
await this.updateConversationName(convId, newTitle);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to update conversation title with confirmation:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates conversation lastModified timestamp and moves it to top of list
|
||||
*/
|
||||
updateConversationTimestamp(): void {
|
||||
if (!this.activeConversation) return;
|
||||
|
||||
const chatIndex = this.conversations.findIndex((c) => c.id === this.activeConversation!.id);
|
||||
|
||||
if (chatIndex !== -1) {
|
||||
this.conversations[chatIndex].lastModified = Date.now();
|
||||
const updatedConv = this.conversations.splice(chatIndex, 1)[0];
|
||||
this.conversations = [updatedConv, ...this.conversations];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current node of the active conversation
|
||||
* @param nodeId - The new current node ID
|
||||
*/
|
||||
async updateCurrentNode(nodeId: string): Promise<void> {
|
||||
if (!this.activeConversation) return;
|
||||
|
||||
await DatabaseService.updateCurrentNode(this.activeConversation.id, nodeId);
|
||||
this.activeConversation = { ...this.activeConversation, currNode: nodeId };
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* Branch Navigation
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Navigates to a specific sibling branch by updating currNode and refreshing messages.
|
||||
* @param siblingId - The sibling message ID to navigate to
|
||||
*/
|
||||
async navigateToSibling(siblingId: string): Promise<void> {
|
||||
if (!this.activeConversation) return;
|
||||
|
||||
const allMessages = await DatabaseService.getConversationMessages(this.activeConversation.id);
|
||||
const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null);
|
||||
const currentFirstUserMessage = this.activeMessages.find(
|
||||
(m) => m.role === MessageRole.USER && m.parent === rootMessage?.id
|
||||
);
|
||||
|
||||
const currentLeafNodeId = findLeafNode(allMessages, siblingId);
|
||||
|
||||
await DatabaseService.updateCurrentNode(this.activeConversation.id, currentLeafNodeId);
|
||||
this.activeConversation = { ...this.activeConversation, currNode: currentLeafNodeId };
|
||||
await this.refreshActiveMessages();
|
||||
|
||||
if (rootMessage && this.activeMessages.length > 0) {
|
||||
const newFirstUserMessage = this.activeMessages.find(
|
||||
(m) => m.role === MessageRole.USER && m.parent === rootMessage.id
|
||||
);
|
||||
|
||||
if (
|
||||
newFirstUserMessage &&
|
||||
newFirstUserMessage.content.trim() &&
|
||||
(!currentFirstUserMessage ||
|
||||
newFirstUserMessage.id !== currentFirstUserMessage.id ||
|
||||
newFirstUserMessage.content.trim() !== currentFirstUserMessage.content.trim())
|
||||
) {
|
||||
await this.updateConversationTitleWithConfirmation(
|
||||
this.activeConversation.id,
|
||||
newFirstUserMessage.content.trim()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* Import & Export
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Downloads a conversation as JSON file.
|
||||
* @param convId - The conversation ID to download
|
||||
*/
|
||||
async downloadConversation(convId: string): Promise<void> {
|
||||
@@ -456,7 +472,7 @@ class ConversationsStore {
|
||||
}
|
||||
|
||||
const allData = await Promise.all(
|
||||
allConversations.map(async (conv: DatabaseConversation) => {
|
||||
allConversations.map(async (conv) => {
|
||||
const messages = await DatabaseService.getConversationMessages(conv.id);
|
||||
return { conv, messages };
|
||||
})
|
||||
@@ -536,15 +552,6 @@ class ConversationsStore {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all messages for a specific conversation
|
||||
* @param convId - The conversation ID
|
||||
* @returns Array of messages
|
||||
*/
|
||||
async getConversationMessages(convId: string): Promise<DatabaseMessage[]> {
|
||||
return await DatabaseService.getConversationMessages(convId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports conversations from provided data (without file picker)
|
||||
* @param data - Array of conversation data with messages
|
||||
@@ -558,61 +565,8 @@ class ConversationsStore {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a message to the active messages array
|
||||
* Used by chatStore when creating new messages
|
||||
* @param message - The message to add
|
||||
*/
|
||||
addMessageToActive(message: DatabaseMessage): void {
|
||||
this.activeMessages.push(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a message at a specific index in active messages
|
||||
* Creates a new object to trigger Svelte 5 reactivity
|
||||
* @param index - The index of the message to update
|
||||
* @param updates - Partial message data to update
|
||||
*/
|
||||
updateMessageAtIndex(index: number, updates: Partial<DatabaseMessage>): void {
|
||||
if (index !== -1 && this.activeMessages[index]) {
|
||||
// Create new object to trigger Svelte 5 reactivity
|
||||
this.activeMessages[index] = { ...this.activeMessages[index], ...updates };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the index of a message in active messages
|
||||
* @param messageId - The message ID to find
|
||||
* @returns The index of the message, or -1 if not found
|
||||
*/
|
||||
findMessageIndex(messageId: string): number {
|
||||
return this.activeMessages.findIndex((m) => m.id === messageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes messages from active messages starting at an index
|
||||
* @param startIndex - The index to start removing from
|
||||
*/
|
||||
sliceActiveMessages(startIndex: number): void {
|
||||
this.activeMessages = this.activeMessages.slice(0, startIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a message from active messages by index
|
||||
* @param index - The index to remove
|
||||
* @returns The removed message or undefined
|
||||
*/
|
||||
removeMessageAtIndex(index: number): DatabaseMessage | undefined {
|
||||
if (index !== -1) {
|
||||
return this.activeMessages.splice(index, 1)[0];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers file download in browser
|
||||
* @param data - The data to download
|
||||
* @param filename - Optional filename for the download
|
||||
*/
|
||||
private triggerDownload(data: ExportedConversations, filename?: string): void {
|
||||
const conversation =
|
||||
@@ -641,26 +595,16 @@ class ConversationsStore {
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Utilities
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Sets the callback function for title update confirmations
|
||||
* @param callback - Function to call when confirmation is needed
|
||||
*/
|
||||
setTitleUpdateConfirmationCallback(
|
||||
callback: (currentTitle: string, newTitle: string) => Promise<boolean>
|
||||
): void {
|
||||
this.titleUpdateConfirmationCallback = callback;
|
||||
}
|
||||
}
|
||||
|
||||
export const conversationsStore = new ConversationsStore();
|
||||
|
||||
// Auto-initialize in browser
|
||||
if (browser) {
|
||||
conversationsStore.init();
|
||||
}
|
||||
|
||||
export const conversations = () => conversationsStore.conversations;
|
||||
export const activeConversation = () => conversationsStore.activeConversation;
|
||||
export const activeMessages = () => conversationsStore.activeMessages;
|
||||
export const isConversationsInitialized = () => conversationsStore.isInitialized;
|
||||
export const usedModalities = () => conversationsStore.usedModalities;
|
||||
|
||||
Reference in New Issue
Block a user