diff --git a/src/app/shared/nodes/generic-node/generic-node.css b/src/app/shared/nodes/generic-node/generic-node.css index 4863ea5..9add75f 100644 --- a/src/app/shared/nodes/generic-node/generic-node.css +++ b/src/app/shared/nodes/generic-node/generic-node.css @@ -796,10 +796,9 @@ .llm-params { margin: 0 12px 12px; - padding: 10px; - border: 1px solid #e2e8f0; - border-radius: 10px; - background: #f8fafc; + padding: 0; + border: none; + background: transparent; display: flex; flex-direction: column; gap: 8px; @@ -815,7 +814,7 @@ .llm-param-grid { display: grid; grid-template-columns: 1fr 1fr; - gap: 6px; + gap: 8px; } .llm-param-groups { @@ -826,15 +825,15 @@ .llm-param-fieldset { margin: 0; - padding: 8px; - border: 1px solid #cbd5e1; - border-radius: 0; - background: transparent; + padding: 10px; + border: 1px solid #e6ecf4; + border-radius: 12px; + background: #fbfdff; } .llm-param-legend { margin: 0; - padding: 0 4px; + padding: 0 6px; font-size: 10px; font-weight: 700; color: #475569; @@ -863,14 +862,21 @@ } .llm-param-chip { - border: 1px solid #dbeafe; - background: #eff6ff; - border-radius: 8px; - padding: 6px 8px; + position: relative; + border: 1px solid #e6ecf4; + background: #ffffff; + border-radius: 10px; + padding: 7px 10px; display: flex; flex-direction: column; min-width: 0; overflow: hidden; + transition: border-color 0.15s ease, box-shadow 0.15s ease; +} + +.llm-param-chip:hover { + border-color: #c7d9f2; + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05); } .llm-param-chip-root { @@ -898,29 +904,32 @@ min-width: 0; } +/* Quiet until the field is pointed at, so a form of many fields is not a wall of pencils. */ .llm-edit-btn { border: none; background: transparent; - color: #64748b; - border-radius: 999px; - width: 18px; - height: 18px; + color: #94a3b8; + border-radius: 7px; + width: 22px; + height: 22px; + font-size: 11px; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; flex-shrink: 0; - transition: color 0.15s ease, background-color 0.15s ease, transform 0.15s ease; + opacity: 0; + transition: color 0.15s ease, background-color 0.15s ease, opacity 0.15s ease; +} + +.llm-param-chip:hover .llm-edit-btn, +.llm-edit-btn:focus-visible { + opacity: 1; } .llm-edit-btn:hover { - color: #2563eb; - background: rgba(37, 99, 235, 0.1); - transform: translateY(-1px); -} - -.llm-edit-btn:active { - transform: translateY(0); + color: #1d4ed8; + background: #eff6ff; } .llm-edit-btn:disabled { @@ -933,9 +942,10 @@ position: relative; width: 34px; height: 20px; - border: 1px solid #bfdbfe; + border: 1px solid #dbe2ea; border-radius: 999px; - background: #dbeafe; + background: #eef2f7; + cursor: pointer; transition: background-color 0.15s ease, border-color 0.15s ease; } diff --git a/src/app/shared/nodes/node-parameters/node-parameters.html b/src/app/shared/nodes/node-parameters/node-parameters.html index 2c18cd5..a554636 100644 --- a/src/app/shared/nodes/node-parameters/node-parameters.html +++ b/src/app/shared/nodes/node-parameters/node-parameters.html @@ -62,7 +62,9 @@ } @if (field.type !== 'boolean') { - {{ field.value }} + {{ shownValue(field) }} } } @else { @@ -171,7 +173,9 @@ } @if (field.type !== 'boolean') { - {{ field.value }} + {{ shownValue(field) }} } } @else { @@ -337,7 +341,7 @@ } @else { diff --git a/src/app/shared/nodes/node-parameters/node-parameters.ts b/src/app/shared/nodes/node-parameters/node-parameters.ts index eb06b86..1e4ce68 100644 --- a/src/app/shared/nodes/node-parameters/node-parameters.ts +++ b/src/app/shared/nodes/node-parameters/node-parameters.ts @@ -6,6 +6,7 @@ import { CommonModule } from '@angular/common'; import { Component, effect, ElementRef, Input, viewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { NodeListFieldComponent } from '../node-list-field/node-list-field'; +import { humanizeChoice } from '../node-utility'; /** * What a node shows of its configuration, built from its schema: the same fields whatever kind of @@ -95,6 +96,16 @@ export class NodeParametersComponent { }); } + /** A field's value as shown: a fixed choice in words, anything else as written. */ + shownValue(field: { value: string; choice?: boolean }): string { + return field.choice ? humanizeChoice(field.value) : field.value; + } + + /** An option of a fixed choice in words, when it has no label of its own. */ + optionLabel(option: { value: unknown; label: string }): string { + return option.label === String(option.value) ? humanizeChoice(option.label) : option.label; + } + onSimpleEditorDialogClick(event: MouseEvent) { if (event.target === this.simpleEditorDialog()?.nativeElement) { this.host.closeSimpleParamEditor(event); diff --git a/src/app/shared/nodes/node-utility.spec.ts b/src/app/shared/nodes/node-utility.spec.ts index cb26a6e..bfa7f91 100644 --- a/src/app/shared/nodes/node-utility.spec.ts +++ b/src/app/shared/nodes/node-utility.spec.ts @@ -16,7 +16,8 @@ import { getValueByPath, toStringOrNull, valueToDisplayString, - resolveStepIcon + resolveStepIcon, + humanizeChoice } from './node-utility'; describe('node-utility', () => { @@ -253,3 +254,14 @@ describe('resolveStepIcon', () => { expect(resolveStepIcon({ 'x-ui-icon': 'bi bi-robot' }, false, { visualRole: 'ACTIVITY' }).value).toBe('bi bi-robot'); }); }); + +describe('humanizeChoice', () => { + it('reads a fixed choice as words, and leaves anything else as it was written', () => { + expect(humanizeChoice('PER_EXECUTION')).toBe('Per execution'); + expect(humanizeChoice('CATALOG')).toBe('Catalog'); + expect(humanizeChoice('notes/*.txt')).toBe('notes/*.txt'); + expect(humanizeChoice('gemma:7b')).toBe('gemma:7b'); + expect(humanizeChoice('Already words')).toBe('Already words'); + expect(humanizeChoice('')).toBe(''); + }); +}); diff --git a/src/app/shared/nodes/node-utility.ts b/src/app/shared/nodes/node-utility.ts index 8303d60..fde9c26 100644 --- a/src/app/shared/nodes/node-utility.ts +++ b/src/app/shared/nodes/node-utility.ts @@ -433,6 +433,16 @@ export function resolveStepIcon( return { type: 'class', value: 'bi bi-stars' }; } +/** + * PER_EXECUTION as "Per execution": a fixed choice shown in words. Only values that look like a + * constant are changed - anything else is shown as it was written. + */ +export function humanizeChoice(value: string): string { + if (!/^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$/.test(value)) return value; + const words = value.toLowerCase().split('_').join(' '); + return words.charAt(0).toUpperCase() + words.slice(1); +} + export function splitTemplatedTextParts(text: string | null): TemplatedTextPart[] { if (!text) return []; diff --git a/src/app/shared/nodes/schema-driven-fields.ts b/src/app/shared/nodes/schema-driven-fields.ts index 9324ab6..d4b8078 100644 --- a/src/app/shared/nodes/schema-driven-fields.ts +++ b/src/app/shared/nodes/schema-driven-fields.ts @@ -104,6 +104,8 @@ export type SchemaParameterFieldView = { enabled: boolean; type: TType; booleanValue: boolean; + /** One of a fixed set of values, which a person reads better as words than as a constant. */ + choice?: boolean; }; export type SchemaRichContentFieldView = { @@ -802,7 +804,8 @@ export function buildSchemaFieldViewModel< expandable: isLongTextValue(value), enabled: params.isPathEnabled(definition.path, params.config), type: definition.type, - booleanValue: getValueByPath(params.config, definition.path) === true + booleanValue: getValueByPath(params.config, definition.path) === true, + choice: ((definition as { enumOptions?: unknown[] }).enumOptions?.length ?? 0) > 0 } satisfies SchemaParameterFieldView; }); diff --git a/src/styles.css b/src/styles.css index 448af17..040e66c 100644 --- a/src/styles.css +++ b/src/styles.css @@ -194,44 +194,63 @@ body.node-focus-modal-open { } .llm-modal { - width: min(360px, calc(100vw - 24px)); + width: min(400px, calc(100vw - 24px)); display: flex; flex-direction: column; - gap: 10px; - border: 1px solid #dbe2ea; - border-radius: 12px; + gap: 14px; + border: 1px solid #e6ecf4; + border-radius: 16px; background: #ffffff; - box-shadow: 0 16px 40px rgba(15, 23, 42, 0.25); - padding: 14px; + box-shadow: 0 24px 56px rgba(15, 23, 42, 0.22); + padding: 18px; } .llm-modal-title { - font-size: 13px; - font-weight: 700; + font-size: 15px; + font-weight: 600; color: #0f172a; } .llm-modal-field { display: flex; flex-direction: column; - gap: 4px; + gap: 6px; } .llm-modal-field label { - font-size: 11px; + font-size: 9.5px; font-weight: 600; - color: #475569; + letter-spacing: 0.06em; + text-transform: uppercase; + color: #94a3b8; } .llm-modal-field input, .llm-modal-field select, .llm-modal-field textarea { - border: 1px solid #cbd5e1; - border-radius: 8px; - font-size: 12px; + border: 1px solid #dbe2ea; + border-radius: 10px; + font-size: 13px; color: #0f172a; - padding: 7px 8px; + background: #fbfdff; + padding: 9px 11px; resize: vertical; + transition: border-color 0.15s ease, box-shadow 0.15s ease, background-color 0.15s ease; +} + +.llm-modal-field input:hover, +.llm-modal-field select:hover, +.llm-modal-field textarea:hover { + border-color: #c7d9f2; +} + +.llm-modal-field input:focus, +.llm-modal-field select:focus, +.llm-modal-field textarea:focus { + outline: none; + background: #ffffff; + border-color: #2563eb; + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.14); } /* Why the value cannot be saved, under the input it belongs to. */ @@ -279,24 +298,34 @@ body.node-focus-modal-open { .llm-btn { border: 1px solid transparent; - border-radius: 8px; + border-radius: 9px; cursor: pointer; - font-size: 12px; + font-size: 12.5px; font-weight: 600; - padding: 6px 10px; + padding: 8px 14px; + transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease; } .llm-btn-ghost { - border-color: #cbd5e1; + border-color: #dbe2ea; color: #475569; background: #ffffff; } +.llm-btn-ghost:hover:not(:disabled) { + border-color: #cbd5e1; + background: #f8fafc; +} + .llm-btn-primary { background: #2563eb; color: #ffffff; } +.llm-btn-primary:hover:not(:disabled) { + background: #1d4ed8; +} + .node-toggle { width: fit-content; display: inline-flex; @@ -514,20 +543,43 @@ body.node-focus-modal-open { } .llm-param-key { - color: #64748b; - font-size: 10px; - letter-spacing: 0.03em; + color: #94a3b8; + font-size: 9.5px; + font-weight: 600; + letter-spacing: 0.06em; text-transform: uppercase; } .llm-param-value { + margin-top: 1px; color: #0f172a; - font-size: 11px; + font-size: 12px; + font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +/* One of a fixed set: a word on a tinted plate, so it reads as a choice and not as typed text. */ +.llm-param-value-choice { + align-self: flex-start; + max-width: 100%; + margin-top: 3px; + padding: 1px 8px; + border-radius: 999px; + background: #eef2ff; + color: #3730a3; + font-size: 11px; + font-weight: 600; +} + +/* Nothing set yet: said in words rather than with a bare dash. */ +.llm-param-value-empty { + color: #b6c0cd; + font-weight: 400; + font-style: italic; +} + .llm-array-sections { display: flex; flex-direction: column;