diff --git a/src/app/shared/nodes/generic-node/generic-node.spec.ts b/src/app/shared/nodes/generic-node/generic-node.spec.ts index b8408b9..724ebcb 100644 --- a/src/app/shared/nodes/generic-node/generic-node.spec.ts +++ b/src/app/shared/nodes/generic-node/generic-node.spec.ts @@ -857,6 +857,35 @@ describe('GenericNodeComponent', () => { expect(component.localEditorGlobalName).toBe(''); }); + it('offers Use default only on a field whose schema says it falls back to one', async () => { + // Every optional field used to get it - a choice between a catalog storage and your own + // connection, an operation, a scope - where there is nothing to go back to. + const component = fixture.componentInstance as any; + const choice = { + ...bindableFieldDefinition(), + path: 'source', + label: 'Connection', + enumOptions: ['CATALOG', 'PERSONAL'], + ui: { visibleWhen: [], enabledWhen: [] } + }; + const knob = { + ...bindableFieldDefinition(), + path: 'llmDescriptor.parameters.temperature', + label: 'Temperature', + type: 'number' as const, + ui: { visibleWhen: [], enabledWhen: [], defaultsWhenEmpty: true } + }; + component.editableFieldDefinitions = [choice, knob]; + component.ensureBlockConfiguration()['llmDescriptor'] = { provider: 'InternalOllama' }; + + await component.openParameterEditor('source'); + expect(component.showLocalEditorDefault()).toBe(false); + + component.closeLocalEditor?.(); + await component.openParameterEditor('llmDescriptor.parameters.temperature'); + expect(component.showLocalEditorDefault()).toBe(true); + }); + it('renders the field description, which the modal never used to show', async () => { // The other half of why none of this was discoverable: the modal held no tip at all, so a // @UiDescription explaining that the field takes a placeholder was invisible. diff --git a/src/app/shared/nodes/generic-node/generic-node.ts b/src/app/shared/nodes/generic-node/generic-node.ts index 9afc4c4..7e507e3 100644 --- a/src/app/shared/nodes/generic-node/generic-node.ts +++ b/src/app/shared/nodes/generic-node/generic-node.ts @@ -213,7 +213,7 @@ export class GenericNodeComponent implements OnDestroy { localEditorPattern: string | null = null; /** The field's own description. The modal never rendered one, so every tip was invisible. */ localEditorTip: string | null = null; - /** Empty means unset on this field, and the modal says so. Every optional field, not just numbers. */ + /** Empty hands the field to a default, and the modal says so - only where the schema says it does. */ localEditorDefaultsWhenEmpty = false; localEditorDefaultValue: string | null = null; localEditorMin: number | null = null; @@ -449,8 +449,12 @@ export class GenericNodeComponent implements OnDestroy { this.localEditorMinLength = definition.ui.minLength ?? null; this.localEditorPattern = definition.ui.pattern ?? null; this.localEditorTip = definition.ui.tip ?? null; - // A boolean has no empty state: false is a value, not an absence. - this.localEditorDefaultsWhenEmpty = definition.type !== 'boolean' && !this.isPathRequired(definition.path); + // Only where the server says emptying the field hands it to a default - the model's sampling + // knobs. An optional field is not one: most have no default to go back to, and a choice + // between alternatives (a source, a scope, an operation) is picked, not reset. A boolean has + // no empty state at all: false is a value, not an absence. + this.localEditorDefaultsWhenEmpty = definition.ui.defaultsWhenEmpty === true + && definition.type !== 'boolean' && !this.isPathRequired(definition.path); this.localEditorDefaultValue = this.schemaDeclaredDefault(definition.path); this.localEditorMin = definition.ui.minimum ?? null; this.localEditorMax = definition.ui.maximum ?? null;