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:
@@ -179,7 +179,7 @@ SYSTEM_PROMPT_SERVICES: dict[str, str] = {
|
||||
|
||||
|
||||
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 = {}
|
||||
for service_id, label in SYSTEM_PROMPT_SERVICES.items():
|
||||
config = load_service_config(service_id)
|
||||
@@ -189,6 +189,8 @@ def load_all_system_prompts() -> dict:
|
||||
"label": label,
|
||||
"system": prompts.get("system", defaults["system"]),
|
||||
"user_template": prompts.get("user_template", defaults["user_template"]),
|
||||
"default_system": defaults["system"],
|
||||
"default_user_template": defaults["user_template"],
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
@@ -225,6 +225,8 @@ export interface ServiceSystemPrompt {
|
||||
label: string;
|
||||
system: string;
|
||||
user_template: string;
|
||||
default_system: string;
|
||||
default_user_template: string;
|
||||
}
|
||||
|
||||
export type SystemPromptsData = Record<string, ServiceSystemPrompt>;
|
||||
|
||||
@@ -12,6 +12,53 @@ import {
|
||||
type Provider = "anthropic" | "ollama" | "lmstudio";
|
||||
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 ──────────────────────────────────────────────────────
|
||||
|
||||
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
||||
@@ -46,10 +93,135 @@ const inputStyle: React.CSSProperties = {
|
||||
const textareaStyle: React.CSSProperties = {
|
||||
...inputStyle,
|
||||
fontFamily: "monospace",
|
||||
fontSize: 13,
|
||||
resize: "vertical",
|
||||
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 & 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 ────────────────────────────────────────────────────────────────
|
||||
|
||||
function GeneralTab() {
|
||||
@@ -265,15 +437,29 @@ function ServicePromptCard({
|
||||
label,
|
||||
initialSystem,
|
||||
initialUserTemplate,
|
||||
defaultSystem,
|
||||
defaultUserTemplate,
|
||||
}: {
|
||||
serviceId: string;
|
||||
label: string;
|
||||
initialSystem: string;
|
||||
initialUserTemplate: string;
|
||||
defaultSystem: string;
|
||||
defaultUserTemplate: string;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const [system, setSystem] = useState(initialSystem);
|
||||
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({
|
||||
mutationFn: () => updateSystemPrompt(serviceId, { system, user_template: userTemplate }),
|
||||
@@ -289,7 +475,9 @@ function ServicePromptCard({
|
||||
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">
|
||||
<textarea
|
||||
@@ -300,9 +488,6 @@ function ServicePromptCard({
|
||||
</Field>
|
||||
|
||||
<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
|
||||
value={userTemplate}
|
||||
onChange={(e) => setUserTemplate(e.target.value)}
|
||||
@@ -310,7 +495,7 @@ function ServicePromptCard({
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 12, marginTop: 12 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 12, flexWrap: "wrap" }}>
|
||||
<button
|
||||
onClick={() => mut.mutate()}
|
||||
disabled={mut.isPending}
|
||||
@@ -325,6 +510,56 @@ function ServicePromptCard({
|
||||
>
|
||||
{mut.isPending ? "Saving…" : "Save"}
|
||||
</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 && (
|
||||
<span style={{ fontSize: 13, color: "#2a9d8f" }}>Saved.</span>
|
||||
)}
|
||||
@@ -332,6 +567,12 @@ function ServicePromptCard({
|
||||
<span style={{ fontSize: 13, color: "#c00" }}>Failed to save.</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isAtDefault && (
|
||||
<p style={{ marginTop: 8, fontSize: 12, color: "#aaa" }}>
|
||||
Using the built-in default prompt.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -343,22 +584,27 @@ function SystemPromptsTab() {
|
||||
});
|
||||
|
||||
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 (
|
||||
<Section title="Service System Prompts">
|
||||
<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).
|
||||
</p>
|
||||
{Object.entries(data).map(([serviceId, { label, system, user_template }]) => (
|
||||
<ServicePromptCard
|
||||
key={serviceId}
|
||||
serviceId={serviceId}
|
||||
label={label}
|
||||
initialSystem={system}
|
||||
initialUserTemplate={user_template}
|
||||
/>
|
||||
))}
|
||||
{Object.entries(data).map(
|
||||
([serviceId, { label, system, user_template, default_system, default_user_template }]) => (
|
||||
<ServicePromptCard
|
||||
key={serviceId}
|
||||
serviceId={serviceId}
|
||||
label={label}
|
||||
initialSystem={system}
|
||||
initialUserTemplate={user_template}
|
||||
defaultSystem={default_system}
|
||||
defaultUserTemplate={default_user_template}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user