From d49ad4aca750ff1fa12cc7f65aa35babb57e9eb0 Mon Sep 17 00:00:00 2001 From: Catherine Renelle <32781029+catrenelle@users.noreply.github.com> Date: Sun, 8 Feb 2026 20:26:07 -0500 Subject: [PATCH] Use editable default values with {placeholders} for AI prompts Instead of showing defaults as greyed-out placeholder text, populate textareas with the actual default prompt text so users can directly edit it. Add {categories}, {examples}, and {transactions} placeholders so users control where dynamic data is inserted in the prompts. Co-Authored-By: Claude Opus 4.6 --- frontend/src/views/settings/SettingsView.vue | 26 ++++++----- .../Services/AiCategorizationService.cs | 46 ++++++++++++++----- 2 files changed, 50 insertions(+), 22 deletions(-) diff --git a/frontend/src/views/settings/SettingsView.vue b/frontend/src/views/settings/SettingsView.vue index 9d09d76..acb2402 100644 --- a/frontend/src/views/settings/SettingsView.vue +++ b/frontend/src/views/settings/SettingsView.vue @@ -470,8 +470,10 @@ Prompt Templates

- Customize the prompts sent to the AI model. Leave empty to use built-in defaults. - The category list is always appended to the system prompt, and the transaction list is always appended to the user prompt. + Customize the prompts sent to the AI model. Use placeholders to control where dynamic data is inserted: + {categories} = category list, + {examples} = recent categorized transactions, + {transactions} = uncategorized transactions to classify.

@@ -488,9 +488,7 @@ v-model="aiSettings.userPromptTemplate" label="User Prompt Template" variant="outlined" - rows="3" - :placeholder="defaultUserPromptTemplate" - persistent-placeholder + rows="5" class="mb-4" /> @@ -867,21 +865,27 @@ Rules: - "confidence" must be a number between 0 and 1 - Include one entry for every transaction -Available categories:` +Available categories: +{categories}` -const defaultUserPromptTemplate = 'Classify these uncategorized transactions:' +const defaultUserPromptTemplate = `{examples}Classify these uncategorized transactions: +{transactions}` function resetPromptsToDefaults() { - aiSettings.systemPrompt = null - aiSettings.userPromptTemplate = null + aiSettings.systemPrompt = defaultSystemPrompt + aiSettings.userPromptTemplate = defaultUserPromptTemplate } async function loadAiSettings() { try { const { data } = await aiCategorizationApi.getSettings() Object.assign(aiSettings, data) + aiSettings.systemPrompt = aiSettings.systemPrompt || defaultSystemPrompt + aiSettings.userPromptTemplate = aiSettings.userPromptTemplate || defaultUserPromptTemplate } catch { // Not configured yet — defaults are fine + aiSettings.systemPrompt = defaultSystemPrompt + aiSettings.userPromptTemplate = defaultUserPromptTemplate } } diff --git a/src/Purrse.Api/Services/AiCategorizationService.cs b/src/Purrse.Api/Services/AiCategorizationService.cs index 9499232..15578cc 100644 --- a/src/Purrse.Api/Services/AiCategorizationService.cs +++ b/src/Purrse.Api/Services/AiCategorizationService.cs @@ -35,9 +35,14 @@ public class AiCategorizationService : IAiCategorizationService - Include one entry for every transaction Available categories: + {categories} """; - internal const string DefaultUserPromptTemplate = "Classify these uncategorized transactions:"; + internal const string DefaultUserPromptTemplate = + """ + {examples}Classify these uncategorized transactions: + {transactions} + """; private static readonly JsonSerializerOptions JsonOptions = new() { @@ -233,45 +238,64 @@ public class AiCategorizationService : IAiCategorizationService private static string BuildSystemMessage(AiCategorizationSettings settings, List categories) { - var sb = new StringBuilder(); - sb.AppendLine(settings.SystemPrompt ?? DefaultSystemPrompt); + var prompt = settings.SystemPrompt ?? DefaultSystemPrompt; + var sb = new StringBuilder(); foreach (var category in categories) { var parentInfo = category.Parent != null ? $" (under \"{category.Parent.Name}\")" : ""; sb.AppendLine($"- \"{category.Name}\"{parentInfo}"); } + var categoryList = sb.ToString(); - return sb.ToString(); + return prompt.Contains("{categories}") + ? prompt.Replace("{categories}", categoryList) + : prompt + "\n" + categoryList; } private static string BuildUserMessage(AiCategorizationSettings settings, List recentCategorized, List batch) { - var sb = new StringBuilder(); + var prompt = settings.UserPromptTemplate ?? DefaultUserPromptTemplate; + // Build examples block + var examplesSb = new StringBuilder(); if (recentCategorized.Count > 0) { - sb.AppendLine("Recent categorized transactions (for reference):"); + examplesSb.AppendLine("Recent categorized transactions (for reference):"); for (int i = 0; i < recentCategorized.Count; i++) { var t = recentCategorized[i]; var categoryName = t.Category?.Parent != null ? $"{t.Category.Parent.Name} > {t.Category.Name}" : t.Category?.Name ?? "Unknown"; - sb.AppendLine($"{i + 1}. \"{t.PayeeName}\" (${t.Amount:F2}) -> \"{categoryName}\""); + examplesSb.AppendLine($"{i + 1}. \"{t.PayeeName}\" (${t.Amount:F2}) -> \"{categoryName}\""); } - sb.AppendLine(); + examplesSb.AppendLine(); } + var examples = examplesSb.ToString(); - sb.AppendLine(settings.UserPromptTemplate ?? DefaultUserPromptTemplate); + // Build transactions block + var txnSb = new StringBuilder(); for (int i = 0; i < batch.Count; i++) { var t = batch[i]; var type = t.Amount >= 0 ? "credit" : "debit"; - sb.AppendLine($"{i}. \"{t.PayeeName}\" (${t.Amount:F2}, {type}, {t.Date:yyyy-MM-dd}, memo: \"{t.Memo}\")"); + txnSb.AppendLine($"{i}. \"{t.PayeeName}\" (${t.Amount:F2}, {type}, {t.Date:yyyy-MM-dd}, memo: \"{t.Memo}\")"); } + var transactions = txnSb.ToString(); - return sb.ToString(); + var result = prompt; + if (result.Contains("{examples}")) + result = result.Replace("{examples}", examples); + else + result = examples + result; + + if (result.Contains("{transactions}")) + result = result.Replace("{transactions}", transactions); + else + result = result + "\n" + transactions; + + return result; } private List ParseClassifications(