Draw a step's header the way a resource card is drawn
The saturated blue (or amber) header bar gives way to a light one: a tinted badge with the step's icon, its name in dark type with its kind under it in the step's colour, and quiet borderless buttons that colour on hover. The kind of step stays in a stripe on the left, as on a resource card. A step's icon now follows its kind - automated, human, decision, end, branches rejoin - with the pictures the palette already uses for them, instead of one raster image for every automated block. A schema that names an icon still wins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3f4fb97626
commit
b6d61c2b8e
|
|
@ -11,6 +11,8 @@
|
|||
-webkit-user-select: none;
|
||||
border-radius: 16px;
|
||||
border: 1px solid #dbe2ea;
|
||||
/* The kind of step as a stripe, as a resource card has: blue automated, amber human. */
|
||||
border-left: 5px solid #3b82f6;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f9fbff 100%);
|
||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.12);
|
||||
overflow: visible;
|
||||
|
|
@ -187,6 +189,7 @@
|
|||
|
||||
.llm-node--human {
|
||||
border-color: #f7d7a7;
|
||||
border-left-color: #f59e0b;
|
||||
background: linear-gradient(180deg, #fffdf9 0%, #fff7ed 100%);
|
||||
box-shadow: 0 10px 24px rgba(124, 45, 18, 0.14);
|
||||
color: #431407;
|
||||
|
|
@ -212,7 +215,7 @@
|
|||
line-height: 1.1;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.03em;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
color: #94a3b8;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
|
@ -224,10 +227,10 @@
|
|||
gap: 4px;
|
||||
margin-top: 2px;
|
||||
padding: 1px 7px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
background: #f8fafc;
|
||||
color: #475569;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
|
|
@ -235,46 +238,44 @@
|
|||
width: fit-content;
|
||||
}
|
||||
|
||||
/*
|
||||
* A light header, the way a resource card is drawn: a tinted badge with the step's icon, its name
|
||||
* in dark type with its kind under it in the step's colour, and quiet buttons that colour on hover.
|
||||
*/
|
||||
.llm-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid #e8edf4;
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||||
padding: 11px 12px;
|
||||
border-bottom: 1px solid #e8eef6;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f5f9ff 100%);
|
||||
border-top-left-radius: 16px;
|
||||
border-top-right-radius: 16px;
|
||||
}
|
||||
|
||||
.llm-node--human .llm-header {
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #ea580c 100%);
|
||||
border-bottom-color: #fdba74;
|
||||
}
|
||||
|
||||
.llm-node--human .llm-icon {
|
||||
color: #fffbeb;
|
||||
}
|
||||
|
||||
.llm-node--human .llm-title {
|
||||
color: #fffbeb;
|
||||
}
|
||||
|
||||
.llm-node--human .llm-subtitle {
|
||||
color: #fff7ed;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #fff7ed 100%);
|
||||
border-bottom-color: #fde7c7;
|
||||
}
|
||||
|
||||
.llm-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
border: 1px solid rgba(255, 255, 255, 0.28);
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
font-size: 19px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.llm-node--human .llm-icon {
|
||||
background: #ffedd5;
|
||||
color: #c2410c;
|
||||
}
|
||||
|
||||
.llm-icon-image {
|
||||
width: 24px;
|
||||
height: 16px;
|
||||
|
|
@ -282,10 +283,16 @@
|
|||
}
|
||||
|
||||
.llm-header-content {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1.2;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
/* The name first, as on a resource card; the kind of block under it. */
|
||||
.llm-header-content .llm-subtitle-row {
|
||||
order: -1;
|
||||
}
|
||||
|
||||
.llm-subtitle-row {
|
||||
|
|
@ -362,15 +369,23 @@
|
|||
}
|
||||
|
||||
.llm-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
color: #eff6ff;
|
||||
margin-top: 1px;
|
||||
font-size: 11.5px;
|
||||
font-weight: 500;
|
||||
color: #1d4ed8;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.llm-node--human .llm-title {
|
||||
color: #c2410c;
|
||||
}
|
||||
|
||||
.llm-subtitle {
|
||||
font-size: 11px;
|
||||
color: #dbeafe;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
|
@ -378,21 +393,28 @@
|
|||
|
||||
.llm-name-edit-btn {
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #eff6ff;
|
||||
border-radius: 999px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
border-radius: 6px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 11px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: background-color 0.15s ease;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease, background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.llm-node:hover .llm-name-edit-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.llm-name-edit-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.llm-warning-wrap {
|
||||
|
|
@ -400,37 +422,38 @@
|
|||
margin-left: 0;
|
||||
}
|
||||
|
||||
.llm-delete-btn {
|
||||
position: relative;
|
||||
z-index: 146;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(185, 28, 28, 0.45);
|
||||
background: rgba(127, 29, 29, 0.2);
|
||||
color: #fff1f2;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.llm-delete-btn,
|
||||
.llm-clone-btn {
|
||||
position: relative;
|
||||
z-index: 146;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(191, 219, 254, 0.7);
|
||||
background: rgba(30, 64, 175, 0.18);
|
||||
color: #eff6ff;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.llm-clone-btn:hover {
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.llm-node--human .llm-clone-btn:hover {
|
||||
background: #fff7ed;
|
||||
color: #c2410c;
|
||||
}
|
||||
|
||||
.llm-delete-btn:hover {
|
||||
background: rgba(127, 29, 29, 0.35);
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.llm-warning-tooltip {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import { findCapability, isModelParameterField, providerAppliesParameter } from
|
|||
import { firstValueFrom, take } from 'rxjs';
|
||||
import { SWIMLANES_ENABLED } from '@shared/feature-flags';
|
||||
import { ConditionalRequiredField, extractSchemaRequirements, SchemaRequirements } from '../schema-requirements';
|
||||
import { evaluateUiConditionRule, flattenPrimitiveValues, readEffectiveUiVisibleConditionRule, formatNodeTitle, getOutputPillClass, getOutputsTitle, getValueByPath, isConditionalByPorts, isHumanInteractiveNode, orderedSchemaPropertyEntries, parentGroupLabel, parentPath, pathToLabel, readUiConditionRule, resolveNodeIcon, resolveSchemaPath, resolveSchemaRef, schemaFieldDescription, schemaFieldLabel, shouldSkipSchemaField, splitTemplatedTextParts, toStringOrNull, type UiConditionRule, validateUniqueByConstraint, valueToDisplayString } from '../node-utility';
|
||||
import { evaluateUiConditionRule, flattenPrimitiveValues, readEffectiveUiVisibleConditionRule, formatNodeTitle, getOutputPillClass, getOutputsTitle, getValueByPath, isConditionalByPorts, isHumanInteractiveNode, orderedSchemaPropertyEntries, parentGroupLabel, parentPath, pathToLabel, readUiConditionRule, resolveSchemaPath, resolveStepIcon, resolveSchemaRef, schemaFieldDescription, schemaFieldLabel, shouldSkipSchemaField, splitTemplatedTextParts, toStringOrNull, type UiConditionRule, validateUniqueByConstraint, valueToDisplayString } from '../node-utility';
|
||||
import {
|
||||
buildSchemaEditableFieldDefinitions,
|
||||
buildSchemaObjectDialog,
|
||||
|
|
@ -628,7 +628,7 @@ export class GenericNodeComponent implements OnDestroy {
|
|||
}
|
||||
|
||||
nodeIcon(): { type: 'class' | 'img'; value: string } {
|
||||
return resolveNodeIcon(this.blockDescriptor?.schema, this.isHumanNode());
|
||||
return resolveStepIcon(this.blockDescriptor?.schema, this.isHumanNode(), this.blockDescriptor?.capabilities);
|
||||
}
|
||||
|
||||
hasExecutionDependencyPorts(): boolean {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ import {
|
|||
splitTemplatedTextParts,
|
||||
getValueByPath,
|
||||
toStringOrNull,
|
||||
valueToDisplayString
|
||||
valueToDisplayString,
|
||||
resolveStepIcon
|
||||
} from './node-utility';
|
||||
|
||||
describe('node-utility', () => {
|
||||
|
|
@ -240,3 +241,15 @@ describe('node-utility', () => {
|
|||
expect(parentGroupLabel('')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveStepIcon', () => {
|
||||
it('pictures a step by its kind, as the palette sorts blocks, unless its schema names an icon', () => {
|
||||
expect(resolveStepIcon(null, false, { visualRole: 'ACTIVITY' }).value).toBe('bi bi-stars');
|
||||
expect(resolveStepIcon(null, true, { visualRole: 'ACTIVITY' }).value).toBe('bi bi-hand-index');
|
||||
expect(resolveStepIcon(null, true, { visualRole: 'DECISION', routesExclusively: true }).value).toBe('bi bi-signpost-split');
|
||||
expect(resolveStepIcon(null, false, { visualRole: 'DECISION', routesExclusively: true }).value).toBe('bi bi-signpost-split');
|
||||
expect(resolveStepIcon(null, false, { visualRole: 'END', terminal: true }).value).toBe('bi bi-flag');
|
||||
expect(resolveStepIcon(null, false, { visualRole: 'MERGE' }).value).toBe('bi bi-sign-merge-left');
|
||||
expect(resolveStepIcon({ 'x-ui-icon': 'bi bi-robot' }, false, { visualRole: 'ACTIVITY' }).value).toBe('bi bi-robot');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -412,6 +412,27 @@ export function resolveNodeIcon(schema: Record<string, unknown> | null | undefin
|
|||
return { type: 'img', value: 'llm_node.png' };
|
||||
}
|
||||
|
||||
/**
|
||||
* A step's icon on the canvas, as a Bootstrap glyph: the one its schema declares, or else one for
|
||||
* what kind of step it is - the same kinds, and the same pictures, the palette sorts blocks by.
|
||||
*/
|
||||
export function resolveStepIcon(
|
||||
schema: Record<string, unknown> | null | undefined,
|
||||
isHuman: boolean,
|
||||
capabilities: { visualRole?: string; terminal?: boolean; routesExclusively?: boolean } | null | undefined
|
||||
): { type: 'class'; value: string } | { type: 'img'; value: string } {
|
||||
const declared = schema?.['x-ui-icon'];
|
||||
if (typeof declared === 'string' && declared.trim().length > 0) {
|
||||
return resolveNodeIcon(schema, isHuman);
|
||||
}
|
||||
const decides = capabilities?.visualRole === 'DECISION' || capabilities?.routesExclusively === true;
|
||||
if (isHuman) return { type: 'class', value: decides ? 'bi bi-signpost-split' : 'bi bi-hand-index' };
|
||||
if (capabilities?.visualRole === 'END' || capabilities?.terminal === true) return { type: 'class', value: 'bi bi-flag' };
|
||||
if (decides) return { type: 'class', value: 'bi bi-signpost-split' };
|
||||
if (capabilities?.visualRole === 'MERGE') return { type: 'class', value: 'bi bi-sign-merge-left' };
|
||||
return { type: 'class', value: 'bi bi-stars' };
|
||||
}
|
||||
|
||||
export function splitTemplatedTextParts(text: string | null): TemplatedTextPart[] {
|
||||
if (!text) return [];
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue