system prompt client global and project

This commit is contained in:
Storme-bit
2026-04-19 02:57:11 -07:00
parent a0154e15e6
commit fa3b0859f0
4 changed files with 144 additions and 44 deletions

View File

@@ -1,10 +1,10 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useSettings } from '../hooks/useSettings';
import { useModels } from '../hooks/useModels';
import { getServiceHealth } from '../api/orchestration'; // ← merged
import { getServiceHealth } from '../api/orchestration';
export default function SettingsView({ onNavigate, onBack, modelProps }) {
const { settings, saveSetting, saving } = useSettings(); // ← single source
const { settings, saveSetting, saving } = useSettings();
return (
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, overflow: 'hidden', background: 'var(--bg-base)' }}>
@@ -51,12 +51,16 @@ export default function SettingsView({ onNavigate, onBack, modelProps }) {
</SettingsSection>
<SettingsSection title="Models">
{/* ← Error boundary wraps ModelsSection only */}
<SettingsSectionErrorBoundary>
<ModelsSection settings={settings} saveSetting={saveSetting} saving={saving} modelProps={modelProps}/>
<ModelsSection settings={settings} saveSetting={saveSetting} saving={saving} modelProps={modelProps} />
</SettingsSectionErrorBoundary>
</SettingsSection>
{/* Global system prompt */}
<SettingsSection title="Behaviour">
<SystemPromptSetting settings={settings} saveSetting={saveSetting} saving={saving} />
</SettingsSection>
<SettingsSection title="About">
<SettingsRow
label="Service Health"
@@ -79,7 +83,8 @@ export default function SettingsView({ onNavigate, onBack, modelProps }) {
);
}
// Error boundary — class component required by React for this pattern
// ── Error boundary ───────────────────────────────────────────
class SettingsSectionErrorBoundary extends React.Component {
constructor(props) {
super(props);
@@ -107,6 +112,8 @@ class SettingsSectionErrorBoundary extends React.Component {
}
}
// ── Layout components ────────────────────────────────────────
function SettingsSection({ title, children }) {
return (
<div style={{ marginBottom: '32px' }}>
@@ -125,11 +132,28 @@ function SettingsSection({ title, children }) {
);
}
function SettingsRow({ label, description, action }) {
return (
<div style={{
display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between',
padding: '14px 16px',
borderBottom: '1px solid var(--border)',
}}>
<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 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]);
@@ -168,30 +192,78 @@ function NumberSetting({ label, description, value, min, max, step = 1, onSave,
);
}
function SettingsRow({ label, description, action }) {
function ComingSoon() {
return <span className="text-xs text-muted" style={{ fontStyle: 'italic' }}>Coming soon</span>;
}
// ── System prompt setting ────────────────────────────────────
function SystemPromptSetting({ settings, saveSetting, saving }) {
const [local, setLocal] = useState(settings?.systemPrompt ?? '');
const [savedPrompt, setSavedPrompt] = useState(settings?.systemPrompt ?? '');
useEffect(() => {
if (settings?.systemPrompt !== undefined) {
setLocal(settings.systemPrompt ?? '');
setSavedPrompt(settings.systemPrompt ?? '');
}
}, [settings?.systemPrompt]);
const isDirty = local !== savedPrompt;
async function handleSave() {
await saveSetting('systemPrompt', local.trim() || null);
setSavedPrompt(local);
}
return (
<div style={{
display: 'flex', alignItems: 'flex-start', 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 style={{ padding: '14px 16px', borderBottom: '1px solid var(--border)' }}>
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: '8px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<span className="text-sm" style={{ color: 'var(--text-primary)', fontWeight: 500 }}>
System Prompt
</span>
<span className="text-xs text-muted">
Default instruction given to the model on every request. Projects can override this.
</span>
</div>
{isDirty && (
<button
className="btn-primary"
style={{ padding: '5px 12px', fontSize: '12px', flexShrink: 0, marginLeft: '16px' }}
disabled={saving}
onClick={handleSave}
>
{saving ? 'Saving…' : 'Save'}
</button>
)}
</div>
<textarea
value={local}
onChange={e => setLocal(e.target.value)}
rows={5}
style={{
width: '100%',
background: 'var(--bg-elevated)',
border: '1px solid var(--border)',
borderRadius: 'var(--radius-md)',
padding: '10px 12px',
color: 'var(--text-primary)',
fontSize: '13px', lineHeight: '1.6',
resize: 'vertical', fontFamily: 'inherit',
outline: 'none', boxSizing: 'border-box',
}}
onFocus={e => e.target.style.borderColor = 'var(--accent)'}
onBlur={e => e.target.style.borderColor = 'var(--border)'}
/>
{!isDirty && local && (
<p className="text-xs text-muted" style={{ marginTop: '6px' }}>Saved</p>
)}
</div>
);
}
function ComingSoon() {
return <span className="text-xs text-muted" style={{ fontStyle: 'italic' }}>Coming soon</span>;
}
// ── Service health ───────────────────────────────────────────
function ServiceHealth() {
const [services, setServices] = useState(null);
@@ -242,25 +314,18 @@ function ServiceHealth() {
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>
@@ -272,11 +337,12 @@ function ServiceHealth() {
);
}
// ── Models section ───────────────────────────────────────────
function ModelsSection({ settings, saveSetting, saving, modelProps }) {
const { models, selectedModel, setSelectedModel } = useModels();
const [selectedInfo, setSelectedInfo] = useState(null);
// Sync info panel when selection changes
useEffect(() => {
const m = models.find(m => m.value === selectedModel);
setSelectedInfo(m ?? null);
@@ -287,7 +353,7 @@ function ModelsSection({ settings, saveSetting, saving, modelProps }) {
<SettingsRow
label="Models Folder"
description="Path to folder containing .gguf files"
action={<ModelsFolderSetting settings={settings} saveSetting={saveSetting} saving={saving}/>}
action={<ModelsFolderSetting settings={settings} saveSetting={saveSetting} saving={saving} />}
/>
<NumberSetting
label="Temperature"
@@ -297,7 +363,6 @@ function ModelsSection({ settings, saveSetting, saving, modelProps }) {
onSave={val => saveSetting('temperature', val)}
saving={saving}
/>
<NumberSetting
label="Repeat Penalty"
description="Penalises repeated tokens — higher reduces repetition (12)"
@@ -343,7 +408,6 @@ function ModelsSection({ settings, saveSetting, saving, modelProps }) {
}
/>
{/* Model info panel */}
{selectedInfo && (
<div style={{
margin: '0', padding: '14px 16px',
@@ -360,7 +424,9 @@ function ModelsSection({ settings, saveSetting, saving, modelProps }) {
)}
<InfoLine
label="Context"
value={modelProps ? `${modelProps.contextWindow.toLocaleString()} tokens` : '—'}
value={modelProps?.contextWindow
? `${modelProps.contextWindow.toLocaleString()} tokens`
: '—'}
/>
<InfoLine
label="Loaded"