summaryrefslogtreecommitdiff
path: root/frontend/src/components/LeftSidebar.tsx
diff options
context:
space:
mode:
authorYurenHao0426 <blackhao0426@gmail.com>2026-02-13 03:02:36 +0000
committerYurenHao0426 <blackhao0426@gmail.com>2026-02-13 03:02:36 +0000
commit7d897ad9bb5ee46839ec91992cbbf4593168f119 (patch)
treeb4549f64176e93474b3b6c4b36294d30a46230b7 /frontend/src/components/LeftSidebar.tsx
parent2f19d8cb84598e0822b525f5fb5c456c07448fb7 (diff)
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>
Diffstat (limited to 'frontend/src/components/LeftSidebar.tsx')
-rw-r--r--frontend/src/components/LeftSidebar.tsx68
1 files changed, 66 insertions, 2 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}