summaryrefslogtreecommitdiff
path: root/frontend/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components')
-rw-r--r--frontend/src/components/ChatInterface.css149
-rw-r--r--frontend/src/components/ChatInterface.jsx117
-rw-r--r--frontend/src/components/Sidebar.css78
-rw-r--r--frontend/src/components/Sidebar.jsx43
-rw-r--r--frontend/src/components/Stage1.css65
-rw-r--r--frontend/src/components/Stage1.jsx36
-rw-r--r--frontend/src/components/Stage2.css153
-rw-r--r--frontend/src/components/Stage2.jsx99
-rw-r--r--frontend/src/components/Stage3.css25
-rw-r--r--frontend/src/components/Stage3.jsx22
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>
+ );
+}