| import { useState, useRef, useEffect } from 'react'; |
| import ChatMessage from './components/ChatMessage'; |
| import ChatInput from './components/ChatInput'; |
| import { askQuestion } from './api/chat'; |
| import './App.css'; |
|
|
| const SUGGESTIONS = [ |
| 'What is the capital of Australia?', |
| 'Who developed the theory of relativity?', |
| 'How does photosynthesis work?', |
| ]; |
|
|
| const PIPELINE_STEPS = [ |
| { |
| label: 'Load', model: 'Docling', color: '#7c3aed', |
| icon: ( |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> |
| <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/> |
| <polyline points="14 2 14 8 20 8"/> |
| </svg> |
| ), |
| }, |
| { |
| label: 'Chunk', model: 'chonkie', color: '#dc2626', |
| icon: ( |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> |
| <circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/> |
| <line x1="20" y1="4" x2="8.12" y2="15.88"/> |
| <line x1="14.47" y1="14.48" x2="20" y2="20"/> |
| <line x1="8.12" y1="8.12" x2="12" y2="12"/> |
| </svg> |
| ), |
| }, |
| { |
| label: 'Embed', model: 'bge-m3', color: '#2563eb', |
| icon: ( |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5" strokeLinecap="round"> |
| <line x1="4" y1="9" x2="20" y2="9"/> |
| <line x1="4" y1="15" x2="20" y2="15"/> |
| <line x1="10" y1="3" x2="8" y2="21"/> |
| <line x1="16" y1="3" x2="14" y2="21"/> |
| </svg> |
| ), |
| }, |
| { |
| label: 'Retrieve', model: 'Qdrant · bge-reranker-v2-m3', color: '#059669', |
| icon: ( |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> |
| <ellipse cx="12" cy="5" rx="9" ry="3"/> |
| <path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/> |
| <path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/> |
| </svg> |
| ), |
| }, |
| { |
| label: 'Generate', model: 'Llama 3.3 70B', color: '#d97706', |
| icon: ( |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> |
| <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/> |
| </svg> |
| ), |
| }, |
| ]; |
|
|
| export default function App() { |
| const [messages, setMessages] = useState([]); |
| const [loading, setLoading] = useState(false); |
| const [dark, setDark] = useState(true); |
| const bottomRef = useRef(null); |
|
|
| useEffect(() => { |
| bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); |
| }, [messages, loading]); |
|
|
| const handleSubmit = async (query) => { |
| if (!query.trim() || loading) return; |
| setMessages((prev) => [...prev, { role: 'user', content: query }]); |
| setLoading(true); |
| try { |
| const data = await askQuestion(query, false); |
| if (data.needs_confirmation) { |
| setMessages((prev) => [...prev, { |
| role: 'assistant', |
| type: 'confirmation', |
| content: data.message, |
| pendingQuery: query, |
| }]); |
| } else { |
| setMessages((prev) => [...prev, { |
| role: 'assistant', |
| content: data.answer, |
| sources: data.sources, |
| }]); |
| } |
| } catch { |
| setMessages((prev) => [...prev, { |
| role: 'assistant', |
| content: 'Could not reach the backend. Make sure the API server is running on port 8000.', |
| sources: [], |
| error: true, |
| }]); |
| } finally { |
| setLoading(false); |
| } |
| }; |
|
|
| const handleConfirm = async (pendingQuery) => { |
| setMessages((prev) => prev.map((m) => |
| m.type === 'confirmation' ? { ...m, type: 'confirmed', content: m.content } : m |
| )); |
| setMessages((prev) => [...prev, { role: 'user', content: 'Yes' }]); |
| setLoading(true); |
| try { |
| const data = await askQuestion(pendingQuery, true); |
| setMessages((prev) => [...prev, { |
| role: 'assistant', |
| content: data.answer, |
| sources: data.sources, |
| }]); |
| } catch { |
| setMessages((prev) => [...prev, { |
| role: 'assistant', |
| content: 'Could not reach the backend.', |
| sources: [], |
| error: true, |
| }]); |
| } finally { |
| setLoading(false); |
| } |
| }; |
|
|
| const handleDecline = () => { |
| setMessages((prev) => prev.map((m) => |
| m.type === 'confirmation' ? { ...m, type: 'declined', content: m.content } : m |
| )); |
| setMessages((prev) => [...prev, |
| { role: 'user', content: 'No' }, |
| { role: 'assistant', content: 'Okay, skipping this one.', sources: [] }, |
| ]); |
| }; |
|
|
| const handleNewChat = () => setMessages([]); |
|
|
| return ( |
| <div className={`layout ${dark ? '' : 'light'}`}> |
| <aside className="sidebar"> |
| <div className="sidebar-logo"> |
| <span className="logo-bolt">⚡</span> |
| <span className="logo-text">ChatJio</span> |
| </div> |
| |
| <button className="new-chat-btn" onClick={handleNewChat}> |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"> |
| <line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/> |
| </svg> |
| New chat |
| </button> |
| |
| <div className="sidebar-section"> |
| <p className="sidebar-label">RAG PIPELINE</p> |
| <div className="pipeline"> |
| {PIPELINE_STEPS.map((step, i) => ( |
| <div key={step.label} className="pipeline-item"> |
| <div className="pipeline-step"> |
| <div className="step-icon" style={{ background: step.color }}> |
| {step.icon} |
| </div> |
| <div className="step-info"> |
| <span className="step-label">{step.label}</span> |
| <span className="step-model">{step.model}</span> |
| </div> |
| </div> |
| {i < PIPELINE_STEPS.length - 1 && <div className="pipeline-connector" />} |
| </div> |
| ))} |
| </div> |
| </div> |
| |
| <div className="dataset-card"> |
| <div className="dataset-header"> |
| <svg width="13" height="13" 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="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/> |
| <path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/> |
| </svg> |
| DATASET |
| </div> |
| <p className="dataset-name">rag-mini-wikipedia</p> |
| <div className="dataset-tags"> |
| <span className="dataset-tag">3,200 passages</span> |
| <span className="dataset-tag">sample</span> |
| </div> |
| <p className="dataset-source">Source · Hugging Face</p> |
| </div> |
| |
| <div className="sidebar-footer"> |
| <button className="dark-toggle" onClick={() => setDark((d) => !d)}> |
| <span className="toggle-left"> |
| {dark |
| ? <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg> |
| : <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg> |
| } |
| {dark ? 'Dark' : 'Light'} |
| </span> |
| <span className="toggle-right">toggle</span> |
| </button> |
| <div className="model-badge"> |
| <span className="model-dot" /> |
| Groq · Free tier |
| </div> |
| <p className="sidebar-sub">All models run locally or free</p> |
| </div> |
| </aside> |
| |
| <main className="chat-main"> |
| <div className="messages-area"> |
| {messages.length === 0 ? ( |
| <div className="welcome"> |
| <div className="welcome-glow" /> |
| <div className="welcome-bolt">⚡</div> |
| <h1 className="welcome-title">ChatJio</h1> |
| <p className="welcome-sub">Ask anything about the mini-wikipedia knowledge base</p> |
| <div className="suggestions"> |
| {SUGGESTIONS.map((s, i) => ( |
| <button key={i} className="suggestion" onClick={() => handleSubmit(s)}> |
| <svg className="suggestion-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"> |
| <circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/> |
| </svg> |
| {s} |
| </button> |
| ))} |
| </div> |
| </div> |
| ) : ( |
| <div className="messages"> |
| {messages.map((msg, i) => ( |
| <ChatMessage |
| key={i} |
| message={msg} |
| onConfirm={msg.type === 'confirmation' ? () => handleConfirm(msg.pendingQuery) : undefined} |
| onDecline={msg.type === 'confirmation' ? handleDecline : undefined} |
| /> |
| ))} |
| {loading && ( |
| <div className="msg-row msg-row--assistant"> |
| <div className="avatar avatar--bot">J</div> |
| <div className="bubble bubble--assistant"> |
| <span className="typing-dot" /> |
| <span className="typing-dot" /> |
| <span className="typing-dot" /> |
| </div> |
| </div> |
| )} |
| <div ref={bottomRef} /> |
| </div> |
| )} |
| </div> |
| <ChatInput onSubmit={handleSubmit} loading={loading} /> |
| </main> |
| </div> |
| ); |
| } |
|
|