diff options
Diffstat (limited to 'frontend/src/components')
| -rw-r--r-- | frontend/src/components/LeftSidebar.tsx | 68 | ||||
| -rw-r--r-- | frontend/src/components/Sidebar.tsx | 25 |
2 files changed, 86 insertions, 7 deletions
diff --git a/frontend/src/components/LeftSidebar.tsx b/frontend/src/components/LeftSidebar.tsx index 6806ca3..5c464bb 100644 --- a/frontend/src/components/LeftSidebar.tsx +++ b/frontend/src/components/LeftSidebar.tsx @@ -62,8 +62,12 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({ isOpen, onToggle }) => { const [openaiApiKey, setOpenaiApiKey] = useState(''); const [geminiApiKey, setGeminiApiKey] = useState(''); const [showGuide, setShowGuide] = useState(false); + const [claudeApiKey, setClaudeApiKey] = useState(''); + const [openrouterApiKey, setOpenrouterApiKey] = useState(''); const [showOpenaiKey, setShowOpenaiKey] = useState(false); const [showGeminiKey, setShowGeminiKey] = useState(false); + const [showClaudeKey, setShowClaudeKey] = useState(false); + const [showOpenrouterKey, setShowOpenrouterKey] = useState(false); const [savingKeys, setSavingKeys] = useState(false); const [keysMessage, setKeysMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); const { getAuthHeader } = useAuthStore(); @@ -86,6 +90,8 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({ isOpen, onToggle }) => { .then(data => { setOpenaiApiKey(data.openai_api_key || ''); setGeminiApiKey(data.gemini_api_key || ''); + setClaudeApiKey(data.claude_api_key || ''); + setOpenrouterApiKey(data.openrouter_api_key || ''); }) .catch(() => {}); } @@ -101,6 +107,8 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({ isOpen, onToggle }) => { body: JSON.stringify({ openai_api_key: openaiApiKey.includes('*') ? undefined : openaiApiKey, gemini_api_key: geminiApiKey.includes('*') ? undefined : geminiApiKey, + claude_api_key: claudeApiKey.includes('*') ? undefined : claudeApiKey, + openrouter_api_key: openrouterApiKey.includes('*') ? undefined : openrouterApiKey, }), }); if (res.ok) { @@ -111,6 +119,8 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({ isOpen, onToggle }) => { }).then(r => r.json()); setOpenaiApiKey(data.openai_api_key || ''); setGeminiApiKey(data.gemini_api_key || ''); + setClaudeApiKey(data.claude_api_key || ''); + setOpenrouterApiKey(data.openrouter_api_key || ''); } else { setKeysMessage({ type: 'error', text: 'Failed to save API keys' }); } @@ -628,8 +638,8 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({ isOpen, onToggle }) => { onChange={e => setGeminiApiKey(e.target.value)} placeholder="AI..." className={`w-full px-3 py-2 pr-10 rounded-lg text-sm ${ - isDark - ? 'bg-gray-700 border-gray-600 text-white placeholder-gray-500' + isDark + ? 'bg-gray-700 border-gray-600 text-white placeholder-gray-500' : 'bg-white border-gray-300 text-gray-900 placeholder-gray-400' } border focus:outline-none focus:ring-2 focus:ring-blue-500`} /> @@ -643,6 +653,60 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({ isOpen, onToggle }) => { </div> </div> + {/* Claude API Key */} + <div> + <label className={`block text-xs mb-1 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}> + Claude API Key + </label> + <div className="relative"> + <input + type={showClaudeKey ? 'text' : 'password'} + value={claudeApiKey} + onChange={e => setClaudeApiKey(e.target.value)} + placeholder="sk-ant-..." + className={`w-full px-3 py-2 pr-10 rounded-lg text-sm ${ + isDark + ? 'bg-gray-700 border-gray-600 text-white placeholder-gray-500' + : 'bg-white border-gray-300 text-gray-900 placeholder-gray-400' + } border focus:outline-none focus:ring-2 focus:ring-blue-500`} + /> + <button + type="button" + onClick={() => setShowClaudeKey(!showClaudeKey)} + className={`absolute right-2 top-1/2 -translate-y-1/2 p-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`} + > + {showClaudeKey ? <EyeOff size={16} /> : <Eye size={16} />} + </button> + </div> + </div> + + {/* OpenRouter API Key (Fallback) */} + <div> + <label className={`block text-xs mb-1 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}> + OpenRouter API Key <span className={`text-[10px] ${isDark ? 'text-gray-500' : 'text-gray-400'}`}>(fallback)</span> + </label> + <div className="relative"> + <input + type={showOpenrouterKey ? 'text' : 'password'} + value={openrouterApiKey} + onChange={e => setOpenrouterApiKey(e.target.value)} + placeholder="sk-or-v1-..." + className={`w-full px-3 py-2 pr-10 rounded-lg text-sm ${ + isDark + ? 'bg-gray-700 border-gray-600 text-white placeholder-gray-500' + : 'bg-white border-gray-300 text-gray-900 placeholder-gray-400' + } border focus:outline-none focus:ring-2 focus:ring-blue-500`} + /> + <button + type="button" + onClick={() => setShowOpenrouterKey(!showOpenrouterKey)} + className={`absolute right-2 top-1/2 -translate-y-1/2 p-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`} + > + {showOpenrouterKey ? <EyeOff size={16} /> : <Eye size={16} />} + </button> + </div> + </div> + {/* Save Button */} <button onClick={handleSaveApiKeys} diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 5141081..78d2475 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -5,6 +5,7 @@ import { useAuthStore } from '../store/authStore'; import type { NodeData, Trace, Message, MergedTrace, MergeStrategy } from '../store/flowStore'; import type { Edge } from 'reactflow'; import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; import { Play, Settings, Info, ChevronLeft, ChevronRight, Maximize2, Edit3, X, Check, FileText, MessageCircle, Send, GripVertical, GitMerge, Trash2, AlertCircle, Loader2, Navigation, Upload, Search, Link } from 'lucide-react'; interface SidebarProps { @@ -312,7 +313,7 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => { scopes, merge_strategy: selectedNode.data.mergeStrategy || 'smart', config: { - provider: selectedNode.data.model.includes('gpt') || selectedNode.data.model === 'o3' ? 'openai' : 'google', + provider: selectedNode.data.model.includes('claude') ? 'claude' : (selectedNode.data.model.includes('gpt') || selectedNode.data.model === 'o3') ? 'openai' : 'google', model_name: selectedNode.data.model, temperature: selectedNode.data.temperature, system_prompt: selectedNode.data.systemPrompt, @@ -1004,7 +1005,9 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => { 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); @@ -1024,7 +1027,7 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => { scopes, merge_strategy: 'smart', config: { - provider: isOpenAI ? 'openai' : 'google', + provider, model_name: modelAtSend, temperature: isReasoning ? 1 : tempAtSend, enable_google_search: webSearchAtSend, @@ -1424,6 +1427,12 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => { }} className="w-full border border-gray-300 rounded-md p-2 text-sm" > + <optgroup label="Claude"> + <option value="claude-sonnet-4-5">claude-sonnet-4.5</option> + <option value="claude-opus-4">claude-opus-4</option> + <option value="claude-opus-4-5">claude-opus-4.5</option> + <option value="claude-opus-4-6">claude-opus-4.6</option> + </optgroup> <optgroup label="Gemini"> <option value="gemini-2.5-flash">gemini-2.5-flash</option> <option value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option> @@ -1841,7 +1850,7 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => { ? 'bg-gray-900 border-gray-700 prose-invert text-gray-200' : 'bg-gray-50 border-gray-200 text-gray-900' }`}> - <ReactMarkdown>{selectedNode.data.response || (streamingNodeId === selectedNode.id ? streamBuffer : '')}</ReactMarkdown> + <ReactMarkdown remarkPlugins={[remarkGfm]}>{selectedNode.data.response || (streamingNodeId === selectedNode.id ? streamBuffer : '')}</ReactMarkdown> </div> )} </div> @@ -2135,7 +2144,7 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => { /> ) : ( <div className="prose prose-sm max-w-none"> - <ReactMarkdown>{selectedNode.data.response}</ReactMarkdown> + <ReactMarkdown remarkPlugins={[remarkGfm]}>{selectedNode.data.response}</ReactMarkdown> </div> )} </div> @@ -2419,6 +2428,12 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => { isDark ? 'bg-gray-700 border-gray-600 text-gray-200' : 'border-gray-300 text-gray-900' }`} > + <optgroup label="Claude"> + <option value="claude-sonnet-4-5">claude-sonnet-4.5</option> + <option value="claude-opus-4">claude-opus-4</option> + <option value="claude-opus-4-5">claude-opus-4.5</option> + <option value="claude-opus-4-6">claude-opus-4.6</option> + </optgroup> <optgroup label="Gemini"> <option value="gemini-2.5-flash">gemini-2.5-flash</option> <option value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option> @@ -2520,7 +2535,7 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => { <p className="whitespace-pre-wrap">{msg.content}</p> ) : ( <div className={`prose prose-sm max-w-none ${isDark ? 'prose-invert' : ''}`}> - <ReactMarkdown>{msg.content}</ReactMarkdown> + <ReactMarkdown remarkPlugins={[remarkGfm]}>{msg.content}</ReactMarkdown> </div> )} </div> |
