refactor(marketing): reproduit fidèlement DashboardHolographique de dictai-narrative.tsx (6 modes uniques + auto-cycle 900ms)

Remplace la section "Comment ça marche" (réacteur orbital générique du commit
03f6e56) par une reproduction fidèle du composant DashboardHolographique
défini dans Website-Sanity/components/sections/dictai-narrative.tsx.

Architecture : container "phone" central (border-radius 44px, color tinting
selon feature active) + 6 modes uniques (Transcription upload+words,
Diarisation conversation Sophie/Marc/Julie, 99+ langues grille staggered,
Exports 7 file icons, Users avatars 1→20, Share folders+tags+files) +
IA Mistral 7B premium card + grid 3 cols × 6 features cliquables.

Auto-cycle 900 ms (1→6→1, skip IA index 0) avec click manuel → isManual
pendant 4500 ms puis reprise auto. Animations Framer Motion → CSS
keyframes + Alpine setInterval (preserves prefers-reduced-motion guard,
aria-live, aria-pressed).

Couleurs source spécifiques préservées (#A78BFA #22D3EE #6B9FFF #34D399
#F59E0B) — identifient les features et restent indépendantes de la palette
brand globale b1/b2/b3.

Test test_fonctionnalites_how_it_works_reactor_section adapté à la nouvelle
structure (dictiaDashboard, 5 sub-data fns, 6 modes par signature unique,
IA premium card animations, auto-cycle 900ms / 4500ms manual reset).
This commit is contained in:
Allison
2026-04-29 09:37:09 -04:00
parent 03f6e56f04
commit 7aaedf2cdf
2 changed files with 663 additions and 183 deletions

View File

@@ -249,60 +249,59 @@
</div> </div>
</section> </section>
{# ===== COMMENT ÇA MARCHE — RÉACTEUR INTERACTIF ===== {# ===== COMMENT ÇA MARCHE — DASHBOARD HOLOGRAPHIQUE =====
Sous-partie visuelle de la section Fonctionnalités (sub-nav reste à 4 ancres). Reproduction fidèle de DashboardHolographique (Website-Sanity/dictai-narrative.tsx).
Reproduit dictai-narrative.tsx (Website-Sanity) en CSS pur + Alpine.js. 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.
#} #}
<section class="bg-brand-bg py-20" aria-labelledby="how-it-works-title"> <section class="bg-brand-bg py-20" aria-labelledby="how-it-works-title">
<style> <style>
/* Anneaux concentriques rotatifs (réacteur DictIA) */ /* Mic pulsing en haut du phone */
.reactor-ring { position: absolute; border-radius: 9999px; pointer-events: none; } .dictia-mic-pulse { animation: dmic-pulse 1.2s ease-in-out infinite; display: inline-block; }
.ring-outer { width: 420px; height: 420px; border: 1px solid rgba(37,99,235,0.25); animation: ring-rotate-cw 30s linear infinite; } @keyframes dmic-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.18); } }
.ring-mid { width: 300px; height: 300px; border: 1px solid rgba(6,182,212,0.30); animation: ring-rotate-ccw 22s linear infinite; }
.ring-inner { width: 180px; height: 180px; border: 1px solid rgba(192,38,211,0.35); animation: ring-rotate-cw 15s linear infinite; }
@keyframes ring-rotate-cw { from { transform: rotate(0); } to { transform: rotate(360deg); } }
@keyframes ring-rotate-ccw { from { transform: rotate(0); } to { transform: rotate(-360deg); } }
/* Particules orbitales — 8 dots qui tournent autour du wordmark */ /* IA card ambient glow + brain glow + LOCAL pulse */
.reactor-orbit-host { position: absolute; left: 50%; top: 50%; width: 1px; height: 1px; } .ia-ambient-glow { animation: ia-glow 3s ease-in-out infinite; }
.orbit { position: absolute; left: 0; top: 0; border-radius: 9999px; transform-origin: 0 0; } @keyframes ia-glow { 0%, 100% { opacity: 0.4; } 50% { opacity: 0.75; } }
.orbit-1 { width: 8px; height: 8px; background: #2563eb; animation: orbit-spin 12s linear infinite; --r: 210px; } .ia-brain-glow { animation: ia-brain 2.2s ease-in-out infinite; }
.orbit-2 { width: 6px; height: 6px; background: #06b6d4; animation: orbit-spin 18s linear infinite reverse; --r: 150px; } @keyframes ia-brain { 0%, 100% { opacity: 0.2; } 50% { opacity: 0.55; } }
.orbit-3 { width: 6px; height: 6px; background: #c026d3; animation: orbit-spin 9s linear infinite; --r: 90px; } .ia-local-badge { animation: ia-local 2.5s ease-in-out infinite; }
.orbit-4 { width: 5px; height: 5px; background: #06b6d4; animation: orbit-spin 14s linear infinite reverse; --r: 210px; animation-delay: -3.5s; } @keyframes ia-local { 0%, 100% { opacity: 0.75; } 50% { opacity: 1; } }
.orbit-5 { width: 7px; height: 7px; background: #c026d3; animation: orbit-spin 20s linear infinite; --r: 150px; animation-delay: -5s; }
.orbit-6 { width: 5px; height: 5px; background: #2563eb; animation: orbit-spin 11s linear infinite reverse; --r: 90px; animation-delay: -2s; }
.orbit-7 { width: 4px; height: 4px; background: #06b6d4; animation: orbit-spin 16s linear infinite; --r: 210px; animation-delay: -8s; }
.orbit-8 { width: 5px; height: 5px; background: #c026d3; animation: orbit-spin 13s linear infinite reverse; --r: 150px; animation-delay: -1s; }
@keyframes orbit-spin {
from { transform: rotate(0deg) translateX(var(--r)) rotate(0deg); }
to { transform: rotate(360deg) translateX(var(--r)) rotate(-360deg); }
}
/* Glow pulsant sous le wordmark central */ /* Curseur clignotant (transcription, IA chat) */
@keyframes reactor-glow-pulse { .dictia-blink { animation: dictia-cursor-blink 0.6s steps(2, end) infinite; }
0%, 100% { opacity: 0.55; transform: scale(1); } @keyframes dictia-cursor-blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }
50% { opacity: 0.85; transform: scale(1.08); }
}
.reactor-glow { animation: reactor-glow-pulse 3.2s ease-in-out infinite; }
/* Spec list item état actif */ /* Dot pulse (MISTRAL 7B · LOCAL header) */
.feature-list-item { transition: background-color 200ms ease-out, color 150ms ease-out, border-color 150ms ease-out; } .dictia-dot-pulse { animation: dictia-dot-pulse-anim 1.8s ease-in-out infinite; }
.feature-list-item.is-active { background: rgba(37,99,235,0.10); border-left-color: #2563eb; color: #060d1a; } @keyframes dictia-dot-pulse-anim { 0%, 100% { opacity: 0.4; transform: scale(1); } 50% { opacity: 1; transform: scale(1.35); } }
/* Slide-up + fade-in (diarisation messages) */
.dictia-msg-in { animation: dictia-fade-up 0.3s ease-out both; }
@keyframes dictia-fade-up { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
/* Spring pop (langues, exports, users, share) */
.dictia-spring { animation: dictia-spring-pop 0.28s cubic-bezier(0.34, 1.56, 0.64, 1) both; }
@keyframes dictia-spring-pop { 0% { opacity: 0; transform: scale(0.5); } 80% { transform: scale(1.08); } 100% { opacity: 1; transform: scale(1); } }
.dictia-spring-y { animation: dictia-spring-y 0.32s cubic-bezier(0.34, 1.56, 0.64, 1) both; }
@keyframes dictia-spring-y { 0% { opacity: 0; transform: translateY(14px) scale(0.65); } 80% { transform: translateY(-2px) scale(1.05); } 100% { opacity: 1; transform: translateY(0) scale(1); } }
.dictia-fade-x { animation: dictia-fade-x 0.32s ease-out both; }
@keyframes dictia-fade-x { from { opacity: 0; transform: translateX(10px); } to { opacity: 1; transform: translateX(0); } }
.dictia-fade-y { animation: dictia-fade-y 0.3s ease-out both; }
@keyframes dictia-fade-y { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
/* Mode wrapper fade */
.dictia-mode-fade { animation: dictia-mode-fade 0.22s ease-out both; }
@keyframes dictia-mode-fade { from { opacity: 0; } to { opacity: 1; } }
/* Hide scrollbar mobile pills */
.dictia-hide-scrollbar::-webkit-scrollbar { display: none; }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.ring-outer, .ring-mid, .ring-inner, .dictia-mic-pulse, .ia-ambient-glow, .ia-brain-glow, .ia-local-badge,
.orbit, .reactor-glow { animation: none; } .dictia-blink, .dictia-dot-pulse, .dictia-msg-in, .dictia-spring,
} .dictia-spring-y, .dictia-fade-x, .dictia-fade-y, .dictia-mode-fade { animation: none; }
/* Mobile: tighter ring sizes pour rester dans le cadre */
@media (max-width: 640px) {
.ring-outer { width: 320px; height: 320px; }
.ring-mid { width: 220px; height: 220px; }
.ring-inner { width: 140px; height: 140px; }
.orbit-1, .orbit-4, .orbit-7 { --r: 160px; }
.orbit-2, .orbit-5, .orbit-8 { --r: 110px; }
.orbit-3, .orbit-6 { --r: 70px; }
} }
</style> </style>
@@ -323,145 +322,604 @@
</p> </p>
</div> </div>
<div x-data='{ {# ── DASHBOARD HOLOGRAPHIQUE — 2 colonnes (phone center + IA right) ── #}
features: ["Transcription", "Diarisation", "99+ langues", "Exports", "Utilisateurs illimités", "Partage &amp; Classement"], <div x-data="dictiaDashboard()" x-init="init()"
details: { class="w-full flex flex-col lg:flex-row lg:justify-center lg:items-start gap-6 lg:gap-10">
"Transcription": { tag: "WhisperX Large-v3", desc: "STT 95%+ FR-CA" },
"Diarisation": { tag: "pyannote · 8 locuteurs max", desc: "Identification automatique" },
"99+ langues": { tag: "Détection automatique", desc: "FR · EN · ES · ZH · ..." },
"Exports": { tag: "DOCX, SRT, JSON, PDF", desc: "7 formats standards" },
"Utilisateurs illimités": { tag: "Aucun frais par utilisateur", desc: "Volume illimité" },
"Partage &amp; Classement": { tag: "Permissions granulaires", desc: "Tags + dossiers" }
},
active: "Transcription",
isHovered: false,
timer: null,
init() {
if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
this.timer = setInterval(() => {
if (!this.isHovered) {
const idx = this.features.indexOf(this.active);
this.active = this.features[(idx + 1) % this.features.length];
}
}, 1600);
},
setActive(name) { this.isHovered = true; this.active = name; },
resumeCycle() { this.isHovered = false; }
}'
x-init="init()"
class="grid lg:grid-cols-[1fr_minmax(0,360px)] gap-8 items-stretch">
{# ── COLONNE GAUCHE : Réacteur central holographique ── #} {# ─────────── ZONE CENTER : Phone container ─────────── #}
<div class="relative bg-brand-navy p-8 rounded overflow-hidden min-h-[480px] flex flex-col items-center justify-center"> <div class="flex flex-col items-center gap-3 lg:w-[280px] flex-shrink-0">
{# Backdrop radial #}
<div class="absolute inset-0 pointer-events-none" aria-hidden="true"
style="background: radial-gradient(ellipse 80% 50% at 50% 50%, rgba(37,99,235,0.10) 0%, transparent 70%);"></div>
{# 3 anneaux concentriques rotatifs #} {# Phone (border-radius 44px, color tinting selon feature active) #}
<div class="absolute inset-0 flex items-center justify-center" aria-hidden="true"> <div class="w-full max-w-[260px] mx-auto flex flex-col overflow-hidden relative"
<div class="reactor-ring ring-outer"></div> :style="`border-radius: 44px; border: 1.5px solid ${activeColor}33; background: rgba(8,12,24,0.8); box-shadow: 0 0 56px ${activeColor}1A, inset 0 0 28px ${activeColor}0A; min-height: 460px; transition: border-color 0.4s, box-shadow 0.4s;`">
<div class="reactor-ring ring-mid"></div>
<div class="reactor-ring ring-inner"></div> {# Ambient color tint overlay #}
{# 8 particules orbitales #} <div class="absolute inset-0 pointer-events-none"
<div class="reactor-orbit-host"> :style="`border-radius: 44px; background-color: ${activeColor}06; transition: background-color 0.4s;`"
<span class="orbit orbit-1"></span> aria-hidden="true"></div>
<span class="orbit orbit-2"></span>
<span class="orbit orbit-3"></span> {# TOP : Logo + Mic pulsing #}
<span class="orbit orbit-4"></span> <div class="flex flex-col items-center justify-center gap-2 relative z-10"
<span class="orbit orbit-5"></span> style="height: 96px; border-bottom: 1px solid rgba(255,255,255,0.06);">
<span class="orbit orbit-6"></span> <img src="/static/images/dictia-logo-nom.png" alt="DictIA"
<span class="orbit orbit-7"></span> style="width: 80px; height: 24px; object-fit: contain; opacity: 0.75;">
<span class="orbit orbit-8"></span> <span class="dictia-mic-pulse" :style="`color: ${activeColor};`" aria-hidden="true">
</div> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:22px;height:22px;">
<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"/>
<line x1="8" y1="23" x2="16" y2="23"/>
</svg>
</span>
</div> </div>
{# Auto badge — top right #} {# MIDDLE : 6 modes (1=Transcription, 2=Diarisation, 3=Langues, 4=Exports, 5=Users, 6=Share, 0=IA chat) #}
<div class="absolute top-4 right-4 inline-flex items-center gap-1.5 text-xs text-white/80 z-10 font-mono"> <div class="flex-1 relative z-10" style="min-height: 220px;"
<span class="w-2 h-2 rounded-full bg-brand-b2 animate-pulse" aria-hidden="true"></span>
Auto
</div>
{# Centre : DictIA wordmark + glow + feature panel #}
<div class="relative z-10 text-center">
<div class="relative inline-block mb-6">
<div class="absolute inset-0 reactor-glow pointer-events-none" aria-hidden="true"
style="background: radial-gradient(circle at 50% 50%, rgba(6,182,212,0.35) 0%, transparent 65%); filter: blur(24px); transform: scale(1.4);"></div>
<p class="relative font-black text-5xl sm:text-6xl grad-text leading-none tracking-tight">DictIA</p>
</div>
{# Panneau feature active — swap fluide via x-transition #}
<div class="bg-white/[0.06] border border-white/[0.10] rounded p-4 backdrop-blur-sm min-w-[260px] max-w-[320px] mx-auto"
role="status" aria-live="polite" aria-atomic="true"> role="status" aria-live="polite" aria-atomic="true">
<template x-for="feat in features" :key="feat">
<div x-show="active === feat" {# Mode 1 : Transcription (upload bar + words appearing) #}
x-transition:enter="transition ease-out duration-300" <template x-if="displayMode === 1">
x-transition:enter-start="opacity-0 translate-y-1" <div class="dictia-mode-fade w-full h-full" x-data="trModeData()" x-init="init()">
x-transition:enter-end="opacity-100 translate-y-0" <div x-show="phase === 'upload'" class="w-full h-full flex flex-col items-center justify-center gap-3 px-4">
x-transition:leave="transition ease-in duration-150" <div class="flex flex-col items-center gap-1.5">
x-transition:leave-start="opacity-100" <div class="w-10 h-12 rounded-md flex flex-col items-center justify-center relative"
x-transition:leave-end="opacity-0"> style="background-color: rgba(34,211,238,0.12); border: 1.5px solid rgba(34,211,238,0.40);">
<p class="text-[11px] uppercase tracking-[0.18em] text-white/60 mb-1 font-bold" x-text="feat"></p> <div class="absolute top-0 right-0" style="width:0;height:0;border-style:solid;border-width:0 7px 7px 0;border-color:transparent rgba(0,0,0,0.40) transparent transparent;"></div>
<p class="text-sm text-white font-semibold font-mono" x-text="details[feat].tag"></p> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:13px;height:13px;color:#22D3EE;" aria-hidden="true">
<p class="text-xs text-white/65 mt-1" x-text="details[feat].desc"></p> <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"/>
</svg>
<span style="font-size:5.5px;font-family:monospace;color:rgba(34,211,238,0.70);letter-spacing:0.06em;">MP3</span>
</div>
<span style="font-size:7px;font-family:monospace;color:rgba(255,255,255,0.42);">reunion-jan14.mp3</span>
</div>
<div class="w-full flex flex-col gap-1">
<div class="flex justify-between">
<span style="font-size:6.5px;font-family:monospace;color:rgba(34,211,238,0.65);" x-text="progress < 100 ? 'Envoi en cours…' : 'Prêt ✓'"></span>
<span style="font-size:6.5px;font-family:monospace;color:rgba(34,211,238,0.50);"><span x-text="progress"></span>%</span>
</div>
<div class="w-full rounded-full overflow-hidden" style="height:3px;background-color:rgba(34,211,238,0.10);">
<div class="h-full rounded-full" :style="`width: ${progress}%; background-color: #22D3EE; transition: width 60ms linear;`"></div>
</div>
</div>
</div>
<div x-show="phase === 'transcribing'" class="w-full h-full p-3 overflow-hidden flex flex-col justify-start">
<p class="font-mono leading-relaxed break-words" style="font-size:9px;color:rgba(34,211,238,0.90);">
<span x-text="words.slice(0, n + 1).join(' ')"></span><span class="dictia-blink ml-px" style="color:#22D3EE;"></span>
</p>
</div>
</div>
</template>
{# Mode 2 : Diarisation (conversation Sophie/Marc/Julie) #}
<template x-if="displayMode === 2">
<div class="dictia-mode-fade w-full h-full flex flex-col justify-end gap-1.5 p-2 overflow-hidden" x-data="diaModeData()" x-init="init()">
<template x-for="(msg, i) in visible" :key="`${cycle}-${startIdx + i}`">
<div class="dictia-msg-in flex items-start gap-1.5">
<div class="rounded-full flex items-center justify-center text-[8px] font-bold shrink-0 mt-0.5"
style="width:20px;height:20px;"
:style="`background-color: ${msg.c}28; border: 1px solid ${msg.c}55; color: ${msg.c};`"
x-text="msg.s.charAt(0)"></div>
<div class="rounded-lg px-2 py-1 flex-1"
:style="`background-color: ${msg.c}10; border: 1px solid ${msg.c}22;`">
<p class="text-[8px] font-bold leading-none mb-0.5" :style="`color: ${msg.c};`" x-text="msg.s"></p>
<p class="text-[8px] font-mono leading-snug" style="color:rgba(255,255,255,0.75);" x-text="msg.t"></p>
</div>
</div> </div>
</template> </template>
</div> </div>
</template>
{# Mode 3 : 99+ langues (grille 100 codes staggered 12ms) #}
<template x-if="displayMode === 3">
<div class="dictia-mode-fade w-full h-full overflow-hidden" style="padding:6px 5px;" x-data="langModeData()">
<div class="grid content-start" style="grid-template-columns: repeat(7, 1fr); gap: 3px;">
<template x-for="(lang, i) in LANGS" :key="lang">
<span class="font-mono font-bold text-center rounded dictia-spring"
:style="`font-size:6.5px;line-height:14px;background-color:${LANG_COLORS[i % LANG_COLORS.length]}12;border:1px solid ${LANG_COLORS[i % LANG_COLORS.length]}28;color:${LANG_COLORS[i % LANG_COLORS.length]};animation-delay:${i * 12}ms;`"
x-text="lang"></span>
</template>
</div> </div>
<p class="text-center font-mono font-bold mt-2 dictia-fade-y"
style="font-size:8px;color:#22D3EE;letter-spacing:0.1em;animation-delay:1.4s;">
99+ langues détectées
</p>
</div>
</template>
{# Mode 4 : Exports (7 file icons spring stagger 90ms) #}
<template x-if="displayMode === 4">
<div class="dictia-mode-fade w-full h-full p-3 flex flex-wrap gap-2 items-center justify-center" x-data="expModeData()">
<template x-for="(f, i) in FILE_TYPES" :key="f.ext">
<div class="dictia-spring-y" :style="`animation-delay:${i * 90}ms;`">
<div class="w-10 h-12 rounded-md flex flex-col items-center justify-center gap-0.5 relative"
:style="`background-color:${f.bg}CC;border:1.5px solid ${f.bg};box-shadow:0 4px 12px ${f.bg}40;`">
<div class="absolute top-0 right-0" style="width:0;height:0;border-style:solid;border-width:0 7px 7px 0;border-color:transparent rgba(0,0,0,0.35) transparent transparent;"></div>
<span class="text-[10px] font-black leading-none" :style="`color:${f.fg};`" x-text="f.sym"></span>
<span class="text-[7px] font-mono font-bold leading-none" :style="`color:${f.fg}BB;`" x-text="f.ext"></span>
</div>
</div>
</template>
</div>
</template>
{# Mode 5 : Users (avatars 1→20 multiplication 200ms) #}
<template x-if="displayMode === 5">
<div class="dictia-mode-fade w-full h-full p-2 flex flex-wrap gap-1.5 items-center justify-center content-center" x-data="usersModeData()" x-init="init()">
<template x-for="i in count" :key="`${cycle}-${i}`">
<div class="rounded-full flex items-center justify-center"
:class="i === count ? 'dictia-spring' : ''"
style="width:24px;height:24px;background-color:#A78BFA22;border:1.5px solid #A78BFA66;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:10px;height:10px;color:#A78BFA;" aria-hidden="true">
<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"/>
</svg>
</div>
</template>
</div>
</template>
{# Mode 6 : Share (folders + tags + files staggered) #}
<template x-if="displayMode === 6">
<div class="dictia-mode-fade w-full h-full flex flex-col gap-1.5" style="padding:8px 7px;">
{# Folders #}
<div class="flex gap-1 flex-wrap">
<template x-for="(f, fi) in [{name:'Réunions',color:'#22D3EE',count:12},{name:'Entretiens',color:'#6B9FFF',count:7},{name:'Formations',color:'#34D399',count:24}]" :key="f.name">
<div class="flex items-center gap-1 rounded-md dictia-fade-y"
:style="`background-color:${f.color}18;border:1px solid ${f.color}35;padding:3px 6px;animation-delay:${fi * 100}ms;`">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:7px;height:7px;" :style="`color:${f.color};`" aria-hidden="true">
<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"/>
</svg>
<span style="font-size:6.5px;font-family:monospace;" :style="`color:${f.color};`" x-text="f.name"></span>
<span style="font-size:6px;font-family:monospace;" :style="`color:${f.color}88;`" x-text="f.count"></span>
</div>
</template>
</div>
{# Tags #}
<div class="flex flex-wrap gap-1">
<template x-for="(tag, ti) in ['#RH','#Direction','#Urgent','#Archivé','#Suivi','#Confidentiel','#2024']" :key="tag">
<span class="rounded dictia-spring"
style="font-size:5.5px;padding:2px 4px;background-color:rgba(52,211,153,0.10);border:1px solid rgba(52,211,153,0.25);color:#34D399;font-family:monospace;"
:style="`animation-delay:${350 + ti * 70}ms;`"
x-text="tag"></span>
</template>
</div>
{# Separator #}
<div style="height:1px;background:rgba(255,255,255,0.05);margin:2px 0;"></div>
{# File rows #}
<div class="flex flex-col gap-1">
<template x-for="(file, fi2) in [{name:'CR-Réunion-Jan14',folder:'Réunions',color:'#22D3EE'},{name:'Entretien-Sophie',folder:'Entretiens',color:'#6B9FFF'},{name:'Formation-RGPD',folder:'Formations',color:'#34D399'}]" :key="file.name">
<div class="flex items-center gap-1.5 rounded-lg dictia-fade-x"
:style="`background-color:${file.color}0C;border:1px solid ${file.color}22;padding:4px 6px;animation-delay:${650 + fi2 * 120}ms;`">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:7px;height:7px;" :style="`color:${file.color};`" 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>
<div class="flex-1 min-w-0">
<p class="truncate" style="font-size:6px;font-family:monospace;color:rgba(255,255,255,0.70);" x-text="file.name"></p>
<p style="font-size:5.5px;" :style="`color:${file.color}99;`" x-text="file.folder"></p>
</div>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:6px;height:6px;flex-shrink:0;" :style="`color:${file.color}66;`" aria-hidden="true">
<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/>
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
</svg>
</div>
</template>
</div>
<p class="text-center font-mono mt-auto dictia-fade-y"
style="font-size:6px;color:rgba(52,211,153,0.55);letter-spacing:0.08em;animation-delay:1.2s;">
partage sécurisé · recherche plein texte
</p>
</div>
</template>
{# Mode 0 : IA chat (Mistral 7B local Q&R typing) — visible si user clique sur IA dans grid #}
<template x-if="displayMode === 0">
<div class="dictia-mode-fade w-full h-full flex flex-col overflow-hidden" x-data="iaModeData()" x-init="init()">
<div class="flex items-center justify-center gap-1.5 py-1.5"
style="border-bottom: 1px solid rgba(167,139,250,0.14); background-color: rgba(167,139,250,0.05);">
<div class="rounded-full flex-shrink-0 dictia-dot-pulse" style="width:6px;height:6px;background-color:#34D399;"></div>
<span style="font-size:6px;font-family:monospace;color:rgba(52,211,153,0.85);letter-spacing:0.14em;">MISTRAL 7B · LOCAL</span>
</div>
<div class="flex-1 flex flex-col justify-end gap-1.5 p-2 overflow-hidden">
<template x-for="(msg, mi) in msgs" :key="mi">
<div class="dictia-fade-y" :class="msg.role === 'user' ? 'flex justify-end' : 'flex justify-start'">
<div class="rounded-lg px-2 py-1.5" style="max-width:92%;"
:style="msg.role === 'user' ? 'background-color:rgba(255,255,255,0.07);border:1px solid rgba(255,255,255,0.12);' : 'background-color:rgba(167,139,250,0.12);border:1px solid rgba(167,139,250,0.30);'">
<p class="font-mono leading-relaxed whitespace-pre-line" style="font-size:7.5px;"
:style="msg.role === 'user' ? 'color:rgba(255,255,255,0.65);' : 'color:#C4B5FD;'">
<span x-text="msg.text"></span><span x-show="msg.role === 'bot' && mi === msgs.length - 1" class="dictia-blink ml-px" style="color:#A78BFA;"></span>
</p>
</div>
</div>
</template>
</div>
<div class="flex items-center justify-center gap-1 py-1.5" style="border-top:1px solid rgba(167,139,250,0.08);">
<span style="font-size:5.5px;font-family:monospace;color:rgba(239,68,68,0.55);letter-spacing:0.06em;">0 donnée envoyée au cloud</span>
</div>
</div>
</template>
</div> </div>
{# ── COLONNE DROITE : IA intégrée Mistral 7B + spec list cliquable ── #} {# BOTTOM : 6 feature icons (skip IA index 0) + auto-cycle status #}
<div class="flex flex-col gap-4"> <div class="relative flex flex-col items-center justify-center gap-2 z-10"
style="height: 90px; border-top: 1px solid rgba(255,255,255,0.06);">
{# Card "IA intégrée Mistral 7B LOCAL" #} <div class="flex items-center gap-3.5">
<div class="bg-white border border-brand-border rounded p-5"> <template x-for="i in [1,2,3,4,5,6]" :key="i">
<div class="flex items-center gap-2 mb-2 flex-wrap"> <button type="button" @click="handleManualSelect(i)"
<p class="eyebrow text-brand-navy/70">IA intégrée</p> class="outline-none p-0 cursor-pointer focus-visible:ring-2 focus-visible:ring-white/40 rounded"
<span class="px-2 py-0.5 rounded-full bg-brand-b1/10 text-brand-b1 text-[10px] font-bold uppercase tracking-[0.14em] border border-brand-b1/20">Local</span> style="background:transparent;border:none;"
</div> :aria-pressed="selectedFeature === i ? 'true' : 'false'"
<p class="text-2xl font-black text-brand-navy mb-1">Mistral 7B</p> :aria-label="`Voir : ${FEATURES[i].title}`">
<p class="text-sm text-brand-navy/70 mb-4">Résumé · Points d'action · Q&amp;R</p> <span :style="`color: ${selectedFeature === i ? FEATURES[i].color : 'rgba(255,255,255,0.28)'}; 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.3 : 1}); display: inline-block;`">
<ul class="space-y-2 text-xs text-brand-navy/80" role="list"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:15px;height:15px;" aria-hidden="true" x-html="featureIconPath(i)"></svg>
<li class="flex items-start gap-2"> </span>
{{ icon_check | safe }} </button>
<span>Données hébergées sur VOS serveurs · jamais partagées</span> </template>
</li> </div>
<li class="flex items-start gap-2"> <p class="text-[9px]" style="color:rgba(255,255,255,0.28);"
{{ icon_check | safe }} x-text="isManual ? 'Cliquez pour changer · Auto reprend bientôt' : '● Auto'"></p>
<span>Zéro connexion OpenAI · Google · Microsoft</span> </div>
</li> </div>
<li class="flex items-start gap-2">
{{ icon_check | safe }} {# Feature info card sous le phone (toujours visible, change avec auto-cycle) #}
<span>Inférence hors-ligne · résultats en secondes</span> <div style="min-height:54px;" class="w-full max-w-[260px]">
</li> <div class="rounded-xl px-3 py-2.5"
</ul> :style="`background-color: ${activeColor}0E; border: 1px solid ${activeColor}33; box-shadow: 0 4px 20px ${activeColor}18;`">
</div> <div class="flex items-center gap-1.5 mb-1 flex-wrap">
<span :style="`color: ${activeColor}; flex-shrink: 0;`" aria-hidden="true">
{# Spec list cliquable / hover — déclenche feature dans réacteur #} <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:12px;height:12px;" x-html="featureIconPath(displayMode)"></svg>
<div class="bg-white border border-brand-border rounded p-3"> </span>
<p class="eyebrow text-brand-navy/70 px-2 mb-2 mt-1">Fonctions clés</p> <span class="text-[11px] font-bold text-white" x-text="FEATURES[displayMode].title"></span>
<ul role="list" class="flex flex-col"> <template x-if="FEATURES[displayMode].badge">
<template x-for="feat in features" :key="feat"> <span class="text-[8px] px-1.5 py-0.5 rounded font-mono font-bold"
<li> :style="`background-color: ${activeColor}1A; color: ${activeColor}; border: 1px solid ${activeColor}44;`"
<button type="button" x-text="FEATURES[displayMode].badge"></span>
@mouseenter="setActive(feat)" </template>
@mouseleave="resumeCycle()" </div>
@focus="setActive(feat)" <p class="text-[10px] leading-snug" style="color:rgba(255,255,255,0.52);" x-text="FEATURES[displayMode].subtitle"></p>
@blur="resumeCycle()" </div>
@click="setActive(feat)" </div>
:class="active === feat ? 'is-active' : 'border-l-2 border-transparent text-brand-navy/70'"
class="feature-list-item w-full text-left px-3 py-2 text-sm font-semibold border-l-2 hover:text-brand-navy hover:bg-brand-bg focus-visible:outline-2 focus-visible:outline-brand-b1 focus-visible:outline-offset-2 rounded-none" {# Mobile : pills horizontales scrollables #}
:aria-pressed="active === feat ? 'true' : 'false'"> <div class="lg:hidden w-full overflow-x-auto dictia-hide-scrollbar" style="scrollbar-width:none;">
<span x-text="feat"></span> <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)"
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>
</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>
</button>
</template>
</div>
</div>
</div>
{# ─────────── ZONE RIGHT : IA Mistral premium card + grid 6 features ─────────── #}
<div class="flex flex-col gap-3 w-full lg:w-[300px] flex-shrink-0">
<div class="mb-1">
<p class="text-[9px] font-medium uppercase tracking-[0.22em] mb-1" style="color:rgba(167,139,250,0.85);">Fonctions clés</p>
<p class="font-bold text-base leading-snug text-brand-navy">Fonctionnalité</p>
</div>
{# IA Mistral 7B premium card #}
<div class="relative rounded-xl overflow-hidden"
style="border:1.5px solid rgba(167,139,250,0.28);background-color:rgba(8,12,24,0.85);box-shadow:0 0 28px rgba(167,139,250,0.12);">
{# Ambient purple glow #}
<div class="absolute inset-0 pointer-events-none ia-ambient-glow"
style="border-radius:inherit;background:radial-gradient(ellipse 120% 60% at 15% 50%, rgba(167,139,250,0.18) 0%, transparent 70%);"
aria-hidden="true"></div>
{# Header #}
<div class="relative px-3 pt-3 pb-2 flex items-start gap-2.5">
<div class="relative flex-shrink-0 mt-0.5">
<div class="absolute -inset-2 rounded-full pointer-events-none ia-brain-glow"
style="background:radial-gradient(circle, rgba(167,139,250,0.4) 0%, transparent 70%);"
aria-hidden="true"></div>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:18px;height:18px;color:#A78BFA;position:relative;z-index:1;" aria-hidden="true">
<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"/>
</svg>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-1.5 flex-wrap">
<span class="font-bold text-sm text-white">IA intégrée</span>
<span class="text-[8px] px-1.5 py-0.5 rounded-md font-mono font-bold"
style="background-color:rgba(167,139,250,0.20);color:#C4B5FD;border:1px solid rgba(167,139,250,0.42);letter-spacing:0.06em;">Mistral 7B</span>
<span class="text-[7px] px-1.5 py-0.5 rounded font-mono font-bold ia-local-badge"
style="background-color:rgba(52,211,153,0.12);color:#34D399;border:1px solid rgba(52,211,153,0.30);letter-spacing:0.10em;">LOCAL</span>
</div>
<p class="text-[10px] mt-0.5" style="color:rgba(255,255,255,0.55);">Résumé · Points d'action · Q&amp;R</p>
</div>
</div>
{# Divider #}
<div style="height:1px;background:linear-gradient(90deg, transparent, rgba(167,139,250,0.22), transparent);" aria-hidden="true"></div>
{# Sovereignty bullets #}
<div class="relative px-3 py-2.5 flex flex-col gap-2">
<div class="flex items-start gap-2">
<span style="color:rgba(167,139,250,0.65);flex-shrink:0;margin-top:1px;" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:10px;height:10px;">
<rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/>
<line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/>
</svg>
</span>
<span class="text-[10px] leading-snug" style="color:rgba(255,255,255,0.85);"><strong>Données hébergées sur VOS serveurs</strong> · jamais partagées</span>
</div>
<div class="flex items-start gap-2">
<span style="color:rgba(167,139,250,0.65);flex-shrink:0;margin-top:1px;" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:10px;height:10px;">
<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>
<span class="text-[10px] leading-snug" style="color:rgba(255,255,255,0.65);">Zéro connexion OpenAI · Google · Microsoft</span>
</div>
<div class="flex items-start gap-2">
<span style="color:rgba(167,139,250,0.65);flex-shrink:0;margin-top:1px;" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="currentColor" stroke="none" style="width:10px;height:10px;"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
</span>
<span class="text-[10px] leading-snug" style="color:rgba(255,255,255,0.65);">Inférence hors-ligne · résultats en secondes</span>
</div>
</div>
</div>
{# Feature grid 3 cols × 6 buttons (cliquables, état actif = featureColor border + glow + scale 1.25) #}
<div class="grid grid-cols-3 gap-1.5">
<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 rounded-xl py-2.5 px-1 outline-none transition-all focus-visible:ring-2 focus-visible:ring-white/40"
:style="`border: 1px solid ${selectedFeature === i ? FEATURES[i].color + '55' : 'rgba(255,255,255,0.07)'}; background-color: ${selectedFeature === i ? FEATURES[i].color + '12' : 'rgba(8,12,24,0.85)'}; box-shadow: ${selectedFeature === i ? '0 0 12px ' + FEATURES[i].color + '22' : '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.45)'}; filter: ${selectedFeature === i ? 'drop-shadow(0 0 5px ' + FEATURES[i].color + 'AA)' : 'none'}; transform: scale(${selectedFeature === i ? 1.25 : 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:14px;height:14px;" aria-hidden="true" x-html="featureIconPath(i)"></svg>
</span>
<span class="text-[7.5px] font-medium text-center leading-tight"
:style="`color: ${selectedFeature === i ? 'rgba(255,255,255,0.92)' : 'rgba(255,255,255,0.55)'}; transition: color 0.2s;`"
x-text="FEATURES[i].title"></span>
</button> </button>
</li>
</template> </template>
</ul>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{# Alpine logic — dictiaDashboard + sub-data functions pour les 6 modes #}
<script>
function dictiaDashboard() {
return {
FEATURES: [
{ idx: 0, title: 'IA intégrée', subtitle: "Résumé, actions, Q&R", color: '#A78BFA', badge: 'Mistral 7B' },
{ idx: 1, title: 'Transcription', subtitle: 'Parole → texte en temps réel', color: '#22D3EE', badge: 'Whisper AI' },
{ idx: 2, title: 'Diarisation', subtitle: 'Identification des locuteurs', color: '#6B9FFF', badge: null },
{ idx: 3, title: '99+ langues', subtitle: 'Détection automatique', color: '#22D3EE', badge: null },
{ idx: 4, title: 'Exports', subtitle: 'DOCX, SRT, JSON, PDF', color: '#6B9FFF', badge: null },
{ idx: 5, title: 'Utilisateurs illimités', subtitle: 'Toute votre équipe', color: '#A78BFA', badge: 'Illimité' },
{ idx: 6, title: 'Partage & Classement', subtitle: 'Dossiers, tags, recherche', color: '#34D399', badge: null }
],
selectedFeature: 1,
isManual: false,
autoCycleTimer: null,
manualResetTimer: null,
get displayMode() { return this.selectedFeature; },
get activeColor() { return this.FEATURES[this.displayMode].color; },
init() {
if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
this.startAutoCycle();
},
startAutoCycle() {
if (this.autoCycleTimer) clearInterval(this.autoCycleTimer);
this.autoCycleTimer = setInterval(() => {
this.selectedFeature = this.selectedFeature < this.FEATURES.length - 1 ? this.selectedFeature + 1 : 1;
}, 900);
},
handleManualSelect(i) {
this.selectedFeature = i;
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);
},
featureIconPath(i) {
const paths = {
0: '<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"/>',
1: '<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"/><line x1="8" y1="23" x2="16" y2="23"/>',
2: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
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"/>'
};
return paths[i] || '';
}
};
}
/* Mode 1 — Transcription : upload bar (60ms × 20 steps) puis words (155ms each) */
function trModeData() {
return {
phase: 'upload', progress: 0, n: 0,
words: "La réunion du lundi 12 janvier débutera à 9h. Ordre du jour : budget Q4 et les objectifs du trimestre. Points d'action à définir en fin de séance.".split(' '),
_timers: [],
init() {
this._cleanup();
this.phase = 'upload'; this.progress = 0; this.n = 0;
const iv1 = setInterval(() => {
this.progress = Math.min(this.progress + 5, 100);
if (this.progress >= 100) {
clearInterval(iv1);
const t1 = setTimeout(() => {
this.phase = 'transcribing'; this.n = 0;
const iv2 = setInterval(() => {
if (this.n >= this.words.length - 1) {
clearInterval(iv2);
const t2 = setTimeout(() => this.init(), 900);
this._timers.push(t2);
} else this.n++;
}, 155);
this._timers.push(iv2);
}, 450);
this._timers.push(t1);
}
}, 60);
this._timers.push(iv1);
},
_cleanup() { this._timers.forEach(t => { clearInterval(t); clearTimeout(t); }); this._timers = []; }
};
}
/* Mode 2 — Diarisation : 6 messages staggered 1600ms */
function diaModeData() {
return {
CONVO: [
{ s: "Sophie", c: "#22D3EE", t: "La réunion commence à 9h." },
{ s: "Marc", c: "#6B9FFF", t: "J'ai les chiffres du Q4 ici." },
{ s: "Julie", c: "#F59E0B", t: "Je propose reporter la démo." },
{ s: "Sophie", c: "#22D3EE", t: "Accord — on vote là-dessus ?" },
{ s: "Marc", c: "#6B9FFF", t: "Approuvé à l'unanimité." },
{ s: "Julie", c: "#F59E0B", t: "Action : CR envoyé ce soir." }
],
shownCount: 1, cycle: 0, _iv: null,
get startIdx() { return Math.max(0, this.shownCount - 4); },
get visible() { return this.CONVO.slice(this.startIdx, this.shownCount); },
init() {
if (this._iv) clearInterval(this._iv);
this.shownCount = 1;
this._iv = setInterval(() => {
if (this.shownCount >= this.CONVO.length) {
clearInterval(this._iv);
setTimeout(() => { this.cycle++; this.init(); }, 800);
} else this.shownCount++;
}, 1600);
}
};
}
/* Mode 3 — Langues : grille 100 codes (CSS-only stagger 12ms) */
function langModeData() {
return {
LANG_COLORS: ['#22D3EE','#6B9FFF','#A78BFA','#34D399','#F59E0B','#22D3EE','#6B9FFF','#A78BFA'],
LANGS: [
'FR','EN','ES','DE','PT','IT','NL','PL','ZH','JA',
'KO','AR','RU','HI','TR','VI','TH','SV','DA','NO',
'FI','CS','RO','HU','EL','HE','UK','BG','FA','ID',
'MS','NB','BN','UR','SW','TL','SK','HR','SR','SL',
'LT','LV','ET','IS','MT','GL','CA','EU','CY','GA',
'AF','MK','BE','LB','MR','TA','TE','KN','ML','PA',
'GU','SI','MY','KM','LO','KA','AZ','KK','UZ','TK',
'MN','NE','PS','AM','SQ','HY','MG','YO','SO','ZU',
'NY','XH','IG','HA','SN','ST','CEB','HT','JW','KY',
'TG','BS','OC','BR','FO','LG','YI','TT','SD','WA'
]
};
}
/* Mode 4 — Exports : 7 file types staggered 90ms (CSS-only) */
function expModeData() {
return {
FILE_TYPES: [
{ ext: 'DOCX', bg: '#1E6FD9', fg: '#fff', sym: 'W' },
{ ext: 'PDF', bg: '#D93E1E', fg: '#fff', sym: 'PDF' },
{ ext: 'SRT', bg: '#7C3AED', fg: '#fff', sym: 'CC' },
{ ext: 'VTT', bg: '#5B21B6', fg: '#DDD6FE', sym: 'CC' },
{ ext: 'TXT', bg: '#374151', fg: '#9CA3AF', sym: '≡' },
{ ext: 'JSON', bg: '#065F46', fg: '#34D399', sym: '{}' },
{ ext: 'MD', bg: '#1C3A5E', fg: '#6B9FFF', sym: '#' }
]
};
}
/* Mode 5 — Users : multiplication 1→20 (200ms each, loop after 700ms) */
function usersModeData() {
return {
MAX: 20, count: 1, cycle: 0, _iv: null,
init() {
if (this._iv) clearInterval(this._iv);
this.count = 1;
this._iv = setInterval(() => {
if (this.count >= this.MAX) {
clearInterval(this._iv);
setTimeout(() => { this.cycle++; this.init(); }, 700);
} else this.count++;
}, 200);
}
};
}
/* Mode 0 — IA chat : Q&R typed char-by-char (28ms/char) */
function iaModeData() {
return {
msgs: [], cycle: 0, _timers: [],
CHAT: [
{ role: 'user', text: "Quels sont les points d'action ?" },
{ role: 'bot', text: "→ Envoyer CR avant 17h\n→ Contacter client vendredi\n→ Réviser budget avec Luc" },
{ role: 'user', text: "Qui a ouvert la réunion ?" },
{ role: 'bot', text: "Sophie a ouvert la séance à 9h00." }
],
init() {
this._cleanup();
this.msgs = [];
const t0 = setTimeout(() => { this.msgs = [{ role: 'user', text: this.CHAT[0].text }]; }, 300);
this._timers.push(t0);
const t1 = setTimeout(() => {
const bot1 = this.CHAT[1].text; let i = 0;
const iv = setInterval(() => {
i++;
this.msgs = [
{ role: 'user', text: this.CHAT[0].text },
{ role: 'bot', text: bot1.slice(0, i) }
];
if (i >= bot1.length) {
clearInterval(iv);
const t2 = setTimeout(() => {
this.msgs = [
{ role: 'user', text: this.CHAT[0].text },
{ role: 'bot', text: this.CHAT[1].text },
{ role: 'user', text: this.CHAT[2].text }
];
const t3 = setTimeout(() => {
const bot2 = this.CHAT[3].text; let j = 0;
const iv2 = setInterval(() => {
j++;
this.msgs = [
{ role: 'user', text: this.CHAT[0].text },
{ role: 'bot', text: this.CHAT[1].text },
{ role: 'user', text: this.CHAT[2].text },
{ role: 'bot', text: bot2.slice(0, j) }
];
if (j >= bot2.length) {
clearInterval(iv2);
const t4 = setTimeout(() => { this.cycle++; this.init(); }, 1200);
this._timers.push(t4);
}
}, 40);
this._timers.push(iv2);
}, 500);
this._timers.push(t3);
}, 700);
this._timers.push(t2);
}
}, 28);
this._timers.push(iv);
}, 900);
this._timers.push(t1);
},
_cleanup() { this._timers.forEach(t => { clearInterval(t); clearTimeout(t); }); this._timers = []; }
};
}
</script>
</section> </section>
{# ===== INTÉGRATIONS ===== #} {# ===== INTÉGRATIONS ===== #}

View File

@@ -132,10 +132,11 @@ def test_fonctionnalites_renders_6_bento_cards():
def test_fonctionnalites_how_it_works_reactor_section(): def test_fonctionnalites_how_it_works_reactor_section():
"""New 'Comment ça marche' interactive reactor section (post-6-features, pre-integrations). """'Comment ça marche' interactive section — fidèle reproduction du composant
DashboardHolographique (Website-Sanity/dictai-narrative.tsx).
Validates structure (heading + reactor + spec list + Mistral card), 6 cycling features, Validates structure (phone container + 6 modes + IA Mistral card + 6 feature grid),
canonical content, and a11y signals (aria-labelledby + aria-live status panel). auto-cycle + manual click logic, canonical content, and a11y signals.
""" """
client = app.test_client() client = app.test_client()
body = client.get('/fonctionnalites').data.decode('utf-8') body = client.get('/fonctionnalites').data.decode('utf-8')
@@ -147,37 +148,58 @@ def test_fonctionnalites_how_it_works_reactor_section():
assert 'en temps réel' in body assert 'en temps réel' in body
assert 'Survolez une fonctionnalité pour voir la machine en action' in body assert 'Survolez une fonctionnalité pour voir la machine en action' in body
# 6 cycling features (canonical names) # 6 cycling features (canonical names — appears in FEATURES JS array)
for feat in ['Transcription', 'Diarisation', '99+ langues', 'Exports', for feat in ['Transcription', 'Diarisation', '99+ langues', 'Exports',
'Utilisateurs illimités', 'Partage &amp; Classement']: 'Utilisateurs illimités']:
assert feat in body, f"Missing cycling feature: {feat}" assert feat in body, f"Missing cycling feature: {feat}"
# Partage & Classement (avec & littéral dans le JS)
assert "'Partage & Classement'" in body, "Missing Partage & Classement feature"
# Reactor visual: rings, orbits, wordmark, Auto badge # Phone container : Alpine root + features + Mic pulsing + DictIA logo
assert 'reactor-ring' in body and 'ring-outer' in body and 'ring-mid' in body and 'ring-inner' in body assert 'dictiaDashboard()' in body, "Missing dictiaDashboard Alpine root"
assert 'orbit orbit-1' in body and 'orbit orbit-8' in body, "Missing 8 orbital particles" assert 'dictia-mic-pulse' in body, "Missing pulsing Mic animation class"
assert '>DictIA<' in body, "Missing reactor centre wordmark" assert 'dictia-logo-nom.png' in body, "Missing DictIA logo image"
# Mistral 7B IA intégrée card with 3 canonical bullets # 6 modes uniques (chacun a son sub-data function ou sa signature unique)
assert 'trModeData()' in body, "Missing Mode 1 (Transcription) data function"
assert 'reunion-jan14.mp3' in body, "Missing transcription upload filename"
assert 'diaModeData()' in body, "Missing Mode 2 (Diarisation) data function"
assert 'Sophie' in body and 'Marc' in body and 'Julie' in body, "Missing diarisation speakers"
assert 'langModeData()' in body, "Missing Mode 3 (Langues) data function"
assert '99+ langues détectées' in body, "Missing langues subtitle"
assert 'expModeData()' in body, "Missing Mode 4 (Exports) data function"
assert 'usersModeData()' in body, "Missing Mode 5 (Users) data function"
assert 'iaModeData()' in body, "Missing Mode 0 (IA chat) data function"
assert 'MISTRAL 7B · LOCAL' in body, "Missing IA chat header label"
# Mode 6 (Share) : folders + tags + files
assert 'CR-Réunion-Jan14' in body, "Missing share file name"
assert '#Confidentiel' in body, "Missing share tag"
assert 'partage sécurisé' in body, "Missing share footer text"
# IA Mistral 7B premium card avec 3 bullets souveraineté
assert 'Mistral 7B' in body assert 'Mistral 7B' in body
assert 'IA intégrée' in body assert 'IA intégrée' in body
assert 'Données hébergées sur VOS serveurs' in body assert 'Données hébergées sur VOS serveurs' in body
assert 'Zéro connexion OpenAI' in body assert 'Zéro connexion OpenAI' in body
assert 'Inférence hors-ligne' in body assert 'Inférence hors-ligne' in body
assert 'ia-ambient-glow' in body, "Missing IA ambient glow animation"
assert 'ia-brain-glow' in body, "Missing IA brain glow animation"
assert 'ia-local-badge' in body, "Missing LOCAL badge pulsing animation"
# Alpine reactor data + auto-cycle + hover/focus stop logic # Auto-cycle + manual select logic
assert "active: \"Transcription\"" in body assert 'startAutoCycle' in body, "Missing auto-cycle function"
assert 'isHovered' in body assert 'handleManualSelect' in body, "Missing manual select handler"
assert 'setActive(feat)' in body assert '900' in body, "Missing 900ms cycle interval"
assert 'resumeCycle()' in body assert '4500' in body, "Missing 4500ms manual reset timer"
# Hover, focus + blur listeners on each list item # Auto / Manual status text
assert '@mouseenter="setActive(feat)"' in body assert 'Auto' in body, "Missing Auto status indicator"
assert '@mouseleave="resumeCycle()"' in body assert 'Auto reprend bientôt' in body, "Missing manual mode hint"
assert '@focus="setActive(feat)"' in body
assert '@blur="resumeCycle()"' in body
# Accessibility: aria-live status panel, prefers-reduced-motion guard # Accessibility: aria-live status panel, prefers-reduced-motion guard
assert 'aria-live="polite"' in body assert 'aria-live="polite"' in body
assert 'prefers-reduced-motion' in body, "Reduced-motion guard missing" assert 'prefers-reduced-motion' in body, "Reduced-motion guard missing"
assert 'aria-pressed' in body, "Missing aria-pressed on feature buttons"
def test_fonctionnalites_export_formats_section(): def test_fonctionnalites_export_formats_section():