From 2adacdbfa1d1049a0497e55f2b3ed00551bf876f Mon Sep 17 00:00:00 2001 From: YurenHao0426 Date: Fri, 13 Feb 2026 22:46:06 +0000 Subject: Add per-model council settings, Quick Chat council mode, and per-member trace selection Council members now support individual temperature, reasoning effort, web search, and context trace overrides. Quick Chat inherits council config from the source node and streams through the 3-stage council pipeline. Blueprint loading migrates old string[] council formats to CouncilMemberConfig[]. Co-Authored-By: Claude Opus 4.6 --- frontend/src/components/Sidebar.tsx | 967 +++++++++++++++++++++++++++--------- 1 file changed, 731 insertions(+), 236 deletions(-) (limited to 'frontend/src/components/Sidebar.tsx') diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 4d7bb51..64cd79a 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef, useMemo } from 'react'; import { useReactFlow } from 'reactflow'; import useFlowStore from '../store/flowStore'; import { useAuthStore } from '../store/authStore'; -import type { NodeData, Trace, Message, MergedTrace, MergeStrategy, CouncilData } from '../store/flowStore'; +import type { NodeData, Trace, Message, MergedTrace, MergeStrategy, CouncilData, CouncilMemberConfig } from '../store/flowStore'; import type { Edge } from 'reactflow'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; @@ -17,7 +17,7 @@ const preprocessLaTeX = (content: string): string => { .replace(/\\\(([\s\S]*?)\\\)/g, (_, math) => `$${math}$`); }; -import { Play, Settings, Info, ChevronLeft, ChevronRight, Maximize2, Edit3, X, Check, FileText, MessageCircle, Send, GripVertical, GitMerge, Trash2, AlertCircle, Loader2, Navigation, Upload, Search, Link, Layers, Eye, EyeOff, Copy, ClipboardCheck, Users } from 'lucide-react'; +import { Play, Settings, Info, ChevronLeft, ChevronRight, ChevronDown, Maximize2, Edit3, X, Check, FileText, MessageCircle, Send, GripVertical, GitMerge, Trash2, AlertCircle, Loader2, Navigation, Upload, Search, Link, Layers, Eye, EyeOff, Copy, ClipboardCheck, Users } from 'lucide-react'; interface SidebarProps { isOpen: boolean; @@ -96,10 +96,19 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => { // Council mode states const [councilStage, setCouncilStage] = useState(''); const [councilStreamBuffer, setCouncilStreamBuffer] = useState(''); + const [openCtxDropdown, setOpenCtxDropdown] = useState(null); const [councilTab, setCouncilTab] = useState<'final' | 'responses' | 'rankings'>('final'); const [councilResponseTab, setCouncilResponseTab] = useState(0); const [councilRankingTab, setCouncilRankingTab] = useState<'aggregate' | number>('aggregate'); + // Quick Chat council mode states + const [quickChatCouncilMode, setQuickChatCouncilMode] = useState(false); + const [quickChatCouncilModels, setQuickChatCouncilModels] = useState([]); + const [quickChatChairmanModel, setQuickChatChairmanModel] = useState(null); + const [quickChatCouncilStage, setQuickChatCouncilStage] = useState(''); + const [quickChatCouncilData, setQuickChatCouncilData] = useState(null); + const [quickChatCouncilConfigOpen, setQuickChatCouncilConfigOpen] = useState(false); + const selectedNode = nodes.find((n) => n.id === selectedNodeId); // Reset stream buffer and modal states when node changes @@ -424,8 +433,8 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => { // Council mode: 3-stage LLM council execution const handleRunCouncil = async () => { if (!selectedNode) return; - const councilModels = selectedNode.data.councilModels || []; - const chairmanModel = selectedNode.data.chairmanModel || councilModels[0]; + const councilModels: CouncilMemberConfig[] = selectedNode.data.councilModels || []; + const chairmanConfig: CouncilMemberConfig = selectedNode.data.chairmanModel || councilModels[0]; if (councilModels.length < 2) return; const tracesCheck = checkActiveTracesComplete(); @@ -473,6 +482,27 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => { ? 'Please analyze the attached files.' : ''; + // Resolve context messages for a specific trace ID + const resolveTraceContext = (traceId: string): Message[] => { + const node = nodes.find(n => n.id === runningNodeId); + if (!node) return []; + // Search incoming traces + let trace: Trace | undefined = (node.data.traces || []).find((t: Trace) => t.id === traceId); + // Then outgoing traces + if (!trace) trace = (node.data.outgoingTraces || []).find((t: Trace) => t.id === traceId); + if (trace) { + const nodePrefix = `${runningNodeId}-`; + const isOriginated = trace.id === `trace-${runningNodeId}` || + trace.id.startsWith('fork-') || + (trace.id.startsWith('prepend-') && trace.id.includes(`-from-${runningNodeId}`)); + return isOriginated ? trace.messages.filter(m => !m.id?.startsWith(nodePrefix)) : [...trace.messages]; + } + // Check merged traces + const merged = (node.data.mergedTraces || []).find((m: MergedTrace) => m.id === traceId); + if (merged) return [...merged.messages]; + return []; + }; + try { const response = await fetch(`/api/run_council_stream?user=${encodeURIComponent(user?.username || 'test')}`, { method: 'POST', @@ -481,11 +511,28 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => { node_id: runningNodeId, incoming_contexts: [{ messages: context }], user_prompt: effectivePrompt, - council_models: councilModels.map((m: string) => ({ model_name: m })), - chairman_model: chairmanModel, + council_models: councilModels.map((cfg) => { + const base: Record = { + model_name: cfg.model, + temperature: cfg.temperature ?? null, + reasoning_effort: cfg.reasoningEffort ?? null, + enable_google_search: cfg.enableWebSearch ?? null, + }; + if (cfg.traceId) { + base.incoming_contexts = [{ messages: resolveTraceContext(cfg.traceId) }]; + } + return base; + }), + chairman_model: { + model_name: chairmanConfig.model, + temperature: chairmanConfig.temperature ?? null, + reasoning_effort: chairmanConfig.reasoningEffort ?? null, + enable_google_search: chairmanConfig.enableWebSearch ?? null, + }, system_prompt: selectedNode.data.systemPrompt || null, temperature: selectedNode.data.temperature, reasoning_effort: selectedNode.data.reasoningEffort || 'medium', + enable_google_search: selectedNode.data.enableGoogleSearch !== false, merge_strategy: selectedNode.data.mergeStrategy || 'smart', attached_file_ids: attachedFiles, scopes, @@ -856,7 +903,19 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => { // handleQuickChatSend will decide whether to overwrite (if empty) or create new node (if has response) setQuickChatLastNodeId(selectedNode.id); } - + + // Copy council settings from node if active + if (selectedNode.data.councilMode && selectedNode.data.councilModels && selectedNode.data.councilModels.length >= 2) { + setQuickChatCouncilMode(true); + setQuickChatCouncilModels([...selectedNode.data.councilModels]); + setQuickChatChairmanModel(selectedNode.data.chairmanModel || selectedNode.data.councilModels[0]); + setQuickChatCouncilConfigOpen(false); + } else { + setQuickChatCouncilMode(false); + } + setQuickChatCouncilData(null); + setQuickChatCouncilStage(''); + setQuickChatOpen(true); // If there's an unsent draft, put it in the input box setQuickChatInput(hasDraftPrompt ? selectedNode.data.userPrompt : ''); @@ -868,6 +927,9 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => { setQuickChatMessages([]); setQuickChatAttachedFiles([]); setQuickChatSentFiles([]); + setQuickChatCouncilMode(false); + setQuickChatCouncilData(null); + setQuickChatCouncilStage(''); }; // Quick Chat file attachment helpers @@ -1221,69 +1283,201 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => { const tempAtSend = quickChatTemp; const effortAtSend = quickChatEffort; const webSearchAtSend = quickChatWebSearch; + const reasoningModels = ['gpt-5', 'gpt-5-chat-latest', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5-pro', 'gpt-5.1', 'gpt-5.1-chat-latest', 'gpt-5.2', 'gpt-5.2-chat-latest', 'gpt-5.2-pro', 'o3']; - try { - // Determine provider - const isClaude = modelAtSend.includes('claude'); - const isOpenAI = modelAtSend.includes('gpt') || modelAtSend === 'o3'; - const provider = isClaude ? 'claude' : isOpenAI ? 'openai' : 'google'; - const reasoningModels = ['gpt-5', 'gpt-5-chat-latest', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5-pro', 'gpt-5.1', 'gpt-5.1-chat-latest', 'gpt-5.2', 'gpt-5.2-chat-latest', 'gpt-5.2-pro', 'o3']; - const isReasoning = reasoningModels.includes(modelAtSend); + // Snapshot council config at send time + const isCouncilSend = quickChatCouncilMode && quickChatCouncilModels.length >= 2; + const councilModelsAtSend = isCouncilSend ? [...quickChatCouncilModels] : []; + const chairmanAtSend = isCouncilSend ? (quickChatChairmanModel || quickChatCouncilModels[0]) : null; + try { // Build scopes for file search (Quick Chat uses a temp scope) const projectPath = currentBlueprintPath || 'untitled'; const scopes = [`${projectPath}/quick_chat_temp`]; - // Call LLM API with current messages as context - const response = await fetch(`/api/run_node_stream?user=${encodeURIComponent(user?.username || 'test')}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...getAuthHeader() }, - body: JSON.stringify({ - node_id: 'quick_chat_temp', - incoming_contexts: [{ messages: messagesBeforeSend }], - user_prompt: userInput || 'Please analyze the attached files.', - attached_file_ids: attachedFilesCopy, - scopes, - merge_strategy: 'smart', - config: { - provider, - model_name: modelAtSend, - temperature: isReasoning ? 1 : tempAtSend, - enable_google_search: webSearchAtSend, - reasoning_effort: effortAtSend, - } - }) - }); + let fullResponse = ''; - if (!response.ok) { - const errText = await response.text(); - throw new Error(errText || `HTTP ${response.status}`); - } - if (!response.body) throw new Error('No response body'); + if (isCouncilSend && chairmanAtSend) { + // ========== COUNCIL MODE ========== + setQuickChatCouncilStage('Starting council...'); + setQuickChatCouncilData({ stage1: null, stage2: null, stage3: null }); - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let fullResponse = ''; + const response = await fetch(`/api/run_council_stream?user=${encodeURIComponent(user?.username || 'test')}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...getAuthHeader() }, + body: JSON.stringify({ + node_id: 'quick_chat_temp', + incoming_contexts: [{ messages: messagesBeforeSend }], + user_prompt: userInput || 'Please analyze the attached files.', + council_models: councilModelsAtSend.map(cfg => ({ + model_name: cfg.model, + temperature: cfg.temperature ?? null, + reasoning_effort: cfg.reasoningEffort ?? null, + enable_google_search: cfg.enableWebSearch ?? null, + })), + chairman_model: { + model_name: chairmanAtSend.model, + temperature: chairmanAtSend.temperature ?? null, + reasoning_effort: chairmanAtSend.reasoningEffort ?? null, + enable_google_search: chairmanAtSend.enableWebSearch ?? null, + }, + system_prompt: selectedNode.data.systemPrompt || null, + temperature: selectedNode.data.temperature, + reasoning_effort: selectedNode.data.reasoningEffort || 'medium', + enable_google_search: selectedNode.data.enableGoogleSearch !== false, + merge_strategy: 'smart', + attached_file_ids: attachedFilesCopy, + scopes, + }), + }); - // Stream response - while (true) { - const { value, done } = await reader.read(); - if (done) break; - const chunk = decoder.decode(value); - fullResponse += chunk; - - // Update display in real-time - setQuickChatMessages(prev => { - const newMsgs = [...prev]; - const lastMsg = newMsgs[newMsgs.length - 1]; - if (lastMsg?.role === 'assistant') { - // Update existing assistant message - return [...newMsgs.slice(0, -1), { ...lastMsg, content: fullResponse }]; - } else { - // Add new assistant message - return [...newMsgs, { id: `qc_${Date.now()}_a`, role: 'assistant', content: fullResponse }]; + if (!response.ok) { + const errText = await response.text(); + throw new Error(errText || `HTTP ${response.status}`); + } + if (!response.body) throw new Error('No response body'); + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let sseBuffer = ''; + let stage1Results: Array<{ model: string; response: string }> = []; + let stage2Data: any = null; + let stage3Full = ''; + let stage3Model = ''; + + while (true) { + const { value, done } = await reader.read(); + if (done) break; + sseBuffer += decoder.decode(value, { stream: true }); + const parts = sseBuffer.split('\n\n'); + sseBuffer = parts.pop() || ''; + + for (const part of parts) { + const line = part.trim(); + if (!line.startsWith('data: ')) continue; + let evt: any; + try { evt = JSON.parse(line.slice(6)); } catch { continue; } + + switch (evt.type) { + case 'stage1_start': + setQuickChatCouncilStage('Stage 1: Collecting responses...'); + break; + case 'stage1_model_complete': + stage1Results = [...stage1Results, evt.data]; + setQuickChatCouncilStage(`Stage 1: ${stage1Results.length}/${councilModelsAtSend.length} models done`); + setQuickChatCouncilData({ stage1: [...stage1Results], stage2: null, stage3: null }); + break; + case 'stage1_complete': + stage1Results = evt.data; + setQuickChatCouncilData({ stage1: stage1Results, stage2: null, stage3: null }); + break; + case 'stage2_start': + setQuickChatCouncilStage('Stage 2: Peer ranking...'); + break; + case 'stage2_complete': + stage2Data = evt.data; + setQuickChatCouncilData({ stage1: stage1Results, stage2: stage2Data, stage3: null }); + break; + case 'stage3_start': + setQuickChatCouncilStage('Stage 3: Chairman synthesizing...'); + break; + case 'stage3_chunk': + stage3Full += evt.data.chunk; + // Stream chairman response into chat + setQuickChatMessages(prev => { + const newMsgs = [...prev]; + const lastMsg = newMsgs[newMsgs.length - 1]; + if (lastMsg?.role === 'assistant') { + return [...newMsgs.slice(0, -1), { ...lastMsg, content: stage3Full }]; + } else { + return [...newMsgs, { id: `qc_${Date.now()}_a`, role: 'assistant', content: stage3Full }]; + } + }); + break; + case 'stage3_complete': + stage3Model = evt.data.model; + stage3Full = evt.data.response; + break; + case 'complete': { + const finalData: CouncilData = { + stage1: stage1Results, + stage2: stage2Data, + stage3: { model: stage3Model, response: stage3Full }, + }; + setQuickChatCouncilData(finalData); + setQuickChatCouncilStage(''); + // Ensure final message is set + setQuickChatMessages(prev => { + const newMsgs = [...prev]; + const lastMsg = newMsgs[newMsgs.length - 1]; + if (lastMsg?.role === 'assistant') { + return [...newMsgs.slice(0, -1), { ...lastMsg, content: stage3Full }]; + } else { + return [...newMsgs, { id: `qc_${Date.now()}_a`, role: 'assistant', content: stage3Full }]; + } + }); + break; + } + case 'error': + setQuickChatCouncilStage(''); + throw new Error(evt.data?.message || 'Council error'); + } } + } + fullResponse = stage3Full; + } else { + // ========== SINGLE MODEL MODE ========== + // Determine provider + const isClaude = modelAtSend.includes('claude'); + const isOpenAI = modelAtSend.includes('gpt') || modelAtSend === 'o3'; + const provider = isClaude ? 'claude' : isOpenAI ? 'openai' : 'google'; + const isReasoning = reasoningModels.includes(modelAtSend); + + const response = await fetch(`/api/run_node_stream?user=${encodeURIComponent(user?.username || 'test')}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...getAuthHeader() }, + body: JSON.stringify({ + node_id: 'quick_chat_temp', + incoming_contexts: [{ messages: messagesBeforeSend }], + user_prompt: userInput || 'Please analyze the attached files.', + attached_file_ids: attachedFilesCopy, + scopes, + merge_strategy: 'smart', + config: { + provider, + model_name: modelAtSend, + temperature: isReasoning ? 1 : tempAtSend, + enable_google_search: webSearchAtSend, + reasoning_effort: effortAtSend, + } + }) }); + + if (!response.ok) { + const errText = await response.text(); + throw new Error(errText || `HTTP ${response.status}`); + } + if (!response.body) throw new Error('No response body'); + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + + while (true) { + const { value, done } = await reader.read(); + if (done) break; + const chunk = decoder.decode(value); + fullResponse += chunk; + + setQuickChatMessages(prev => { + const newMsgs = [...prev]; + const lastMsg = newMsgs[newMsgs.length - 1]; + if (lastMsg?.role === 'assistant') { + return [...newMsgs.slice(0, -1), { ...lastMsg, content: fullResponse }]; + } else { + return [...newMsgs, { id: `qc_${Date.now()}_a`, role: 'assistant', content: fullResponse }]; + } + }); + } } // Determine whether to overwrite current node or create new one @@ -1295,18 +1489,25 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => { if (!fromNodeHasResponse && fromNode) { // Overwrite the source node (it's empty) - updateNodeData(fromNodeId, { + const nodeUpdate: any = { userPrompt: userInput, response: fullResponse, - model: modelAtSend, - temperature: isReasoning ? 1 : tempAtSend, - reasoningEffort: effortAtSend, - enableGoogleSearch: webSearchAtSend, + model: isCouncilSend ? chairmanAtSend!.model : modelAtSend, + temperature: isCouncilSend ? selectedNode.data.temperature : (reasoningModels.includes(modelAtSend) ? 1 : tempAtSend), + reasoningEffort: isCouncilSend ? (selectedNode.data.reasoningEffort || 'medium') : effortAtSend, + enableGoogleSearch: isCouncilSend ? (selectedNode.data.enableGoogleSearch !== false) : webSearchAtSend, attachedFileIds: attachedFilesCopy, status: 'success', querySentAt: Date.now(), - responseReceivedAt: Date.now() - }); + responseReceivedAt: Date.now(), + }; + if (isCouncilSend) { + nodeUpdate.councilMode = true; + nodeUpdate.councilModels = councilModelsAtSend; + nodeUpdate.chairmanModel = chairmanAtSend; + nodeUpdate.councilData = quickChatCouncilData; + } + updateNodeData(fromNodeId, nodeUpdate); // Update trace to reflect current node now has content setQuickChatTrace(prev => prev ? { @@ -1328,19 +1529,15 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => { y: sourceNode.position.y }; - const newNode = { - id: newNodeId, - type: 'llmNode', - position: newPos, - data: { - label: 'Quick Chat', - model: modelAtSend, - temperature: isReasoning ? 1 : tempAtSend, + const newNodeData: any = { + label: isCouncilSend ? 'Council Chat' : 'Quick Chat', + model: isCouncilSend ? chairmanAtSend!.model : modelAtSend, + temperature: isCouncilSend ? selectedNode.data.temperature : (reasoningModels.includes(modelAtSend) ? 1 : tempAtSend), systemPrompt: '', userPrompt: userInput, mergeStrategy: 'smart' as const, - reasoningEffort: effortAtSend, - enableGoogleSearch: webSearchAtSend, + reasoningEffort: isCouncilSend ? (selectedNode.data.reasoningEffort || 'medium') : effortAtSend, + enableGoogleSearch: isCouncilSend ? (selectedNode.data.enableGoogleSearch !== false) : webSearchAtSend, traces: [], outgoingTraces: [], forkedTraces: [], @@ -1351,7 +1548,20 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => { status: 'success' as const, inputs: 1, querySentAt: Date.now(), - responseReceivedAt: Date.now() + responseReceivedAt: Date.now(), + }; + if (isCouncilSend) { + newNodeData.councilMode = true; + newNodeData.councilModels = councilModelsAtSend; + newNodeData.chairmanModel = chairmanAtSend; + newNodeData.councilData = quickChatCouncilData; + } + const newNode = { + id: newNodeId, + type: 'llmNode', + position: newPos, + data: { + ...newNodeData, } }; @@ -1553,13 +1763,15 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => { } catch (error) { console.error('Quick chat error:', error); - setQuickChatMessages(prev => [...prev, { - id: `qc_err_${Date.now()}`, - role: 'assistant', - content: `Error: ${error}` + setQuickChatMessages(prev => [...prev, { + id: `qc_err_${Date.now()}`, + role: 'assistant', + content: `Error: ${error}` }]); + setQuickChatCouncilStage(''); } finally { setQuickChatLoading(false); + setQuickChatCouncilStage(''); // Refocus the input after sending setTimeout(() => { quickChatInputRef.current?.focus(); @@ -1697,66 +1909,239 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => {
-
- {[ - { value: 'claude-sonnet-4-5', label: 'claude-sonnet-4.5' }, - { value: 'claude-opus-4', label: 'claude-opus-4' }, - { value: 'claude-opus-4-5', label: 'claude-opus-4.5' }, - { value: 'claude-opus-4-6', label: 'claude-opus-4.6' }, - { value: 'gemini-2.5-flash', label: 'gemini-2.5-flash' }, - { value: 'gemini-2.5-flash-lite', label: 'gemini-2.5-flash-lite' }, - { value: 'gemini-3-pro-preview', label: 'gemini-3-pro-preview' }, - { value: 'gpt-4.1', label: 'gpt-4.1' }, - { value: 'gpt-4o', label: 'gpt-4o' }, - { value: 'gpt-5', label: 'gpt-5' }, - { value: 'gpt-5-mini', label: 'gpt-5-mini' }, - { value: 'gpt-5-nano', label: 'gpt-5-nano' }, - { value: 'gpt-5.1', label: 'gpt-5.1' }, - { value: 'gpt-5.2', label: 'gpt-5.2' }, - { value: 'o3', label: 'o3', premium: true }, - ].map(m => { - const selected = (selectedNode.data.councilModels || []).includes(m.value); - const disabled = m.premium && !canUsePremiumModels; - return ( - - ); - })} +
+ {(() => { + const councilModelsList = [ + { value: 'claude-sonnet-4-5', label: 'claude-sonnet-4.5' }, + { value: 'claude-opus-4', label: 'claude-opus-4' }, + { value: 'claude-opus-4-5', label: 'claude-opus-4.5' }, + { value: 'claude-opus-4-6', label: 'claude-opus-4.6' }, + { value: 'gemini-2.5-flash', label: 'gemini-2.5-flash' }, + { value: 'gemini-2.5-flash-lite', label: 'gemini-2.5-flash-lite' }, + { value: 'gemini-3-pro-preview', label: 'gemini-3-pro-preview' }, + { value: 'gpt-4.1', label: 'gpt-4.1' }, + { value: 'gpt-4o', label: 'gpt-4o' }, + { value: 'gpt-5', label: 'gpt-5' }, + { value: 'gpt-5-mini', label: 'gpt-5-mini' }, + { value: 'gpt-5-nano', label: 'gpt-5-nano' }, + { value: 'gpt-5.1', label: 'gpt-5.1' }, + { value: 'gpt-5.2', label: 'gpt-5.2' }, + { value: 'o3', label: 'o3', premium: true }, + ]; + const members: CouncilMemberConfig[] = selectedNode.data.councilModels || []; + const isReasoningModel = (v: string) => ['gpt-5', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5-pro', 'gpt-5.1', 'gpt-5.2', 'gpt-5.2-pro', 'o3'].includes(v); + const supportsWebSearch = (v: string) => v.startsWith('gemini-') || v.startsWith('gpt-5') || v === 'o3' || v === 'gpt-4o'; + const updateCouncilMember = (modelName: string, field: string, value: any) => { + const updated = [...members]; + const idx = updated.findIndex(c => c.model === modelName); + if (idx >= 0) { + updated[idx] = { ...updated[idx], [field]: value }; + handleChange('councilModels', updated); + } + }; + // Gather available complete traces for per-member context selection + const availableTraces: Array<{ id: string; label: string; color: string }> = []; + const incTraces = selectedNode.data.traces || []; + const outTraces = (selectedNode.data.outgoingTraces || []) as Trace[]; + const origTraces = outTraces.filter(t => { + if (t.id.startsWith('merged-')) return false; + return t.sourceNodeId === selectedNode.id || t.id.includes(`fork-${selectedNode.id}`) || t.id === `trace-${selectedNode.id}`; + }); + const trMap = new Map(); + origTraces.forEach(t => trMap.set(t.id, t)); + incTraces.forEach(t => trMap.set(t.id, t)); + trMap.forEach((trace, id) => { + if (isTraceComplete(trace)) { + availableTraces.push({ id, label: `#${id.slice(-4)}`, color: trace.color }); + } + }); + (selectedNode.data.mergedTraces || []).forEach((mt: MergedTrace) => { + if (mt.messages && mt.messages.length > 0) { + availableTraces.push({ id: mt.id, label: `M#${mt.id.slice(-4)}`, color: mt.colors?.[0] || '#888' }); + } + }); + const defaultTraceId = selectedNode.data.activeTraceIds?.[0] || ''; + const defaultTraceColor = availableTraces.find(t => t.id === defaultTraceId)?.color + || trMap.get(defaultTraceId)?.color || '#888'; + return councilModelsList.map(m => { + const selected = members.some(c => c.model === m.value); + const disabled = (m as any).premium && !canUsePremiumModels; + const cfg = members.find(c => c.model === m.value); + return ( +
+ + {selected && cfg && ( +
+ + {isReasoningModel(m.value) && ( + + )} + {supportsWebSearch(m.value) && ( + + )} + {availableTraces.length > 1 && (() => { + const selectedTrace = availableTraces.find(t => t.id === cfg.traceId); + return ( +
+ + {openCtxDropdown === `ctx-${m.value}` && ( +
+
{ updateCouncilMember(m.value, 'traceId', undefined); setOpenCtxDropdown(null); }} + className={`flex items-center gap-1.5 px-2 py-1 text-xs cursor-pointer ${!cfg.traceId ? 'font-semibold' : ''} ${isDark ? 'hover:bg-gray-700 text-gray-300' : 'hover:bg-gray-100 text-gray-700'}`} + > + + Default +
+ {availableTraces.map(t => ( +
{ updateCouncilMember(m.value, 'traceId', t.id); setOpenCtxDropdown(null); }} + className={`flex items-center gap-1.5 px-2 py-1 text-xs cursor-pointer ${cfg.traceId === t.id ? 'font-semibold' : ''} ${isDark ? 'hover:bg-gray-700 text-gray-300' : 'hover:bg-gray-100 text-gray-700'}`} + > + + {t.label} +
+ ))} +
+ )} +
+ ); + })()} +
+ )} +
+ ); + }); + })()}
+ {(() => { + const chairCfg = selectedNode.data.chairmanModel; + if (!chairCfg) return null; + const isReasoningModel = (v: string) => ['gpt-5', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5-pro', 'gpt-5.1', 'gpt-5.2', 'gpt-5.2-pro', 'o3'].includes(v); + const supportsWebSearch = (v: string) => v.startsWith('gemini-') || v.startsWith('gpt-5') || v === 'o3' || v === 'gpt-4o'; + const updateChairman = (field: string, value: any) => { + handleChange('chairmanModel', { ...chairCfg, [field]: value }); + }; + return ( +
+ + {isReasoningModel(chairCfg.model) && ( + + )} + {supportsWebSearch(chairCfg.model) && ( + + )} +
+ ); + })()}
)} @@ -2098,18 +2483,23 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => { )} {selectedNode.data.councilMode ? ( - + <> + +
+ Inspired by karpathy/llm-council +
+ ) : (
)} + ) : isEditing ? (
@@ -2950,44 +3341,74 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => { )} #{quickChatTrace.id.slice(-8)}
-
- {/* Model Selector */} - + + + {/* Model Selector — hidden in council mode */} + {!quickChatCouncilMode ? ( + + ) : ( + + )}
+ {/* Council Config Panel (collapsible) */} + {quickChatCouncilMode && quickChatCouncilConfigOpen && ( +
+
+ {[ + { value: 'claude-sonnet-4-5', label: 'claude-sonnet-4.5' }, + { value: 'claude-opus-4', label: 'claude-opus-4' }, + { value: 'claude-opus-4-5', label: 'claude-opus-4.5' }, + { value: 'claude-opus-4-6', label: 'claude-opus-4.6' }, + { value: 'gemini-2.5-flash', label: 'gemini-2.5-flash' }, + { value: 'gemini-2.5-flash-lite', label: 'gemini-2.5-flash-lite' }, + { value: 'gemini-3-pro-preview', label: 'gemini-3-pro-preview' }, + { value: 'gpt-4.1', label: 'gpt-4.1' }, + { value: 'gpt-4o', label: 'gpt-4o' }, + { value: 'gpt-5', label: 'gpt-5' }, + { value: 'gpt-5-mini', label: 'gpt-5-mini' }, + { value: 'gpt-5-nano', label: 'gpt-5-nano' }, + { value: 'gpt-5.1', label: 'gpt-5.1' }, + { value: 'gpt-5.2', label: 'gpt-5.2' }, + { value: 'o3', label: 'o3' }, + ].map(m => { + const sel = quickChatCouncilModels.some(c => c.model === m.value); + return ( + + ); + })} +
+ {quickChatCouncilModels.length > 0 && ( +
+ Chairman: + +
+ )} +
+ )} + {/* Chat Messages */}
{quickChatMessages.length === 0 ? ( @@ -3102,10 +3581,13 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => { )} {quickChatLoading && (
-
- + + {quickChatCouncilStage && ( + {quickChatCouncilStage} + )}
)} @@ -3116,56 +3598,65 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => {
- {/* Temperature (hide for reasoning models) */} - {!['gpt-5', 'gpt-5-chat-latest', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5-pro', 'gpt-5.1', 'gpt-5.1-chat-latest', 'gpt-5.2', 'gpt-5.2-chat-latest', 'gpt-5.2-pro', 'o3'].includes(quickChatModel) && ( -
- Temp: - setQuickChatTemp(parseFloat(e.target.value))} - className="w-20" - /> - {quickChatTemp} -
- )} - - {/* Reasoning Effort (only for reasoning models, except chat-latest) */} - {['gpt-5', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5-pro', 'gpt-5.1', 'gpt-5.2', 'gpt-5.2-pro', 'o3'].includes(quickChatModel) && ( -
- Effort: - -
- )} - - {/* Web Search */} - {(quickChatModel.startsWith('gemini') || quickChatModel.startsWith('gpt-5') || ['o3', 'gpt-4o'].includes(quickChatModel)) && ( - + {quickChatCouncilMode ? ( + + + {quickChatCouncilModels.length} models · Chairman: {quickChatChairmanModel?.model || '—'} + + ) : ( + <> + {/* Temperature (hide for reasoning models) */} + {!['gpt-5', 'gpt-5-chat-latest', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5-pro', 'gpt-5.1', 'gpt-5.1-chat-latest', 'gpt-5.2', 'gpt-5.2-chat-latest', 'gpt-5.2-pro', 'o3'].includes(quickChatModel) && ( +
+ Temp: + setQuickChatTemp(parseFloat(e.target.value))} + className="w-20" + /> + {quickChatTemp} +
+ )} + + {/* Reasoning Effort (only for reasoning models, except chat-latest) */} + {['gpt-5', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5-pro', 'gpt-5.1', 'gpt-5.2', 'gpt-5.2-pro', 'o3'].includes(quickChatModel) && ( +
+ Effort: + +
+ )} + + {/* Web Search */} + {(quickChatModel.startsWith('gemini') || quickChatModel.startsWith('gpt-5') || ['o3', 'gpt-4o'].includes(quickChatModel)) && ( + + )} + )} - + {/* File Attachment Buttons - Hidden for Gemini */} - {!quickChatModel.startsWith('gemini') && ( + {!quickChatModel.startsWith('gemini') && !quickChatCouncilMode && (
-- cgit v1.2.3