From 2a34d16b529f2fe92d4f607aa42f4dd3a58b4491 Mon Sep 17 00:00:00 2001 From: Anais Saraiva Date: Mon, 21 Sep 2026 18:15:13 +0200 Subject: [PATCH] :) --- docs/FRONTEND.md | 7 ++++--- frontend/src/App.tsx | 2 +- frontend/src/components/SharePage.tsx | 13 +++++++++---- frontend/src/components/Sidebar.tsx | 4 ++-- frontend/src/components/dialogs.tsx | 6 +++--- frontend/src/lib/types.ts | 1 + src/db.ts | 3 ++- src/index.ts | 1 + 8 files changed, 23 insertions(+), 14 deletions(-) diff --git a/docs/FRONTEND.md b/docs/FRONTEND.md index 05728bb..3e39d42 100644 --- a/docs/FRONTEND.md +++ b/docs/FRONTEND.md @@ -19,7 +19,7 @@ read-only `SharePage` (no auth), everything else renders the authenticated app. - `lib/api.ts` — typed `fetch` wrapper: Bearer header from `localStorage`, `{ error }` parsing, `ApiError` with status, auto logout+reload on 401 (except on `/share/*`). `chatStream()` parses SSE `token`/`done`/`error`. -- Boot shows `LoadingScreen` for at least 2.5 s (`MIN_SPLASH_MS` in `App.tsx`) +- Boot shows `LoadingScreen` for at least 1.5 s (`MIN_SPLASH_MS` in `App.tsx`) so the bloom animation always plays, even when the session restores instantly. - Sending: ensure a server conversation exists (created lazily on first send), optimistic user message, stream tokens into `streaming`, then append the full @@ -57,8 +57,9 @@ read-only `SharePage` (no auth), everything else renders the authenticated app. `/api/admin/settings`), SMTP viewer (password blurred by default, eye toggle to reveal) + connectivity tester (verify + send test mail). Follows the platform theme. - `SharePage.tsx` — public read-only view, fetches `/api/share/:id` unauthenticated - (`{ title, topic, model, authorName, authorAvatarUrl, sharedAt, messages }`); - user messages render the author's avatar via `Avatar` (URL or initials). + (`{ title, topic, model, authorName, authorAvatarUrl, archived, sharedAt, messages }`); + user messages render the author's avatar via `Avatar` (URL or initials); + an "Archived" pill shows when the source conversation is archived. - `Markdown.tsx` — `react-markdown` + GFM; code blocks get a language label and a copy button. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ba4fa2f..b9e8083 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -36,7 +36,7 @@ function reviewIdFromPath(): string | null { /** Minimum time the boot splash stays visible, so the bloom animation * actually plays even when the session restores instantly. */ -const MIN_SPLASH_MS = 2500; +const MIN_SPLASH_MS = 1500; function applyTheme(theme: string) { const root = document.documentElement; diff --git a/frontend/src/components/SharePage.tsx b/frontend/src/components/SharePage.tsx index cc77cd4..8fad9ab 100644 --- a/frontend/src/components/SharePage.tsx +++ b/frontend/src/components/SharePage.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { Bot, CloudOff, Globe, User as UserIcon } from "lucide-react"; +import { Archive, Bot, CloudOff, Globe, User as UserIcon } from "lucide-react"; import { api } from "../lib/api.ts"; import type { SharedChat } from "../lib/types.ts"; import { AssistantAvatar, Avatar, FlowerMark } from "./ui.tsx"; @@ -23,11 +23,16 @@ export function SharePage({ publicId }: { publicId: string }) {
-

{chat?.title ?? t("share.fallback")}

+

{chat?.title ?? "Shared chat"}

{chat && ( -

- {t("share.by", { name: chat.authorName })} · {new Date(chat.sharedAt).toLocaleDateString()} +

+ by {chat.authorName} · {new Date(chat.sharedAt).toLocaleDateString()} {chat.model && <> · {chat.model.split(":").pop()}} + {chat.archived && ( + + Archived + + )}

)}
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 11d7056..d8880db 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -274,13 +274,13 @@ export function Sidebar({ )}
-
{user.isAdmin && ( diff --git a/frontend/src/components/dialogs.tsx b/frontend/src/components/dialogs.tsx index de28e5e..801b2fe 100644 --- a/frontend/src/components/dialogs.tsx +++ b/frontend/src/components/dialogs.tsx @@ -498,6 +498,9 @@ export function SettingsModal({ )}
+ {search.trim() && visibleCats.length === 0 ? ( +

{t("settings.noMatch", { q: search.trim() })}

+ ) : (
- - {search.trim() && visibleCats.length === 0 && ( -

{t("settings.noMatch", { q: search.trim() })}

)} {error &&

{error}

} diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 399f6a2..6b6b050 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -52,6 +52,7 @@ export interface SharedChat { model: string; authorName: string; authorAvatarUrl: string; + archived: boolean; sharedAt: number; messages: ChatMessage[]; } diff --git a/src/db.ts b/src/db.ts index 1ceb810..cbcd259 100644 --- a/src/db.ts +++ b/src/db.ts @@ -408,7 +408,7 @@ export function deleteShare(convId: string): void { getDb().query("DELETE FROM shares WHERE conv_id = ?").run(convId); } -export function getShareByPublic(publicId: string): { conv: ConversationRow; messages: MessageRow[]; authorName: string; authorAvatarUrl: string; sharedAt: number } | null { +export function getShareByPublic(publicId: string): { conv: ConversationRow; messages: MessageRow[]; authorName: string; authorAvatarUrl: string; archived: boolean; sharedAt: number } | null { const d = getDb(); const s = d.query("SELECT * FROM shares WHERE public_id = ?").get(publicId) as { conv_id: string; created_at: number } | null; if (!s) return null; @@ -420,6 +420,7 @@ export function getShareByPublic(publicId: string): { conv: ConversationRow; mes messages: getMessages(conv.id).filter((m) => m.role !== "system"), authorName: author?.display_name || "Someone", authorAvatarUrl: author?.avatar_url || "", + archived: conv.archived_at !== 0, sharedAt: s.created_at, }; } diff --git a/src/index.ts b/src/index.ts index e1f9f92..a59feed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -201,6 +201,7 @@ const server = Bun.serve({ model: s.conv.model, authorName: s.authorName, authorAvatarUrl: s.authorAvatarUrl, + archived: s.archived, sharedAt: s.sharedAt, messages: s.messages.map((m) => ({ role: m.role, content: m.content })), });