/** * Prompt-library template variables — pure helpers shared by the server (render on `/use`) and the client * (live detection in the editor, fill-in sheet). No React, no DB, no `server-only`. * * Syntax: `{{name}}` (whitespace inside the braces is tolerated: `{{ name }}`). Names start with a letter or * underscore and may contain letters, digits, `_`, `-` and `.`. Anything else is left untouched, so JSON examples * and Jinja-like blocks (`{% … %}`) inside a prompt are safe. */ export const VARIABLE_RE = /\{\{\s*([A-Za-z_][\w.-]*)\s*\}\}/g; export interface PromptVariable { name: string; label?: string; default?: string; required?: boolean; /** When set, the fill-in UI shows a select instead of a free text input. */ options?: string[]; } /** Unique variable names in order of first appearance. */ export function parseVariables(content: string): string[] { const out: string[] = []; const seen = new Set(); if (!content) return out; for (const m of content.matchAll(VARIABLE_RE)) { const name = m[1]; if (!seen.has(name)) { seen.add(name); out.push(name); } } return out; } /** * Reconciles the declared variable list with the names actually present in the content: keeps * label/default/options for names that still exist, appends new names, drops removed ones. Order = content order. */ export function mergeVariables(content: string, declared: PromptVariable[] | null | undefined): PromptVariable[] { const byName = new Map((declared ?? []).map((v) => [v.name, v])); return parseVariables(content).map((name) => { const prev = byName.get(name); return prev ? { ...prev, name } : { name }; }); } /** Human label fallback: `customer_name` → `Customer name`, `lang.code` → `Lang code`. */ export function labelFor(v: PromptVariable): string { if (v.label?.trim()) return v.label.trim(); const words = v.name.replace(/[_.-]+/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").trim().toLowerCase(); return words.charAt(0).toUpperCase() + words.slice(1); } export interface RenderResult { text: string; /** Variables that had neither a value nor a default (left as empty strings). */ missing: string[]; /** Variables that were filled with their declared default. */ defaulted: string[]; } /** * Replaces every `{{name}}` with `values[name]`, falling back to the declared default, then to `""`. * Unknown names (not declared) still render from `values` when provided. */ export function renderTemplate(content: string, values: Record = {}, declared: PromptVariable[] = []): RenderResult { const defaults = new Map(declared.map((v) => [v.name, v.default])); const missing = new Set(); const defaulted = new Set(); const text = content.replace(VARIABLE_RE, (_m, name: string) => { const v = values[name]; if (v !== undefined && v !== null && v !== "") return String(v); const d = defaults.get(name); if (d !== undefined && d !== "") { defaulted.add(name); return d; } missing.add(name); return ""; }); return { text, missing: [...missing], defaulted: [...defaulted] }; } /** Variables that the fill-in sheet must ask for: everything without a default (or explicitly required). */ export function variablesToAsk(vars: PromptVariable[]): PromptVariable[] { return vars.filter((v) => v.required || v.default === undefined || v.default === ""); }