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>
</section>
{# ===== COMMENT ÇA MARCHE — RÉACTEUR INTERACTIF =====
Sous-partie visuelle de la section Fonctionnalités (sub-nav reste à 4 ancres).
Reproduit dictai-narrative.tsx (Website-Sanity) en CSS pur + Alpine.js.
{# ===== 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.
#}
<section class="bg-brand-bg py-20" aria-labelledby="how-it-works-title">
<style>
/* Anneaux concentriques rotatifs (réacteur DictIA) */
.reactor-ring { position: absolute; border-radius: 9999px; pointer-events: none; }
.ring-outer { width: 420px; height: 420px; border: 1px solid rgba(37,99,235,0.25); animation: ring-rotate-cw 30s linear infinite; }
.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); } }
/* Mic pulsing en haut du phone */
.dictia-mic-pulse { animation: dmic-pulse 1.2s ease-in-out infinite; display: inline-block; }
@keyframes dmic-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.18); } }
/* Particules orbitales — 8 dots qui tournent autour du wordmark */
.reactor-orbit-host { position: absolute; left: 50%; top: 50%; width: 1px; height: 1px; }
.orbit { position: absolute; left: 0; top: 0; border-radius: 9999px; transform-origin: 0 0; }
.orbit-1 { width: 8px; height: 8px; background: #2563eb; animation: orbit-spin 12s linear infinite; --r: 210px; }
.orbit-2 { width: 6px; height: 6px; background: #06b6d4; animation: orbit-spin 18s linear infinite reverse; --r: 150px; }
.orbit-3 { width: 6px; height: 6px; background: #c026d3; animation: orbit-spin 9s linear infinite; --r: 90px; }
.orbit-4 { width: 5px; height: 5px; background: #06b6d4; animation: orbit-spin 14s linear infinite reverse; --r: 210px; animation-delay: -3.5s; }
.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); }
}
/* IA card ambient glow + brain glow + LOCAL pulse */
.ia-ambient-glow { animation: ia-glow 3s ease-in-out infinite; }
@keyframes ia-glow { 0%, 100% { opacity: 0.4; } 50% { opacity: 0.75; } }
.ia-brain-glow { animation: ia-brain 2.2s ease-in-out infinite; }
@keyframes ia-brain { 0%, 100% { opacity: 0.2; } 50% { opacity: 0.55; } }
.ia-local-badge { animation: ia-local 2.5s ease-in-out infinite; }
@keyframes ia-local { 0%, 100% { opacity: 0.75; } 50% { opacity: 1; } }
/* Glow pulsant sous le wordmark central */
@keyframes reactor-glow-pulse {
0%, 100% { opacity: 0.55; transform: scale(1); }
50% { opacity: 0.85; transform: scale(1.08); }
}
.reactor-glow { animation: reactor-glow-pulse 3.2s ease-in-out infinite; }
/* Curseur clignotant (transcription, IA chat) */
.dictia-blink { animation: dictia-cursor-blink 0.6s steps(2, end) infinite; }
@keyframes dictia-cursor-blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }
/* Spec list item état actif */
.feature-list-item { transition: background-color 200ms ease-out, color 150ms ease-out, border-color 150ms ease-out; }
.feature-list-item.is-active { background: rgba(37,99,235,0.10); border-left-color: #2563eb; color: #060d1a; }
/* Dot pulse (MISTRAL 7B · LOCAL header) */
.dictia-dot-pulse { animation: dictia-dot-pulse-anim 1.8s ease-in-out infinite; }
@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) {
.ring-outer, .ring-mid, .ring-inner,
.orbit, .reactor-glow { 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; }
.dictia-mic-pulse, .ia-ambient-glow, .ia-brain-glow, .ia-local-badge,
.dictia-blink, .dictia-dot-pulse, .dictia-msg-in, .dictia-spring,
.dictia-spring-y, .dictia-fade-x, .dictia-fade-y, .dictia-mode-fade { animation: none; }
}
</style>
@@ -323,145 +322,604 @@
</p>
</div>
<div x-data='{
features: ["Transcription", "Diarisation", "99+ langues", "Exports", "Utilisateurs illimités", "Partage &amp; Classement"],
details: {
"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">
{# ── DASHBOARD HOLOGRAPHIQUE — 2 colonnes (phone center + IA right) ── #}
<div x-data="dictiaDashboard()" x-init="init()"
class="w-full flex flex-col lg:flex-row lg:justify-center lg:items-start gap-6 lg:gap-10">
{# ── COLONNE GAUCHE : Réacteur central holographique ── #}
<div class="relative bg-brand-navy p-8 rounded overflow-hidden min-h-[480px] flex flex-col items-center justify-center">
{# 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>
{# ─────────── ZONE CENTER : Phone container ─────────── #}
<div class="flex flex-col items-center gap-3 lg:w-[280px] flex-shrink-0">
{# 3 anneaux concentriques rotatifs #}
<div class="absolute inset-0 flex items-center justify-center" aria-hidden="true">
<div class="reactor-ring ring-outer"></div>
<div class="reactor-ring ring-mid"></div>
<div class="reactor-ring ring-inner"></div>
{# 8 particules orbitales #}
<div class="reactor-orbit-host">
<span class="orbit orbit-1"></span>
<span class="orbit orbit-2"></span>
<span class="orbit orbit-3"></span>
<span class="orbit orbit-4"></span>
<span class="orbit orbit-5"></span>
<span class="orbit orbit-6"></span>
<span class="orbit orbit-7"></span>
<span class="orbit orbit-8"></span>
</div>
{# Phone (border-radius 44px, color tinting selon feature active) #}
<div class="w-full max-w-[260px] mx-auto flex flex-col overflow-hidden relative"
: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;`">
{# Ambient color tint overlay #}
<div class="absolute inset-0 pointer-events-none"
:style="`border-radius: 44px; background-color: ${activeColor}06; transition: background-color 0.4s;`"
aria-hidden="true"></div>
{# TOP : Logo + Mic pulsing #}
<div class="flex flex-col items-center justify-center gap-2 relative z-10"
style="height: 96px; border-bottom: 1px solid rgba(255,255,255,0.06);">
<img src="/static/images/dictia-logo-nom.png" alt="DictIA"
style="width: 80px; height: 24px; object-fit: contain; opacity: 0.75;">
<span class="dictia-mic-pulse" :style="`color: ${activeColor};`" aria-hidden="true">
<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>
{# Auto badge — top right #}
<div class="absolute top-4 right-4 inline-flex items-center gap-1.5 text-xs text-white/80 z-10 font-mono">
<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"
{# MIDDLE : 6 modes (1=Transcription, 2=Diarisation, 3=Langues, 4=Exports, 5=Users, 6=Share, 0=IA chat) #}
<div class="flex-1 relative z-10" style="min-height: 220px;"
role="status" aria-live="polite" aria-atomic="true">
<template x-for="feat in features" :key="feat">
<div x-show="active === feat"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-1"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0">
<p class="text-[11px] uppercase tracking-[0.18em] text-white/60 mb-1 font-bold" x-text="feat"></p>
<p class="text-sm text-white font-semibold font-mono" x-text="details[feat].tag"></p>
<p class="text-xs text-white/65 mt-1" x-text="details[feat].desc"></p>
{# Mode 1 : Transcription (upload bar + words appearing) #}
<template x-if="displayMode === 1">
<div class="dictia-mode-fade w-full h-full" x-data="trModeData()" x-init="init()">
<div x-show="phase === 'upload'" class="w-full h-full flex flex-col items-center justify-center gap-3 px-4">
<div class="flex flex-col items-center gap-1.5">
<div class="w-10 h-12 rounded-md flex flex-col items-center justify-center relative"
style="background-color: rgba(34,211,238,0.12); border: 1.5px solid rgba(34,211,238,0.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.40) transparent transparent;"></div>
<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">
<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>
</template>
</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>
<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>
{# ── COLONNE DROITE : IA intégrée Mistral 7B + spec list cliquable ── #}
<div class="flex flex-col gap-4">
{# Card "IA intégrée Mistral 7B LOCAL" #}
<div class="bg-white border border-brand-border rounded p-5">
<div class="flex items-center gap-2 mb-2 flex-wrap">
<p class="eyebrow text-brand-navy/70">IA intégrée</p>
<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>
</div>
<p class="text-2xl font-black text-brand-navy mb-1">Mistral 7B</p>
<p class="text-sm text-brand-navy/70 mb-4">Résumé · Points d'action · Q&amp;R</p>
<ul class="space-y-2 text-xs text-brand-navy/80" role="list">
<li class="flex items-start gap-2">
{{ icon_check | safe }}
<span>Données hébergées sur VOS serveurs · jamais partagées</span>
</li>
<li class="flex items-start gap-2">
{{ icon_check | safe }}
<span>Zéro connexion OpenAI · Google · Microsoft</span>
</li>
<li class="flex items-start gap-2">
{{ icon_check | safe }}
<span>Inférence hors-ligne · résultats en secondes</span>
</li>
</ul>
</div>
{# Spec list cliquable / hover — déclenche feature dans réacteur #}
<div class="bg-white border border-brand-border rounded p-3">
<p class="eyebrow text-brand-navy/70 px-2 mb-2 mt-1">Fonctions clés</p>
<ul role="list" class="flex flex-col">
<template x-for="feat in features" :key="feat">
<li>
<button type="button"
@mouseenter="setActive(feat)"
@mouseleave="resumeCycle()"
@focus="setActive(feat)"
@blur="resumeCycle()"
@click="setActive(feat)"
: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"
:aria-pressed="active === feat ? 'true' : 'false'">
<span x-text="feat"></span>
{# BOTTOM : 6 feature icons (skip IA index 0) + auto-cycle status #}
<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);">
<div class="flex items-center gap-3.5">
<template x-for="i in [1,2,3,4,5,6]" :key="i">
<button type="button" @click="handleManualSelect(i)"
class="outline-none p-0 cursor-pointer focus-visible:ring-2 focus-visible:ring-white/40 rounded"
style="background:transparent;border:none;"
: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.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;`">
<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>
</span>
</button>
</template>
</div>
<p class="text-[9px]" style="color:rgba(255,255,255,0.28);"
x-text="isManual ? 'Cliquez pour changer · Auto reprend bientôt' : '● Auto'"></p>
</div>
</div>
{# Feature info card sous le phone (toujours visible, change avec auto-cycle) #}
<div style="min-height:54px;" class="w-full max-w-[260px]">
<div class="rounded-xl px-3 py-2.5"
:style="`background-color: ${activeColor}0E; border: 1px solid ${activeColor}33; box-shadow: 0 4px 20px ${activeColor}18;`">
<div class="flex items-center gap-1.5 mb-1 flex-wrap">
<span :style="`color: ${activeColor}; flex-shrink: 0;`" aria-hidden="true">
<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>
</span>
<span class="text-[11px] font-bold text-white" x-text="FEATURES[displayMode].title"></span>
<template x-if="FEATURES[displayMode].badge">
<span class="text-[8px] px-1.5 py-0.5 rounded font-mono font-bold"
:style="`background-color: ${activeColor}1A; color: ${activeColor}; border: 1px solid ${activeColor}44;`"
x-text="FEATURES[displayMode].badge"></span>
</template>
</div>
<p class="text-[10px] leading-snug" style="color:rgba(255,255,255,0.52);" x-text="FEATURES[displayMode].subtitle"></p>
</div>
</div>
{# Mobile : pills 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)"
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>
</li>
</template>
</ul>
</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>
{# ===== INTÉGRATIONS ===== #}

View File

@@ -132,10 +132,11 @@ def test_fonctionnalites_renders_6_bento_cards():
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,
canonical content, and a11y signals (aria-labelledby + aria-live status panel).
Validates structure (phone container + 6 modes + IA Mistral card + 6 feature grid),
auto-cycle + manual click logic, canonical content, and a11y signals.
"""
client = app.test_client()
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 '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',
'Utilisateurs illimités', 'Partage &amp; Classement']:
'Utilisateurs illimités']:
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
assert 'reactor-ring' in body and 'ring-outer' in body and 'ring-mid' in body and 'ring-inner' in body
assert 'orbit orbit-1' in body and 'orbit orbit-8' in body, "Missing 8 orbital particles"
assert '>DictIA<' in body, "Missing reactor centre wordmark"
# Phone container : Alpine root + features + Mic pulsing + DictIA logo
assert 'dictiaDashboard()' in body, "Missing dictiaDashboard Alpine root"
assert 'dictia-mic-pulse' in body, "Missing pulsing Mic animation class"
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 'IA intégrée' in body
assert 'Données hébergées sur VOS serveurs' in body
assert 'Zéro connexion OpenAI' 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
assert "active: \"Transcription\"" in body
assert 'isHovered' in body
assert 'setActive(feat)' in body
assert 'resumeCycle()' in body
# Hover, focus + blur listeners on each list item
assert '@mouseenter="setActive(feat)"' in body
assert '@mouseleave="resumeCycle()"' in body
assert '@focus="setActive(feat)"' in body
assert '@blur="resumeCycle()"' in body
# 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 '4500' in body, "Missing 4500ms manual reset timer"
# Auto / Manual status text
assert 'Auto' in body, "Missing Auto status indicator"
assert 'Auto reprend bientôt' in body, "Missing manual mode hint"
# Accessibility: aria-live status panel, prefers-reduced-motion guard
assert 'aria-live="polite"' in body
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():