refactor: Remove ConversationsService
This commit is contained in:
parent
ddf98bdf28
commit
42483f463d
|
|
@ -1,279 +0,0 @@
|
||||||
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,3 +1,2 @@
|
||||||
export { chatService } from './chat';
|
export { chatService } from './chat';
|
||||||
export { PropsService } from './props';
|
export { PropsService } from './props';
|
||||||
export { conversationsService } from './conversations';
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { conversationsService } from '$lib/services/conversations';
|
import { goto } from '$app/navigation';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { DatabaseService } from '$lib/services/database';
|
||||||
import { config } from '$lib/stores/settings.svelte';
|
import { config } from '$lib/stores/settings.svelte';
|
||||||
import { filterByLeafNodeId, findLeafNode } from '$lib/utils/branching';
|
import { filterByLeafNodeId, findLeafNode } from '$lib/utils/branching';
|
||||||
import type { DatabaseConversation, DatabaseMessage } from '$lib/types/database';
|
import type {
|
||||||
|
DatabaseConversation,
|
||||||
|
DatabaseMessage,
|
||||||
|
ExportedConversations
|
||||||
|
} from '$lib/types/database';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ConversationsStore - Persistent conversation data and lifecycle management
|
* ConversationsStore - Persistent conversation data and lifecycle management
|
||||||
|
|
@ -22,13 +28,12 @@ import type { DatabaseConversation, DatabaseMessage } from '$lib/types/database'
|
||||||
* **Architecture & Relationships:**
|
* **Architecture & Relationships:**
|
||||||
* - **ConversationsStore** (this class): Persistent conversation data management
|
* - **ConversationsStore** (this class): Persistent conversation data management
|
||||||
* - Manages conversation list and active conversation state
|
* - Manages conversation list and active conversation state
|
||||||
* - Handles conversation CRUD operations via ConversationsService
|
* - Handles conversation CRUD operations via DatabaseService
|
||||||
* - Maintains active message array for current conversation
|
* - Maintains active message array for current conversation
|
||||||
* - Coordinates branching navigation (currNode tracking)
|
* - Coordinates branching navigation (currNode tracking)
|
||||||
*
|
*
|
||||||
* - **ChatStore**: Uses conversation data as context for active AI streaming
|
* - **ChatStore**: Uses conversation data as context for active AI streaming
|
||||||
* - **ConversationsService**: Database operations for conversation persistence
|
* - **DatabaseService**: Low-level IndexedDB storage for conversations and messages
|
||||||
* - **DatabaseService**: Low-level storage for conversations and messages
|
|
||||||
*
|
*
|
||||||
* **Key Features:**
|
* **Key Features:**
|
||||||
* - **Conversation Lifecycle**: Create, load, update, delete conversations
|
* - **Conversation Lifecycle**: Create, load, update, delete conversations
|
||||||
|
|
@ -82,7 +87,7 @@ class ConversationsStore {
|
||||||
* Loads all conversations from the database
|
* Loads all conversations from the database
|
||||||
*/
|
*/
|
||||||
async loadConversations(): Promise<void> {
|
async loadConversations(): Promise<void> {
|
||||||
this.conversations = await conversationsService.loadAllConversations();
|
this.conversations = await DatabaseService.getAllConversations();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -91,15 +96,14 @@ class ConversationsStore {
|
||||||
* @returns The ID of the created conversation
|
* @returns The ID of the created conversation
|
||||||
*/
|
*/
|
||||||
async createConversation(name?: string): Promise<string> {
|
async createConversation(name?: string): Promise<string> {
|
||||||
const conversation = await conversationsService.createConversation(name);
|
const conversationName = name || `Chat ${new Date().toLocaleString()}`;
|
||||||
|
const conversation = await DatabaseService.createConversation(conversationName);
|
||||||
|
|
||||||
this.conversations.unshift(conversation);
|
this.conversations.unshift(conversation);
|
||||||
this.activeConversation = conversation;
|
this.activeConversation = conversation;
|
||||||
this.activeMessages = [];
|
this.activeMessages = [];
|
||||||
|
|
||||||
// Active processing conversation is now set by ChatStore when streaming starts
|
await goto(`#/chat/${conversation.id}`);
|
||||||
|
|
||||||
await conversationsService.navigateToConversation(conversation.id);
|
|
||||||
|
|
||||||
return conversation.id;
|
return conversation.id;
|
||||||
}
|
}
|
||||||
|
|
@ -111,7 +115,7 @@ class ConversationsStore {
|
||||||
*/
|
*/
|
||||||
async loadConversation(convId: string): Promise<boolean> {
|
async loadConversation(convId: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const conversation = await conversationsService.loadConversation(convId);
|
const conversation = await DatabaseService.getConversation(convId);
|
||||||
|
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -119,18 +123,15 @@ class ConversationsStore {
|
||||||
|
|
||||||
this.activeConversation = conversation;
|
this.activeConversation = conversation;
|
||||||
|
|
||||||
// Active processing conversation is now set by ChatStore when streaming starts
|
|
||||||
|
|
||||||
if (conversation.currNode) {
|
if (conversation.currNode) {
|
||||||
const allMessages = await conversationsService.getConversationMessages(convId);
|
const allMessages = await DatabaseService.getConversationMessages(convId);
|
||||||
this.activeMessages = filterByLeafNodeId(
|
this.activeMessages = filterByLeafNodeId(
|
||||||
allMessages,
|
allMessages,
|
||||||
conversation.currNode,
|
conversation.currNode,
|
||||||
false
|
false
|
||||||
) as DatabaseMessage[];
|
) as DatabaseMessage[];
|
||||||
} else {
|
} else {
|
||||||
// Load all messages for conversations without currNode (backward compatibility)
|
this.activeMessages = await DatabaseService.getConversationMessages(convId);
|
||||||
this.activeMessages = await conversationsService.getConversationMessages(convId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -156,9 +157,7 @@ class ConversationsStore {
|
||||||
async refreshActiveMessages(): Promise<void> {
|
async refreshActiveMessages(): Promise<void> {
|
||||||
if (!this.activeConversation) return;
|
if (!this.activeConversation) return;
|
||||||
|
|
||||||
const allMessages = await conversationsService.getConversationMessages(
|
const allMessages = await DatabaseService.getConversationMessages(this.activeConversation.id);
|
||||||
this.activeConversation.id
|
|
||||||
);
|
|
||||||
|
|
||||||
if (allMessages.length === 0) {
|
if (allMessages.length === 0) {
|
||||||
this.activeMessages = [];
|
this.activeMessages = [];
|
||||||
|
|
@ -182,7 +181,7 @@ class ConversationsStore {
|
||||||
*/
|
*/
|
||||||
async updateConversationName(convId: string, name: string): Promise<void> {
|
async updateConversationName(convId: string, name: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await conversationsService.updateConversationName(convId, name);
|
await DatabaseService.updateConversation(convId, { name });
|
||||||
|
|
||||||
const convIndex = this.conversations.findIndex((c) => c.id === convId);
|
const convIndex = this.conversations.findIndex((c) => c.id === convId);
|
||||||
|
|
||||||
|
|
@ -224,7 +223,7 @@ class ConversationsStore {
|
||||||
const currentConfig = config();
|
const currentConfig = config();
|
||||||
|
|
||||||
if (currentConfig.askForTitleConfirmation && onConfirmationNeeded) {
|
if (currentConfig.askForTitleConfirmation && onConfirmationNeeded) {
|
||||||
const conversation = await conversationsService.loadConversation(convId);
|
const conversation = await DatabaseService.getConversation(convId);
|
||||||
if (!conversation) return false;
|
if (!conversation) return false;
|
||||||
|
|
||||||
const shouldUpdate = await onConfirmationNeeded(conversation.name, newTitle);
|
const shouldUpdate = await onConfirmationNeeded(conversation.name, newTitle);
|
||||||
|
|
@ -246,7 +245,7 @@ class ConversationsStore {
|
||||||
async updateCurrentNode(nodeId: string): Promise<void> {
|
async updateCurrentNode(nodeId: string): Promise<void> {
|
||||||
if (!this.activeConversation) return;
|
if (!this.activeConversation) return;
|
||||||
|
|
||||||
await conversationsService.updateCurrentNode(this.activeConversation.id, nodeId);
|
await DatabaseService.updateCurrentNode(this.activeConversation.id, nodeId);
|
||||||
this.activeConversation.currNode = nodeId;
|
this.activeConversation.currNode = nodeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -272,10 +271,7 @@ class ConversationsStore {
|
||||||
async navigateToSibling(siblingId: string): Promise<void> {
|
async navigateToSibling(siblingId: string): Promise<void> {
|
||||||
if (!this.activeConversation) return;
|
if (!this.activeConversation) return;
|
||||||
|
|
||||||
// Get the current first user message before navigation
|
const allMessages = await DatabaseService.getConversationMessages(this.activeConversation.id);
|
||||||
const allMessages = await conversationsService.getConversationMessages(
|
|
||||||
this.activeConversation.id
|
|
||||||
);
|
|
||||||
const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null);
|
const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null);
|
||||||
const currentFirstUserMessage = this.activeMessages.find(
|
const currentFirstUserMessage = this.activeMessages.find(
|
||||||
(m) => m.role === 'user' && m.parent === rootMessage?.id
|
(m) => m.role === 'user' && m.parent === rootMessage?.id
|
||||||
|
|
@ -283,7 +279,7 @@ class ConversationsStore {
|
||||||
|
|
||||||
const currentLeafNodeId = findLeafNode(allMessages, siblingId);
|
const currentLeafNodeId = findLeafNode(allMessages, siblingId);
|
||||||
|
|
||||||
await conversationsService.updateCurrentNode(this.activeConversation.id, currentLeafNodeId);
|
await DatabaseService.updateCurrentNode(this.activeConversation.id, currentLeafNodeId);
|
||||||
this.activeConversation.currNode = currentLeafNodeId;
|
this.activeConversation.currNode = currentLeafNodeId;
|
||||||
await this.refreshActiveMessages();
|
await this.refreshActiveMessages();
|
||||||
|
|
||||||
|
|
@ -315,14 +311,14 @@ class ConversationsStore {
|
||||||
*/
|
*/
|
||||||
async deleteConversation(convId: string): Promise<void> {
|
async deleteConversation(convId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await conversationsService.deleteConversation(convId);
|
await DatabaseService.deleteConversation(convId);
|
||||||
|
|
||||||
this.conversations = this.conversations.filter((c) => c.id !== convId);
|
this.conversations = this.conversations.filter((c) => c.id !== convId);
|
||||||
|
|
||||||
if (this.activeConversation?.id === convId) {
|
if (this.activeConversation?.id === convId) {
|
||||||
this.activeConversation = null;
|
this.activeConversation = null;
|
||||||
this.activeMessages = [];
|
this.activeMessages = [];
|
||||||
await conversationsService.navigateToNewChat();
|
await goto(`?new_chat=true#/`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete conversation:', error);
|
console.error('Failed to delete conversation:', error);
|
||||||
|
|
@ -334,17 +330,19 @@ class ConversationsStore {
|
||||||
* @param convId - The conversation ID to download
|
* @param convId - The conversation ID to download
|
||||||
*/
|
*/
|
||||||
async downloadConversation(convId: string): Promise<void> {
|
async downloadConversation(convId: string): Promise<void> {
|
||||||
if (this.activeConversation?.id === convId) {
|
let conversation: DatabaseConversation | null;
|
||||||
// Use current active conversation data
|
let messages: DatabaseMessage[];
|
||||||
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);
|
if (this.activeConversation?.id === convId) {
|
||||||
conversationsService.downloadConversation(conversation, messages);
|
conversation = this.activeConversation;
|
||||||
|
messages = this.activeMessages;
|
||||||
|
} else {
|
||||||
|
conversation = await DatabaseService.getConversation(convId);
|
||||||
|
if (!conversation) return;
|
||||||
|
messages = await DatabaseService.getConversationMessages(convId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.triggerDownload({ conv: conversation, messages });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -352,20 +350,91 @@ class ConversationsStore {
|
||||||
* @returns The list of exported conversations
|
* @returns The list of exported conversations
|
||||||
*/
|
*/
|
||||||
async exportAllConversations(): Promise<DatabaseConversation[]> {
|
async exportAllConversations(): Promise<DatabaseConversation[]> {
|
||||||
return await conversationsService.exportAllConversations();
|
const allConversations = await DatabaseService.getAllConversations();
|
||||||
|
|
||||||
|
if (allConversations.length === 0) {
|
||||||
|
throw new Error('No conversations to export');
|
||||||
|
}
|
||||||
|
|
||||||
|
const allData = 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
|
* Imports conversations from a JSON file
|
||||||
|
* Opens file picker and processes the selected file
|
||||||
* @returns The list of imported conversations
|
* @returns The list of imported conversations
|
||||||
*/
|
*/
|
||||||
async importConversations(): Promise<DatabaseConversation[]> {
|
async importConversations(): Promise<DatabaseConversation[]> {
|
||||||
const importedConversations = await conversationsService.importConversations();
|
return new Promise((resolve, reject) => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = '.json';
|
||||||
|
|
||||||
// Refresh conversations list after import
|
input.onchange = async (e) => {
|
||||||
await this.loadConversations();
|
const file = (e.target as HTMLInputElement)?.files?.[0];
|
||||||
|
|
||||||
return importedConversations;
|
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
|
||||||
|
) {
|
||||||
|
importedData = [parsedData];
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid file format');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await DatabaseService.importConversations(importedData);
|
||||||
|
toast.success(`Imported ${result.imported} conversation(s), skipped ${result.skipped}`);
|
||||||
|
|
||||||
|
await this.loadConversations();
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -374,7 +443,7 @@ class ConversationsStore {
|
||||||
* @returns Array of messages
|
* @returns Array of messages
|
||||||
*/
|
*/
|
||||||
async getConversationMessages(convId: string): Promise<DatabaseMessage[]> {
|
async getConversationMessages(convId: string): Promise<DatabaseMessage[]> {
|
||||||
return await conversationsService.getConversationMessages(convId);
|
return await DatabaseService.getConversationMessages(convId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -427,6 +496,37 @@ class ConversationsStore {
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers file download in browser
|
||||||
|
*/
|
||||||
|
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?.trim() || '';
|
||||||
|
const truncatedSuffix = conversationName
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]/gi, '_')
|
||||||
|
.replace(/_+/g, '_')
|
||||||
|
.substring(0, 20);
|
||||||
|
const downloadFilename = filename || `conversation_${conversation.id}_${truncatedSuffix}.json`;
|
||||||
|
|
||||||
|
const blob = new Blob([JSON.stringify(data, null, 2)], { 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 conversationsStore = new ConversationsStore();
|
export const conversationsStore = new ConversationsStore();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue