diff options
Diffstat (limited to 'frontend/src/components')
| -rw-r--r-- | frontend/src/components/LeftSidebar.tsx | 193 |
1 files changed, 187 insertions, 6 deletions
diff --git a/frontend/src/components/LeftSidebar.tsx b/frontend/src/components/LeftSidebar.tsx index 1a111bf..d929dcc 100644 --- a/frontend/src/components/LeftSidebar.tsx +++ b/frontend/src/components/LeftSidebar.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react' import { useReactFlow } from 'reactflow'; import { Folder, FileText, Archive, ChevronLeft, ChevronRight, Trash2, MessageSquare, - MoreVertical, Download, Upload, Plus, RefreshCw, Edit3, Loader2, LogOut, User + MoreVertical, Download, Upload, Plus, RefreshCw, Edit3, Loader2, LogOut, User, Settings, Key, X, Eye, EyeOff } from 'lucide-react'; import useFlowStore, { type FSItem, type BlueprintDocument, type FileMeta } from '../store/flowStore'; import { useAuthStore } from '../store/authStore'; @@ -54,6 +54,60 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({ isOpen, onToggle }) => { const [openaiPurpose, setOpenaiPurpose] = useState<string>('user_data'); const [fileSearch, setFileSearch] = useState(''); + // User Settings Modal State + const [showUserSettings, setShowUserSettings] = useState(false); + const [openaiApiKey, setOpenaiApiKey] = useState(''); + const [geminiApiKey, setGeminiApiKey] = useState(''); + const [showOpenaiKey, setShowOpenaiKey] = useState(false); + const [showGeminiKey, setShowGeminiKey] = useState(false); + const [savingKeys, setSavingKeys] = useState(false); + const [keysMessage, setKeysMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + const { getAuthHeader } = useAuthStore(); + + // Load API keys when settings modal opens + useEffect(() => { + if (showUserSettings) { + fetch('/api/auth/api-keys', { + headers: { ...getAuthHeader() }, + }) + .then(res => res.json()) + .then(data => { + setOpenaiApiKey(data.openai_api_key || ''); + setGeminiApiKey(data.gemini_api_key || ''); + }) + .catch(() => {}); + } + }, [showUserSettings, getAuthHeader]); + + const handleSaveApiKeys = async () => { + setSavingKeys(true); + setKeysMessage(null); + try { + const res = await fetch('/api/auth/api-keys', { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...getAuthHeader() }, + body: JSON.stringify({ + openai_api_key: openaiApiKey.includes('*') ? undefined : openaiApiKey, + gemini_api_key: geminiApiKey.includes('*') ? undefined : geminiApiKey, + }), + }); + if (res.ok) { + setKeysMessage({ type: 'success', text: 'API keys saved successfully!' }); + // Reload masked keys + const data = await fetch('/api/auth/api-keys', { + headers: { ...getAuthHeader() }, + }).then(r => r.json()); + setOpenaiApiKey(data.openai_api_key || ''); + setGeminiApiKey(data.gemini_api_key || ''); + } else { + setKeysMessage({ type: 'error', text: 'Failed to save API keys' }); + } + } catch { + setKeysMessage({ type: 'error', text: 'Network error' }); + } + setSavingKeys(false); + }; + const handleDragStart = (e: React.DragEvent, archiveId: string) => { e.dataTransfer.setData('archiveId', archiveId); e.dataTransfer.effectAllowed = 'copy'; @@ -459,15 +513,15 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({ isOpen, onToggle }) => { <div className="flex items-center gap-1"> {user && ( <button - onClick={logout} + onClick={() => setShowUserSettings(true)} className={`p-1 rounded transition-colors ${ isDark - ? 'hover:bg-red-900/50 text-gray-400 hover:text-red-400' - : 'hover:bg-red-50 text-gray-500 hover:text-red-500' + ? 'hover:bg-gray-700 text-gray-400 hover:text-gray-200' + : 'hover:bg-gray-200 text-gray-500 hover:text-gray-700' }`} - title="Logout" + title="User Settings" > - <LogOut size={16} /> + <Settings size={16} /> </button> )} <button @@ -479,6 +533,133 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({ isOpen, onToggle }) => { </div> </div> + {/* User Settings Modal */} + {showUserSettings && ( + <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={() => setShowUserSettings(false)}> + <div + className={`w-full max-w-md mx-4 rounded-xl shadow-2xl ${isDark ? 'bg-gray-800' : 'bg-white'}`} + onClick={e => e.stopPropagation()} + > + {/* Header */} + <div className={`flex items-center justify-between p-4 border-b ${isDark ? 'border-gray-700' : 'border-gray-200'}`}> + <h2 className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>User Settings</h2> + <button + onClick={() => setShowUserSettings(false)} + className={`p-1 rounded ${isDark ? 'hover:bg-gray-700' : 'hover:bg-gray-100'}`} + > + <X size={20} className={isDark ? 'text-gray-400' : 'text-gray-500'} /> + </button> + </div> + + {/* Content */} + <div className="p-4 space-y-4"> + {/* User Info */} + <div className={`flex items-center gap-3 p-3 rounded-lg ${isDark ? 'bg-gray-700/50' : 'bg-gray-100'}`}> + <div className={`w-10 h-10 rounded-full flex items-center justify-center ${isDark ? 'bg-blue-600' : 'bg-blue-500'}`}> + <User size={20} className="text-white" /> + </div> + <div> + <p className={`font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>{user?.username}</p> + <p className={`text-sm ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>{user?.email}</p> + </div> + </div> + + {/* API Keys Section */} + <div className="space-y-3"> + <h3 className={`text-sm font-medium flex items-center gap-2 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}> + <Key size={16} /> API Keys + </h3> + + {/* OpenAI API Key */} + <div> + <label className={`block text-xs mb-1 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}> + OpenAI API Key + </label> + <div className="relative"> + <input + type={showOpenaiKey ? 'text' : 'password'} + value={openaiApiKey} + onChange={e => setOpenaiApiKey(e.target.value)} + placeholder="sk-..." + 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={() => setShowOpenaiKey(!showOpenaiKey)} + className={`absolute right-2 top-1/2 -translate-y-1/2 p-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`} + > + {showOpenaiKey ? <EyeOff size={16} /> : <Eye size={16} />} + </button> + </div> + </div> + + {/* Gemini API Key */} + <div> + <label className={`block text-xs mb-1 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}> + Gemini API Key + </label> + <div className="relative"> + <input + type={showGeminiKey ? 'text' : 'password'} + value={geminiApiKey} + 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' + : '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={() => setShowGeminiKey(!showGeminiKey)} + className={`absolute right-2 top-1/2 -translate-y-1/2 p-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`} + > + {showGeminiKey ? <EyeOff size={16} /> : <Eye size={16} />} + </button> + </div> + </div> + + {/* Save Button */} + <button + onClick={handleSaveApiKeys} + disabled={savingKeys} + className="w-full py-2 px-4 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2" + > + {savingKeys ? <Loader2 size={16} className="animate-spin" /> : null} + {savingKeys ? 'Saving...' : 'Save API Keys'} + </button> + + {/* Message */} + {keysMessage && ( + <p className={`text-xs ${keysMessage.type === 'success' ? 'text-green-500' : 'text-red-500'}`}> + {keysMessage.text} + </p> + )} + </div> + </div> + + {/* Footer */} + <div className={`p-4 border-t ${isDark ? 'border-gray-700' : 'border-gray-200'}`}> + <button + onClick={() => { logout(); setShowUserSettings(false); }} + className={`w-full py-2 px-4 rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2 ${ + isDark + ? 'bg-red-900/30 hover:bg-red-900/50 text-red-400' + : 'bg-red-50 hover:bg-red-100 text-red-600' + }`} + > + <LogOut size={16} /> Log Out + </button> + </div> + </div> + </div> + )} + {/* Tabs */} <div className={`flex border-b ${isDark ? 'border-gray-700' : 'border-gray-200'}`}> <button |
