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/components | |
v0
Diffstat (limited to 'frontend/src/components')
| -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 |
10 files changed, 787 insertions, 0 deletions
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> + ); +} |
