Spaces:
Runtime error
Runtime error
feat: synchronize text-to-sql-bot codebase with Hugging Face Space repository, including Docker build configurations
6086e71 | import { useState, useEffect, useCallback } from 'react'; | |
| import Sidebar from './Sidebar'; | |
| import Topbar from './Topbar'; | |
| import ChatWindow from '../chat/ChatWindow'; | |
| import Composer from '../chat/Composer'; | |
| import Toast from '../ui/Toast'; | |
| import useChatStore from '../../store/useChatStore'; | |
| import { detectConversational, getConversationalResponse, streamChat, fetchSchema } from '../../api/client'; | |
| const STAGE_TO_STEP = { | |
| 'classifying': 0, 'Understanding': 0, | |
| 'retrieving': 0, 'Retrieving': 0, | |
| 'Generating': 1, 'SQL': 1, 'Regenerating': 1, | |
| 'Validating': 2, 'Validation': 2, | |
| 'Executing': 3, 'Execution': 3, | |
| 'Preparing': 4, 'response': 4, 'Generating insights': 4, | |
| }; | |
| export default function AppShell() { | |
| const [sidebarOpen, setSidebarOpen] = useState(false); | |
| const store = useChatStore(); | |
| const { addUserMessage, updateMessage, setIsSending, addToast, setSchemaTables, pushContext } = store; | |
| // Global keyboard shortcuts | |
| useEffect(() => { | |
| const handler = (e) => { | |
| // Ctrl+N or Cmd+N — new chat | |
| if ((e.ctrlKey || e.metaKey) && e.key === 'n') { | |
| e.preventDefault(); | |
| store.newChat(); | |
| } | |
| // Ctrl+/ — focus composer | |
| if ((e.ctrlKey || e.metaKey) && e.key === '/') { | |
| e.preventDefault(); | |
| document.querySelector('#composer-input')?.focus(); | |
| } | |
| }; | |
| window.addEventListener('keydown', handler); | |
| return () => window.removeEventListener('keydown', handler); | |
| }, [store]); | |
| const handleSubmit = useCallback(async (question) => { | |
| if (!question?.trim() || store.isSending) return; | |
| const { chat, pendingId } = addUserMessage(question); | |
| setIsSending(true); | |
| // ── Conversational fast-path (client-side) ───────────── | |
| const intentType = detectConversational(question); | |
| if (intentType) { | |
| updateMessage(chat.id, pendingId, { _chatMode: true }); | |
| const response = getConversationalResponse(intentType); | |
| const tokens = response.split(/(\s+)/); | |
| let text = ''; | |
| for (const token of tokens) { | |
| text += token; | |
| updateMessage(chat.id, pendingId, { pending: false, streaming: true, streamText: text, data: { message: text } }); | |
| await new Promise(r => setTimeout(r, token.trim() ? 22 : 8)); | |
| } | |
| updateMessage(chat.id, pendingId, { pending: false, streaming: false, streamText: text, data: { message: text } }); | |
| setIsSending(false); | |
| return; | |
| } | |
| // ── Full AI pipeline (SQL mode) ───────────────────────── | |
| const assembled = { intent: '', sql: '', explanation: '', answer: [], message: '', insights: [], follow_ups: [], row_count: 0, execution_time_ms: 0 }; | |
| let _accumulatedSql = ''; | |
| let _accumulatedText = ''; | |
| let _currentStep = 0; | |
| try { | |
| await streamChat({ | |
| question: store.selectedSchema !== 'default' ? `${question}\n\nUse table: ${store.selectedSchema}` : question, | |
| history: (store.getActiveChat()?.context ?? []).slice(-6), | |
| onChunk: (type, chunk) => { | |
| switch (type) { | |
| case 'stage': { | |
| const stepKey = Object.keys(STAGE_TO_STEP).find(k => chunk.message?.includes(k)); | |
| _currentStep = stepKey ? STAGE_TO_STEP[stepKey] : _currentStep; | |
| updateMessage(chat.id, pendingId, { _stageText: chunk.message, _pipelineStep: _currentStep, pending: true, streaming: true }); | |
| break; | |
| } | |
| case 'token': | |
| if (chunk.token) { | |
| if (_currentStep <= 1 && assembled.intent !== '') { | |
| _accumulatedSql += chunk.token; | |
| updateMessage(chat.id, pendingId, { _streamingSql: _accumulatedSql, streaming: true, pending: false, _pipelineStep: 1 }); | |
| } else if (_currentStep >= 3) { | |
| _accumulatedText += chunk.token; | |
| updateMessage(chat.id, pendingId, { streamText: _accumulatedText, streaming: true, pending: false }); | |
| } | |
| } | |
| break; | |
| case 'summary_token': | |
| // Progressive summary streaming — tokens arrive as LLM generates | |
| if (chunk.token) { | |
| _accumulatedText += chunk.token; | |
| _currentStep = 4; | |
| updateMessage(chat.id, pendingId, { | |
| streamText: _accumulatedText, | |
| streaming: true, | |
| pending: false, | |
| _pipelineStep: 4, | |
| }); | |
| } | |
| break; | |
| case 'intent': | |
| assembled.intent = chunk.intent ?? ''; | |
| _currentStep = 1; | |
| updateMessage(chat.id, pendingId, { _pipelineStep: 1, pending: false, streaming: true }); | |
| break; | |
| case 'sql': | |
| assembled.sql = chunk.sql ?? _accumulatedSql; | |
| assembled.explanation = chunk.explanation ?? ''; | |
| _currentStep = 2; | |
| updateMessage(chat.id, pendingId, { | |
| data: { ...assembled }, | |
| _streamingSql: assembled.sql, | |
| _pipelineStep: 2, | |
| streaming: true | |
| }); | |
| break; | |
| case 'results': | |
| assembled.answer = chunk.data ?? []; | |
| assembled.row_count = chunk.row_count ?? 0; | |
| assembled.execution_time_ms = chunk.execution_time_ms ?? 0; | |
| _currentStep = 3; | |
| updateMessage(chat.id, pendingId, { data: { ...assembled }, _pipelineStep: 3, streaming: true }); | |
| break; | |
| case 'message': | |
| assembled.message = chunk.message ?? _accumulatedText; | |
| assembled.insights = chunk.insights ?? []; | |
| assembled.follow_ups = chunk.follow_ups ?? []; | |
| _currentStep = 4; | |
| updateMessage(chat.id, pendingId, { | |
| data: { ...assembled }, | |
| streamText: assembled.message || _accumulatedText, | |
| streaming: false, | |
| pending: false, | |
| _pipelineStep: 5 | |
| }); | |
| break; | |
| case 'error': | |
| updateMessage(chat.id, pendingId, { pending: false, streaming: false, error: chunk.error ?? 'An error occurred.' }); | |
| break; | |
| case 'done': | |
| assembled.execution_time_ms = assembled.execution_time_ms || chunk.total_time_ms || 0; | |
| break; | |
| } | |
| } | |
| }); | |
| if (assembled.sql) { | |
| pushContext(chat.id, { user: question, sql: assembled.sql }); | |
| addToast('Query complete', 'success'); | |
| } | |
| } catch (err) { | |
| const msg = err.message === 'Failed to fetch' | |
| ? 'Cannot reach the server. Make sure it\'s running.' | |
| : err.message; | |
| updateMessage(chat.id, pendingId, { pending: false, streaming: false, error: msg }); | |
| addToast('Query failed', 'error'); | |
| } finally { | |
| setIsSending(false); | |
| } | |
| }, [store, addUserMessage, updateMessage, setIsSending, addToast, pushContext]); | |
| // Listen for sidebar saved-query shortcuts | |
| useEffect(() => { | |
| const handler = (e) => handleSubmit(e.detail.query); | |
| window.addEventListener('plainsql:submit', handler); | |
| return () => window.removeEventListener('plainsql:submit', handler); | |
| }, [handleSubmit]); | |
| const handleRegenerate = useCallback((msgId) => { | |
| const chat = store.getActiveChat(); | |
| if (!chat) return; | |
| const idx = chat.messages.findIndex(m => m.id === msgId); | |
| if (idx < 1) return; | |
| const userMsg = chat.messages[idx - 1]; | |
| if (userMsg?.role !== 'user') return; | |
| // Remove old assistant message and re-submit | |
| useChatStore.setState(s => ({ | |
| chats: s.chats.map(c => c.id !== chat.id ? c : { ...c, messages: c.messages.filter(m => m.id !== msgId) }) | |
| })); | |
| handleSubmit(userMsg.content); | |
| }, [store, handleSubmit]); | |
| return ( | |
| <div className="flex h-full overflow-hidden" style={{ background: 'var(--surface-0)' }}> | |
| {/* Subtle background gradient */} | |
| <div className="fixed inset-0 pointer-events-none bg-grid opacity-60" /> | |
| <div className="fixed top-0 left-0 w-full h-full pointer-events-none" | |
| style={{ background: 'radial-gradient(ellipse 80% 50% at 50% -20%, rgba(59,130,246,0.06) 0%, transparent 60%)' }} /> | |
| <Sidebar open={sidebarOpen} onClose={() => setSidebarOpen(false)} /> | |
| <div className="flex flex-col flex-1 min-w-0 relative z-10"> | |
| <Topbar onMenuClick={() => setSidebarOpen(true)} /> | |
| <ChatWindow onPrompt={handleSubmit} onRegenerate={handleRegenerate} /> | |
| <Composer onSubmit={handleSubmit} /> | |
| </div> | |
| <Toast /> | |
| </div> | |
| ); | |
| } | |