From ffbd47563e7549c1691ebcfff6ff88f6f50ebe02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CBeeRad=E2=80=9D?= Date: Tue, 30 Dec 2025 09:11:19 +1100 Subject: [PATCH] sync: Settings panel redesign + Map visualization overhaul MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Settings viewers: consistent minimal styling across all tabs - MapViewer: cluster layout, node sizing by edge count, transparent flat circles, connected node highlighting - SettingsCog: green ring around settings icon for visibility - All viewers: refined typography, spacing, and card styling 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/components/settings/ApiKeysViewer.tsx | 446 +++++++---------- src/components/settings/ContextViewer.tsx | 289 +++++------ src/components/settings/LogsViewer.tsx | 133 +++-- src/components/settings/MapViewer.tsx | 524 +++++++++----------- src/components/settings/SettingsCog.tsx | 4 +- src/components/settings/ToolsViewer.tsx | 137 +++-- src/components/settings/WorkflowsViewer.tsx | 207 ++++---- 7 files changed, 753 insertions(+), 987 deletions(-) diff --git a/src/components/settings/ApiKeysViewer.tsx b/src/components/settings/ApiKeysViewer.tsx index 58fa064..40837df 100644 --- a/src/components/settings/ApiKeysViewer.tsx +++ b/src/components/settings/ApiKeysViewer.tsx @@ -1,19 +1,15 @@ "use client"; -import { useState, useEffect } from 'react'; +import { useState, useEffect, type CSSProperties } from 'react'; import { apiKeyService, ApiKeyStatus } from '@/services/storage/apiKeys'; export default function ApiKeysViewer() { const [openaiKey, setOpenaiKey] = useState(''); const [anthropicKey, setAnthropicKey] = useState(''); - const [status, setStatus] = useState({ - openai: 'not-set', - anthropic: 'not-set' - }); + const [status, setStatus] = useState({ openai: 'not-set', anthropic: 'not-set' }); const [showKeys, setShowKeys] = useState(false); useEffect(() => { - // Load existing keys and status const stored = apiKeyService.getStoredKeys(); setOpenaiKey(stored.openai || ''); setAnthropicKey(stored.anthropic || ''); @@ -22,52 +18,18 @@ export default function ApiKeysViewer() { const handleSaveOpenAi = async () => { if (!openaiKey.trim()) return; - - try { - apiKeyService.setOpenAiKey(openaiKey.trim()); - // Test the connection - const isConnected = await apiKeyService.testOpenAiConnection(); - setStatus(prev => ({ ...prev, openai: isConnected ? 'connected' : 'failed' })); - - if (isConnected) { - alert('OpenAI API key saved and connection verified!'); - } else { - alert('OpenAI API key saved but connection failed. Please check your key.'); - } - } catch (error) { - alert(`Failed to save OpenAI key: ${error instanceof Error ? error.message : 'Unknown error'}`); - } + apiKeyService.setOpenAiKey(openaiKey.trim()); + setStatus(prev => ({ ...prev, openai: 'testing' })); + const ok = await apiKeyService.testOpenAiConnection(); + setStatus(prev => ({ ...prev, openai: ok ? 'connected' : 'failed' })); }; const handleSaveAnthropic = async () => { if (!anthropicKey.trim()) return; - - try { - apiKeyService.setAnthropicKey(anthropicKey.trim()); - // Test the connection - const isConnected = await apiKeyService.testAnthropicConnection(); - setStatus(prev => ({ ...prev, anthropic: isConnected ? 'connected' : 'failed' })); - - if (isConnected) { - alert('Anthropic API key saved and connection verified!'); - } else { - alert('Anthropic API key saved but connection failed. Please check your key.'); - } - } catch (error) { - alert(`Failed to save Anthropic key: ${error instanceof Error ? error.message : 'Unknown error'}`); - } - }; - - const handleTestOpenAi = async () => { - setStatus(prev => ({ ...prev, openai: 'testing' })); - const isConnected = await apiKeyService.testOpenAiConnection(); - setStatus(prev => ({ ...prev, openai: isConnected ? 'connected' : 'failed' })); - }; - - const handleTestAnthropic = async () => { + apiKeyService.setAnthropicKey(anthropicKey.trim()); setStatus(prev => ({ ...prev, anthropic: 'testing' })); - const isConnected = await apiKeyService.testAnthropicConnection(); - setStatus(prev => ({ ...prev, anthropic: isConnected ? 'connected' : 'failed' })); + const ok = await apiKeyService.testAnthropicConnection(); + setStatus(prev => ({ ...prev, anthropic: ok ? 'connected' : 'failed' })); }; const handleClearOpenAi = () => { @@ -82,269 +44,203 @@ export default function ApiKeysViewer() { setStatus(prev => ({ ...prev, anthropic: 'not-set' })); }; - const getStatusIcon = (statusValue: string) => { - switch (statusValue) { - case 'connected': return '✅'; - case 'failed': return '❌'; - case 'testing': return '⏳'; - default: return '⚪'; - } - }; - - const getStatusColor = (statusValue: string) => { - switch (statusValue) { - case 'connected': return '#22c55e'; - case 'failed': return '#ef4444'; - case 'testing': return '#f59e0b'; - default: return '#6b7280'; - } + const getStatusLabel = (s: string) => { + if (s === 'connected') return { text: 'Connected', color: '#22c55e' }; + if (s === 'failed') return { text: 'Failed', color: '#ef4444' }; + if (s === 'testing') return { text: 'Testing...', color: '#6b7280' }; + return { text: 'Not set', color: '#6b7280' }; }; return ( -
-
-

- API Configuration -

-

- Keys are stored locally and never shared with RA-H servers. -

-
+
+

Keys are stored locally and never shared.

- {/* Show/Hide Keys Toggle */} -
- -
+ - {/* OpenAI Section */} -
-
-

- OpenAI API Key -

-
- - {getStatusIcon(status.openai)} - - - {status.openai === 'connected' && 'Connected'} - {status.openai === 'failed' && 'Connection Failed'} - {status.openai === 'testing' && 'Testing...'} - {status.openai === 'not-set' && 'Not Set'} - -
+ {/* OpenAI */} +
+
+ OpenAI + + {getStatusLabel(status.openai).text} +
- -
- setOpenaiKey(e.target.value)} - placeholder="sk-proj-... or sk-..." - style={{ - width: '100%', - padding: '12px', - fontSize: '14px', - background: '#1a1a1a', - border: '1px solid #333', - borderRadius: '6px', - color: '#fff', - fontFamily: 'monospace' - }} - /> -
- -
+ setOpenaiKey(e.target.value)} + placeholder="sk-..." + style={inputStyle} + /> +
- -
- {/* Anthropic Section */} -
-
-

- Anthropic API Key -

-
- - {getStatusIcon(status.anthropic)} - - - {status.anthropic === 'connected' && 'Connected'} - {status.anthropic === 'failed' && 'Connection Failed'} - {status.anthropic === 'testing' && 'Testing...'} - {status.anthropic === 'not-set' && 'Not Set'} - -
+ {/* Anthropic */} +
+
+ Anthropic + + {getStatusLabel(status.anthropic).text} +
- -
- setAnthropicKey(e.target.value)} - placeholder="sk-ant-api03-..." - style={{ - width: '100%', - padding: '12px', - fontSize: '14px', - background: '#1a1a1a', - border: '1px solid #333', - borderRadius: '6px', - color: '#fff', - fontFamily: 'monospace' - }} - /> -
- -
+ setAnthropicKey(e.target.value)} + placeholder="sk-ant-..." + style={inputStyle} + /> +
- -
- {/* Help Section */} -
-
- How to get API keys: -
- -

- Your keys are stored locally and never sent to RA-H servers. They are only used to communicate directly with OpenAI and Anthropic APIs. -

+ {/* Help */} +
+
Get keys from:
+
); -} \ No newline at end of file +} + +const containerStyle: CSSProperties = { + padding: 24, + height: '100%', + overflow: 'auto', +}; + +const descStyle: CSSProperties = { + fontSize: 13, + color: '#6b7280', + marginBottom: 16, +}; + +const toggleStyle: CSSProperties = { + display: 'flex', + alignItems: 'center', + fontSize: 12, + color: '#6b7280', + cursor: 'pointer', + marginBottom: 20, +}; + +const cardStyle: CSSProperties = { + background: 'rgba(255, 255, 255, 0.02)', + border: '1px solid rgba(255, 255, 255, 0.06)', + borderRadius: 8, + padding: 16, + marginBottom: 12, +}; + +const cardHeaderStyle: CSSProperties = { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 12, +}; + +const cardTitleStyle: CSSProperties = { + fontSize: 13, + fontWeight: 500, + color: '#e5e7eb', +}; + +const inputStyle: CSSProperties = { + width: '100%', + padding: '10px 12px', + fontSize: 13, + fontFamily: 'monospace', + background: 'rgba(0, 0, 0, 0.3)', + border: '1px solid rgba(255, 255, 255, 0.08)', + borderRadius: 6, + color: '#e5e7eb', + marginBottom: 12, + outline: 'none', +}; + +const buttonRowStyle: CSSProperties = { + display: 'flex', + gap: 8, +}; + +const btnPrimaryStyle: CSSProperties = { + padding: '8px 14px', + fontSize: 12, + fontWeight: 500, + background: '#22c55e', + color: '#052e16', + border: 'none', + borderRadius: 6, + cursor: 'pointer', +}; + +const btnSecondaryStyle: CSSProperties = { + padding: '8px 14px', + fontSize: 12, + fontWeight: 500, + background: 'rgba(255, 255, 255, 0.06)', + color: '#9ca3af', + border: 'none', + borderRadius: 6, + cursor: 'pointer', +}; + +const helpStyle: CSSProperties = { + marginTop: 8, + padding: 16, + background: 'rgba(255, 255, 255, 0.02)', + border: '1px solid rgba(255, 255, 255, 0.06)', + borderRadius: 8, + fontSize: 12, + color: '#6b7280', +}; + +const linkStyle: CSSProperties = { + color: '#22c55e', + textDecoration: 'none', +}; diff --git a/src/components/settings/ContextViewer.tsx b/src/components/settings/ContextViewer.tsx index 1c325de..0eae459 100644 --- a/src/components/settings/ContextViewer.tsx +++ b/src/components/settings/ContextViewer.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState, type CSSProperties } from 'react'; import type { Node } from '@/types/database'; interface AutoContextSettings { @@ -14,49 +14,33 @@ interface NodeWithMetrics extends Node { export default function ContextViewer() { const [nodes, setNodes] = useState([]); - const [autoContextEnabled, setAutoContextEnabled] = useState(false); - const [lastMigrated, setLastMigrated] = useState(); + const [enabled, setEnabled] = useState(false); const [loadingNodes, setLoadingNodes] = useState(true); const [loadingSettings, setLoadingSettings] = useState(true); const [saving, setSaving] = useState(false); - const [error, setError] = useState(null); useEffect(() => { const loadNodes = async () => { - setLoadingNodes(true); try { - const response = await fetch('/api/nodes?sortBy=edges&limit=10'); - if (!response.ok) { - throw new Error('Failed to load nodes'); - } - const payload = await response.json(); + const res = await fetch('/api/nodes?sortBy=edges&limit=10'); + const payload = await res.json(); setNodes(payload.data || []); - } catch (err) { - console.error(err); - setError('Unable to load top nodes.'); + } catch (e) { + console.error(e); } finally { setLoadingNodes(false); } }; const loadSettings = async () => { - setLoadingSettings(true); try { - const response = await fetch('/api/system/auto-context'); - if (!response.ok) { - throw new Error('Failed to load auto-context settings'); - } - const payload = (await response.json()) as { - success: boolean; - data?: AutoContextSettings; - }; + const res = await fetch('/api/system/auto-context'); + const payload = await res.json() as { success: boolean; data?: AutoContextSettings }; if (payload.success && payload.data) { - setAutoContextEnabled(Boolean(payload.data.autoContextEnabled)); - setLastMigrated(payload.data.lastPinnedMigration); + setEnabled(payload.data.autoContextEnabled); } - } catch (err) { - console.error(err); - setError('Unable to load auto-context settings.'); + } catch (e) { + console.error(e); } finally { setLoadingSettings(false); } @@ -66,176 +50,135 @@ export default function ContextViewer() { loadSettings(); }, []); - const toggleDescription = useMemo(() => { - if (!autoContextEnabled) { - return 'Disabled — chats and workflows will not receive BACKGROUND CONTEXT.'; - } - return 'Enabled — top 10 most-connected nodes will be added to BACKGROUND CONTEXT.'; - }, [autoContextEnabled]); - const handleToggle = async () => { if (saving) return; setSaving(true); - setError(null); try { - const response = await fetch('/api/system/auto-context', { + const res = await fetch('/api/system/auto-context', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ autoContextEnabled: !autoContextEnabled }), + body: JSON.stringify({ autoContextEnabled: !enabled }), }); - if (!response.ok) { - throw new Error(await response.text()); - } - - const payload = (await response.json()) as { - success: boolean; - data?: AutoContextSettings; - }; - + const payload = await res.json() as { success: boolean; data?: AutoContextSettings }; if (payload.success && payload.data) { - setAutoContextEnabled(payload.data.autoContextEnabled); - setLastMigrated(payload.data.lastPinnedMigration); - } else { - throw new Error(payload?.data ? 'Settings update failed' : 'Unknown error'); + setEnabled(payload.data.autoContextEnabled); } - } catch (err) { - console.error('Failed to update auto-context toggle', err); - setError('Unable to update setting. Please try again.'); + } catch (e) { + console.error(e); } finally { setSaving(false); } }; return ( -
-
-

Auto-Context

-

- Auto-context grabs your 10 nodes with the most edges and drops them into BACKGROUND - CONTEXT so ra-h knows which ideas connect everything else. -

-
+
+

+ Top 10 most-connected nodes are added to background context for chats and workflows. +

-
-
-
Enable Auto-Context
-
- {loadingSettings ? 'Loading preference…' : toggleDescription} + {/* Toggle */} +
+
+
+
Auto-Context
+
+ {loadingSettings ? 'Loading...' : enabled ? 'Enabled' : 'Disabled'} +
-
- -
- - {lastMigrated && ( -
- Auto-enabled because you previously pinned nodes · {new Date(lastMigrated).toLocaleString()} + > + +
- )} - -
-
Top 10 nodes by connections
- {loadingNodes ? ( -
Loading nodes…
- ) : nodes.length === 0 ? ( -
- No connected nodes yet. Create nodes and add edges to see context hubs. -
- ) : ( -
- {nodes.map((node) => ( -
-
-
- [NODE:{node.id}] {node.title || 'Untitled node'} -
-
- {node.edge_count ?? 0} connections -
-
-
- {node.dimensions && node.dimensions.length > 0 ? ( - node.dimensions.slice(0, 4).map((dimension) => ( - - {dimension} - - )) - ) : ( - No dimensions - )} -
-
- ))} -
- )}
- {error && ( -
- {error} + {/* Nodes List */} +
Top Nodes
+ {loadingNodes ? ( +
Loading...
+ ) : nodes.length === 0 ? ( +
No connected nodes yet.
+ ) : ( +
+ {nodes.map((node) => ( +
+
+ {node.title || 'Untitled'} + {node.edge_count ?? 0} +
+ {node.dimensions && node.dimensions.length > 0 && ( +
+ {node.dimensions.slice(0, 3).map((dim) => ( + {dim} + ))} +
+ )} +
+ ))}
)}
); } + +const containerStyle: CSSProperties = { padding: 24, height: '100%', overflow: 'auto' }; +const descStyle: CSSProperties = { fontSize: 13, color: '#6b7280', marginBottom: 20, lineHeight: 1.5 }; + +const cardStyle: CSSProperties = { + background: 'rgba(255, 255, 255, 0.02)', + border: '1px solid rgba(255, 255, 255, 0.06)', + borderRadius: 8, + padding: 16, + marginBottom: 24, +}; + +const labelStyle: CSSProperties = { fontSize: 13, fontWeight: 500, color: '#e5e7eb', marginBottom: 8 }; +const subLabelStyle: CSSProperties = { fontSize: 12, color: '#6b7280' }; +const mutedStyle: CSSProperties = { fontSize: 13, color: '#6b7280' }; + +const toggleStyle: CSSProperties = { + width: 48, + height: 26, + borderRadius: 13, + border: 'none', + cursor: 'pointer', + position: 'relative', + transition: 'background 0.15s', +}; + +const toggleKnobStyle: CSSProperties = { + position: 'absolute', + top: 4, + width: 18, + height: 18, + borderRadius: '50%', + background: '#fff', + transition: 'left 0.15s', +}; + +const nodeCardStyle: CSSProperties = { + padding: '12px 14px', + background: 'rgba(255, 255, 255, 0.02)', + border: '1px solid rgba(255, 255, 255, 0.06)', + borderRadius: 6, +}; + +const nodeTitleStyle: CSSProperties = { fontSize: 13, fontWeight: 500, color: '#e5e7eb' }; +const edgeCountStyle: CSSProperties = { fontSize: 12, color: '#22c55e' }; + +const dimTagStyle: CSSProperties = { + padding: '2px 8px', + borderRadius: 4, + fontSize: 11, + background: 'rgba(34, 197, 94, 0.1)', + color: '#22c55e', +}; diff --git a/src/components/settings/LogsViewer.tsx b/src/components/settings/LogsViewer.tsx index 162d6fa..68d3a16 100644 --- a/src/components/settings/LogsViewer.tsx +++ b/src/components/settings/LogsViewer.tsx @@ -127,86 +127,82 @@ export default function LogsViewer() {
-