summaryrefslogtreecommitdiff
path: root/src/renderer
diff options
context:
space:
mode:
authorhaoyuren <13851610112@163.com>2026-03-15 20:52:13 -0500
committerhaoyuren <13851610112@163.com>2026-03-15 20:52:13 -0500
commit66a403488f3a7bc32a02bc9933c396dc4c4e031d (patch)
treeaf52777ac10a1b31487203c4281178e9188c27ba /src/renderer
parent6d4ee5ccd5529d6d2764da73e73fac57d7bfb216 (diff)
Add API key management UI and wire S2 key to MCP
- Settings modal on project list page for OpenAI, Anthropic, OpenRouter, Gemini, Semantic Scholar keys - Keys stored in userData/api-keys.json, masked by default with show/hide toggle - S2 API key passed to MCP server via .lattex-mcp.json to avoid rate limits Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'src/renderer')
-rw-r--r--src/renderer/src/components/ProjectList.tsx72
1 files changed, 72 insertions, 0 deletions
diff --git a/src/renderer/src/components/ProjectList.tsx b/src/renderer/src/components/ProjectList.tsx
index 2fe23de..7a9fd7f 100644
--- a/src/renderer/src/components/ProjectList.tsx
+++ b/src/renderer/src/components/ProjectList.tsx
@@ -32,6 +32,9 @@ export default function ProjectList({ onOpenProject }: Props) {
const [sortOrder, setSortOrder] = useState<SortOrder>('desc')
const [showNewProject, setShowNewProject] = useState(false)
const [newProjectName, setNewProjectName] = useState('Untitled Project')
+ const [showApiKeys, setShowApiKeys] = useState(false)
+ const [apiKeys, setApiKeys] = useState<Record<string, string>>({})
+ const [apiKeysVisible, setApiKeysVisible] = useState<Record<string, boolean>>({})
const { setStatusMessage } = useAppStore()
const loadProjects = useCallback(async () => {
@@ -123,6 +126,32 @@ export default function ProjectList({ onOpenProject }: Props) {
useAppStore.getState().setScreen('login')
}
+ const openApiKeys = async () => {
+ const keys = await window.api.getApiKeys()
+ setApiKeys(keys)
+ setApiKeysVisible({})
+ setShowApiKeys(true)
+ }
+
+ const saveApiKeys = async () => {
+ // Strip empty keys before saving
+ const cleaned: Record<string, string> = {}
+ for (const [k, v] of Object.entries(apiKeys)) {
+ if (v.trim()) cleaned[k] = v.trim()
+ }
+ await window.api.setApiKeys(cleaned)
+ setShowApiKeys(false)
+ setStatusMessage('API keys saved')
+ }
+
+ const API_KEY_FIELDS = [
+ { id: 'openai', label: 'OpenAI', placeholder: 'sk-...' },
+ { id: 'anthropic', label: 'Anthropic (Claude)', placeholder: 'sk-ant-...' },
+ { id: 'openrouter', label: 'OpenRouter', placeholder: 'sk-or-...' },
+ { id: 'gemini', label: 'Google Gemini', placeholder: 'AIza...' },
+ { id: 'semanticScholar', label: 'Semantic Scholar', placeholder: 'API key (optional, avoids rate limits)' }
+ ]
+
const toggleSort = (key: SortKey) => {
if (sortBy === key) {
setSortOrder((o) => (o === 'asc' ? 'desc' : 'asc'))
@@ -206,6 +235,9 @@ export default function ProjectList({ onOpenProject }: Props) {
<div className="projects-header">
<h1>Latte<span className="lattex-x">X</span></h1>
<div className="projects-header-actions">
+ <button className="btn btn-secondary btn-sm" onClick={openApiKeys}>
+ API Keys
+ </button>
<button className="btn btn-secondary btn-sm" onClick={handleLogout}>
Sign out
</button>
@@ -312,6 +344,46 @@ export default function ProjectList({ onOpenProject }: Props) {
</div>
</div>
)}
+ {showApiKeys && (
+ <div className="modal-overlay" onClick={() => setShowApiKeys(false)}>
+ <div className="modal-box" onClick={(e) => e.stopPropagation()} style={{ minWidth: 460 }}>
+ <h3 style={{ margin: '0 0 4px' }}>API Keys</h3>
+ <p style={{ margin: '0 0 16px', fontSize: 12, color: 'var(--text-secondary)' }}>
+ Keys are stored locally on this device.
+ </p>
+ {API_KEY_FIELDS.map((field) => (
+ <div key={field.id} style={{ marginBottom: 12 }}>
+ <label style={{ display: 'block', fontSize: 12, fontWeight: 500, marginBottom: 4, color: 'var(--text-secondary)' }}>
+ {field.label}
+ </label>
+ <div style={{ display: 'flex', gap: 4 }}>
+ <input
+ type={apiKeysVisible[field.id] ? 'text' : 'password'}
+ className="modal-input"
+ value={apiKeys[field.id] || ''}
+ onChange={(e) => setApiKeys({ ...apiKeys, [field.id]: e.target.value })}
+ placeholder={field.placeholder}
+ spellCheck={false}
+ autoComplete="off"
+ />
+ <button
+ className="btn btn-secondary btn-sm"
+ onClick={() => setApiKeysVisible({ ...apiKeysVisible, [field.id]: !apiKeysVisible[field.id] })}
+ style={{ flexShrink: 0, padding: '6px 8px', fontSize: 11 }}
+ title={apiKeysVisible[field.id] ? 'Hide' : 'Show'}
+ >
+ {apiKeysVisible[field.id] ? 'Hide' : 'Show'}
+ </button>
+ </div>
+ </div>
+ ))}
+ <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 16 }}>
+ <button className="btn btn-secondary btn-sm" onClick={() => setShowApiKeys(false)}>Cancel</button>
+ <button className="btn btn-primary btn-sm" onClick={saveApiKeys}>Save</button>
+ </div>
+ </div>
+ </div>
+ )}
</div>
)
}