SPB Git forge

spb/uqo-chat

Public
14commits 1branches 0releases
1.4 MBsize
maindefault branch
17 days agolast push
Python 64.6% TypeScript 33.7% CSS 0.8%
5.4 KB · 86 lines tsx
Raw Blame History
1import { useState } from 'react';2import { useNavigate } from 'react-router-dom';3import { Dialog } from '@/components/ui/dialog';4import { Button } from '@/components/ui/button';5import { useUI } from '@/stores/ui';6import { useAuth } from '@/stores/auth';7import { api, setToken } from '@/lib/api';89export function SettingsDialog() {10  const { settingsOpen, setSettings } = useUI();11  const { user, updatePrefs } = useAuth();12  const nav = useNavigate();13  const [busy, setBusy] = useState(false);14  const [cur, setCur] = useState('');15  const [nw, setNw] = useState('');16  const [pwMsg, setPwMsg] = useState<string | null>(null);17  if (!user) return null;18  const p = user.preferences;1920  const purge = async () => {21    if (!confirm('Supprimer définitivement toutes tes conversations, fichiers et ton compte ? Cette action est irréversible.')) return;22    setBusy(true);23    try {24      await api('/me', { method: 'DELETE' });25      setToken(null);26      useAuth.setState({ user: null });27      nav('/connexion');28    } finally {29      setBusy(false);30      setSettings(false);31    }32  };3334  const Row = ({ label, children, hint }: { label: string; hint?: string; children: React.ReactNode }) => (35    <div className="flex items-center justify-between gap-4 py-3 border-b border-neutral-line/70 last:border-0">36      <div><div className="text-sm font-medium">{label}</div>{hint && <div className="text-xs text-neutral-muted">{hint}</div>}</div>37      {children}38    </div>39  );4041  return (42    <Dialog open={settingsOpen} onOpenChange={setSettings} title="Préférences" description={user.email}>43      <Row label="Nom affiché">44        <input defaultValue={user.display_name || ''} onBlur={(e) => e.target.value !== user.display_name && updatePrefs({ display_name: e.target.value })} className="h-11 w-44 rounded-xl border border-neutral-line px-3 text-sm" />45      </Row>46      <Row label="Tutoiement" hint="Le tuteur te tutoie ou te vouvoie">47        <button onClick={() => updatePrefs({ tutoiement: !(p.tutoiement ?? true) })} className={`h-9 w-16 rounded-full relative transition-colors ${(p.tutoiement ?? true) ? 'bg-uqo-blue' : 'bg-neutral-line'}`} aria-pressed={p.tutoiement ?? true} aria-label="Tutoiement">48          <span className={`absolute top-1 h-7 w-7 rounded-full bg-white shadow transition-all ${(p.tutoiement ?? true) ? 'left-8' : 'left-1'}`} />49        </button>50      </Row>51      <Row label="Cours par défaut">52        <select value={p.course || 'IMM1003'} onChange={(e) => { updatePrefs({ course: e.target.value }); useUI.getState().setCourse(e.target.value); }} className="h-11 rounded-xl border border-neutral-line px-3 text-sm bg-white">53          <option>IMM1003</option><option>IMM1033</option>54        </select>55      </Row>56      <Row label="Réflexion approfondie par défaut" hint="Modèle plus puissant, plus lent et plus coûteux">57        <input type="checkbox" checked={!!p.deep} onChange={(e) => updatePrefs({ deep: e.target.checked })} className="h-5 w-5 accent-uqo-blue" />58      </Row>59      <Row label="Taille du texte">60        <div className="flex gap-1">61          {[0.9, 1, 1.15, 1.3].map((v) => (62            <button key={v} onClick={() => updatePrefs({ font_scale: v })} className={`h-9 px-2.5 rounded-lg text-sm border ${(p.font_scale ?? 1) === v ? 'bg-uqo-blue text-white border-uqo-blue' : 'border-neutral-line'}`}>{v === 0.9 ? 'A−' : v === 1 ? 'A' : v === 1.15 ? 'A+' : 'A++'}</button>63          ))}64        </div>65      </Row>66      <div className="mt-4 rounded-xl border border-neutral-line p-4 text-sm">67        <div className="font-medium">{user.has_password ? 'Changer mon mot de passe' : 'Définir un mot de passe'}</div>68        <p className="text-neutral-muted mt-1">Tu te connectes avec ton courriel et ce mot de passe. Oublié ? Utilise « Première connexion ou mot de passe oublié » sur la page de connexion.</p>69        <div className="mt-2 grid gap-2 sm:grid-cols-2">70          {user.has_password && <input type="password" value={cur} onChange={(e) => setCur(e.target.value)} placeholder="Mot de passe actuel" autoComplete="current-password" className="h-11 rounded-xl border border-neutral-line px-3" />}71          <input type="password" value={nw} onChange={(e) => setNw(e.target.value)} placeholder="Nouveau mot de passe (≥ 8)" autoComplete="new-password" className="h-11 rounded-xl border border-neutral-line px-3" />72        </div>73        <div className="mt-2 flex items-center gap-3">74          <Button size="sm" variant="secondary" disabled={nw.length < 8} onClick={() => api('/me/password', { method: 'POST', body: JSON.stringify({ current_password: cur || null, new_password: nw }) }).then(() => { setPwMsg('Mot de passe enregistré.'); setCur(''); setNw(''); useAuth.getState().load(); }).catch((e) => setPwMsg(e.message))}>Enregistrer</Button>75          {pwMsg && <span className="text-neutral-muted">{pwMsg}</span>}76        </div>77      </div>78      <div className="mt-4 rounded-xl bg-neutral-surface p-4 text-sm">79        <div className="font-medium">Vie privée (Loi 25)</div>80        <p className="text-neutral-muted mt-1">Tes conversations sont conservées 12 mois puis anonymisées ; les fichiers 7 jours (30 si épinglés). Aucune donnée n'est vendue ni utilisée pour entraîner des modèles. <a href="/confidentialite" className="text-uqo-blue underline">Politique complète</a>.</p>81        <Button variant="danger" size="sm" className="mt-3" onClick={purge} disabled={busy}>Supprimer mes données</Button>82      </div>83    </Dialog>84  );85}86