SPB Git forge

spb/immbot-ai

Public
1commits 1branches 0releases
1.5 MBsize
maindefault branch
20 days agolast push
TypeScript 98.3% CSS 0.9% Shell 0.7%
8.9 KB · 198 lines tsx
Raw Blame History
1"use client";2// Paramètres : profil (lecture), changement de mot de passe, thème,3// raccourcis clavier documentés, transparence (à propos).4import { useState } from "react";5import { Info, Keyboard, KeyRound, Palette, UserRound } from "lucide-react";6import { PageHeader } from "@/components/app-shell";7import { Button, Card, Input, Label, Spinner } from "@/components/ui";8import { ThemeToggle } from "@/components/theme-toggle";9import { ErrorBanner, postJson } from "./shared";1011const SHORTCUTS: { keys: string[]; description: string }[] = [12  { keys: ["⌘", "K"], description: "Nouvelle conversation dans le chat" },13  { keys: ["⌘", "J"], description: "Ouvrir le centre d'apprentissage" },14  { keys: ["Échap"], description: "Arrêter la réponse en cours (chat) ou fermer un panneau" },15  { keys: ["Entrée"], description: "Envoyer le message (Maj+Entrée : nouvelle ligne)" },16  { keys: ["Espace"], description: "Retourner la carte mémoire (flashcards)" },17  { keys: ["1", "–", "4"], description: "Noter la carte : Encore / Difficile / Bien / Facile" },18];1920export function SettingsApp({21  displayName,22  username,23  email,24  role,25}: {26  displayName: string;27  username: string;28  email: string | null;29  role: "student" | "instructor" | "admin";30}) {31  const [current, setCurrent] = useState("");32  const [next, setNext] = useState("");33  const [confirmPwd, setConfirmPwd] = useState("");34  const [busy, setBusy] = useState(false);35  const [error, setError] = useState<string | null>(null);36  const [success, setSuccess] = useState(false);3738  async function changePassword(e: React.FormEvent) {39    e.preventDefault();40    setError(null);41    setSuccess(false);42    if (next !== confirmPwd) {43      setError("La confirmation ne correspond pas au nouveau mot de passe.");44      return;45    }46    setBusy(true);47    try {48      await postJson("/api/auth/change-password", { currentPassword: current, newPassword: next });49      setSuccess(true);50      setCurrent("");51      setNext("");52      setConfirmPwd("");53    } catch (err) {54      setError(err instanceof Error ? err.message : "Erreur lors du changement de mot de passe.");55    } finally {56      setBusy(false);57    }58  }5960  const roleLabel = role === "student" ? "Étudiant·e" : role === "admin" ? "Administrateur" : "Professeur";6162  return (63    <main className="px-4 sm:px-6 py-6 max-w-2xl mx-auto w-full space-y-6">64      <PageHeader title="Paramètres" subtitle="Votre compte, votre affichage et le fonctionnement de la plateforme." />6566      {/* Profil */}67      <Card className="p-5">68        <h2 className="flex items-center gap-2 font-semibold text-fg mb-4"><UserRound size={17} className="text-brand-500" /> Profil</h2>69        <dl className="grid grid-cols-[130px_1fr] gap-y-2.5 text-sm">70          <dt className="text-muted">Nom affiché</dt>71          <dd className="text-fg font-medium">{displayName}</dd>72          <dt className="text-muted">Identifiant</dt>73          <dd className="text-fg font-mono text-[13px]">{username}</dd>74          {email && (75            <>76              <dt className="text-muted">Courriel</dt>77              <dd className="text-fg">{email}</dd>78            </>79          )}80          <dt className="text-muted">Rôle</dt>81          <dd className="text-fg">{roleLabel}</dd>82        </dl>83        <p className="text-[12px] text-muted mt-3">Pour modifier votre nom affiché ou vos cours, contactez votre professeur.</p>84      </Card>8586      {/* Mot de passe */}87      <Card className="p-5">88        <h2 className="flex items-center gap-2 font-semibold text-fg mb-4"><KeyRound size={17} className="text-brand-500" /> Mot de passe</h2>89        <form onSubmit={changePassword} className="space-y-3.5">90          <div>91            <Label htmlFor="pwd-current">Mot de passe actuel</Label>92            <Input93              id="pwd-current" type="password" required autoComplete="current-password"94              value={current} onChange={(e) => setCurrent(e.target.value)}95            />96          </div>97          <div className="grid sm:grid-cols-2 gap-3.5">98            <div>99              <Label htmlFor="pwd-new">Nouveau mot de passe</Label>100              <Input101                id="pwd-new" type="password" required autoComplete="new-password" minLength={8}102                value={next} onChange={(e) => setNext(e.target.value)}103              />104            </div>105            <div>106              <Label htmlFor="pwd-confirm">Confirmer le nouveau</Label>107              <Input108                id="pwd-confirm" type="password" required autoComplete="new-password"109                value={confirmPwd} onChange={(e) => setConfirmPwd(e.target.value)}110              />111            </div>112          </div>113          {error && <ErrorBanner message={error} />}114          {success && (115            <p className="text-sm text-emerald-600 dark:text-emerald-400 bg-emerald-500/10 border border-emerald-500/25 rounded-xl px-4 py-3" role="status">116              Mot de passe modifié — vos autres sessions ont été déconnectées par sécurité.117            </p>118          )}119          <Button type="submit" disabled={busy || !current || !next || !confirmPwd}>120            {busy ? <Spinner /> : "Changer le mot de passe"}121          </Button>122        </form>123      </Card>124125      {/* Thème */}126      <Card className="p-5">127        <h2 className="flex items-center gap-2 font-semibold text-fg mb-3"><Palette size={17} className="text-brand-500" /> Thème</h2>128        <div className="flex items-center gap-3">129          <ThemeToggle className="border border-app bg-card" />130          <p className="text-[13px] text-muted">131            Cliquez pour alterner entre clair, sombre et système. « Système » suit automatiquement le réglage de votre132            appareil. Le choix est mémorisé sur cet appareil.133          </p>134        </div>135      </Card>136137      {/* Raccourcis clavier */}138      <Card className="p-5">139        <h2 className="flex items-center gap-2 font-semibold text-fg mb-4"><Keyboard size={17} className="text-brand-500" /> Raccourcis clavier</h2>140        <ul className="space-y-2.5">141          {SHORTCUTS.map((s, i) => (142            <li key={i} className="flex items-center gap-3 text-sm">143              <span className="flex gap-1 shrink-0 w-24">144                {s.keys.map((k, j) => (145                  <kbd key={j} className="px-1.5 py-0.5 rounded-md border border-app bg-surface-2 dark:bg-brand-900/50 text-[11.5px] font-semibold text-fg font-mono">146                    {k}147                  </kbd>148                ))}149              </span>150              <span className="text-fg">{s.description}</span>151            </li>152          ))}153        </ul>154      </Card>155156      {/* À propos / transparence */}157      <Card className="p-5">158        <h2 className="flex items-center gap-2 font-semibold text-fg mb-4"><Info size={17} className="text-brand-500" /> À propos et transparence</h2>159        <div className="space-y-4 text-[13.5px] leading-relaxed text-fg">160          <div>161            <h3 className="font-semibold mb-1">Modes de connaissances</h3>162            <p className="text-muted">163              <strong className="text-fg">Cours uniquement</strong> (par défaut) : les réponses s'appuient exclusivement sur le164              matériel officiel des cours, avec les diapositives citées.{" "}165              <strong className="text-fg">Cours + général</strong> : le matériel cité est complété par les connaissances du166              modèle, clairement distinguées.{" "}167              <strong className="text-fg">Général</strong> : réponse sans le matériel du cours, toujours signalée comme telle.168            </p>169          </div>170          <div>171            <h3 className="font-semibold mb-1">Origine des données</h3>172            <p className="text-muted">173              Le contenu pédagogique provient des documents officiels d'IMM1003 et IMM1033 (UQO), indexés localement.174              Vos activités (cartes, quiz, examens) restent sur le serveur du cours et servent uniquement à estimer votre175              maîtrise et à personnaliser les recommandations. La maîtrise affichée est une <em>estimation</em>, jamais une note.176            </p>177          </div>178          <div>179            <h3 className="font-semibold mb-1">Budget d'usage</h3>180            <p className="text-muted">181              Les fonctions d'IA (chat, résumés, génération de cartes) consomment un budget partagé plafonné par personne et182              par jour, pour garder la plateforme équitable et durable. Si vous atteignez la limite, un message vous l'indique —183              les révisions (flashcards, quiz, examens) restent toujours disponibles, car elles ne consomment pas d'IA.184            </p>185          </div>186          <div>187            <h3 className="font-semibold mb-1">Limites</h3>188            <p className="text-muted">189              Immbot AI peut se tromper. Vérifiez les sources citées, signalez les erreurs avec l'icône drapeau du chat, et190              référez-vous aux documents officiels du cours en cas de doute.191            </p>192          </div>193        </div>194      </Card>195    </main>196  );197}198