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.NAME_INPUT); const [previousStep, setPreviousStep] = useState(null); const [userData, setUserData] = useState({ name: '' }); const [progress, setProgress] = useState(0); const [messages, setMessages] = useState([]); const [inputValue, setInputValue] = useState(''); const [isTyping, setIsTyping] = useState(false); const [showPlusMenu, setShowPlusMenu] = useState(false); const chatEndRef = useRef(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) => { 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 (
{/* Header */}
RAG Chatbot
{/* Main Container */}
{/* INITIAL STEPS */} {(step === AppStep.NAME_INPUT || step === AppStep.PDF_UPLOAD) && (

Il Tuo Assistente PDF
Intelligente

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.

1. Upload

PDF

2. Analisi

3. Chat

)} {step === AppStep.NAME_INPUT && (
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 />
)} {step === AppStep.PDF_UPLOAD && (

Ciao {userData.name}

Siamo pronti ad analizzare il tuo file.

Carica il documento ed inizia a chattare

Trascina qui il file o clicca per sfogliare

)} {step === AppStep.PROCESSING && (

Stiamo processando il tuo documento...

Quasi pronto...

)} {/* CHAT STEP */} {step === AppStep.CHAT && (
{messages.map((msg, idx) => { if (msg.content === 'PRESET_WELCOME') { return (
PDF

{userData.name}, il file{" "} "{truncateFileName(userData.fileName || '', 40)}" è pronto.

Chiedimi qualunque cosa!

); } const isUser = msg.role === 'user'; return (
{!isUser && (
)}
{msg.content}

{msg.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}

); })} {isTyping && (
)}
{showPlusMenu && (
)}
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" />
)} {/* HOW IT WORKS STEP - AGGIORNATA CON LE TUE SPIEGAZIONI */} {step === AppStep.HOW_IT_WORKS && (

Architettura GraphRAG

Un sistema di recupero deterministico potenziato da orchestratori LLM.

{/* Step 1 */}

Ingestion & Struttura

Il PDF non viene solo "letto", ma processato per estrarre la gerarchia logica. Utilizziamo librerie specializzate per ricostruire tabelle, titoli e sezioni, garantendo che l'informazione non perda mai il suo contesto originale durante la scomposizione.

{/* Step 2 */}

Chunking

Applichiamo una strategia di chunking ricorsivo. 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.

{/* Step 3 */}

Knowledge Graph (Neo4j)

I chunk e le entità vengono mappati in Neo4j. 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.

{/* Step 4 */}

LangGraph

L'intero flusso è gestito da LangGraph. 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.

{/* Sezione Strategia Ibrida */}

Recupero Ibrido & Intelligente

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.

Routing

Mistral come Router Strategico

Utilizziamo Mistral per decidere la via di recupero. Se la domanda contiene entità specifiche già mappate nel DB, Mistral formula una Query Cypher basata sulle NE estratte, ottenendo dati certi in millisecondi direttamente dal database a grafo. Altrimenti, si prioritizza una strategia di recupero vettoriale, o ibrida.

Vector

Ricerca Vettoriale

Il sistema attiva una ricerca vettoriale quando il router identifica quesiti di natura concettuale o descrittiva. Utilizziamo il modello multilingual-e5-large (1024 dim), permettendo di individuare correlazioni semantiche profonde tra la domanda e il documento.

Entities

Named Entity Extraction (GLiNER)

Sfruttiamo GLiNER 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.

Rerank

Reranking-v2-m3

I risultati grezzi del database vengono filtrati da un Cross-Encoder (BGE-Reranker-v2-m3). Questo modello ri-analizza la pertinenza di ogni chunk rispetto alla domanda originale, scartando il "rumore" e passando solo il contesto più pertinente.

Ricerca Globale

Se la ricerca vettoriale sul documento attivo produce uno score di pertinenza basso (inferiore a 0.7), il sistema attiva automaticamente una Ricerca globale. Questa scansione estende l'analisi a tutti i documenti precedentemente indicizzati nel database Neo4j per verificare se l'informazione necessaria è presente in altri PDF.

Sintesi Finale

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.

)}
{/* Developer Footer */} {isInitialPage && (

Developed by Valerio Botto

)}
); }; export default App;