update
This commit is contained in:
@@ -42,3 +42,10 @@ Système de service mails aussi pour les comptes (optionel) pour recovery passwo
|
||||
when disabling attachment sendability: the text "Message" placeholder nto the composer chat are too tigh to the left border, make a fallback padding when no attachment button
|
||||
|
||||
make in the settigns: an sub category: changing key, admiting TOTP key/passkey for security
|
||||
|
||||
--- bugd II:
|
||||
sur la page share, l' avatar de l'utilisateur ne s'affiche pas -> fix sa
|
||||
|
||||
--- improvements:
|
||||
éviter de ask h24 ollama pour ces modèles quand un utilisateur ce connece. c'est le backend qui sert les demande d el'utilisateurs. iL relay pas sur le ollama. Le backend vas cacher avec un TLL les modèles présent en mémire dans un objet qui est dans le backend. refresh avec un TT: toutes les minutes.
|
||||
L'administrateur lui, qand il demande les modèles dams le ollama, lui, force un refresh .
|
||||
|
||||
@@ -89,7 +89,7 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
|
||||
{totpToken ? (
|
||||
<form onSubmit={submitTotp} className="mt-6 space-y-3">
|
||||
<p className="text-center text-sm opacity-70">
|
||||
Two-factor for <strong>@{totpUsername}</strong> — enter the 6-digit code from your authenticator app.
|
||||
{t("login.totpIntro", { user: "@" + totpUsername })}
|
||||
</p>
|
||||
<div className="relative">
|
||||
<KeyRound size={17} className="pointer-events-none absolute left-5 top-1/2 -translate-y-1/2 text-stone-400 dark:text-zinc-500" />
|
||||
@@ -156,13 +156,13 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
|
||||
</p>
|
||||
)}
|
||||
<Button type="submit" variant="gradient" size="lg" className="w-full" disabled={busy}>
|
||||
{busy ? <Spinner /> : (<>Unlock <ArrowRight size={17} /></>)}
|
||||
{busy ? <Spinner /> : (<>{t("login.unlock")} <ArrowRight size={17} /></>)}
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
<div className="mt-4 flex items-center gap-3 text-xs opacity-40">
|
||||
<span className="h-px flex-1 bg-current" />
|
||||
New here?
|
||||
{t("login.newHere")}
|
||||
<span className="h-px flex-1 bg-current" />
|
||||
</div>
|
||||
<Button
|
||||
@@ -170,11 +170,11 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
|
||||
size="lg"
|
||||
className="mt-3 w-full"
|
||||
disabled={!registrationOn}
|
||||
title={registrationOn ? "Create an account" : "Registration currently disabled on this platform"}
|
||||
title={registrationOn ? t("login.regTipOn") : t("login.regTipOff")}
|
||||
onClick={() => setRegisterOpen(true)}
|
||||
>
|
||||
<UserPlus size={16} />
|
||||
Register
|
||||
{t("login.register")}
|
||||
</Button>
|
||||
<RegisterDialog
|
||||
open={registerOpen}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { cn } from "../lib/cn.ts";
|
||||
import { ACCENT_PRESETS, applyAccent, currentAccent, previewAccent, type AccentState } from "../lib/accent.ts";
|
||||
import { FEATURES, setFeature, useFeatures } from "../lib/features.ts";
|
||||
import type { Conversation, FilePreview, User } from "../lib/types.ts";
|
||||
import { Avatar, bumpCdnVersion, Button, CopyButton, Field, Input, Modal, Picker, Spinner, Toggle } from "./ui.tsx";
|
||||
import { Avatar, bumpCdnVersion, Button, ConfirmDialog, CopyButton, Field, Input, Modal, Picker, Spinner, Toggle } from "./ui.tsx";
|
||||
|
||||
/* ---------- Rename + topic ---------- */
|
||||
|
||||
@@ -705,7 +705,7 @@ function SecuritySection({ onKeyRotated }: { onKeyRotated?: () => void }) {
|
||||
const [error, setError] = useState("");
|
||||
const [rotatedKey, setRotatedKey] = useState<string | null>(null);
|
||||
const [armingRotate, setArmingRotate] = useState(false);
|
||||
const [armingDelete, setArmingDelete] = useState(false);
|
||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
api.totpStatus().then((r) => setTotpOn(r.enabled)).catch(() => setTotpOn(false));
|
||||
@@ -776,14 +776,11 @@ function SecuritySection({ onKeyRotated }: { onKeyRotated?: () => void }) {
|
||||
}
|
||||
|
||||
async function removeAccount() {
|
||||
if (!armingDelete) {
|
||||
setArmingDelete(true);
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
await api.deleteMe();
|
||||
setConfirmDelete(false);
|
||||
onKeyRotated?.();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Delete failed");
|
||||
@@ -796,24 +793,10 @@ function SecuritySection({ onKeyRotated }: { onKeyRotated?: () => void }) {
|
||||
<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>
|
||||
{rotatedKey ? (
|
||||
<div className="rounded-2xl border border-accent-500/40 bg-accent-50 p-3 dark:bg-accent-950/30">
|
||||
<p className="mb-2 text-sm font-medium">New key — copy it now, it won't be shown again:</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="min-w-0 flex-1 truncate rounded-xl bg-white px-3 py-2 text-sm dark:bg-zinc-900">{rotatedKey}</code>
|
||||
<CopyButton text={rotatedKey} />
|
||||
</div>
|
||||
<p className="mt-2 text-xs opacity-70">Your old sessions are revoked. Log back in with the new key.</p>
|
||||
<Button size="sm" className="mt-2 w-full" onClick={() => onKeyRotated?.()}>Done — log me out</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<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" disabled={busy} onClick={rotate}>
|
||||
{armingRotate ? "Click again to confirm rotation" : "Rotate my access key"}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<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" disabled={busy} onClick={rotate}>
|
||||
{armingRotate ? "Click again to confirm rotation" : "Rotate my access key"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -880,12 +863,35 @@ function SecuritySection({ onKeyRotated }: { onKeyRotated?: () => void }) {
|
||||
<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 size="sm" variant="secondary" disabled={busy} onClick={() => setConfirmDelete(true)} className="!text-red-600 dark:!text-red-400">
|
||||
Delete my account
|
||||
</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}>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<ConfirmDialog
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+31
-11
@@ -56,8 +56,12 @@ const en = {
|
||||
"login.forgot": "Forgot your access key?",
|
||||
"login.perk1": "Private by design — no cookies, no tracking",
|
||||
"login.perk2": "Your own models, streamed in real time",
|
||||
"login.adminNote":
|
||||
"Admin key lives in the server .env; user keys are issued by the admin.",
|
||||
"login.adminNotePre": "Admin key lives in the server ",
|
||||
"login.adminNotePost": "; user keys are issued by the admin.",
|
||||
"login.totpIntro": "Two-factor for @{user} — enter the 6-digit code from your authenticator app.",
|
||||
"login.totpAria": "Authenticator code",
|
||||
"login.totpInvalid": "Invalid code",
|
||||
"login.verify": "Verify",
|
||||
|
||||
"sidebar.newChat": "New chat",
|
||||
"sidebar.collapse": "Collapse sidebar",
|
||||
@@ -357,8 +361,12 @@ const es: Dict = {
|
||||
"login.forgot": "¿Olvidaste tu clave de acceso?",
|
||||
"login.perk1": "Privado por diseño — sin cookies ni rastreo",
|
||||
"login.perk2": "Tus propios modelos, en tiempo real",
|
||||
"login.adminNote":
|
||||
"La clave de admin vive en el .env del servidor; las claves de usuario las emite el admin.",
|
||||
"login.adminNotePre": "La clave de admin vive en el .env del servidor",
|
||||
"login.adminNotePost": "; las claves de usuario las emite el admin.",
|
||||
"login.totpIntro": "Doble factor para @{user} — escribe el código de 6 dígitos de tu app.",
|
||||
"login.totpAria": "Código de autenticación",
|
||||
"login.totpInvalid": "Código inválido",
|
||||
"login.verify": "Verificar",
|
||||
|
||||
"sidebar.newChat": "Nuevo chat",
|
||||
"sidebar.collapse": "Ocultar barra lateral",
|
||||
@@ -650,8 +658,12 @@ const fr: Dict = {
|
||||
"login.forgot": "Clé d'accès oubliée ?",
|
||||
"login.perk1": "Privé par conception — sans cookies ni suivi",
|
||||
"login.perk2": "Vos propres modèles, en temps réel",
|
||||
"login.adminNote":
|
||||
"La clé admin vit dans le .env du serveur ; les clés utilisateur sont émises par l'admin.",
|
||||
"login.adminNotePre": "La clé admin vit dans le .env du serveur",
|
||||
"login.adminNotePost": " ; les clés utilisateur sont émises par l'admin.",
|
||||
"login.totpIntro": "Double facteur pour @{user} — entrez le code à 6 chiffres de votre app.",
|
||||
"login.totpAria": "Code d'authentification",
|
||||
"login.totpInvalid": "Code invalide",
|
||||
"login.verify": "Vérifier",
|
||||
|
||||
"sidebar.newChat": "Nouveau chat",
|
||||
"sidebar.collapse": "Masquer la barre latérale",
|
||||
@@ -943,8 +955,12 @@ const ru: Dict = {
|
||||
"login.forgot": "Забыли ключ доступа?",
|
||||
"login.perk1": "Приватность по умолчанию — без cookies и трекинга",
|
||||
"login.perk2": "Ваши модели, в реальном времени",
|
||||
"login.adminNote":
|
||||
"Ключ админа живёт в .env сервера; ключи пользователей выдаёт админ.",
|
||||
"login.adminNotePre": "Ключ админа живёт в .env сервера",
|
||||
"login.adminNotePost": "; ключи пользователей выдаёт админ.",
|
||||
"login.totpIntro": "Двухфакторка для @{user} — введите 6-значный код из приложения.",
|
||||
"login.totpAria": "Код аутентификатора",
|
||||
"login.totpInvalid": "Неверный код",
|
||||
"login.verify": "Проверить",
|
||||
|
||||
"sidebar.newChat": "Новый чат",
|
||||
"sidebar.collapse": "Скрыть панель",
|
||||
@@ -1225,8 +1241,12 @@ const it: Dict = {
|
||||
"login.forgot": "Chiave dimenticata?",
|
||||
"login.perk1": "Privato per progettazione — niente cookie né traccianti",
|
||||
"login.perk2": "I tuoi modelli, in tempo reale",
|
||||
"login.adminNote":
|
||||
"La chiave admin vive nel .env del server; le chiavi utente le emette l'admin.",
|
||||
"login.adminNotePre": "La chiave admin vive nel .env del server",
|
||||
"login.adminNotePost": "; le chiavi utente le emette l'admin.",
|
||||
"login.totpIntro": "Doppio fattore per @{user} — inserisci il codice a 6 cifre dall'app.",
|
||||
"login.totpAria": "Codice autenticatore",
|
||||
"login.totpInvalid": "Codice non valido",
|
||||
"login.verify": "Verifica",
|
||||
|
||||
"sidebar.newChat": "Nuova chat",
|
||||
"sidebar.collapse": "Nascondi barra laterale",
|
||||
@@ -1815,7 +1835,7 @@ export function t(
|
||||
let s: string = dicts[current][key] ?? dicts.en[key] ?? key;
|
||||
if (vars) {
|
||||
for (const [k, v] of Object.entries(vars))
|
||||
s = s.split(`{${k}}`).join(String(v));
|
||||
s = s.replaceAll(`{${k}}`, String(v));
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
|
||||
Reference in New Issue
Block a user