diff options
| author | karpathy <andrej.karpathy@gmail.com> | 2025-11-22 14:27:53 -0800 |
|---|---|---|
| committer | karpathy <andrej.karpathy@gmail.com> | 2025-11-22 14:27:53 -0800 |
| commit | eb0eb26f4cefa4880c895ff017f312e8674f9b73 (patch) | |
| tree | ea20b736519a5b4149b0356fec93447eef950e6b /frontend/src | |
v0
Diffstat (limited to 'frontend/src')
| -rw-r--r-- | frontend/src/App.css | 15 | ||||
| -rw-r--r-- | frontend/src/App.jsx | 120 | ||||
| -rw-r--r-- | frontend/src/api.js | 68 | ||||
| -rw-r--r-- | frontend/src/assets/react.svg | 1 | ||||
| -rw-r--r-- | frontend/src/components/ChatInterface.css | 149 | ||||
| -rw-r--r-- | frontend/src/components/ChatInterface.jsx | 117 | ||||
| -rw-r--r-- | frontend/src/components/Sidebar.css | 78 | ||||
| -rw-r--r-- | frontend/src/components/Sidebar.jsx | 43 | ||||
| -rw-r--r-- | frontend/src/components/Stage1.css | 65 | ||||
| -rw-r--r-- | frontend/src/components/Stage1.jsx | 36 | ||||
| -rw-r--r-- | frontend/src/components/Stage2.css | 153 | ||||
| -rw-r--r-- | frontend/src/components/Stage2.jsx | 99 | ||||
| -rw-r--r-- | frontend/src/components/Stage3.css | 25 | ||||
| -rw-r--r-- | frontend/src/components/Stage3.jsx | 22 | ||||
| -rw-r--r-- | frontend/src/index.css | 98 | ||||
| -rw-r--r-- | frontend/src/main.jsx | 10 |
16 files changed, 1099 insertions, 0 deletions
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 ( + <div className="app"> + <Sidebar + conversations={conversations} + currentConversationId={currentConversationId} + onSelectConversation={handleSelectConversation} + onNewConversation={handleNewConversation} + /> + <ChatInterface + conversation={currentConversation} + onSendMessage={handleSendMessage} + isLoading={isLoading} + /> + </div> + ); +} + +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 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
\ 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 ( + <div className="chat-interface"> + <div className="empty-state"> + <h2>Welcome to LLM Council</h2> + <p>Create a new conversation to get started</p> + </div> + </div> + ); + } + + return ( + <div className="chat-interface"> + <div className="messages-container"> + {conversation.messages.length === 0 ? ( + <div className="empty-state"> + <h2>Start a conversation</h2> + <p>Ask a question to consult the LLM Council</p> + </div> + ) : ( + conversation.messages.map((msg, index) => ( + <div key={index} className="message-group"> + {msg.role === 'user' ? ( + <div className="user-message"> + <div className="message-label">You</div> + <div className="message-content"> + <div className="markdown-content"> + <ReactMarkdown>{msg.content}</ReactMarkdown> + </div> + </div> + </div> + ) : ( + <div className="assistant-message"> + <div className="message-label">LLM Council</div> + <Stage1 responses={msg.stage1} /> + <Stage2 + rankings={msg.stage2} + labelToModel={msg.metadata?.label_to_model} + aggregateRankings={msg.metadata?.aggregate_rankings} + /> + <Stage3 finalResponse={msg.stage3} /> + </div> + )} + </div> + )) + )} + + {isLoading && ( + <div className="loading-indicator"> + <div className="spinner"></div> + <span>Consulting the council...</span> + </div> + )} + + <div ref={messagesEndRef} /> + </div> + + <form className="input-form" onSubmit={handleSubmit}> + <textarea + className="message-input" + placeholder="Ask your question... (Shift+Enter for new line, Enter to send)" + value={input} + onChange={(e) => setInput(e.target.value)} + onKeyDown={handleKeyDown} + disabled={isLoading} + rows={3} + /> + <button + type="submit" + className="send-button" + disabled={!input.trim() || isLoading} + > + Send + </button> + </form> + </div> + ); +} diff --git a/frontend/src/components/Sidebar.css b/frontend/src/components/Sidebar.css new file mode 100644 index 0000000..c4f4b97 --- /dev/null +++ b/frontend/src/components/Sidebar.css @@ -0,0 +1,78 @@ +.sidebar { + width: 260px; + background: #f8f8f8; + border-right: 1px solid #e0e0e0; + display: flex; + flex-direction: column; + height: 100vh; +} + +.sidebar-header { + padding: 16px; + border-bottom: 1px solid #e0e0e0; +} + +.sidebar-header h1 { + font-size: 18px; + margin: 0 0 12px 0; + color: #333; +} + +.new-conversation-btn { + width: 100%; + padding: 10px; + background: #4a90e2; + border: 1px solid #4a90e2; + border-radius: 6px; + color: #fff; + cursor: pointer; + font-size: 14px; + transition: background 0.2s; + font-weight: 500; +} + +.new-conversation-btn:hover { + background: #357abd; + border-color: #357abd; +} + +.conversation-list { + flex: 1; + overflow-y: auto; + padding: 8px; +} + +.no-conversations { + padding: 16px; + text-align: center; + color: #999; + font-size: 14px; +} + +.conversation-item { + padding: 12px; + margin-bottom: 4px; + border-radius: 6px; + cursor: pointer; + transition: background 0.2s; +} + +.conversation-item:hover { + background: #f0f0f0; +} + +.conversation-item.active { + background: #e8f0fe; + border: 1px solid #4a90e2; +} + +.conversation-title { + color: #333; + font-size: 14px; + margin-bottom: 4px; +} + +.conversation-meta { + color: #999; + font-size: 12px; +} diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx new file mode 100644 index 0000000..c189690 --- /dev/null +++ b/frontend/src/components/Sidebar.jsx @@ -0,0 +1,43 @@ +import { useState, useEffect } from 'react'; +import './Sidebar.css'; + +export default function Sidebar({ + conversations, + currentConversationId, + onSelectConversation, + onNewConversation, +}) { + return ( + <div className="sidebar"> + <div className="sidebar-header"> + <h1>LLM Council</h1> + <button className="new-conversation-btn" onClick={onNewConversation}> + + New Conversation + </button> + </div> + + <div className="conversation-list"> + {conversations.length === 0 ? ( + <div className="no-conversations">No conversations yet</div> + ) : ( + conversations.map((conv) => ( + <div + key={conv.id} + className={`conversation-item ${ + conv.id === currentConversationId ? 'active' : '' + }`} + onClick={() => onSelectConversation(conv.id)} + > + <div className="conversation-title"> + Conversation {conv.id.slice(0, 8)}... + </div> + <div className="conversation-meta"> + {conv.message_count} messages + </div> + </div> + )) + )} + </div> + </div> + ); +} diff --git a/frontend/src/components/Stage1.css b/frontend/src/components/Stage1.css new file mode 100644 index 0000000..1f5133e --- /dev/null +++ b/frontend/src/components/Stage1.css @@ -0,0 +1,65 @@ +.stage { + margin: 24px 0; + padding: 20px; + background: #fafafa; + border-radius: 8px; + border: 1px solid #e0e0e0; +} + +.stage-title { + margin: 0 0 16px 0; + color: #333; + font-size: 16px; + font-weight: 600; +} + +.tabs { + display: flex; + gap: 8px; + margin-bottom: 16px; + flex-wrap: wrap; +} + +.tab { + padding: 8px 16px; + background: #ffffff; + border: 1px solid #d0d0d0; + border-radius: 6px 6px 0 0; + color: #666; + cursor: pointer; + font-size: 14px; + transition: all 0.2s; +} + +.tab:hover { + background: #f0f0f0; + color: #333; + border-color: #4a90e2; +} + +.tab.active { + background: #ffffff; + color: #4a90e2; + border-color: #4a90e2; + border-bottom-color: #ffffff; + font-weight: 600; +} + +.tab-content { + background: #ffffff; + padding: 16px; + border-radius: 6px; + border: 1px solid #e0e0e0; +} + +.model-name { + color: #888; + font-size: 12px; + margin-bottom: 12px; + font-family: monospace; +} + +.response-text { + color: #333; + line-height: 1.6; +} diff --git a/frontend/src/components/Stage1.jsx b/frontend/src/components/Stage1.jsx new file mode 100644 index 0000000..071937c --- /dev/null +++ b/frontend/src/components/Stage1.jsx @@ -0,0 +1,36 @@ +import { useState } from 'react'; +import ReactMarkdown from 'react-markdown'; +import './Stage1.css'; + +export default function Stage1({ responses }) { + const [activeTab, setActiveTab] = useState(0); + + if (!responses || responses.length === 0) { + return null; + } + + return ( + <div className="stage stage1"> + <h3 className="stage-title">Stage 1: Individual Responses</h3> + + <div className="tabs"> + {responses.map((resp, index) => ( + <button + key={index} + className={`tab ${activeTab === index ? 'active' : ''}`} + onClick={() => setActiveTab(index)} + > + {resp.model.split('/')[1] || resp.model} + </button> + ))} + </div> + + <div className="tab-content"> + <div className="model-name">{responses[activeTab].model}</div> + <div className="response-text markdown-content"> + <ReactMarkdown>{responses[activeTab].response}</ReactMarkdown> + </div> + </div> + </div> + ); +} diff --git a/frontend/src/components/Stage2.css b/frontend/src/components/Stage2.css new file mode 100644 index 0000000..99c460a --- /dev/null +++ b/frontend/src/components/Stage2.css @@ -0,0 +1,153 @@ +.stage2 { + background: #fafafa; +} + +.stage2 h4 { + margin: 20px 0 8px 0; + color: #333; + font-size: 14px; + font-weight: 600; +} + +.stage2 h4:first-of-type { + margin-top: 0; +} + +.stage-description { + margin: 0 0 12px 0; + color: #666; + font-size: 13px; + line-height: 1.5; +} + +.aggregate-rankings { + background: #f0f7ff; + padding: 16px; + border-radius: 8px; + margin-bottom: 20px; + border: 2px solid #d0e7ff; +} + +.aggregate-rankings h4 { + margin: 0 0 12px 0; + color: #2a7ae2; + font-size: 15px; +} + +.aggregate-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.aggregate-item { + display: flex; + align-items: center; + gap: 12px; + padding: 10px; + background: #ffffff; + border-radius: 6px; + border: 1px solid #d0e7ff; +} + +.rank-position { + color: #2a7ae2; + font-weight: 700; + font-size: 16px; + min-width: 35px; +} + +.rank-model { + flex: 1; + color: #333; + font-family: monospace; + font-size: 14px; + font-weight: 500; +} + +.rank-score { + color: #666; + font-size: 13px; + font-family: monospace; +} + +.stage2 .tabs { + display: flex; + gap: 8px; + margin-bottom: 16px; + flex-wrap: wrap; +} + +.stage2 .tab { + padding: 8px 16px; + background: #ffffff; + border: 1px solid #d0d0d0; + border-radius: 6px 6px 0 0; + color: #666; + cursor: pointer; + font-size: 14px; + transition: all 0.2s; +} + +.stage2 .tab:hover { + background: #f0f0f0; + color: #333; + border-color: #4a90e2; +} + +.stage2 .tab.active { + background: #ffffff; + color: #4a90e2; + border-color: #4a90e2; + border-bottom-color: #ffffff; + font-weight: 600; +} + +.stage2 .tab-content { + background: #ffffff; + padding: 16px; + border-radius: 6px; + border: 1px solid #e0e0e0; + margin-bottom: 20px; +} + +.ranking-model { + color: #888; + font-size: 12px; + font-family: monospace; + margin-bottom: 12px; +} + +.ranking-content { + color: #333; + line-height: 1.6; + font-size: 14px; +} + +.parsed-ranking { + margin-top: 16px; + padding-top: 16px; + border-top: 2px solid #e0e0e0; +} + +.parsed-ranking strong { + color: #2a7ae2; + font-size: 13px; +} + +.parsed-ranking ol { + margin: 8px 0 0 0; + padding-left: 24px; + color: #333; +} + +.parsed-ranking li { + margin: 4px 0; + font-family: monospace; + font-size: 13px; +} + +.rank-count { + color: #999; + font-size: 12px; +} diff --git a/frontend/src/components/Stage2.jsx b/frontend/src/components/Stage2.jsx new file mode 100644 index 0000000..2550fa6 --- /dev/null +++ b/frontend/src/components/Stage2.jsx @@ -0,0 +1,99 @@ +import { useState } from 'react'; +import ReactMarkdown from 'react-markdown'; +import './Stage2.css'; + +function deAnonymizeText(text, labelToModel) { + if (!labelToModel) return text; + + let result = text; + // Replace each "Response X" with the actual model name + Object.entries(labelToModel).forEach(([label, model]) => { + const modelShortName = model.split('/')[1] || model; + result = result.replace(new RegExp(label, 'g'), `**${modelShortName}**`); + }); + return result; +} + +export default function Stage2({ rankings, labelToModel, aggregateRankings }) { + const [activeTab, setActiveTab] = useState(0); + + if (!rankings || rankings.length === 0) { + return null; + } + + return ( + <div className="stage stage2"> + <h3 className="stage-title">Stage 2: Peer Rankings</h3> + + <h4>Raw Evaluations</h4> + <p className="stage-description"> + Each model evaluated all responses (anonymized as Response A, B, C, etc.) and provided rankings. + Below, model names are shown in <strong>bold</strong> for readability, but the original evaluation used anonymous labels. + </p> + + <div className="tabs"> + {rankings.map((rank, index) => ( + <button + key={index} + className={`tab ${activeTab === index ? 'active' : ''}`} + onClick={() => setActiveTab(index)} + > + {rank.model.split('/')[1] || rank.model} + </button> + ))} + </div> + + <div className="tab-content"> + <div className="ranking-model"> + {rankings[activeTab].model} + </div> + <div className="ranking-content markdown-content"> + <ReactMarkdown> + {deAnonymizeText(rankings[activeTab].ranking, labelToModel)} + </ReactMarkdown> + </div> + + {rankings[activeTab].parsed_ranking && + rankings[activeTab].parsed_ranking.length > 0 && ( + <div className="parsed-ranking"> + <strong>Extracted Ranking:</strong> + <ol> + {rankings[activeTab].parsed_ranking.map((label, i) => ( + <li key={i}> + {labelToModel && labelToModel[label] + ? labelToModel[label].split('/')[1] || labelToModel[label] + : label} + </li> + ))} + </ol> + </div> + )} + </div> + + {aggregateRankings && aggregateRankings.length > 0 && ( + <div className="aggregate-rankings"> + <h4>Aggregate Rankings (Street Cred)</h4> + <p className="stage-description"> + Combined results across all peer evaluations (lower score is better): + </p> + <div className="aggregate-list"> + {aggregateRankings.map((agg, index) => ( + <div key={index} className="aggregate-item"> + <span className="rank-position">#{index + 1}</span> + <span className="rank-model"> + {agg.model.split('/')[1] || agg.model} + </span> + <span className="rank-score"> + Avg: {agg.average_rank.toFixed(2)} + </span> + <span className="rank-count"> + ({agg.rankings_count} votes) + </span> + </div> + ))} + </div> + </div> + )} + </div> + ); +} diff --git a/frontend/src/components/Stage3.css b/frontend/src/components/Stage3.css new file mode 100644 index 0000000..954a9d9 --- /dev/null +++ b/frontend/src/components/Stage3.css @@ -0,0 +1,25 @@ +.stage3 { + background: #f0fff0; + border-color: #c8e6c8; +} + +.final-response { + background: #ffffff; + padding: 20px; + border-radius: 6px; + border: 1px solid #c8e6c8; +} + +.chairman-label { + color: #2d8a2d; + font-size: 12px; + font-family: monospace; + margin-bottom: 12px; + font-weight: 600; +} + +.final-text { + color: #333; + line-height: 1.7; + font-size: 15px; +} diff --git a/frontend/src/components/Stage3.jsx b/frontend/src/components/Stage3.jsx new file mode 100644 index 0000000..9a9dbf7 --- /dev/null +++ b/frontend/src/components/Stage3.jsx @@ -0,0 +1,22 @@ +import ReactMarkdown from 'react-markdown'; +import './Stage3.css'; + +export default function Stage3({ finalResponse }) { + if (!finalResponse) { + return null; + } + + return ( + <div className="stage stage3"> + <h3 className="stage-title">Stage 3: Final Council Answer</h3> + <div className="final-response"> + <div className="chairman-label"> + Chairman: {finalResponse.model.split('/')[1] || finalResponse.model} + </div> + <div className="final-text markdown-content"> + <ReactMarkdown>{finalResponse.response}</ReactMarkdown> + </div> + </div> + </div> + ); +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..698f393 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,98 @@ +:root { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; + line-height: 1.5; + font-weight: 400; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; + background: #f5f5f5; +} + +#root { + height: 100vh; + width: 100vw; + overflow: hidden; +} + +/* Global markdown styling */ +.markdown-content { + padding: 12px; +} + +.markdown-content p { + margin: 0 0 12px 0; +} + +.markdown-content p:last-child { + margin-bottom: 0; +} + +.markdown-content h1, +.markdown-content h2, +.markdown-content h3, +.markdown-content h4, +.markdown-content h5, +.markdown-content h6 { + margin: 16px 0 8px 0; +} + +.markdown-content h1:first-child, +.markdown-content h2:first-child, +.markdown-content h3:first-child, +.markdown-content h4:first-child, +.markdown-content h5:first-child, +.markdown-content h6:first-child { + margin-top: 0; +} + +.markdown-content ul, +.markdown-content ol { + margin: 0 0 12px 0; + padding-left: 24px; +} + +.markdown-content li { + margin: 4px 0; +} + +.markdown-content pre { + background: #f5f5f5; + padding: 12px; + border-radius: 4px; + overflow-x: auto; + margin: 0 0 12px 0; +} + +.markdown-content code { + background: #f5f5f5; + padding: 2px 6px; + border-radius: 3px; + font-family: monospace; + font-size: 0.9em; +} + +.markdown-content pre code { + background: none; + padding: 0; +} + +.markdown-content blockquote { + margin: 0 0 12px 0; + padding-left: 16px; + border-left: 4px solid #ddd; + color: #666; +} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..b9a1a6d --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.jsx' + +createRoot(document.getElementById('root')).render( + <StrictMode> + <App /> + </StrictMode>, +) |
