From 79bf70710e773aa8c4b1376d7c0cced59fc11e37 Mon Sep 17 00:00:00 2001 From: D4kooo Date: Fri, 29 May 2026 12:53:15 +0200 Subject: [PATCH 01/38] =?UTF-8?q?feat(onboarding):=20boutons=20d'aide=20?= =?UTF-8?q?=C2=AB=20i=20=C2=BB=20sur=20les=20modules=20+=20guides?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Composant réutilisable : petit bouton (i) près du titre d'un module ouvrant un popover (aide contextuelle courte + lien « En savoir plus » vers la doc publique). URL surchargeable via NEXT_PUBLIC_DOCS_URL. Posé sur Dashboard (prise en main), Settings → Providers, Settings → Connecteurs, Documents et l'écran « nouvelle conversation » du chat. Ajoute deux guides utilisateur : prise en main (onboarding 5 étapes) et travailler par projet. Le bouton de la page Projets suivra (fichier en cours de refonte). --- .env.example | 4 + docs/user/getting-started.md | 98 ++++++++++++++++++++++ docs/user/projects.md | 66 +++++++++++++++ src/app/(app)/chat/chat-shell.tsx | 14 +++- src/app/(app)/dashboard/page.tsx | 14 +++- src/app/(app)/documents/page.tsx | 10 ++- src/app/(app)/settings/connectors/page.tsx | 14 +++- src/app/(app)/settings/providers/page.tsx | 10 ++- src/components/module-help.tsx | 78 +++++++++++++++++ 9 files changed, 297 insertions(+), 11 deletions(-) create mode 100644 docs/user/getting-started.md create mode 100644 docs/user/projects.md create mode 100644 src/components/module-help.tsx diff --git a/.env.example b/.env.example index d75e1d5..c263c03 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,10 @@ # Application NODE_ENV=development NEXT_PUBLIC_APP_URL=http://localhost:3000 +# URL de la documentation publique (boutons d'aide « i » des modules). +# Défaut : https://louis.data-ring.net/docs. À surcharger seulement si vous +# hébergez votre propre miroir de la doc. +# NEXT_PUBLIC_DOCS_URL=https://louis.data-ring.net/docs # Base de données (PostgreSQL + pgvector) # Compatible avec le docker-compose.yml fourni (port 5433 pour ne pas diff --git a/docs/user/getting-started.md b/docs/user/getting-started.md new file mode 100644 index 0000000..a2388bf --- /dev/null +++ b/docs/user/getting-started.md @@ -0,0 +1,98 @@ +# Prise en main + +Ce guide vous fait passer d'une instance Louis vide à votre première +conversation utile, en cinq étapes. Comptez **5 à 10 minutes**. + +> Vous découvrez Louis sans l'avoir installé ? L'installation (Docker, +> base de données, secrets) est côté administrateur : voir +> [Installation](../installation/docker-compose.md). Ce guide suppose une +> instance déjà en ligne et un compte créé pour vous. + +## Le principe en une phrase + +Louis n'embarque **aucune clé** ni **aucun connecteur** par défaut. Vous +branchez les vôtres : vos clés IA restent chez vous, vos données restent +sur votre infrastructure. C'est le sens du « Bring Your Own Key ». + +## Étape 1 — Connecter une clé provider IA (obligatoire) + +Sans au moins une clé provider active, le chat ne peut pas répondre. + +1. Ouvrez **Paramètres → Providers** (`/settings/providers`) +2. Cliquez **Ajouter une clé**, choisissez un provider, collez la clé +3. Activez la clé (toggle **Actif**), puis testez la connexion + +**Commencez par Mistral** (🇫🇷) : c'est le seul provider qui fournit aussi +les *embeddings* nécessaires à la recherche sémantique dans vos documents +(RAG) en v0.1. Sans clé Mistral active, le chat fonctionne mais la +recherche dans vos documents est limitée. + +→ Détail de chaque provider (souverains FR/UE, international, self-hosted) : +[Configuration des providers](../configuration/providers.md). + +## Étape 2 — Connecter vos sources juridiques (optionnel) + +Pour que Louis interroge des sources de droit officielles pendant une +conversation : + +- **PISTE** (DILA) — donne accès à **Légifrance**. Inscription sur + [piste.gouv.fr](https://piste.gouv.fr/), puis **Paramètres → + Connecteurs**. +- **Pappers** — base entreprises (SIREN, dirigeants, bénéficiaires + effectifs). Clé sur [pappers.fr/api](https://www.pappers.fr/api). + +> Couverture réelle en v0.1 : **Légifrance** et **Pappers** sont +> fonctionnels. Judilibre, JADE, INPI et BODACC sont prévus mais pas +> encore implémentés — voir [État des fonctionnalités](../feature-status.md). + +→ Pas à pas et tests : [Configuration des connecteurs](../configuration/connectors.md). + +## Étape 3 — Lancer une première conversation + +1. Allez sur **Conversations** (`/chat`) +2. Choisissez un provider et un modèle dans le sélecteur (le badge + FR / UE / US reste visible pendant toute la conversation) +3. Tapez votre question, `Entrée` pour envoyer + +Si Louis appelle un outil (recherche Légifrance, lecture d'un document…), +une pastille cliquable apparaît dans la réponse : cliquez-la pour voir +exactement ce que le modèle a reçu et renvoyé. + +→ Tout le chat (joindre des documents, workflows, export) : +[Utiliser le chat](./chat.md). + +## Étape 4 — Importer un document + +1. **Documents → Uploader** (PDF, DOCX ou texte, ≤ 25 Mo) +2. Une fois importé, joignez-le à une conversation via l'icône trombone, + ou laissez Louis le retrouver par recherche sémantique + +Avec une clé Mistral active, le document est découpé et indexé pour le +RAG. Sans, il reste consultable et joignable (son texte va alors dans le +contexte du message). + +→ Dossiers, versions, aperçu : [Gérer les documents](./documents.md). + +## Étape 5 — Organiser un dossier client en projet + +Un **projet** regroupe les conversations et les documents d'un même +dossier, et **restreint le RAG à ce périmètre** : l'IA ne raisonne que +sur les pièces et échanges du projet. + +1. **Projets → Nouveau projet** +2. Rattachez-le à un dossier de documents (nouveau ou existant) +3. Déplacez-y vos conversations et documents via `⋮ → Déplacer vers projet` + +→ Détail du fonctionnement : [Travailler par projet](./projects.md). + +## Et après ? + +- **Workflows** — des prompts cabinet réutilisables (résumé d'arrêt, + analyse de clause…) insérables en un clic dans le chat. +- **Serveurs MCP** — branchez n'importe quel outil métier (base de + précédents, ERP, signature) via le Model Context Protocol : + [Connecteurs → MCP](../configuration/connectors.md). +- **Coûts & usage** — suivez la dépense estimée par conversation et au + global dans **Paramètres → Coûts & usage**. +- **Administration** (si vous êtes admin) — comptes, journal d'audit, + sauvegardes : [Guide admin](../admin/users.md). diff --git a/docs/user/projects.md b/docs/user/projects.md new file mode 100644 index 0000000..1207daf --- /dev/null +++ b/docs/user/projects.md @@ -0,0 +1,66 @@ +# Travailler par projet + +Un **projet** est un dossier client : il regroupe des conversations et des +documents, et **restreint le raisonnement de l'IA à ce périmètre**. C'est +le bon réflexe dès qu'un dossier prend de l'ampleur. + +## Ce qu'un projet change concrètement + +Quand vous discutez **dans le contexte d'un projet**, Louis ne prend en +compte que : + +- les **documents** rangés dans le dossier du projet (et ses + sous-dossiers, récursivement) ; +- l'**historique des autres conversations** du même projet. + +Autrement dit, les outils documentaires (`search_documents`, +`read_document`…) et la recherche d'historique sont **scopés** au projet — +pas de fuite vers les autres dossiers du cabinet, et pas de bruit venu +d'affaires sans rapport. Un document généré dans une conversation de +projet atterrit directement dans le dossier du projet. + +> Mental model : un projet = un espace de connaissance fermé (ses pièces +> + ses échanges), à la manière d'un NotebookLM ou d'un Projet Claude — +> pas une recherche globale sur tout le cabinet. + +## Créer un projet + +1. **Projets → Nouveau projet** +2. Donnez-lui un nom (ex. « Dupont c/ Martin ») +3. Choisissez son **emplacement de stockage** : + - **Nouveau dossier** (pré-rempli au nom du projet), ou + - **Dossier existant** dans votre arborescence `/documents` + +Un projet a **toujours** un dossier de stockage : c'est lui qui définit +quels documents appartiennent au projet. + +## Rattacher conversations et documents + +Depuis une conversation, une entrée de la sidebar ou un document : + +> `⋮ → Déplacer vers projet` + +- Une **conversation** déplacée vers un projet voit son contexte RAG se + restreindre au projet. Un breadcrumb projet (avec un point bleu) + apparaît en haut du chat. +- Un **document** déplacé est rangé dans le dossier du projet — il entre + donc dans le périmètre RAG du projet. + +## Démarrer une conversation déjà dans un projet + +Depuis la page d'un projet, lancez une nouvelle conversation : elle est +créée d'emblée rattachée au projet, avec le périmètre RAG correspondant. + +## Bon à savoir + +- **Sans document dans le projet**, les outils de recherche renvoient + simplement « rien trouvé » plutôt que de retomber sur tous vos + documents — c'est volontaire, pour éviter toute fuite inter-projets. +- L'indexation de l'historique des conversations (pour la recherche + croisée intra-projet) nécessite une **clé Mistral active**. Sans elle, + l'indexation est ignorée silencieusement. +- Hors projet, le comportement historique reste inchangé : l'IA voit + l'ensemble de vos documents. + +→ Pour gérer les fichiers eux-mêmes (dossiers, versions, aperçu) : +[Gérer les documents](./documents.md). diff --git a/src/app/(app)/chat/chat-shell.tsx b/src/app/(app)/chat/chat-shell.tsx index 88c6830..e3955ed 100644 --- a/src/app/(app)/chat/chat-shell.tsx +++ b/src/app/(app)/chat/chat-shell.tsx @@ -24,6 +24,7 @@ import { DefaultChatTransport, type UIMessage } from "ai"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { LouisLogo } from "@/components/louis-logo"; +import { ModuleHelp } from "@/components/module-help"; import { Dropzone, uploadDocument } from "@/components/dropzone"; import { useSmoothText } from "@/lib/use-smooth-text"; import { useStickToBottom } from "@/lib/use-stick-to-bottom"; @@ -2124,9 +2125,16 @@ function EmptyState() {
-

- Une nouvelle conversation. -

+
+

+ Une nouvelle conversation. +

+ + Choisissez un modèle, posez votre question. Joignez un document + (trombone) ou insérez un workflow (étoiles). Chaque appel + d'outil est inspectable. + +

Tapez votre question dans le composer ci-dessous — ou parcourez ces points d'entrée. diff --git a/src/app/(app)/dashboard/page.tsx b/src/app/(app)/dashboard/page.tsx index c429167..b9fac15 100644 --- a/src/app/(app)/dashboard/page.tsx +++ b/src/app/(app)/dashboard/page.tsx @@ -21,6 +21,7 @@ import { aggregateCosts, formatTotals, } from "@/lib/providers/pricing"; +import { ModuleHelp } from "@/components/module-help"; export default async function DashboardPage() { const session = await auth(); @@ -96,9 +97,16 @@ export default async function DashboardPage() {

Tableau de bord

-

- Bonjour{firstName ? `, ${firstName}` : ""}. -

+
+

+ Bonjour{firstName ? `, ${firstName}` : ""}. +

+ + Nouveau sur Louis ? Le parcours de mise en route en 5 étapes : + connecter une clé provider, vos sources juridiques, puis lancer + votre première conversation. + +
{!hasProvider && } diff --git a/src/app/(app)/documents/page.tsx b/src/app/(app)/documents/page.tsx index 464be4d..4d57b27 100644 --- a/src/app/(app)/documents/page.tsx +++ b/src/app/(app)/documents/page.tsx @@ -17,6 +17,7 @@ import { DocumentRow } from "./document-row"; import { FolderRow } from "./folder-row"; import { NewFolderButton } from "./new-folder-button"; import { DocumentsDropzone } from "./documents-dropzone"; +import { ModuleHelp } from "@/components/module-help"; type SP = { folder?: string }; @@ -118,7 +119,14 @@ export default async function DocumentsPage({
-

Documents

+
+

Documents

+ + Importez vos PDF / DOCX (≤ 25 Mo), organisez-les en dossiers et + versionnez-les. Interrogez-les ensuite depuis le chat (pièce + jointe ou recherche sémantique). + +

Vos fichiers sont stockés sur votre infrastructure (S3, MinIO, OVH Object Storage…). Organisez-les en dossiers et diff --git a/src/app/(app)/settings/connectors/page.tsx b/src/app/(app)/settings/connectors/page.tsx index 215b487..ff34415 100644 --- a/src/app/(app)/settings/connectors/page.tsx +++ b/src/app/(app)/settings/connectors/page.tsx @@ -9,6 +9,7 @@ import { type ConnectorType, } from "@/lib/connectors/catalog"; import { ConnectorCard } from "./connector-card"; +import { ModuleHelp } from "@/components/module-help"; // Connecteurs prévus dans la roadmap mais pas encore implémentés. Affichés // en cartes "Bientôt" pour montrer où va le produit. @@ -59,9 +60,16 @@ export default async function ConnectorsPage() { return (

-

- Connecteurs juridiques -

+
+

+ Connecteurs juridiques +

+ + PISTE donne accès à Légifrance ; Pappers aux données entreprises. + Vos identifiants restent chiffrés sur votre instance. Vous pouvez + aussi brancher n'importe quel serveur MCP. + +

Branchez vos accès aux sources de droit français. Vos identifiants, vos quotas, vos contrats — Louis ne s'interpose pas. diff --git a/src/app/(app)/settings/providers/page.tsx b/src/app/(app)/settings/providers/page.tsx index 73882ce..eda305f 100644 --- a/src/app/(app)/settings/providers/page.tsx +++ b/src/app/(app)/settings/providers/page.tsx @@ -5,6 +5,7 @@ import { auth } from "@/auth"; import { db } from "@/db"; import { providerKeys, type ProviderKey } from "@/db/schema"; import { type ProviderType } from "@/lib/providers/catalog"; +import { ModuleHelp } from "@/components/module-help"; import { ProviderCard } from "./provider-card"; type Group = { @@ -56,7 +57,14 @@ export default async function ProvidersPage() { return (

-

Gestion des clés API

+
+

Gestion des clés API

+ + Louis fonctionne en Bring Your Own Key : ajoutez votre clé, + activez-la, testez la connexion. Commencez par Mistral + {" "}(🇫🇷) — c'est lui qui alimente la recherche dans vos documents. + +

Connectez vos propres clés. Une seule règle : vos clés ne quittent jamais votre instance. diff --git a/src/components/module-help.tsx b/src/components/module-help.tsx new file mode 100644 index 0000000..09a8d0b --- /dev/null +++ b/src/components/module-help.tsx @@ -0,0 +1,78 @@ +"use client"; + +import Link from "next/link"; +import { IconInfoCircle, IconArrowUpRight } from "@tabler/icons-react"; +import { Button } from "@/components/ui/button"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; + +// Base de la doc publique. Surchargeable (NEXT_PUBLIC_DOCS_URL) pour pointer +// vers un miroir self-hosted ; par défaut la doc officielle DataRing. +const DOCS_BASE = + process.env.NEXT_PUBLIC_DOCS_URL ?? "https://louis.data-ring.net/docs"; + +/** + * Petit bouton « info » (i) à poser près du titre d'un module. Au clic : + * popover avec une aide contextuelle courte (setup/usage) + un lien vers la + * page de documentation complète (nouvel onglet). + * + * Exemple : + * + * Configurez une clé provider, puis lancez votre première conversation. + * + */ +export function ModuleHelp({ + slug, + title, + children, + align = "start", + label = "Aide sur ce module", +}: { + /** Chemin de la page de doc, ex. "user/getting-started" (sans slash initial). */ + slug: string; + /** Titre affiché en haut du popover. */ + title: string; + /** Aide contextuelle courte (2-3 phrases). */ + children: React.ReactNode; + align?: "start" | "center" | "end"; + /** aria-label du bouton (accessibilité). */ + label?: string; +}) { + const href = `${DOCS_BASE}/${slug}`; + + return ( + + + + + +

+ {title} +

+
+ {children} +
+ + En savoir plus + + + + + ); +} From a6d3b037172c6c918961d676ceaaac6e2cfc95f9 Mon Sep 17 00:00:00 2001 From: D4kooo Date: Sun, 31 May 2026 11:45:21 +0200 Subject: [PATCH 02/38] fix(a11y): accessibility, theming & perf pass across all modules Systematic audit-driven fixes across 7 module groups (UI surface), verified with tsc + lint + production build (all green). Criticals: - Chat: scope the streaming live-region (was re-announcing every token) - Board: gate infinite edge animation + node pulse behind reduced-motion; add text/icon status badges (was color-only) - Settings: drop invalid CutoutCard role="button" wrapping nested interactives; add explicit "Configurer" trigger + hoist delete dialog Systemic: - aria-current on active nav (sidebar / settings / admin) - Spinner primitive: role=status on wrapper, sr-only label, motion-safe - form helper text wired via aria-describedby - citation highlight -> --highlight token (was hardcoded yellow) - list/table semantics: ul/li, scope, caption, role=progressbar, dl metrics - reduced-motion gating on spring/infinite animations - dead "Bientot" controls removed; emoji -> @tabler icons - print footer contrast; admin stats no longer hidden on mobile Bugs: - broken oklch(oklch()) vignette gradient (rendered nothing) -> color-mix - hardcoded black MiniMap maskColor -> token (dark-mode correct) Perf: - React.memo on assistant markdown rows via stable onOpenDoc, so historical messages stop re-rendering/re-linkifying on every streamed token --- src/app/(app)/admin/admin-nav.tsx | 4 +- src/app/(app)/admin/cabinet/cabinet-form.tsx | 1 + src/app/(app)/admin/users/[id]/page.tsx | 9 ++- src/app/(app)/admin/users/user-row.tsx | 53 +++++++++++- src/app/(app)/admin/users/users-table.tsx | 2 + src/app/(app)/board/[id]/add-agent-dialog.tsx | 2 +- src/app/(app)/board/[id]/agent-flow-node.tsx | 29 ++++++- src/app/(app)/board/[id]/animated-edge.tsx | 4 +- src/app/(app)/board/[id]/inline-rename.tsx | 5 +- src/app/(app)/board/[id]/page.tsx | 9 ++- .../(app)/board/[id]/pipeline-mode-bar.tsx | 40 ++++++++-- .../(app)/board/[id]/pipeline-workflow.tsx | 4 +- src/app/(app)/board/agent-edit-sheet.tsx | 29 ++++--- src/app/(app)/chat/agent-theatre.tsx | 9 ++- .../(app)/chat/assistant-message-actions.tsx | 4 +- src/app/(app)/chat/chat-shell.tsx | 48 ++++++++--- src/app/(app)/chat/composer-menu.tsx | 11 --- src/app/(app)/chat/docx-view.tsx | 6 +- src/app/(app)/chat/edit-card.tsx | 5 +- src/app/(app)/chat/live-workflow-panel.tsx | 9 ++- src/app/(app)/chat/pdf-view.tsx | 7 +- src/app/(app)/chat/thinking-indicator.tsx | 8 +- src/app/(app)/command-palette.tsx | 2 +- src/app/(app)/dashboard/page.tsx | 14 ++-- src/app/(app)/documents/document-row.tsx | 1 + .../(app)/documents/documents-dropzone.tsx | 6 +- src/app/(app)/documents/folder-row.tsx | 3 + src/app/(app)/documents/page.tsx | 33 +++++--- src/app/(app)/mobile-nav.tsx | 1 + .../settings/connectors/connector-card.tsx | 80 +++++++++---------- src/app/(app)/settings/connectors/page.tsx | 12 +-- .../(app)/settings/general/theme-picker.tsx | 3 +- src/app/(app)/settings/mcp/add-mcp-dialog.tsx | 3 +- .../(app)/settings/profile/password-form.tsx | 3 +- src/app/(app)/settings/providers/page.tsx | 13 +-- .../settings/providers/provider-card.tsx | 37 ++++----- src/app/(app)/settings/settings-nav.tsx | 4 +- .../settings/skills/skill-form-dialog.tsx | 6 +- src/app/(app)/settings/usage/page.tsx | 14 ++-- src/app/(app)/sidebar-content.tsx | 14 +++- .../tabular-reviews/[id]/auto-refresh.tsx | 35 ++++++-- .../[id]/column-edit-popover.tsx | 18 +---- .../tabular-reviews/[id]/review-grid.tsx | 36 ++++++--- .../tabular-reviews/new/new-review-form.tsx | 13 ++- src/app/login/login-form.tsx | 6 +- src/app/print/chat/[id]/page.tsx | 8 +- src/components/ui/checkbox.tsx | 1 + src/components/ui/select.tsx | 2 + src/components/ui/spinner.tsx | 5 +- 49 files changed, 431 insertions(+), 240 deletions(-) diff --git a/src/app/(app)/admin/admin-nav.tsx b/src/app/(app)/admin/admin-nav.tsx index c9c3369..e8b53b0 100644 --- a/src/app/(app)/admin/admin-nav.tsx +++ b/src/app/(app)/admin/admin-nav.tsx @@ -44,6 +44,7 @@ export function AdminNav({ horizontal }: { horizontal?: boolean }) { {sections.map((group) => (
-

+

{group.group}

    @@ -77,6 +78,7 @@ export function AdminNav({ horizontal }: { horizontal?: boolean }) {
  • Affiché dans l'UI et utilisé par défaut dans le footer des diff --git a/src/app/(app)/admin/users/[id]/page.tsx b/src/app/(app)/admin/users/[id]/page.tsx index bee9b24..610b3bf 100644 --- a/src/app/(app)/admin/users/[id]/page.tsx +++ b/src/app/(app)/admin/users/[id]/page.tsx @@ -191,7 +191,14 @@ export default async function AdminUserDetailPage({

-
+
= 100 diff --git a/src/app/(app)/admin/users/user-row.tsx b/src/app/(app)/admin/users/user-row.tsx index be7d434..b9930d1 100644 --- a/src/app/(app)/admin/users/user-row.tsx +++ b/src/app/(app)/admin/users/user-row.tsx @@ -220,6 +220,50 @@ export function UserRow({ {feedback && (
{feedback}
)} + + {/* Résumé compact des stats sur mobile : la rangée détaillée + (Conv./Docs/Projets/Ce mois) est masquée < md, on reflow donc + les chiffres ici pour ne pas les perdre sur petit écran. */} +
+ + Conv. + + {entry.stats.convCount} + + + + Docs + + {entry.stats.docCount} + + + + Projets + + {entry.stats.projectCount} + + + + Ce mois + 0 && + monthSpentCents >= quotaCents + ? "text-destructive" + : "text-foreground" + }`} + > + {formatEurFromCents(monthSpentCents)} + {quotaCents != null && ( + + {" / "} + {formatEurFromCents(quotaCents)} + + )} + + +
{/* Stats compactes : 4 chiffres en tabular-nums. Cabinet-friendly : @@ -250,7 +294,14 @@ export function UserRow({ )}
{quotaPercent != null && ( -
+
= 100 diff --git a/src/app/(app)/admin/users/users-table.tsx b/src/app/(app)/admin/users/users-table.tsx index e01f347..bb382d9 100644 --- a/src/app/(app)/admin/users/users-table.tsx +++ b/src/app/(app)/admin/users/users-table.tsx @@ -69,6 +69,7 @@ export function UsersTable({ rows, currentUserId, nowMs }: Props) { value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Rechercher par email ou nom…" + aria-label="Rechercher un utilisateur" className="w-full rounded-md border border-input bg-background pl-8 pr-3 py-1.5 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50" />
@@ -78,6 +79,7 @@ export function UsersTable({ rows, currentUserId, nowMs }: Props) { key={f} type="button" onClick={() => setFilter(f)} + aria-pressed={filter === f} className={`px-2.5 py-1.5 rounded-md text-xs transition-colors ${ filter === f ? "bg-foreground text-background" diff --git a/src/app/(app)/board/[id]/add-agent-dialog.tsx b/src/app/(app)/board/[id]/add-agent-dialog.tsx index dac94f1..d7136c5 100644 --- a/src/app/(app)/board/[id]/add-agent-dialog.tsx +++ b/src/app/(app)/board/[id]/add-agent-dialog.tsx @@ -163,7 +163,7 @@ export function AddAgentDialog({
{mode === "council" && ( -

+

{(() => { const debaters = Math.max(0, agentCount - 1); const calls = rounds * debaters + 1; return ( <> - ⚠️ Coût estimé : {calls} appel{calls > 1 ? "s" : ""} LLM par - question — {debaters} débatteur{debaters > 1 ? "s" : ""} sur{" "} - {rounds} tour{rounds > 1 ? "s" : ""}, plus 1 synthèse finale. + + + Coût estimé : {calls} appel{calls > 1 ? "s" : ""} LLM par + question — {debaters} débatteur{debaters > 1 ? "s" : ""} sur{" "} + {rounds} tour{rounds > 1 ? "s" : ""}, plus 1 synthèse finale. + ); })()} diff --git a/src/app/(app)/board/[id]/pipeline-workflow.tsx b/src/app/(app)/board/[id]/pipeline-workflow.tsx index 3833441..4fd497a 100644 --- a/src/app/(app)/board/[id]/pipeline-workflow.tsx +++ b/src/app/(app)/board/[id]/pipeline-workflow.tsx @@ -349,7 +349,7 @@ function PipelineWorkflowInner({ {/* Vignette radiale subtile pour donner du caractère au canvas */}

{/* Bouton reset : visible dès qu'au moins un agent a des @@ -409,7 +409,7 @@ function PipelineWorkflowInner({ pannable zoomable className="!bg-card !border !border-border" - maskColor="rgb(0 0 0 / 0.05)" + maskColor="color-mix(in oklab, var(--foreground) 6%, transparent)" nodeColor="var(--color-foreground)" /> )} diff --git a/src/app/(app)/board/agent-edit-sheet.tsx b/src/app/(app)/board/agent-edit-sheet.tsx index 11f0069..90d8edc 100644 --- a/src/app/(app)/board/agent-edit-sheet.tsx +++ b/src/app/(app)/board/agent-edit-sheet.tsx @@ -251,15 +251,26 @@ export function AgentEditSheet({ maxLength={8000} aria-describedby={`prompt-help-${agent.id}`} /> - {systemPrompt.length > 2000 && ( -

- ⚠️ Ce prompt sera répété à chaque appel de cet agent — un - prompt long multiplie les coûts en mode council/parallel. -

- )} +

2000 + ? "flex items-start gap-1 text-xs text-warning" + : "text-xs text-muted-foreground" + } + > + {systemPrompt.length > 2000 ? ( + <> + + + Ce prompt sera répété à chaque appel de cet agent — un + prompt long multiplie les coûts en mode council/parallel. + + + ) : ( + "Vide = prompt « factory » du rôle. Plus le prompt est long, plus chaque appel coûte cher." + )} +

diff --git a/src/app/(app)/chat/agent-theatre.tsx b/src/app/(app)/chat/agent-theatre.tsx index 6047773..a34cdff 100644 --- a/src/app/(app)/chat/agent-theatre.tsx +++ b/src/app/(app)/chat/agent-theatre.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useRef } from "react"; -import { AnimatePresence, motion } from "motion/react"; +import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { @@ -259,6 +259,7 @@ function TurnCard({ turn }: { turn: AgentTurn }) { const meta = roleMeta(turn.role); const Icon = meta.icon; const isFinal = turn.key.endsWith("-final"); + const reduceMotion = useReducedMotion(); return ( 12 && ( <> - + {availableModels.length - 12} modèles supplémentaires — changez de modèle dans le composer - + )} diff --git a/src/app/(app)/chat/chat-shell.tsx b/src/app/(app)/chat/chat-shell.tsx index e3955ed..d9bd1d6 100644 --- a/src/app/(app)/chat/chat-shell.tsx +++ b/src/app/(app)/chat/chat-shell.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react"; +import { memo, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useChat } from "@ai-sdk/react"; @@ -675,7 +675,7 @@ function hasRenderableText( * un rendu char-par-char à ~60fps indépendant du débit SSE. Sur les * messages historiques, render direct sans buffer. */ -function AssistantMarkdownPart({ +const AssistantMarkdownPart = memo(function AssistantMarkdownPart({ text, isLive, mergedDocuments, @@ -729,7 +729,7 @@ function AssistantMarkdownPart({ {linkifyDocMentions(display, mergedDocuments)} ); -} +}); function DocPickerContent({ documents, @@ -915,6 +915,14 @@ export function ChatShell({ }, [initialConversationId] ); + // Adaptateur stable (signature ReactMarkdown/ToolPart → setOpenDoc) pour que + // `React.memo(AssistantMarkdownPart)` ne se ré-invalide pas à chaque token : + // les messages historiques ne re-rendent plus pendant le streaming. + const handleOpenDoc = useCallback( + (documentId: string, targetText: string) => + setOpenDoc({ documentId, targetText }), + [setOpenDoc] + ); // Tracking des document_id déjà auto-ouverts dans le DocPanel pour ne // pas réouvrir à chaque re-render. Survit au remount qui se produit // quand l'URL passe de /chat à /chat?id=xxx via sessionStorage. @@ -1045,6 +1053,10 @@ export function ChatShell({ [] ); + // Annonce de complétion pour lecteurs d'écran (région live dédiée, à part + // du thread streamé). Vide pendant le stream, renseigné dans onFinish. + const [statusText, setStatusText] = useState(""); + const { messages, setMessages, @@ -1064,6 +1076,7 @@ export function ChatShell({ // d'affichage stable indépendant du rythme du provider. experimental_throttle: 50, onFinish: ({ message }) => { + setStatusText("Réponse de Louis terminée."); const meta = message?.metadata as | { conversationId?: string; usage?: Usage } | undefined; @@ -1235,6 +1248,8 @@ export function ChatShell({ setEditingMessageId(null); setEditingDraft(""); setEditError(null); + // Le textarea inline d'édition se démonte → rendre le focus au composer. + composerRef.current?.focus(); } async function saveEditing(messageId: string) { @@ -1272,6 +1287,8 @@ export function ChatShell({ }); setEditingMessageId(null); setEditingDraft(""); + // Le textarea inline d'édition se démonte → rendre le focus au composer. + composerRef.current?.focus(); regenerate({ body: { providerKeyId, @@ -1326,6 +1343,8 @@ export function ChatShell({ function handleSubmit() { const trimmed = input.trim(); if (!trimmed || isBusy) return; + // Reset l'annonce live pour qu'elle se re-déclenche à la fin du tour. + setStatusText(""); // Mémorise les attachements pour les associer au message user qui // va apparaître dans `messages` au prochain tick (cf useEffect plus // bas qui consume cette ref). @@ -1349,6 +1368,10 @@ export function ChatShell({ // Le doc reste accessible via le picker / la sidebar /documents s'il // est nécessaire pour un prochain message. setAttachedDocIds([]); + // Rend le focus au composer après envoi (le textarea reste monté ; + // simplement disabled pendant le stream → focus revient au prochain + // tour de saisie). + composerRef.current?.focus(); } // Associe les attachements pending au dernier message user dès qu'il @@ -1585,11 +1608,16 @@ export function ChatShell({
{/* Messages or empty state */} + {/* Région live dédiée : annonce uniquement une string de complétion + courte (cf. statusText) — n'enveloppe PAS le thread streamé pour + éviter de ré-annoncer chaque token. */} +
+ {statusText} +
@@ -1615,6 +1643,7 @@ export function ChatShell({ return (
{/* Wrapper d'étapes : utile UNIQUEMENT pour pipelines @@ -1770,9 +1799,7 @@ export function ChatShell({ text={part.text} isLive={isLiveMessage} mergedDocuments={mergedDocuments} - onOpenDoc={(documentId, targetText) => - setOpenDoc({ documentId, targetText }) - } + onOpenDoc={handleOpenDoc} /> ) : ( @@ -1797,9 +1824,7 @@ export function ChatShell({ input={p.input} output={p.output} state={p.state} - onOpenDoc={(documentId, targetText) => - setOpenDoc({ documentId, targetText }) - } + onOpenDoc={handleOpenDoc} /> ); } @@ -1982,6 +2007,7 @@ export function ChatShell({ } }} placeholder="Posez votre question…" + aria-label="Votre message à Louis" rows={1} disabled={isBusy} className="w-full resize-none rounded-t-2xl bg-transparent px-4 pt-3 pb-1 text-[15px] leading-[1.55] placeholder:text-muted-foreground focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 max-h-[240px] overflow-y-auto" diff --git a/src/app/(app)/chat/composer-menu.tsx b/src/app/(app)/chat/composer-menu.tsx index 4c22a2c..21b0410 100644 --- a/src/app/(app)/chat/composer-menu.tsx +++ b/src/app/(app)/chat/composer-menu.tsx @@ -7,7 +7,6 @@ import { IconSparkles, IconBriefcase, IconSettings, - IconWorld, IconFileText, IconKey, IconCpu, @@ -190,16 +189,6 @@ export function ComposerMenu({ Tous les réglages - - {/* Slot futur : web search toggle, style picker… */} - - - - Recherche web - - bientôt - - ); diff --git a/src/app/(app)/chat/docx-view.tsx b/src/app/(app)/chat/docx-view.tsx index ad3a826..89e7374 100644 --- a/src/app/(app)/chat/docx-view.tsx +++ b/src/app/(app)/chat/docx-view.tsx @@ -135,7 +135,7 @@ export function DocxView({ } .docx-view-container .docx-wrapper > section.docx { margin: 1rem auto !important; - box-shadow: 0 0 0 1px oklch(0.92 0.008 265); + box-shadow: 0 0 0 1px var(--border); } `} {loading && ( @@ -180,8 +180,8 @@ function highlightAndScroll( range.setEnd(node, start + needle.trim().length); const mark = document.createElement("mark"); mark.className = "louis-quote-mark"; - mark.style.backgroundColor = "oklch(0.94 0.12 90)"; - mark.style.color = "inherit"; + mark.style.backgroundColor = "var(--highlight)"; + mark.style.color = "var(--highlight-foreground)"; range.surroundContents(mark); const rect = mark.getBoundingClientRect(); const scrollRect = scrollEl.getBoundingClientRect(); diff --git a/src/app/(app)/chat/edit-card.tsx b/src/app/(app)/chat/edit-card.tsx index fd64adb..e6c28ff 100644 --- a/src/app/(app)/chat/edit-card.tsx +++ b/src/app/(app)/chat/edit-card.tsx @@ -61,8 +61,9 @@ export function EditCard({ raw }: { raw: string }) { Suggestion d'édition
{decision === "kept" && ( - - ✓ Conservée + + + Conservée )}
diff --git a/src/app/(app)/chat/live-workflow-panel.tsx b/src/app/(app)/chat/live-workflow-panel.tsx index 4714b26..449ed73 100644 --- a/src/app/(app)/chat/live-workflow-panel.tsx +++ b/src/app/(app)/chat/live-workflow-panel.tsx @@ -1,6 +1,6 @@ "use client"; -import { AnimatePresence, motion } from "motion/react"; +import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { IconBriefcase, IconLoader2, IconCheck, IconAlertTriangle, IconX } from "@tabler/icons-react"; import { cn } from "@/lib/utils"; import { roleMeta } from "../board/agent-role-meta"; @@ -47,6 +47,7 @@ export function LiveWorkflowPanel({ onClose, onOpenTheatre, }: LiveWorkflowPanelProps) { + const reduceMotion = useReducedMotion(); return ( {open && ( @@ -54,7 +55,11 @@ export function LiveWorkflowPanel({ initial={{ opacity: 0, y: 20, scale: 0.96 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, y: 12, scale: 0.98 }} - transition={{ type: "spring", damping: 24, stiffness: 280 }} + transition={ + reduceMotion + ? { duration: 0.2, ease: [0.23, 1, 0.32, 1] } + : { type: "spring", damping: 24, stiffness: 280 } + } role="status" aria-live="polite" aria-atomic="false" diff --git a/src/app/(app)/chat/pdf-view.tsx b/src/app/(app)/chat/pdf-view.tsx index 7eaf6ec..f8203fc 100644 --- a/src/app/(app)/chat/pdf-view.tsx +++ b/src/app/(app)/chat/pdf-view.tsx @@ -115,14 +115,11 @@ export function PdfView({ fileUrl, targetText }: Props) {
); diff --git a/src/app/(app)/chat/thinking-indicator.tsx b/src/app/(app)/chat/thinking-indicator.tsx index 621b2a2..d3fb6ae 100644 --- a/src/app/(app)/chat/thinking-indicator.tsx +++ b/src/app/(app)/chat/thinking-indicator.tsx @@ -33,9 +33,13 @@ export function ThinkingIndicator() { }, []); return ( -
+
- + -
+
↑↓ pour naviguer · ↵ pour ouvrir · ESC pour fermer ⌘K
diff --git a/src/app/(app)/dashboard/page.tsx b/src/app/(app)/dashboard/page.tsx index b9fac15..1215459 100644 --- a/src/app/(app)/dashboard/page.tsx +++ b/src/app/(app)/dashboard/page.tsx @@ -264,17 +264,17 @@ function Stat({ href?: string; }) { const inner = ( - <> -

+

+
{label} -

-

+

+
{value} -

+
{hint && ( -

{hint}

+
{hint}
)} - +
); if (href) { return ( diff --git a/src/app/(app)/documents/document-row.tsx b/src/app/(app)/documents/document-row.tsx index 534e081..ac7c41e 100644 --- a/src/app/(app)/documents/document-row.tsx +++ b/src/app/(app)/documents/document-row.tsx @@ -280,6 +280,7 @@ export function DocumentRow({ type="file" accept=".pdf,.docx,.txt,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,text/plain" className="hidden" + aria-label="Téléverser une nouvelle version" onChange={onReplaceChange} /> {replaceError && ( diff --git a/src/app/(app)/documents/documents-dropzone.tsx b/src/app/(app)/documents/documents-dropzone.tsx index 4f7410a..615515a 100644 --- a/src/app/(app)/documents/documents-dropzone.tsx +++ b/src/app/(app)/documents/documents-dropzone.tsx @@ -50,7 +50,11 @@ export function DocumentsDropzone({ overlayHint="PDF, DOCX ou texte — 25 Mo max par fichier" > {(uploadingCount > 0 || error) && ( -
+
{uploadingCount > 0 && ( diff --git a/src/app/(app)/documents/folder-row.tsx b/src/app/(app)/documents/folder-row.tsx index b9fbedf..ca9d6b1 100644 --- a/src/app/(app)/documents/folder-row.tsx +++ b/src/app/(app)/documents/folder-row.tsx @@ -62,6 +62,7 @@ export function FolderRow({ folder }: { folder: DocumentFolder }) {
setDraft(e.target.value)} onKeyDown={(e) => { @@ -78,6 +79,7 @@ export function FolderRow({ folder }: { folder: DocumentFolder }) { onClick={commitRename} className="size-8 inline-flex items-center justify-center rounded-md text-primary hover:bg-accent" title="Valider" + aria-label="Valider" > @@ -89,6 +91,7 @@ export function FolderRow({ folder }: { folder: DocumentFolder }) { }} className="size-8 inline-flex items-center justify-center rounded-md text-muted-foreground hover:bg-accent" title="Annuler" + aria-label="Annuler" > diff --git a/src/app/(app)/documents/page.tsx b/src/app/(app)/documents/page.tsx index 4d57b27..d16d848 100644 --- a/src/app/(app)/documents/page.tsx +++ b/src/app/(app)/documents/page.tsx @@ -119,8 +119,11 @@ export default async function DocumentsPage({
-
-

Documents

+

+ Fichiers · dossiers · versions +

+
+

Documents

Importez vos PDF / DOCX (≤ 25 Mo), organisez-les en dossiers et versionnez-les. Interrogez-les ensuite depuis le chat (pièce @@ -184,20 +187,26 @@ export default async function DocumentsPage({ {isEmpty ? ( ) : ( -
+
    {subFolders.map((f) => ( - +
  • + +
  • ))} {familyViews.map((fv) => ( - +
  • + +
  • ))} -
+ )} diff --git a/src/app/(app)/mobile-nav.tsx b/src/app/(app)/mobile-nav.tsx index 7d97a74..76df9f0 100644 --- a/src/app/(app)/mobile-nav.tsx +++ b/src/app/(app)/mobile-nav.tsx @@ -43,6 +43,7 @@ export function MobileNav({ user, conversations, projects }: Props) { diff --git a/src/app/(app)/settings/connectors/connector-card.tsx b/src/app/(app)/settings/connectors/connector-card.tsx index 5271687..0e9ee2e 100644 --- a/src/app/(app)/settings/connectors/connector-card.tsx +++ b/src/app/(app)/settings/connectors/connector-card.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useTransition, type MouseEvent } from "react"; +import { useState, useTransition } from "react"; import { IconCheck, IconCircleDashed, @@ -60,10 +60,6 @@ type Props = { keys: ConnectorKey[]; }; -function stopCardClick(e: MouseEvent) { - e.stopPropagation(); -} - export function ConnectorCard({ type, keys }: Props) { const meta = CONNECTOR_CATALOG[type]; const Icon = meta.icon; @@ -100,16 +96,6 @@ export function ConnectorCard({ type, keys }: Props) { <> { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - openDialog(); - } - }} > {primary.isActive ? "Activé" : "Inactif"} @@ -197,7 +182,6 @@ export function ConnectorCard({ type, keys }: Props) { href={meta.docsUrl} target="_blank" rel="noopener noreferrer" - onClick={stopCardClick} className="text-muted-foreground hover:text-foreground transition-colors" aria-label="Documentation" > @@ -210,14 +194,10 @@ export function ConnectorCard({ type, keys }: Props) { className="-mt-1 -mr-1 size-7 inline-flex shrink-0 items-center justify-center rounded-md border border-border hover:bg-accent transition-colors disabled:opacity-50" aria-label="Actions" disabled={pending} - onClick={stopCardClick} > - + - {isConfigured && ( - - « {primary.label} » sera supprimé. La clé chiffrée est - retirée — l'intégration ne fonctionnera plus tant - qu'une nouvelle clé n'est pas saisie. - - } - pending={pending} - onConfirm={() => { - startTransition(async () => { - await deleteConnectorKey(primary.id); - setDeleteOpen(false); - }); - }} - /> - )} -

{meta.description}

@@ -276,6 +234,18 @@ export function ConnectorCard({ type, keys }: Props) {

)} + +
+ +
@@ -286,6 +256,28 @@ export function ConnectorCard({ type, keys }: Props) {
+ {isConfigured && ( + + « {primary.label} » sera supprimé. La clé chiffrée est + retirée — l'intégration ne fonctionnera plus tant + qu'une nouvelle clé n'est pas saisie. + + } + pending={pending} + onConfirm={() => { + startTransition(async () => { + await deleteConnectorKey(primary.id); + setDeleteOpen(false); + }); + }} + /> + )} + diff --git a/src/app/(app)/settings/connectors/page.tsx b/src/app/(app)/settings/connectors/page.tsx index ff34415..1536d19 100644 --- a/src/app/(app)/settings/connectors/page.tsx +++ b/src/app/(app)/settings/connectors/page.tsx @@ -1,6 +1,6 @@ import { redirect } from "next/navigation"; import { desc, eq } from "drizzle-orm"; -import { IconShieldLock, IconInfoCircle, IconClock } from "@tabler/icons-react"; +import { IconShieldLock, IconClock } from "@tabler/icons-react"; import { auth } from "@/auth"; import { db } from "@/db"; import { connectorKeys, type ConnectorKey } from "@/db/schema"; @@ -116,16 +116,6 @@ export default async function ConnectorsPage() { ))}
- -
); } diff --git a/src/app/(app)/settings/general/theme-picker.tsx b/src/app/(app)/settings/general/theme-picker.tsx index 25a628c..0180f44 100644 --- a/src/app/(app)/settings/general/theme-picker.tsx +++ b/src/app/(app)/settings/general/theme-picker.tsx @@ -2,7 +2,7 @@ import { useSyncExternalStore } from "react"; import { useTheme } from "next-themes"; -import { IconSun, IconMoon, IconDeviceLaptop } from "@tabler/icons-react"; +import { IconSun, IconMoon, IconDeviceLaptop, IconCheck } from "@tabler/icons-react"; function useMounted(): boolean { return useSyncExternalStore( @@ -47,6 +47,7 @@ export function ThemePicker() { > {o.label} + {active && } ); })} diff --git a/src/app/(app)/settings/mcp/add-mcp-dialog.tsx b/src/app/(app)/settings/mcp/add-mcp-dialog.tsx index 5b54ca5..8f73ac0 100644 --- a/src/app/(app)/settings/mcp/add-mcp-dialog.tsx +++ b/src/app/(app)/settings/mcp/add-mcp-dialog.tsx @@ -112,8 +112,9 @@ export function AddMcpDialog() { name="headers" placeholder='{"Authorization": "Bearer …"}' autoComplete="off" + aria-describedby="headers-help" /> -

+

Format JSON. Laisser vide si aucune authentification.

diff --git a/src/app/(app)/settings/profile/password-form.tsx b/src/app/(app)/settings/profile/password-form.tsx index 80e6e96..450a6b8 100644 --- a/src/app/(app)/settings/profile/password-form.tsx +++ b/src/app/(app)/settings/profile/password-form.tsx @@ -48,8 +48,9 @@ export function PasswordForm() { autoComplete="new-password" minLength={10} required + aria-describedby="newPassword-help" /> -

+

Minimum 10 caractères. Un gestionnaire de mots de passe est recommandé.

diff --git a/src/app/(app)/settings/providers/page.tsx b/src/app/(app)/settings/providers/page.tsx index eda305f..9623802 100644 --- a/src/app/(app)/settings/providers/page.tsx +++ b/src/app/(app)/settings/providers/page.tsx @@ -1,6 +1,6 @@ import { redirect } from "next/navigation"; import { desc, eq } from "drizzle-orm"; -import { IconShieldLock, IconInfoCircle } from "@tabler/icons-react"; +import { IconShieldLock } from "@tabler/icons-react"; import { auth } from "@/auth"; import { db } from "@/db"; import { providerKeys, type ProviderKey } from "@/db/schema"; @@ -104,17 +104,6 @@ export default async function ProvidersPage() {
))} - -
); } diff --git a/src/app/(app)/settings/providers/provider-card.tsx b/src/app/(app)/settings/providers/provider-card.tsx index d6e4902..2b7a54b 100644 --- a/src/app/(app)/settings/providers/provider-card.tsx +++ b/src/app/(app)/settings/providers/provider-card.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useTransition, type MouseEvent } from "react"; +import { useState, useTransition } from "react"; import { IconCheck, IconAlertTriangle, @@ -66,11 +66,6 @@ type Props = { keys: ProviderKey[]; }; -/** Stop a click from bubbling up to the CutoutCard root (which opens the dialog). */ -function stopCardClick(e: MouseEvent) { - e.stopPropagation(); -} - export function ProviderCard({ type, keys }: Props) { const meta = PROVIDER_CATALOG[type]; const primary = keys.find((k) => k.isDefault) ?? keys[0] ?? null; @@ -107,16 +102,6 @@ export function ProviderCard({ type, keys }: Props) { <> { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - openDialog(); - } - }} > {primary.isActive ? "Activé" : "Inactif"} @@ -206,7 +190,6 @@ export function ProviderCard({ type, keys }: Props) { href={meta.docsUrl} target="_blank" rel="noopener noreferrer" - onClick={stopCardClick} className="text-muted-foreground hover:text-foreground transition-colors" aria-label="Documentation" > @@ -219,14 +202,10 @@ export function ProviderCard({ type, keys }: Props) { className="-mt-1 -mr-1 size-7 inline-flex shrink-0 items-center justify-center rounded-md border border-border hover:bg-accent transition-colors disabled:opacity-50" aria-label="Actions" disabled={pending} - onClick={stopCardClick} > - + { @@ -271,6 +250,18 @@ export function ProviderCard({ type, keys }: Props) {

)} + +
+ +
{/* Reveal-on-hover CTA */} diff --git a/src/app/(app)/settings/settings-nav.tsx b/src/app/(app)/settings/settings-nav.tsx index 8ef8d98..8566b02 100644 --- a/src/app/(app)/settings/settings-nav.tsx +++ b/src/app/(app)/settings/settings-nav.tsx @@ -67,6 +67,7 @@ export function SettingsNav({ {all.map((group) => (
-

+

{group.group}

    @@ -97,6 +98,7 @@ export function SettingsNav({
  • void }) { value={triggerHint} onChange={(e) => setTriggerHint(e.target.value)} placeholder="ex. Quand l'utilisateur cite une jurisprudence ou un article de loi" + aria-describedby="skill-hint-help" /> -

    +

    Phrase descriptive lue par un petit modèle qui décide d'activer ou non la compétence selon la demande.

    @@ -186,8 +187,9 @@ function SkillForm({ mode, onClose }: { mode: Mode; onClose: () => void }) { onChange={(e) => setSystemPrompt(e.target.value)} placeholder="Le texte qui sera injecté dans le prompt système quand la compétence est activée…" className="w-full resize-y rounded-md border border-input bg-card px-3 py-2 text-sm font-mono leading-relaxed shadow-xs placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50" + aria-describedby="skill-prompt-help" /> -

    +

    Soyez explicite, donnez des exemples si nécessaire. Cette instruction prend le pas sur le comportement par défaut.

    diff --git a/src/app/(app)/settings/usage/page.tsx b/src/app/(app)/settings/usage/page.tsx index b1d57c7..9c5ce35 100644 --- a/src/app/(app)/settings/usage/page.tsx +++ b/src/app/(app)/settings/usage/page.tsx @@ -112,14 +112,14 @@ export default async function UsagePage() { {/* Coût du mois — typographie large, asymétrique, pas une carte */}
    -
    -

    +

    +
    Coût estimé ce mois -

    -

    +

    +
    {formatTotals(totalsMonth)} -

    -
    + +
    -
    +
    {label}
    diff --git a/src/app/(app)/sidebar-content.tsx b/src/app/(app)/sidebar-content.tsx index ddb4e66..c8c1d9a 100644 --- a/src/app/(app)/sidebar-content.tsx +++ b/src/app/(app)/sidebar-content.tsx @@ -147,7 +147,7 @@ export function SidebarContent({ {/* Nav + conversations */}
    -