ChatJio / frontend /src /App.jsx
sehscape's picture
deploy: initial ChatJio deployment for HuggingFace Spaces
f3269f9
Raw
History Blame Contribute Delete
10.4 kB
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>
);
}