This commit is contained in:
k
2026-09-21 17:20:08 +02:00
parent 2705dd47b3
commit 6d2d9b897e
4 changed files with 105 additions and 9 deletions
+3 -1
View File
@@ -73,6 +73,8 @@ from the **Accounts** panel (each gets a one-time access key to hand over).
| POST | `/api/auth/totp` | challenge | second login step `{totpToken, code}` → session | | POST | `/api/auth/totp` | challenge | second login step `{totpToken, code}` → session |
| GET/PATCH/DELETE | `/api/conversations/:id` | owner | incl. messages on GET | | GET/PATCH/DELETE | `/api/conversations/:id` | owner | incl. messages on GET |
| POST/GET/DELETE | `/api/conversations/:id/share` | owner | public link `/share/:publicId` | | POST/GET/DELETE | `/api/conversations/:id/share` | owner | public link `/share/:publicId` |
| GET | `/api/conversations/archived` | Bearer | archived chats (hidden from sidebar + search) |
| POST | `/api/conversations/:id/archive`, `/api/conversations/:id/unarchive` | owner | archive (read-only) / restore to the list |
| GET | `/api/share/:publicId` | **public** | read-only shared chat JSON | | GET | `/api/share/:publicId` | **public** | read-only shared chat JSON |
| GET | `/api/models` | Bearer | `[{id: "driver:model", …}]` across enabled drivers | | GET | `/api/models` | Bearer | `[{id: "driver:model", …}]` across enabled drivers |
| POST | `/api/chat` | Bearer | `{model, messages, conversationId?, stream?}` → JSON or SSE | | POST | `/api/chat` | Bearer | `{model, messages, conversationId?, stream?}` → JSON or SSE |
@@ -84,7 +86,7 @@ reply into that conversation (ownership enforced) and auto-titles new chats.
`$DATA_DIR/kisassistant.db` (WAL mode). Tables: `users` (id, username, `$DATA_DIR/kisassistant.db` (WAL mode). Tables: `users` (id, username,
display_name, avatar_url, theme, email, key_hash), `sessions` (hashed Bearer tokens), display_name, avatar_url, theme, email, key_hash), `sessions` (hashed Bearer tokens),
`conversations` (id, user, title, topic, model), `messages`, `shares`, `conversations` (id, user, title, topic, model, archived_at), `messages`, `shares`,
`resets` (recovery tokens), `settings` (feature flags) `resets` (recovery tokens), `settings` (feature flags)
(conv ↔ public id). No migration system — schema is created idempotently at boot. (conv ↔ public id). No migration system — schema is created idempotently at boot.
+5 -4
View File
@@ -22,9 +22,10 @@ browser ──► Bun.serve (src/index.ts)
are auto-titled from it. are auto-titled from it.
4. Non-streaming → `driver.chat()` → JSON. Streaming → `driver.chatStream()` 4. Non-streaming → `driver.chat()` → JSON. Streaming → `driver.chatStream()`
→ SSE events `token` / `done` / `error`. The full reply is persisted after a → SSE events `token` / `done` / `error`. The full reply is persisted after a
successful stream; on error the partial text is kept as-is (no internal successful stream; on error the partial text is kept as-is (no internal
markers). Server logs each stream open/done/abort per driver when markers). Server logs each stream open/done/abort per driver when
`DRIVER_DEBUG=true`. `DRIVER_DEBUG=true`. `POST /api/chat` with an archived `conversationId`
is rejected (403) — archived chats are read-only.
Streaming guarantees (ChatGPT-style, no buffering): Streaming guarantees (ChatGPT-style, no buffering):
@@ -47,7 +48,7 @@ Created idempotently at boot in `getDb()` (no migration framework; keep it so).
| `sessions` | `token_hash PK, user_id, created_at, expires_at`. Bearer tokens, 256-bit, hashed with sha256. Expired rows are purged lazily on access. | | `sessions` | `token_hash PK, user_id, created_at, expires_at`. Bearer tokens, 256-bit, hashed with sha256. Expired rows are purged lazily on access. |
| `resets` | `token_hash PK, user_id, created_at, expires_at`. One-time key-recovery tokens (single use, consumed on POST). | | `resets` | `token_hash PK, user_id, created_at, expires_at`. One-time key-recovery tokens (single use, consumed on POST). |
| `settings` | `key PK, value, updated_at`. Admin-controlled feature flags (`registration_enabled` default off, `tools_ocr_enabled` default on). | | `settings` | `key PK, value, updated_at`. Admin-controlled feature flags (`registration_enabled` default off, `tools_ocr_enabled` default on). |
| `conversations` | `id, user_id, title, topic, model, created_at, updated_at`. | | `conversations` | `id, user_id, title, topic, model, archived_at, created_at, updated_at`. `archived_at = 0` means live; otherwise a unix-ms archive timestamp (added idempotently via `ALTER TABLE`, like `users.email`). |
| `messages` | `id AUTOINCREMENT, conv_id → conversations ON DELETE CASCADE, role, content, created_at`. | | `messages` | `id AUTOINCREMENT, conv_id → conversations ON DELETE CASCADE, role, content, created_at`. |
| `shares` | `conv_id PK → conversations ON DELETE CASCADE, public_id UNIQUE, created_at`. | | `shares` | `conv_id PK → conversations ON DELETE CASCADE, public_id UNIQUE, created_at`. |
+7 -4
View File
@@ -35,15 +35,18 @@ read-only `SharePage` (no auth), everything else renders the authenticated app.
**Never use a native `<select>`**; use `Picker` for model and theme choices. **Never use a native `<select>`**; use `Picker` for model and theme choices.
- `Login.tsx` — username + access-key card. - `Login.tsx` — username + access-key card.
- `Sidebar.tsx` — collapsible conversation list with per-item context menu - `Sidebar.tsx` — collapsible conversation list with per-item context menu
(right-click or `···`: Rename / Set topic / Share publicly / Delete), user (right-click or `···`: Rename / Set topic / Share publicly / Archive /
chip (opens settings), Accounts button (admin), logout. Delete), user chip (opens settings), Accounts button (admin), logout.
Archived chats leave the list (and search); they live in settings.
- `Chat.tsx` — header (title, topic badge, driver-grouped model `Picker`, - `Chat.tsx` — header (title, topic badge, driver-grouped model `Picker`,
share button), message list (`Markdown` for assistant, emerald bubble for share button), message list (`Markdown` for assistant, emerald bubble for
user), rounded composer with paperclip attach menu, attachment chips user), rounded composer with paperclip attach menu, attachment chips
(Enter to send, Shift+Enter for newline). (Enter to send, Shift+Enter for newline). With `readOnly` (archived chat)
the composer and share button are replaced by an unarchive banner.
- `dialogs.tsx` — `ConvEditDialog` (title + topic), `ShareModal` (public link, - `dialogs.tsx` — `ConvEditDialog` (title + topic), `ShareModal` (public link,
copy, unshare), `SettingsModal` (display name, avatar upload box with copy, unshare), `SettingsModal` (display name, avatar upload box with
drag & drop, avatar URL, theme). drag & drop, avatar URL, theme, plus an "Archived chats" category to
view / unarchive / delete archived conversations).
- `AdminPanel.tsx` — No-KYC account management: create (key shown once), - `AdminPanel.tsx` — No-KYC account management: create (key shown once),
regenerate (revokes sessions), edit, delete with confirm, plus server-side regenerate (revokes sessions), edit, delete with confirm, plus server-side
search / sort (newest, oldest, A–Z, Z–A) / email filter / pagination. search / sort (newest, oldest, A–Z, Z–A) / email filter / pagination.
+90
View File
@@ -38,6 +38,10 @@ const en = {
"common.light": "Light", "common.light": "Light",
"common.dark": "Dark", "common.dark": "Dark",
"common.saveFailed": "Save failed", "common.saveFailed": "Save failed",
"common.loadFailed": "Load failed",
"common.deleteFailed": "Delete failed",
"common.loadFailed": "Load failed",
"common.deleteFailed": "Delete failed",
"common.backToLogin": "Back to login", "common.backToLogin": "Back to login",
"loading.tagline": "Preparing your chats", "loading.tagline": "Preparing your chats",
@@ -83,6 +87,7 @@ const en = {
"sidebar.adminCenter": "Admin Center", "sidebar.adminCenter": "Admin Center",
"sidebar.adminCenterTip": "Open the Admin Center", "sidebar.adminCenterTip": "Open the Admin Center",
"sidebar.settingsTip": "Profile & settings", "sidebar.settingsTip": "Profile & settings",
"sidebar.logout": "Log out",
"sidebar.archive": "Archive", "sidebar.archive": "Archive",
"chat.expand": "Expand sidebar", "chat.expand": "Expand sidebar",
@@ -460,6 +465,16 @@ const en = {
"archived.readonly": "This chat is archived and read-only.", "archived.readonly": "This chat is archived and read-only.",
"archived.unarchive": "Unarchive", "archived.unarchive": "Unarchive",
"archived.title": "Archived chats",
"archived.empty": "No archived chats.",
"archived.view": "View",
"archived.deleteTitle": "Delete this archived chat?",
"archived.deleteMsg": "It will be permanently removed, with all its messages.",
"archived.title": "Archived chats",
"archived.empty": "No archived conversations yet.",
"archived.view": "View",
"archived.deleteTitle": "Delete this archived chat?",
"archived.deleteMsg": "This permanently removes the conversation and all its messages.",
} as const; } as const;
export type StringKey = keyof typeof en; export type StringKey = keyof typeof en;
@@ -485,6 +500,10 @@ const es: Dict = {
"common.light": "Claro", "common.light": "Claro",
"common.dark": "Oscuro", "common.dark": "Oscuro",
"common.saveFailed": "No se pudo guardar", "common.saveFailed": "No se pudo guardar",
"common.loadFailed": "No se pudo cargar",
"common.deleteFailed": "No se pudo eliminar",
"common.loadFailed": "No se pudo cargar",
"common.deleteFailed": "No se pudo eliminar",
"common.backToLogin": "Volver al inicio de sesión", "common.backToLogin": "Volver al inicio de sesión",
"loading.tagline": "Preparando tus chats", "loading.tagline": "Preparando tus chats",
@@ -530,6 +549,7 @@ const es: Dict = {
"sidebar.adminCenter": "Centro de admin", "sidebar.adminCenter": "Centro de admin",
"sidebar.adminCenterTip": "Abrir el Centro de admin", "sidebar.adminCenterTip": "Abrir el Centro de admin",
"sidebar.settingsTip": "Perfil y ajustes", "sidebar.settingsTip": "Perfil y ajustes",
"sidebar.logout": "Cerrar sesión",
"sidebar.archive": "Archivar", "sidebar.archive": "Archivar",
"chat.expand": "Mostrar barra lateral", "chat.expand": "Mostrar barra lateral",
@@ -902,6 +922,16 @@ const es: Dict = {
"archived.readonly": "Este chat está archivado y es de solo lectura.", "archived.readonly": "Este chat está archivado y es de solo lectura.",
"archived.unarchive": "Desarchivar", "archived.unarchive": "Desarchivar",
"archived.title": "Archivados",
"archived.empty": "Sin chats archivados.",
"archived.view": "Ver",
"archived.deleteTitle": "¿Eliminar este chat archivado?",
"archived.deleteMsg": "Se eliminará para siempre, con todos sus mensajes.",
"archived.title": "Chats archivados",
"archived.empty": "No hay conversaciones archivadas.",
"archived.view": "Ver",
"archived.deleteTitle": "¿Eliminar este chat archivado?",
"archived.deleteMsg": "Esto elimina permanentemente la conversación y todos sus mensajes.",
}; };
const fr: Dict = { const fr: Dict = {
@@ -924,6 +954,10 @@ const fr: Dict = {
"common.light": "Clair", "common.light": "Clair",
"common.dark": "Sombre", "common.dark": "Sombre",
"common.saveFailed": "Échec de l'enregistrement", "common.saveFailed": "Échec de l'enregistrement",
"common.loadFailed": "Chargement impossible",
"common.deleteFailed": "Suppression impossible",
"common.loadFailed": "Échec du chargement",
"common.deleteFailed": "Échec de la suppression",
"common.backToLogin": "Retour à la connexion", "common.backToLogin": "Retour à la connexion",
"loading.tagline": "Préparation de vos chats", "loading.tagline": "Préparation de vos chats",
@@ -969,6 +1003,7 @@ const fr: Dict = {
"sidebar.adminCenter": "Centre admin", "sidebar.adminCenter": "Centre admin",
"sidebar.adminCenterTip": "Ouvrir le Centre admin", "sidebar.adminCenterTip": "Ouvrir le Centre admin",
"sidebar.settingsTip": "Profil et réglages", "sidebar.settingsTip": "Profil et réglages",
"sidebar.logout": "Déconnexion",
"sidebar.archive": "Archiver", "sidebar.archive": "Archiver",
"chat.expand": "Afficher la barre latérale", "chat.expand": "Afficher la barre latérale",
@@ -1341,6 +1376,16 @@ const fr: Dict = {
"archived.readonly": "Ce chat est archivé en lecture seule.", "archived.readonly": "Ce chat est archivé en lecture seule.",
"archived.unarchive": "Désarchiver", "archived.unarchive": "Désarchiver",
"archived.title": "Archives",
"archived.empty": "Aucun chat archivé.",
"archived.view": "Voir",
"archived.deleteTitle": "Supprimer ce chat archivé ?",
"archived.deleteMsg": "Il sera définitivement supprimé, avec tous ses messages.",
"archived.title": "Conversations archivées",
"archived.empty": "Aucune conversation archivée.",
"archived.view": "Voir",
"archived.deleteTitle": "Supprimer cette conversation archivée ?",
"archived.deleteMsg": "Cela supprime définitivement la conversation et tous ses messages.",
}; };
const ru: Dict = { const ru: Dict = {
@@ -1363,6 +1408,10 @@ const ru: Dict = {
"common.light": "Светлая", "common.light": "Светлая",
"common.dark": "Тёмная", "common.dark": "Тёмная",
"common.saveFailed": "Не удалось сохранить", "common.saveFailed": "Не удалось сохранить",
"common.loadFailed": "Не загрузилось",
"common.deleteFailed": "Не удалилось",
"common.loadFailed": "Не удалось загрузить",
"common.deleteFailed": "Не удалось удалить",
"common.backToLogin": "Назад ко входу", "common.backToLogin": "Назад ко входу",
"loading.tagline": "Готовим ваши чаты", "loading.tagline": "Готовим ваши чаты",
@@ -1408,6 +1457,7 @@ const ru: Dict = {
"sidebar.adminCenter": "Админ-центр", "sidebar.adminCenter": "Админ-центр",
"sidebar.adminCenterTip": "Открыть Админ-центр", "sidebar.adminCenterTip": "Открыть Админ-центр",
"sidebar.settingsTip": "Профиль и настройки", "sidebar.settingsTip": "Профиль и настройки",
"sidebar.logout": "Выйти",
"sidebar.archive": "В архив", "sidebar.archive": "В архив",
"chat.expand": "Показать панель", "chat.expand": "Показать панель",
@@ -1767,6 +1817,16 @@ const ru: Dict = {
"archived.readonly": "Чат в архиве, только чтение.", "archived.readonly": "Чат в архиве, только чтение.",
"archived.unarchive": "Вернуть", "archived.unarchive": "Вернуть",
"archived.title": "Архив",
"archived.empty": "Архив пуст.",
"archived.view": "Открыть",
"archived.deleteTitle": "Удалить архивный чат?",
"archived.deleteMsg": "Будет удалён навсегда вместе с сообщениями.",
"archived.title": "Архив чатов",
"archived.empty": "Нет архивных чатов.",
"archived.view": "Открыть",
"archived.deleteTitle": "Удалить этот архивный чат?",
"archived.deleteMsg": "Это навсегда удалит разговор и все сообщения.",
}; };
const it: Dict = { const it: Dict = {
@@ -1789,6 +1849,10 @@ const it: Dict = {
"common.light": "Chiaro", "common.light": "Chiaro",
"common.dark": "Scuro", "common.dark": "Scuro",
"common.saveFailed": "Salvataggio non riuscito", "common.saveFailed": "Salvataggio non riuscito",
"common.loadFailed": "Caricamento fallito",
"common.deleteFailed": "Eliminazione fallita",
"common.loadFailed": "Caricamento non riuscito",
"common.deleteFailed": "Eliminazione non riuscita",
"common.backToLogin": "Torna al login", "common.backToLogin": "Torna al login",
"loading.tagline": "Preparo le tue chat", "loading.tagline": "Preparo le tue chat",
@@ -1834,6 +1898,7 @@ const it: Dict = {
"sidebar.adminCenter": "Centro admin", "sidebar.adminCenter": "Centro admin",
"sidebar.adminCenterTip": "Apri il Centro admin", "sidebar.adminCenterTip": "Apri il Centro admin",
"sidebar.settingsTip": "Profilo e impostazioni", "sidebar.settingsTip": "Profilo e impostazioni",
"sidebar.logout": "Esci",
"sidebar.archive": "Archivia", "sidebar.archive": "Archivia",
"chat.expand": "Mostra barra laterale", "chat.expand": "Mostra barra laterale",
@@ -2199,6 +2264,16 @@ const it: Dict = {
"archived.readonly": "Questa chat è archiviata e in sola lettura.", "archived.readonly": "Questa chat è archiviata e in sola lettura.",
"archived.unarchive": "Ripristina", "archived.unarchive": "Ripristina",
"archived.title": "Archiviate",
"archived.empty": "Nessuna chat archiviata.",
"archived.view": "Apri",
"archived.deleteTitle": "Eliminare questa chat archiviata?",
"archived.deleteMsg": "Sarà eliminata per sempre, con tutti i suoi messaggi.",
"archived.title": "Chat archiviate",
"archived.empty": "Nessuna conversazione archiviata.",
"archived.view": "Vedi",
"archived.deleteTitle": "Eliminare questa chat archiviata?",
"archived.deleteMsg": "Questo rimuove definitivamente la conversazione e tutti i suoi messaggi.",
}; };
const pt: Dict = { const pt: Dict = {
@@ -2221,6 +2296,10 @@ const pt: Dict = {
"common.light": "Claro", "common.light": "Claro",
"common.dark": "Escuro", "common.dark": "Escuro",
"common.saveFailed": "Falha ao guardar", "common.saveFailed": "Falha ao guardar",
"common.loadFailed": "Falha ao carregar",
"common.deleteFailed": "Falha ao eliminar",
"common.loadFailed": "Falha ao carregar",
"common.deleteFailed": "Falha ao eliminar",
"common.backToLogin": "Voltar ao login", "common.backToLogin": "Voltar ao login",
"loading.tagline": "A preparar os teus chats", "loading.tagline": "A preparar os teus chats",
@@ -2266,6 +2345,7 @@ const pt: Dict = {
"sidebar.adminCenter": "Centro admin", "sidebar.adminCenter": "Centro admin",
"sidebar.adminCenterTip": "Abrir o Centro admin", "sidebar.adminCenterTip": "Abrir o Centro admin",
"sidebar.settingsTip": "Perfil e definições", "sidebar.settingsTip": "Perfil e definições",
"sidebar.logout": "Sair",
"sidebar.archive": "Arquivar", "sidebar.archive": "Arquivar",
"chat.expand": "Mostrar barra lateral", "chat.expand": "Mostrar barra lateral",
@@ -2622,6 +2702,16 @@ const pt: Dict = {
"archived.readonly": "Este chat está arquivado e é só de leitura.", "archived.readonly": "Este chat está arquivado e é só de leitura.",
"archived.unarchive": "Desarquivar", "archived.unarchive": "Desarquivar",
"archived.title": "Arquivados",
"archived.empty": "Sem chats arquivados.",
"archived.view": "Abrir",
"archived.deleteTitle": "Eliminar este chat arquivado?",
"archived.deleteMsg": "Será eliminado para sempre, com todas as mensagens.",
"archived.title": "Conversas arquivadas",
"archived.empty": "Nenhuma conversa arquivada.",
"archived.view": "Ver",
"archived.deleteTitle": "Eliminar esta conversa arquivada?",
"archived.deleteMsg": "Isto remove permanentemente a conversa e todas as mensagens.",
}; };
const dicts: Record<LangId, Dict> = { en, es, fr, ru, it, pt }; const dicts: Record<LangId, Dict> = { en, es, fr, ru, it, pt };