From 7d897ad9bb5ee46839ec91992cbbf4593168f119 Mon Sep 17 00:00:00 2001
From: YurenHao0426 <blackhao0426@gmail.com>
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 <noreply@anthropic.com>
---
 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<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}
-- 
cgit v1.2.3

