| import React, { useState, useEffect } from 'react'; |
| import { Send, Settings as SettingsIcon, GraduationCap, Mic } from 'lucide-react'; |
| import Settings from './components/Settings'; |
| import ChatWindow from './components/ChatWindow'; |
| import VoiceSessionModal from './components/VoiceSessionModal'; |
| import './App.css'; |
|
|
| const API_BASE = window.location.origin === 'http://localhost:5173' ? 'http://localhost:8000' : ''; |
|
|
| const samples = { |
| confusion: [ |
| "Wait, why does a negative times a negative make a positive? I don't get it.", |
| "I'm looking at this cell diagram and I can't tell the difference between the cell wall and the cell membrane.", |
| "Our teacher said the Earth is tilted, but how does that make summer and winter? It doesn't make sense.", |
| "Is a virus alive or is it not? My textbook says both and I'm really mixed up.", |
| "What is the difference between a variable and a constant in algebra? I'm lost.", |
| "Why does dividing by a fraction mean multiplying by its reciprocal? It seems arbitrary.", |
| "What is the difference between speed and velocity? They sound like the same thing.", |
| "Why is the mitochondria called the powerhouse of the cell? What does it actually do?" |
| ], |
| frustration: [ |
| "I've tried to solve this quadratic equation three times using the formula, but I keep getting a negative under the square root!", |
| "My science experiment failed again! The volcano didn't bubble at all and I did everything exactly right!", |
| "This long division with decimals is taking forever and I keep getting the wrong remainder! I hate this!", |
| "This word problem about two trains leaving different cities is making my head spin. I hate word problems!", |
| "I don't understand how to convert grams to moles. I keep getting the wrong conversion factor and it's so frustrating!", |
| "I've tried balancing this chemical equation five times and the numbers never match up!", |
| "Im trying to draw this ray diagram for a concave lens and the lines are crossing in the wrong place. I give up!", |
| "This physics problem about friction has too many variables and I don't even know where to start!" |
| ], |
| boredom: [ |
| "Ugh, why do we have to learn about sedimentary rocks? They just sit there. Who cares?", |
| "This math worksheet is just 50 of the same exact addition problems. This is so boring.", |
| "We are just copying definitions of different math properties from the board. This is so boring.", |
| "Another lecture on the phases of mitosis... we've covered this three years in a row now.", |
| "I finished all my science reading early. There's nothing else to do except stare at the wall.", |
| "We have to measure the temperature of this water every two minutes for an hour. This is so tedious.", |
| "Calculating the area of twenty slightly different rectangles is putting me to sleep.", |
| "This lecture on cell organelles is just slides of definitions. I'm falling asleep." |
| ], |
| confidence: [ |
| "I totally mastered multiplying fractions! Give me a hard practice problem to try!", |
| "I just derived the formula for the volume of a sphere all by myself!", |
| "I know exactly how to balance any redox reaction now. Try me!", |
| "I got a perfect score on the calculus midterm today! I really understand derivatives now!", |
| "I can explain the entire water cycle in my sleep! Evaporation, condensation, precipitation, easy!", |
| "I just solved the hardest logic puzzle in the workbook on my very first try!", |
| "I can calculate the trajectory of a projectile in my head now, it's so easy!", |
| "I fully understand how DNA replication works and could draw every step from memory!" |
| ], |
| neutral: [ |
| "How do I calculate the hypotenuse of a right triangle when the sides are 3 and 4?", |
| "What are the three main types of rocks found in the Earth's crust?", |
| "Can you explain how photosynthesis converts sunlight into chemical energy?", |
| "What is the chemical formula for photosynthesis and cellular respiration?", |
| "How do you find the slope of a line from two points on a graph?", |
| "What is the difference between an isotope and an ion in chemistry?", |
| "Could you list the steps of the scientific method in order?", |
| "What is the value of the constant pi, and how is it calculated?" |
| ] |
| }; |
|
|
| export default function App() { |
| const [apiKey, setApiKeyState] = useState(() => localStorage.getItem('gemini_api_key') || ''); |
| const [selectedSampleCategory, setSelectedSampleCategory] = useState('confusion'); |
| const [message, setMessage] = useState(''); |
| const [loading, setLoading] = useState(false); |
| const [error, setError] = useState(null); |
| const [history, setHistory] = useState([]); |
| const [showSettings, setShowSettings] = useState(false); |
| const [showVoiceModal, setShowVoiceModal] = useState(false); |
| const [backendStatus, setBackendStatus] = useState({ |
| status: 'loading', |
| gemini_api_key_configured: false |
| }); |
|
|
| const setApiKey = (val) => { |
| setApiKeyState(val); |
| localStorage.setItem('gemini_api_key', val); |
| }; |
|
|
|
|
| const checkBackendStatus = async () => { |
| try { |
| const res = await fetch(`${API_BASE}/api/status`); |
| if (res.ok) { |
| const data = await res.json(); |
| setBackendStatus(data); |
| return data.status; |
| } |
| } catch (err) { |
| console.error("Failed to fetch backend status:", err); |
| setBackendStatus({ |
| status: 'failed', |
| gemini_api_key_configured: false |
| }); |
| } |
| return 'failed'; |
| }; |
|
|
| |
| useEffect(() => { |
| checkBackendStatus(); |
| }, []); |
|
|
| const handleSubmit = async (e, followUpText = null) => { |
| if (e) e.preventDefault(); |
| |
| const activeMessage = followUpText !== null ? followUpText : message; |
| if (!activeMessage.trim() || loading) return; |
|
|
| setLoading(true); |
| setError(null); |
|
|
| |
| let currentHistory = history; |
| if (followUpText === null) { |
| currentHistory = []; |
| setHistory([]); |
| } |
|
|
| |
| const nextHistory = [...currentHistory, { role: 'user', content: activeMessage }]; |
| setHistory(nextHistory); |
|
|
| try { |
| |
| const optimizedHistoryPayload = currentHistory.map(msg => ({ |
| role: msg.role, |
| content: msg.content |
| })); |
|
|
| const res = await fetch(`${API_BASE}/api/chat`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| message: activeMessage, |
| gemini_api_key: apiKey || null, |
| history: optimizedHistoryPayload |
| }) |
| }); |
|
|
| if (!res.ok) { |
| const data = await res.json(); |
| throw new Error(data.detail || `Server error: ${res.statusText}`); |
| } |
|
|
| const data = await res.json(); |
|
|
| |
| setHistory([ |
| ...nextHistory, |
| { |
| role: 'assistant', |
| content: data.response, |
| sentiment: data.sentiment, |
| latency: data.latency, |
| tokens: data.tokens, |
| cost: data.cost, |
| prompt_context: data.prompt_context |
| } |
| ]); |
|
|
| if (followUpText === null) { |
| setMessage(''); |
| } |
| } catch (err) { |
| setError(err.message || 'An unexpected error occurred while communicating with the server.'); |
| console.error(err); |
| } finally { |
| setLoading(false); |
| } |
| }; |
|
|
| const handleQuickPrompt = (promptText) => { |
| setMessage(promptText); |
| }; |
|
|
| const clearChat = () => { |
| setHistory([]); |
| setError(null); |
| }; |
|
|
| return ( |
| <div className="app-container"> |
| {/* Header */} |
| <header className="app-header"> |
| <div className="header-title-section"> |
| <h1 style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}> |
| <GraduationCap size={36} color="var(--primary)" /> |
| Socratic Sentiment Tutor |
| </h1> |
| <p>Context-aware sentiment detection & guidance chatbot (Option B)</p> |
| </div> |
| |
| <div className="header-status"> |
| {history.length > 0 && ( |
| <button className="settings-toggle-btn" onClick={clearChat} style={{ border: '1px solid rgba(244, 63, 94, 0.2)', color: 'var(--color-frustrated)' }}> |
| Clear Conversation |
| </button> |
| )} |
| |
| <button |
| className="settings-toggle-btn" |
| onClick={() => setShowSettings(!showSettings)} |
| > |
| <SettingsIcon size={18} /> |
| Configure Tutor |
| </button> |
| </div> |
| </header> |
| |
| {/* Settings Panel */} |
| {showSettings && ( |
| <Settings |
| apiKey={apiKey} |
| setApiKey={setApiKey} |
| backendStatus={backendStatus} |
| checkBackendStatus={checkBackendStatus} |
| /> |
| )} |
| |
| {/* Main Grid */} |
| <main className="main-grid" style={{ gridTemplateColumns: '1fr' }}> |
| {/* Chat input box */} |
| <section className="query-card"> |
| <form onSubmit={handleSubmit} className="query-input-wrapper"> |
| <textarea |
| value={message} |
| onChange={(e) => setMessage(e.target.value)} |
| placeholder="Ask an educational question... (e.g. 'I don't understand how recursion works!')" |
| className="query-textarea" |
| onKeyDown={(e) => { |
| if (e.key === 'Enter' && !e.shiftKey) { |
| e.preventDefault(); |
| handleSubmit(); |
| } |
| }} |
| /> |
| <div style={{ display: 'flex', gap: '0.5rem' }}> |
| <button |
| type="submit" |
| className="send-button" |
| disabled={loading || !message.trim()} |
| > |
| <Send size={18} /> |
| Start Conversation |
| </button> |
| <button |
| type="button" |
| className="send-button voice-btn" |
| onClick={() => setShowVoiceModal(true)} |
| title="Start voice session" |
| style={{ |
| background: 'linear-gradient(135deg, var(--secondary), #7c3aed)', |
| padding: '0.6rem 1.1rem' |
| }} |
| > |
| <Mic size={18} /> |
| Voice Session |
| </button> |
| </div> |
| </form> |
| |
| {/* Quick prompts */} |
| <div className="quick-prompts-section" style={{ display: 'flex', flexDirection: 'column', gap: '0.8rem', marginTop: '0.5rem' }}> |
| <div style={{ display: 'flex', alignItems: 'center', gap: '0.8rem', flexWrap: 'wrap', borderBottom: '1px solid var(--border-color)', paddingBottom: '0.6rem' }}> |
| <span className="quick-prompt-label" style={{ marginRight: 'auto' }}>Sample Initial Inquiries:</span> |
| <div style={{ display: 'flex', gap: '0.4rem' }}> |
| {Object.keys(samples).map((cat) => ( |
| <button |
| key={cat} |
| type="button" |
| onClick={() => setSelectedSampleCategory(cat)} |
| style={{ |
| background: selectedSampleCategory === cat ? 'var(--primary)' : 'rgba(255,255,255,0.03)', |
| border: '1px solid ' + (selectedSampleCategory === cat ? 'var(--primary)' : 'var(--border-color)'), |
| color: selectedSampleCategory === cat ? '#fff' : 'var(--text-secondary)', |
| padding: '0.3rem 0.75rem', |
| borderRadius: '16px', |
| fontSize: '0.8rem', |
| fontWeight: 600, |
| cursor: 'pointer', |
| textTransform: 'capitalize', |
| transition: 'var(--transition)' |
| }} |
| > |
| {cat} |
| </button> |
| ))} |
| </div> |
| </div> |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '0.5rem' }}> |
| {samples[selectedSampleCategory].map((promptText, i) => ( |
| <button |
| key={i} |
| type="button" |
| className="quick-prompt-btn" |
| onClick={() => handleQuickPrompt(promptText)} |
| style={{ textAlign: 'left', width: '100%', whiteSpace: 'normal', lineHeight: '1.4', padding: '0.6rem 1rem', borderRadius: '8px' }} |
| > |
| {promptText} |
| </button> |
| ))} |
| </div> |
| </div> |
| </section> |
| |
| {/* Conversation Chat Output */} |
| <section style={{ width: '100%' }}> |
| <ChatWindow |
| history={history} |
| loading={loading} |
| error={error} |
| onSubmitFollowUp={(text) => handleSubmit(null, text)} |
| /> |
| </section> |
| </main> |
| |
| {/* Voice Session Overlay Modal */} |
| <VoiceSessionModal |
| isOpen={showVoiceModal} |
| onClose={() => setShowVoiceModal(false)} |
| apiKey={apiKey} |
| /> |
| </div> |
| ); |
| } |
|
|