summaryrefslogtreecommitdiff
path: root/frontend/src
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src')
-rw-r--r--frontend/src/components/LeftSidebar.tsx193
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