system prompt client global and project
This commit is contained in:
@@ -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 (1–2)"
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user