From 718c7f50992656a97434ce5041e716145ec3a5c8 Mon Sep 17 00:00:00 2001
From: blackhao <13851610112@163.com>
Date: Wed, 10 Dec 2025 21:22:14 -0600
Subject: set keys

---
 frontend/src/components/LeftSidebar.tsx | 193 +++++++++++++++++++++++++++++++-
 1 file changed, 187 insertions(+), 6 deletions(-)

(limited to 'frontend/src/components/LeftSidebar.tsx')

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 
-- 
cgit v1.2.3

