feat(marketing): refonte stratégique 'Comment ça marche' — 4 catégories × 12 sous-modes

Audit complet des fonctionnalités réelles DictIA (manuel utilisateur + composants
production) puis restructuration de l'animation phone container en 4 catégories
logiques regroupant 12 sous-modes (6 historiques + 6 nouveaux).

CATÉGORIES (bottom tab bar 4 boutons + sub-mode dots indicator) :
- Capture (b2 cyan)         : Transcription, Recording live, Recherche IA
- Transformation IA (b1)    : Diarisation, 99+ langues, Résumé+actions, Chat IA
- Distribution (b3 fuchsia) : Exports, Intégrations Hub, Partage, Users
- Gouvernance (b1 blue)     : Audit trail, Conformité Loi 25 + 9 ordres pros

NOUVEAUX MODES IMPLÉMENTÉS :
- Mode 7 Recording live : minuteur 99s + waveform 24 bars random + 3 boutons
  sources (mic/système/combiné) selon manuel utilisation v1.0
- Mode 8 Recherche sémantique : query typed + 3 résultats highlight RAG
- Mode 9 Résumé + actions : décisions/actions stagger + extraction ICS
- Mode 10 Intégrations : hub central DictIA + 8 logos en orbite (Word, Outlook,
  Teams, Notion, Obsidian, Zapier, Make, n8n) + lignes connexion SVG
- Mode 11 Audit trail : 6 events horodatés (INFO/AUTH/PROC/READ/EXP/SHARE) +
  badge consentement tracé immutable (Loi 25 art. 8)
- Mode 12 Conformité Loi 25 : 6 badges (Loi 25/96/EFVP CAI/MCN/AGPL/0 Cloud
  Act US) + 9 ordres pros (Barreau, CNQ, CPA, ChAD, OACIQ, CMQ, OIIQ, OPQ, OEQ)

REFACTOR Alpine dictiaDashboard() :
- FEATURES étendu de 7 à 13 entrées (idx 0-12)
- CATEGORIES array avec submodes[], iconPath, color, subtitle
- activeCategory + handleCategorySelect(ci) en plus de handleManualSelect(i)
- Auto-cycle 1100ms entre sous-modes ; switch catégorie quand fin atteinte
- Right grid 3×6 → 2×2 categories cards (preview sub-modes dots)
- Bottom tab bar 6 modes → 4 catégories (icons larger 18px) + sub-mode dots
- Mobile pills par catégorie (au lieu de par mode)

Préservé : palette brand-b1/b2/b3 stricte, phone shell statique 280×580,
WCAG AA, prefers-reduced-motion, eyebrow text-brand-navy, IA Mistral card,
section integrations, architecture, conformité-resume.

Tests : 9/9 passent dans test_fonctionnalites_*. Assertions ajoutées pour les
4 catégories + 6 nouveaux modes + handleCategorySelect + 1100ms cycle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Allison
2026-04-29 12:57:44 -04:00
parent 323f0c81c4
commit 8a7650f9fa
2 changed files with 594 additions and 62 deletions

View File

@@ -249,11 +249,14 @@
</div>
</section>
{# ===== COMMENT ÇA MARCHE — DASHBOARD HOLOGRAPHIQUE =====
Reproduction fidèle de DashboardHolographique (Website-Sanity/dictai-narrative.tsx).
Phone container central + 6 modes uniques (Transcription, Diarisation, Langues,
Exports, Users, Share) + IA Mistral 7B premium card + grid 6 features.
Auto-cycle 900ms (1→6→1, skip IA index 0). Click manuel → 4500ms isManual.
{# ===== COMMENT ÇA MARCHE — DASHBOARD HOLOGRAPHIQUE (4 CATÉGORIES × 12 SOUS-MODES) =====
Reproduction fidèle de DashboardHolographique (Website-Sanity/dictai-narrative.tsx),
refonte stratégique 2026-04-29 : 4 catégories (Capture · IA · Distribution · Gouvernance)
regroupant 12 sous-modes (6 historiques + 6 nouveaux : Recording, Recherche, Résumé,
Intégrations, Audit, Loi 25, AGPL).
Phone container central + 12 sous-modes + IA Mistral 7B premium card + grid 4 catégories.
Auto-cycle : sous-mode toutes les 1100ms ; passage à catégorie suivante quand fin atteinte.
Click manuel → 4500ms isManual. Catégorie 4 (Gouvernance) inclut audit trail / Loi 25 / AGPL.
#}
<section class="bg-brand-bg py-20 relative overflow-hidden" aria-labelledby="how-it-works-title">
<style>
@@ -461,8 +464,8 @@
<div class="max-w-[1200px] mx-auto px-6 relative">
<div class="text-center max-w-2xl mx-auto mb-10">
{# Eyebrow : grad-text uppercase tracking-widest text-xs avec dot pulse brand-b3 #}
<p class="eyebrow grad-text mb-4 inline-flex items-center gap-2 justify-center px-3 py-1 rounded-full font-mono uppercase tracking-widest text-xs font-bold"
{# Eyebrow : typo NOIRE sur fond clair, dot pulse brand-b3 pour accent #}
<p class="eyebrow text-brand-navy mb-4 inline-flex items-center gap-2 justify-center px-3 py-1 rounded-full font-mono uppercase tracking-widest text-xs font-bold"
style="background:linear-gradient(135deg, rgba(6,182,212,0.10), rgba(192,38,211,0.10)); border:1px solid rgba(192,38,211,0.20);">
<span class="dictia-dot-pulse inline-block w-1.5 h-1.5 rounded-full bg-brand-b3" aria-hidden="true"></span>
COMMENT ÇA MARCHE
@@ -477,8 +480,8 @@
{# Stats row #}
<div class="flex flex-wrap items-center justify-center gap-x-8 gap-y-3 text-brand-navy">
<div class="flex items-center gap-2">
<span class="text-2xl font-black grad-text">6</span>
<span class="text-xs font-medium text-brand-navy/60 uppercase tracking-wider">modules</span>
<span class="text-2xl font-black grad-text">4</span>
<span class="text-xs font-medium text-brand-navy/60 uppercase tracking-wider">catégories · 6 modules</span>
</div>
<div class="w-px h-6 bg-brand-navy/15 hidden sm:block" aria-hidden="true"></div>
<div class="flex items-center gap-2">
@@ -952,27 +955,279 @@
</div>
</div>
</template>
{# Mode 7 : Recording live (waveform live + minuteur + 3 boutons mic/syst/combiné) — Catégorie Capture #}
<template x-if="displayMode === 7">
<div class="dictia-mode-fade w-full h-full flex flex-col overflow-hidden" x-data="recModeData()" x-init="init()">
<div class="flex items-center justify-between px-3 py-2 flex-shrink-0"
style="background:rgba(6,182,212,0.06);border-bottom:1px solid rgba(6,182,212,0.12);">
<span class="font-mono text-[10px] font-bold tracking-widest" style="color:rgba(6,182,212,0.90);">ENREGISTREMENT LIVE</span>
<div class="flex items-center gap-1">
<span class="dictia-rec-dot block rounded-full" style="width:5px;height:5px;background:#ef4444;"></span>
<span class="font-mono text-[9px] font-bold tracking-widest" style="color:rgba(239,68,68,0.90);">REC</span>
</div>
</div>
{# Minuteur central + waveform live #}
<div class="flex flex-col items-center justify-center gap-2 flex-1 px-3">
<p class="font-mono font-black text-2xl text-white tabular-nums leading-none" x-text="time"></p>
<p class="font-mono text-[9px] uppercase tracking-widest" style="color:rgba(6,182,212,0.65);">Microphone + Système</p>
{# Waveform 24 barres dynamiques (pattern live aléatoire CSS-only) #}
<div class="flex items-center justify-center gap-0.5 mt-1" style="height:36px;" aria-hidden="true">
<template x-for="(h, bi) in BARS" :key="bi">
<div class="dictia-wave-bar bg-brand-b2"
:style="`width:2px;height:${h}px;animation-delay:${bi * 30}ms;`"></div>
</template>
</div>
{# Estimation taille fichier #}
<p class="font-mono text-[9px] mt-2" style="color:rgba(6,182,212,0.55);">~<span x-text="size"></span> Mo · auto-save 5 s</p>
</div>
{# 3 boutons sources (rouge mic / bleu système / violet combiné) — selon manuel #}
<div class="flex items-center justify-center gap-2 px-3 py-2 flex-shrink-0"
style="background:rgba(8,12,24,0.5);border-top:1px solid rgba(255,255,255,0.06);">
<span class="rounded-full flex items-center justify-center" style="width:18px;height:18px;background:rgba(239,68,68,0.30);border:1px solid rgba(239,68,68,0.55);">
<svg viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:9px;height:9px;" aria-hidden="true">
<rect x="9" y="2" width="6" height="12" rx="3"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
</svg>
</span>
<span class="rounded-full flex items-center justify-center" style="width:18px;height:18px;background:rgba(37,99,235,0.30);border:1px solid rgba(37,99,235,0.55);">
<svg viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:9px;height:9px;" aria-hidden="true">
<rect x="3" y="4" width="18" height="14" rx="2"/><line x1="9" y1="22" x2="15" y2="22"/>
</svg>
</span>
<span class="rounded-full flex items-center justify-center dictia-spring" style="width:22px;height:22px;background:rgba(192,38,211,0.30);border:1.5px solid rgba(192,38,211,0.65);">
<svg viewBox="0 0 24 24" fill="none" stroke="#c026d3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:11px;height:11px;" aria-hidden="true">
<path d="M12 2v8M5 7l7 7 7-7"/>
</svg>
</span>
</div>
</div>
</template>
{# Mode 8 : Recherche sémantique (search bar + résultats highlight) — Catégorie Capture #}
<template x-if="displayMode === 8">
<div class="dictia-mode-fade w-full h-full flex flex-col overflow-hidden" x-data="searchModeData()" x-init="init()">
<div class="flex items-center justify-center gap-2 py-2 flex-shrink-0"
style="background:rgba(6,182,212,0.06);border-bottom:1px solid rgba(6,182,212,0.12);">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:11px;height:11px;color:rgba(6,182,212,0.90);" aria-hidden="true">
<circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<span class="font-mono text-[10px] font-bold tracking-widest" style="color:rgba(6,182,212,0.90);">RECHERCHE IA SÉMANTIQUE</span>
</div>
{# Search bar simulée (typing query) #}
<div class="px-3 py-2 flex-shrink-0">
<div class="rounded flex items-center gap-2 px-2 py-1.5"
style="background:rgba(255,255,255,0.06);border:1px solid rgba(6,182,212,0.30);">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:9px;height:9px;color:rgba(6,182,212,0.70);" aria-hidden="true">
<circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<p class="font-mono text-[10px] flex-1 truncate" style="color:rgba(255,255,255,0.85);">
<span x-text="query"></span><span class="dictia-blink ml-px inline-block align-middle bg-brand-b2" style="width:2px;height:9px;">&nbsp;</span>
</p>
</div>
</div>
{# Résultats : 3 transcriptions matchées avec phrase highlight #}
<div class="flex-1 dictia-mode-scroll dictia-fade-bottom flex flex-col gap-1.5 px-3 pb-2">
<template x-for="(r, ri) in results" :key="ri">
<div class="rounded dictia-fade-y"
:style="`background-color:${r.c}10;border:1px solid ${r.c}30;padding:6px 8px;animation-delay:${ri * 280 + 600}ms;`">
<div class="flex items-center gap-1.5 mb-1">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" :style="`color:${r.c};width:9px;height:9px;`" aria-hidden="true">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>
</svg>
<span class="font-mono text-[9px] font-bold" :style="`color:${r.c};`" x-text="r.title"></span>
<span class="font-mono text-[9px] ml-auto" style="color:rgba(255,255,255,0.40);" x-text="r.score + ' %'"></span>
</div>
<p class="font-mono text-[9px] leading-snug" style="color:rgba(255,255,255,0.75);">
<span :style="`background:${r.c}28;color:${r.c};padding:0 2px;border-radius:2px;`" x-text="r.match"></span>
</p>
</div>
</template>
</div>
</div>
</template>
{# Mode 9 : Résumé + extraction d'événements (panel structuré décisions/actions/ICS) — Catégorie IA #}
<template x-if="displayMode === 9">
<div class="dictia-mode-fade w-full h-full flex flex-col overflow-hidden" x-data="summaryModeData()" x-init="init()">
<div class="flex items-center justify-between px-3 py-2 flex-shrink-0"
style="background:rgba(37,99,235,0.06);border-bottom:1px solid rgba(37,99,235,0.12);">
<span class="font-mono text-[10px] font-bold tracking-widest" style="color:rgba(37,99,235,0.90);">RÉSUMÉ EXÉCUTIF</span>
<span class="font-mono text-[9px] text-brand-b1/60">Mistral Nemo 12B</span>
</div>
<div class="flex-1 dictia-mode-scroll dictia-fade-bottom flex flex-col gap-2 px-3 py-2">
{# Décisions #}
<div class="dictia-fade-y" style="animation-delay:200ms;">
<p class="font-mono text-[9px] font-bold uppercase tracking-wider mb-1" style="color:rgba(37,99,235,0.85);">Décisions</p>
<template x-for="(d, di) in decisions.slice(0, dShown)" :key="di">
<div class="flex items-start gap-1.5 mb-0.5 dictia-fade-x">
<span class="text-brand-b1 leading-none mt-0.5" aria-hidden="true"></span>
<p class="text-[10px] font-mono leading-snug" style="color:rgba(255,255,255,0.85);" x-text="d"></p>
</div>
</template>
</div>
{# Actions à suivre #}
<div class="dictia-fade-y" style="animation-delay:900ms;" x-show="dShown >= decisions.length">
<p class="font-mono text-[9px] font-bold uppercase tracking-wider mb-1" style="color:rgba(192,38,211,0.85);">Actions à suivre</p>
<template x-for="(a, ai) in actions.slice(0, aShown)" :key="ai">
<div class="flex items-start gap-1.5 mb-0.5 dictia-fade-x">
<span class="rounded flex items-center justify-center mt-0.5 flex-shrink-0" style="width:9px;height:9px;background:rgba(192,38,211,0.30);border:1px solid rgba(192,38,211,0.55);" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="#c026d3" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" style="width:5px;height:5px;"><polyline points="20 6 9 17 4 12"/></svg>
</span>
<p class="text-[10px] font-mono leading-snug" style="color:rgba(255,255,255,0.85);" x-text="a"></p>
</div>
</template>
</div>
{# Événement ICS détecté (extraction calendrier) #}
<div class="dictia-fade-y rounded mt-1 flex-shrink-0" x-show="aShown >= actions.length"
style="background:rgba(6,182,212,0.10);border:1px solid rgba(6,182,212,0.30);padding:5px 8px;animation-delay:1800ms;">
<div class="flex items-center gap-1.5 mb-0.5">
<svg viewBox="0 0 24 24" fill="none" stroke="#06b6d4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:9px;height:9px;" aria-hidden="true">
<rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>
</svg>
<span class="font-mono text-[9px] font-bold tracking-widest" style="color:rgba(6,182,212,0.95);">ÉVÉNEMENT DÉTECTÉ → ICS</span>
</div>
<p class="font-mono text-[9px]" style="color:rgba(255,255,255,0.70);">Comité de suivi · 21 janv. 14h00</p>
</div>
</div>
</div>
</template>
{# Mode 10 : Intégrations animées (logos hub Word, Outlook, Teams, Notion, Obsidian, Zapier...) — Catégorie Distribution #}
<template x-if="displayMode === 10">
<div class="dictia-mode-fade w-full h-full flex flex-col overflow-hidden" x-data="integModeData()" x-init="init()">
<div class="flex items-center justify-center gap-2 py-2 flex-shrink-0"
style="background:rgba(192,38,211,0.06);border-bottom:1px solid rgba(192,38,211,0.12);">
<span class="dictia-dot-pulse block rounded-full" style="width:5px;height:5px;background:#c026d3;"></span>
<span class="font-mono text-[10px] font-bold tracking-widest" style="color:rgba(192,38,211,0.90);">INTÉGRATIONS HUB</span>
</div>
{# Hub central + 8 logos en orbite (grille pour mobile, ring pour desktop) #}
<div class="flex-1 relative flex items-center justify-center px-3 py-2 overflow-hidden">
{# Cercle central DictIA #}
<div class="rounded-full flex items-center justify-center relative z-10"
style="width:42px;height:42px;background:linear-gradient(135deg,rgba(6,182,212,0.25),rgba(192,38,211,0.25));border:1.5px solid rgba(192,38,211,0.50);box-shadow:0 0 20px rgba(192,38,211,0.30);">
<span class="font-black text-[10px] grad-text">Dict</span>
</div>
{# 8 logos animés en cercle autour du hub (positions absolutes) #}
<template x-for="(l, li) in LOGOS" :key="l.name">
<div class="absolute dictia-spring rounded flex items-center justify-center"
:style="`width:28px;height:28px;left:${l.x}%;top:${l.y}%;transform:translate(-50%,-50%);background:${l.c}18;border:1px solid ${l.c}45;animation-delay:${li * 90 + 200}ms;`">
<span class="font-mono font-black text-[9px]" :style="`color:${l.c};`" x-text="l.short"></span>
</div>
</template>
{# Lignes de connexion SVG hub→logos #}
<svg class="absolute inset-0 pointer-events-none" width="100%" height="100%" aria-hidden="true" style="opacity:0.20;">
<line x1="50%" y1="50%" x2="20%" y2="20%" stroke="#c026d3" stroke-width="0.5" stroke-dasharray="2 2"/>
<line x1="50%" y1="50%" x2="80%" y2="20%" stroke="#06b6d4" stroke-width="0.5" stroke-dasharray="2 2"/>
<line x1="50%" y1="50%" x2="20%" y2="80%" stroke="#2563eb" stroke-width="0.5" stroke-dasharray="2 2"/>
<line x1="50%" y1="50%" x2="80%" y2="80%" stroke="#c026d3" stroke-width="0.5" stroke-dasharray="2 2"/>
</svg>
</div>
<div class="flex items-center justify-center gap-3 px-3 py-2 flex-shrink-0"
style="background:rgba(192,38,211,0.04);border-top:1px solid rgba(192,38,211,0.10);">
<span class="font-mono text-[9px] tracking-wide" style="color:rgba(192,38,211,0.75);">REST · Webhook · OAuth2</span>
</div>
</div>
</template>
{# Mode 11 : Audit trail (event log horodaté qui a fait quoi) — Catégorie Gouvernance #}
<template x-if="displayMode === 11">
<div class="dictia-mode-fade w-full h-full flex flex-col overflow-hidden" x-data="auditModeData()" x-init="init()">
<div class="flex items-center justify-between px-3 py-2 flex-shrink-0"
style="background:rgba(37,99,235,0.06);border-bottom:1px solid rgba(37,99,235,0.12);">
<span class="font-mono text-[10px] font-bold tracking-widest" style="color:rgba(37,99,235,0.90);">AUDIT TRAIL</span>
<span class="font-mono text-[9px]" style="color:rgba(37,99,235,0.55);">Loi 25 art. 8</span>
</div>
<div class="flex-1 dictia-mode-scroll dictia-fade-bottom flex flex-col gap-1 px-2 py-2">
<template x-for="(e, ei) in events.slice(0, shown)" :key="ei">
<div class="rounded flex items-start gap-1.5 dictia-fade-x"
:style="`background-color:${e.c}0C;border-left:2px solid ${e.c};padding:3px 6px;animation-delay:${ei * 100}ms;`">
<span class="font-mono text-[9px] font-bold mt-px" :style="`color:${e.c};`" x-text="e.t"></span>
<span class="rounded font-mono text-[8px] font-bold tracking-widest mt-px"
style="padding:0 4px;"
:style="`background:${e.c}22;color:${e.c};`" x-text="e.lvl"></span>
<p class="font-mono text-[9px] leading-snug flex-1" style="color:rgba(255,255,255,0.78);" x-text="e.msg"></p>
</div>
</template>
</div>
<div class="flex items-center justify-center gap-2 px-3 py-2 flex-shrink-0"
style="background:rgba(16,185,129,0.06);border-top:1px solid rgba(16,185,129,0.14);">
<span class="dictia-dot-pulse block rounded-full" style="width:5px;height:5px;background:#10b981;"></span>
<span class="font-mono text-[9px] font-bold tracking-widest" style="color:rgba(16,185,129,0.90);">CONSENTEMENT TRACÉ · IMMUTABLE</span>
</div>
</div>
</template>
{# Mode 12 : Conformité Loi 25 + 9 ordres pros (badges défilants + EFVP CAI) — Catégorie Gouvernance #}
<template x-if="displayMode === 12">
<div class="dictia-mode-fade w-full h-full flex flex-col overflow-hidden" x-data="loi25ModeData()" x-init="init()">
<div class="flex items-center justify-center gap-2 py-2 flex-shrink-0"
style="background:rgba(192,38,211,0.06);border-bottom:1px solid rgba(192,38,211,0.12);">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:11px;height:11px;color:rgba(192,38,211,0.90);" aria-hidden="true">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4"/>
</svg>
<span class="font-mono text-[10px] font-bold tracking-widest" style="color:rgba(192,38,211,0.90);">CONFORMITÉ QUÉBEC</span>
</div>
<div class="flex-1 dictia-mode-scroll dictia-fade-bottom flex flex-col gap-1.5 px-3 py-2">
{# Badges Loi 25 / Loi 96 / EFVP CAI / Cadre IA MCN #}
<div class="flex flex-wrap gap-1.5">
<template x-for="(b, bi) in BADGES" :key="b.t">
<span class="rounded font-mono dictia-spring inline-flex items-center gap-1"
:style="`background:${b.c}18;border:1px solid ${b.c}45;color:${b.c};padding:2px 6px;animation-delay:${bi * 110}ms;font-size:9px;font-weight:bold;`">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" style="width:7px;height:7px;" aria-hidden="true">
<polyline points="20 6 9 17 4 12"/>
</svg>
<span x-text="b.t"></span>
</span>
</template>
</div>
{# 9 ordres pros mappés #}
<p class="font-mono text-[9px] font-bold uppercase tracking-wider mt-1" style="color:rgba(192,38,211,0.75);">9 ordres professionnels</p>
<div class="grid grid-cols-3 gap-1">
<template x-for="(o, oi) in ORDERS" :key="o">
<span class="rounded font-mono dictia-fade-y text-center text-[9px]"
style="padding:2px;background:rgba(192,38,211,0.10);border:1px solid rgba(192,38,211,0.25);color:rgba(255,255,255,0.85);"
:style="`animation-delay:${oi * 60 + 800}ms;`"
x-text="o"></span>
</template>
</div>
</div>
<div class="flex items-center justify-center gap-2 px-3 py-2 flex-shrink-0"
style="background:rgba(16,185,129,0.06);border-top:1px solid rgba(16,185,129,0.14);">
<span class="font-mono text-[9px] font-bold tracking-widest" style="color:rgba(16,185,129,0.90);">EFVP CAI · CADRE IA MCN</span>
</div>
</div>
</template>
</div>
{# BOTTOM : 6 feature icons + label + tab indicator + Auto pill — hauteur fixe 90px (cadre stable) #}
<div class="relative flex flex-col items-center justify-center gap-1.5 z-10 flex-shrink-0"
{# BOTTOM : 4 catégories tabs (Capture / IA / Distribution / Gouvernance) + sub-mode dots + Auto pill — hauteur fixe 90px #}
<div class="relative flex flex-col items-center justify-center gap-1 z-10 flex-shrink-0"
style="height: 90px; border-top: 1px solid rgba(255,255,255,0.06); background: linear-gradient(180deg, transparent, rgba(0,0,0,0.32));">
{# Sub-mode dots indicator (shows position within active category) #}
<div class="flex items-center gap-1" style="height:6px;" aria-hidden="true">
<template x-for="(sm, di) in CATEGORIES[activeCategory].submodes" :key="di">
<span class="block rounded-full"
:style="`width:${sm === selectedFeature ? '10px' : '4px'};height:4px;background:${sm === selectedFeature ? CATEGORIES[activeCategory].color : 'rgba(255,255,255,0.20)'};transition:width 0.2s, background 0.2s;`"></span>
</template>
</div>
{# 4 catégories tabs #}
<div class="flex items-end gap-1">
<template x-for="i in [1,2,3,4,5,6]" :key="i">
<button type="button" @click="handleManualSelect(i)"
class="dictia-feat-btn outline-none cursor-pointer focus-visible:ring-2 focus-visible:ring-white/40 flex flex-col items-center justify-end relative"
style="width:34px;height:42px;border:none;padding:4px 0 6px;background:transparent;"
:aria-pressed="selectedFeature === i ? 'true' : 'false'"
:aria-label="`Voir : ${FEATURES[i].title}`">
<span :style="`color: ${selectedFeature === i ? FEATURES[i].color : 'rgba(255,255,255,0.30)'}; transition: color 0.2s, transform 0.15s, filter 0.2s; filter: ${selectedFeature === i ? 'drop-shadow(0 0 6px ' + FEATURES[i].color + 'CC)' : 'none'}; transform: scale(${selectedFeature === i ? 1.15 : 1}); display: inline-block;`">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:16px;height:16px;" aria-hidden="true" x-html="featureIconPath(i)"></svg>
<template x-for="(cat, ci) in CATEGORIES" :key="ci">
<button type="button" @click="handleCategorySelect(ci)"
class="dictia-feat-btn dictia-cat-btn outline-none cursor-pointer focus-visible:ring-2 focus-visible:ring-white/40 flex flex-col items-center justify-end relative"
style="width:54px;height:42px;border:none;padding:4px 0 6px;background:transparent;"
:aria-pressed="activeCategory === ci ? 'true' : 'false'"
:aria-label="`Catégorie : ${cat.title}`">
<span :style="`color: ${activeCategory === ci ? cat.color : 'rgba(255,255,255,0.30)'}; transition: color 0.2s, transform 0.15s, filter 0.2s; filter: ${activeCategory === ci ? 'drop-shadow(0 0 6px ' + cat.color + 'CC)' : 'none'}; transform: scale(${activeCategory === ci ? 1.15 : 1}); display: inline-block;`">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:18px;height:18px;" aria-hidden="true" x-html="cat.iconPath"></svg>
</span>
<span class="text-[9px] font-mono font-semibold mt-0.5 leading-none uppercase tracking-wider"
:style="`color: ${selectedFeature === i ? FEATURES[i].color : 'rgba(255,255,255,0.40)'};`"
x-text="featureShortLabel(i)"></span>
{# Tab indicator bottom border 2px (style tab bar) #}
<span x-show="selectedFeature === i" class="absolute" aria-hidden="true"
:style="`bottom: 0; left: 4px; right: 4px; height: 2px; background: ${FEATURES[i].color}; border-radius: 1px; box-shadow: 0 0 8px ${FEATURES[i].color};`"></span>
<span class="text-[9px] font-mono font-bold mt-0.5 leading-none uppercase tracking-wider"
:style="`color: ${activeCategory === ci ? cat.color : 'rgba(255,255,255,0.40)'};`"
x-text="cat.short"></span>
{# Tab indicator bottom border 2px #}
<span x-show="activeCategory === ci" class="absolute" aria-hidden="true"
:style="`bottom: 0; left: 6px; right: 6px; height: 2px; background: ${cat.color}; border-radius: 1px; box-shadow: 0 0 8px ${cat.color};`"></span>
</button>
</template>
</div>
@@ -1031,19 +1286,19 @@
</div>
</div>
{# Mobile : pills horizontales scrollables #}
{# Mobile : pills catégories horizontales scrollables #}
<div class="lg:hidden w-full overflow-x-auto dictia-hide-scrollbar" style="scrollbar-width:none;">
<div class="flex gap-2 px-1 pb-1" style="width:max-content;">
<template x-for="i in [1,2,3,4,5,6]" :key="i">
<button type="button" @click="handleManualSelect(i)"
<template x-for="(cat, ci) in CATEGORIES" :key="ci">
<button type="button" @click="handleCategorySelect(ci)"
class="flex items-center gap-1.5 px-2.5 py-1.5 rounded-full shrink-0 transition-all focus-visible:ring-2 focus-visible:ring-brand-b1"
:style="`border: 1px solid ${selectedFeature === i ? FEATURES[i].color + '70' : 'rgba(0,0,0,0.10)'}; background-color: ${selectedFeature === i ? FEATURES[i].color + '18' : 'rgba(0,0,0,0.04)'}; outline: none;`">
<span :style="`color: ${selectedFeature === i ? FEATURES[i].color : 'rgba(0,0,0,0.40)'}; transition: color 0.2s;`">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:11px;height:11px;" aria-hidden="true" x-html="featureIconPath(i)"></svg>
:style="`border: 1px solid ${activeCategory === ci ? cat.color + '70' : 'rgba(0,0,0,0.10)'}; background-color: ${activeCategory === ci ? cat.color + '18' : 'rgba(0,0,0,0.04)'}; outline: none;`">
<span :style="`color: ${activeCategory === ci ? cat.color : 'rgba(0,0,0,0.40)'}; transition: color 0.2s;`">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:12px;height:12px;" aria-hidden="true" x-html="cat.iconPath"></svg>
</span>
<span class="text-[11px] font-medium whitespace-nowrap"
:style="`color: ${selectedFeature === i ? FEATURES[i].color : 'rgba(0,0,0,0.50)'};`"
x-text="FEATURES[i].title"></span>
<span class="text-[11px] font-bold whitespace-nowrap"
:style="`color: ${activeCategory === ci ? cat.color : 'rgba(0,0,0,0.50)'};`"
x-text="cat.title"></span>
</button>
</template>
</div>
@@ -1057,7 +1312,7 @@
<div class="mb-1 flex items-center gap-2">
<span class="block w-1 h-6 rounded-full" style="background:linear-gradient(180deg,#06b6d4,#c026d3);" aria-hidden="true"></span>
<div>
<p class="text-xs font-mono font-bold uppercase tracking-widest text-brand-b3">Fonctions clés</p>
<p class="text-xs font-mono font-bold uppercase tracking-widest text-brand-b3">4 catégories · 12 sous-modes</p>
<p class="font-bold text-lg leading-snug text-brand-navy">Le moteur IA local</p>
</div>
</div>
@@ -1155,20 +1410,32 @@
</div>
</div>
{# Feature grid 3 cols × 6 buttons — fond TOUJOURS dark navy (actif/inactif) pour garantir contraste WCAG AA blanc-sur-dark sur section claire #}
<div class="grid grid-cols-3 gap-2">
<template x-for="i in [1,2,3,4,5,6]" :key="i">
<button type="button" @click="handleManualSelect(i)"
class="flex flex-col items-center gap-1.5 rounded-xl py-3 px-2 outline-none transition-all focus-visible:ring-2 focus-visible:ring-white/40"
:style="`border: 1px solid ${selectedFeature === i ? FEATURES[i].color + '70' : 'rgba(255,255,255,0.10)'}; background-color: ${selectedFeature === i ? 'rgba(8,12,24,0.95)' : 'rgba(8,12,24,0.85)'}; box-shadow: ${selectedFeature === i ? '0 0 16px ' + FEATURES[i].color + '40, inset 0 0 16px ' + FEATURES[i].color + '20' : 'none'}; cursor: pointer;`"
:aria-pressed="selectedFeature === i ? 'true' : 'false'"
:aria-label="`Sélectionner : ${FEATURES[i].title}`">
<span :style="`color: ${selectedFeature === i ? FEATURES[i].color : 'rgba(255,255,255,0.55)'}; filter: ${selectedFeature === i ? 'drop-shadow(0 0 6px ' + FEATURES[i].color + 'BB)' : 'none'}; transform: scale(${selectedFeature === i ? 1.20 : 1}) translateY(${selectedFeature === i ? -1 : 0}px); transition: all 0.2s; display:inline-block;`">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:18px;height:18px;" aria-hidden="true" x-html="featureIconPath(i)"></svg>
</span>
<span class="text-[10px] font-semibold text-center leading-tight"
:style="`color: ${selectedFeature === i ? 'rgba(255,255,255,0.98)' : 'rgba(255,255,255,0.70)'}; transition: color 0.2s;`"
x-text="FEATURES[i].title"></span>
{# Category grid 2×2 — 4 catégories avec sous-modes affichés dessous (architecture lisible) #}
<div class="grid grid-cols-2 gap-2">
<template x-for="(cat, ci) in CATEGORIES" :key="ci">
<button type="button" @click="handleCategorySelect(ci)"
class="flex flex-col items-start gap-2 rounded-xl py-3 px-3 outline-none transition-all focus-visible:ring-2 focus-visible:ring-white/40 text-left"
:style="`border: 1px solid ${activeCategory === ci ? cat.color + '70' : 'rgba(255,255,255,0.10)'}; background-color: ${activeCategory === ci ? 'rgba(8,12,24,0.95)' : 'rgba(8,12,24,0.85)'}; box-shadow: ${activeCategory === ci ? '0 0 16px ' + cat.color + '40, inset 0 0 16px ' + cat.color + '20' : 'none'}; cursor: pointer;`"
:aria-pressed="activeCategory === ci ? 'true' : 'false'"
:aria-label="`Catégorie : ${cat.title}`">
<div class="flex items-center gap-2 w-full">
<span :style="`color: ${activeCategory === ci ? cat.color : 'rgba(255,255,255,0.55)'}; filter: ${activeCategory === ci ? 'drop-shadow(0 0 6px ' + cat.color + 'BB)' : 'none'}; transition: all 0.2s; display:inline-block;`">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:18px;height:18px;" aria-hidden="true" x-html="cat.iconPath"></svg>
</span>
<span class="text-[11px] font-bold leading-tight flex-1"
:style="`color: ${activeCategory === ci ? 'rgba(255,255,255,0.98)' : 'rgba(255,255,255,0.85)'};`"
x-text="cat.title"></span>
</div>
<p class="text-[10px] font-mono leading-snug w-full"
:style="`color: ${activeCategory === ci ? cat.color + 'EE' : 'rgba(255,255,255,0.55)'};`"
x-text="cat.subtitle"></p>
{# Mini sous-modes dots dans la card #}
<div class="flex items-center gap-0.5 mt-auto" aria-hidden="true">
<template x-for="(sm, si) in cat.submodes" :key="si">
<span class="block rounded-full"
:style="`width:${activeCategory === ci && sm === selectedFeature ? '8px' : '3px'};height:3px;background:${activeCategory === ci ? (sm === selectedFeature ? cat.color : cat.color + '55') : 'rgba(255,255,255,0.15)'};transition:all 0.2s;`"></span>
</template>
</div>
</button>
</template>
</div>
@@ -1178,20 +1445,64 @@
</div>
{# Alpine logic — dictiaDashboard + sub-data functions pour les 6 modes #}
{# Alpine logic — dictiaDashboard + 4 catégories × 12 sous-modes (refonte stratégique 2026-04-29) #}
<script>
function dictiaDashboard() {
return {
/* FEATURES indexes (0-12) — même structure que v1 (0=IA, 1-6=existants), nouveaux 7-12 ajoutés */
FEATURES: [
{ idx: 0, title: 'IA intégrée', subtitle: "Résumé, actions, Q&R", color: '#c026d3', badge: 'Mistral 7B' }, /* brand-b3 */
{ idx: 0, title: 'IA intégrée', subtitle: "Résumé, actions, Q&R", color: '#c026d3', badge: 'Mistral 7B' }, /* brand-b3 */
{ idx: 1, title: 'Transcription', subtitle: 'Parole → texte en temps réel', color: '#06b6d4', badge: 'Whisper AI' }, /* brand-b2 */
{ idx: 2, title: 'Diarisation', subtitle: 'Identification des locuteurs', color: '#2563eb', badge: null }, /* brand-b1 */
{ idx: 3, title: '99+ langues', subtitle: 'Détection automatique', color: '#06b6d4', badge: null }, /* brand-b2 */
{ idx: 4, title: 'Exports', subtitle: 'DOCX, SRT, JSON, PDF', color: '#2563eb', badge: null }, /* brand-b1 */
{ idx: 5, title: 'Utilisateurs illimités', subtitle: 'Toute votre équipe', color: '#c026d3', badge: 'Illimité' }, /* brand-b3 */
{ idx: 6, title: 'Partage & Classement', subtitle: 'Dossiers, tags, recherche', color: '#06b6d4', badge: null } /* brand-b2 */
{ idx: 2, title: 'Diarisation', subtitle: 'Identification des locuteurs', color: '#2563eb', badge: null }, /* brand-b1 */
{ idx: 3, title: '99+ langues', subtitle: 'Détection automatique', color: '#06b6d4', badge: null }, /* brand-b2 */
{ idx: 4, title: 'Exports', subtitle: 'DOCX, SRT, JSON, PDF', color: '#2563eb', badge: null }, /* brand-b1 */
{ idx: 5, title: 'Utilisateurs illimités', subtitle: 'Toute votre équipe', color: '#c026d3', badge: 'Illimité' }, /* brand-b3 */
{ idx: 6, title: 'Partage & Classement', subtitle: 'Dossiers, tags, recherche', color: '#06b6d4', badge: null }, /* brand-b2 */
/* === Nouveaux modes 2026-04-29 === */
{ idx: 7, title: 'Enregistrement live', subtitle: 'Mic + système + minuteur', color: '#06b6d4', badge: 'PWA' }, /* brand-b2 */
{ idx: 8, title: 'Recherche IA', subtitle: 'Sémantique sur toute la bibliothèque', color: '#06b6d4', badge: 'RAG' }, /* brand-b2 */
{ idx: 9, title: 'Résumé + actions', subtitle: 'Décisions, ICS, événements', color: '#2563eb', badge: 'Nemo 12B' }, /* brand-b1 */
{ idx: 10, title: 'Intégrations', subtitle: 'Word, Notion, Teams, Zapier...', color: '#c026d3', badge: '8 hubs' }, /* brand-b3 */
{ idx: 11, title: 'Audit trail', subtitle: 'Traçabilité Loi 25 immutable', color: '#2563eb', badge: 'Loi 25' }, /* brand-b1 */
{ idx: 12, title: 'Conformité', subtitle: '9 ordres pros · EFVP CAI', color: '#c026d3', badge: 'MCN' } /* brand-b3 */
],
/* CATEGORIES : 4 regroupements logiques. Chaque catégorie a ses sous-modes (ids des FEATURES). */
CATEGORIES: [
{
title: 'Capture',
short: 'Capture',
subtitle: 'Audio in : upload, recording, recherche',
color: '#06b6d4', /* brand-b2 cyan */
submodes: [1, 7, 8], /* Transcription (upload), Recording live, Recherche sémantique */
iconPath: '<rect x="9" y="2" width="6" height="12" rx="3"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/>'
},
{
title: 'Transformation IA',
short: 'IA',
subtitle: 'Diarisation, langues, résumés, Q&R',
color: '#2563eb', /* brand-b1 blue */
submodes: [2, 3, 9, 0], /* Diarisation, Langues, Résumé+actions, Chat IA */
iconPath: '<path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2Z"/><path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2Z"/>'
},
{
title: 'Distribution',
short: 'Distrib',
subtitle: 'Exports, intégrations, partage',
color: '#c026d3', /* brand-b3 fuchsia */
submodes: [4, 10, 6, 5], /* Exports, Intégrations, Partage, Users */
iconPath: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>'
},
{
title: 'Gouvernance',
short: 'Gouv',
subtitle: 'Audit, Loi 25, AGPL, ordres pros',
color: '#2563eb', /* brand-b1 blue (sobre, institutionnel) */
submodes: [11, 12], /* Audit trail, Conformité Loi 25 */
iconPath: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4"/>'
}
],
selectedFeature: 1,
activeCategory: 0,
isManual: false,
autoCycleTimer: null,
manualResetTimer: null,
@@ -1201,14 +1512,42 @@
if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
this.startAutoCycle();
},
/* Auto-cycle logic : avance dans les sous-modes de la catégorie active toutes les 1100ms.
Quand on atteint le dernier sous-mode → switch catégorie suivante (boucle 0→1→2→3→0). */
startAutoCycle() {
if (this.autoCycleTimer) clearInterval(this.autoCycleTimer);
this.autoCycleTimer = setInterval(() => {
this.selectedFeature = this.selectedFeature < this.FEATURES.length - 1 ? this.selectedFeature + 1 : 1;
}, 900);
const cat = this.CATEGORIES[this.activeCategory];
const currentIdx = cat.submodes.indexOf(this.selectedFeature);
if (currentIdx < cat.submodes.length - 1) {
/* Avance dans la catégorie courante */
this.selectedFeature = cat.submodes[currentIdx + 1];
} else {
/* Fin de catégorie → passe à la suivante */
this.activeCategory = (this.activeCategory + 1) % this.CATEGORIES.length;
this.selectedFeature = this.CATEGORIES[this.activeCategory].submodes[0];
}
}, 1100);
},
/* Click manuel sur un sous-mode (compatibilité 6 boutons originaux) */
handleManualSelect(i) {
this.selectedFeature = i;
/* Trouve la catégorie qui contient ce sous-mode */
for (let ci = 0; ci < this.CATEGORIES.length; ci++) {
if (this.CATEGORIES[ci].submodes.includes(i)) { this.activeCategory = ci; break; }
}
this.isManual = true;
if (this.autoCycleTimer) { clearInterval(this.autoCycleTimer); this.autoCycleTimer = null; }
if (this.manualResetTimer) clearTimeout(this.manualResetTimer);
this.manualResetTimer = setTimeout(() => {
this.isManual = false;
this.startAutoCycle();
}, 4500);
},
/* Click manuel sur une catégorie : sélectionne son premier sous-mode */
handleCategorySelect(ci) {
this.activeCategory = ci;
this.selectedFeature = this.CATEGORIES[ci].submodes[0];
this.isManual = true;
if (this.autoCycleTimer) { clearInterval(this.autoCycleTimer); this.autoCycleTimer = null; }
if (this.manualResetTimer) clearTimeout(this.manualResetTimer);
@@ -1225,12 +1564,19 @@
3: '<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>',
4: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><polyline points="9 15 12 18 15 15"/>',
5: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="20" y1="8" x2="20" y2="14"/><line x1="23" y1="11" x2="17" y2="11"/>',
6: '<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>'
6: '<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
7: '<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/>',
8: '<circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>',
9: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="9" y1="13" x2="15" y2="13"/><line x1="9" y1="17" x2="15" y2="17"/>',
10: '<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>',
11: '<path d="M12 2v6"/><path d="M12 22v-6"/><path d="M4.93 4.93l4.24 4.24"/><path d="M14.83 14.83l4.24 4.24"/><circle cx="12" cy="12" r="4"/>',
12: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4"/>'
};
return paths[i] || '';
},
featureShortLabel(i) {
const labels = { 1: 'Trans', 2: 'Diari', 3: 'Lang', 4: 'Exp', 5: 'Users', 6: 'Part' };
const labels = { 1: 'Trans', 2: 'Diari', 3: 'Lang', 4: 'Exp', 5: 'Users', 6: 'Part',
7: 'Rec', 8: 'Search', 9: 'Résu', 10: 'Hub', 11: 'Audit', 12: 'Conf' };
return labels[i] || '';
}
};
@@ -1424,6 +1770,167 @@
_cleanup() { this._timers.forEach(t => { clearInterval(t); clearTimeout(t); }); this._timers = []; }
};
}
/* === MODES AJOUTÉS 2026-04-29 === */
/* Mode 7 — Recording live : minuteur incrémentiel + waveform random pattern */
function recModeData() {
return {
sec: 0, BARS: [], _iv: null, _ivBars: null,
get time() {
const m = String(Math.floor(this.sec / 60)).padStart(2, '0');
const s = String(this.sec % 60).padStart(2, '0');
return `${m}:${s}`;
},
get size() { return Math.round(this.sec * 0.16 * 10) / 10; },
init() {
if (this._iv) clearInterval(this._iv);
if (this._ivBars) clearInterval(this._ivBars);
this.sec = 0;
/* 24 bars hauteur initiale random 6-26px */
this.BARS = Array.from({ length: 24 }, () => Math.floor(Math.random() * 20) + 6);
this._iv = setInterval(() => {
this.sec++;
if (this.sec >= 99) { clearInterval(this._iv); setTimeout(() => this.init(), 800); }
}, 1000);
/* Update bars heights chaque 250ms pour effet "live" */
this._ivBars = setInterval(() => {
this.BARS = this.BARS.map(() => Math.floor(Math.random() * 20) + 6);
}, 250);
}
};
}
/* Mode 8 — Recherche sémantique : query typed + résultats matched */
function searchModeData() {
return {
QUERY: 'préoccupations budgétaires',
query: '', results: [], _timers: [],
RESULTS_DATA: [
{ title: 'CR-Réunion-Jan14', match: 'contraintes budgétaires Q4', score: 94, c: '#06b6d4' },
{ title: 'Comité-Direction', match: 'compressions à anticiper', score: 87, c: '#2563eb' },
{ title: 'Entretien-Sophie', match: 'inquiétudes financières', score: 82, c: '#c026d3' }
],
init() {
this._cleanup();
this.query = ''; this.results = [];
let i = 0;
const ivType = setInterval(() => {
i++; this.query = this.QUERY.slice(0, i);
if (i >= this.QUERY.length) {
clearInterval(ivType);
const t1 = setTimeout(() => { this.results = this.RESULTS_DATA; }, 500);
this._timers.push(t1);
const t2 = setTimeout(() => this.init(), 5500);
this._timers.push(t2);
}
}, 60);
this._timers.push(ivType);
},
_cleanup() { this._timers.forEach(t => { clearInterval(t); clearTimeout(t); }); this._timers = []; }
};
}
/* Mode 9 — Résumé + extraction d'événements : décisions/actions stagger */
function summaryModeData() {
return {
decisions: [
'Approuvé budget Q4 à l\'unanimité',
'Reporter démo à février',
'Validation finale 25 janv.'
],
actions: [
'Marc : envoyer CR avant 17h',
'Sophie : contacter client vendredi',
'Julie : réviser budget avec Luc'
],
dShown: 0, aShown: 0, _timers: [],
init() {
this._cleanup();
this.dShown = 0; this.aShown = 0;
let di = 0;
const ivD = setInterval(() => {
di++; this.dShown = di;
if (di >= this.decisions.length) {
clearInterval(ivD);
let ai = 0;
const t1 = setTimeout(() => {
const ivA = setInterval(() => {
ai++; this.aShown = ai;
if (ai >= this.actions.length) {
clearInterval(ivA);
const t2 = setTimeout(() => this.init(), 3500);
this._timers.push(t2);
}
}, 600);
this._timers.push(ivA);
}, 700);
this._timers.push(t1);
}
}, 600);
this._timers.push(ivD);
},
_cleanup() { this._timers.forEach(t => { clearInterval(t); clearTimeout(t); }); this._timers = []; }
};
}
/* Mode 10 — Intégrations Hub : 8 logos en orbite */
function integModeData() {
return {
LOGOS: [
{ name: 'Word', short: 'W', c: '#2563eb', x: 20, y: 30 },
{ name: 'Outlook', short: 'O', c: '#06b6d4', x: 80, y: 30 },
{ name: 'Teams', short: 'T', c: '#c026d3', x: 15, y: 55 },
{ name: 'Notion', short: 'N', c: '#2563eb', x: 85, y: 55 },
{ name: 'Obsidian', short: 'Ob', c: '#c026d3', x: 25, y: 80 },
{ name: 'Zapier', short: 'Z', c: '#06b6d4', x: 75, y: 80 },
{ name: 'Make', short: 'Mk', c: '#2563eb', x: 50, y: 18 },
{ name: 'n8n', short: 'n8', c: '#06b6d4', x: 50, y: 88 }
],
init() {}
};
}
/* Mode 11 — Audit trail : event log staggered */
function auditModeData() {
return {
events: [
{ t: '09:01', lvl: 'INFO', c: '#06b6d4', msg: 'Sophie a uploadé reunion-jan14.mp3' },
{ t: '09:02', lvl: 'AUTH', c: '#2563eb', msg: 'Consentement explicite enregistré' },
{ t: '09:04', lvl: 'PROC', c: '#c026d3', msg: 'WhisperX terminé · diarisation 3 loc.' },
{ t: '09:08', lvl: 'READ', c: '#06b6d4', msg: 'Marc a écouté le segment 02:14-04:30' },
{ t: '09:12', lvl: 'EXP', c: '#2563eb', msg: 'Export DOCX par Julie · IP 198.x.x.42' },
{ t: '09:15', lvl: 'SHARE', c: '#c026d3', msg: 'Partage interne accordé : Luc (édition)' }
],
shown: 0, _iv: null,
init() {
if (this._iv) clearInterval(this._iv);
this.shown = 0;
this._iv = setInterval(() => {
if (this.shown >= this.events.length) {
clearInterval(this._iv);
setTimeout(() => this.init(), 2400);
} else this.shown++;
}, 700);
}
};
}
/* Mode 12 — Conformité Loi 25 + 9 ordres pros */
function loi25ModeData() {
return {
BADGES: [
{ t: 'Loi 25', c: '#06b6d4' },
{ t: 'Loi 96', c: '#2563eb' },
{ t: 'EFVP CAI', c: '#c026d3' },
{ t: 'Cadre IA MCN', c: '#06b6d4' },
{ t: 'AGPL v3', c: '#2563eb' },
{ t: '0 Cloud Act US', c: '#c026d3' }
],
ORDERS: ['Barreau', 'CNQ', 'CPA', 'ChAD', 'OACIQ', 'CMQ', 'OIIQ', 'OPQ', 'OEQ'],
init() {}
};
}
</script>
</section>

View File

@@ -190,8 +190,32 @@ def test_fonctionnalites_how_it_works_reactor_section():
# Auto-cycle + manual select logic
assert 'startAutoCycle' in body, "Missing auto-cycle function"
assert 'handleManualSelect' in body, "Missing manual select handler"
assert '900' in body, "Missing 900ms cycle interval"
assert '1100' in body, "Missing 1100ms sub-mode cycle interval (refonte 2026-04-29)"
assert '4500' in body, "Missing 4500ms manual reset timer"
# Refonte 2026-04-29 : 4 catégories × 12 sous-modes
assert 'CATEGORIES' in body, "Missing CATEGORIES array (refonte 4 catégories)"
assert 'activeCategory' in body, "Missing activeCategory state"
assert 'handleCategorySelect' in body, "Missing category click handler"
for cat_title in ['Capture', 'Transformation IA', 'Distribution', 'Gouvernance']:
assert cat_title in body, f"Missing category : {cat_title}"
# Nouveaux modes 7-12
assert 'recModeData' in body, "Missing Mode 7 (Recording live)"
assert 'searchModeData' in body, "Missing Mode 8 (Recherche sémantique)"
assert 'summaryModeData' in body, "Missing Mode 9 (Résumé + actions)"
assert 'integModeData' in body, "Missing Mode 10 (Intégrations Hub)"
assert 'auditModeData' in body, "Missing Mode 11 (Audit trail)"
assert 'loi25ModeData' in body, "Missing Mode 12 (Conformité Loi 25)"
# Mode panels content keywords
assert 'ENREGISTREMENT LIVE' in body, "Missing recording header"
assert 'RECHERCHE IA SÉMANTIQUE' in body, "Missing semantic search header"
assert 'RÉSUMÉ EXÉCUTIF' in body, "Missing summary header"
assert 'INTÉGRATIONS HUB' in body, "Missing integrations hub header"
assert 'AUDIT TRAIL' in body, "Missing audit trail header"
assert 'CONFORMITÉ QUÉBEC' in body, "Missing conformity header"
assert 'ÉVÉNEMENT DÉTECTÉ' in body, "Missing ICS event detection"
assert '9 ordres professionnels' in body, "Missing 9 ordres listing"
for ordre in ['Barreau', 'CNQ', 'CPA', 'OIIQ']:
assert ordre in body, f"Missing ordre pro: {ordre}"
# Auto / Manual status text
assert 'Auto' in body, "Missing Auto status indicator"
assert 'Auto reprend bientôt' in body, "Missing manual mode hint"
@@ -249,8 +273,9 @@ def test_fonctionnalites_how_it_works_reactor_section():
assert 'dictia-bg-orbs' in body, "Missing floating orbs"
assert 'dictia-connecting-line' in body, "Missing connecting line phone↔IA"
# Stats row in section header
# Stats row in section header (refonte 2026-04-29 : 4 catégories · 6 modules)
assert 'modules' in body, "Missing stats row 'modules'"
assert 'catégories' in body, "Missing stats row 'catégories' (refonte)"
assert 'cloud' in body, "Missing stats row 'cloud'"
assert 'Voir une démo' in body, "Missing demo CTA link"