diff options
Diffstat (limited to 'renderer/src/components')
| -rw-r--r-- | renderer/src/components/Header.tsx | 7 | ||||
| -rw-r--r-- | renderer/src/components/SettingsPage.tsx | 51 | ||||
| -rw-r--r-- | renderer/src/components/settings/SystemPromptsSettings.tsx | 142 |
3 files changed, 200 insertions, 0 deletions
diff --git a/renderer/src/components/Header.tsx b/renderer/src/components/Header.tsx index fc0289d..3a530d3 100644 --- a/renderer/src/components/Header.tsx +++ b/renderer/src/components/Header.tsx | |||
| @@ -18,6 +18,7 @@ interface HeaderProps { | |||
| 18 | theme: Theme; | 18 | theme: Theme; |
| 19 | onToggleTheme: () => void; | 19 | onToggleTheme: () => void; |
| 20 | gitBranch: string | null; | 20 | gitBranch: string | null; |
| 21 | onOpenSettings: () => void; | ||
| 21 | } | 22 | } |
| 22 | 23 | ||
| 23 | const phaseLabels: Record<Phase, string> = { | 24 | const phaseLabels: Record<Phase, string> = { |
| @@ -43,6 +44,7 @@ export function Header({ | |||
| 43 | theme, | 44 | theme, |
| 44 | onToggleTheme, | 45 | onToggleTheme, |
| 45 | gitBranch, | 46 | gitBranch, |
| 47 | onOpenSettings, | ||
| 46 | }: HeaderProps) { | 48 | }: HeaderProps) { |
| 47 | const handleDeleteProject = () => { | 49 | const handleDeleteProject = () => { |
| 48 | if (!selectedProject || !onDeleteProject) return; | 50 | if (!selectedProject || !onDeleteProject) return; |
| @@ -222,6 +224,11 @@ export function Header({ | |||
| 222 | <button className="theme-toggle" onClick={onToggleTheme}> | 224 | <button className="theme-toggle" onClick={onToggleTheme}> |
| 223 | {theme === "dark" ? "[light]" : "[dark]"} | 225 | {theme === "dark" ? "[light]" : "[dark]"} |
| 224 | </button> | 226 | </button> |
| 227 | |||
| 228 | {/* ── Settings button ── */} | ||
| 229 | <button className="settings-btn" onClick={onOpenSettings} title="Settings"> | ||
| 230 | ⚙ | ||
| 231 | </button> | ||
| 225 | </div> | 232 | </div> |
| 226 | </header> | 233 | </header> |
| 227 | ); | 234 | ); |
diff --git a/renderer/src/components/SettingsPage.tsx b/renderer/src/components/SettingsPage.tsx new file mode 100644 index 0000000..5267665 --- /dev/null +++ b/renderer/src/components/SettingsPage.tsx | |||
| @@ -0,0 +1,51 @@ | |||
| 1 | import React, { useState } from "react"; | ||
| 2 | import { SystemPromptsSettings } from "./settings/SystemPromptsSettings"; | ||
| 3 | |||
| 4 | type SettingsSection = "system-prompts"; | ||
| 5 | |||
| 6 | interface SettingsPageProps { | ||
| 7 | onClose: () => void; | ||
| 8 | } | ||
| 9 | |||
| 10 | export function SettingsPage({ onClose }: SettingsPageProps) { | ||
| 11 | const [activeSection, setActiveSection] = | ||
| 12 | useState<SettingsSection>("system-prompts"); | ||
| 13 | |||
| 14 | return ( | ||
| 15 | <div className="settings-overlay"> | ||
| 16 | {/* Header — matches the main app header height/style */} | ||
| 17 | <div className="settings-header"> | ||
| 18 | <div className="settings-header-left"> | ||
| 19 | <span className="settings-title">\u2699 Settings</span> | ||
| 20 | </div> | ||
| 21 | <button | ||
| 22 | className="settings-close" | ||
| 23 | onClick={onClose} | ||
| 24 | title="Close settings" | ||
| 25 | > | ||
| 26 | \u00d7 | ||
| 27 | </button> | ||
| 28 | </div> | ||
| 29 | |||
| 30 | <div className="settings-body"> | ||
| 31 | {/* Side nav */} | ||
| 32 | <nav className="settings-nav"> | ||
| 33 | <button | ||
| 34 | className={`settings-nav-item${ | ||
| 35 | activeSection === "system-prompts" ? " active" : "" | ||
| 36 | }`} | ||
| 37 | onClick={() => setActiveSection("system-prompts")} | ||
| 38 | > | ||
| 39 | System Prompts | ||
| 40 | </button> | ||
| 41 | {/* Future sections added here */} | ||
| 42 | </nav> | ||
| 43 | |||
| 44 | {/* Content */} | ||
| 45 | <div className="settings-content"> | ||
| 46 | {activeSection === "system-prompts" && <SystemPromptsSettings />} | ||
| 47 | </div> | ||
| 48 | </div> | ||
| 49 | </div> | ||
| 50 | ); | ||
| 51 | } | ||
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 @@ | |||
| 1 | import React, { useState, useEffect } from "react"; | ||
| 2 | import type { Phase } from "../../types"; | ||
| 3 | |||
| 4 | const PHASES: Phase[] = ["research", "plan", "implement"]; | ||
| 5 | const PHASE_LABELS: Record<Phase, string> = { | ||
| 6 | research: "Research", | ||
| 7 | plan: "Plan", | ||
| 8 | implement: "Implement", | ||
| 9 | }; | ||
| 10 | |||
| 11 | const api = window.api; | ||
| 12 | |||
| 13 | export 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 | } | ||
