feat: Initialize AI Studio project with React and Vite

Sets up a new AI Studio project using React, Vite, and TypeScript. Includes basic styling, layout for a strategic simulator, and essential dependencies.
This commit is contained in:
Danny Stocker 2025-11-21 07:32:58 +01:00
parent 6b3baccc94
commit db2cc93fab
8 changed files with 699 additions and 8 deletions

24
.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View file

@ -1,11 +1,20 @@
<div align="center"> <div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" /> <img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
<h1>Built with AI Studio</h2>
<p>The fastest path from prompt to production with Gemini.</p>
<a href="https://aistudio.google.com/apps">Start building</a>
</div> </div>
# Run and deploy your AI Studio app
This contains everything you need to run your app locally.
View your app in AI Studio: https://ai.studio/apps/drive/1BMdgG2F3XgqJLpiOrmttuhBBTUR9rQVq
## Run Locally
**Prerequisites:** Node.js
1. Install dependencies:
`npm install`
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
3. Run the app:
`npm run dev`

55
index.html Normal file
View file

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#0f172a" />
<title>Gedimat Lunel - Simulateur Stratégique</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<style>
body {
margin: 0;
padding: 0;
background-color: #0f172a;
color: #f8fafc;
font-family: 'Inter', sans-serif;
overflow: hidden;
}
* {
box-sizing: border-box;
}
/* Animations */
@keyframes slideInRight {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes popIn {
from { opacity: 0; transform: translate(-50%, -40%) scale(0.9); }
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
@keyframes ping {
0% { transform: scale(1); opacity: 1; }
100% { transform: scale(2); opacity: 0; }
}
</style>
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@18.2.0",
"react-dom": "https://esm.sh/react-dom@18.2.0",
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client",
"lucide-react": "https://esm.sh/lucide-react@0.263.1",
"react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/",
"react/": "https://aistudiocdn.com/react@^19.2.0/"
}
}
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/index.tsx"></script>
</body>
</html>

524
index.tsx Normal file
View file

@ -0,0 +1,524 @@
import React, { useState, useEffect, useRef } from 'react';
import { createRoot } from 'react-dom/client';
import { AlertTriangle, CheckCircle, TrendingUp, TrendingDown, Truck, MessageCircle, Clock, Smartphone } from 'lucide-react';
/**
* GEDIMAT LUNEL NEGOCE - SIMULATEUR STRATÉGIQUE V2.0 (CINEMA MODE)
* Version Pure JS pour compatibilité maximale
*/
// --- CONSTANTES & STYLES ---
const COLORS = {
bg: "#0f172a", // Slate 900
card: "#1e293b", // Slate 800
success: "#10b981", // Emerald 500
danger: "#ef4444", // Red 500
warning: "#f59e0b", // Amber 500
primary: "#3b82f6", // Blue 500
text: "#f8fafc",
textMuted: "#94a3b8"
};
const STYLES = {
container: {
fontFamily: '"Inter", "Segoe UI", sans-serif',
backgroundColor: COLORS.bg,
color: COLORS.text,
height: '100vh',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
position: 'relative',
} as React.CSSProperties,
header: {
padding: '20px',
backgroundColor: 'rgba(15, 23, 42, 0.9)',
borderBottom: `1px solid ${COLORS.card}`,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
zIndex: 10,
height: '80px'
} as React.CSSProperties,
metricBox: {
padding: '8px 16px',
borderRadius: '8px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
minWidth: '120px',
border: '1px solid rgba(255,255,255,0.05)'
} as React.CSSProperties,
mainStage: {
flex: 1,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
position: 'relative',
padding: '20px'
} as React.CSSProperties,
card: {
backgroundColor: COLORS.card,
borderRadius: '16px',
padding: '40px',
width: '650px',
maxWidth: '100%',
boxShadow: '0 20px 50px rgba(0,0,0,0.5)',
position: 'relative',
border: `1px solid ${COLORS.textMuted}20`
} as React.CSSProperties,
button: (variant: string, isActive = false): React.CSSProperties => ({
padding: '16px 24px',
borderRadius: '12px',
fontWeight: 600,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '12px',
width: '100%',
transition: 'all 0.2s',
backgroundColor: isActive ? (variant === 'success' ? `${COLORS.success}30` : `${COLORS.danger}30`) : 'rgba(255,255,255,0.03)',
color: isActive ? (variant === 'success' ? COLORS.success : COLORS.danger) : COLORS.textMuted,
border: `1px solid ${isActive ? (variant === 'success' ? COLORS.success : COLORS.danger) : 'rgba(255,255,255,0.1)'}`,
transform: isActive ? 'scale(1.02)' : 'scale(1)'
})
};
// --- COMPOSANTS UI ---
const StatCounter = ({ label, value, type }: { label: string; value: number; type: string }) => (
<div style={{ ...STYLES.metricBox, backgroundColor: type === 'cost' ? `${COLORS.danger}10` : `${COLORS.success}10` }}>
<span style={{ fontSize: '11px', color: COLORS.textMuted, textTransform: 'uppercase', letterSpacing: '1px' }}>{label}</span>
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
{type === 'cost' ? <TrendingDown size={16} color={COLORS.danger} /> : <TrendingUp size={16} color={COLORS.success} />}
<span style={{ fontSize: '20px', fontWeight: 'bold', color: type === 'cost' ? COLORS.danger : COLORS.success }}>
{value.toLocaleString('fr-FR', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 })}
</span>
</div>
</div>
);
const VirtualCursor = ({ x, y, clicking }: { x: number; y: number; clicking: boolean }) => (
<div style={{
position: 'fixed',
top: 0, left: 0,
transform: `translate(${x}px, ${y}px)`,
transition: 'transform 0.6s cubic-bezier(0.22, 1, 0.36, 1)',
pointerEvents: 'none',
zIndex: 9999
}}>
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" style={{ filter: 'drop-shadow(2px 4px 6px rgba(0,0,0,0.5))' }}>
<path d="M3 3L10.07 19.97L12.58 12.58L19.97 10.07L3 3Z" fill="white" stroke="black" strokeWidth="2"/>
</svg>
{clicking && (
<div style={{
position: 'absolute', top: -10, left: -10, width: 40, height: 40,
borderRadius: '50%', border: `2px solid ${COLORS.primary}`,
animation: 'ping 0.4s cubic-bezier(0, 0, 0.2, 1)'
}} />
)}
</div>
);
const FlashCard = ({ title, message, type }: { title: string; message: string; type: string }) => (
<div style={{
position: 'absolute',
top: '50%', left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: type === 'bad' ? '#7f1d1d' : '#064e3b',
color: 'white',
padding: '30px',
borderRadius: '16px',
textAlign: 'center',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.8)',
zIndex: 100,
animation: 'popIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275)',
minWidth: '350px',
border: `1px solid ${type === 'bad' ? COLORS.danger : COLORS.success}`
}}>
{type === 'bad' ? <AlertTriangle size={48} style={{ marginBottom: 15 }} /> : <CheckCircle size={48} style={{ marginBottom: 15 }} />}
<h2 style={{ margin: '0 0 10px 0', fontSize: '24px', fontWeight: 'bold' }}>{title}</h2>
<p style={{ margin: 0, fontSize: '16px', opacity: 0.9, lineHeight: '1.5' }}>{message}</p>
</div>
);
const PhoneMockup = ({ show, type }: { show: boolean; type: string }) => {
if (!show) return null;
return (
<div style={{
position: 'absolute',
right: -220, top: '50%',
transform: 'translateY(-50%)',
width: 260, height: 480,
backgroundColor: '#fff',
borderRadius: 30,
border: '8px solid #1e293b',
boxShadow: '0 20px 50px rgba(0,0,0,0.5)',
overflow: 'hidden',
display: 'flex', flexDirection: 'column',
zIndex: 50,
animation: 'slideInRight 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275)'
}}>
{/* Notch */}
<div style={{ height: 25, backgroundColor: '#1e293b', width: '50%', alignSelf: 'center', borderBottomLeftRadius: 10, borderBottomRightRadius: 10 }}></div>
{/* Header */}
<div style={{ backgroundColor: type === 'whatsapp' ? '#075e54' : '#f5f5f5', padding: '10px 15px', color: type === 'whatsapp' ? '#fff' : '#333', borderBottom: '1px solid #ddd' }}>
<div style={{ fontSize: 10, opacity: 0.8 }}>16:02</div>
<div style={{ fontSize: 14, fontWeight: 'bold', display: 'flex', alignItems: 'center', gap: 5 }}>
{type === 'whatsapp' && <div style={{width: 24, height: 24, borderRadius: '50%', backgroundColor: '#25D366'}}></div>}
{type === 'whatsapp' ? 'Chantier DUPONT' : 'Messagerie'}
</div>
</div>
{/* Body */}
<div style={{ flex: 1, padding: 15, backgroundColor: type === 'whatsapp' ? '#ece5dd' : '#fff', display: 'flex', flexDirection: 'column', gap: 10, backgroundImage: type === 'whatsapp' ? 'url("https://user-images.githubusercontent.com/15075759/28719144-86dc0f70-73b1-11e7-911d-60d70fcded21.png")' : 'none', backgroundSize: 'cover' }}>
{type === 'whatsapp' ? (
<>
<div style={{ alignSelf: 'center', backgroundColor: 'rgba(0,0,0,0.2)', color: '#fff', padding: '2px 8px', borderRadius: 10, fontSize: 10, marginBottom: 10 }}>Aujourd'hui</div>
{/* Message Commercial */}
<div style={{ alignSelf: 'flex-end', backgroundColor: '#dcf8c6', padding: 10, borderRadius: '8px 0 8px 8px', fontSize: 12, maxWidth: '90%', color: '#000', boxShadow: '0 1px 1px rgba(0,0,0,0.1)' }}>
<div style={{fontSize: 10, fontWeight: 'bold', color: '#e542a3', marginBottom: 2}}>Sophie (Logistique)</div>
Commande #402 chargée.<br/>Arrivée demain 10h00.
<div style={{ fontSize: 9, opacity: 0.5, textAlign: 'right', marginTop: 2 }}>16:00</div>
</div>
{/* Photo */}
<div style={{ alignSelf: 'flex-end', backgroundColor: '#dcf8c6', padding: 4, borderRadius: '8px 0 8px 8px', maxWidth: '90%', boxShadow: '0 1px 1px rgba(0,0,0,0.1)' }}>
<div style={{ width: 160, height: 100, backgroundColor: '#86efac', borderRadius: 4, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#166534', fontSize: 10, flexDirection: 'column' }}>
<Truck size={24} />
[PHOTO PALETTES]
</div>
<div style={{ fontSize: 9, opacity: 0.5, textAlign: 'right', paddingRight: 4, paddingBottom: 2, color: '#000' }}>16:00</div>
</div>
</>
) : (
<>
<div style={{ alignSelf: 'center', color: '#999', fontSize: 10, marginBottom: 10 }}>Hier 17:12</div>
<div style={{ alignSelf: 'flex-start', backgroundColor: '#e5e5ea', padding: 10, borderRadius: '15px 15px 15px 0', fontSize: 12, maxWidth: '80%', color: '#000' }}>
GEDIMAT: Votre livraison est prévue pour demain dans la journée.
</div>
</>
)}
</div>
{/* Footer Input */}
<div style={{ height: 50, backgroundColor: '#f0f0f0', borderTop: '1px solid #ddd' }}></div>
</div>
);
};
function GedimatSimulator() {
const [metrics, setMetrics] = useState({ waste: 0, savings: 0 });
const [cursor, setCursor] = useState({ x: -100, y: -100, clicking: false });
const [activeModule, setActiveModule] = useState(0); // 0: Intro, 1: Transport, 2: 15:30, 3: WhatsApp, 4: Summary
const [flashcard, setFlashcard] = useState<{ title: string; msg: string; type: string } | null>(null);
const [simStep, setSimStep] = useState(0);
const [phoneType, setPhoneType] = useState<string | null>(null);
const btnBadRef = useRef<HTMLButtonElement>(null);
const btnGoodRef = useRef<HTMLButtonElement>(null);
const startBtnRef = useRef<HTMLButtonElement>(null);
// Initialiser cursor au centre
useEffect(() => {
setCursor({ x: window.innerWidth / 2, y: window.innerHeight / 2, clicking: false });
}, []);
const moveCursorTo = (ref: React.RefObject<HTMLElement>, callback?: () => void) => {
if (ref.current) {
const rect = ref.current.getBoundingClientRect();
const targetX = rect.left + rect.width / 2;
const targetY = rect.top + rect.height / 2;
setCursor(prev => ({ ...prev, x: targetX, y: targetY }));
setTimeout(() => {
setCursor(prev => ({ ...prev, clicking: true })); // Click down
setTimeout(() => {
setCursor(prev => ({ ...prev, clicking: false })); // Click up
if (callback) callback();
}, 300);
}, 800); // Temps de trajet
}
};
// --- SCÉNARIO AUTOMATIQUE ---
useEffect(() => {
let timeout: ReturnType<typeof setTimeout>;
const runScenario = async () => {
// AUTO START
if (activeModule === 0) {
timeout = setTimeout(() => {
if (startBtnRef.current) {
moveCursorTo(startBtnRef, () => setActiveModule(1));
}
}, 2000);
}
// ---------------- MODULE 1: TRANSPORT ----------------
if (activeModule === 1) {
if (simStep === 0) {
// Expliquer le problème
setFlashcard({ title: "LE PROBLÈME", msg: "Gisors et Méru commandent séparément au même fournisseur.", type: 'bad' });
timeout = setTimeout(() => {
setFlashcard(null);
// Action "Mauvaise"
moveCursorTo(btnBadRef, () => {
setMetrics(prev => ({ ...prev, waste: prev.waste + 180 }));
setFlashcard({ title: "DOUBLE FACTURATION", msg: "2 camions payés pour rien. Gaspillage : 180€.", type: 'bad' });
setTimeout(() => { setFlashcard(null); setSimStep(1); }, 3000);
});
}, 3000);
} else if (simStep === 1) {
// Expliquer la solution
timeout = setTimeout(() => {
// Action "Bonne"
moveCursorTo(btnGoodRef, () => {
setMetrics(prev => ({ ...prev, savings: prev.savings + 90 }));
setFlashcard({ title: "SOLUTION XCEL", msg: "Consolidation J-1. Livraison unique à Méru + Navette interne.", type: 'good' });
setTimeout(() => { setFlashcard(null); setActiveModule(2); setSimStep(0); }, 3500);
});
}, 1000);
}
}
// ---------------- MODULE 2: PROTOCOLE 15:30 ----------------
if (activeModule === 2) {
if (simStep === 0) {
setFlashcard({ title: "LE PROBLÈME", msg: "Le camion n'est pas là à 15h30. On ne sait rien.", type: 'bad' });
timeout = setTimeout(() => {
setFlashcard(null);
moveCursorTo(btnBadRef, () => {
setMetrics(prev => ({ ...prev, waste: prev.waste + 2000 }));
setFlashcard({ title: "CLIENT PERDU", msg: "Le client découvre le retard demain matin. Chantier arrêté.", type: 'bad' });
setTimeout(() => { setFlashcard(null); setSimStep(1); }, 3000);
});
}, 3000);
} else if (simStep === 1) {
timeout = setTimeout(() => {
moveCursorTo(btnGoodRef, () => {
setMetrics(prev => ({ ...prev, savings: prev.savings + 500 }));
setFlashcard({ title: "PROTOCOLE 15:30", msg: "Alerte immédiate. Taxi-colis activé. Client prévenu à 16h.", type: 'good' });
setTimeout(() => { setFlashcard(null); setActiveModule(3); setSimStep(0); }, 3500);
});
}, 1000);
}
}
// ---------------- MODULE 3: WHATSAPP CONCIERGE ----------------
if (activeModule === 3) {
if (simStep === 0) {
setFlashcard({ title: "LE PROBLÈME", msg: "SMS impersonnel envoyé à 17h. Aucune réponse.", type: 'bad' });
timeout = setTimeout(() => {
setFlashcard(null);
moveCursorTo(btnBadRef, () => {
setPhoneType('sms'); // Show SMS phone
setFlashcard({ title: "COMMUNICATION FROIDE", msg: "Le client n'est pas rassuré. Il rappelle le commercial.", type: 'bad' });
setTimeout(() => { setPhoneType(null); setFlashcard(null); setSimStep(1); }, 4000);
});
}, 3000);
} else if (simStep === 1) {
timeout = setTimeout(() => {
moveCursorTo(btnGoodRef, () => {
setMetrics(prev => ({ ...prev, savings: prev.savings + 1200 }));
setPhoneType('whatsapp'); // Show WhatsApp phone
setFlashcard({ title: "EFFET 'CONCIERGE'", msg: "Groupe WhatsApp Chantier. Photo envoyée. Confiance totale.", type: 'good' });
setTimeout(() => { setPhoneType(null); setFlashcard(null); setActiveModule(4); }, 5000);
});
}, 1000);
}
}
};
runScenario();
return () => clearTimeout(timeout);
}, [activeModule, simStep]);
// --- RENDU DES MODULES ---
return (
<div style={STYLES.container}>
{/* HEADER P&L */}
<div style={STYLES.header}>
<div style={{ fontWeight: 'bold', fontSize: '20px', color: 'white', paddingLeft: 20 }}>GEDIMAT <span style={{color: COLORS.primary}}>XCEL</span></div>
<div style={{ display: 'flex', gap: '20px', paddingRight: 20 }}>
<StatCounter label="Pertes potentielles" value={metrics.waste} type="cost" />
<StatCounter label="Gains projetés" value={metrics.savings} type="profit" />
</div>
</div>
{/* MAIN STAGE */}
<div style={STYLES.mainStage}>
<div style={STYLES.card}>
{activeModule === 0 && (
<div style={{ textAlign: 'center', height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<h1 style={{ fontSize: '42px', marginBottom: '20px', background: 'linear-gradient(to right, #fff, #94a3b8)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>Gedimat XCEL v3.55</h1>
<p style={{ color: COLORS.textMuted, marginBottom: '40px', fontSize: '18px' }}>Simulation d'Impact Stratégique & Financier</p>
<button
ref={startBtnRef}
onClick={() => setActiveModule(1)}
style={{ ...STYLES.button('primary', true), margin: '0 auto', width: 'auto', fontSize: '18px', padding: '15px 40px' }}
>
LANCER LA DÉMONSTRATION
</button>
</div>
)}
{activeModule === 1 && (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '30px' }}>
<h3 style={{ display: 'flex', alignItems: 'center', gap: '15px', margin: 0, fontSize: '24px' }}>
<Truck color={COLORS.primary} size={32} /> Consolidation Gisors-Méru
</h3>
<span style={{ backgroundColor: '#334155', padding: '4px 12px', borderRadius: '20px', fontSize: '14px', display: 'flex', alignItems: 'center' }}>MODULE 1/3</span>
</div>
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: '30px' }}>
<p style={{ color: COLORS.textMuted, fontSize: '18px', textAlign: 'center', maxWidth: '80%' }}>
Gisors commande des tuiles à Beauvais. Méru commande du ciment à Beauvais.<br/>
Que fait-on ?
</p>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '30px' }}>
<button ref={btnBadRef} style={STYLES.button('danger', simStep === 0 && flashcard?.type === 'bad')} disabled={simStep !== 0}>
<div>
<div style={{ fontWeight: 'bold', fontSize: '16px', marginBottom: '5px' }}>MODE CLASSIQUE</div>
<div style={{ fontSize: '14px', opacity: 0.7 }}>2 commandes séparées</div>
</div>
<AlertTriangle size={24} />
</button>
<button ref={btnGoodRef} style={STYLES.button('success', simStep === 1 && flashcard?.type === 'good')} disabled={simStep !== 1}>
<div>
<div style={{ fontWeight: 'bold', fontSize: '16px', marginBottom: '5px' }}>MODE XCEL (HUB)</div>
<div style={{ fontSize: '14px', opacity: 0.7 }}>Check J-1 & Groupage</div>
</div>
<CheckCircle size={24} />
</button>
</div>
</div>
)}
{activeModule === 2 && (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '30px' }}>
<h3 style={{ display: 'flex', alignItems: 'center', gap: '15px', margin: 0, fontSize: '24px' }}>
<Clock color={COLORS.warning} size={32} /> Protocole 15:30
</h3>
<span style={{ backgroundColor: '#334155', padding: '4px 12px', borderRadius: '20px', fontSize: '14px', display: 'flex', alignItems: 'center' }}>MODULE 2/3</span>
</div>
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: '30px', flexDirection: 'column' }}>
<div style={{ fontSize: '48px', fontWeight: 'bold', color: COLORS.warning, marginBottom: '20px', fontFamily: 'monospace' }}>15:30</div>
<p style={{ color: COLORS.textMuted, fontSize: '18px', textAlign: 'center', maxWidth: '80%' }}>
Pas de nouvelles de Médiafret pour la livraison de demain.
</p>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '30px' }}>
<button ref={btnBadRef} style={STYLES.button('danger', simStep === 0 && flashcard?.type === 'bad')} disabled={simStep !== 0}>
<div>
<div style={{ fontWeight: 'bold', fontSize: '16px', marginBottom: '5px' }}>ATTENDRE</div>
<div style={{ fontSize: '14px', opacity: 0.7 }}>Espérer qu'il arrive...</div>
</div>
<AlertTriangle size={24} />
</button>
<button ref={btnGoodRef} style={STYLES.button('success', simStep === 1 && flashcard?.type === 'good')} disabled={simStep !== 1}>
<div>
<div style={{ fontWeight: 'bold', fontSize: '16px', marginBottom: '5px' }}>ACTIVER PLAN B</div>
<div style={{ fontSize: '14px', opacity: 0.7 }}>Taxi-Colis & Alerte</div>
</div>
<CheckCircle size={24} />
</button>
</div>
</div>
)}
{activeModule === 3 && (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '30px' }}>
<h3 style={{ display: 'flex', alignItems: 'center', gap: '15px', margin: 0, fontSize: '24px' }}>
<Smartphone color={COLORS.success} size={32} /> Interface Client
</h3>
<span style={{ backgroundColor: '#334155', padding: '4px 12px', borderRadius: '20px', fontSize: '14px', display: 'flex', alignItems: 'center' }}>MODULE 3/3</span>
</div>
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: '30px' }}>
<p style={{ color: COLORS.textMuted, fontSize: '18px', textAlign: 'center', maxWidth: '80%' }}>
Le client s'inquiète pour sa livraison de demain.<br/>Comment communiquer ?
</p>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '30px' }}>
<button ref={btnBadRef} style={STYLES.button('danger', simStep === 0 && flashcard?.type === 'bad')} disabled={simStep !== 0}>
<div>
<div style={{ fontWeight: 'bold', fontSize: '16px', marginBottom: '5px' }}>SMS STANDARD</div>
<div style={{ fontSize: '14px', opacity: 0.7 }}>Texte froid, 17h00</div>
</div>
<MessageCircle size={24} />
</button>
<button ref={btnGoodRef} style={STYLES.button('success', simStep === 1 && flashcard?.type === 'good')} disabled={simStep !== 1}>
<div>
<div style={{ fontWeight: 'bold', fontSize: '16px', marginBottom: '5px' }}>WHATSAPP VIP</div>
<div style={{ fontSize: '14px', opacity: 0.7 }}>Photo + Humain</div>
</div>
<Smartphone size={24} />
</button>
</div>
<PhoneMockup show={phoneType !== null} type={phoneType || 'whatsapp'} />
</div>
)}
{activeModule === 4 && (
<div style={{ textAlign: 'center', height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<h2 style={{ fontSize: '36px', marginBottom: '40px', color: COLORS.success }}>DÉMONSTRATION TERMINÉE</h2>
<div style={{ display: 'flex', justifyContent: 'center', gap: '60px', marginBottom: '60px' }}>
<div style={{ padding: 30, backgroundColor: 'rgba(239, 68, 68, 0.1)', borderRadius: 16, border: `1px solid ${COLORS.danger}` }}>
<div style={{ fontSize: '48px', fontWeight: 'bold', color: COLORS.danger, marginBottom: 10 }}>
{metrics.waste.toLocaleString()}
</div>
<div style={{ color: COLORS.textMuted, fontSize: '16px' }}>Pertes Identifiées</div>
</div>
<div style={{ padding: 30, backgroundColor: 'rgba(16, 185, 129, 0.1)', borderRadius: 16, border: `1px solid ${COLORS.success}` }}>
<div style={{ fontSize: '48px', fontWeight: 'bold', color: COLORS.success, marginBottom: 10 }}>
{metrics.savings.toLocaleString()}
</div>
<div style={{ color: COLORS.textMuted, fontSize: '16px' }}>Gains Stratégiques</div>
</div>
</div>
<button
onClick={() => { setMetrics({ waste: 0, savings: 0 }); setActiveModule(1); setSimStep(0); }}
style={{ ...STYLES.button('ghost'), margin: '0 auto', width: 'auto', fontSize: '16px' }}
>
REJOUER LA SÉQUENCE
</button>
</div>
)}
{/* Overlay Flashcard */}
{flashcard && <FlashCard title={flashcard.title} message={flashcard.msg} type={flashcard.type} />}
</div>
</div>
{/* CURSOR LAYER */}
<VirtualCursor x={cursor.x} y={cursor.y} clicking={cursor.clicking} />
{/* FOOTER LABEL */}
<div style={{
position: 'absolute', bottom: 20, left: '50%', transform: 'translateX(-50%)',
color: COLORS.textMuted, fontSize: '12px', opacity: 0.5
}}>
Démonstration Automatique Algorithme v3.55
</div>
</div>
);
}
const root = createRoot(document.getElementById('root')!);
root.render(<GedimatSimulator />);

5
metadata.json Normal file
View file

@ -0,0 +1,5 @@
{
"description": "Generated by Gemini.",
"requestFramePermissions": [],
"name": "App"
}

22
package.json Normal file
View file

@ -0,0 +1,22 @@
{
"name": "app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.2.0",
"react-dom": "^19.2.0",
"lucide-react": "0.263.1"
},
"devDependencies": {
"@types/node": "^22.14.0",
"@vitejs/plugin-react": "^5.0.0",
"typescript": "~5.8.2",
"vite": "^6.2.0"
}
}

29
tsconfig.json Normal file
View file

@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"types": [
"node"
],
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"jsx": "react-jsx",
"paths": {
"@/*": [
"./*"
]
},
"allowImportingTsExtensions": true,
"noEmit": true
}
}

23
vite.config.ts Normal file
View file

@ -0,0 +1,23 @@
import path from 'path';
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, '.', '');
return {
server: {
port: 3000,
host: '0.0.0.0',
},
plugins: [react()],
define: {
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
},
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
}
}
};
});