// Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // Project: chat.spboucher.ai import crypto from "node:crypto"; import { getDb } from "@/lib/db/database"; export interface ConversationRow { id: string; title: string; pinned: number; current_leaf_id: string | null; created_at: number; updated_at: number; } export interface MessageRow { id: string; conversation_id: string; parent_id: string | null; role: "user" | "assistant" | "system"; content: string; reasoning: string | null; model_id: string | null; model_name: string | null; provider: string | null; generation_id: string | null; status: "pending" | "streaming" | "completed" | "cancelled" | "failed"; error_message: string | null; created_at: number; } export function listConversations(): ConversationRow[] { return getDb() .prepare("SELECT * FROM conversations ORDER BY pinned DESC, updated_at DESC") .all() as ConversationRow[]; } export function getConversation(id: string): ConversationRow | null { return (getDb().prepare("SELECT * FROM conversations WHERE id = ?").get(id) as ConversationRow) ?? null; } export function createConversation(title?: string): ConversationRow { const db = getDb(); const id = crypto.randomUUID(); const now = Date.now(); db.prepare( "INSERT INTO conversations (id, title, created_at, updated_at) VALUES (?, ?, ?, ?)" ).run(id, title?.slice(0, 80) || "New conversation", now, now); return getConversation(id)!; } export function updateConversation( id: string, patch: { title?: string; pinned?: boolean; currentLeafId?: string | null } ): void { const db = getDb(); if (patch.title !== undefined) { db.prepare("UPDATE conversations SET title = ?, updated_at = ? WHERE id = ?").run( patch.title.slice(0, 120), Date.now(), id ); } if (patch.pinned !== undefined) { db.prepare("UPDATE conversations SET pinned = ? WHERE id = ?").run(patch.pinned ? 1 : 0, id); } if (patch.currentLeafId !== undefined) { db.prepare("UPDATE conversations SET current_leaf_id = ? WHERE id = ?").run(patch.currentLeafId, id); } } export function deleteConversation(id: string): void { getDb().prepare("DELETE FROM conversations WHERE id = ?").run(id); } export function touchConversation(id: string): void { getDb().prepare("UPDATE conversations SET updated_at = ? WHERE id = ?").run(Date.now(), id); } export function listMessages(conversationId: string): MessageRow[] { return getDb() .prepare("SELECT * FROM messages WHERE conversation_id = ? ORDER BY created_at, id") .all(conversationId) as MessageRow[]; } export function getMessage(id: string): MessageRow | null { return (getDb().prepare("SELECT * FROM messages WHERE id = ?").get(id) as MessageRow) ?? null; } export function insertMessage(m: { conversationId: string; parentId: string | null; role: "user" | "assistant" | "system"; content?: string; modelId?: string; modelName?: string; provider?: string; generationId?: string; status?: MessageRow["status"]; }): MessageRow { const db = getDb(); const id = crypto.randomUUID(); db.prepare( `INSERT INTO messages (id, conversation_id, parent_id, role, content, model_id, model_name, provider, generation_id, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ).run( id, m.conversationId, m.parentId, m.role, m.content ?? "", m.modelId ?? null, m.modelName ?? null, m.provider ?? null, m.generationId ?? null, m.status ?? "completed", Date.now() ); return getMessage(id)!; } /** Walk parent pointers from a leaf to the root: the active thread, oldest first. */ export function threadToLeaf(conversationId: string, leafId: string | null): MessageRow[] { if (!leafId) return []; const byId = new Map(listMessages(conversationId).map((m) => [m.id, m])); const thread: MessageRow[] = []; let cursor = byId.get(leafId); while (cursor) { thread.push(cursor); cursor = cursor.parent_id ? byId.get(cursor.parent_id) : undefined; } return thread.reverse(); }