From eb0eb26f4cefa4880c895ff017f312e8674f9b73 Mon Sep 17 00:00:00 2001 From: karpathy Date: Sat, 22 Nov 2025 14:27:53 -0800 Subject: v0 --- frontend/src/App.css | 15 +++ frontend/src/App.jsx | 120 +++++++++++++++++++++++ frontend/src/api.js | 68 +++++++++++++ frontend/src/assets/react.svg | 1 + frontend/src/components/ChatInterface.css | 149 +++++++++++++++++++++++++++++ frontend/src/components/ChatInterface.jsx | 117 +++++++++++++++++++++++ frontend/src/components/Sidebar.css | 78 +++++++++++++++ frontend/src/components/Sidebar.jsx | 43 +++++++++ frontend/src/components/Stage1.css | 65 +++++++++++++ frontend/src/components/Stage1.jsx | 36 +++++++ frontend/src/components/Stage2.css | 153 ++++++++++++++++++++++++++++++ frontend/src/components/Stage2.jsx | 99 +++++++++++++++++++ frontend/src/components/Stage3.css | 25 +++++ frontend/src/components/Stage3.jsx | 22 +++++ frontend/src/index.css | 98 +++++++++++++++++++ frontend/src/main.jsx | 10 ++ 16 files changed, 1099 insertions(+) create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.jsx create mode 100644 frontend/src/api.js create mode 100644 frontend/src/assets/react.svg create mode 100644 frontend/src/components/ChatInterface.css create mode 100644 frontend/src/components/ChatInterface.jsx create mode 100644 frontend/src/components/Sidebar.css create mode 100644 frontend/src/components/Sidebar.jsx create mode 100644 frontend/src/components/Stage1.css create mode 100644 frontend/src/components/Stage1.jsx create mode 100644 frontend/src/components/Stage2.css create mode 100644 frontend/src/components/Stage2.jsx create mode 100644 frontend/src/components/Stage3.css create mode 100644 frontend/src/components/Stage3.jsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.jsx (limited to 'frontend/src') diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..6863cd4 --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,15 @@ +* { + box-sizing: border-box; +} + +.app { + display: flex; + height: 100vh; + width: 100vw; + overflow: hidden; + background: #ffffff; + color: #333; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..0396e3a --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,120 @@ +import { useState, useEffect } from 'react'; +import Sidebar from './components/Sidebar'; +import ChatInterface from './components/ChatInterface'; +import { api } from './api'; +import './App.css'; + +function App() { + const [conversations, setConversations] = useState([]); + const [currentConversationId, setCurrentConversationId] = useState(null); + const [currentConversation, setCurrentConversation] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + // Load conversations on mount + useEffect(() => { + loadConversations(); + }, []); + + // Load conversation details when selected + useEffect(() => { + if (currentConversationId) { + loadConversation(currentConversationId); + } + }, [currentConversationId]); + + const loadConversations = async () => { + try { + const convs = await api.listConversations(); + setConversations(convs); + } catch (error) { + console.error('Failed to load conversations:', error); + } + }; + + const loadConversation = async (id) => { + try { + const conv = await api.getConversation(id); + setCurrentConversation(conv); + } catch (error) { + console.error('Failed to load conversation:', error); + } + }; + + const handleNewConversation = async () => { + try { + const newConv = await api.createConversation(); + setConversations([ + { id: newConv.id, created_at: newConv.created_at, message_count: 0 }, + ...conversations, + ]); + setCurrentConversationId(newConv.id); + } catch (error) { + console.error('Failed to create conversation:', error); + } + }; + + const handleSelectConversation = (id) => { + setCurrentConversationId(id); + }; + + const handleSendMessage = async (content) => { + if (!currentConversationId) return; + + setIsLoading(true); + try { + // Optimistically add user message to UI + const userMessage = { role: 'user', content }; + setCurrentConversation((prev) => ({ + ...prev, + messages: [...prev.messages, userMessage], + })); + + // Send message and get council response + const response = await api.sendMessage(currentConversationId, content); + + // Add assistant message to UI + const assistantMessage = { + role: 'assistant', + stage1: response.stage1, + stage2: response.stage2, + stage3: response.stage3, + metadata: response.metadata, + }; + + setCurrentConversation((prev) => ({ + ...prev, + messages: [...prev.messages, assistantMessage], + })); + + // Reload conversations list to update message count + await loadConversations(); + } catch (error) { + console.error('Failed to send message:', error); + // Remove optimistic user message on error + setCurrentConversation((prev) => ({ + ...prev, + messages: prev.messages.slice(0, -1), + })); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ + +
+ ); +} + +export default App; diff --git a/frontend/src/api.js b/frontend/src/api.js new file mode 100644 index 0000000..479f0ef --- /dev/null +++ b/frontend/src/api.js @@ -0,0 +1,68 @@ +/** + * API client for the LLM Council backend. + */ + +const API_BASE = 'http://localhost:8001'; + +export const api = { + /** + * List all conversations. + */ + async listConversations() { + const response = await fetch(`${API_BASE}/api/conversations`); + if (!response.ok) { + throw new Error('Failed to list conversations'); + } + return response.json(); + }, + + /** + * Create a new conversation. + */ + async createConversation() { + const response = await fetch(`${API_BASE}/api/conversations`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({}), + }); + if (!response.ok) { + throw new Error('Failed to create conversation'); + } + return response.json(); + }, + + /** + * Get a specific conversation. + */ + async getConversation(conversationId) { + const response = await fetch( + `${API_BASE}/api/conversations/${conversationId}` + ); + if (!response.ok) { + throw new Error('Failed to get conversation'); + } + return response.json(); + }, + + /** + * Send a message in a conversation. + */ + async sendMessage(conversationId, content) { + const response = await fetch( + `${API_BASE}/api/conversations/${conversationId}/message`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ content }), + } + ); + if (!response.ok) { + throw new Error('Failed to send message'); + } + return response.json(); + }, +}; diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/ChatInterface.css b/frontend/src/components/ChatInterface.css new file mode 100644 index 0000000..531d2a3 --- /dev/null +++ b/frontend/src/components/ChatInterface.css @@ -0,0 +1,149 @@ +.chat-interface { + flex: 1; + display: flex; + flex-direction: column; + height: 100vh; + background: #ffffff; +} + +.messages-container { + flex: 1; + overflow-y: auto; + padding: 24px; +} + +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: #666; + text-align: center; +} + +.empty-state h2 { + margin: 0 0 8px 0; + font-size: 24px; + color: #333; +} + +.empty-state p { + margin: 0; + font-size: 16px; +} + +.message-group { + margin-bottom: 32px; +} + +.user-message, +.assistant-message { + margin-bottom: 16px; +} + +.message-label { + font-size: 12px; + font-weight: 600; + color: #666; + margin-bottom: 8px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.user-message .message-content { + background: #f0f7ff; + padding: 16px; + border-radius: 8px; + border: 1px solid #d0e7ff; + color: #333; + line-height: 1.6; + max-width: 80%; + white-space: pre-wrap; +} + +.loading-indicator { + display: flex; + align-items: center; + gap: 12px; + padding: 16px; + color: #666; + font-size: 14px; +} + +.spinner { + width: 20px; + height: 20px; + border: 2px solid #e0e0e0; + border-top-color: #4a90e2; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.input-form { + display: flex; + align-items: flex-end; + gap: 12px; + padding: 24px; + border-top: 1px solid #e0e0e0; + background: #fafafa; +} + +.message-input { + flex: 1; + padding: 14px; + background: #ffffff; + border: 1px solid #d0d0d0; + border-radius: 8px; + color: #333; + font-size: 15px; + font-family: inherit; + line-height: 1.5; + outline: none; + resize: vertical; + min-height: 80px; + max-height: 300px; +} + +.message-input:focus { + border-color: #4a90e2; + box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1); +} + +.message-input:disabled { + opacity: 0.5; + cursor: not-allowed; + background: #f5f5f5; +} + +.send-button { + padding: 14px 28px; + background: #4a90e2; + border: 1px solid #4a90e2; + border-radius: 8px; + color: #fff; + font-size: 15px; + font-weight: 600; + cursor: pointer; + transition: background 0.2s; + white-space: nowrap; + align-self: flex-end; +} + +.send-button:hover:not(:disabled) { + background: #357abd; + border-color: #357abd; +} + +.send-button:disabled { + opacity: 0.5; + cursor: not-allowed; + background: #ccc; + border-color: #ccc; +} diff --git a/frontend/src/components/ChatInterface.jsx b/frontend/src/components/ChatInterface.jsx new file mode 100644 index 0000000..951183f --- /dev/null +++ b/frontend/src/components/ChatInterface.jsx @@ -0,0 +1,117 @@ +import { useState, useEffect, useRef } from 'react'; +import ReactMarkdown from 'react-markdown'; +import Stage1 from './Stage1'; +import Stage2 from './Stage2'; +import Stage3 from './Stage3'; +import './ChatInterface.css'; + +export default function ChatInterface({ + conversation, + onSendMessage, + isLoading, +}) { + const [input, setInput] = useState(''); + const messagesEndRef = useRef(null); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + useEffect(() => { + scrollToBottom(); + }, [conversation]); + + const handleSubmit = (e) => { + e.preventDefault(); + if (input.trim() && !isLoading) { + onSendMessage(input); + setInput(''); + } + }; + + const handleKeyDown = (e) => { + // Submit on Enter (without Shift) + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSubmit(e); + } + }; + + if (!conversation) { + return ( +
+
+

Welcome to LLM Council

+

Create a new conversation to get started

+
+
+ ); + } + + return ( +
+
+ {conversation.messages.length === 0 ? ( +
+

Start a conversation

+

Ask a question to consult the LLM Council

+
+ ) : ( + conversation.messages.map((msg, index) => ( +
+ {msg.role === 'user' ? ( +
+
You
+
+
+ {msg.content} +
+
+
+ ) : ( +
+
LLM Council
+ + + +
+ )} +
+ )) + )} + + {isLoading && ( +
+
+ Consulting the council... +
+ )} + +
+
+ +
+