Files
nexusAI/packages/chat-client/src/components/SettingsView.jsx
2026-04-17 23:35:31 -07:00

240 lines
8.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useCallback } from 'react';
import { useSettings } from '../hooks/useSettings';
import { getServiceHealth } from '../api/orchestration';
export default function SettingsView({ onNavigate }) {
const { settings, saveSetting, saving } = useSettings();
return (
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, overflow: 'hidden', background: 'var(--bg-base)' }}>
<div className="panel-header" style={{ padding: '0 24px' }}>
<span className="text-base" style={{ fontWeight: 500, color: 'var(--text-secondary)' }}>Settings</span>
</div>
<div className="flex-1 scroll-y" style={{ padding: '24px' }}>
<SettingsSection title="Memory">
<SettingsRow
label="Memory Viewer"
description="Browse, search, and delete stored episodes"
action={<button className="btn-primary" style={{ padding: '6px 14px', fontSize: '13px' }}
onClick={() => onNavigate('memory')}>Open </button>}
/>
<NumberSetting
label="Recent Episode Limit"
description="Recent episodes injected into each prompt"
value={settings?.recentEpisodeLimit}
min={1} max={20}
onSave={val => saveSetting('recentEpisodeLimit', val)}
saving={saving}
/>
<NumberSetting
label="Semantic Search Limit"
description="Max episodes retrieved via vector search per query"
value={settings?.semanticLimit}
min={1} max={20}
onSave={val => saveSetting('semanticLimit', val)}
saving={saving}
/>
<NumberSetting
label="Score Threshold"
description="Minimum similarity score for semantic results (01)"
value={settings?.scoreThreshold}
min={0} max={1} step={0.05}
onSave={val => saveSetting('scoreThreshold', val)}
saving={saving}
/>
</SettingsSection>
<SettingsSection title="Models">
<SettingsRow label="Active Model" description="Model used for inference" action={<ComingSoon />} />
<SettingsRow label="Temperature" description="Response creativity / randomness" action={<ComingSoon />} />
<SettingsRow label="Context Window" description="Max tokens per request (-c flag)" action={<ComingSoon />} />
</SettingsSection>
<SettingsSection title="About">
<SettingsRow
label="Service Health"
description="Ping all four services"
action={<ServiceHealth />}
/>
<SettingsRow
label="Version"
description="NexusAI"
action={<span className="text-sm text-muted">v0.1.0</span>}
/>
</SettingsSection>
<SettingsSection title="Appearance">
<SettingsRow label="Theme" description="UI colour scheme" action={<ComingSoon />} />
</SettingsSection>
</div>
</div>
);
}
function SettingsSection({ title, children }) {
return (
<div style={{ marginBottom: '32px' }}>
<p className="label-upper" style={{ marginBottom: '12px', color: 'var(--text-secondary)' }}>
{title}
</p>
<div style={{
background: 'var(--bg-surface)',
border: '1px solid var(--border)',
borderRadius: 'var(--radius-lg)',
overflow: 'hidden',
}}>
{children}
</div>
</div>
);
}
function NumberSetting({ label, description, value, min, max, step = 1, onSave, saving }) {
const [local, setLocal] = useState(value ?? '');
const isDirty = local !== '' && Number(local) !== value;
// Sync when settings load from API
useEffect(() => {
if (value !== undefined) setLocal(value);
}, [value]);
return (
<SettingsRow
label={label}
description={description}
action={
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<input
type="number"
value={local}
min={min} max={max} step={step}
onChange={e => setLocal(e.target.value)}
style={{
width: '64px', padding: '5px 8px', textAlign: 'center',
background: 'var(--bg-elevated)', border: '1px solid var(--border)',
borderRadius: 'var(--radius-md)', color: 'var(--text-primary)',
fontSize: '13px', outline: 'none',
}}
/>
{isDirty && (
<button
className="btn-primary"
style={{ padding: '5px 10px', fontSize: '12px' }}
disabled={saving}
onClick={() => onSave(Number(local))}
>
Save
</button>
)}
</div>
}
/>
);
}
function SettingsRow({ label, description, action }) {
return (
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '14px 16px',
borderBottom: '1px solid var(--border)',
}}
// Remove bottom border on last child via CSS would need a class;
// easiest to just let it render — the section border-radius clips it cleanly
>
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<span className="text-sm" style={{ color: 'var(--text-primary)', fontWeight: 500 }}>{label}</span>
{description && <span className="text-xs text-muted">{description}</span>}
</div>
<div style={{ flexShrink: 0, marginLeft: 16 }}>
{action}
</div>
</div>
);
}
function ComingSoon() {
return <span className="text-xs text-muted" style={{ fontStyle: 'italic' }}>Coming soon</span>;
}
function ServiceHealth() {
const [services, setServices] = useState(null);
const [loading, setLoading] = useState(false);
const [lastChecked, setLastChecked] = useState(null);
const check = useCallback(async () => {
setLoading(true);
try {
setServices(await getServiceHealth());
setLastChecked(new Date());
} catch (err) {
console.error('[ServiceHealth]', err.message);
} finally {
setLoading(false);
}
}, []);
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<button
className="btn-primary"
style={{ padding: '5px 12px', fontSize: '12px' }}
disabled={loading}
onClick={check}
>
{loading ? 'Checking…' : 'Check Now'}
</button>
{lastChecked && (
<span className="text-xs text-muted">
{lastChecked.toLocaleTimeString()}
</span>
)}
</div>
{services && (
<div style={{
display: 'flex', flexDirection: 'column',
border: '1px solid var(--border)',
borderRadius: 'var(--radius-md)',
overflow: 'hidden', marginTop: 4,
}}>
{services.map((svc, i) => (
<div key={svc.key} style={{
display: 'flex', alignItems: 'center', gap: 10,
padding: '8px 12px',
borderBottom: i < services.length - 1 ? '1px solid var(--border)' : 'none',
background: 'var(--bg-elevated)',
}}>
{/* Status dot */}
<div style={{
width: 8, height: 8, borderRadius: '50%', flexShrink: 0,
background: svc.status === 'healthy' ? '#2ecc71' : '#e74c3c',
}} />
{/* Label */}
<span className="text-sm" style={{ minWidth: 90, color: 'var(--text-primary)' }}>
{svc.label}
</span>
{/* Detail — show model for inference, nothing extra for others */}
<span className="text-xs text-muted" style={{ flex: 1 }}>
{svc.key === 'inference' && svc.detail?.model
? svc.detail.model
: svc.status === 'unreachable' ? 'Unreachable' : ''}
</span>
{/* Latency */}
<span className="text-xs text-muted" style={{ flexShrink: 0 }}>
{svc.latency}ms
</span>
</div>
))}
</div>
)}
</div>
);
}