+
renameVariable(row.name, value)" />
updateVariable(row.name, { type: value })" />
updateVariable(row.name, { description: value })" />
@@ -64,6 +64,7 @@ import TemplatePreview from './TemplatePreview.vue'
import TemplateAssistant from './TemplateAssistant.vue'
import { applyTemplateProposal } from '../api/assistant.js'
import { createTemplate, renderTemplate, updateTemplate } from '../api/templates.js'
+import { createVariableKeyStore, removeTemplateVariable, renameTemplateVariable, updateTemplateVariable } from '../api/templateVariables.js'
const props = defineProps({ template: { type: Object, default: null } })
const emit = defineEmits(['saved', 'cancel'])
@@ -86,7 +87,8 @@ function blankTemplate () {
function clone (value) { return JSON.parse(JSON.stringify(value)) }
const form = ref(blankTemplate())
const isEditing = computed(() => Boolean(props.template?.id))
-const variableRows = computed(() => Object.entries(form.value.evaluationMethod.variableSchema.properties || {}).map(([name, definition]) => ({ name, definition: definition || {} })))
+const variableKeys = createVariableKeyStore()
+const variableRows = computed(() => Object.entries(form.value.evaluationMethod.variableSchema.properties || {}).map(([name, definition]) => ({ name, key: variableKeys.keyFor(name), definition: definition || {} })))
const canRender = computed(() => Boolean(form.value.id && Object.keys(errors.value).length === 0))
watch(() => props.template, value => { form.value = value ? clone(value) : blankTemplate(); previewValue.value = '' }, { immediate: true })
@@ -104,25 +106,20 @@ function addVariable () {
updateVariable(name, { type: 'string', description: '' }, true)
}
function updateVariable (name, patch, create = false) {
- const properties = { ...form.value.evaluationMethod.variableSchema.properties }
- properties[name] = { ...(properties[name] || {}), ...patch }
- form.value.evaluationMethod.variableSchema.properties = properties
+ form.value.evaluationMethod.variableSchema = updateTemplateVariable(form.value.evaluationMethod.variableSchema, name, patch)
if (create) toggleRequired(name, true)
}
function renameVariable (oldName, nextName) {
const name = nextName.trim()
- if (!name || name === oldName || form.value.evaluationMethod.variableSchema.properties[name]) return
- const properties = { ...form.value.evaluationMethod.variableSchema.properties }
- properties[name] = properties[oldName]
- delete properties[oldName]
- form.value.evaluationMethod.variableSchema.properties = properties
- form.value.evaluationMethod.variableSchema.required = (form.value.evaluationMethod.variableSchema.required || []).map(item => item === oldName ? name : item)
+ const schema = form.value.evaluationMethod.variableSchema
+ const nextSchema = renameTemplateVariable(schema, oldName, name)
+ if (nextSchema === schema) return
+ variableKeys.rename(oldName, name)
+ form.value.evaluationMethod.variableSchema = nextSchema
}
function removeVariable (name) {
- const properties = { ...form.value.evaluationMethod.variableSchema.properties }
- delete properties[name]
- form.value.evaluationMethod.variableSchema.properties = properties
- toggleRequired(name, false)
+ variableKeys.remove(name)
+ form.value.evaluationMethod.variableSchema = removeTemplateVariable(form.value.evaluationMethod.variableSchema, name)
}
function toggleRequired (name, checked) {
const current = new Set(form.value.evaluationMethod.variableSchema.required || [])
diff --git a/vla-manager/test/template-assistant.test.mjs b/vla-manager/test/template-assistant.test.mjs
index 9ab8c85a..808c3433 100644
--- a/vla-manager/test/template-assistant.test.mjs
+++ b/vla-manager/test/template-assistant.test.mjs
@@ -2,6 +2,82 @@ import test from 'node:test'
import assert from 'node:assert/strict'
import { applyTemplateProposal, assistantErrorMessage } from '../src/api/assistant.js'
+import {
+ assistantTextChunks,
+ formatAssistantJson,
+ normaliseAssistantExamples,
+ tokeniseAssistantJson
+} from '../src/api/assistantPresentation.js'
+import { createVariableKeyStore, renameTemplateVariable } from '../src/api/templateVariables.js'
+
+test('renames one template variable without disturbing siblings or focus identity', () => {
+ const schema = {
+ type: 'object',
+ properties: {
+ first: { type: 'string', description: 'First value' },
+ second: { type: 'number', description: 'Second value' }
+ },
+ required: ['first', 'second']
+ }
+ const keys = createVariableKeyStore()
+ const firstKey = keys.keyFor('first')
+ const secondKey = keys.keyFor('second')
+
+ const next = renameTemplateVariable(schema, 'first', 'firs')
+ keys.rename('first', 'firs')
+
+ assert.deepEqual(next.properties, {
+ firs: { type: 'string', description: 'First value' },
+ second: { type: 'number', description: 'Second value' }
+ })
+ assert.deepEqual(next.required, ['firs', 'second'])
+ assert.deepEqual(schema.properties, {
+ first: { type: 'string', description: 'First value' },
+ second: { type: 'number', description: 'Second value' }
+ })
+ assert.equal(keys.keyFor('firs'), firstKey)
+ assert.equal(keys.keyFor('second'), secondKey)
+})
+
+test('normalises assistant examples into passing and failing lists', () => {
+ assert.deepEqual(
+ normaliseAssistantExamples({
+ passing: { timestamp: '2026-09-08T08:00:00Z', production_kwh: 420 },
+ failing: [
+ { timestamp: 'not-a-date' },
+ { timestamp: '2026-09-08T08:00:00Z', production_kwh: -2 }
+ ]
+ }),
+ {
+ passing: [{ timestamp: '2026-09-08T08:00:00Z', production_kwh: 420 }],
+ failing: [
+ { timestamp: 'not-a-date' },
+ { timestamp: '2026-09-08T08:00:00Z', production_kwh: -2 }
+ ]
+ }
+ )
+})
+
+test('formats assistant JSON without losing primitive values', () => {
+ assert.equal(formatAssistantJson({ valid: true, count: 2 }), '{\n "valid": true,\n "count": 2\n}')
+ assert.equal(formatAssistantJson('not available'), 'not available')
+})
+
+test('tokenises JSON for readable syntax colouring', () => {
+ const tokens = tokeniseAssistantJson({ valid: true, count: 2, label: 'ok' })
+
+ assert.ok(tokens.some(token => token.type === 'key' && token.text === '"valid"'))
+ assert.ok(tokens.some(token => token.type === 'boolean' && token.text === 'true'))
+ assert.ok(tokens.some(token => token.type === 'number' && token.text === '2'))
+ assert.ok(tokens.some(token => token.type === 'string' && token.text === '"ok"'))
+})
+
+test('splits assistant text into whitespace-preserving word chunks', () => {
+ assert.deepEqual(
+ assistantTextChunks('Template ready. Review the draft.'),
+ ['Template ', 'ready. ', 'Review ', 'the ', 'draft.']
+ )
+})
test('applies only template fields from an assistant proposal', () => {
const current = {