feat: simplify settings surface\n\n- keep only API keys, context, and agents in settings\n- restyle the retained settings panels for light and dark themes\n- preserve the API keys workflow in the open-source build
This commit is contained in:
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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)',
|
||||
};
|
||||
|
||||
@@ -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 device—tools 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,
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user