:)
This commit is contained in:
+4
-3
@@ -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`,
|
- `lib/api.ts` — typed `fetch` wrapper: Bearer header from `localStorage`,
|
||||||
`{ error }` parsing, `ApiError` with status, auto logout+reload on 401
|
`{ error }` parsing, `ApiError` with status, auto logout+reload on 401
|
||||||
(except on `/share/*`). `chatStream()` parses SSE `token`/`done`/`error`.
|
(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.
|
so the bloom animation always plays, even when the session restores instantly.
|
||||||
- Sending: ensure a server conversation exists (created lazily on first send),
|
- Sending: ensure a server conversation exists (created lazily on first send),
|
||||||
optimistic user message, stream tokens into `streaming`, then append the full
|
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
|
`/api/admin/settings`), SMTP viewer (password blurred by default, eye toggle
|
||||||
to reveal) + connectivity tester (verify + send test mail). Follows the platform theme.
|
to reveal) + connectivity tester (verify + send test mail). Follows the platform theme.
|
||||||
- `SharePage.tsx` — public read-only view, fetches `/api/share/:id` unauthenticated
|
- `SharePage.tsx` — public read-only view, fetches `/api/share/:id` unauthenticated
|
||||||
(`{ title, topic, model, authorName, authorAvatarUrl, sharedAt, messages }`);
|
(`{ title, topic, model, authorName, authorAvatarUrl, archived, sharedAt, messages }`);
|
||||||
user messages render the author's avatar via `Avatar` (URL or initials).
|
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
|
- `Markdown.tsx` — `react-markdown` + GFM; code blocks get a language label and
|
||||||
a copy button.
|
a copy button.
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ function reviewIdFromPath(): string | null {
|
|||||||
|
|
||||||
/** Minimum time the boot splash stays visible, so the bloom animation
|
/** Minimum time the boot splash stays visible, so the bloom animation
|
||||||
* actually plays even when the session restores instantly. */
|
* actually plays even when the session restores instantly. */
|
||||||
const MIN_SPLASH_MS = 2500;
|
const MIN_SPLASH_MS = 1500;
|
||||||
|
|
||||||
function applyTheme(theme: string) {
|
function applyTheme(theme: string) {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from "react";
|
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 { api } from "../lib/api.ts";
|
||||||
import type { SharedChat } from "../lib/types.ts";
|
import type { SharedChat } from "../lib/types.ts";
|
||||||
import { AssistantAvatar, Avatar, FlowerMark } from "./ui.tsx";
|
import { AssistantAvatar, Avatar, FlowerMark } from "./ui.tsx";
|
||||||
@@ -23,11 +23,16 @@ export function SharePage({ publicId }: { publicId: string }) {
|
|||||||
<div className="mx-auto flex w-full max-w-3xl items-center gap-2.5 px-4 py-3">
|
<div className="mx-auto flex w-full max-w-3xl items-center gap-2.5 px-4 py-3">
|
||||||
<FlowerMark size={36} dynamic={false} />
|
<FlowerMark size={36} dynamic={false} />
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="truncate font-semibold">{chat?.title ?? t("share.fallback")}</p>
|
<p className="truncate font-semibold">{chat?.title ?? "Shared chat"}</p>
|
||||||
{chat && (
|
{chat && (
|
||||||
<p className="flex items-center gap-1.5 text-xs opacity-60">
|
<p className="flex flex-wrap items-center gap-1.5 text-xs opacity-60">
|
||||||
<Globe size={11} /> {t("share.by", { name: chat.authorName })} · {new Date(chat.sharedAt).toLocaleDateString()}
|
<Globe size={11} /> by {chat.authorName} · {new Date(chat.sharedAt).toLocaleDateString()}
|
||||||
{chat.model && <> · {chat.model.split(":").pop()}</>}
|
{chat.model && <> · {chat.model.split(":").pop()}</>}
|
||||||
|
{chat.archived && (
|
||||||
|
<span className="inline-flex items-center gap-1 rounded-full bg-stone-200/70 px-2 py-0.5 font-medium opacity-100 dark:bg-zinc-800">
|
||||||
|
<Archive size={11} /> Archived
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -274,13 +274,13 @@ export function Sidebar({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-2 rounded-2xl border border-stone-200/70 bg-white p-2.5 dark:border-zinc-800 dark:bg-zinc-900">
|
<div className="space-y-2 rounded-2xl border border-stone-200/70 bg-white p-2.5 dark:border-zinc-800 dark:bg-zinc-900">
|
||||||
<button onClick={onOpenSettings} className="m-0.5 flex w-[calc(100%-4px)] items-center gap-2.5 rounded-xl p-1.5 text-left transition hover:bg-accent-600/10" title={t("sidebar.settingsTip")}>
|
<button onClick={onOpenSettings} className="flex w-full items-center gap-2.5 rounded-xl p-1.5 text-left transition hover:bg-accent-600/10" title={t("sidebar.settingsTip")}>
|
||||||
<Avatar name={user.displayName} url={user.avatarUrl} size={34} />
|
<Avatar name={user.displayName} url={user.avatarUrl} size={34} />
|
||||||
<span className="min-w-0 flex-1">
|
<span className="min-w-0 flex-1">
|
||||||
<span className="block truncate text-sm font-medium">{user.displayName}</span>
|
<span className="block truncate text-sm font-medium">{user.displayName}</span>
|
||||||
<span className="block truncate text-xs opacity-60">@{user.username}{user.isAdmin ? ` · ${t("sidebar.admin")}` : ""}</span>
|
<span className="block truncate text-xs opacity-60">@{user.username}{user.isAdmin ? ` · ${t("sidebar.admin")}` : ""}</span>
|
||||||
</span>
|
</span>
|
||||||
<Settings size={16} className="opacity-50" />
|
<Settings size={16} className="mr-2 shrink-0 opacity-50" />
|
||||||
</button>
|
</button>
|
||||||
<div className="flex gap-1.5">
|
<div className="flex gap-1.5">
|
||||||
{user.isAdmin && (
|
{user.isAdmin && (
|
||||||
|
|||||||
@@ -498,6 +498,9 @@ export function SettingsModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{search.trim() && visibleCats.length === 0 ? (
|
||||||
|
<p className="px-2 py-4 text-center text-sm opacity-50">{t("settings.noMatch", { q: search.trim() })}</p>
|
||||||
|
) : (
|
||||||
<div className="flex flex-col gap-4 sm:flex-row">
|
<div className="flex flex-col gap-4 sm:flex-row">
|
||||||
<nav aria-label="Settings categories" className="flex shrink-0 gap-1.5 overflow-x-auto sm:w-44 sm:flex-col">
|
<nav aria-label="Settings categories" className="flex shrink-0 gap-1.5 overflow-x-auto sm:w-44 sm:flex-col">
|
||||||
{visibleCats.map((c) => (
|
{visibleCats.map((c) => (
|
||||||
@@ -635,9 +638,6 @@ export function SettingsModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{search.trim() && visibleCats.length === 0 && (
|
|
||||||
<p className="px-2 py-4 text-center text-sm opacity-50">{t("settings.noMatch", { q: search.trim() })}</p>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && <p className="text-sm text-red-600 dark:text-red-400">{error}</p>}
|
{error && <p className="text-sm text-red-600 dark:text-red-400">{error}</p>}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export interface SharedChat {
|
|||||||
model: string;
|
model: string;
|
||||||
authorName: string;
|
authorName: string;
|
||||||
authorAvatarUrl: string;
|
authorAvatarUrl: string;
|
||||||
|
archived: boolean;
|
||||||
sharedAt: number;
|
sharedAt: number;
|
||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -408,7 +408,7 @@ export function deleteShare(convId: string): void {
|
|||||||
getDb().query("DELETE FROM shares WHERE conv_id = ?").run(convId);
|
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 d = getDb();
|
||||||
const s = d.query("SELECT * FROM shares WHERE public_id = ?").get(publicId) as { conv_id: string; created_at: number } | null;
|
const s = d.query("SELECT * FROM shares WHERE public_id = ?").get(publicId) as { conv_id: string; created_at: number } | null;
|
||||||
if (!s) return 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"),
|
messages: getMessages(conv.id).filter((m) => m.role !== "system"),
|
||||||
authorName: author?.display_name || "Someone",
|
authorName: author?.display_name || "Someone",
|
||||||
authorAvatarUrl: author?.avatar_url || "",
|
authorAvatarUrl: author?.avatar_url || "",
|
||||||
|
archived: conv.archived_at !== 0,
|
||||||
sharedAt: s.created_at,
|
sharedAt: s.created_at,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ const server = Bun.serve({
|
|||||||
model: s.conv.model,
|
model: s.conv.model,
|
||||||
authorName: s.authorName,
|
authorName: s.authorName,
|
||||||
authorAvatarUrl: s.authorAvatarUrl,
|
authorAvatarUrl: s.authorAvatarUrl,
|
||||||
|
archived: s.archived,
|
||||||
sharedAt: s.sharedAt,
|
sharedAt: s.sharedAt,
|
||||||
messages: s.messages.map((m) => ({ role: m.role, content: m.content })),
|
messages: s.messages.map((m) => ({ role: m.role, content: m.content })),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user