This commit is contained in:
k
2026-09-21 17:14:32 +02:00
parent acdfe4ca8e
commit 2705dd47b3
5 changed files with 244 additions and 11 deletions
+17
View File
@@ -243,6 +243,22 @@ export function Chat({
</>
)}
</div>
{readOnly ? (
<div className="mx-auto flex w-full max-w-3xl items-center gap-3 rounded-[1.75rem] border border-stone-200 bg-stone-100 p-2 pl-4 shadow-lg dark:border-zinc-700 dark:bg-zinc-800/60">
<span className="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-accent-600/10 text-accent-600 dark:text-accent-400">
<Archive size={17} />
</span>
<p className="min-w-0 flex-1 text-sm opacity-70">{t("archived.readonly")}</p>
<button
type="button"
onClick={onUnarchive}
className="inline-flex shrink-0 items-center gap-1.5 rounded-full bg-accent-600 px-4 py-2.5 text-sm font-medium text-white transition hover:bg-accent-500 dark:bg-accent-500 dark:text-zinc-950 dark:hover:bg-accent-400"
>
<ArchiveRestore size={15} />
{t("archived.unarchive")}
</button>
</div>
) : (
<form onSubmit={submit} className={`mx-auto flex w-full max-w-3xl items-end gap-2 rounded-[1.75rem] border border-stone-200 bg-white p-2 shadow-lg dark:border-zinc-700 dark:bg-zinc-900 ${features.attachments ? "pl-2" : "pl-5"}`}>
{features.attachments && (
<button
@@ -281,6 +297,7 @@ export function Chat({
<SendHorizontal size={17} />
</button>
</form>
)}
<p className="py-2 text-center text-xs opacity-50">
<a href="/wiki" target="_blank" rel="noreferrer" className="inline-flex items-center gap-1 underline-offset-2 hover:underline">
<BookOpen size={12} />
+5 -1
View File
@@ -3,7 +3,7 @@ import { ArrowRight, KeyRound, MessageSquareText, ShieldCheck, User, UserPlus, Z
import { api, setToken } from "../lib/api.ts";
import { useT } from "../lib/i18n.ts";
import type { User as UserType } from "../lib/types.ts";
import { Button, FlowerMark, Input, Spinner } from "./ui.tsx";
import { Button, FlowerMark, Input, LangPicker, Spinner } from "./ui.tsx";
import { RecoverDialog, RegisterDialog } from "./dialogs.tsx";
import { AccessRequestModal } from "./ReviewPage.tsx";
@@ -202,6 +202,10 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
)}
<RecoverDialog open={recoverOpen} onClose={() => setRecoverOpen(false)} from={recoveryFrom} />
<div className="mt-4 flex justify-center">
<LangPicker />
</div>
<ul className="mt-6 space-y-2 border-t border-stone-200/70 pt-5 dark:border-zinc-800">
{PERKS.map((p) => (
<li key={p.text} className="flex items-center gap-2.5 text-[13px] opacity-70">
+132 -1
View File
@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from "react";
import { Fingerprint, Globe, FileText, ImagePlus, KeyRound, Link2, Mail, Palette, Pencil, ScanText, Search, Settings2, ShieldCheck, Sparkles, Tag, Trash2, Unplug, User as UserIcon, UserPlus, X } from "lucide-react";
import { Archive, ArchiveRestore, Eye, Fingerprint, Globe, FileText, ImagePlus, KeyRound, Link2, Mail, Palette, Pencil, ScanText, Search, Settings2, ShieldCheck, Sparkles, Tag, Trash2, Unplug, User as UserIcon, UserPlus, X } from "lucide-react";
import { api } from "../lib/api.ts";
import { cn } from "../lib/cn.ts";
import { ACCENT_PRESETS, applyAccent, currentAccent, previewAccent, type AccentState } from "../lib/accent.ts";
@@ -345,11 +345,15 @@ export function SettingsModal({
onClose,
onSaved,
onKeyRotated,
onViewArchived,
onArchivedChanged,
}: {
user: User | null;
onClose: () => void;
onSaved: (u: User) => void;
onKeyRotated?: () => void;
onViewArchived: (c: Conversation) => void;
onArchivedChanged: (deletedId?: string) => void;
}) {
const { t } = useT();
const [displayName, setDisplayName] = useState("");
@@ -382,6 +386,7 @@ export function SettingsModal({
{ id: "appearance", title: t("settings.appearance"), icon: Palette, keywords: ["appearance", "theme", "dark", "light", "color", "colour", "accent", "palette", "language", "langue", "idioma", "lingua", "язык"] },
{ id: "features", title: t("settings.features"), icon: Sparkles, keywords: ["features", "thinking", "attachments", "search", "deep", "upload", "ocr", "capabilities", "enable", "disable"] },
{ id: "security", title: t("settings.security"), icon: ShieldCheck, keywords: ["security", "key", "rotate", "password", "totp", "2fa", "two", "factor", "authenticator", "passkey", "webauthn"] },
{ id: "archived", title: t("archived.title"), icon: Archive, keywords: ["archived", "archive", "old", "hidden", "read-only", "readonly", "archiv"] },
] as const;
type CatId = (typeof CATS)[number]["id"];
const [cat, setCat] = useState<CatId>("profile");
@@ -622,6 +627,12 @@ export function SettingsModal({
<SecuritySection onKeyRotated={onKeyRotated} />
</SettingsPane>
)}
{activeCat === "archived" && (
<SettingsPane>
<ArchivedSection onView={onViewArchived} onChanged={onArchivedChanged} />
</SettingsPane>
)}
</div>
</div>
@@ -651,6 +662,126 @@ function SettingsPane({ children }: { children: React.ReactNode }) {
);
}
/* ---------- Archived chats (settings): view, unarchive, delete ---------- */
function ArchivedSection({ onView, onChanged }: { onView: (c: Conversation) => void; onChanged: (deletedId?: string) => void }) {
const { t } = useT();
const [list, setList] = useState<Conversation[] | null>(null);
const [error, setError] = useState("");
const [busyId, setBusyId] = useState<string | null>(null);
const [deleteTarget, setDeleteTarget] = useState<Conversation | null>(null);
async function refresh() {
try {
const res = await api.archivedConvs();
setList(res.conversations);
} catch (err) {
setError(err instanceof Error ? err.message : t("common.loadFailed"));
}
}
useEffect(() => {
refresh();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
async function unarchive(c: Conversation) {
setBusyId(c.id);
setError("");
try {
await api.unarchiveConv(c.id);
await refresh();
onChanged();
} catch (err) {
setError(err instanceof Error ? err.message : t("common.saveFailed"));
} finally {
setBusyId(null);
}
}
async function remove(c: Conversation) {
setBusyId(c.id);
setError("");
try {
await api.deleteConv(c.id);
setDeleteTarget(null);
await refresh();
onChanged(c.id);
} catch (err) {
setError(err instanceof Error ? err.message : t("common.deleteFailed"));
} finally {
setBusyId(null);
}
}
if (list === null && !error) {
return <p className="flex items-center gap-2 py-4 text-sm opacity-60"><Spinner size={15} /> {t("common.loading")}</p>;
}
return (
<div>
<p className="-mb-2 text-xs opacity-60">{t("archived.readonly")}</p>
{error && <p className="text-sm text-red-600 dark:text-red-400">{error}</p>}
{(list ?? []).length === 0 ? (
<p className="py-4 text-center text-sm opacity-50">{t("archived.empty")}</p>
) : (
<ul className="space-y-2">
{(list ?? []).map((c) => (
<li key={c.id} className="flex items-center gap-2 rounded-2xl border border-stone-200/70 px-3.5 py-2.5 dark:border-zinc-800">
<span className="grid h-9 w-9 shrink-0 place-items-center rounded-full bg-accent-600/10 text-accent-600 dark:text-accent-400">
<Archive size={15} />
</span>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{c.title}</p>
<p className="truncate text-xs opacity-50">{new Date(c.updated_at).toLocaleDateString()}</p>
</div>
<div className="flex shrink-0 gap-1">
<button
type="button"
title={t("archived.view")}
aria-label={`${t("archived.view")}: ${c.title}`}
onClick={() => onView(c)}
className="rounded-full p-2 transition hover:bg-stone-100 dark:hover:bg-zinc-800"
>
<Eye size={15} />
</button>
<button
type="button"
title={t("archived.unarchive")}
aria-label={`${t("archived.unarchive")}: ${c.title}`}
disabled={busyId === c.id}
onClick={() => unarchive(c)}
className="rounded-full p-2 transition hover:bg-stone-100 disabled:opacity-50 dark:hover:bg-zinc-800"
>
<ArchiveRestore size={15} />
</button>
<button
type="button"
title={t("common.delete")}
aria-label={`${t("common.delete")}: ${c.title}`}
disabled={busyId === c.id}
onClick={() => setDeleteTarget(c)}
className="rounded-full p-2 text-red-600 transition hover:bg-red-50 disabled:opacity-50 dark:text-red-400 dark:hover:bg-red-950/40"
>
<Trash2 size={15} />
</button>
</div>
</li>
))}
</ul>
)}
<ConfirmDialog
open={deleteTarget !== null}
onClose={() => setDeleteTarget(null)}
title={t("archived.deleteTitle")}
message={t("archived.deleteMsg")}
confirmLabel={t("common.delete")}
onConfirm={() => deleteTarget && remove(deleteTarget)}
/>
</div>
);
}
/* ---------- Accent color (settings) ---------- */
function AccentSection() {