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-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-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); --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"] { html[data-theme="light"] {
@@ -97,6 +110,19 @@ html[data-theme="light"] {
--rah-backdrop: rgba(0, 0, 0, 0.5); --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-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); --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 */ /* Modern Clean Design System - RA-H */
@@ -254,4 +280,4 @@ html, body {
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
+25 -21
View File
@@ -37,8 +37,11 @@ export default function ApiKeysViewer() {
<span style={cardTitleStyle}>OpenAI API Key</span> <span style={cardTitleStyle}>OpenAI API Key</span>
<span style={{ <span style={{
fontSize: 12, fontSize: 12,
color: status === 'configured' ? '#22c55e' : color: status === 'checking'
status === 'checking' ? '#6b7280' : '#ef4444' ? 'var(--settings-muted)'
: status === 'configured'
? 'var(--settings-text)'
: 'var(--settings-danger)'
}}> }}>
{status === 'configured' ? 'Configured' : {status === 'configured' ? 'Configured' :
status === 'checking' ? 'Checking...' : 'Not configured'} status === 'checking' ? 'Checking...' : 'Not configured'}
@@ -52,7 +55,7 @@ export default function ApiKeysViewer() {
<div style={codeBlockStyle}> <div style={codeBlockStyle}>
<code>OPENAI_API_KEY=sk-your-key-here</code> <code>OPENAI_API_KEY=sk-your-key-here</code>
</div> </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. Restart the app after changing the key.
</p> </p>
</div> </div>
@@ -84,8 +87,8 @@ const containerStyle: CSSProperties = {
}; };
const featuresBoxStyle: CSSProperties = { const featuresBoxStyle: CSSProperties = {
background: 'rgba(34, 197, 94, 0.08)', background: 'var(--settings-card-bg)',
border: '1px solid rgba(34, 197, 94, 0.2)', border: '1px solid var(--settings-border)',
borderRadius: 8, borderRadius: 8,
padding: 16, padding: 16,
marginBottom: 20, marginBottom: 20,
@@ -94,7 +97,7 @@ const featuresBoxStyle: CSSProperties = {
const featuresHeaderStyle: CSSProperties = { const featuresHeaderStyle: CSSProperties = {
fontSize: 13, fontSize: 13,
fontWeight: 500, fontWeight: 500,
color: '#22c55e', color: 'var(--settings-text)',
marginBottom: 8, marginBottom: 8,
}; };
@@ -102,20 +105,20 @@ const featuresListStyle: CSSProperties = {
margin: 0, margin: 0,
paddingLeft: 20, paddingLeft: 20,
fontSize: 13, fontSize: 13,
color: '#d1d5db', color: 'var(--settings-subtext)',
lineHeight: 1.6, lineHeight: 1.6,
}; };
const noteStyle: CSSProperties = { const noteStyle: CSSProperties = {
marginTop: 12, marginTop: 12,
fontSize: 12, fontSize: 12,
color: '#6b7280', color: 'var(--settings-muted)',
fontStyle: 'italic', fontStyle: 'italic',
}; };
const cardStyle: CSSProperties = { const cardStyle: CSSProperties = {
background: 'rgba(255, 255, 255, 0.02)', background: 'var(--settings-card-bg)',
border: '1px solid rgba(255, 255, 255, 0.06)', border: '1px solid var(--settings-border)',
borderRadius: 8, borderRadius: 8,
padding: 16, padding: 16,
marginBottom: 12, marginBottom: 12,
@@ -131,46 +134,47 @@ const cardHeaderStyle: CSSProperties = {
const cardTitleStyle: CSSProperties = { const cardTitleStyle: CSSProperties = {
fontSize: 13, fontSize: 13,
fontWeight: 500, fontWeight: 500,
color: '#e5e7eb', color: 'var(--settings-text)',
}; };
const instructionsStyle: CSSProperties = { const instructionsStyle: CSSProperties = {
fontSize: 13, fontSize: 13,
color: '#d1d5db', color: 'var(--settings-subtext)',
lineHeight: 1.5, lineHeight: 1.5,
}; };
const codeInlineStyle: CSSProperties = { const codeInlineStyle: CSSProperties = {
background: 'rgba(255, 255, 255, 0.08)', background: 'var(--settings-code-bg)',
padding: '2px 6px', padding: '2px 6px',
borderRadius: 4, borderRadius: 4,
fontSize: 12, fontSize: 12,
fontFamily: 'monospace', fontFamily: 'monospace',
color: '#22c55e', color: 'var(--settings-text)',
}; };
const codeBlockStyle: CSSProperties = { const codeBlockStyle: CSSProperties = {
background: 'rgba(0, 0, 0, 0.4)', background: 'var(--settings-code-bg)',
border: '1px solid rgba(255, 255, 255, 0.08)', border: '1px solid var(--settings-border)',
borderRadius: 6, borderRadius: 6,
padding: '10px 12px', padding: '10px 12px',
fontSize: 13, fontSize: 13,
fontFamily: 'monospace', fontFamily: 'monospace',
color: '#e5e7eb', color: 'var(--settings-text)',
marginBottom: 8, marginBottom: 8,
}; };
const helpStyle: CSSProperties = { const helpStyle: CSSProperties = {
fontSize: 12, fontSize: 12,
color: '#6b7280', color: 'var(--settings-muted)',
}; };
const linkStyle: CSSProperties = { const linkStyle: CSSProperties = {
color: '#22c55e', color: 'var(--settings-text)',
textDecoration: 'none', textDecoration: 'none',
background: 'transparent', background: 'transparent',
border: 'none', border: '1px solid var(--settings-border-strong)',
padding: 0, borderRadius: 6,
padding: '10px 14px',
cursor: 'pointer', cursor: 'pointer',
font: 'inherit', font: 'inherit',
}; };
+16 -15
View File
@@ -90,7 +90,7 @@ export default function ContextViewer() {
disabled={loadingSettings || saving} disabled={loadingSettings || saving}
style={{ style={{
...toggleStyle, ...toggleStyle,
background: enabled ? '#22c55e' : 'rgba(255, 255, 255, 0.1)', background: enabled ? 'var(--settings-active-bg)' : 'var(--settings-code-bg)',
}} }}
> >
<span style={{ <span style={{
@@ -131,25 +131,25 @@ export default function ContextViewer() {
} }
const containerStyle: CSSProperties = { padding: 24, height: '100%', overflow: 'auto' }; 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 = { const cardStyle: CSSProperties = {
background: 'rgba(255, 255, 255, 0.02)', background: 'var(--settings-card-bg)',
border: '1px solid rgba(255, 255, 255, 0.06)', border: '1px solid var(--settings-border)',
borderRadius: 8, borderRadius: 8,
padding: 16, padding: 16,
marginBottom: 24, marginBottom: 24,
}; };
const labelStyle: CSSProperties = { fontSize: 13, fontWeight: 500, color: '#e5e7eb', marginBottom: 8 }; const labelStyle: CSSProperties = { fontSize: 13, fontWeight: 500, color: 'var(--settings-text)', marginBottom: 8 };
const subLabelStyle: CSSProperties = { fontSize: 12, color: '#6b7280' }; const subLabelStyle: CSSProperties = { fontSize: 12, color: 'var(--settings-muted)' };
const mutedStyle: CSSProperties = { fontSize: 13, color: '#6b7280' }; const mutedStyle: CSSProperties = { fontSize: 13, color: 'var(--settings-muted)' };
const toggleStyle: CSSProperties = { const toggleStyle: CSSProperties = {
width: 48, width: 48,
height: 26, height: 26,
borderRadius: 13, borderRadius: 13,
border: 'none', border: '1px solid var(--settings-border)',
cursor: 'pointer', cursor: 'pointer',
position: 'relative', position: 'relative',
transition: 'background 0.15s', transition: 'background 0.15s',
@@ -161,24 +161,25 @@ const toggleKnobStyle: CSSProperties = {
width: 18, width: 18,
height: 18, height: 18,
borderRadius: '50%', borderRadius: '50%',
background: '#fff', background: 'var(--settings-text)',
transition: 'left 0.15s', transition: 'left 0.15s',
}; };
const nodeCardStyle: CSSProperties = { const nodeCardStyle: CSSProperties = {
padding: '12px 14px', padding: '12px 14px',
background: 'rgba(255, 255, 255, 0.02)', background: 'var(--settings-card-bg)',
border: '1px solid rgba(255, 255, 255, 0.06)', border: '1px solid var(--settings-border)',
borderRadius: 6, borderRadius: 6,
}; };
const nodeTitleStyle: CSSProperties = { fontSize: 13, fontWeight: 500, color: '#e5e7eb' }; const nodeTitleStyle: CSSProperties = { fontSize: 13, fontWeight: 500, color: 'var(--settings-text)' };
const edgeCountStyle: CSSProperties = { fontSize: 12, color: '#22c55e' }; const edgeCountStyle: CSSProperties = { fontSize: 12, color: 'var(--settings-muted)' };
const dimTagStyle: CSSProperties = { const dimTagStyle: CSSProperties = {
padding: '2px 8px', padding: '2px 8px',
borderRadius: 4, borderRadius: 4,
fontSize: 11, fontSize: 11,
background: 'rgba(34, 197, 94, 0.1)', background: 'var(--settings-chip-bg)',
color: '#22c55e', color: 'var(--settings-subtext)',
border: '1px solid var(--settings-border)',
}; };
+108 -61
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState, type CSSProperties } from 'react';
interface McpStatus { interface McpStatus {
enabled: boolean; enabled: boolean;
@@ -64,30 +64,21 @@ export default function ExternalAgentsPanel() {
}, [connectorUrl]); }, [connectorUrl]);
return ( return (
<div style={{ padding: '32px', color: '#e2e8f0', overflowY: 'auto' }}> <div style={containerStyle}>
<h2 style={{ fontSize: '20px', fontWeight: 600, marginBottom: '8px' }}>External Agents</h2> <p style={descStyle}>
<p style={{ color: '#94a3b8', marginBottom: '24px', lineHeight: 1.5 }}>
Connect Claude, ChatGPT, Gemini, or any MCP-compatible assistant to your local RA-H database. 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> </p>
<div <div style={cardStyle}>
style={{
border: '1px solid #1f2937',
borderRadius: '10px',
padding: '20px',
marginBottom: '24px',
background: '#090909'
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div> <div>
<div style={{ fontSize: '14px', color: '#94a3b8' }}>Connector URL</div> <div style={labelStyle}>Connector URL</div>
<div style={{ fontSize: '18px', color: connectorUrl ? '#fff' : '#64748b', marginTop: '4px' }}> <div style={{ ...statusTextStyle, color: connectorUrl ? 'var(--settings-text)' : 'var(--settings-muted)' }}>
{loading ? 'Loading…' : connectorUrl ?? 'Unavailable (MCP server not running)'} {loading ? 'Loading…' : connectorUrl ?? 'Unavailable (MCP server not running)'}
</div> </div>
{status.last_updated && ( {status.last_updated && (
<div style={{ fontSize: '12px', color: '#475569', marginTop: '6px' }}> <div style={microcopyStyle}>
Updated {new Date(status.last_updated).toLocaleTimeString()} Updated {new Date(status.last_updated).toLocaleTimeString()}
</div> </div>
)} )}
@@ -97,36 +88,24 @@ export default function ExternalAgentsPanel() {
onClick={handleCopy} onClick={handleCopy}
disabled={!connectorUrl} disabled={!connectorUrl}
style={{ style={{
background: connectorUrl ? '#22c55e' : '#1f2937', ...buttonStyle,
color: connectorUrl ? '#000' : '#475569',
border: 'none',
borderRadius: '6px',
padding: '10px 16px',
cursor: connectorUrl ? 'pointer' : 'not-allowed', cursor: connectorUrl ? 'pointer' : 'not-allowed',
fontWeight: 600 opacity: connectorUrl ? 1 : 0.4,
}} }}
> >
{copyState === 'copied' ? 'Copied' : 'Copy URL'} {copyState === 'copied' ? 'Copied' : 'Copy URL'}
</button> </button>
</div> </div>
{status.last_error && ( {status.last_error && (
<div style={{ marginTop: '12px', fontSize: '13px', color: '#fbbf24' }}> <div style={{ ...helperTextStyle, color: 'var(--settings-danger)', marginTop: 12 }}>
{status.last_error} {status.last_error}
</div> </div>
)} )}
</div> </div>
<div <div style={cardStyle}>
style={{ <div style={labelStyle}>How to use in Claude or ChatGPT</div>
border: '1px solid #1f2937', <ol style={stepsStyle}>
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' }}>
<li>Open the MCP / connectors settings in your assistant.</li> <li>Open the MCP / connectors settings in your assistant.</li>
<li>Select Add connector choose HTTP paste the URL above.</li> <li>Select Add connector choose HTTP paste the URL above.</li>
<li>Give the connector a friendly name (e.g., RA-H).</li> <li>Give the connector a friendly name (e.g., RA-H).</li>
@@ -134,23 +113,15 @@ export default function ExternalAgentsPanel() {
</ol> </ol>
</div> </div>
<div <div style={cardStyle}>
style={{ <div style={helperTextStyle}>
border: '1px solid #3f1d1d', External agents can edit your local graph. Only enable trusted connectors and monitor their output.
borderRadius: '10px', Disconnect the connector or close RA-H if anything unexpected happens.
padding: '16px', </div>
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> </div>
{error && ( {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' }}> <div style={{ display: 'grid', gap: '16px' }}>
@@ -173,16 +144,92 @@ export default function ExternalAgentsPanel() {
function HelperCard({ title, body }: { title: string; body: string }) { function HelperCard({ title, body }: { title: string; body: string }) {
return ( return (
<div <div style={helperCardStyle}>
style={{ <div style={helperCardTitleStyle}>{title}</div>
border: '1px solid #1f2937', <div style={helperCardBodyStyle}>{body}</div>
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> </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"; "use client";
import { useEffect, useState } from 'react'; import { useEffect, useState, type CSSProperties } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import LogsViewer from './LogsViewer';
import ToolsViewer from './ToolsViewer';
import ApiKeysViewer from './ApiKeysViewer'; import ApiKeysViewer from './ApiKeysViewer';
import DatabaseViewer from './DatabaseViewer';
import ExternalAgentsPanel from './ExternalAgentsPanel'; import ExternalAgentsPanel from './ExternalAgentsPanel';
import ContextViewer from './ContextViewer'; import ContextViewer from './ContextViewer';
import SkillsViewer from './GuidesViewer';
export type SettingsTab = export type SettingsTab = 'apikeys' | 'context' | 'agents';
| 'logs'
| 'tools'
| 'guides'
| 'apikeys'
| 'database'
| 'context'
| 'agents';
interface SettingsModalProps { interface SettingsModalProps {
isOpen: boolean; isOpen: boolean;
@@ -24,17 +14,26 @@ interface SettingsModalProps {
initialTab?: SettingsTab; 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) { export default function SettingsModal({ isOpen, onClose, initialTab }: SettingsModalProps) {
const [activeTab, setActiveTab] = useState<TabType>('logs'); const [activeTab, setActiveTab] = useState<SettingsTab>(DEFAULT_TAB);
useEffect(() => { useEffect(() => {
if (!isOpen) return; if (!isOpen) return;
const handleEscape = (e: KeyboardEvent) => { const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') { if (e.key === 'Escape') onClose();
onClose();
}
}; };
window.addEventListener('keydown', handleEscape); window.addEventListener('keydown', handleEscape);
@@ -42,271 +41,55 @@ export default function SettingsModal({ isOpen, onClose, initialTab }: SettingsM
}, [isOpen, onClose]); }, [isOpen, onClose]);
useEffect(() => { useEffect(() => {
if (!isOpen || !initialTab) return; if (!isOpen) return;
setActiveTab(initialTab); setActiveTab(isSettingsTab(initialTab) ? initialTab : DEFAULT_TAB);
}, [initialTab, isOpen]); }, [initialTab, isOpen]);
if (!isOpen) return null; if (!isOpen) return null;
const modalContent = ( return createPortal(
<div <div style={backdropStyle} onClick={onClose}>
style={{ <div style={modalStyle} onClick={(e) => e.stopPropagation()}>
position: 'fixed', <div style={sidebarStyle}>
top: 0, <div style={logoStyle}>Settings</div>
left: 0,
right: 0, <nav style={navStyle}>
bottom: 0, {TAB_ORDER.map((tab) => (
background: 'rgba(0, 0, 0, 0.8)', <div
display: 'flex', key={tab}
alignItems: 'center', onClick={() => setActiveTab(tab)}
justifyContent: 'center', style={{
zIndex: 1000 ...navItemStyle,
}} color: activeTab === tab ? 'var(--settings-text)' : 'var(--settings-muted)',
onClick={onClose} background: activeTab === tab ? 'var(--settings-active-bg)' : 'transparent',
> border: activeTab === tab ? '1px solid var(--settings-active-border)' : '1px solid transparent',
<div }}
style={{ >
width: '80vw', {TAB_LABELS[tab]}
height: '85vh', </div>
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>
</nav> </nav>
<div <div style={footerStyle}>
style={{ <div style={footerLabelStyle}>Local Mode</div>
marginTop: 'auto', <p style={footerTextStyle}>
padding: '24px', This build runs entirely on your machine. Add your API key here to unlock descriptions,
borderTop: '1px solid var(--rah-border)', embeddings, and agent-assisted workflows.
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.
</p> </p>
</div> </div>
</div> </div>
{/* Content Area */} <div style={contentStyle}>
<div <div style={headerStyle}>
style={{ <h2 style={titleStyle}>{TAB_LABELS[activeTab]}</h2>
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>
<button <button
onClick={onClose} onClick={onClose}
style={{ style={closeBtnStyle}
background: 'transparent',
border: 'none',
color: 'var(--rah-text-muted)',
cursor: 'pointer',
fontSize: '24px',
lineHeight: 1,
padding: '4px 8px',
transition: 'color 0.2s'
}}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.color = 'var(--rah-text-active)'; e.currentTarget.style.color = 'var(--settings-text)';
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.color = 'var(--rah-text-muted)'; e.currentTarget.style.color = 'var(--settings-muted)';
}} }}
title="Close (ESC)" title="Close (ESC)"
> >
@@ -314,20 +97,131 @@ export default function SettingsModal({ isOpen, onClose, initialTab }: SettingsM
</button> </button>
</div> </div>
{/* Content */} <div style={contentAreaStyle}>
<div style={{ flex: 1, overflow: 'hidden' }}>
{activeTab === 'logs' && <LogsViewer key={isOpen ? 'open' : 'closed'} />}
{activeTab === 'tools' && <ToolsViewer />}
{activeTab === 'guides' && <SkillsViewer />}
{activeTab === 'apikeys' && <ApiKeysViewer />} {activeTab === 'apikeys' && <ApiKeysViewer />}
{activeTab === 'database' && <DatabaseViewer />}
{activeTab === 'context' && <ContextViewer />} {activeTab === 'context' && <ContextViewer />}
{activeTab === 'agents' && <ExternalAgentsPanel />} {activeTab === 'agents' && <ExternalAgentsPanel />}
</div> </div>
</div> </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',
};