system prompt client global and project
This commit is contained in:
@@ -6,6 +6,7 @@ export default function ProjectModal({ project, mode, onSave, onDelete, onClose
|
||||
const [name, setName] = useState(project?.name ?? '');
|
||||
const [description, setDescription] = useState(project?.description ?? '');
|
||||
const [colour, setColour] = useState(project?.colour ?? COLOURS[0]);
|
||||
const [systemPrompt, setSystemPrompt] = useState(project?.system_prompt ?? '');
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -15,13 +16,20 @@ export default function ProjectModal({ project, mode, onSave, onDelete, onClose
|
||||
function handleSubmit() {
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) return;
|
||||
onSave({ name: trimmed, description: description.trim() || null, colour, icon: null, isolated: 1 });
|
||||
onSave({
|
||||
name: trimmed,
|
||||
description: description.trim() || null,
|
||||
colour,
|
||||
icon: null,
|
||||
isolated: 1,
|
||||
system_prompt: systemPrompt.trim() || null,
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
|
||||
function handleKeyDown(e) {
|
||||
if (e.key === 'Enter' && mode !== 'confirm-delete') handleSubmit();
|
||||
if (e.key === 'Escape') onClose();
|
||||
// Don't submit on Enter — textarea fields make Enter ambiguous
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -35,7 +43,8 @@ export default function ProjectModal({ project, mode, onSave, onDelete, onClose
|
||||
background: 'var(--bg-surface)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--radius-lg)',
|
||||
padding: '24px', width: '380px',
|
||||
padding: '24px', width: '420px',
|
||||
maxHeight: '90vh', overflowY: 'auto',
|
||||
display: 'flex', flexDirection: 'column', gap: '16px',
|
||||
}}>
|
||||
{mode === 'confirm-delete' ? (
|
||||
@@ -122,6 +131,32 @@ export default function ProjectModal({ project, mode, onSave, onDelete, onClose
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System Prompt */}
|
||||
<div className="flex-col" style={{ gap: '6px' }}>
|
||||
<label className="label-upper">
|
||||
System Prompt <span style={{ opacity: 0.5 }}>(optional)</span>
|
||||
</label>
|
||||
<p className="text-xs text-muted" style={{ marginTop: '-2px' }}>
|
||||
Overrides the global system prompt for conversations in this project.
|
||||
Leave blank to use the global default.
|
||||
</p>
|
||||
<textarea
|
||||
value={systemPrompt}
|
||||
onChange={e => setSystemPrompt(e.target.value)}
|
||||
placeholder="You are a helpful assistant specialised in..."
|
||||
rows={4}
|
||||
style={{
|
||||
background: 'var(--bg-elevated)', border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--radius-md)', padding: '8px 12px',
|
||||
color: 'var(--text-primary)', fontSize: '13px', outline: 'none',
|
||||
width: '100%', resize: 'vertical', fontFamily: 'inherit',
|
||||
lineHeight: '1.6',
|
||||
}}
|
||||
onFocus={e => e.target.style.borderColor = 'var(--accent)'}
|
||||
onBlur={e => e.target.style.borderColor = 'var(--border)'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex" style={{ gap: '8px', justifyContent: 'flex-end' }}>
|
||||
<button className="btn-reset text-base text-muted"
|
||||
onClick={onClose}
|
||||
|
||||
@@ -36,9 +36,9 @@ export default function ProjectView({ project, onNavigate, onBack, onSelectSessi
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave({ name, description, colour, icon, isolated }) {
|
||||
async function handleSave({ name, description, colour, icon, isolated, system_prompt }) {
|
||||
try {
|
||||
await updateProject(project.id, { name, description, colour, icon, isolated });
|
||||
await updateProject(project.id, { name, description, colour, icon, isolated, system_prompt });
|
||||
onProjectsChange?.();
|
||||
setModal(null);
|
||||
} catch (err) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -104,7 +104,6 @@ export default function Sidebar({
|
||||
}
|
||||
|
||||
const sessionRowProps = (session) => ({
|
||||
key: session.external_id,
|
||||
session,
|
||||
isActive: activeSession?.external_id === session.external_id,
|
||||
isHovered: hoveredId === session.external_id,
|
||||
@@ -236,7 +235,7 @@ export default function Sidebar({
|
||||
</span>
|
||||
</div>
|
||||
{projectSessions.map(session => (
|
||||
<SessionRow {...sessionRowProps(session)} />
|
||||
<SessionRow key={session.external_id} {...sessionRowProps(session)} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -251,7 +250,7 @@ export default function Sidebar({
|
||||
</div>
|
||||
)}
|
||||
{unassigned.map(session => (
|
||||
<SessionRow {...sessionRowProps(session)} />
|
||||
<SessionRow key={session.external_id} {...sessionRowProps(session)} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user