Python 64.6%
TypeScript 33.7%
CSS 0.8%
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