Make the AI insight's prompt instructions editable via Settings

Split the system prompt into an editable part (persona/focus/tone/
format — what the briefing prioritises and how strict its length is)
and a fixed structural appendix (how the Manual Context and Recent
Previous Insights sections get interpreted, which map directly to
conditional data the code assembles, not just prose — kept safe from
being accidentally edited away).

New ai_insights_prompt_instructions config key, a
GET /api/ai-insights/default-prompt endpoint so the Settings UI can
pre-fill/reset to the built-in default, and a textarea in the AI
Insights settings card.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-25 11:09:44 +00:00
parent 8f8e273650
commit e447d48e37
6 changed files with 73 additions and 23 deletions

View file

@ -105,3 +105,7 @@ export function generateInsight(): Promise<{ success: boolean; content: string;
export function testAiInsightsConnection(): Promise<{ status: string; message: string }> {
return request('/ai-insights/test', { method: 'POST' })
}
export function getDefaultPrompt(): Promise<{ prompt: string }> {
return request('/ai-insights/default-prompt')
}

View file

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import { RefreshCw, Download, X, CheckSquare, Square, Bot } from 'lucide-react'
import { getSettings, saveSettings, getDepartments, triggerSync, getSyncStatus, cancelBackfill, cancelRotaBackfill, testAiInsightsConnection, generateInsight } from '../api'
import { getSettings, saveSettings, getDepartments, triggerSync, getSyncStatus, cancelBackfill, cancelRotaBackfill, testAiInsightsConnection, generateInsight, getDefaultPrompt } from '../api'
import type { AppSetting, Department } from '../types'
function fmtDate(iso: string | null): string {
@ -26,14 +26,16 @@ export default function SettingsPage() {
const [testMessage, setTestMessage] = useState('')
const [genStatus, setGenStatus] = useState<'idle' | 'generating' | 'done' | 'error'>('idle')
const [genMessage, setGenMessage] = useState('')
const [defaultPrompt, setDefaultPrompt] = useState('')
useEffect(() => {
Promise.all([getSettings(), getSyncStatus()])
.then(([settRes, statusRes]) => {
Promise.all([getSettings(), getSyncStatus(), getDefaultPrompt()])
.then(([settRes, statusRes, promptRes]) => {
const map: Record<string, string> = {}
for (const s of settRes.settings as AppSetting[]) map[s.key] = s.value
setSettings(map)
setSyncStatus(statusRes)
setDefaultPrompt(promptRes.prompt)
// Parse saved departments if present
if (map.departments) {
@ -63,6 +65,7 @@ export default function SettingsPage() {
{ key: 'ai_insights_schedule_time', value: settings.ai_insights_schedule_time ?? '07:15' },
{ key: 'ai_insights_daily_token_budget', value: settings.ai_insights_daily_token_budget ?? '5000' },
{ key: 'ai_insights_manual_context', value: settings.ai_insights_manual_context ?? '' },
{ key: 'ai_insights_prompt_instructions', value: settings.ai_insights_prompt_instructions ?? '' },
])
setSaved(true)
setTimeout(() => setSaved(false), 2000)
@ -362,6 +365,33 @@ export default function SettingsPage() {
</p>
</div>
<div style={{ marginBottom: 12 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
<label style={{ fontSize: 12, fontWeight: 500, color: 'var(--text-muted)' }}>
Prompt Instructions
</label>
<button
className="btn btn-secondary"
style={{ fontSize: 11, padding: '2px 8px' }}
onClick={() => handleChange('ai_insights_prompt_instructions', defaultPrompt)}
>
Reset to default
</button>
</div>
<textarea
rows={8}
style={{ width: '100%', resize: 'vertical', fontFamily: 'inherit', fontSize: 12.5 }}
value={settings.ai_insights_prompt_instructions || defaultPrompt}
onChange={e => handleChange('ai_insights_prompt_instructions', e.target.value)}
/>
<p style={{ margin: '4px 0 0', fontSize: 12, color: 'var(--text-muted)' }}>
The persona/focus/tone/format instructions given to the model edit freely to change what it
prioritises, how strict the length limit is, or its tone. Pre-filled with the built-in default;
how the Manual Context and Recent Previous Insights sections above get interpreted is always
appended automatically and can't be removed here.
</p>
</div>
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', alignItems: 'flex-start' }}>
<div>
<button className="btn btn-secondary" onClick={handleTestConnection} disabled={testStatus === 'testing'}>