From 7d897ad9bb5ee46839ec91992cbbf4593168f119 Mon Sep 17 00:00:00 2001 From: YurenHao0426 Date: Fri, 13 Feb 2026 03:02:36 +0000 Subject: Add Claude provider, OpenRouter fallback, and GFM markdown support - Add Claude (Anthropic) as third LLM provider with streaming support - Add OpenRouter as transparent fallback when official API keys are missing or fail - Add remark-gfm to ReactMarkdown for table/strikethrough rendering - Claude models: sonnet-4.5, opus-4, opus-4.5, opus-4.6 - Backend: new stream_claude(), stream_openrouter(), provider routing, API key CRUD - Frontend: model selectors, API key inputs for Claude and OpenRouter - Auto-migration for new DB columns (claude_api_key, openrouter_api_key) Co-Authored-By: Claude Opus 4.6 --- frontend/src/components/LeftSidebar.tsx | 68 ++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) (limited to 'frontend/src/components/LeftSidebar.tsx') 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ isOpen, onToggle }) => { + {/* Claude API Key */} +
+ +
+ 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`} + /> + +
+
+ + {/* OpenRouter API Key (Fallback) */} +
+ +
+ 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`} + /> + +
+
+ {/* Save Button */}