.
This commit is contained in:
@@ -23,6 +23,7 @@ export interface ConversationRow {
|
||||
title: string;
|
||||
topic: string;
|
||||
model: string;
|
||||
archived_at: number;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
@@ -119,6 +120,11 @@ export function getDb(): Database {
|
||||
if (!cols.some((c) => c.name === "email")) {
|
||||
db.exec("ALTER TABLE users ADD COLUMN email TEXT NOT NULL DEFAULT ''");
|
||||
}
|
||||
// Archive flag added after the initial schema — keep idempotent.
|
||||
const convCols = db.query("PRAGMA table_info(conversations)").all() as { name: string }[];
|
||||
if (!convCols.some((c) => c.name === "archived_at")) {
|
||||
db.exec("ALTER TABLE conversations ADD COLUMN archived_at INTEGER NOT NULL DEFAULT 0");
|
||||
}
|
||||
// Admin pseudo-account row (auth still goes through APP_PASSWORD from .env).
|
||||
const admin = db.query("SELECT id FROM users WHERE username = 'admin'").get() as { id: string } | null;
|
||||
if (!admin) {
|
||||
@@ -242,6 +248,7 @@ export function createConversation(userId: string, opts: { title?: string; topic
|
||||
title: opts.title?.slice(0, 120) || "New chat",
|
||||
topic: opts.topic?.slice(0, 120) || "",
|
||||
model: opts.model?.slice(0, 160) || "",
|
||||
archived_at: 0,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
@@ -252,7 +259,21 @@ export function createConversation(userId: string, opts: { title?: string; topic
|
||||
}
|
||||
|
||||
export function listConversations(userId: string): ConversationRow[] {
|
||||
return getDb().query("SELECT * FROM conversations WHERE user_id = ? ORDER BY updated_at DESC").all(userId) as ConversationRow[];
|
||||
return getDb().query("SELECT * FROM conversations WHERE user_id = ? AND archived_at = 0 ORDER BY updated_at DESC").all(userId) as ConversationRow[];
|
||||
}
|
||||
|
||||
/** Archived chats: hidden from the sidebar, managed in settings. */
|
||||
export function listArchivedConversations(userId: string): ConversationRow[] {
|
||||
return getDb().query("SELECT * FROM conversations WHERE user_id = ? AND archived_at != 0 ORDER BY updated_at DESC").all(userId) as ConversationRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* (Un)archive a conversation. archived_at is left out of updated_at on
|
||||
* purpose so an unarchived chat returns to its chronological place.
|
||||
*/
|
||||
export function setConversationArchived(id: string, archived: boolean): ConversationRow | null {
|
||||
getDb().query("UPDATE conversations SET archived_at = ? WHERE id = ?").run(archived ? Date.now() : 0, id);
|
||||
return getConversation(id);
|
||||
}
|
||||
|
||||
export function getConversation(id: string): ConversationRow | null {
|
||||
|
||||
+17
-1
@@ -34,8 +34,10 @@ import {
|
||||
getUserByUsername,
|
||||
isRegistrationEnabled,
|
||||
listConversations,
|
||||
listArchivedConversations,
|
||||
listUsers,
|
||||
peekReset,
|
||||
setConversationArchived,
|
||||
setSetting,
|
||||
toPublicUser,
|
||||
touchConversation,
|
||||
@@ -917,6 +919,11 @@ const server = Bun.serve({
|
||||
return json({ conversations: listConversations(user.id) });
|
||||
}
|
||||
|
||||
// Archived chats (hidden from the sidebar, managed in settings).
|
||||
if (path === "/api/conversations/archived" && req.method === "GET") {
|
||||
return json({ conversations: listArchivedConversations(user.id) });
|
||||
}
|
||||
|
||||
// Full-text-ish search over your own chats: titles, topics AND old
|
||||
// prompts/replies. Returns the matching conversations, most recent
|
||||
// first, with a short snippet of the first hit.
|
||||
@@ -928,7 +935,7 @@ const server = Bun.serve({
|
||||
`SELECT c.*, m.content AS hit, m.role AS hit_role FROM conversations c
|
||||
LEFT JOIN messages m ON m.conv_id = c.id
|
||||
AND m.content LIKE ? ESCAPE '\\'
|
||||
WHERE c.user_id = ? AND (c.title LIKE ? ESCAPE '\\' OR c.topic LIKE ? ESCAPE '\\' OR m.id IS NOT NULL)
|
||||
WHERE c.user_id = ? AND c.archived_at = 0 AND (c.title LIKE ? ESCAPE '\\' OR c.topic LIKE ? ESCAPE '\\' OR m.id IS NOT NULL)
|
||||
GROUP BY c.id ORDER BY c.updated_at DESC LIMIT 30`,
|
||||
).all(like, user.id, like, like) as (ConversationRow & { hit: string | null; hit_role: string | null })[];
|
||||
return json({
|
||||
@@ -959,6 +966,13 @@ const server = Bun.serve({
|
||||
return json({ conversation: conv }, 201);
|
||||
}
|
||||
|
||||
const archiveMatch = path.match(/^\/api\/conversations\/([0-9a-f-]{10,50})\/(archive|unarchive)$/);
|
||||
if (archiveMatch && req.method === "POST") {
|
||||
const conv = getConversation(archiveMatch[1]!);
|
||||
if (!conv || conv.user_id !== user.id) return json({ error: "conversation not found" }, 404);
|
||||
return json({ conversation: setConversationArchived(conv.id, archiveMatch[2] === "archive") });
|
||||
}
|
||||
|
||||
const convMatch = path.match(/^\/api\/conversations\/([0-9a-f-]{10,50})(\/(share))?$/);
|
||||
if (convMatch) {
|
||||
const conv = getConversation(convMatch[1]!);
|
||||
@@ -1075,6 +1089,8 @@ const server = Bun.serve({
|
||||
if (typeof body.conversationId !== "string") return json({ error: "bad conversationId" }, 400);
|
||||
conv = getConversation(body.conversationId);
|
||||
if (!conv || conv.user_id !== user.id) return json({ error: "conversation not found" }, 404);
|
||||
// Archived chats are read-only: no new messages may be written.
|
||||
if (conv.archived_at) return json({ error: "conversation is archived (read-only) — unarchive it to write again" }, 403);
|
||||
// Persist the new user message (dedupe against last stored one).
|
||||
const lastUser = [...messages].reverse().find((m) => m.role === "user");
|
||||
const stored = getMessages(conv.id);
|
||||
|
||||
Reference in New Issue
Block a user