ValerioBotto's picture
Update frontend/src/App.tsx
0b0b69b verified
import React, { useState, useEffect, useRef } from 'react';
import type { Message, UserData } from './types';
import { AppStep } from './types';
import { IconFileUp, IconSend, IconBot, IconGear, IconSparkles, IconArrowRight, IconLayers, IconDatabase, IconWorkflow, IconGlobe } from './components/Icons';
import ReactMarkdown from 'react-markdown';
// COSTANTE BACKEND: Vuota perché il frontend è servito dallo stesso server su HF
const BACKEND_URL = '';
const App: React.FC = () => {
const [step, setStep] = useState<AppStep>(AppStep.NAME_INPUT);
const [previousStep, setPreviousStep] = useState<AppStep | null>(null);
const [userData, setUserData] = useState<UserData>({ name: '' });
const [progress, setProgress] = useState(0);
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState('');
const [isTyping, setIsTyping] = useState(false);
const [showPlusMenu, setShowPlusMenu] = useState(false);
const chatEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages, isTyping]);
const handleNameSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (userData.name.trim()) {
setStep(AppStep.PDF_UPLOAD);
}
};
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setUserData({ ...userData, fileName: file.name });
setStep(AppStep.PROCESSING);
setProgress(10);
const formData = new FormData();
formData.append('file', file);
formData.append('user_id', userData.name);
const progressInterval = setInterval(() => {
setProgress(prev => (prev < 90 ? prev + 5 : prev));
}, 800);
try {
const response = await fetch(`${BACKEND_URL}/upload`, {
method: 'POST',
body: formData,
});
clearInterval(progressInterval);
if (response.ok) {
setProgress(100);
setTimeout(() => {
setStep(AppStep.CHAT);
setMessages([{
role: 'ai',
content: 'PRESET_WELCOME',
timestamp: new Date()
}]);
}, 600);
} else {
throw new Error("Errore durante l'elaborazione del server");
}
} catch (error) {
clearInterval(progressInterval);
console.error("Errore durante l'ingestione:", error);
alert("Errore nel caricamento del file. Assicurati che il backend sia attivo.");
setStep(AppStep.PDF_UPLOAD);
}
}
};
const truncateFileName = (name: string, limit = 25) => {
if (name.length <= limit) return name;
return name.substring(0, limit) + '...';
};
const handleHomeClick = () => {
setStep(AppStep.NAME_INPUT);
setUserData({ name: '' });
setMessages([]);
setProgress(0);
};
const handleHowItWorksClick = () => {
setPreviousStep(step);
setStep(AppStep.HOW_IT_WORKS);
};
const returnFromHowItWorks = () => {
if (previousStep) {
setStep(previousStep);
} else {
setStep(AppStep.NAME_INPUT);
}
};
const handleSendMessage = async (e: React.FormEvent) => {
e.preventDefault();
if (!inputValue.trim() || isTyping) return;
const userMsg: Message = {
role: 'user',
content: inputValue,
timestamp: new Date()
};
setMessages(prev => [...prev, userMsg]);
setInputValue('');
setIsTyping(true);
try {
const queryParams = new URLSearchParams({
query: inputValue,
filename: userData.fileName || "documento",
user_id: userData.name
});
const response = await fetch(`${BACKEND_URL}/chat?${queryParams}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) throw new Error('Errore nella risposta del server');
const data = await response.json();
setMessages(prev => [...prev, {
role: 'ai',
content: data.answer,
timestamp: new Date()
}]);
} catch (error) {
console.error("Errore chat:", error);
setMessages(prev => [...prev, {
role: 'ai',
content: "Scusa, si è verificato un errore nel collegamento con il server. Assicurati che il backend sia attivo.",
timestamp: new Date()
}]);
} finally {
setIsTyping(false);
}
};
const isInitialPage = step === AppStep.NAME_INPUT || step === AppStep.PDF_UPLOAD || step === AppStep.PROCESSING;
return (
<div className="min-h-screen w-screen flex flex-col font-outfit bg-white overflow-x-hidden">
{/* Header */}
<header className={`glass-header fixed top-0 w-full z-50 py-3 md:py-4 px-4 md:px-12 flex justify-between items-center transition-opacity ${step === AppStep.CHAT || step === AppStep.HOW_IT_WORKS ? 'opacity-30 hover:opacity-100' : 'opacity-100'}`}>
<div onClick={handleHomeClick} className="flex items-center gap-2 cursor-pointer group">
<div className="bg-[#FF6600] text-white p-1.5 md:p-2 rounded-lg group-hover:scale-105 transition-transform flex items-center justify-center">
<IconBot className="w-6 h-6 md:w-8 h-8" />
</div>
<span className="text-xl md:text-2xl font-extrabold tracking-tighter transition-colors group-hover:text-[#FF6600]">RAG Chatbot</span>
</div>
<div>
<button onClick={handleHowItWorksClick} className="group bg-[#FF6600] text-white w-10 h-10 md:w-auto md:px-4 md:py-2 rounded-lg font-bold text-sm hover:bg-black transition-all active:scale-95 shadow-lg shadow-[#FF6600]/20 flex items-center justify-center">
<span className="md:hidden text-black font-black text-lg group-hover:text-white transition-colors">?</span>
<span className="hidden md:inline">Come funziona</span>
</button>
</div>
</header>
{/* Main Container */}
<main className={`flex-grow flex flex-col items-center ${(step === AppStep.CHAT || step === AppStep.HOW_IT_WORKS) ? 'h-screen pt-20' : 'pt-28 md:pt-32'}`}>
<div className={`w-full ${(step === AppStep.CHAT || step === AppStep.HOW_IT_WORKS) ? 'h-full flex flex-col' : 'max-w-4xl'}`}>
{/* INITIAL STEPS */}
{(step === AppStep.NAME_INPUT || step === AppStep.PDF_UPLOAD) && (
<div className="text-center mb-10 md:mb-12 animate-in fade-in slide-in-from-bottom-4 duration-700 px-6">
<h1 className="text-4xl md:text-6xl font-black mb-4 leading-tight">
Il Tuo Assistente PDF <br/>
<span className="text-[#FF6600]">Intelligente</span>
</h1>
<h2 className="text-base md:text-xl text-gray-600 font-light max-w-2xl mx-auto mb-10">
Carica il tuo PDF e lascialo analizzare dal nostro sistema: Poni domande in chat in linguaggio naturale e ottieni risposte mirate basate esclusivamente sul contenuto del file.
</h2>
<div className="flex flex-row items-center justify-center gap-2 md:gap-8 mb-12">
<div className="flex flex-col items-center gap-3 w-20 md:w-32 group cursor-default">
<div className="w-12 h-12 md:w-16 md:h-16 rounded-full border-2 flex items-center justify-center transition-all bg-gray-50 border-gray-100 text-gray-400 group-hover:text-[#FF6600] group-hover:border-[#FF6600]/30 group-hover:bg-[#FF6600]/5">
<IconFileUp className="w-5 h-5 md:w-7 md:h-7" />
</div>
<p className="text-[8px] md:text-[10px] font-bold tracking-widest uppercase text-gray-400 group-hover:text-[#FF6600]">1. Upload</p>
</div>
<div><IconArrowRight className="text-gray-200 w-3 h-3 md:w-5 md:h-5 animate-pulse" /></div>
<div className="flex flex-col items-center gap-3 w-20 md:w-32 group cursor-default">
<div className="w-12 h-12 md:w-16 md:h-16 rounded-full bg-gray-50 border-2 border-gray-100 flex items-center justify-center text-gray-400 group-hover:text-[#FF6600] group-hover:border-[#FF6600]/30 group-hover:bg-[#FF6600]/5 transition-all">
<div className="relative">
<IconGear className="w-5 h-5 md:w-7 md:h-7 animate-gear-rotate" />
<span className="absolute -top-1 -right-1 bg-[#FF6600] text-white text-[6px] md:text-[8px] font-bold px-1 rounded-sm">PDF</span>
</div>
</div>
<p className="text-[8px] md:text-[10px] font-bold tracking-widest uppercase text-gray-400 group-hover:text-[#FF6600] transition-colors">2. Analisi</p>
</div>
<div><IconArrowRight className="text-gray-200 w-3 h-3 md:w-5 md:h-5 animate-pulse" /></div>
<div className="flex flex-col items-center gap-3 w-20 md:w-32 group cursor-default">
<div className="w-12 h-12 md:w-16 md:h-16 rounded-full bg-gray-50 border-2 border-gray-100 flex items-center justify-center text-gray-400 shadow-sm group-hover:text-[#FF6600] group-hover:border-[#FF6600]/30 group-hover:bg-[#FF6600]/5 transition-all">
<IconSparkles className="w-5 h-5 md:w-7 md:h-7" />
</div>
<p className="text-[8px] md:text-[10px] font-bold tracking-widest uppercase text-gray-400 group-hover:text-[#FF6600] transition-colors">3. Chat</p>
</div>
</div>
</div>
)}
{step === AppStep.NAME_INPUT && (
<div className="flex justify-center py-2 animate-in fade-in slide-in-from-top-4 duration-500 px-6">
<div className="bg-white border border-black/5 p-8 md:p-10 rounded-[2.5rem] shadow-2xl max-w-md w-full orange-glow">
<form onSubmit={handleNameSubmit} className="flex flex-col gap-6 md:gap-8">
<div>
<label className="block text-xs font-bold uppercase tracking-[0.2em] text-gray-400 mb-4 ml-2">Inserisci il tuo nome o User ID</label>
<input type="text" value={userData.name} onChange={(e) => setUserData({...userData, name: e.target.value})} placeholder="Es: Laura" className="w-full bg-gray-50 border-2 border-transparent focus:border-[#FF6600]/30 focus:bg-white outline-none px-6 md:px-8 py-4 md:py-5 rounded-[1.5rem] text-lg md:text-xl font-semibold text-black shadow-sm" autoFocus />
</div>
<button type="submit" disabled={!userData.name.trim()} className="bg-[#FF6600] text-white font-bold py-4 md:py-5 rounded-[1.5rem] hover:bg-black shadow-lg shadow-[#FF6600]/20 transition-all transform active:scale-95 disabled:opacity-30">Continua</button>
</form>
</div>
</div>
)}
{step === AppStep.PDF_UPLOAD && (
<div className="flex flex-col items-center py-2 animate-in fade-in slide-in-from-bottom-8 duration-700 px-6">
<div className="text-center mb-8">
<h3 className="text-2xl md:text-3xl font-black text-black mb-2 tracking-tighter uppercase">Ciao <span className="text-[#FF6600]">{userData.name}</span></h3>
<p className="text-gray-400 font-medium">Siamo pronti ad analizzare il tuo file.</p>
</div>
<div className="bg-white border-2 border-dashed border-gray-200 p-8 md:p-12 rounded-[3rem] max-w-2xl w-full flex flex-col items-center justify-center gap-6 hover:border-[#FF6600] group transition-all cursor-pointer relative shadow-sm">
<input type="file" accept="application/pdf" onChange={handleFileUpload} className="absolute inset-0 opacity-0 cursor-pointer" />
<div className="bg-gray-50 p-4 md:p-6 rounded-full group-hover:bg-[#FF6600]/10 transition-colors">
<IconFileUp className="w-10 h-10 md:w-12 md:h-12 text-gray-300 group-hover:text-[#FF6600]" />
</div>
<div className="text-center">
<p className="text-lg md:text-xl font-bold mb-1">Carica il documento ed inizia a chattare</p>
<p className="text-gray-400 text-sm font-medium">Trascina qui il file o clicca per sfogliare</p>
</div>
</div>
</div>
)}
{step === AppStep.PROCESSING && (
<div className="flex-grow flex flex-col items-center justify-center py-16 gap-8 animate-in fade-in px-6">
<div className="relative w-20 h-20 md:w-24 md:h-24">
<div className="absolute inset-0 border-4 border-gray-50 rounded-full"></div>
<div className="absolute inset-0 border-4 border-t-[#FF6600] rounded-full animate-spin"></div>
</div>
<div className="text-center max-md:max-w-xs max-w-md">
<h3 className="text-xl md:text-2xl font-extrabold mb-2">Stiamo processando il tuo documento...</h3>
<p className="text-gray-500 mb-8 font-light">Quasi pronto...</p>
<div className="w-full bg-gray-50 h-2 rounded-full overflow-hidden">
<div className="bg-[#FF6600] h-full transition-all duration-300" style={{ width: `${progress}%` }}></div>
</div>
</div>
</div>
)}
{/* CHAT STEP */}
{step === AppStep.CHAT && (
<div className="flex-grow flex flex-col w-full max-w-3xl mx-auto h-full overflow-hidden">
<div className="flex-grow overflow-y-auto px-6 py-10 space-y-12 chat-scroll">
{messages.map((msg, idx) => {
if (msg.content === 'PRESET_WELCOME') {
return (
<div key={idx} className="animate-in slide-in-from-bottom-4 duration-700">
<div className="flex items-start gap-5">
<div className="w-12 h-12 rounded-2xl bg-[#FF6600] flex items-center justify-center shadow-lg shadow-[#FF6600]/20 flex-shrink-0">
<div className="relative flex items-center justify-center w-full h-full">
<IconBot className="w-9 h-9" />
<div className="absolute -top-1 -right-1 bg-white text-black text-[7px] font-black px-1 rounded-sm border border-black/5 uppercase">PDF</div>
</div>
</div>
<div className="flex flex-col gap-1 pt-1">
<h2 className="text-xl font-normal text-gray-500 font-verdana leading-tight">
<span className="text-[#FF6600] font-medium">{userData.name}</span>, il file{" "}
<span className="text-black font-medium hover:text-[#FF6600] cursor-pointer" title={userData.fileName}>
"{truncateFileName(userData.fileName || '', 40)}"
</span> è pronto.
</h2>
<p className="text-sm font-light text-gray-400">Chiedimi qualunque cosa!</p>
</div>
</div>
</div>
);
}
const isUser = msg.role === 'user';
return (
<div key={idx} className={`flex ${isUser ? 'justify-end' : 'justify-start'} animate-in fade-in duration-300`}>
<div className={`flex gap-4 max-w-[85%] ${isUser ? 'flex-row-reverse' : 'flex-row'}`}>
{!isUser && (
<div className="w-10 h-10 rounded-lg bg-gray-50 flex items-center justify-center flex-shrink-0 mt-1">
<IconBot className="w-7 h-7" />
</div>
)}
<div className={`p-4 ${isUser ? 'bg-gray-100 text-gray-800 rounded-2xl rounded-tr-none' : 'text-gray-700 markdown-container'}`}>
<div className="font-verdana">
<ReactMarkdown>{msg.content}</ReactMarkdown>
</div>
<p className="text-[9px] mt-2 opacity-30 font-bold tracking-tighter">
{msg.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</p>
</div>
</div>
</div>
);
})}
{isTyping && (
<div className="flex justify-start animate-in fade-in">
<div className="flex gap-4">
<div className="w-10 h-10 rounded-lg bg-gray-50 flex items-center justify-center">
<IconBot className="w-7 h-7 opacity-30 animate-pulse" />
</div>
<div className="flex gap-1.5 items-center py-2">
<span className="w-1.5 h-1.5 bg-gray-200 rounded-full animate-bounce"></span>
<span className="w-1.5 h-1.5 bg-gray-200 rounded-full animate-bounce delay-150"></span>
<span className="w-1.5 h-1.5 bg-gray-200 rounded-full animate-bounce delay-300"></span>
</div>
</div>
</div>
)}
<div ref={chatEndRef} />
</div>
<div className="px-6 pb-10 pt-4 bg-white">
<form onSubmit={handleSendMessage} className="relative max-w-2xl mx-auto">
<div className="relative flex items-center bg-gray-50 border border-gray-100 rounded-[2.5rem] p-2 focus-within:bg-white focus-within:ring-2 focus-within:ring-[#FF6600]/10 shadow-sm">
<div className="relative flex-shrink-0">
<button type="button" onClick={() => setShowPlusMenu(!showPlusMenu)} className={`w-12 h-12 flex items-center justify-center rounded-full transition-all ${showPlusMenu ? 'bg-black text-white' : 'bg-[#FF6600] text-white hover:bg-black'}`}>
<span className={`text-2xl transition-transform font-light ${showPlusMenu ? 'rotate-45' : ''}`}>+</span>
</button>
{showPlusMenu && (
<div className="absolute bottom-full left-0 mb-4 bg-white border border-gray-100 rounded-2xl shadow-xl p-2 min-w-[150px] animate-in fade-in">
<button onClick={() => { setStep(AppStep.PDF_UPLOAD); setShowPlusMenu(false); }} className="w-full text-left px-4 py-3 text-sm font-semibold hover:bg-gray-50 rounded-xl flex items-center gap-3 text-gray-700">
<IconFileUp className="w-4 h-4 text-[#FF6600]" /> Cambia PDF
</button>
</div>
)}
</div>
<input type="text" value={inputValue} onChange={(e) => setInputValue(e.target.value)} placeholder="Scrivi un messaggio..." className="flex-grow bg-transparent border-none outline-none px-4 md:px-6 py-4 text-[14px] text-gray-800 font-verdana" />
<button type="submit" disabled={!inputValue.trim() || isTyping} className="text-[#FF6600] w-10 h-10 md:w-12 md:h-12 flex items-center justify-center flex-shrink-0 rounded-full hover:bg-[#FF6600]/10 disabled:opacity-10">
<IconSend className="w-5 h-5 md:w-6 md:h-6" />
</button>
</div>
</form>
</div>
</div>
)}
{/* HOW IT WORKS STEP - AGGIORNATA CON LE TUE SPIEGAZIONI */}
{step === AppStep.HOW_IT_WORKS && (
<div className="flex-grow overflow-y-auto px-6 py-10 chat-scroll animate-in fade-in duration-700">
<div className="max-w-4xl mx-auto space-y-12 pb-20">
<div className="text-center mb-16">
<h1 className="text-4xl md:text-5xl font-black mb-4 font-outfit">Architettura <span className="text-[#FF6600]">GraphRAG</span></h1>
<p className="text-gray-500 font-light text-lg">Un sistema di recupero deterministico potenziato da orchestratori LLM.</p>
</div>
<div className="grid md:grid-cols-2 gap-8">
{/* Step 1 */}
<div className="bg-white border border-gray-100 p-8 rounded-3xl shadow-sm hover:orange-glow transition-all">
<div className="bg-[#FF6600] text-white w-12 h-12 rounded-xl flex items-center justify-center mb-6">
<IconFileUp className="w-6 h-6" />
</div>
<h3 className="text-xl font-bold mb-3">Ingestion & Struttura</h3>
<p className="text-gray-600 font-light text-sm leading-relaxed">
Il PDF non viene solo "letto", ma processato per estrarre la <strong>gerarchia logica</strong>. Utilizziamo librerie specializzate per ricostruire tabelle, titoli e sezioni, garantendo che l'informazione non perda mai il suo contesto originale durante la scomposizione.
</p>
</div>
{/* Step 2 */}
<div className="bg-white border border-gray-100 p-8 rounded-3xl shadow-sm hover:orange-glow transition-all">
<div className="bg-black text-white w-12 h-12 rounded-xl flex items-center justify-center mb-6">
<IconLayers className="w-6 h-6" />
</div>
<h3 className="text-xl font-bold mb-3">Chunking</h3>
<p className="text-gray-600 font-light text-sm leading-relaxed">
Applichiamo una strategia di <strong>chunking ricorsivo</strong>. Il testo viene diviso in frammenti ottimizzati per gli embedding, mantenendo sovrapposizioni intelligenti tra i paragrafi per non interrompere la coerenza semantica tra i nodi del database.
</p>
</div>
{/* Step 3 */}
<div className="bg-white border border-gray-100 p-8 rounded-3xl shadow-sm hover:orange-glow transition-all">
<div className="bg-black text-white w-12 h-12 rounded-xl flex items-center justify-center mb-6">
<IconDatabase className="w-6 h-6" />
</div>
<h3 className="text-xl font-bold mb-3">Knowledge Graph (Neo4j)</h3>
<p className="text-gray-600 font-light text-sm leading-relaxed">
I chunk e le entità vengono mappati in <strong>Neo4j</strong>. Ogni frammento diventa un nodo collegato non solo vettorialmente, ma anche per relazione logica (es. "appartiene alla sezione X"). Questo permette una navigazione dei dati multimodale: semantica e a grafo.
</p>
</div>
{/* Step 4 */}
<div className="bg-white border border-gray-100 p-8 rounded-3xl shadow-sm hover:orange-glow transition-all">
<div className="bg-black text-white w-12 h-12 rounded-xl flex items-center justify-center mb-6">
<IconWorkflow className="w-6 h-6" />
</div>
<h3 className="text-xl font-bold mb-3">LangGraph</h3>
<p className="text-gray-500 font-light text-sm leading-relaxed">
L'intero flusso è gestito da <strong>LangGraph</strong>. Un sistema a stati finiti che coordina i nodi di analisi, recupero e validazione. Questo garantisce che ogni query segua un percorso logico rigoroso prima di produrre un output.
</p>
</div>
</div>
{/* Sezione Strategia Ibrida */}
<div className="bg-gray-50 border border-gray-100 p-10 rounded-[3rem] shadow-inner">
<div className="text-center mb-10">
<h3 className="text-2xl font-black mb-2 font-outfit">Recupero <span className="text-[#FF6600]">Ibrido & Intelligente</span></h3>
<p className="text-gray-500 text-sm font-light max-w-2xl mx-auto">
l'LLM agisce esclusivamente come Orchestratore Decisionale: analizza l'intento della domanda per determinare la rotta di recupero ottimale. Non 'pesca' i dati dalla sua memoria, non 'inventa' nulla. Piuttosto, sceglie se azionare il motore di Ricerca Vettoriale o se comporre una Query per interrogare direttamente il DB.
</p>
</div>
<div className="grid md:grid-cols-2 gap-8">
<div className="space-y-6">
<div className="bg-white p-6 rounded-2xl shadow-sm border border-black/5">
<div className="flex items-center gap-3 mb-3">
<div className="bg-[#FF6600]/10 px-2 py-1 rounded md font-bold text-[#FF6600] text-[10px] uppercase">Routing</div>
<h4 className="font-bold text-sm">Mistral come Router Strategico</h4>
</div>
<p className="text-xs text-gray-600 leading-relaxed font-light">
Utilizziamo <strong>Mistral</strong> per decidere la via di recupero. Se la domanda contiene entità specifiche già mappate nel DB, Mistral formula una <strong>Query Cypher</strong> basata sulle NE estratte, ottenendo dati certi in millisecondi direttamente dal database a grafo. Altrimenti, si prioritizza una strategia di recupero vettoriale, o ibrida.
</p>
</div>
<div className="bg-white p-6 rounded-2xl shadow-sm border border-black/5">
<div className="flex items-center gap-3 mb-3">
<div className="bg-black/5 px-2 py-1 rounded md font-bold text-gray-500 text-[10px] uppercase">Vector</div>
<h4 className="font-bold text-sm">Ricerca Vettoriale</h4>
</div>
<p className="text-xs text-gray-600 leading-relaxed font-light">
Il sistema attiva una ricerca vettoriale quando il router identifica quesiti di natura concettuale o descrittiva. Utilizziamo il modello <strong>multilingual-e5-large</strong> (1024 dim), permettendo di individuare correlazioni semantiche profonde tra la domanda e il documento.
</p>
</div>
</div>
<div className="space-y-6">
<div className="bg-white p-6 rounded-2xl shadow-sm border border-black/5">
<div className="flex items-center gap-3 mb-3">
<div className="bg-[#FF6600]/10 px-2 py-1 rounded md font-bold text-[#FF6600] text-[10px] uppercase">Entities</div>
<h4 className="font-bold text-sm">Named Entity Extraction (GLiNER)</h4>
</div>
<p className="text-xs text-gray-600 leading-relaxed font-light">
Sfruttiamo <strong>GLiNER</strong> per identificare entità chiave (nomi, luoghi, termini tecnici). Queste entità permettono a Mistral di generare istantaneamente Query Cypher mirate, garantendo un recupero dati deterministico direttamente dal database a grafo Neo4j.
</p>
</div>
<div className="bg-white p-6 rounded-2xl shadow-sm border border-black/5">
<div className="flex items-center gap-3 mb-3">
<div className="bg-black/5 px-2 py-1 rounded md font-bold text-gray-500 text-[10px] uppercase">Rerank</div>
<h4 className="font-bold text-sm">Reranking-v2-m3</h4>
</div>
<p className="text-xs text-gray-600 leading-relaxed font-light">
I risultati grezzi del database vengono filtrati da un <strong>Cross-Encoder (BGE-Reranker-v2-m3)</strong>. Questo modello ri-analizza la pertinenza di ogni chunk rispetto alla domanda originale, scartando il "rumore" e passando solo il contesto più pertinente.
</p>
</div>
</div>
</div>
</div>
<div className="bg-white border border-gray-100 p-8 rounded-[3rem] shadow-sm hover:orange-glow transition-all max-w-2xl mx-auto">
<div className="flex flex-col items-center text-center">
<div className="bg-[#FF6600] text-white w-14 h-14 rounded-2xl flex items-center justify-center mb-6 shadow-lg shadow-[#FF6600]/20">
<IconGlobe className="w-8 h-8" />
</div>
<h3 className="text-2xl font-black mb-3 font-outfit">Ricerca Globale</h3>
<p className="text-gray-600 font-light text-sm leading-relaxed">
Se la ricerca vettoriale sul documento attivo produce uno score di pertinenza basso (inferiore a 0.7), il sistema attiva automaticamente una <strong>Ricerca globale</strong>. Questa scansione estende l'analisi a tutti i documenti precedentemente indicizzati nel database Neo4j per verificare se l'informazione necessaria è presente in altri PDF.
</p>
</div>
</div>
<div className="flex flex-col items-center text-center max-w-2xl mx-auto space-y-6">
<div className="bg-[#FF6600] text-white w-16 h-16 rounded-2xl flex items-center justify-center shadow-lg shadow-[#FF6600]/20">
<IconBot className="w-10 h-10" />
</div>
<h3 className="text-2xl font-black font-outfit">Sintesi Finale</h3>
<p className="text-gray-600 font-light leading-relaxed">
L'LLM finale non genera testo libero ma agisce esclusivamente da sintetizzatore: riceve i dati filtrati dal Reranker e produce una risposta che è una diretta conseguenza dei documenti. Ogni affermazione è ancorata a una fonte certa, riducendo drasticamente il rischio di allucinazioni.
</p>
</div>
<div className="flex justify-center pt-10">
<button onClick={returnFromHowItWorks} className="bg-black text-white px-12 py-5 rounded-2xl font-bold hover:bg-[#FF6600] transition-all shadow-xl active:scale-95">
Torna alla Chat
</button>
</div>
</div>
</div>
)}
</div>
</main>
{/* Developer Footer */}
{isInitialPage && (
<div className="w-full mt-20 py-12 px-6 border-t border-gray-100 flex flex-col items-center gap-6 animate-in fade-in slide-in-from-bottom-4 duration-1000 bg-white">
<div className="flex items-center gap-8">
<a href="https://github.com/ValerioBotto" target="_blank" rel="noopener noreferrer" className="group flex flex-col items-center gap-2">
<div className="p-3 bg-gray-50 rounded-xl group-hover:bg-black group-hover:text-white transition-all">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"></path><path d="M9 18c-4.51 2-5-2-7-2"></path></svg>
</div>
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-400 group-hover:text-black">GitHub</span>
</a>
<a href="https://www.linkedin.com/in/valerio-botto-4844b2190/" target="_blank" rel="noopener noreferrer" className="group flex flex-col items-center gap-2">
<div className="p-3 bg-gray-50 rounded-xl group-hover:bg-[#0077b5] group-hover:text-white transition-all">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path><rect width="4" height="12" x="2" y="9"></rect><circle cx="4" cy="4" r="2"></circle></svg>
</div>
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-400 group-hover:text-[#0077b5]">LinkedIn</span>
</a>
</div>
<div className="text-center">
<p className="text-[10px] text-gray-300 font-black tracking-[0.4em] uppercase">
Developed by <span className="text-black">Valerio Botto</span>
</p>
</div>
</div>
)}
</div>
);
};
export default App;