refactor: Database, Conversations & Chat services + stores architecture improvements (WIP)
This commit is contained in:
parent
ccd6c27183
commit
fed6c82eeb
|
|
@ -2,15 +2,14 @@
|
|||
import { ChatMessage } from '$lib/components/app';
|
||||
import { DatabaseService } from '$lib/services/database';
|
||||
import {
|
||||
activeConversation,
|
||||
continueAssistantMessage,
|
||||
deleteMessage,
|
||||
editAssistantMessage,
|
||||
editMessageWithBranching,
|
||||
editUserMessagePreserveResponses,
|
||||
navigateToSibling,
|
||||
regenerateMessageWithBranching
|
||||
} from '$lib/stores/chat.svelte';
|
||||
import { activeConversation, navigateToSibling } from '$lib/stores/conversations.svelte';
|
||||
import { getMessageSiblings } from '$lib/utils/branching';
|
||||
|
||||
interface Props {
|
||||
|
|
|
|||
|
|
@ -19,15 +19,17 @@
|
|||
INITIAL_SCROLL_DELAY
|
||||
} from '$lib/constants/auto-scroll';
|
||||
import {
|
||||
activeMessages,
|
||||
activeConversation,
|
||||
deleteConversation,
|
||||
dismissErrorDialog,
|
||||
errorDialog,
|
||||
isLoading,
|
||||
sendMessage,
|
||||
stopGeneration
|
||||
} from '$lib/stores/chat.svelte';
|
||||
import {
|
||||
activeMessages,
|
||||
activeConversation,
|
||||
deleteConversation
|
||||
} from '$lib/stores/conversations.svelte';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import {
|
||||
supportsVision,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
import { PROCESSING_INFO_TIMEOUT } from '$lib/constants/processing-info';
|
||||
import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
|
||||
import { slotsService } from '$lib/services/slots';
|
||||
import { isLoading, activeMessages, activeConversation } from '$lib/stores/chat.svelte';
|
||||
import { isLoading } from '$lib/stores/chat.svelte';
|
||||
import { activeMessages, activeConversation } from '$lib/stores/conversations.svelte';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
|
||||
const processingState = useProcessingState();
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
import { DatabaseService } from '$lib/services/database';
|
||||
import type { ExportedConversations } from '$lib/types/database';
|
||||
import { createMessageCountMap } from '$lib/utils/conversation-utils';
|
||||
import { chatStore } from '$lib/stores/chat.svelte';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
|
||||
let exportedConversations = $state<DatabaseConversation[]>([]);
|
||||
let importedConversations = $state<DatabaseConversation[]>([]);
|
||||
|
|
@ -138,7 +138,7 @@
|
|||
|
||||
await DatabaseService.importConversations(selectedData);
|
||||
|
||||
await chatStore.loadConversations();
|
||||
await conversationsStore.loadConversations();
|
||||
|
||||
importedConversations = selectedConversations;
|
||||
showImportSummary = true;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
conversations,
|
||||
deleteConversation,
|
||||
updateConversationName
|
||||
} from '$lib/stores/chat.svelte';
|
||||
} from '$lib/stores/conversations.svelte';
|
||||
import ChatSidebarActions from './ChatSidebarActions.svelte';
|
||||
|
||||
const sidebar = Sidebar.useSidebar();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { Trash2, Pencil, MoreHorizontal, Download, Loader2 } from '@lucide/svelte';
|
||||
import { ActionDropdown } from '$lib/components/app';
|
||||
import { downloadConversation, getAllLoadingConversations } from '$lib/stores/chat.svelte';
|
||||
import { getAllLoadingChats } from '$lib/stores/chat.svelte';
|
||||
import { downloadConversation } from '$lib/stores/conversations.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -25,7 +26,7 @@
|
|||
let renderActionsDropdown = $state(false);
|
||||
let dropdownOpen = $state(false);
|
||||
|
||||
let isLoading = $derived(getAllLoadingConversations().includes(conversation.id));
|
||||
let isLoading = $derived(getAllLoadingChats().includes(conversation.id));
|
||||
|
||||
function handleEdit(event: Event) {
|
||||
event.stopPropagation();
|
||||
|
|
|
|||
|
|
@ -22,26 +22,32 @@ import type {
|
|||
} from '$lib/types/database';
|
||||
import type { ChatMessagePromptProgress, ChatMessageTimings } from '$lib/types/chat';
|
||||
import type { SettingsChatServiceOptions } from '$lib/types/settings';
|
||||
|
||||
/**
|
||||
* ChatService - Low-level API communication layer for llama.cpp server interactions
|
||||
* ChatService - Low-level API communication layer for Chat Completions
|
||||
*
|
||||
* This service handles direct communication with the llama.cpp server's chat completion API.
|
||||
* **Terminology - Chat vs Conversation:**
|
||||
* - **Chat**: The active interaction space with the Chat Completions API. This service
|
||||
* handles the real-time communication with the AI backend - sending messages, receiving
|
||||
* streaming responses, and managing request lifecycles. "Chat" is ephemeral and runtime-focused.
|
||||
* - **Conversation**: The persistent database entity storing all messages and metadata.
|
||||
* Managed by ConversationsService/Store, conversations persist across sessions.
|
||||
*
|
||||
* This service handles direct communication with the llama-server's Chat Completions API.
|
||||
* It provides the network layer abstraction for AI model interactions while remaining
|
||||
* stateless and focused purely on API communication.
|
||||
*
|
||||
* **Architecture & Relationship with ChatStore:**
|
||||
* **Architecture & Relationships:**
|
||||
* - **ChatService** (this class): Stateless API communication layer
|
||||
* - Handles HTTP requests/responses with llama.cpp server
|
||||
* - Handles HTTP requests/responses with the llama-server
|
||||
* - Manages streaming and non-streaming response parsing
|
||||
* - Provides request abortion capabilities
|
||||
* - Provides per-conversation request abortion capabilities
|
||||
* - Converts database messages to API format
|
||||
* - Handles error translation for server responses
|
||||
*
|
||||
* - **ChatStore**: Stateful orchestration and UI state management
|
||||
* - Uses ChatService for all AI model communication
|
||||
* - Manages conversation state, message history, and UI reactivity
|
||||
* - Coordinates with DatabaseService for persistence
|
||||
* - Handles complex workflows like branching and regeneration
|
||||
* - **ChatStore**: Uses ChatService for all AI model communication
|
||||
* - **SlotsService**: Receives timing data updates during streaming
|
||||
* - **ConversationsStore**: Provides message context for API requests
|
||||
*
|
||||
* **Key Responsibilities:**
|
||||
* - Message format conversion (DatabaseMessage → API format)
|
||||
|
|
@ -694,7 +700,7 @@ export class ChatService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get server properties - static method for API compatibility
|
||||
* Get server properties - static method for API compatibility (to be refactored)
|
||||
*/
|
||||
static async getServerProps(): Promise<ApiLlamaCppServerProps> {
|
||||
try {
|
||||
|
|
@ -721,7 +727,7 @@ export class ChatService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get model information from /models endpoint
|
||||
* Get model information from /models endpoint (to be refactored)
|
||||
*/
|
||||
static async getModels(): Promise<ApiModelListResponse> {
|
||||
try {
|
||||
|
|
@ -753,7 +759,7 @@ export class ChatService {
|
|||
*
|
||||
* @public
|
||||
*/
|
||||
public abort(conversationId?: string): void {
|
||||
public abortChatCompletionRequest(conversationId?: string): void {
|
||||
if (conversationId) {
|
||||
const abortController = this.abortControllers.get(conversationId);
|
||||
if (abortController) {
|
||||
|
|
@ -828,6 +834,14 @@ export class ChatService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts model name from Chat Completions API response data.
|
||||
* Handles various response formats including streaming chunks and final responses.
|
||||
*
|
||||
* @param data - Raw response data from the Chat Completions API
|
||||
* @returns Model name string if found, undefined otherwise
|
||||
* @private
|
||||
*/
|
||||
private extractModelName(data: unknown): string | undefined {
|
||||
const asRecord = (value: unknown): Record<string, unknown> | undefined => {
|
||||
return typeof value === 'object' && value !== null
|
||||
|
|
@ -861,6 +875,15 @@ export class ChatService {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the processing state in SlotsService with timing data from streaming response.
|
||||
* Calculates tokens per second and forwards metrics for UI display.
|
||||
*
|
||||
* @param timings - Timing information from the Chat Completions API response
|
||||
* @param promptProgress - Prompt processing progress data
|
||||
* @param conversationId - Optional conversation ID for per-conversation state tracking
|
||||
* @private
|
||||
*/
|
||||
private updateProcessingState(
|
||||
timings?: ChatMessageTimings,
|
||||
promptProgress?: ChatMessagePromptProgress,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,279 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { DatabaseService } from '$lib/services/database';
|
||||
import type {
|
||||
DatabaseConversation,
|
||||
DatabaseMessage,
|
||||
ExportedConversations
|
||||
} from '$lib/types/database';
|
||||
|
||||
/**
|
||||
* ConversationsService - Database operations for persistent conversation management
|
||||
*
|
||||
* **Terminology - Chat vs Conversation:**
|
||||
* - **Chat**: The active interaction space with the Chat Completions API. Represents the
|
||||
* real-time streaming session and UI visualization. Managed by ChatService/Store.
|
||||
* - **Conversation**: The persistent database entity storing all messages and metadata.
|
||||
* This service handles all database operations for conversations - they survive across
|
||||
* sessions, page reloads, and browser restarts. Contains message history, branching
|
||||
* structure, timestamps, and conversation metadata.
|
||||
*
|
||||
* This service handles all conversation-level database operations including CRUD,
|
||||
* import/export, and navigation. It provides a stateless abstraction layer between
|
||||
* ConversationsStore and DatabaseService.
|
||||
*
|
||||
* **Architecture & Relationships:**
|
||||
* - **ConversationsService** (this class): Stateless database operations layer
|
||||
* - Handles conversation CRUD operations via DatabaseService
|
||||
* - Manages import/export with JSON serialization
|
||||
* - Provides navigation helpers for routing
|
||||
* - Does not manage reactive UI state
|
||||
*
|
||||
* - **ConversationsStore**: Uses this service for all database operations
|
||||
* - **DatabaseService**: Low-level IndexedDB operations
|
||||
* - **ChatStore**: Indirectly uses conversation data for AI context
|
||||
*
|
||||
* **Key Responsibilities:**
|
||||
* - Conversation CRUD (create, read, update, delete)
|
||||
* - Message retrieval for conversations
|
||||
* - Import/export with file download/upload
|
||||
* - Navigation helpers (goto conversation, new chat)
|
||||
*/
|
||||
export class ConversationsService {
|
||||
/**
|
||||
* Creates a new conversation in the database
|
||||
* @param name - Optional name for the conversation, defaults to timestamped name
|
||||
* @returns The created conversation
|
||||
*/
|
||||
async createConversation(name?: string): Promise<DatabaseConversation> {
|
||||
const conversationName = name || `Chat ${new Date().toLocaleString()}`;
|
||||
return await DatabaseService.createConversation(conversationName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all conversations from the database
|
||||
* @returns Array of all conversations
|
||||
*/
|
||||
async loadAllConversations(): Promise<DatabaseConversation[]> {
|
||||
return await DatabaseService.getAllConversations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a specific conversation by ID
|
||||
* @param convId - The conversation ID to load
|
||||
* @returns The conversation or null if not found
|
||||
*/
|
||||
async loadConversation(convId: string): Promise<DatabaseConversation | null> {
|
||||
return await DatabaseService.getConversation(convId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all messages for a conversation
|
||||
* @param convId - The conversation ID
|
||||
* @returns Array of messages in the conversation
|
||||
*/
|
||||
async getConversationMessages(convId: string): Promise<DatabaseMessage[]> {
|
||||
return await DatabaseService.getConversationMessages(convId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
await DatabaseService.updateConversation(convId, { name });
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current node (currNode) of a conversation
|
||||
* @param convId - The conversation ID to update
|
||||
* @param nodeId - The new current node ID
|
||||
*/
|
||||
async updateCurrentNode(convId: string, nodeId: string): Promise<void> {
|
||||
await DatabaseService.updateCurrentNode(convId, nodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the lastModified timestamp of a conversation
|
||||
* @param convId - The conversation ID to update
|
||||
*/
|
||||
async updateTimestamp(convId: string): Promise<void> {
|
||||
await DatabaseService.updateConversation(convId, { lastModified: Date.now() });
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a conversation and all its messages
|
||||
* @param convId - The conversation ID to delete
|
||||
*/
|
||||
async deleteConversation(convId: string): Promise<void> {
|
||||
await DatabaseService.deleteConversation(convId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a conversation as JSON file
|
||||
* @param conversation - The conversation to download
|
||||
* @param messages - The messages in the conversation
|
||||
*/
|
||||
downloadConversation(conversation: DatabaseConversation, messages: DatabaseMessage[]): void {
|
||||
const conversationData: ExportedConversations = {
|
||||
conv: conversation,
|
||||
messages
|
||||
};
|
||||
|
||||
this.triggerDownload(conversationData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports all conversations with their messages as a JSON file
|
||||
* @returns The list of exported conversations
|
||||
*/
|
||||
async exportAllConversations(): Promise<DatabaseConversation[]> {
|
||||
const allConversations = await DatabaseService.getAllConversations();
|
||||
|
||||
if (allConversations.length === 0) {
|
||||
throw new Error('No conversations to export');
|
||||
}
|
||||
|
||||
const allData: ExportedConversations = await Promise.all(
|
||||
allConversations.map(async (conv) => {
|
||||
const messages = await DatabaseService.getConversationMessages(conv.id);
|
||||
return { conv, messages };
|
||||
})
|
||||
);
|
||||
|
||||
const blob = new Blob([JSON.stringify(allData, null, 2)], {
|
||||
type: 'application/json'
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `all_conversations_${new Date().toISOString().split('T')[0]}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
toast.success(`All conversations (${allConversations.length}) prepared for download`);
|
||||
|
||||
return allConversations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports conversations from a JSON file
|
||||
* Opens file picker and processes the selected file
|
||||
* @returns Promise resolving to the list of imported conversations
|
||||
*/
|
||||
async importConversations(): Promise<DatabaseConversation[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement)?.files?.[0];
|
||||
|
||||
if (!file) {
|
||||
reject(new Error('No file selected'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const text = await file.text();
|
||||
const parsedData = JSON.parse(text);
|
||||
let importedData: ExportedConversations;
|
||||
|
||||
if (Array.isArray(parsedData)) {
|
||||
importedData = parsedData;
|
||||
} else if (
|
||||
parsedData &&
|
||||
typeof parsedData === 'object' &&
|
||||
'conv' in parsedData &&
|
||||
'messages' in parsedData
|
||||
) {
|
||||
// Single conversation object
|
||||
importedData = [parsedData];
|
||||
} else {
|
||||
throw new Error(
|
||||
'Invalid file format: expected array of conversations or single conversation object'
|
||||
);
|
||||
}
|
||||
|
||||
const result = await DatabaseService.importConversations(importedData);
|
||||
|
||||
toast.success(`Imported ${result.imported} conversation(s), skipped ${result.skipped}`);
|
||||
|
||||
// Extract the conversation objects from imported data
|
||||
const importedConversations = (
|
||||
Array.isArray(importedData) ? importedData : [importedData]
|
||||
).map((item) => item.conv);
|
||||
|
||||
resolve(importedConversations);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
console.error('Failed to import conversations:', err);
|
||||
toast.error('Import failed', {
|
||||
description: message
|
||||
});
|
||||
reject(new Error(`Import failed: ${message}`));
|
||||
}
|
||||
};
|
||||
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to a specific conversation route
|
||||
* @param convId - The conversation ID to navigate to
|
||||
*/
|
||||
async navigateToConversation(convId: string): Promise<void> {
|
||||
await goto(`#/chat/${convId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to new chat route
|
||||
*/
|
||||
async navigateToNewChat(): Promise<void> {
|
||||
await goto(`?new_chat=true#/`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers file download in browser
|
||||
* @param data - Data to download
|
||||
* @param filename - Optional filename
|
||||
*/
|
||||
private triggerDownload(data: ExportedConversations, filename?: string): void {
|
||||
const conversation =
|
||||
'conv' in data ? data.conv : Array.isArray(data) ? data[0]?.conv : undefined;
|
||||
|
||||
if (!conversation) {
|
||||
console.error('Invalid data: missing conversation');
|
||||
return;
|
||||
}
|
||||
|
||||
const conversationName = conversation.name ? conversation.name.trim() : '';
|
||||
const convId = conversation.id || 'unknown';
|
||||
const truncatedSuffix = conversationName
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]/gi, '_')
|
||||
.replace(/_+/g, '_')
|
||||
.substring(0, 20);
|
||||
const downloadFilename = filename || `conversation_${convId}_${truncatedSuffix}.json`;
|
||||
|
||||
const conversationJson = JSON.stringify(data, null, 2);
|
||||
const blob = new Blob([conversationJson], {
|
||||
type: 'application/json'
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = downloadFilename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
export const conversationsService = new ConversationsService();
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import Dexie, { type EntityTable } from 'dexie';
|
||||
import { filterByLeafNodeId, findDescendantMessages } from '$lib/utils/branching';
|
||||
import { findDescendantMessages } from '$lib/utils/branching';
|
||||
|
||||
class LlamacppDatabase extends Dexie {
|
||||
conversations!: EntityTable<DatabaseConversation, string>;
|
||||
|
|
@ -16,61 +16,56 @@ class LlamacppDatabase extends Dexie {
|
|||
}
|
||||
|
||||
const db = new LlamacppDatabase();
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
/**
|
||||
* DatabaseService - Persistent data layer for conversation and message management
|
||||
* DatabaseService - Stateless IndexedDB communication layer
|
||||
*
|
||||
* This service provides a comprehensive data access layer built on IndexedDB using Dexie.
|
||||
* It handles all persistent storage operations for conversations, messages, and application settings
|
||||
* with support for complex conversation branching and message threading.
|
||||
* **Terminology - Chat vs Conversation:**
|
||||
* - **Chat**: The active interaction space with the Chat Completions API (ephemeral, runtime).
|
||||
* - **Conversation**: The persistent database entity storing all messages and metadata.
|
||||
* This service handles raw database operations for conversations - the lowest layer
|
||||
* in the persistence stack.
|
||||
*
|
||||
* **Architecture & Relationships:**
|
||||
* - **DatabaseService** (this class): Stateless data persistence layer
|
||||
* - Manages IndexedDB operations through Dexie ORM
|
||||
* - Handles conversation and message CRUD operations
|
||||
* - Supports complex branching with parent-child relationships
|
||||
* This service provides a stateless data access layer built on IndexedDB using Dexie ORM.
|
||||
* It handles all low-level storage operations for conversations and messages with support
|
||||
* for complex branching and message threading. All methods are static - no instance state.
|
||||
*
|
||||
* **Architecture & Relationships (bottom to top):**
|
||||
* - **DatabaseService** (this class): Stateless IndexedDB operations
|
||||
* - Lowest layer - direct Dexie/IndexedDB communication
|
||||
* - Pure CRUD operations without business logic
|
||||
* - Handles branching tree structure (parent-child relationships)
|
||||
* - Provides transaction safety for multi-table operations
|
||||
*
|
||||
* - **ChatStore & ConversationsStore**: Primary consumers for state management
|
||||
* - Use DatabaseService for all persistence operations
|
||||
* - Coordinate UI state with database state
|
||||
* - Handle conversation lifecycle and message branching
|
||||
* - **ConversationsService**: Stateless business logic layer
|
||||
* - Uses DatabaseService for all persistence operations
|
||||
* - Adds import/export, navigation, and higher-level operations
|
||||
*
|
||||
* - **ConversationsStore**: Reactive state management for conversations
|
||||
* - Uses ConversationsService for database operations
|
||||
* - Manages conversation list, active conversation, and messages in memory
|
||||
*
|
||||
* - **ChatStore**: Active AI interaction management
|
||||
* - Uses ConversationsStore for conversation context
|
||||
* - Directly uses DatabaseService for message CRUD during streaming
|
||||
*
|
||||
* **Key Features:**
|
||||
* - **Conversation Management**: Create, read, update, delete conversations
|
||||
* - **Message Branching**: Support for tree-like conversation structures
|
||||
* - **Conversation CRUD**: Create, read, update, delete conversations
|
||||
* - **Message CRUD**: Add, update, delete messages with branching support
|
||||
* - **Branch Operations**: Create branches, find descendants, cascade deletions
|
||||
* - **Transaction Safety**: Atomic operations for data consistency
|
||||
* - **Path Resolution**: Navigate conversation branches and find leaf nodes
|
||||
* - **Cascading Deletion**: Remove entire conversation branches
|
||||
*
|
||||
* **Database Schema:**
|
||||
* - `conversations`: Conversation metadata with current node tracking
|
||||
* - `messages`: Individual messages with parent-child relationships
|
||||
* - `conversations`: id, lastModified, currNode, name
|
||||
* - `messages`: id, convId, type, role, timestamp, parent, children
|
||||
*
|
||||
* **Branching Model:**
|
||||
* Messages form a tree structure where each message can have multiple children,
|
||||
* enabling conversation branching and alternative response paths. The conversation's
|
||||
* `currNode` tracks the currently active branch endpoint.
|
||||
*/
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export class DatabaseService {
|
||||
/**
|
||||
* Adds a new message to the database.
|
||||
*
|
||||
* @param message - Message to add (without id)
|
||||
* @returns The created message
|
||||
*/
|
||||
static async addMessage(message: Omit<DatabaseMessage, 'id'>): Promise<DatabaseMessage> {
|
||||
const newMessage: DatabaseMessage = {
|
||||
...message,
|
||||
id: uuid()
|
||||
};
|
||||
|
||||
await db.messages.add(newMessage);
|
||||
return newMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new conversation.
|
||||
*
|
||||
|
|
@ -255,18 +250,6 @@ export class DatabaseService {
|
|||
return await db.conversations.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all leaf nodes (messages with no children) in a conversation.
|
||||
* Useful for finding all possible conversation endpoints.
|
||||
*
|
||||
* @param convId - Conversation ID
|
||||
* @returns Array of leaf node message IDs
|
||||
*/
|
||||
static async getConversationLeafNodes(convId: string): Promise<string[]> {
|
||||
const allMessages = await this.getConversationMessages(convId);
|
||||
return allMessages.filter((msg) => msg.children.length === 0).map((msg) => msg.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all messages in a conversation, sorted by timestamp (oldest first).
|
||||
*
|
||||
|
|
@ -277,34 +260,6 @@ export class DatabaseService {
|
|||
return await db.messages.where('convId').equals(convId).sortBy('timestamp');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the conversation path from root to the current leaf node.
|
||||
* Uses the conversation's currNode to determine the active branch.
|
||||
*
|
||||
* @param convId - Conversation ID
|
||||
* @returns Array of messages in the current conversation path
|
||||
*/
|
||||
static async getConversationPath(convId: string): Promise<DatabaseMessage[]> {
|
||||
const conversation = await this.getConversation(convId);
|
||||
|
||||
if (!conversation) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const allMessages = await this.getConversationMessages(convId);
|
||||
|
||||
if (allMessages.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// If no currNode is set, use the latest message as leaf
|
||||
const leafNodeId =
|
||||
conversation.currNode ||
|
||||
allMessages.reduce((latest, msg) => (msg.timestamp > latest.timestamp ? msg : latest)).id;
|
||||
|
||||
return filterByLeafNodeId(allMessages, leafNodeId, false) as DatabaseMessage[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a conversation.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export { chatService } from './chat';
|
||||
export { slotsService } from './slots';
|
||||
export { PropsService } from './props';
|
||||
export { conversationsService } from './conversations';
|
||||
|
|
|
|||
|
|
@ -279,8 +279,8 @@ export class SlotsService {
|
|||
return this.lastKnownState;
|
||||
}
|
||||
try {
|
||||
const { chatStore } = await import('$lib/stores/chat.svelte');
|
||||
const messages = chatStore.activeMessages;
|
||||
const { conversationsStore } = await import('$lib/stores/conversations.svelte');
|
||||
const messages = conversationsStore.activeMessages;
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const message = messages[i];
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,459 @@
|
|||
import { browser } from '$app/environment';
|
||||
import { conversationsService } from '$lib/services/conversations';
|
||||
import { slotsService } from '$lib/services/slots';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import { filterByLeafNodeId, findLeafNode } from '$lib/utils/branching';
|
||||
import type { DatabaseConversation, DatabaseMessage } from '$lib/types/database';
|
||||
|
||||
/**
|
||||
* 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 ConversationsService
|
||||
* - Maintains active message array for current conversation
|
||||
* - Coordinates branching navigation (currNode tracking)
|
||||
*
|
||||
* - **ChatStore**: Uses conversation data as context for active AI streaming
|
||||
* - **ConversationsService**: Database operations for conversation persistence
|
||||
* - **SlotsService**: Notified of active conversation changes
|
||||
* - **DatabaseService**: Low-level 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 {
|
||||
/** List of all conversations */
|
||||
conversations = $state<DatabaseConversation[]>([]);
|
||||
|
||||
/** Currently active conversation */
|
||||
activeConversation = $state<DatabaseConversation | null>(null);
|
||||
|
||||
/** Messages in the active conversation (filtered by currNode path) */
|
||||
activeMessages = $state<DatabaseMessage[]>([]);
|
||||
|
||||
/** Whether the store has been initialized */
|
||||
isInitialized = $state(false);
|
||||
|
||||
/** Callback for title update confirmation dialog */
|
||||
titleUpdateConfirmationCallback?: (currentTitle: string, newTitle: string) => Promise<boolean>;
|
||||
|
||||
constructor() {
|
||||
if (browser) {
|
||||
this.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all conversations from the database
|
||||
*/
|
||||
async loadConversations(): Promise<void> {
|
||||
this.conversations = await conversationsService.loadAllConversations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new conversation and navigates to it
|
||||
* @param name - Optional name for the conversation
|
||||
* @returns The ID of the created conversation
|
||||
*/
|
||||
async createConversation(name?: string): Promise<string> {
|
||||
const conversation = await conversationsService.createConversation(name);
|
||||
|
||||
this.conversations.unshift(conversation);
|
||||
this.activeConversation = conversation;
|
||||
this.activeMessages = [];
|
||||
|
||||
slotsService.setActiveConversation(conversation.id);
|
||||
|
||||
await conversationsService.navigateToConversation(conversation.id);
|
||||
|
||||
return conversation.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a specific conversation and its messages
|
||||
* @param convId - The conversation ID to load
|
||||
* @returns True if conversation was loaded successfully
|
||||
*/
|
||||
async loadConversation(convId: string): Promise<boolean> {
|
||||
try {
|
||||
const conversation = await conversationsService.loadConversation(convId);
|
||||
|
||||
if (!conversation) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.activeConversation = conversation;
|
||||
|
||||
slotsService.setActiveConversation(convId);
|
||||
|
||||
if (conversation.currNode) {
|
||||
const allMessages = await conversationsService.getConversationMessages(convId);
|
||||
this.activeMessages = filterByLeafNodeId(
|
||||
allMessages,
|
||||
conversation.currNode,
|
||||
false
|
||||
) as DatabaseMessage[];
|
||||
} else {
|
||||
// Load all messages for conversations without currNode (backward compatibility)
|
||||
this.activeMessages = await conversationsService.getConversationMessages(convId);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to load conversation:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the active conversation and messages
|
||||
* Used when navigating away from chat or starting fresh
|
||||
*/
|
||||
clearActiveConversation(): void {
|
||||
this.activeConversation = null;
|
||||
this.activeMessages = [];
|
||||
slotsService.setActiveConversation(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes active messages based on currNode after branch navigation
|
||||
*/
|
||||
async refreshActiveMessages(): Promise<void> {
|
||||
if (!this.activeConversation) return;
|
||||
|
||||
const allMessages = await conversationsService.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.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 conversationsService.updateConversationName(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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 conversationsService.loadConversation(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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 conversationsService.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;
|
||||
|
||||
// Get the current first user message before navigation
|
||||
const allMessages = await conversationsService.getConversationMessages(
|
||||
this.activeConversation.id
|
||||
);
|
||||
const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null);
|
||||
const currentFirstUserMessage = this.activeMessages.find(
|
||||
(m) => m.role === 'user' && m.parent === rootMessage?.id
|
||||
);
|
||||
|
||||
const currentLeafNodeId = findLeafNode(allMessages, siblingId);
|
||||
|
||||
await conversationsService.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) => 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a conversation and all its messages
|
||||
* @param convId - The conversation ID to delete
|
||||
*/
|
||||
async deleteConversation(convId: string): Promise<void> {
|
||||
try {
|
||||
await conversationsService.deleteConversation(convId);
|
||||
|
||||
this.conversations = this.conversations.filter((c) => c.id !== convId);
|
||||
|
||||
if (this.activeConversation?.id === convId) {
|
||||
this.activeConversation = null;
|
||||
this.activeMessages = [];
|
||||
await conversationsService.navigateToNewChat();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete conversation:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a conversation as JSON file
|
||||
* @param convId - The conversation ID to download
|
||||
*/
|
||||
async downloadConversation(convId: string): Promise<void> {
|
||||
if (this.activeConversation?.id === convId) {
|
||||
// Use current active conversation data
|
||||
conversationsService.downloadConversation(this.activeConversation, this.activeMessages);
|
||||
} else {
|
||||
// Load the conversation if not currently active
|
||||
const conversation = await conversationsService.loadConversation(convId);
|
||||
if (!conversation) return;
|
||||
|
||||
const messages = await conversationsService.getConversationMessages(convId);
|
||||
conversationsService.downloadConversation(conversation, messages);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports all conversations with their messages as a JSON file
|
||||
* @returns The list of exported conversations
|
||||
*/
|
||||
async exportAllConversations(): Promise<DatabaseConversation[]> {
|
||||
return await conversationsService.exportAllConversations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports conversations from a JSON file
|
||||
* @returns The list of imported conversations
|
||||
*/
|
||||
async importConversations(): Promise<DatabaseConversation[]> {
|
||||
const importedConversations = await conversationsService.importConversations();
|
||||
|
||||
// Refresh conversations list after import
|
||||
await this.loadConversations();
|
||||
|
||||
return importedConversations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all messages for a specific conversation
|
||||
* @param convId - The conversation ID
|
||||
* @returns Array of messages
|
||||
*/
|
||||
async getConversationMessages(convId: string): Promise<DatabaseMessage[]> {
|
||||
return await conversationsService.getConversationMessages(convId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
export const conversationsStore = new ConversationsStore();
|
||||
|
||||
// Export getter functions for reactive access
|
||||
export const conversations = () => conversationsStore.conversations;
|
||||
export const activeConversation = () => conversationsStore.activeConversation;
|
||||
export const activeMessages = () => conversationsStore.activeMessages;
|
||||
export const isConversationsInitialized = () => conversationsStore.isInitialized;
|
||||
|
||||
// Export conversation operations
|
||||
export const createConversation = conversationsStore.createConversation.bind(conversationsStore);
|
||||
export const loadConversation = conversationsStore.loadConversation.bind(conversationsStore);
|
||||
export const deleteConversation = conversationsStore.deleteConversation.bind(conversationsStore);
|
||||
export const clearActiveConversation =
|
||||
conversationsStore.clearActiveConversation.bind(conversationsStore);
|
||||
export const updateConversationName =
|
||||
conversationsStore.updateConversationName.bind(conversationsStore);
|
||||
export const downloadConversation =
|
||||
conversationsStore.downloadConversation.bind(conversationsStore);
|
||||
export const exportAllConversations =
|
||||
conversationsStore.exportAllConversations.bind(conversationsStore);
|
||||
export const importConversations = conversationsStore.importConversations.bind(conversationsStore);
|
||||
export const navigateToSibling = conversationsStore.navigateToSibling.bind(conversationsStore);
|
||||
export const refreshActiveMessages =
|
||||
conversationsStore.refreshActiveMessages.bind(conversationsStore);
|
||||
export const setTitleUpdateConfirmationCallback =
|
||||
conversationsStore.setTitleUpdateConfirmationCallback.bind(conversationsStore);
|
||||
|
|
@ -2,11 +2,11 @@
|
|||
import '../app.css';
|
||||
import { page } from '$app/state';
|
||||
import { ChatSidebar, DialogConversationTitleUpdate } from '$lib/components/app';
|
||||
import { isLoading } from '$lib/stores/chat.svelte';
|
||||
import {
|
||||
activeMessages,
|
||||
isLoading,
|
||||
setTitleUpdateConfirmationCallback
|
||||
} from '$lib/stores/chat.svelte';
|
||||
} from '$lib/stores/conversations.svelte';
|
||||
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||
import { serverStore } from '$lib/stores/server.svelte';
|
||||
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
||||
|
|
|
|||
|
|
@ -1,21 +1,28 @@
|
|||
<script lang="ts">
|
||||
import { ChatScreen } from '$lib/components/app';
|
||||
import { chatStore, isInitialized } from '$lib/stores/chat.svelte';
|
||||
import { sendMessage, clearUIState } from '$lib/stores/chat.svelte';
|
||||
import {
|
||||
conversationsStore,
|
||||
isConversationsInitialized,
|
||||
clearActiveConversation,
|
||||
createConversation
|
||||
} from '$lib/stores/conversations.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/state';
|
||||
|
||||
let qParam = $derived(page.url.searchParams.get('q'));
|
||||
|
||||
onMount(async () => {
|
||||
if (!isInitialized) {
|
||||
await chatStore.initialize();
|
||||
if (!isConversationsInitialized()) {
|
||||
await conversationsStore.initialize();
|
||||
}
|
||||
|
||||
chatStore.clearActiveConversation();
|
||||
clearActiveConversation();
|
||||
clearUIState();
|
||||
|
||||
if (qParam !== null) {
|
||||
await chatStore.createConversation();
|
||||
await chatStore.sendMessage(qParam);
|
||||
await createConversation();
|
||||
await sendMessage(qParam);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -3,13 +3,12 @@
|
|||
import { page } from '$app/state';
|
||||
import { afterNavigate } from '$app/navigation';
|
||||
import { ChatScreen } from '$lib/components/app';
|
||||
import { isLoading, stopGeneration, syncLoadingStateForChat } from '$lib/stores/chat.svelte';
|
||||
import {
|
||||
chatStore,
|
||||
activeConversation,
|
||||
isLoading,
|
||||
stopGeneration,
|
||||
activeMessages
|
||||
} from '$lib/stores/chat.svelte';
|
||||
activeMessages,
|
||||
loadConversation
|
||||
} from '$lib/stores/conversations.svelte';
|
||||
import { selectModel, modelOptions, selectedModelId } from '$lib/stores/models.svelte';
|
||||
|
||||
let chatId = $derived(page.params.id);
|
||||
|
|
@ -67,9 +66,10 @@
|
|||
}
|
||||
|
||||
(async () => {
|
||||
const success = await chatStore.loadConversation(chatId);
|
||||
|
||||
if (!success) {
|
||||
const success = await loadConversation(chatId);
|
||||
if (success) {
|
||||
syncLoadingStateForChat(chatId);
|
||||
} else {
|
||||
await goto('#/');
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
Loading…
Reference in New Issue