diff --git a/src/app/shared/nodes/schema-driven-fields.spec.ts b/src/app/shared/nodes/schema-driven-fields.spec.ts index 518447a..b843336 100644 --- a/src/app/shared/nodes/schema-driven-fields.spec.ts +++ b/src/app/shared/nodes/schema-driven-fields.spec.ts @@ -5,6 +5,7 @@ import { import { parentGroupLabel } from './node-utility'; import { buildOrderedSchemaDisplay, + buildTemplatedRichContentParts, buildSchemaEditableFieldDefinitions, buildSchemaFieldViewModel, buildSchemaRetrieverContext, @@ -668,3 +669,55 @@ describe('optional groups', () => { }); }); +describe('buildTemplatedRichContentParts', () => { + /** Splitting a value on its ${{...}} placeholders so the editor can mark them. */ + const splitParts = (content: string) => + content.split(/(\$\{\{[^}]*\}\})/).filter((part) => part.length > 0) + .map((part) => ({ text: part, isDynamicInput: part.startsWith('${{') })); + + function schemaWith(field: Record) { + return { type: 'object', properties: { subject: field } }; + } + + it('splits a one-line field that says it takes placeholders', () => { + // It used to require x-ui-widget: textarea as well, so a field drawn as anything else could + // not declare that its value is interpolated - which is what LLMDescriptor.model needs. + const schema = schemaWith({ type: 'string', 'x-ui-accept-variable-as-placeholder': true }); + + const parts = buildTemplatedRichContentParts( + { subject: 'model is ${{global.modelName}}' }, 'subject', schema, splitParts); + + expect(parts).toEqual([ + { text: 'model is ', isDynamicInput: false }, + { text: '${{global.modelName}}', isDynamicInput: true } + ]); + }); + + it('still splits a textarea, which is every field that declared it until now', () => { + const schema = schemaWith({ + type: 'string', + 'x-ui-widget': 'textarea', + 'x-ui-accept-variable-as-placeholder': true + }); + + const parts = buildTemplatedRichContentParts( + { subject: 'Summarise ${{text}}' }, 'subject', schema, splitParts); + + expect(parts.some((part) => part.isDynamicInput)).toBe(true); + }); + + it('leaves a field that never claimed to take placeholders as one flat part', () => { + const schema = schemaWith({ type: 'string' }); + + const parts = buildTemplatedRichContentParts( + { subject: 'literally ${{not a placeholder}}' }, 'subject', schema, splitParts); + + expect(parts).toEqual([{ text: 'literally ${{not a placeholder}}', isDynamicInput: false }]); + }); + + it('has nothing to split when the value is empty', () => { + const schema = schemaWith({ type: 'string', 'x-ui-accept-variable-as-placeholder': true }); + + expect(buildTemplatedRichContentParts({ subject: ' ' }, 'subject', schema, splitParts)).toEqual([]); + }); +}); diff --git a/src/app/shared/nodes/schema-driven-fields.ts b/src/app/shared/nodes/schema-driven-fields.ts index 471042b..2385794 100644 --- a/src/app/shared/nodes/schema-driven-fields.ts +++ b/src/app/shared/nodes/schema-driven-fields.ts @@ -707,7 +707,11 @@ export function buildTemplatedRichContentParts( if (!content) return []; const ui = getSchemaPathUiMeta(root, path); - if (ui.widget === 'textarea' && ui.acceptVariableAsPlaceholder) { + // The flag alone decides: whether a value is interpolated is a property of the value, not of the + // control used to type it. Requiring a textarea too meant a one-line field could not say it + // takes ${{...}} - and every field that says so today is a textarea anyway, so nothing that + // already worked changes. + if (ui.acceptVariableAsPlaceholder) { return splitParts(content); }