Add reset-to-default button and how-to docs to system prompt editor

Each service prompt card now shows:
- A collapsible how-to panel with placeholder docs, required JSON
  response keys, and usage notes
- A "Reset to Default" button (with confirmation step) that restores
  the built-in prompt without saving, letting the admin review first
- A "Using the built-in default prompt" indicator when unchanged

Backend includes default_system / default_user_template in the
system-prompts API response so the frontend never duplicates defaults.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-04-17 15:17:55 +02:00
parent 1d01cc3b0e
commit bc7a74062d
3 changed files with 266 additions and 16 deletions
+3 -1
View File
@@ -179,7 +179,7 @@ SYSTEM_PROMPT_SERVICES: dict[str, str] = {
def load_all_system_prompts() -> dict: def load_all_system_prompts() -> dict:
"""Return {service_id: {label, system, user_template}} for all registered services.""" """Return {service_id: {label, system, user_template, default_system, default_user_template}}."""
result: dict = {} result: dict = {}
for service_id, label in SYSTEM_PROMPT_SERVICES.items(): for service_id, label in SYSTEM_PROMPT_SERVICES.items():
config = load_service_config(service_id) config = load_service_config(service_id)
@@ -189,6 +189,8 @@ def load_all_system_prompts() -> dict:
"label": label, "label": label,
"system": prompts.get("system", defaults["system"]), "system": prompts.get("system", defaults["system"]),
"user_template": prompts.get("user_template", defaults["user_template"]), "user_template": prompts.get("user_template", defaults["user_template"]),
"default_system": defaults["system"],
"default_user_template": defaults["user_template"],
} }
return result return result
+2
View File
@@ -225,6 +225,8 @@ export interface ServiceSystemPrompt {
label: string; label: string;
system: string; system: string;
user_template: string; user_template: string;
default_system: string;
default_user_template: string;
} }
export type SystemPromptsData = Record<string, ServiceSystemPrompt>; export type SystemPromptsData = Record<string, ServiceSystemPrompt>;
+254 -8
View File
@@ -12,6 +12,53 @@ import {
type Provider = "anthropic" | "ollama" | "lmstudio"; type Provider = "anthropic" | "ollama" | "lmstudio";
type Tab = "general" | "system-prompts"; type Tab = "general" | "system-prompts";
// ── Static per-service documentation ──────────────────────────────────────────
interface ServiceDoc {
description: string;
placeholders: { name: string; required: boolean; description: string }[];
requiredResponseKeys: string[];
notes: string[];
}
const SERVICE_DOCS: Record<string, ServiceDoc> = {
doc_service: {
description:
"Extracts structured data from uploaded PDF documents. The system prompt instructs the model to return ONLY a JSON object. The user template is sent once per document with the extracted text injected at {text}.",
placeholders: [
{
name: "{text}",
required: true,
description:
"The first 50 000 characters of text extracted from the PDF. Must be present in the user template.",
},
],
requiredResponseKeys: [
"title",
"document_type",
"total_amount",
"currency",
"vendor_name",
"customer_name",
"billing_address",
"customer_address",
"invoice_number",
"invoice_date",
"due_date",
"tags",
"line_items",
"suggested_categories",
],
notes: [
'The system prompt should instruct the model to return ONLY a JSON object — no markdown, no code fences, no explanation.',
'document_type must be one of: invoice, bill, receipt, order, expense, revenue, unknown.',
'tags and suggested_categories must be JSON arrays of strings.',
'line_items must be a JSON array of objects, each with keys description and amount.',
'Missing keys will be stored as null — the service will not error on absent optional fields.',
],
},
};
// ── Shared layout helpers ────────────────────────────────────────────────────── // ── Shared layout helpers ──────────────────────────────────────────────────────
function Section({ title, children }: { title: string; children: React.ReactNode }) { function Section({ title, children }: { title: string; children: React.ReactNode }) {
@@ -46,10 +93,135 @@ const inputStyle: React.CSSProperties = {
const textareaStyle: React.CSSProperties = { const textareaStyle: React.CSSProperties = {
...inputStyle, ...inputStyle,
fontFamily: "monospace", fontFamily: "monospace",
fontSize: 13,
resize: "vertical", resize: "vertical",
minHeight: 120, minHeight: 120,
}; };
// ── Service documentation panel ────────────────────────────────────────────────
function ServiceDocsPanel({ serviceId }: { serviceId: string }) {
const [open, setOpen] = useState(false);
const doc = SERVICE_DOCS[serviceId];
if (!doc) return null;
return (
<div style={{ marginBottom: 16 }}>
<button
onClick={() => setOpen((v) => !v)}
style={{
background: "none",
border: "none",
cursor: "pointer",
fontSize: 13,
color: "#555",
padding: 0,
display: "flex",
alignItems: "center",
gap: 6,
}}
>
<span style={{ fontSize: 11 }}>{open ? "▼" : "▶"}</span>
How-to &amp; required keys
</button>
{open && (
<div
style={{
marginTop: 10,
padding: "14px 16px",
background: "#f8f9fa",
border: "1px solid #e0e0e0",
borderRadius: 5,
fontSize: 13,
lineHeight: 1.6,
}}
>
<p style={{ marginBottom: 10 }}>{doc.description}</p>
{doc.placeholders.length > 0 && (
<>
<p style={{ fontWeight: 600, marginBottom: 6 }}>Template placeholders</p>
<table style={{ width: "100%", borderCollapse: "collapse", marginBottom: 12 }}>
<thead>
<tr style={{ background: "#eee" }}>
<th style={thStyle}>Placeholder</th>
<th style={thStyle}>Required</th>
<th style={thStyle}>Description</th>
</tr>
</thead>
<tbody>
{doc.placeholders.map((p) => (
<tr key={p.name}>
<td style={tdStyle}>
<code style={codeStyle}>{p.name}</code>
</td>
<td style={{ ...tdStyle, textAlign: "center" }}>
{p.required ? (
<span style={{ color: "#c00", fontWeight: 600 }}>Yes</span>
) : (
"No"
)}
</td>
<td style={tdStyle}>{p.description}</td>
</tr>
))}
</tbody>
</table>
</>
)}
{doc.requiredResponseKeys.length > 0 && (
<>
<p style={{ fontWeight: 600, marginBottom: 6 }}>Expected JSON response keys</p>
<div style={{ display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 12 }}>
{doc.requiredResponseKeys.map((k) => (
<code key={k} style={codeStyle}>
{k}
</code>
))}
</div>
</>
)}
{doc.notes.length > 0 && (
<>
<p style={{ fontWeight: 600, marginBottom: 6 }}>Notes</p>
<ul style={{ paddingLeft: 18, margin: 0 }}>
{doc.notes.map((n, i) => (
<li key={i} style={{ marginBottom: 4 }}>
{n}
</li>
))}
</ul>
</>
)}
</div>
)}
</div>
);
}
const thStyle: React.CSSProperties = {
padding: "5px 10px",
textAlign: "left",
fontWeight: 600,
fontSize: 12,
borderBottom: "1px solid #ddd",
};
const tdStyle: React.CSSProperties = {
padding: "5px 10px",
verticalAlign: "top",
borderBottom: "1px solid #eee",
};
const codeStyle: React.CSSProperties = {
background: "#ececec",
padding: "1px 5px",
borderRadius: 3,
fontFamily: "monospace",
fontSize: 12,
};
// ── General tab ──────────────────────────────────────────────────────────────── // ── General tab ────────────────────────────────────────────────────────────────
function GeneralTab() { function GeneralTab() {
@@ -265,15 +437,29 @@ function ServicePromptCard({
label, label,
initialSystem, initialSystem,
initialUserTemplate, initialUserTemplate,
defaultSystem,
defaultUserTemplate,
}: { }: {
serviceId: string; serviceId: string;
label: string; label: string;
initialSystem: string; initialSystem: string;
initialUserTemplate: string; initialUserTemplate: string;
defaultSystem: string;
defaultUserTemplate: string;
}) { }) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [system, setSystem] = useState(initialSystem); const [system, setSystem] = useState(initialSystem);
const [userTemplate, setUserTemplate] = useState(initialUserTemplate); const [userTemplate, setUserTemplate] = useState(initialUserTemplate);
const [showResetConfirm, setShowResetConfirm] = useState(false);
const isAtDefault =
system === defaultSystem && userTemplate === defaultUserTemplate;
const handleReset = () => {
setSystem(defaultSystem);
setUserTemplate(defaultUserTemplate);
setShowResetConfirm(false);
};
const mut = useMutation({ const mut = useMutation({
mutationFn: () => updateSystemPrompt(serviceId, { system, user_template: userTemplate }), mutationFn: () => updateSystemPrompt(serviceId, { system, user_template: userTemplate }),
@@ -289,7 +475,9 @@ function ServicePromptCard({
marginBottom: 24, marginBottom: 24,
}} }}
> >
<h3 style={{ fontSize: 16, marginBottom: 16 }}>{label}</h3> <h3 style={{ fontSize: 16, marginBottom: 14 }}>{label}</h3>
<ServiceDocsPanel serviceId={serviceId} />
<Field label="System Prompt"> <Field label="System Prompt">
<textarea <textarea
@@ -300,9 +488,6 @@ function ServicePromptCard({
</Field> </Field>
<Field label="User Prompt Template"> <Field label="User Prompt Template">
<p style={{ fontSize: 12, color: "#888", marginBottom: 6 }}>
Use <code style={{ background: "#f4f4f4", padding: "1px 4px", borderRadius: 3 }}>{"{text}"}</code> as the placeholder for the document text.
</p>
<textarea <textarea
value={userTemplate} value={userTemplate}
onChange={(e) => setUserTemplate(e.target.value)} onChange={(e) => setUserTemplate(e.target.value)}
@@ -310,7 +495,7 @@ function ServicePromptCard({
/> />
</Field> </Field>
<div style={{ display: "flex", alignItems: "center", gap: 12, marginTop: 12 }}> <div style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 12, flexWrap: "wrap" }}>
<button <button
onClick={() => mut.mutate()} onClick={() => mut.mutate()}
disabled={mut.isPending} disabled={mut.isPending}
@@ -325,6 +510,56 @@ function ServicePromptCard({
> >
{mut.isPending ? "Saving…" : "Save"} {mut.isPending ? "Saving…" : "Save"}
</button> </button>
{!isAtDefault && !showResetConfirm && (
<button
onClick={() => setShowResetConfirm(true)}
style={{
padding: "8px 16px",
cursor: "pointer",
borderRadius: 4,
border: "1px solid #ccc",
background: "transparent",
fontSize: 14,
}}
>
Reset to Default
</button>
)}
{showResetConfirm && (
<span style={{ fontSize: 13, display: "flex", alignItems: "center", gap: 8 }}>
<span style={{ color: "#555" }}>Discard changes and restore the built-in default?</span>
<button
onClick={handleReset}
style={{
padding: "4px 12px",
cursor: "pointer",
borderRadius: 4,
border: "1px solid #c00",
color: "#c00",
background: "transparent",
fontSize: 13,
}}
>
Yes, reset
</button>
<button
onClick={() => setShowResetConfirm(false)}
style={{
padding: "4px 12px",
cursor: "pointer",
borderRadius: 4,
border: "1px solid #ccc",
background: "transparent",
fontSize: 13,
}}
>
Cancel
</button>
</span>
)}
{mut.isSuccess && ( {mut.isSuccess && (
<span style={{ fontSize: 13, color: "#2a9d8f" }}>Saved.</span> <span style={{ fontSize: 13, color: "#2a9d8f" }}>Saved.</span>
)} )}
@@ -332,6 +567,12 @@ function ServicePromptCard({
<span style={{ fontSize: 13, color: "#c00" }}>Failed to save.</span> <span style={{ fontSize: 13, color: "#c00" }}>Failed to save.</span>
)} )}
</div> </div>
{isAtDefault && (
<p style={{ marginTop: 8, fontSize: 12, color: "#aaa" }}>
Using the built-in default prompt.
</p>
)}
</div> </div>
); );
} }
@@ -343,22 +584,27 @@ function SystemPromptsTab() {
}); });
if (isLoading) return <div style={{ padding: 16 }}>Loading</div>; if (isLoading) return <div style={{ padding: 16 }}>Loading</div>;
if (isError || !data) return <div style={{ padding: 16, color: "#c00" }}>Failed to load system prompts.</div>; if (isError || !data)
return <div style={{ padding: 16, color: "#c00" }}>Failed to load system prompts.</div>;
return ( return (
<Section title="Service System Prompts"> <Section title="Service System Prompts">
<p style={{ fontSize: 13, color: "#666", marginBottom: 20 }}> <p style={{ fontSize: 13, color: "#666", marginBottom: 20 }}>
Each service sends its own system prompt to the AI model. Changes take effect within 30 seconds (config cache TTL). Each service sends its own system prompt to the AI model. Changes take effect within 30 seconds (config cache TTL).
</p> </p>
{Object.entries(data).map(([serviceId, { label, system, user_template }]) => ( {Object.entries(data).map(
([serviceId, { label, system, user_template, default_system, default_user_template }]) => (
<ServicePromptCard <ServicePromptCard
key={serviceId} key={serviceId}
serviceId={serviceId} serviceId={serviceId}
label={label} label={label}
initialSystem={system} initialSystem={system}
initialUserTemplate={user_template} initialUserTemplate={user_template}
defaultSystem={default_system}
defaultUserTemplate={default_user_template}
/> />
))} )
)}
</Section> </Section>
); );
} }