aboutsummaryrefslogtreecommitdiffstats
path: root/renderer/src/components/settings/SystemPromptsSettings.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'renderer/src/components/settings/SystemPromptsSettings.tsx')
-rw-r--r--renderer/src/components/settings/SystemPromptsSettings.tsx142
1 files changed, 142 insertions, 0 deletions
diff --git a/renderer/src/components/settings/SystemPromptsSettings.tsx b/renderer/src/components/settings/SystemPromptsSettings.tsx
new file mode 100644
index 0000000..a0c6a22
--- /dev/null
+++ b/renderer/src/components/settings/SystemPromptsSettings.tsx
@@ -0,0 +1,142 @@
1import React, { useState, useEffect } from "react";
2import type { Phase } from "../../types";
3
4const PHASES: Phase[] = ["research", "plan", "implement"];
5const PHASE_LABELS: Record<Phase, string> = {
6 research: "Research",
7 plan: "Plan",
8 implement: "Implement",
9};
10
11const api = window.api;
12
13export function SystemPromptsSettings() {
14 const [activePhase, setActivePhase] = useState<Phase>("research");
15 // Hardcoded default text per phase (fetched once from main process)
16 const [defaults, setDefaults] = useState<Record<Phase, string> | null>(null);
17 // What is currently saved in the DB per phase (null = not customized)
18 const [saved, setSaved] = useState<Record<Phase, string | null>>({
19 research: null,
20 plan: null,
21 implement: null,
22 });
23 // Live textarea content per phase — initialised to saved ?? default on load
24 const [texts, setTexts] = useState<Record<Phase, string>>({
25 research: "",
26 plan: "",
27 implement: "",
28 });
29 const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved">("idle");
30
31 useEffect(() => {
32 Promise.all([
33 api.getDefaultSystemPrompts(),
34 api.getSettings([
35 "systemPrompt.research",
36 "systemPrompt.plan",
37 "systemPrompt.implement",
38 ]),
39 ]).then(([defs, settings]) => {
40 const d = defs as Record<Phase, string>;
41 const s: Record<Phase, string | null> = {
42 research: settings["systemPrompt.research"],
43 plan: settings["systemPrompt.plan"],
44 implement: settings["systemPrompt.implement"],
45 };
46 setDefaults(d);
47 setSaved(s);
48 setTexts({
49 research: s.research ?? d.research,
50 plan: s.plan ?? d.plan,
51 implement: s.implement ?? d.implement,
52 });
53 });
54 }, []);
55
56 if (!defaults) {
57 return (
58 <div style={{ color: "var(--text-secondary)", fontSize: 12 }}>
59 Loading...
60 </div>
61 );
62 }
63
64 const currentText = texts[activePhase];
65 const isCustomized = saved[activePhase] !== null;
66 // Dirty = textarea differs from what is in DB (or from default if not customized)
67 const isDirty = currentText !== (saved[activePhase] ?? defaults[activePhase]);
68
69 const handleChange = (val: string) => {
70 setTexts(prev => ({ ...prev, [activePhase]: val }));
71 setSaveStatus("idle");
72 };
73
74 const handleSave = async () => {
75 setSaveStatus("saving");
76 await api.setSetting(`systemPrompt.${activePhase}`, currentText);
77 setSaved(prev => ({ ...prev, [activePhase]: currentText }));
78 setSaveStatus("saved");
79 setTimeout(() => setSaveStatus("idle"), 2000);
80 };
81
82 const handleReset = async () => {
83 await api.deleteSetting(`systemPrompt.${activePhase}`);
84 setSaved(prev => ({ ...prev, [activePhase]: null }));
85 setTexts(prev => ({ ...prev, [activePhase]: defaults[activePhase] }));
86 setSaveStatus("idle");
87 };
88
89 return (
90 <div>
91 <div className="settings-section-title">System Prompts</div>
92 <div className="settings-section-desc">
93 Customize the instructions sent to Claude at the start of each workflow
94 phase. Changes take effect on the next message sent in any session.
95 </div>
96
97 <div className="settings-tabs">
98 {PHASES.map(phase => (
99 <button
100 key={phase}
101 className={`settings-tab${activePhase === phase ? " active" : ""}`}
102 onClick={() => setActivePhase(phase)}
103 >
104 {PHASE_LABELS[phase]}
105 {saved[phase] !== null ? " \u25cf" : ""}
106 </button>
107 ))}
108 </div>
109
110 <textarea
111 className={`settings-textarea${isCustomized ? " is-custom" : ""}`}
112 value={currentText}
113 onChange={e => handleChange(e.target.value)}
114 spellCheck={false}
115 />
116
117 <div className="settings-actions">
118 {isCustomized && (
119 <button className="btn-secondary" onClick={handleReset}>
120 Reset to Default
121 </button>
122 )}
123 <button
124 className="btn-primary"
125 onClick={handleSave}
126 disabled={saveStatus === "saving" || !isDirty}
127 >
128 {saveStatus === "saving"
129 ? "Saving\u2026"
130 : saveStatus === "saved"
131 ? "Saved \u2713"
132 : "Save"}
133 </button>
134 {(isCustomized || isDirty) && (
135 <span className="settings-custom-badge">
136 {isDirty ? "unsaved" : "custom"}
137 </span>
138 )}
139 </div>
140 </div>
141 );
142}