const { useState, useEffect, useMemo } = React;
// --- SOUND ENGINE (Web Audio API) ---
const SoundEngine = (() => {
let ctx = null;
const getCtx = () => {
if (!ctx) ctx = new (window.AudioContext || window.webkitAudioContext)();
if (ctx.state === 'suspended') ctx.resume();
return ctx;
};
const playTone = (freq, duration, vol = 0.12, type = 'sine') => {
try {
const c = getCtx();
const osc = c.createOscillator();
const gain = c.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, c.currentTime);
gain.gain.setValueAtTime(vol, c.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, c.currentTime + duration);
osc.connect(gain);
gain.connect(c.destination);
osc.start(c.currentTime);
osc.stop(c.currentTime + duration);
} catch (e) {}
};
return {
hover: () => playTone(880, 0.08, 0.06, 'sine'),
click: () => {
playTone(660, 0.06, 0.1, 'sine');
setTimeout(() => playTone(990, 0.08, 0.08, 'sine'), 50);
},
spin: () => {
for (let i = 0; i < 6; i++) {
setTimeout(() => playTone(300 + i * 120, 0.12, 0.04 + i * 0.01, 'triangle'), i * 80);
}
},
copy: () => {
playTone(523, 0.1, 0.08, 'sine');
setTimeout(() => playTone(659, 0.1, 0.08, 'sine'), 80);
setTimeout(() => playTone(784, 0.15, 0.1, 'sine'), 160);
},
washStart: () => {
// Washing machine start: low rumble + beep
playTone(120, 0.3, 0.08, 'sawtooth');
setTimeout(() => playTone(220, 0.2, 0.06, 'triangle'), 100);
setTimeout(() => playTone(880, 0.15, 0.1, 'sine'), 350);
setTimeout(() => playTone(880, 0.15, 0.1, 'sine'), 550);
},
knobTurn: () => playTone(440, 0.05, 0.08, 'triangle'),
};
})();
// --- ICONE SVG ---
const IconCar = ({ size = 24, className = "" }) => (
);
const IconRefreshCw = ({ size = 24, className = "" }) => (
);
const IconCopy = ({ size = 24, className = "" }) => (
);
const IconCheck = ({ size = 24, className = "" }) => (
);
const IconChevronRight = ({ size = 24, className = "" }) => (
);
const IconInfo = ({ size = 24, className = "" }) => (
);
const IconMessageSquare = ({ size = 24, className = "" }) => (
);
// --- CONFIGURAZIONE PROMPT CONDIVISA ---
const PROMPT_CONFIG = window.PROMPT_CONFIG;
const SIMULATOR_CONFIG = PROMPT_CONFIG.simulator;
const OPTIMIZER_CONFIG = PROMPT_CONFIG.optimizer;
const PLUTCHIK_EMOTIONS = SIMULATOR_CONFIG.options.emotions;
const SALES_PHASES = SIMULATOR_CONFIG.options.phases;
const CHANNELS = SIMULATOR_CONFIG.options.channels;
const VEHICLES = SIMULATOR_CONFIG.options.vehicles;
const MSG_TYPES = OPTIMIZER_CONFIG.options.messageTypes;
const STYLES = OPTIMIZER_CONFIG.options.styles;
const SIMULATOR_DEFAULTS = SIMULATOR_CONFIG.options.defaults;
const SIMULATOR_SHORT_LABELS = SIMULATOR_CONFIG.options.shortLabels;
const OPTIMIZER_DEFAULTS = OPTIMIZER_CONFIG.options.defaults;
// --- FUNZIONI DI SUPPORTO MATEMATICO (SVG) ---
const polarToCartesian = (centerX, centerY, radius, angleInDegrees) => {
const angleInRadians = (angleInDegrees * Math.PI) / 180.0;
return { x: centerX + radius * Math.cos(angleInRadians), y: centerY + radius * Math.sin(angleInRadians) };
};
const getAnnularSectorPath = (cx, cy, r1, r2, startAngle, endAngle) => {
const startOuter = polarToCartesian(cx, cy, r2, endAngle);
const endOuter = polarToCartesian(cx, cy, r2, startAngle);
const startInner = polarToCartesian(cx, cy, r1, endAngle);
const endInner = polarToCartesian(cx, cy, r1, startAngle);
const largeArcFlag = endAngle - startAngle <= 180 ? '0' : '1';
return ['M', startOuter.x, startOuter.y, 'A', r2, r2, 0, largeArcFlag, 0, endOuter.x, endOuter.y, 'L', endInner.x, endInner.y, 'A', r1, r1, 0, largeArcFlag, 1, startInner.x, startInner.y, 'Z'].join(' ');
};
const normalizeAngle = (angle) => ((angle % 360) + 360) % 360;
const getTargetRotation = (index, totalItems) => { const itemAngle = (index + 0.5) * (360 / totalItems); return 270 - itemAngle; };
const calculateShortestSpin = (currentRot, targetRotMod360, forceSpin = false) => {
const curMod = normalizeAngle(currentRot);
let diff = targetRotMod360 - curMod;
if (!forceSpin) { if (diff > 180) diff -= 360; if (diff < -180) diff += 360; return currentRot + diff; }
else { if (diff < 0) diff += 360; return currentRot + diff + (360 * 3); }
};
// ============================================================
// COMPONENTE OTTIMIZZATORE (Lavatrice)
// ============================================================
function OptimizerTab() {
const [msgType, setMsgType] = useState(OPTIMIZER_DEFAULTS.messageType);
const [style, setStyle] = useState(OPTIMIZER_DEFAULTS.style);
const [message, setMessage] = useState('');
const [objective, setObjective] = useState('');
const [mode, setMode] = useState(null); // 'fix' | 'feedback'
const [isWashing, setIsWashing] = useState(false);
const [generatedPrompt, setGeneratedPrompt] = useState('');
const [copied, setCopied] = useState(false);
const [showPrompt, setShowPrompt] = useState(false);
const canStart = message.trim().length >= OPTIMIZER_DEFAULTS.minMessageLength;
const generatePrompt = (actionMode) => {
const typeLabel = MSG_TYPES.find(t => t.id === msgType)?.label || msgType;
const styleLabel = STYLES.find(s => s.id === style)?.label || style;
const obj = objective.trim() || OPTIMIZER_CONFIG.defaultObjective;
return OPTIMIZER_CONFIG.buildPrompt({
mode: actionMode,
message: message.trim(),
typeLabel,
styleLabel,
objective: obj,
});
};
const handleStart = (actionMode) => {
if (!canStart) return;
SoundEngine.washStart();
setMode(actionMode);
setIsWashing(true);
setShowPrompt(false);
setTimeout(() => {
const prompt = generatePrompt(actionMode);
setGeneratedPrompt(prompt);
setIsWashing(false);
setShowPrompt(true);
}, 2000);
};
const copyToClipboard = async () => {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(generatedPrompt);
} else {
const textArea = document.createElement("textarea");
textArea.value = generatedPrompt;
textArea.style.position = "fixed";
textArea.style.opacity = "0";
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
}
SoundEngine.copy();
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error("Errore durante la copia:", err);
}
};
// --- Knob Selector component ---
const KnobSelector = ({ items, value, onChange, label, renderItem }) => (
{label}
{items.map(item => {
const isActive = (item.id || item) === value;
return (
{ SoundEngine.knobTurn(); onChange(item.id || item); }}
className={`relative px-4 py-2.5 rounded-full border-2 font-medium text-sm transition-all duration-200 ${
isActive
? 'bg-cyan-500/20 border-cyan-500 text-cyan-300 shadow-lg shadow-cyan-900/30 scale-105'
: 'bg-slate-800/60 border-slate-700 text-slate-400 hover:border-slate-500 hover:text-slate-300'
}`}
>
{renderItem ? renderItem(item) : (item.icon ? `${item.icon} ${item.label}` : item.label)}
{isActive && (
)}
);
})}
);
return (
{/* COLONNA SINISTRA - Input & Lavatrice */}
{/* Textarea messaggio */}
Incolla qui il tuo messaggio
{/* Obiettivo */}
Cosa vuoi ottenere con questa comunicazione?
{/* PANNELLO LAVATRICE */}
{/* Subtle metallic texture */}
{/* Knob selectors */}
`${item.icon} ${item.label}`}
/>
{/* Separator */}
{/* OBLO (washing machine door) - action buttons */}
{/* Inner glass effect */}
{/* Spinning drum when washing */}
{isWashing && (
)}
{/* Content inside the door */}
{isWashing ? (
🌀
{mode === 'fix' ? 'Ottimizzazione...' : 'Analisi...'}
) : showPrompt ? (
{'\u2705'}
Pronto!
Scorri per il prompt
) : (
)}
{/* Two big round START buttons */}
handleStart('fix')}
disabled={!canStart || isWashing}
className={`group w-28 h-28 sm:w-32 sm:h-32 rounded-full border-4 flex flex-col items-center justify-center gap-1.5 transition-all duration-300 ${
canStart && !isWashing
? 'bg-gradient-to-br from-cyan-600 to-cyan-800 border-cyan-400 text-white shadow-xl shadow-cyan-900/50 hover:scale-105 hover:shadow-cyan-800/60 active:scale-95 cursor-pointer'
: 'bg-slate-800/50 border-slate-700 text-slate-600 cursor-not-allowed'
}`}
>
🔧
Aiutami a sistemarla
handleStart('feedback')}
disabled={!canStart || isWashing}
className={`group w-28 h-28 sm:w-32 sm:h-32 rounded-full border-4 flex flex-col items-center justify-center gap-1.5 transition-all duration-300 ${
canStart && !isWashing
? 'bg-gradient-to-br from-amber-600 to-orange-800 border-amber-400 text-white shadow-xl shadow-amber-900/50 hover:scale-105 hover:shadow-amber-800/60 active:scale-95 cursor-pointer'
: 'bg-slate-800/50 border-slate-700 text-slate-600 cursor-not-allowed'
}`}
>
💬
Dammi feedback
{/* COLONNA DESTRA - Output prompt */}
{showPrompt && generatedPrompt ? (
Riepilogo configurazione:
{MSG_TYPES.find(t => t.id === msgType)?.icon}
Canale:
{MSG_TYPES.find(t => t.id === msgType)?.label}
🎨
Stile:
{STYLES.find(s => s.id === style)?.label}
{mode === 'fix' ? '🔧' : '💬'}
Azione:
{mode === 'fix' ? 'Riscrittura' : 'Feedback'}
Prompt Generato
{copied ? : }
{copied ? 'Copiato!' : 'Copia Prompt'}
Copia il prompt e incollalo in ChatGPT, Claude, Copilot o Gemini. L'IA analizzerà il tuo messaggio e ti fornirà il risultato.
) : (
🧹
Il cesto è vuoto
Incolla il tuo messaggio, configura le impostazioni e premi uno dei due pulsanti per avviare il lavaggio.
)}
);
}
// ============================================================
// COMPONENTE LOGIN GATE
// ============================================================
function LoginGate() {
const [authUser, setAuthUser] = useState(null);
const [authError, setAuthError] = useState(null);
const [pendingDisclaimers, setPendingDisclaimers] = useState([]);
const [disclaimerChecks, setDisclaimerChecks] = useState({});
const [sessionCode, setSessionCode] = useState('');
const [sessionCodeLocked, setSessionCodeLocked] = useState(false);
const [email, setEmail] = useState('');
const [name, setName] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [authStep, setAuthStep] = useState('login'); // 'login' | 'disclaimers' | 'authenticated'
const [accessDate, setAccessDate] = useState(null);
const [sessionChecked, setSessionChecked] = useState(false);
const [loginFields, setLoginFields] = useState('both'); // 'both' | 'email' | 'name'
// On mount: read ?s= URL param and check existing session + fetch settings in parallel
useEffect(() => {
const urlCode = new URLSearchParams(location.search).get('s');
if (urlCode) {
setSessionCode(urlCode.trim().toUpperCase());
setSessionCodeLocked(true);
}
(async () => {
const [settingsResult, authResult] = await Promise.allSettled([
fetch('/api/autoprompter/settings', { credentials: 'include' }),
fetch('/api/autoprompter/auth/check', { credentials: 'include' }),
]);
if (settingsResult.status === 'fulfilled' && settingsResult.value.ok) {
try {
const d = await settingsResult.value.json();
if (d.loginFields) setLoginFields(d.loginFields);
} catch (_) {}
}
if (authResult.status === 'fulfilled' && authResult.value.ok) {
try {
const data = await authResult.value.json();
if (data.authenticated && data.user) {
setAuthUser({ user: data.user });
setAuthStep('authenticated');
}
} catch (_) {}
}
setSessionChecked(true);
})();
}, []);
const handleLogin = async (e) => {
e.preventDefault();
if (!sessionCode.trim()) return;
const identityMissing =
(loginFields === 'email' && !email.trim()) ||
(loginFields === 'name' && !name.trim()) ||
(loginFields === 'both' && !email.trim() && !name.trim());
if (identityMissing) return;
setIsLoading(true);
setAuthError(null);
try {
const res = await fetch('/api/autoprompter/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
session_code: sessionCode.trim(),
email: email.trim() || null,
name: name.trim() || null
})
});
const data = await res.json();
if (res.status === 429) {
setAuthError({ type: 'rate_limit', message: 'Troppi tentativi. Riprova tra un minuto.' });
} else if (res.status === 403 && data.error === 'invalid_session') {
setAuthError({ type: 'invalid_session', message: data.message || 'Codice sessione non valido.' });
} else if (res.status === 403 && (data.error === 'access_denied' || data.error === 'not_found')) {
setAuthError({ type: 'access_denied', message: data.message || 'Non è stato possibile verificare l\'accesso. Controlla l\'indirizzo email o contatta l\'amministratore.' });
} else if (res.status === 403 && data.error === 'not_yet') {
setAuthError({ type: 'not_yet', message: `Il tuo accesso sarà attivo dal ${data.date || 'data non disponibile'}.` });
setAccessDate(data.date || null);
} else if (res.status === 403 && data.error === 'expired') {
setAuthError({ type: 'expired', message: 'Il tuo periodo di accesso è terminato.' });
} else if (res.ok) {
if (data.disclaimers && data.disclaimers.length > 0) {
setPendingDisclaimers(data.disclaimers);
const checks = {};
data.disclaimers.forEach((d, i) => { checks[i] = false; });
setDisclaimerChecks(checks);
setAuthStep('disclaimers');
setAuthUser(data);
} else {
setAuthUser(data);
setAuthStep('authenticated');
}
} else {
setAuthError({ type: 'unknown', message: 'Errore durante l\'autenticazione. Riprova.' });
}
} catch (err) {
setAuthError({ type: 'network', message: 'Impossibile contattare il server. Verifica la connessione e riprova.' });
}
setIsLoading(false);
};
const allDisclaimersChecked = pendingDisclaimers.length > 0 && pendingDisclaimers.every((d, i) => !d.enabled || disclaimerChecks[i]);
const handleAcceptDisclaimers = async () => {
setIsLoading(true);
try {
const body = { disclaimer1_accepted: false, disclaimer2_accepted: false };
pendingDisclaimers.forEach((d, i) => {
if (d.slot === 'disclaimer1') body.disclaimer1_accepted = !!disclaimerChecks[i];
if (d.slot === 'disclaimer2') body.disclaimer2_accepted = !!disclaimerChecks[i];
});
const res = await fetch('/api/autoprompter/auth/accept', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(body)
});
if (res.ok) {
setAuthStep('authenticated');
} else {
setAuthError({ type: 'accept_error', message: 'Errore nell\'accettazione. Riprova.' });
}
} catch (err) {
setAuthError({ type: 'network', message: 'Impossibile contattare il server. Verifica la connessione e riprova.' });
}
setIsLoading(false);
};
const handleLogout = async () => {
try {
await fetch('/api/autoprompter/auth/logout', {
method: 'POST',
credentials: 'include'
});
} catch (err) {
// Logout request failed, reset local state anyway
}
setAuthUser(null);
setAuthError(null);
setPendingDisclaimers([]);
setDisclaimerChecks({});
if (!sessionCodeLocked) setSessionCode('');
setEmail('');
setName('');
setAuthStep('login');
setAccessDate(null);
};
const Branding = () => (
);
// --- SESSION CHECK LOADING ---
if (!sessionChecked) {
return (
Verifica sessione in corso...
);
}
// --- LOGIN SCREEN ---
if (authStep === 'login') {
return (
Accedi alla piattaforma
{loginFields === 'email'
? 'Inserisci la tua email per accedere.'
: loginFields === 'name'
? 'Inserisci il tuo nome per accedere.'
: 'Inserisci la tua email e/o il tuo nome per accedere.'}
{/* Error states */}
{authError && (authError.type === 'access_denied' || authError.type === 'rate_limit') && (
)}
{authError && authError.type === 'invalid_session' && (
)}
{authError && authError.type === 'not_yet' && (
📅
Il tuo accesso non è ancora attivo
Ti informeremo quando il tuo account sarà pronto. Controlla la tua email per aggiornamenti.
{accessDate &&
Data di attivazione: {accessDate}
}
)}
{authError && authError.type === 'expired' && (
⏳
Periodo di accesso terminato
Il tuo periodo di utilizzo è scaduto. Contatta il tuo referente per informazioni sul rinnovo.
)}
{authError && (authError.type === 'unknown' || authError.type === 'network') && (
)}
Riservato ai soggetti che hanno sottoscritto un accordo con Logotel .
);
}
// --- DISCLAIMERS SCREEN ---
if (authStep === 'disclaimers') {
return (
Condizioni di utilizzo
Per procedere, leggi e accetta le seguenti condizioni.
{pendingDisclaimers.map((d, i) => (
{d.content}
{d.enabled !== false && (
setDisclaimerChecks(prev => ({ ...prev, [i]: e.target.checked }))}
className="w-5 h-5 rounded border-2 border-slate-600 bg-slate-950 text-cyan-500 focus:ring-cyan-500 focus:ring-offset-0 cursor-pointer accent-cyan-500"
/>
Ho letto e accetto
)}
))}
{authError && authError.type === 'accept_error' && (
)}
{isLoading ? 'Invio in corso...' : 'Accedi'}
);
}
// --- AUTHENTICATED: show app ---
return ;
}
// ============================================================
// COMPONENTE PRINCIPALE APP
// ============================================================
function App({ authUser, onLogout }) {
const [activeTab, setActiveTab] = useState('simulator');
const [simulatorEnabled, setSimulatorEnabled] = useState(true);
const [optimizerEnabled, setOptimizerEnabled] = useState(false);
// Load app settings (tab visibility)
useEffect(() => {
fetch('/api/autoprompter/settings', { credentials: 'include' })
.then(r => r.json())
.then(data => {
const simOn = data.simulatorEnabled !== false;
const optOn = data.optimizerEnabled !== false;
setSimulatorEnabled(simOn);
setOptimizerEnabled(optOn);
// Set active tab to first available
if (!simOn && optOn) setActiveTab('optimizer');
else if (simOn) setActiveTab('simulator');
})
.catch(() => {});
}, []);
// --- SIMULATORE STATE ---
const isDesktop = () => window.innerWidth >= 1024;
const [openSections, setOpenSections] = useState({ 1: isDesktop(), 2: false, 3: false, 4: false });
const [stepsDone, setStepsDone] = useState({ 1: false, 2: false, 3: false, 4: false });
const [idxEmotion, setIdxEmotion] = useState(SIMULATOR_DEFAULTS.emotionIndex);
const [idxPhase, setIdxPhase] = useState(SIMULATOR_DEFAULTS.phaseIndex);
const [idxChannel, setIdxChannel] = useState(SIMULATOR_DEFAULTS.channelIndex);
const [idxVehicle, setIdxVehicle] = useState(SIMULATOR_DEFAULTS.vehicleIndex);
const [rotEmotion, setRotEmotion] = useState(0);
const [rotPhase, setRotPhase] = useState(0);
const [rotChannel, setRotChannel] = useState(0);
const [copied, setCopied] = useState(false);
const [hoveredItem, setHoveredItem] = useState(null);
useEffect(() => {
setRotEmotion(getTargetRotation(SIMULATOR_DEFAULTS.emotionIndex, PLUTCHIK_EMOTIONS.length));
setRotPhase(getTargetRotation(SIMULATOR_DEFAULTS.phaseIndex, SALES_PHASES.length));
setRotChannel(getTargetRotation(SIMULATOR_DEFAULTS.channelIndex, CHANNELS.length));
}, []);
const toggleSection = (num) => { setOpenSections(prev => ({ ...prev, [num]: !prev[num] })); };
const markStepDone = (num) => { setStepsDone(prev => ({ ...prev, [num]: true })); };
const updateRotation = (type, newIndex, forceSpin = false) => {
if (forceSpin) SoundEngine.spin(); else SoundEngine.click();
if (type === 'emotion') { setRotEmotion(prev => calculateShortestSpin(prev, getTargetRotation(newIndex, PLUTCHIK_EMOTIONS.length), forceSpin)); setIdxEmotion(newIndex); }
else if (type === 'phase') { setRotPhase(prev => calculateShortestSpin(prev, getTargetRotation(newIndex, SALES_PHASES.length), forceSpin)); setIdxPhase(newIndex); }
else if (type === 'channel') { setRotChannel(prev => calculateShortestSpin(prev, getTargetRotation(newIndex, CHANNELS.length), forceSpin)); setIdxChannel(newIndex); }
markStepDone(1);
};
const handleVehicleChange = (idx) => { setIdxVehicle(idx); markStepDone(1); };
const handleRandomize = () => {
const rE = Math.floor(Math.random() * PLUTCHIK_EMOTIONS.length);
const rP = Math.floor(Math.random() * SALES_PHASES.length);
const rC = Math.floor(Math.random() * CHANNELS.length);
const rV = Math.floor(Math.random() * VEHICLES.length);
setIdxVehicle(rV);
updateRotation('emotion', rE, true); updateRotation('phase', rP, true); updateRotation('channel', rC, true);
markStepDone(1); setOpenSections(prev => ({ ...prev, 1: true }));
};
const copyToClipboard = async () => {
try {
if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(generatedPrompt); }
else { const t = document.createElement("textarea"); t.value = generatedPrompt; t.style.position = "fixed"; t.style.opacity = "0"; document.body.appendChild(t); t.select(); document.execCommand('copy'); document.body.removeChild(t); }
SoundEngine.copy(); setCopied(true); markStepDone(2);
setOpenSections(prev => ({ ...prev, 2: false, 3: true }));
setTimeout(() => setCopied(false), 2000);
} catch (err) { console.error("Errore durante la copia:", err); }
};
const generatedPrompt = useMemo(() => {
const emotion = PLUTCHIK_EMOTIONS[idxEmotion]; const phase = SALES_PHASES[idxPhase]; const channel = CHANNELS[idxChannel];
const vehicle = VEHICLES[idxVehicle];
return SIMULATOR_CONFIG.buildPrompt({
emotion,
phase,
channel,
vehicle,
});
}, [idxEmotion, idxPhase, idxChannel, idxVehicle]);
// --- COMPONENTI UI SIMULATORE ---
const CX = 500; const CY = 500;
const getShortLabel = (name) => SIMULATOR_SHORT_LABELS[name] || name;
const renderRing = (items, radius1, radius2, type, activeIndex) => {
const total = items.length; const step = 360 / total;
return items.map((item, i) => {
const startAngle = i * step + 0.5; const endAngle = (i + 1) * step - 0.5;
const midAngle = startAngle + (endAngle - startAngle) / 2;
const textRadius = radius1 + (radius2 - radius1) / 2;
const textPos = polarToCartesian(CX, CY, textRadius, midAngle);
const isSelected = i === activeIndex;
const isHovered = hoveredItem && hoveredItem.type === type && hoveredItem.index === i;
let fillColor = item.color;
let textColor = item.text || (type !== 'emotion' ? (isSelected ? 'white' : '#94a3b8') : 'white');
const opacity = isHovered ? 1 : (isSelected ? 1 : 0.55);
let fontSize; if (type === 'emotion') fontSize = 22; else if (type === 'phase') fontSize = 16; else fontSize = 17;
const ringLabel = type === 'emotion' ? 'Emozione' : (type === 'phase' ? 'Fase' : 'Canale');
return (
updateRotation(type, i)}
onMouseEnter={() => { SoundEngine.hover(); setHoveredItem({ type, index: i, name: item.name, color: item.color, textColor: item.text || 'white', ringLabel }); }}
onMouseLeave={() => setHoveredItem(null)}
onTouchStart={(e) => { e.stopPropagation(); SoundEngine.hover(); setHoveredItem({ type, index: i, name: item.name, color: item.color, textColor: item.text || 'white', ringLabel }); }}
onTouchEnd={() => setTimeout(() => setHoveredItem(null), 1200)}
className="cursor-pointer transition-opacity" style={{ opacity }}>
{isHovered && (
)}
{isHovered ? item.name : getShortLabel(item.name)}
);
});
};
const AccordionHeader = ({ num, title }) => {
const isOpen = openSections[num]; const isDone = stepsDone[num];
return (
toggleSection(num)}
className={`w-full flex items-center gap-3 sm:gap-4 mb-3 cursor-pointer group transition-all p-3 sm:p-4 rounded-2xl border ${isOpen ? 'bg-slate-800/80 border-cyan-500/50 shadow-md shadow-cyan-900/20' : 'bg-slate-900/60 border-slate-800 hover:border-slate-600'}`}>
{isDone ? : {num} }
{title}
);
};
const SelectionLegend = () => (
{PLUTCHIK_EMOTIONS[idxEmotion].name}
{SALES_PHASES[idxPhase].name}
{CHANNELS[idxChannel].name}
);
// --- TAB CONFIG ---
const TAB_CONFIG = {
simulator: { title: 'Simulatore di gestione emotiva di Clienti', desc: 'Segui i passaggi per creare la tua simulazione su misura. Clicca sui pannelli per aprire e chiudere le varie sezioni.' },
optimizer: { title: 'Ottimizzatore di messaggi e comunicazioni', desc: 'Incolla un tuo messaggio, scegli stile e obiettivo, e lascia che la lavatrice faccia il lavoro sporco.' },
};
return (
{/* HEADER */}
{TAB_CONFIG[activeTab].title}
{TAB_CONFIG[activeTab].desc}
{/* TAB BAR — hidden when only one tab is available */}
{(simulatorEnabled && optimizerEnabled) &&
{simulatorEnabled && { setActiveTab('simulator'); SoundEngine.click(); }}
className={`flex items-center gap-2 px-4 sm:px-5 py-2.5 rounded-lg font-medium text-sm transition-all ${
activeTab === 'simulator'
? 'bg-cyan-600 text-white shadow-lg shadow-cyan-900/40'
: 'text-slate-400 hover:text-white hover:bg-slate-800/60'
}`}
>
Simulatore
}
{optimizerEnabled && { setActiveTab('optimizer'); SoundEngine.click(); }}
className={`flex items-center gap-2 px-4 sm:px-5 py-2.5 rounded-lg font-medium text-sm transition-all ${
activeTab === 'optimizer'
? 'bg-cyan-600 text-white shadow-lg shadow-cyan-900/40'
: 'text-slate-400 hover:text-white hover:bg-slate-800/60'
}`}
>
Ottimizzatore
}
}
{/* CONTENT */}
{activeTab === 'simulator' ? (
/* ==================== TAB SIMULATORE ==================== */
{/* COLONNA SINISTRA (Step 1) */}
{openSections[1] && (
Veicolo di interesse
handleVehicleChange(Number(e.target.value))}
className="w-full bg-slate-950 border border-slate-700 rounded-lg px-4 py-3 focus:outline-none focus:border-cyan-500 focus:ring-1 focus:ring-cyan-500 transition-colors text-base text-white appearance-none cursor-pointer"
style={{ backgroundImage: "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2 4l4 4 4-4' fill='none' stroke='%2394a3b8' stroke-width='2'/%3E%3C/svg%3E\")", backgroundRepeat: 'no-repeat', backgroundPosition: 'right 16px center' }}
>
{VEHICLES.map((v, i) => (
{v.name}
))}
Gira la Ruota!
{renderRing(PLUTCHIK_EMOTIONS, 310, 460, 'emotion', idxEmotion)}
{renderRing(SALES_PHASES, 185, 295, 'phase', idxPhase)}
{renderRing(CHANNELS, 85, 170, 'channel', idxChannel)}
{hoveredItem && (
{hoveredItem.ringLabel}
{hoveredItem.name}
)}
Seleziona i livelli cliccando sugli spicchi o usa la randomizzazione. Le scelte si allineano sul puntatore in alto.
{ markStepDone(1); setOpenSections(prev => ({...prev, 1: false, 2: true})); }}
className="px-5 py-2.5 bg-slate-800 hover:bg-slate-700 border border-slate-700 text-white rounded-lg transition-colors flex items-center gap-2 text-sm sm:text-base">
Conferma e Vai al Prompt
)}
{/* COLONNA DESTRA (Steps 2, 3, 4) */}
{openSections[2] && (
La tua configurazione:
Emozione: {PLUTCHIK_EMOTIONS[idxEmotion].name}
Fase: {SALES_PHASES[idxPhase].name}
Canale: {CHANNELS[idxChannel].name}
Prompt di Addestramento
{copied ? : }
{copied ? 'Copiato!' : 'Copia Prompt'}
)}
{openSections[3] && (
Come procedere all'allenamento:
🤖
Incolla nel tuo LLM preferito Apri ChatGPT, Claude, Copilot o Gemini e incolla il prompt che hai appena copiato. L'IA entrerà istantaneamente nella parte del cliente e ti rivolgerà la prima frase.
🎙️
Consiglio PRO: Usa la Voce! Se usi l'App dal tuo smartphone (o l'estensione vocale su PC), puoi abilitare la Conversazione Vocale . Non serve scrivere nulla: parla con l'IA naturalmente come faresti con un vero cliente nel salone. La simulazione diventerà incredibilmente realistica!
🎯
L'obiettivo: Customer Centricity Rispondi con calma alle sue obiezioni. Se usi il giusto tono empatico e trovi la soluzione al suo problema, vedrai il cliente "sciogliersi" e l'emozione diventerà positiva. Se sbagli approccio, peggiorerà.
{ markStepDone(3); setOpenSections(prev => ({...prev, 3: false, 4: true})); }}
className="px-4 sm:px-5 py-2.5 bg-slate-800 hover:bg-slate-700 border border-slate-700 text-white rounded-lg transition-colors flex items-center gap-2 text-sm">
Ho capito, sono pronto!
)}
{openSections[4] && (
Come terminare e farsi valutare
Quando ritieni di aver chiuso la trattativa, risolto il problema, o semplicemente se senti di non riuscire a gestire la situazione, puoi fermare il gioco di ruolo. Ti basta pronunciare o scrivere le parole magiche:
FINE SIMULAZIONE
Il Bot ti consegnerà una vera e propria pagella strutturata in 4 punti:
🎯 Customer Centricity: Analisi di come hai saputo accogliere e (se sei stato bravo) ribaltare l'emozione iniziale del cliente.
🟢 Punti di forza: Quali argomentazioni e comportamenti empatici hanno funzionato meglio.
🔴 Aree di miglioramento: Errori commessi, frasi da evitare o momenti in cui hai forzato troppo la mano.
📊 Valutazione: Un voto oggettivo da 1 a 10 sull'intera performance.
{ markStepDone(4); setOpenSections(prev => ({...prev, 4: false})); }}
className="px-4 sm:px-5 py-2.5 bg-slate-800 hover:bg-slate-700 border border-slate-700 text-white rounded-lg transition-colors flex items-center gap-2 text-sm">
Segna come completato
)}
) : (
/* ==================== TAB OTTIMIZZATORE ==================== */
)}
{/* FOOTER DISCLAIMER */}
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render( );