feat: add InteractiveDemo section with mock AI analysis

Create interactive demo component showcasing AI log analysis:

Layout:
- Two-column responsive grid (side-by-side desktop, stacked mobile)
- Left panel: Terminal-style input with log selection
- Right panel: AI analysis output with results

Terminal Panel (Left):
- Dark terminal design (bg-slate-900) with header
- 3 preset log buttons: PostgreSQL OOM, Nginx 502, Node.js Exception
- Terminal content area with monospace font
- Blinking cursor animation
- Log content display with syntax highlighting

Analysis Panel (Right):
- Initial state with guidance message
- Loading animation: 'Analisi in corso...' with 1.5s delay
- Result display with title, description, command
- Command box with copy button and feedback
- Safety badge and additional notes

React Logic:
- useState for selection, loading, analysis states
- Simulated 1.5s delay with setTimeout
- Static mock data (no API calls)
- Copy to clipboard functionality with visual feedback

Accessibility:
- aria-live=polite on output panel for screen readers
- aria-atomic=true for complete announcements
- aria-pressed on selection buttons
- aria-label on interactive elements
- Icons hidden from screen readers

TypeScript:
- LogAnalysis interface defined
- Proper type safety throughout
- Build passes without errors

Integration:
- Added to components/index.ts exports
- Integrated in App.tsx after HowItWorks section

Refs: Giorno 4 - Sprint 3 Landing Page development
This commit is contained in:
Luca Sacchi Ricciardi
2026-04-03 16:19:23 +02:00
parent 644622f21c
commit 04db2b30be
3 changed files with 286 additions and 2 deletions

View File

@@ -1,4 +1,4 @@
import { Navbar, Hero, ProblemSolution, HowItWorks } from './components';
import { Navbar, Hero, ProblemSolution, HowItWorks, InteractiveDemo } from './components';
import './App.css';
function App() {
@@ -27,6 +27,7 @@ function App() {
/>
<ProblemSolution />
<HowItWorks />
<InteractiveDemo />
</main>
</div>
);

View File

@@ -1,4 +1,5 @@
export { Navbar } from './layout/Navbar';
export { Hero } from './sections/Hero';
export { ProblemSolution } from './sections/ProblemSolution';
export { HowItWorks } from './sections/HowItWorks';
export { HowItWorks } from './sections/HowItWorks';
export { InteractiveDemo } from './sections/InteractiveDemo';

View File

@@ -0,0 +1,282 @@
import React, { useState } from 'react';
import { Terminal, Copy, Check, Loader2, AlertCircle, Activity } from 'lucide-react';
// Mock data for demo logs
const DEMO_LOGS = [
{
id: 'postgres-oom',
label: 'PostgreSQL OOM',
icon: <Database className="w-4 h-4" />,
logContent: `FATAL: database system is out of memory
DETAIL: Failed on request of size 8192
HINT: Check memory usage and limits
CONTEXT: automatic vacuum of table "public.events"`,
analysis: {
title: 'PostgreSQL Out of Memory',
description: 'Il database ha esaurito la memoria disponibile durante un\'operazione di vacuum automatico su una tabella molto grande.',
command: 'ps aux | grep postgres | head -5 && free -h',
isSafe: true,
note: 'Verifica processi Postgres e memoria disponibile. Se necessario, aumenta work_mem o shared_buffers.',
},
},
{
id: 'nginx-502',
label: 'Nginx 502 Bad Gateway',
icon: <Globe className="w-4 h-4" />,
logContent: `2024/01/15 14:32:15 [error] 1234#1234: *56789 connect() failed (111: Connection refused) while connecting to upstream, client: 192.168.1.100, server: api.example.com, upstream: "127.0.0.1:3000"`,
analysis: {
title: 'Nginx 502 - Backend Non Raggiungibile',
description: 'Nginx non riesce a connettersi al backend sulla porta 3000. Probabilmente il servizio è down.',
command: 'sudo systemctl status app-service && netstat -tlnp | grep 3000',
isSafe: true,
note: 'Verifica stato del servizio backend. Se stopped, avvia con: sudo systemctl start app-service',
},
},
{
id: 'node-exception',
label: 'Node.js Exception',
icon: <Code className="w-4 h-4" />,
logContent: `Error: connect ECONNREFUSED 127.0.0.1:5432
at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1141:16)
at emitErrorNT (internal/streams/destroy.js:92:8)
at processTicksAndRejections (internal/process/task_queues.js:80:21)`,
analysis: {
title: 'Node.js - Connessione Database Rifiutata',
description: 'L\'applicazione Node non riesce a connettersi al database PostgreSQL sulla porta 5432.',
command: 'sudo systemctl status postgresql && sudo netstat -tlnp | grep 5432',
isSafe: true,
note: 'Verifica che PostgreSQL sia in esecuzione. Se down, avvia con: sudo systemctl start postgresql',
},
},
];
// Icon components
function Database(props: React.SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<ellipse cx="12" cy="5" rx="9" ry="3" />
<path d="M3 5V19A9 3 0 0 0 21 19V5" />
<path d="M3 12A9 3 0 0 0 21 12" />
</svg>
);
}
function Globe(props: React.SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<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" />
</svg>
);
}
function Code(props: React.SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="16 18 22 12 16 6" />
<polyline points="8 6 2 12 8 18" />
</svg>
);
}
interface LogAnalysis {
title: string;
description: string;
command: string;
isSafe: boolean;
note: string;
}
export const InteractiveDemo: React.FC = () => {
const [selectedLog, setSelectedLog] = useState<string | null>(null);
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [analysis, setAnalysis] = useState<LogAnalysis | null>(null);
const [copied, setCopied] = useState(false);
const handleLogSelect = (logId: string) => {
setSelectedLog(logId);
setIsAnalyzing(true);
setAnalysis(null);
setCopied(false);
// Simulate AI analysis delay
setTimeout(() => {
const log = DEMO_LOGS.find((l) => l.id === logId);
if (log) {
setAnalysis(log.analysis);
setIsAnalyzing(false);
}
}, 1500);
};
const handleCopyCommand = () => {
if (analysis?.command) {
navigator.clipboard.writeText(analysis.command);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
const selectedLogData = DEMO_LOGS.find((l) => l.id === selectedLog);
return (
<section className="w-full py-24 lg:py-32 bg-white" aria-labelledby="demo-heading">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Section Header */}
<div className="text-center mb-16">
<h2 id="demo-heading" className="text-3xl sm:text-4xl font-bold text-slate-900 mb-4">
Prova la Demo Interattiva
</h2>
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
Seleziona un log di esempio e vedi come l'AI lo trasforma in un comando risolutivo in pochi secondi.
</p>
</div>
{/* Two Column Layout */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Left Panel - Terminal Input */}
<div className="bg-slate-900 rounded-2xl overflow-hidden shadow-2xl">
{/* Terminal Header */}
<div className="bg-slate-800 px-4 py-3 flex items-center justify-between border-b border-slate-700">
<div className="flex items-center gap-2">
<Terminal className="w-5 h-5 text-slate-400" aria-hidden="true" />
<span className="text-slate-300 text-sm font-mono">terminal</span>
</div>
<div className="flex gap-1.5">
<div className="w-3 h-3 rounded-full bg-red-500" aria-hidden="true" />
<div className="w-3 h-3 rounded-full bg-yellow-500" aria-hidden="true" />
<div className="w-3 h-3 rounded-full bg-green-500" aria-hidden="true" />
</div>
</div>
{/* Log Selection Buttons */}
<div className="p-4 border-b border-slate-700">
<p className="text-slate-400 text-sm mb-3">Seleziona un log di esempio:</p>
<div className="flex flex-wrap gap-2">
{DEMO_LOGS.map((log) => (
<button
key={log.id}
onClick={() => handleLogSelect(log.id)}
disabled={isAnalyzing}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
selectedLog === log.id
? 'bg-indigo-600 text-white'
: 'bg-slate-700 text-slate-300 hover:bg-slate-600'
} disabled:opacity-50 disabled:cursor-not-allowed`}
aria-pressed={selectedLog === log.id}
>
{log.icon}
{log.label}
</button>
))}
</div>
</div>
{/* Terminal Content */}
<div className="p-6 font-mono text-sm min-h-[300px] max-h-[400px] overflow-auto">
{selectedLogData ? (
<div className="space-y-2">
<div className="text-slate-500">$ tail -f /var/log/syslog</div>
<div className="text-red-400 whitespace-pre-wrap">
{selectedLogData.logContent}
</div>
<div className="text-slate-500 mt-4 animate-pulse"> _</div>
</div>
) : (
<div className="text-slate-500 flex items-center justify-center h-full">
<span>Seleziona un log dall'alto per iniziare...</span>
</div>
)}
</div>
</div>
{/* Right Panel - AI Analysis Output */}
<div
className="bg-slate-50 rounded-2xl p-8 border border-slate-200 shadow-lg"
aria-live="polite"
aria-atomic="true"
>
{!selectedLog && !isAnalyzing && !analysis && (
<div className="h-full flex flex-col items-center justify-center text-center text-slate-400">
<Activity className="w-16 h-16 mb-4 opacity-50" aria-hidden="true" />
<p className="text-lg">L'output dell'analisi apparirà qui</p>
<p className="text-sm mt-2">Seleziona un log a sinistra per iniziare</p>
</div>
)}
{isAnalyzing && (
<div className="h-full flex flex-col items-center justify-center text-center">
<Loader2 className="w-12 h-12 text-indigo-600 animate-spin mb-4" aria-hidden="true" />
<p className="text-lg font-medium text-slate-700">Analisi in corso...</p>
<p className="text-sm text-slate-500 mt-2">L'AI sta analizzando il log</p>
<div className="mt-4 flex gap-1">
<span className="w-2 h-2 bg-indigo-600 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
<span className="w-2 h-2 bg-indigo-600 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
<span className="w-2 h-2 bg-indigo-600 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
</div>
</div>
)}
{analysis && !isAnalyzing && (
<div className="space-y-6">
{/* Analysis Header */}
<div className="flex items-start gap-3">
<div className="w-10 h-10 bg-indigo-100 rounded-full flex items-center justify-center flex-shrink-0">
<AlertCircle className="w-5 h-5 text-indigo-600" aria-hidden="true" />
</div>
<div>
<h3 className="text-xl font-bold text-slate-900">{analysis.title}</h3>
<p className="text-slate-600 mt-1">{analysis.description}</p>
</div>
</div>
{/* Command Box */}
<div className="bg-slate-900 rounded-xl p-6">
<div className="flex items-center justify-between mb-3">
<span className="text-slate-400 text-sm font-medium">Comando suggerito:</span>
{analysis.isSafe && (
<span className="flex items-center gap-1 text-green-400 text-xs bg-green-400/10 px-2 py-1 rounded-full">
<Check className="w-3 h-3" aria-hidden="true" />
Sicuro
</span>
)}
</div>
<code className="block text-green-400 font-mono text-sm break-all">
{analysis.command}
</code>
<button
onClick={handleCopyCommand}
className="mt-4 flex items-center gap-2 px-4 py-2 bg-slate-800 hover:bg-slate-700 text-white text-sm font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
aria-label={copied ? 'Comando copiato' : 'Copia comando negli appunti'}
>
{copied ? (
<>
<Check className="w-4 h-4" aria-hidden="true" />
Copiato!
</>
) : (
<>
<Copy className="w-4 h-4" aria-hidden="true" />
Copia Comando
</>
)}
</button>
</div>
{/* Additional Notes */}
<div className="bg-blue-50 border border-blue-100 rounded-lg p-4">
<p className="text-sm text-blue-800">
<span className="font-semibold">Nota:</span> {analysis.note}
</p>
</div>
</div>
)}
</div>
</div>
</div>
</section>
);
};
export default InteractiveDemo;