|
|
import Dexie, { type EntityTable } from 'dexie'; |
|
|
import { filterByLeafNodeId, findDescendantMessages } from '$lib/utils/branching'; |
|
|
|
|
|
class LlamacppDatabase extends Dexie { |
|
|
conversations!: EntityTable<DatabaseConversation, string>; |
|
|
messages!: EntityTable<DatabaseMessage, string>; |
|
|
|
|
|
constructor() { |
|
|
super('LlamacppWebui'); |
|
|
|
|
|
this.version(1).stores({ |
|
|
conversations: 'id, lastModified, currNode, name', |
|
|
messages: 'id, convId, type, role, timestamp, parent, children' |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
const db = new LlamacppDatabase(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import { v4 as uuid } from 'uuid'; |
|
|
|
|
|
export class DatabaseStore { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static async addMessage(message: Omit<DatabaseMessage, 'id'>): Promise<DatabaseMessage> { |
|
|
const newMessage: DatabaseMessage = { |
|
|
...message, |
|
|
id: uuid() |
|
|
}; |
|
|
|
|
|
await db.messages.add(newMessage); |
|
|
return newMessage; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static async createConversation(name: string): Promise<DatabaseConversation> { |
|
|
const conversation: DatabaseConversation = { |
|
|
id: uuid(), |
|
|
name, |
|
|
lastModified: Date.now(), |
|
|
currNode: '' |
|
|
}; |
|
|
|
|
|
await db.conversations.add(conversation); |
|
|
return conversation; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static async createMessageBranch( |
|
|
message: Omit<DatabaseMessage, 'id'>, |
|
|
parentId: string | null |
|
|
): Promise<DatabaseMessage> { |
|
|
return await db.transaction('rw', [db.conversations, db.messages], async () => { |
|
|
|
|
|
if (parentId !== null) { |
|
|
const parentMessage = await db.messages.get(parentId); |
|
|
if (!parentMessage) { |
|
|
throw new Error(`Parent message ${parentId} not found`); |
|
|
} |
|
|
} |
|
|
|
|
|
const newMessage: DatabaseMessage = { |
|
|
...message, |
|
|
id: uuid(), |
|
|
parent: parentId, |
|
|
children: [] |
|
|
}; |
|
|
|
|
|
await db.messages.add(newMessage); |
|
|
|
|
|
|
|
|
if (parentId !== null) { |
|
|
const parentMessage = await db.messages.get(parentId); |
|
|
if (parentMessage) { |
|
|
await db.messages.update(parentId, { |
|
|
children: [...parentMessage.children, newMessage.id] |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
await this.updateConversation(message.convId, { |
|
|
currNode: newMessage.id |
|
|
}); |
|
|
|
|
|
return newMessage; |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static async createRootMessage(convId: string): Promise<string> { |
|
|
const rootMessage: DatabaseMessage = { |
|
|
id: uuid(), |
|
|
convId, |
|
|
type: 'root', |
|
|
timestamp: Date.now(), |
|
|
role: 'system', |
|
|
content: '', |
|
|
parent: null, |
|
|
thinking: '', |
|
|
children: [] |
|
|
}; |
|
|
|
|
|
await db.messages.add(rootMessage); |
|
|
return rootMessage.id; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static async deleteConversation(id: string): Promise<void> { |
|
|
await db.transaction('rw', [db.conversations, db.messages], async () => { |
|
|
await db.conversations.delete(id); |
|
|
await db.messages.where('convId').equals(id).delete(); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static async deleteMessage(messageId: string): Promise<void> { |
|
|
await db.transaction('rw', db.messages, async () => { |
|
|
const message = await db.messages.get(messageId); |
|
|
if (!message) return; |
|
|
|
|
|
|
|
|
if (message.parent) { |
|
|
const parent = await db.messages.get(message.parent); |
|
|
if (parent) { |
|
|
parent.children = parent.children.filter((childId: string) => childId !== messageId); |
|
|
await db.messages.put(parent); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
await db.messages.delete(messageId); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static async deleteMessageCascading( |
|
|
conversationId: string, |
|
|
messageId: string |
|
|
): Promise<string[]> { |
|
|
return await db.transaction('rw', db.messages, async () => { |
|
|
|
|
|
const allMessages = await db.messages.where('convId').equals(conversationId).toArray(); |
|
|
|
|
|
|
|
|
const descendants = findDescendantMessages(allMessages, messageId); |
|
|
const allToDelete = [messageId, ...descendants]; |
|
|
|
|
|
|
|
|
const message = await db.messages.get(messageId); |
|
|
if (message && message.parent) { |
|
|
const parent = await db.messages.get(message.parent); |
|
|
if (parent) { |
|
|
parent.children = parent.children.filter((childId: string) => childId !== messageId); |
|
|
await db.messages.put(parent); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
await db.messages.bulkDelete(allToDelete); |
|
|
|
|
|
return allToDelete; |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static async getAllConversations(): Promise<DatabaseConversation[]> { |
|
|
return await db.conversations.orderBy('lastModified').reverse().toArray(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static async getConversation(id: string): Promise<DatabaseConversation | undefined> { |
|
|
return await db.conversations.get(id); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static async getConversationMessages(convId: string): Promise<DatabaseMessage[]> { |
|
|
return await db.messages.where('convId').equals(convId).sortBy('timestamp'); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 []; |
|
|
} |
|
|
|
|
|
|
|
|
const leafNodeId = |
|
|
conversation.currNode || |
|
|
allMessages.reduce((latest, msg) => (msg.timestamp > latest.timestamp ? msg : latest)).id; |
|
|
|
|
|
return filterByLeafNodeId(allMessages, leafNodeId, false) as DatabaseMessage[]; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static async updateConversation( |
|
|
id: string, |
|
|
updates: Partial<Omit<DatabaseConversation, 'id'>> |
|
|
): Promise<void> { |
|
|
await db.conversations.update(id, { |
|
|
...updates, |
|
|
lastModified: Date.now() |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static async updateCurrentNode(convId: string, nodeId: string): Promise<void> { |
|
|
await this.updateConversation(convId, { |
|
|
currNode: nodeId |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static async updateMessage( |
|
|
id: string, |
|
|
updates: Partial<Omit<DatabaseMessage, 'id'>> |
|
|
): Promise<void> { |
|
|
await db.messages.update(id, updates); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static async importConversations( |
|
|
data: { conv: DatabaseConversation; messages: DatabaseMessage[] }[] |
|
|
): Promise<{ imported: number; skipped: number }> { |
|
|
let importedCount = 0; |
|
|
let skippedCount = 0; |
|
|
|
|
|
return await db.transaction('rw', [db.conversations, db.messages], async () => { |
|
|
for (const item of data) { |
|
|
const { conv, messages } = item; |
|
|
|
|
|
const existing = await db.conversations.get(conv.id); |
|
|
if (existing) { |
|
|
console.warn(`Conversation "${conv.name}" already exists, skipping...`); |
|
|
skippedCount++; |
|
|
continue; |
|
|
} |
|
|
|
|
|
await db.conversations.add(conv); |
|
|
for (const msg of messages) { |
|
|
await db.messages.put(msg); |
|
|
} |
|
|
|
|
|
importedCount++; |
|
|
} |
|
|
|
|
|
return { imported: importedCount, skipped: skippedCount }; |
|
|
}); |
|
|
} |
|
|
} |
|
|
|