fixes
This commit is contained in:
@@ -1,17 +1,14 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { ArrowRight, KeyRound, MessageSquareText, ShieldCheck, User, UserPlus, Zap } from "lucide-react";
|
||||
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 { RecoverDialog, RegisterDialog } from "./dialogs.tsx";
|
||||
import { AccessRequestModal } from "./ReviewPage.tsx";
|
||||
|
||||
const PERKS = [
|
||||
{ icon: ShieldCheck, text: "Private by design — no cookies, no tracking" },
|
||||
{ icon: Zap, text: "Your own models, streamed in real time" },
|
||||
];
|
||||
|
||||
export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
|
||||
const { t } = useT();
|
||||
const [username, setUsername] = useState("admin");
|
||||
const [key, setKey] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
@@ -23,6 +20,11 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
|
||||
const [registrationOn, setRegistrationOn] = useState(false);
|
||||
const [requestOpen, setRequestOpen] = useState(false);
|
||||
const [requestOn, setRequestOn] = useState(false);
|
||||
|
||||
const PERKS = [
|
||||
{ icon: ShieldCheck, text: t("login.perk1") },
|
||||
{ icon: Zap, text: t("login.perk2") },
|
||||
];
|
||||
// TOTP second step (set when the server answers totpRequired).
|
||||
const [totpToken, setTotpToken] = useState<string | null>(null);
|
||||
const [totpUsername, setTotpUsername] = useState("");
|
||||
@@ -52,7 +54,7 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
|
||||
setToken(res.token);
|
||||
onLogin(res.user);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Login failed");
|
||||
setError(err instanceof Error ? err.message : t("login.failed"));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
@@ -80,8 +82,8 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
|
||||
<div className="rounded-[2rem] border border-white/60 bg-white/80 p-8 shadow-2xl shadow-accent-900/5 backdrop-blur-xl dark:border-zinc-700/60 dark:bg-zinc-900/80">
|
||||
<div className="mb-2 flex flex-col items-center text-center">
|
||||
<FlowerMark size={68} dynamic={false} className="mb-4 drop-shadow-lg" />
|
||||
<h1 className="text-2xl font-bold tracking-tight">Welcome back</h1>
|
||||
<p className="mt-1 text-sm opacity-60">Sign in to KisAssistant to continue</p>
|
||||
<h1 className="text-2xl font-bold tracking-tight">{t("login.welcome")}</h1>
|
||||
<p className="mt-1 text-sm opacity-60">{t("login.subtitle")}</p>
|
||||
</div>
|
||||
|
||||
{totpToken ? (
|
||||
@@ -129,8 +131,8 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
autoComplete="username"
|
||||
placeholder="Username"
|
||||
aria-label="Username"
|
||||
placeholder={t("login.usernamePh")}
|
||||
aria-label={t("login.usernamePh")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -143,8 +145,8 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
|
||||
value={key}
|
||||
onChange={(e) => setKey(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
placeholder="Access key"
|
||||
aria-label="Access key"
|
||||
placeholder={t("login.keyPh")}
|
||||
aria-label={t("login.keyPh")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -189,13 +191,13 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
|
||||
onClick={() => setRequestOpen(true)}
|
||||
>
|
||||
<MessageSquareText size={16} />
|
||||
Request access
|
||||
{t("login.requestAccess")}
|
||||
</Button>
|
||||
)}
|
||||
<AccessRequestModal open={requestOpen} onClose={() => setRequestOpen(false)} />
|
||||
{recoveryOn && (
|
||||
<button onClick={() => setRecoverOpen(true)} className="mt-3 w-full text-center text-sm opacity-60 transition hover:opacity-100 hover:underline">
|
||||
Forgot your access key?
|
||||
{t("login.forgot")}
|
||||
</button>
|
||||
)}
|
||||
<RecoverDialog open={recoverOpen} onClose={() => setRecoverOpen(false)} from={recoveryFrom} />
|
||||
|
||||
@@ -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, Unplug, User as UserIcon, UserPlus, X } from "lucide-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 { api } from "../lib/api.ts";
|
||||
import { cn } from "../lib/cn.ts";
|
||||
import { ACCENT_PRESETS, applyAccent, currentAccent, previewAccent, type AccentState } from "../lib/accent.ts";
|
||||
@@ -761,6 +761,36 @@ function SecuritySection({ onKeyRotated }: { onKeyRotated?: () => void }) {
|
||||
}
|
||||
|
||||
async function disableTotp() {
|
||||
if (code.replace(/\D/g, "").length !== 6) return;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
await api.totpDisable(code.replace(/\D/g, ""));
|
||||
setTotpOn(false);
|
||||
setCode("");
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Invalid code");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeAccount() {
|
||||
if (!armingDelete) {
|
||||
setArmingDelete(true);
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
await api.deleteMe();
|
||||
onKeyRotated?.();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Delete failed");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
@@ -845,6 +875,16 @@ function SecuritySection({ onKeyRotated }: { onKeyRotated?: () => void }) {
|
||||
</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
|
||||
</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" disabled={busy} onClick={removeAccount} className="!text-red-600 dark:!text-red-400">
|
||||
{armingDelete ? "Click again to permanently delete" : "Delete my account"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && <p className="text-sm text-red-600 dark:text-red-400">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -268,6 +268,7 @@ export function Modal({
|
||||
dragStart.current = null;
|
||||
}
|
||||
}, [open ]);
|
||||
const { t } = useT();
|
||||
|
||||
if (!open) return null;
|
||||
const Icon = icon;
|
||||
@@ -308,7 +309,7 @@ export function Modal({
|
||||
</span>
|
||||
)}
|
||||
<h2 className="text-lg font-semibold">{title}</h2>
|
||||
<button onClick={onClose} aria-label="Close" className="ml-auto rounded-full p-1.5 hover:bg-stone-200/60 dark:hover:bg-zinc-800">
|
||||
<button onClick={onClose} aria-label={t("common.close")} className="ml-auto rounded-full p-1.5 hover:bg-stone-200/60 dark:hover:bg-zinc-800">
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -325,7 +326,7 @@ export function ConfirmDialog({
|
||||
onConfirm,
|
||||
title,
|
||||
message,
|
||||
confirmLabel = "Delete",
|
||||
confirmLabel,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
@@ -334,12 +335,13 @@ export function ConfirmDialog({
|
||||
message: string;
|
||||
confirmLabel?: string;
|
||||
}) {
|
||||
const { t } = useT();
|
||||
return (
|
||||
<Modal open={open} onClose={onClose} title={title}>
|
||||
<p className="text-sm opacity-80">{message}</p>
|
||||
<div className="mt-5 flex justify-end gap-2">
|
||||
<Button variant="secondary" size="sm" onClick={onClose}>Cancel</Button>
|
||||
<Button variant="danger" size="sm" onClick={() => { onConfirm(); onClose(); }}>{confirmLabel}</Button>
|
||||
<Button variant="secondary" size="sm" onClick={onClose}>{t("common.cancel")}</Button>
|
||||
<Button variant="danger" size="sm" onClick={() => { onConfirm(); onClose(); }}>{confirmLabel ?? t("common.delete")}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
@@ -559,7 +561,8 @@ export function Toggle({
|
||||
);
|
||||
}
|
||||
|
||||
export function CopyButton({ text, label = "Copy" }: { text: string; label?: string }) {
|
||||
export function CopyButton({ text, label }: { text: string; label?: string }) {
|
||||
const { t } = useT();
|
||||
const [done, setDone] = useState(false);
|
||||
return (
|
||||
<button
|
||||
@@ -572,7 +575,7 @@ export function CopyButton({ text, label = "Copy" }: { text: string; label?: str
|
||||
className="inline-flex items-center gap-1.5 rounded-full border border-stone-200 bg-white px-3 py-1.5 text-xs font-medium transition hover:bg-stone-50 dark:border-zinc-700 dark:bg-zinc-900 dark:hover:bg-zinc-800"
|
||||
>
|
||||
{done ? <Check size={13} className="text-accent-500" /> : <Copy size={13} />}
|
||||
{done ? "Copied" : label}
|
||||
{done ? t("common.copied") : (label ?? t("common.copy"))}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -581,12 +584,13 @@ export function CopyButton({ text, label = "Copy" }: { text: string; label?: str
|
||||
|
||||
/** Animated blooming flower shown while the app boots (themed, mobile-safe). */
|
||||
export function LoadingScreen() {
|
||||
const { t } = useT();
|
||||
return (
|
||||
<div className="loading-enter grid min-h-full place-items-center" role="status" aria-label="Loading KisAssistant">
|
||||
<div className="flex flex-col items-center gap-4 px-6 text-center">
|
||||
<FlowerMark className="h-16 w-16 sm:h-20 sm:w-20" />
|
||||
<p className="font-serif text-2xl font-bold tracking-tight">KisAssistant</p>
|
||||
<p className="loading-dots text-sm opacity-60">Preparing your chats</p>
|
||||
<p className="loading-dots text-sm opacity-60">{t("loading.tagline")}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user