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:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -2,3 +2,4 @@ export { Navbar } from './layout/Navbar';
|
||||
export { Hero } from './sections/Hero';
|
||||
export { ProblemSolution } from './sections/ProblemSolution';
|
||||
export { HowItWorks } from './sections/HowItWorks';
|
||||
export { InteractiveDemo } from './sections/InteractiveDemo';
|
||||
282
frontend/src/components/sections/InteractiveDemo.tsx
Normal file
282
frontend/src/components/sections/InteractiveDemo.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user