Private
Public Access
1
0

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 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-02-08 20:26:07 -05:00
parent e742b0fbe7
commit d49ad4aca7
2 changed files with 50 additions and 22 deletions
+15 -11
View File
@@ -470,8 +470,10 @@
<v-card-title>Prompt Templates</v-card-title>
<v-card-text>
<p class="text-body-2 text-medium-emphasis mb-4">
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:
<strong>{categories}</strong> = category list,
<strong>{examples}</strong> = recent categorized transactions,
<strong>{transactions}</strong> = uncategorized transactions to classify.
</p>
<v-textarea
@@ -479,8 +481,6 @@
label="System Prompt"
variant="outlined"
rows="8"
:placeholder="defaultSystemPrompt"
persistent-placeholder
class="mb-4"
/>
@@ -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
}
}
@@ -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<Category> 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<Transaction> recentCategorized, List<Transaction> 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<TransactionClassification> ParseClassifications(