mirror of
https://github.com/zadam/trilium.git
synced 2025-11-09 06:45:49 +01:00
hey look, it doesn't crash again
This commit is contained in:
217
src/services/llm/chat_storage_service.ts
Normal file
217
src/services/llm/chat_storage_service.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
import notes from '../notes.js';
|
||||
import sql from '../sql.js';
|
||||
import attributes from '../attributes.js';
|
||||
import type { Message } from './ai_interface.js';
|
||||
|
||||
interface StoredChat {
|
||||
id: string;
|
||||
title: string;
|
||||
messages: Message[];
|
||||
noteId?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service for storing and retrieving chat histories
|
||||
* Chats are stored as a special type of note
|
||||
*/
|
||||
export class ChatStorageService {
|
||||
private static readonly CHAT_LABEL = 'triliumChat';
|
||||
private static readonly CHAT_ROOT_LABEL = 'triliumChatRoot';
|
||||
private static readonly CHAT_TYPE = 'code';
|
||||
private static readonly CHAT_MIME = 'application/json';
|
||||
|
||||
/**
|
||||
* Get or create the root note for all chats
|
||||
*/
|
||||
async getOrCreateChatRoot(): Promise<string> {
|
||||
const existingRoot = await sql.getRow<{noteId: string}>(
|
||||
`SELECT noteId FROM attributes WHERE name = ? AND value = ?`,
|
||||
['label', ChatStorageService.CHAT_ROOT_LABEL]
|
||||
);
|
||||
|
||||
if (existingRoot) {
|
||||
return existingRoot.noteId;
|
||||
}
|
||||
|
||||
// Create root note for chats
|
||||
const { note } = notes.createNewNote({
|
||||
parentNoteId: 'root',
|
||||
title: 'AI Chats',
|
||||
type: 'text',
|
||||
content: 'This note contains your saved AI chat conversations.'
|
||||
});
|
||||
|
||||
attributes.createLabel(
|
||||
note.noteId,
|
||||
ChatStorageService.CHAT_ROOT_LABEL,
|
||||
''
|
||||
);
|
||||
|
||||
return note.noteId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new chat
|
||||
*/
|
||||
async createChat(title: string, messages: Message[] = []): Promise<StoredChat> {
|
||||
const rootNoteId = await this.getOrCreateChatRoot();
|
||||
const now = new Date();
|
||||
|
||||
const { note } = notes.createNewNote({
|
||||
parentNoteId: rootNoteId,
|
||||
title: title || 'New Chat ' + now.toLocaleString(),
|
||||
type: ChatStorageService.CHAT_TYPE,
|
||||
mime: ChatStorageService.CHAT_MIME,
|
||||
content: JSON.stringify({
|
||||
messages,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
}, null, 2)
|
||||
});
|
||||
|
||||
attributes.createLabel(
|
||||
note.noteId,
|
||||
ChatStorageService.CHAT_LABEL,
|
||||
''
|
||||
);
|
||||
|
||||
return {
|
||||
id: note.noteId,
|
||||
title: title || 'New Chat ' + now.toLocaleString(),
|
||||
messages,
|
||||
noteId: note.noteId,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all chats
|
||||
*/
|
||||
async getAllChats(): Promise<StoredChat[]> {
|
||||
const chats = await sql.getRows<{noteId: string, title: string, dateCreated: string, dateModified: string, content: string}>(
|
||||
`SELECT notes.noteId, notes.title, notes.dateCreated, notes.dateModified, note_contents.content
|
||||
FROM notes
|
||||
JOIN note_contents ON notes.noteId = note_contents.noteId
|
||||
JOIN attributes ON notes.noteId = attributes.noteId
|
||||
WHERE attributes.name = ? AND attributes.value = ?
|
||||
ORDER BY notes.dateModified DESC`,
|
||||
['label', ChatStorageService.CHAT_LABEL]
|
||||
);
|
||||
|
||||
return chats.map(chat => {
|
||||
let messages: Message[] = [];
|
||||
try {
|
||||
const content = JSON.parse(chat.content);
|
||||
messages = content.messages || [];
|
||||
} catch (e) {
|
||||
console.error('Failed to parse chat content:', e);
|
||||
}
|
||||
|
||||
return {
|
||||
id: chat.noteId,
|
||||
title: chat.title,
|
||||
messages,
|
||||
noteId: chat.noteId,
|
||||
createdAt: new Date(chat.dateCreated),
|
||||
updatedAt: new Date(chat.dateModified)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific chat
|
||||
*/
|
||||
async getChat(chatId: string): Promise<StoredChat | null> {
|
||||
const chat = await sql.getRow<{noteId: string, title: string, dateCreated: string, dateModified: string, content: string}>(
|
||||
`SELECT notes.noteId, notes.title, notes.dateCreated, notes.dateModified, note_contents.content
|
||||
FROM notes
|
||||
JOIN note_contents ON notes.noteId = note_contents.noteId
|
||||
WHERE notes.noteId = ?`,
|
||||
[chatId]
|
||||
);
|
||||
|
||||
if (!chat) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let messages: Message[] = [];
|
||||
try {
|
||||
const content = JSON.parse(chat.content);
|
||||
messages = content.messages || [];
|
||||
} catch (e) {
|
||||
console.error('Failed to parse chat content:', e);
|
||||
}
|
||||
|
||||
return {
|
||||
id: chat.noteId,
|
||||
title: chat.title,
|
||||
messages,
|
||||
noteId: chat.noteId,
|
||||
createdAt: new Date(chat.dateCreated),
|
||||
updatedAt: new Date(chat.dateModified)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update messages in a chat
|
||||
*/
|
||||
async updateChat(chatId: string, messages: Message[], title?: string): Promise<StoredChat | null> {
|
||||
const chat = await this.getChat(chatId);
|
||||
|
||||
if (!chat) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
|
||||
// Update content directly using SQL since we don't have a method for this in the notes service
|
||||
await sql.execute(
|
||||
`UPDATE note_contents SET content = ? WHERE noteId = ?`,
|
||||
[JSON.stringify({
|
||||
messages,
|
||||
createdAt: chat.createdAt,
|
||||
updatedAt: now
|
||||
}, null, 2), chatId]
|
||||
);
|
||||
|
||||
// Update title if provided
|
||||
if (title && title !== chat.title) {
|
||||
await sql.execute(
|
||||
`UPDATE notes SET title = ? WHERE noteId = ?`,
|
||||
[title, chatId]
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...chat,
|
||||
title: title || chat.title,
|
||||
messages,
|
||||
updatedAt: now
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a chat
|
||||
*/
|
||||
async deleteChat(chatId: string): Promise<boolean> {
|
||||
try {
|
||||
// Mark note as deleted using SQL since we don't have deleteNote in the exports
|
||||
await sql.execute(
|
||||
`UPDATE notes SET isDeleted = 1 WHERE noteId = ?`,
|
||||
[chatId]
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('Failed to delete chat:', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
const chatStorageService = new ChatStorageService();
|
||||
export default chatStorageService;
|
||||
Reference in New Issue
Block a user