Merge branch 'feature/settings-panel-overhaul'

This commit is contained in:
“BeeRad”
2026-03-21 16:07:24 +11:00
5 changed files with 347 additions and 375 deletions
+27 -1
View File
@@ -66,6 +66,19 @@ html[data-theme="dark"] {
--rah-backdrop: rgba(0, 0, 0, 0.85);
--rah-shadow-modal: 0 8px 32px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(255, 255, 255, 0.05);
--rah-shadow-floating: 0 4px 12px rgba(0, 0, 0, 0.4);
--settings-bg: rgba(10, 10, 10, 0.95);
--settings-sidebar-bg: rgba(0, 0, 0, 0.3);
--settings-card-bg: rgba(255, 255, 255, 0.02);
--settings-code-bg: rgba(0, 0, 0, 0.24);
--settings-border: rgba(255, 255, 255, 0.06);
--settings-border-strong: rgba(255, 255, 255, 0.14);
--settings-text: #e5e7eb;
--settings-subtext: #d1d5db;
--settings-muted: #6b7280;
--settings-active-bg: rgba(255, 255, 255, 0.04);
--settings-active-border: rgba(255, 255, 255, 0.12);
--settings-chip-bg: rgba(255, 255, 255, 0.04);
--settings-danger: #f87171;
}
html[data-theme="light"] {
@@ -97,6 +110,19 @@ html[data-theme="light"] {
--rah-backdrop: rgba(0, 0, 0, 0.5);
--rah-shadow-modal: 0 10px 36px rgba(0, 0, 0, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.04);
--rah-shadow-floating: 0 6px 20px rgba(0, 0, 0, 0.16);
--settings-bg: #ffffff;
--settings-sidebar-bg: #f5f5f5;
--settings-card-bg: #fafafa;
--settings-code-bg: #f3f4f6;
--settings-border: rgba(0, 0, 0, 0.08);
--settings-border-strong: rgba(0, 0, 0, 0.16);
--settings-text: #111111;
--settings-subtext: #333333;
--settings-muted: #6b7280;
--settings-active-bg: rgba(0, 0, 0, 0.06);
--settings-active-border: rgba(0, 0, 0, 0.08);
--settings-chip-bg: rgba(0, 0, 0, 0.03);
--settings-danger: #b91c1c;
}
/* Modern Clean Design System - RA-H */
@@ -254,4 +280,4 @@ html, body {
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
}
+25 -21
View File
@@ -37,8 +37,11 @@ export default function ApiKeysViewer() {
<span style={cardTitleStyle}>OpenAI API Key</span>
<span style={{
fontSize: 12,
color: status === 'configured' ? '#22c55e' :
status === 'checking' ? '#6b7280' : '#ef4444'
color: status === 'checking'
? 'var(--settings-muted)'
: status === 'configured'
? 'var(--settings-text)'
: 'var(--settings-danger)'
}}>
{status === 'configured' ? 'Configured' :
status === 'checking' ? 'Checking...' : 'Not configured'}
@@ -52,7 +55,7 @@ export default function ApiKeysViewer() {
<div style={codeBlockStyle}>
<code>OPENAI_API_KEY=sk-your-key-here</code>
</div>
<p style={{ margin: 0, fontSize: 12, color: '#6b7280' }}>
<p style={{ margin: 0, fontSize: 12, color: 'var(--settings-muted)' }}>
Restart the app after changing the key.
</p>
</div>
@@ -84,8 +87,8 @@ const containerStyle: CSSProperties = {
};
const featuresBoxStyle: CSSProperties = {
background: 'rgba(34, 197, 94, 0.08)',
border: '1px solid rgba(34, 197, 94, 0.2)',
background: 'var(--settings-card-bg)',
border: '1px solid var(--settings-border)',
borderRadius: 8,
padding: 16,
marginBottom: 20,
@@ -94,7 +97,7 @@ const featuresBoxStyle: CSSProperties = {
const featuresHeaderStyle: CSSProperties = {
fontSize: 13,
fontWeight: 500,
color: '#22c55e',
color: 'var(--settings-text)',
marginBottom: 8,
};
@@ -102,20 +105,20 @@ const featuresListStyle: CSSProperties = {
margin: 0,
paddingLeft: 20,
fontSize: 13,
color: '#d1d5db',
color: 'var(--settings-subtext)',
lineHeight: 1.6,
};
const noteStyle: CSSProperties = {
marginTop: 12,
fontSize: 12,
color: '#6b7280',
color: 'var(--settings-muted)',
fontStyle: 'italic',
};
const cardStyle: CSSProperties = {
background: 'rgba(255, 255, 255, 0.02)',
border: '1px solid rgba(255, 255, 255, 0.06)',
background: 'var(--settings-card-bg)',
border: '1px solid var(--settings-border)',
borderRadius: 8,
padding: 16,
marginBottom: 12,
@@ -131,46 +134,47 @@ const cardHeaderStyle: CSSProperties = {
const cardTitleStyle: CSSProperties = {
fontSize: 13,
fontWeight: 500,
color: '#e5e7eb',
color: 'var(--settings-text)',
};
const instructionsStyle: CSSProperties = {
fontSize: 13,
color: '#d1d5db',
color: 'var(--settings-subtext)',
lineHeight: 1.5,
};
const codeInlineStyle: CSSProperties = {
background: 'rgba(255, 255, 255, 0.08)',
background: 'var(--settings-code-bg)',
padding: '2px 6px',
borderRadius: 4,
fontSize: 12,
fontFamily: 'monospace',
color: '#22c55e',
color: 'var(--settings-text)',
};
const codeBlockStyle: CSSProperties = {
background: 'rgba(0, 0, 0, 0.4)',
border: '1px solid rgba(255, 255, 255, 0.08)',
background: 'var(--settings-code-bg)',
border: '1px solid var(--settings-border)',
borderRadius: 6,
padding: '10px 12px',
fontSize: 13,
fontFamily: 'monospace',
color: '#e5e7eb',
color: 'var(--settings-text)',
marginBottom: 8,
};
const helpStyle: CSSProperties = {
fontSize: 12,
color: '#6b7280',
color: 'var(--settings-muted)',
};
const linkStyle: CSSProperties = {
color: '#22c55e',
color: 'var(--settings-text)',
textDecoration: 'none',
background: 'transparent',
border: 'none',
padding: 0,
border: '1px solid var(--settings-border-strong)',
borderRadius: 6,
padding: '10px 14px',
cursor: 'pointer',
font: 'inherit',
};
+16 -15
View File
@@ -90,7 +90,7 @@ export default function ContextViewer() {
disabled={loadingSettings || saving}
style={{
...toggleStyle,
background: enabled ? '#22c55e' : 'rgba(255, 255, 255, 0.1)',
background: enabled ? 'var(--settings-active-bg)' : 'var(--settings-code-bg)',
}}
>
<span style={{
@@ -131,25 +131,25 @@ export default function ContextViewer() {
}
const containerStyle: CSSProperties = { padding: 24, height: '100%', overflow: 'auto' };
const descStyle: CSSProperties = { fontSize: 13, color: '#6b7280', marginBottom: 20, lineHeight: 1.5 };
const descStyle: CSSProperties = { fontSize: 13, color: 'var(--settings-muted)', marginBottom: 20, lineHeight: 1.5 };
const cardStyle: CSSProperties = {
background: 'rgba(255, 255, 255, 0.02)',
border: '1px solid rgba(255, 255, 255, 0.06)',
background: 'var(--settings-card-bg)',
border: '1px solid var(--settings-border)',
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 labelStyle: CSSProperties = { fontSize: 13, fontWeight: 500, color: 'var(--settings-text)', marginBottom: 8 };
const subLabelStyle: CSSProperties = { fontSize: 12, color: 'var(--settings-muted)' };
const mutedStyle: CSSProperties = { fontSize: 13, color: 'var(--settings-muted)' };
const toggleStyle: CSSProperties = {
width: 48,
height: 26,
borderRadius: 13,
border: 'none',
border: '1px solid var(--settings-border)',
cursor: 'pointer',
position: 'relative',
transition: 'background 0.15s',
@@ -161,24 +161,25 @@ const toggleKnobStyle: CSSProperties = {
width: 18,
height: 18,
borderRadius: '50%',
background: '#fff',
background: 'var(--settings-text)',
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)',
background: 'var(--settings-card-bg)',
border: '1px solid var(--settings-border)',
borderRadius: 6,
};
const nodeTitleStyle: CSSProperties = { fontSize: 13, fontWeight: 500, color: '#e5e7eb' };
const edgeCountStyle: CSSProperties = { fontSize: 12, color: '#22c55e' };
const nodeTitleStyle: CSSProperties = { fontSize: 13, fontWeight: 500, color: 'var(--settings-text)' };
const edgeCountStyle: CSSProperties = { fontSize: 12, color: 'var(--settings-muted)' };
const dimTagStyle: CSSProperties = {
padding: '2px 8px',
borderRadius: 4,
fontSize: 11,
background: 'rgba(34, 197, 94, 0.1)',
color: '#22c55e',
background: 'var(--settings-chip-bg)',
color: 'var(--settings-subtext)',
border: '1px solid var(--settings-border)',
};
+108 -61
View File
@@ -1,6 +1,6 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState, type CSSProperties } from 'react';
interface McpStatus {
enabled: boolean;
@@ -64,30 +64,21 @@ export default function ExternalAgentsPanel() {
}, [connectorUrl]);
return (
<div style={{ padding: '32px', color: '#e2e8f0', overflowY: 'auto' }}>
<h2 style={{ fontSize: '20px', fontWeight: 600, marginBottom: '8px' }}>External Agents</h2>
<p style={{ color: '#94a3b8', marginBottom: '24px', lineHeight: 1.5 }}>
<div style={containerStyle}>
<p style={descStyle}>
Connect Claude, ChatGPT, Gemini, or any MCP-compatible assistant to your local RA-H database.
Everything stays on devicetools simply call this connector to add or search nodes.
Everything stays on device. External tools only talk to the local MCP connector you expose here.
</p>
<div
style={{
border: '1px solid #1f2937',
borderRadius: '10px',
padding: '20px',
marginBottom: '24px',
background: '#090909'
}}
>
<div style={cardStyle}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<div style={{ fontSize: '14px', color: '#94a3b8' }}>Connector URL</div>
<div style={{ fontSize: '18px', color: connectorUrl ? '#fff' : '#64748b', marginTop: '4px' }}>
<div style={labelStyle}>Connector URL</div>
<div style={{ ...statusTextStyle, color: connectorUrl ? 'var(--settings-text)' : 'var(--settings-muted)' }}>
{loading ? 'Loading…' : connectorUrl ?? 'Unavailable (MCP server not running)'}
</div>
{status.last_updated && (
<div style={{ fontSize: '12px', color: '#475569', marginTop: '6px' }}>
<div style={microcopyStyle}>
Updated {new Date(status.last_updated).toLocaleTimeString()}
</div>
)}
@@ -97,36 +88,24 @@ export default function ExternalAgentsPanel() {
onClick={handleCopy}
disabled={!connectorUrl}
style={{
background: connectorUrl ? '#22c55e' : '#1f2937',
color: connectorUrl ? '#000' : '#475569',
border: 'none',
borderRadius: '6px',
padding: '10px 16px',
...buttonStyle,
cursor: connectorUrl ? 'pointer' : 'not-allowed',
fontWeight: 600
opacity: connectorUrl ? 1 : 0.4,
}}
>
{copyState === 'copied' ? 'Copied' : 'Copy URL'}
{copyState === 'copied' ? 'Copied' : 'Copy URL'}
</button>
</div>
{status.last_error && (
<div style={{ marginTop: '12px', fontSize: '13px', color: '#fbbf24' }}>
{status.last_error}
<div style={{ ...helperTextStyle, color: 'var(--settings-danger)', marginTop: 12 }}>
{status.last_error}
</div>
)}
</div>
<div
style={{
border: '1px solid #1f2937',
borderRadius: '10px',
padding: '20px',
marginBottom: '24px',
background: '#0c111d'
}}
>
<h3 style={{ marginBottom: '12px', fontSize: '16px', fontWeight: 600 }}>How to use in Claude or ChatGPT</h3>
<ol style={{ paddingLeft: '20px', lineHeight: 1.6, color: '#cbd5f5' }}>
<div style={cardStyle}>
<div style={labelStyle}>How to use in Claude or ChatGPT</div>
<ol style={stepsStyle}>
<li>Open the MCP / connectors settings in your assistant.</li>
<li>Select Add connector choose HTTP paste the URL above.</li>
<li>Give the connector a friendly name (e.g., RA-H).</li>
@@ -134,23 +113,15 @@ export default function ExternalAgentsPanel() {
</ol>
</div>
<div
style={{
border: '1px solid #3f1d1d',
borderRadius: '10px',
padding: '16px',
background: '#170e0e',
color: '#fca5a5',
marginBottom: '24px',
lineHeight: 1.5
}}
>
External agents can edit your local graph. Only enable trusted connectors and monitor their output.
Disconnect the connector or close RA-H if anything unexpected happens.
<div style={cardStyle}>
<div style={helperTextStyle}>
External agents can edit your local graph. Only enable trusted connectors and monitor their output.
Disconnect the connector or close RA-H if anything unexpected happens.
</div>
</div>
{error && (
<div style={{ color: '#f87171', marginBottom: '16px', fontSize: '14px' }}>{error}</div>
<div style={{ ...helperTextStyle, color: 'var(--settings-danger)', marginBottom: 16 }}>{error}</div>
)}
<div style={{ display: 'grid', gap: '16px' }}>
@@ -173,16 +144,92 @@ export default function ExternalAgentsPanel() {
function HelperCard({ title, body }: { title: string; body: string }) {
return (
<div
style={{
border: '1px solid #1f2937',
borderRadius: '8px',
padding: '14px',
background: '#0f172a'
}}
>
<div style={{ fontWeight: 600, marginBottom: '6px', color: '#f1f5f9' }}>{title}</div>
<div style={{ color: '#cbd5f5', fontSize: '14px', lineHeight: 1.5 }}>{body}</div>
<div style={helperCardStyle}>
<div style={helperCardTitleStyle}>{title}</div>
<div style={helperCardBodyStyle}>{body}</div>
</div>
);
}
const containerStyle: CSSProperties = {
padding: 24,
height: '100%',
overflow: 'auto',
};
const descStyle: CSSProperties = {
fontSize: 13,
color: 'var(--settings-muted)',
marginBottom: 20,
lineHeight: 1.5,
};
const cardStyle: CSSProperties = {
background: 'var(--settings-card-bg)',
border: '1px solid var(--settings-border)',
borderRadius: 8,
padding: 16,
marginBottom: 16,
};
const labelStyle: CSSProperties = {
fontSize: 12,
fontWeight: 500,
color: 'var(--settings-text)',
marginBottom: 8,
};
const statusTextStyle: CSSProperties = {
fontSize: 13,
marginTop: 4,
};
const microcopyStyle: CSSProperties = {
fontSize: 11,
color: 'var(--settings-muted)',
marginTop: 6,
};
const buttonStyle: CSSProperties = {
padding: '10px 16px',
background: 'transparent',
color: 'var(--settings-text)',
border: '1px solid var(--settings-border-strong)',
borderRadius: 6,
fontSize: 12,
fontWeight: 500,
};
const stepsStyle: CSSProperties = {
paddingLeft: 20,
lineHeight: 1.6,
color: 'var(--settings-subtext)',
fontSize: 13,
margin: 0,
};
const helperTextStyle: CSSProperties = {
fontSize: 12,
color: 'var(--settings-subtext)',
lineHeight: 1.5,
};
const helperCardStyle: CSSProperties = {
border: '1px solid var(--settings-border)',
borderRadius: 8,
padding: 14,
background: 'var(--settings-card-bg)',
};
const helperCardTitleStyle: CSSProperties = {
fontWeight: 600,
marginBottom: 6,
color: 'var(--settings-text)',
fontSize: 13,
};
const helperCardBodyStyle: CSSProperties = {
color: 'var(--settings-subtext)',
fontSize: 13,
lineHeight: 1.5,
};
+171 -277
View File
@@ -1,22 +1,12 @@
"use client";
import { useEffect, useState } from 'react';
import { useEffect, useState, type CSSProperties } from 'react';
import { createPortal } from 'react-dom';
import LogsViewer from './LogsViewer';
import ToolsViewer from './ToolsViewer';
import ApiKeysViewer from './ApiKeysViewer';
import DatabaseViewer from './DatabaseViewer';
import ExternalAgentsPanel from './ExternalAgentsPanel';
import ContextViewer from './ContextViewer';
import SkillsViewer from './GuidesViewer';
export type SettingsTab =
| 'logs'
| 'tools'
| 'guides'
| 'apikeys'
| 'database'
| 'context'
| 'agents';
export type SettingsTab = 'apikeys' | 'context' | 'agents';
interface SettingsModalProps {
isOpen: boolean;
@@ -24,17 +14,26 @@ interface SettingsModalProps {
initialTab?: SettingsTab;
}
type TabType = SettingsTab;
const DEFAULT_TAB: SettingsTab = 'apikeys';
const TAB_ORDER: SettingsTab[] = ['apikeys', 'context', 'agents'];
const TAB_LABELS: Record<SettingsTab, string> = {
apikeys: 'API Keys',
context: 'Context',
agents: 'Agents',
};
function isSettingsTab(value: unknown): value is SettingsTab {
return typeof value === 'string' && TAB_ORDER.includes(value as SettingsTab);
}
export default function SettingsModal({ isOpen, onClose, initialTab }: SettingsModalProps) {
const [activeTab, setActiveTab] = useState<TabType>('logs');
const [activeTab, setActiveTab] = useState<SettingsTab>(DEFAULT_TAB);
useEffect(() => {
if (!isOpen) return;
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', handleEscape);
@@ -42,271 +41,55 @@ export default function SettingsModal({ isOpen, onClose, initialTab }: SettingsM
}, [isOpen, onClose]);
useEffect(() => {
if (!isOpen || !initialTab) return;
setActiveTab(initialTab);
if (!isOpen) return;
setActiveTab(isSettingsTab(initialTab) ? initialTab : DEFAULT_TAB);
}, [initialTab, isOpen]);
if (!isOpen) return null;
const modalContent = (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.8)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000
}}
onClick={onClose}
>
<div
style={{
width: '80vw',
height: '85vh',
background: 'var(--rah-bg-surface)',
border: '1px solid var(--rah-border-strong)',
borderRadius: '8px',
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.6)',
display: 'flex',
overflow: 'hidden'
}}
onClick={(e) => e.stopPropagation()}
>
{/* Sidebar */}
<div
style={{
width: '20%',
background: 'var(--rah-bg-base)',
borderRight: '1px solid var(--rah-border-strong)',
display: 'flex',
flexDirection: 'column',
padding: '24px 0'
}}
>
<div
style={{
padding: '0 24px',
marginBottom: '24px',
fontSize: '18px',
fontWeight: '600',
color: 'var(--rah-text-active)'
}}
>
Settings
</div>
<nav>
<div
onClick={() => setActiveTab('logs')}
style={{
padding: '12px 24px',
fontSize: '14px',
color: activeTab === 'logs' ? 'var(--rah-text-active)' : 'var(--rah-text-muted)',
background: activeTab === 'logs' ? '#1a3a2a' : 'transparent', // semantic active state kept
borderLeft: activeTab === 'logs' ? '3px solid #22c55e' : '3px solid transparent',
cursor: 'pointer',
transition: 'all 0.2s'
}}
>
Logs
</div>
<div
onClick={() => setActiveTab('tools')}
style={{
padding: '12px 24px',
fontSize: '14px',
color: activeTab === 'tools' ? 'var(--rah-text-active)' : 'var(--rah-text-muted)',
background: activeTab === 'tools' ? '#1a3a2a' : 'transparent',
borderLeft: activeTab === 'tools' ? '3px solid #22c55e' : '3px solid transparent',
cursor: 'pointer',
transition: 'all 0.2s'
}}
>
Tools
</div>
<div
onClick={() => setActiveTab('guides')}
style={{
padding: '12px 24px',
fontSize: '14px',
color: activeTab === 'guides' ? 'var(--rah-text-active)' : 'var(--rah-text-muted)',
background: activeTab === 'guides' ? '#1a3a2a' : 'transparent',
borderLeft: activeTab === 'guides' ? '3px solid #22c55e' : '3px solid transparent',
cursor: 'pointer',
transition: 'all 0.2s'
}}
>
Skills
</div>
<div
onClick={() => setActiveTab('apikeys')}
style={{
padding: '12px 24px',
fontSize: '14px',
color: activeTab === 'apikeys' ? 'var(--rah-text-active)' : 'var(--rah-text-muted)',
background: activeTab === 'apikeys' ? '#1a3a2a' : 'transparent',
borderLeft: activeTab === 'apikeys' ? '3px solid #22c55e' : '3px solid transparent',
cursor: 'pointer',
transition: 'all 0.2s'
}}
>
API Keys
</div>
<div
onClick={() => setActiveTab('database')}
style={{
padding: '12px 24px',
fontSize: '14px',
color: activeTab === 'database' ? 'var(--rah-text-active)' : 'var(--rah-text-muted)',
background: activeTab === 'database' ? '#1a3a2a' : 'transparent',
borderLeft: activeTab === 'database' ? '3px solid #22c55e' : '3px solid transparent',
cursor: 'pointer',
transition: 'all 0.2s'
}}
>
Database
</div>
<div
onClick={() => setActiveTab('context')}
style={{
padding: '12px 24px',
fontSize: '14px',
color: activeTab === 'context' ? 'var(--rah-text-active)' : 'var(--rah-text-muted)',
background: activeTab === 'context' ? '#1a3a2a' : 'transparent',
borderLeft: activeTab === 'context' ? '3px solid #22c55e' : '3px solid transparent',
cursor: 'pointer',
transition: 'all 0.2s'
}}
>
Context
</div>
<div
onClick={() => setActiveTab('agents')}
style={{
padding: '12px 24px',
fontSize: '14px',
color: activeTab === 'agents' ? 'var(--rah-text-active)' : 'var(--rah-text-muted)',
background: activeTab === 'agents' ? '#1a3a2a' : 'transparent',
borderLeft: activeTab === 'agents' ? '3px solid #22c55e' : '3px solid transparent',
cursor: 'pointer',
transition: 'all 0.2s'
}}
>
External Agents
</div>
<div
style={{
padding: '12px 24px',
fontSize: '14px',
color: 'var(--rah-text-muted)',
opacity: 0.4,
cursor: 'not-allowed'
}}
>
Backups
</div>
<div
style={{
padding: '12px 24px',
fontSize: '14px',
color: 'var(--rah-text-muted)',
opacity: 0.4,
cursor: 'not-allowed'
}}
>
Preferences
</div>
return createPortal(
<div style={backdropStyle} onClick={onClose}>
<div style={modalStyle} onClick={(e) => e.stopPropagation()}>
<div style={sidebarStyle}>
<div style={logoStyle}>Settings</div>
<nav style={navStyle}>
{TAB_ORDER.map((tab) => (
<div
key={tab}
onClick={() => setActiveTab(tab)}
style={{
...navItemStyle,
color: activeTab === tab ? 'var(--settings-text)' : 'var(--settings-muted)',
background: activeTab === tab ? 'var(--settings-active-bg)' : 'transparent',
border: activeTab === tab ? '1px solid var(--settings-active-border)' : '1px solid transparent',
}}
>
{TAB_LABELS[tab]}
</div>
))}
</nav>
<div
style={{
marginTop: 'auto',
padding: '24px',
borderTop: '1px solid var(--rah-border)',
display: 'flex',
flexDirection: 'column',
gap: '12px'
}}
>
<div
style={{
fontSize: '11px',
fontWeight: 600,
letterSpacing: '0.08em',
textTransform: 'uppercase',
color: '#64748b'
}}
>
Local Mode
</div>
<p
style={{
fontSize: '13px',
color: '#94a3b8',
margin: 0,
lineHeight: 1.5
}}
>
This open-source build runs entirely on your machine. Add keys via the API Keys tab to unlock every agent.
<div style={footerStyle}>
<div style={footerLabelStyle}>Local Mode</div>
<p style={footerTextStyle}>
This build runs entirely on your machine. Add your API key here to unlock descriptions,
embeddings, and agent-assisted workflows.
</p>
</div>
</div>
{/* Content Area */}
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column'
}}
>
{/* Header */}
<div
style={{
padding: '16px 24px',
borderBottom: '1px solid var(--rah-border-strong)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}
>
<h2
style={{
margin: 0,
fontSize: '16px',
fontWeight: '600',
color: 'var(--rah-text-active)'
}}
>
{activeTab === 'logs' && 'System Logs'}
{activeTab === 'tools' && 'Tools'}
{activeTab === 'guides' && 'Skills'}
{activeTab === 'apikeys' && 'API Keys'}
{activeTab === 'database' && 'Knowledge Database'}
{activeTab === 'context' && 'Auto-Context'}
{activeTab === 'agents' && 'External Agents'}
</h2>
<div style={contentStyle}>
<div style={headerStyle}>
<h2 style={titleStyle}>{TAB_LABELS[activeTab]}</h2>
<button
onClick={onClose}
style={{
background: 'transparent',
border: 'none',
color: 'var(--rah-text-muted)',
cursor: 'pointer',
fontSize: '24px',
lineHeight: 1,
padding: '4px 8px',
transition: 'color 0.2s'
}}
style={closeBtnStyle}
onMouseEnter={(e) => {
e.currentTarget.style.color = 'var(--rah-text-active)';
e.currentTarget.style.color = 'var(--settings-text)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = 'var(--rah-text-muted)';
e.currentTarget.style.color = 'var(--settings-muted)';
}}
title="Close (ESC)"
>
@@ -314,20 +97,131 @@ export default function SettingsModal({ isOpen, onClose, initialTab }: SettingsM
</button>
</div>
{/* Content */}
<div style={{ flex: 1, overflow: 'hidden' }}>
{activeTab === 'logs' && <LogsViewer key={isOpen ? 'open' : 'closed'} />}
{activeTab === 'tools' && <ToolsViewer />}
{activeTab === 'guides' && <SkillsViewer />}
<div style={contentAreaStyle}>
{activeTab === 'apikeys' && <ApiKeysViewer />}
{activeTab === 'database' && <DatabaseViewer />}
{activeTab === 'context' && <ContextViewer />}
{activeTab === 'agents' && <ExternalAgentsPanel />}
</div>
</div>
</div>
</div>
</div>,
document.body
);
return createPortal(modalContent, document.body);
}
const backdropStyle: CSSProperties = {
position: 'fixed',
inset: 0,
background: 'var(--rah-backdrop)',
backdropFilter: 'blur(8px)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
};
const modalStyle: CSSProperties = {
width: '88vw',
height: '90vh',
maxWidth: '1400px',
background: 'var(--settings-bg)',
border: '1px solid var(--settings-border)',
borderRadius: '12px',
boxShadow: 'var(--rah-shadow-modal)',
display: 'flex',
overflow: 'hidden',
};
const sidebarStyle: CSSProperties = {
width: '220px',
background: 'var(--settings-sidebar-bg)',
borderRight: '1px solid var(--settings-border)',
display: 'flex',
flexDirection: 'column',
padding: '20px 0',
};
const logoStyle: CSSProperties = {
padding: '0 20px 20px',
fontSize: '15px',
fontWeight: 600,
color: 'var(--settings-text)',
};
const navStyle: CSSProperties = {
flex: 1,
display: 'flex',
flexDirection: 'column',
gap: '6px',
padding: '0 12px',
};
const navItemStyle: CSSProperties = {
padding: '10px 12px',
fontSize: '13px',
cursor: 'pointer',
transition: 'all 0.15s ease',
borderRadius: '8px',
};
const footerStyle: CSSProperties = {
marginTop: 'auto',
padding: '16px 20px',
borderTop: '1px solid var(--settings-border)',
display: 'flex',
flexDirection: 'column',
gap: '8px',
};
const footerLabelStyle: CSSProperties = {
fontSize: '10px',
fontWeight: 600,
letterSpacing: '0.06em',
textTransform: 'uppercase',
color: 'var(--settings-muted)',
};
const footerTextStyle: CSSProperties = {
fontSize: '12px',
color: 'var(--settings-subtext)',
margin: 0,
lineHeight: 1.5,
};
const contentStyle: CSSProperties = {
flex: 1,
display: 'flex',
flexDirection: 'column',
minWidth: 0,
};
const headerStyle: CSSProperties = {
padding: '16px 24px',
borderBottom: '1px solid var(--settings-border)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
};
const titleStyle: CSSProperties = {
margin: 0,
fontSize: '14px',
fontWeight: 500,
color: 'var(--settings-text)',
};
const closeBtnStyle: CSSProperties = {
background: 'transparent',
border: 'none',
color: 'var(--settings-muted)',
cursor: 'pointer',
fontSize: '24px',
lineHeight: 1,
padding: '4px 8px',
transition: 'color 0.15s ease',
};
const contentAreaStyle: CSSProperties = {
flex: 1,
overflow: 'hidden',
};