This commit is contained in:
k
2026-09-21 16:38:40 +02:00
parent 8038e02303
commit 70c21698e8
2 changed files with 38 additions and 36 deletions
+11 -9
View File
@@ -14,6 +14,7 @@ import {
} from "lucide-react";
import type { Conversation, User } from "../lib/types.ts";
import { api } from "../lib/api.ts";
import { useT } from "../lib/i18n.ts";
import { Avatar, Button, ContextMenu, FlowerMark, IconButton } from "./ui.tsx";
import { cn } from "../lib/cn.ts";
@@ -58,6 +59,7 @@ export function Sidebar({
mobileOpen: boolean;
onCloseMobile: () => void;
}) {
const { t } = useT();
const [menu, setMenu] = useState<MenuState | null>(null);
const [query, setQuery] = useState("");
const [results, setResults] = useState<(Conversation & { snippet: string | null; snippetRole: string | null })[] | null>(null);
@@ -132,12 +134,12 @@ export function Sidebar({
<div className="flex items-center gap-1">
<Button variant="secondary" size="sm" className="flex-1" onClick={onNew}>
<MessageSquarePlus size={16} />
New chat
{t("sidebar.newChat")}
</Button>
<IconButton title="Collapse sidebar" onClick={onToggle} className="hidden md:inline-flex">
<IconButton title={t("sidebar.collapse")} onClick={onToggle} className="hidden md:inline-flex">
<PanelLeftClose size={18} />
</IconButton>
<IconButton title="Close chats" onClick={onCloseMobile} className="md:hidden">
<IconButton title={t("sidebar.closeDrawer")} onClick={onCloseMobile} className="md:hidden">
<X size={18} />
</IconButton>
</div>
@@ -146,13 +148,13 @@ export function Sidebar({
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search chats…"
aria-label="Search conversations"
placeholder={t("sidebar.searchPh")}
aria-label={t("sidebar.searchAria")}
className="w-full rounded-2xl border border-stone-200/70 bg-stone-100 py-2 pl-9 pr-8 text-sm outline-none transition placeholder:text-stone-400 focus:border-accent-500/60 focus:bg-white dark:border-zinc-800 dark:bg-zinc-800/60 dark:placeholder:text-zinc-500 dark:focus:bg-zinc-900"
/>
{query && (
<button
aria-label="Clear search"
aria-label={t("sidebar.clearSearch")}
onClick={() => setQuery("")}
className="absolute right-2.5 top-1/2 -translate-y-1/2 rounded-full p-0.5 opacity-60 transition hover:opacity-100"
>
@@ -164,9 +166,9 @@ export function Sidebar({
<div className="flex-1 space-y-1 overflow-y-auto py-1">
{results !== null ? (
<>
{searching && <p className="px-3 py-4 text-center text-sm opacity-50">Searching…</p>}
{searching && <p className="px-3 py-4 text-center text-sm opacity-50">{t("sidebar.searching")}</p>}
{!searching && results.length === 0 && (
<p className="px-3 py-6 text-center text-sm opacity-50">No match for “{query.trim()}”.</p>
<p className="px-3 py-6 text-center text-sm opacity-50">{t("sidebar.noMatch", { q: query.trim() })}</p>
)}
{results.map((c) => (
<div
@@ -185,7 +187,7 @@ export function Sidebar({
<span className="block truncate">{c.title}</span>
{c.snippet && (
<span className="mt-0.5 block truncate text-xs opacity-60">
{c.snippetRole === "user" ? "you: " : ""}{c.snippet}
{c.snippetRole === "user" ? t("sidebar.youPrefix") : ""}{c.snippet}
</span>
)}
</div>
+27 -27
View File
@@ -805,25 +805,25 @@ function SecuritySection({ onKeyRotated }: { onKeyRotated?: () => void }) {
return (
<div className="space-y-5">
<div>
<p className="mb-1 flex items-center gap-1.5 text-sm font-medium"><KeyRound size={14} className="opacity-60" /> Access key</p>
<p className="mb-2 text-xs opacity-60">Rotate your access key. Old sessions are revoked immediately — you'll log back in with the new key.</p>
<Button size="sm" variant="secondary" type="button" disabled={busy} onClick={rotate}>
{armingRotate ? "Click again to confirm rotation" : "Rotate my access key"}
<p className="mb-1 flex items-center gap-1.5 text-sm font-medium"><KeyRound size={14} className="opacity-60" /> {t("sec.accessKey")}</p>
<p className="mb-2 text-xs opacity-60">{t("sec.accessKeyHint")}</p>
<Button size="sm" variant="secondary" disabled={busy} onClick={rotate}>
{armingRotate ? t("sec.rotateConfirm") : t("sec.rotate")}
</Button>
</div>
<div>
<p className="mb-1 flex items-center gap-1.5 text-sm font-medium"><ShieldCheck size={14} className="opacity-60" /> Two-factor (TOTP)</p>
{totpOn === null && <p className="flex items-center gap-2 text-sm opacity-60"><Spinner size={14} /> Checking…</p>}
<p className="mb-1 flex items-center gap-1.5 text-sm font-medium"><ShieldCheck size={14} className="opacity-60" /> {t("sec.totp")}</p>
{totpOn === null && <p className="flex items-center gap-2 text-sm opacity-60"><Spinner size={14} /> {t("sec.totpChecking")}</p>}
{totpOn === false && !secret && (
<>
<p className="mb-2 text-xs opacity-60">Add a 6-digit code from your authenticator app on top of your access key.</p>
<Button size="sm" variant="secondary" type="button" disabled={busy} onClick={startTotp}>Enable two-factor</Button>
<p className="mb-2 text-xs opacity-60">{t("sec.totpAddHint")}</p>
<Button size="sm" variant="secondary" disabled={busy} onClick={startTotp}>{t("sec.totpEnable")}</Button>
</>
)}
{secret && (
<div className="space-y-2 rounded-2xl border border-stone-200 p-3 dark:border-zinc-700">
<p className="text-xs opacity-70">Add this secret to your authenticator app (or open the otpauth link), then enter a code to confirm:</p>
<p className="text-xs opacity-70">{t("sec.totpSecretHint")}</p>
<div className="flex items-center gap-2">
<code className="min-w-0 flex-1 break-all rounded-xl bg-stone-100 px-3 py-2 font-mono text-sm dark:bg-zinc-800">{secret}</code>
<CopyButton text={secret} />
@@ -834,30 +834,30 @@ function SecuritySection({ onKeyRotated }: { onKeyRotated?: () => void }) {
value={code}
onChange={(e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6))}
placeholder="000000"
aria-label="Authenticator code"
aria-label={t("login.totpAria")}
inputMode="numeric"
className="text-center tracking-[0.4em]"
/>
<Button size="sm" type="button" disabled={busy || code.length !== 6} onClick={confirmTotp}>Confirm</Button>
<Button size="sm" disabled={busy || code.length !== 6} onClick={confirmTotp}>{t("sec.totpConfirm")}</Button>
</div>
</div>
)}
{totpOn === true && (
<div className="flex flex-wrap items-end gap-2">
<div className="min-w-36 flex-1">
<Field label="Disable with a current code">
<Field label={t("sec.totpDisableLabel")}>
<Input
value={code}
onChange={(e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6))}
placeholder="000000"
aria-label="Current authenticator code"
aria-label={t("login.totpAria")}
inputMode="numeric"
className="text-center tracking-[0.4em]"
/>
</Field>
</div>
<Button size="sm" variant="secondary" type="button" disabled={busy || code.length !== 6} onClick={disableTotp} className="!text-red-600 dark:!text-red-400">
Disable 2FA
<Button size="sm" variant="secondary" disabled={busy || code.length !== 6} onClick={disableTotp} className="!text-red-600 dark:!text-red-400">
{t("sec.totpDisable")}
</Button>
</div>
)}
@@ -866,31 +866,31 @@ function SecuritySection({ onKeyRotated }: { onKeyRotated?: () => void }) {
<div className="flex items-center gap-3 rounded-2xl px-1 py-1 opacity-50">
<Fingerprint size={16} />
<span className="min-w-0 flex-1">
<span className="block text-sm font-medium">Passkeys <span className="ml-1 text-xs opacity-60">(soon)</span></span>
<span className="block text-xs opacity-60">WebAuthn / platform authenticators — needs a server RP setup.</span>
<span className="block text-sm font-medium">{t("sec.passkeys")} <span className="ml-1 text-xs opacity-60">{t("feat.soon")}</span></span>
<span className="block text-xs opacity-60">{t("sec.passkeysHint")}</span>
</span>
</div>
<div className="rounded-2xl border border-red-500/30 p-3">
<p className="mb-1 flex items-center gap-1.5 text-sm font-medium text-red-600 dark:text-red-400">
<Trash2 size={14} /> Danger zone
<Trash2 size={14} /> {t("sec.danger")}
</p>
<p className="mb-2 text-xs opacity-60">Permanently delete your account with all chats, shares and sessions. Cannot be undone.</p>
<Button size="sm" variant="secondary" type="button" disabled={busy} onClick={() => setConfirmDelete(true)} className="!text-red-600 dark:!text-red-400">
Delete my account
<p className="mb-2 text-xs opacity-60">{t("sec.dangerHint")}</p>
<Button size="sm" variant="secondary" disabled={busy} onClick={() => setConfirmDelete(true)} className="!text-red-600 dark:!text-red-400">
{t("sec.deleteMe")}
</Button>
</div>
{error && <p className="text-sm text-red-600 dark:text-red-400">{error}</p>}
<Modal open={rotatedKey !== null} onClose={() => {}} title="New access key" icon={KeyRound}>
<Modal open={rotatedKey !== null} onClose={() => {}} title={t("sec.newKeyTitle")} icon={KeyRound}>
<div className="space-y-4">
<p className="text-sm opacity-80">Copy it now — it won't be shown again. Your old sessions are revoked.</p>
<p className="text-sm opacity-80">{t("sec.newKeyHint")}</p>
<div className="flex items-center gap-2">
<code className="min-w-0 flex-1 truncate rounded-2xl bg-stone-100 px-3 py-2.5 font-mono text-sm dark:bg-zinc-800">{rotatedKey}</code>
{rotatedKey && <CopyButton text={rotatedKey} />}
</div>
<Button size="sm" className="w-full" onClick={() => onKeyRotated?.()}>Done — log me out</Button>
<Button size="sm" className="w-full" onClick={() => onKeyRotated?.()}>{t("sec.newKeyDone")}</Button>
</div>
</Modal>
@@ -898,9 +898,9 @@ function SecuritySection({ onKeyRotated }: { onKeyRotated?: () => void }) {
open={confirmDelete}
onClose={() => setConfirmDelete(false)}
onConfirm={removeAccount}
title="Delete your account?"
message="This permanently removes your account with all conversations, shares and sessions. This cannot be undone."
confirmLabel="Delete forever"
title={t("sec.deleteTitle")}
message={t("sec.deleteMsg")}
confirmLabel={t("sec.deleteForever")}
/>
</div>
);