From 95ea0131392d9b49ae95aedba94ce64a948bfe19 Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Tue, 29 Sep 2026 11:41:57 +0200 Subject: [PATCH] ui: make resource links visible and interactive --- src/app/shared/custom-socket/custom-socket.ts | 12 +++-- .../nodes/generic-node/generic-node.css | 47 ++++++++++++++++++- .../nodes/generic-node/generic-node.html | 18 ++++--- .../shared/nodes/generic-node/generic-node.ts | 8 ++++ .../nodes/resource-node/resource-node.css | 4 +- .../nodes/task-step-node/task-step-node.css | 43 ++++++++++++++++- .../nodes/task-step-node/task-step-node.html | 17 +++++-- .../nodes/task-step-node/task-step-node.ts | 7 +++ src/app/utilities/rete-editor.ts | 35 ++++++++++++++ 9 files changed, 171 insertions(+), 20 deletions(-) diff --git a/src/app/shared/custom-socket/custom-socket.ts b/src/app/shared/custom-socket/custom-socket.ts index 2170162..0358a9c 100644 --- a/src/app/shared/custom-socket/custom-socket.ts +++ b/src/app/shared/custom-socket/custom-socket.ts @@ -33,7 +33,7 @@ export class CustomSocket implements AfterViewInit, OnDestroy { @HostBinding("style.width") get width() { - return this.kind === "resource" ? "9px" : "8px"; + return this.kind === "resource" ? "7px" : "8px"; } @HostBinding("style.height") @@ -42,7 +42,9 @@ export class CustomSocket implements AfterViewInit, OnDestroy { } @HostBinding("style.display") d = "block"; - @HostBinding("style.borderRadius") br = "999px"; + @HostBinding("style.borderRadius") get borderRadius() { + return this.kind === "resource" ? "4px" : "999px"; + } @HostBinding("style.border") border = "1px solid rgba(255,255,255,0.9)"; @HostBinding("style.cursor") get cursor() { return this.data?.__readonly === true ? "default" : "crosshair"; @@ -55,7 +57,7 @@ export class CustomSocket implements AfterViewInit, OnDestroy { @HostBinding("style.background") get bg() { if (this.kind === "resource") { - return "linear-gradient(145deg, #2dd4bf 0%, #0f766e 100%)"; + return "linear-gradient(180deg, #5eead4 0%, #0f766e 100%)"; } if (this.kind === "dependency") { return this.side === "input" @@ -70,7 +72,7 @@ export class CustomSocket implements AfterViewInit, OnDestroy { @HostBinding("style.boxShadow") get sh() { if (this.kind === "resource") { - return "0 2px 6px rgba(15,118,110,0.45), 0 0 0 1px rgba(15,118,110,0.45)"; + return "0 1px 4px rgba(15,118,110,0.32), 0 0 0 1px rgba(13,148,136,0.42)"; } if (this.kind === "dependency") { const c = this.side === "input" ? "rgba(217,119,6,0.45)" : "rgba(37,99,235,0.45)"; @@ -86,7 +88,7 @@ export class CustomSocket implements AfterViewInit, OnDestroy { } private resolveSocketKind(): "data" | "dependency" | "resource" { - const kind = this.data?.__hfKind; + const kind = this.data?.__hfKind ?? this.data?.payload?.__hfKind; return kind === "dependency" || kind === "resource" ? kind : "data"; } diff --git a/src/app/shared/nodes/generic-node/generic-node.css b/src/app/shared/nodes/generic-node/generic-node.css index 39b6c45..4863ea5 100644 --- a/src/app/shared/nodes/generic-node/generic-node.css +++ b/src/app/shared/nodes/generic-node/generic-node.css @@ -595,6 +595,32 @@ gap: 0; } +.llm-body-resource .llm-column { + grid-column: 1 / -1; +} + +/* The Resource pill is the connection target itself, rather than having a separate tiny handle. */ +.llm-row-resource { + position: relative; +} + +.llm-row-resource .llm-pill-resource { + position: absolute; + inset: 0; + left: 0; + right: 0; + z-index: 21; +} + +.llm-row-resource .llm-pill-resource ::ng-deep > * { + position: absolute !important; + inset: 0; + width: 100% !important; + height: 100% !important; + margin: 0 !important; + opacity: 0; +} + .llm-socket { position: relative; left: -16px; @@ -1024,7 +1050,26 @@ } .llm-pill-resource { + width: 100%; + justify-content: center; + text-align: center; border-color: #5eead4; - background: #f0fdfa; + background: linear-gradient(135deg, #f0fdfa 0%, #ccfbf1 100%); color: #0f766e; + font-family: var(--font-sans); + font-size: 10px; + font-weight: 800; + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.llm-pill-resource--connected { + border-color: #14b8a6; + background: linear-gradient(135deg, #ccfbf1 0%, #99f6e4 100%); + box-shadow: inset 0 0 0 1px rgba(13, 148, 136, 0.14); +} + +.llm-pill-resource--connected .bi { + font-size: 14px; + line-height: 1; } diff --git a/src/app/shared/nodes/generic-node/generic-node.html b/src/app/shared/nodes/generic-node/generic-node.html index 8cac08d..5ec73b6 100644 --- a/src/app/shared/nodes/generic-node/generic-node.html +++ b/src/app/shared/nodes/generic-node/generic-node.html @@ -213,12 +213,13 @@ @if (resourceLinkInput) {
-
Resource
@if (resourceLinkInput; as resourceLinkInput) { -
-
+ -
- Works on + @if (resourceLinkIsConnected) { + + Resource connected + } @else { + Resource + } +
}
diff --git a/src/app/shared/nodes/generic-node/generic-node.ts b/src/app/shared/nodes/generic-node/generic-node.ts index 2de0603..e252f31 100644 --- a/src/app/shared/nodes/generic-node/generic-node.ts +++ b/src/app/shared/nodes/generic-node/generic-node.ts @@ -273,6 +273,14 @@ export class GenericNodeComponent implements OnDestroy { return input ? { key: FLOW_RESOURCE_LINK_PORT_KEY, socket: (input as any).socket as ClassicPreset.Socket } : null; } + /** A resource link is single-valued; the canvas stores the selected resource in this field. */ + get resourceLinkIsConnected(): boolean { + const nodeData = this.data?.data as Record | undefined; + const field = nodeData?.['__resourceRefField']; + const value = typeof field === 'string' ? nodeData?.['specificConfiguration']?.[field] : null; + return typeof value === 'string' && value.trim().length > 0; + } + get dependantOutput(): RenderedSocketPort | null { const output = this.data?.outputs?.[FLOW_DEPENDANT_PORT_KEY]; return output diff --git a/src/app/shared/nodes/resource-node/resource-node.css b/src/app/shared/nodes/resource-node/resource-node.css index 8b2cee2..147ebbd 100644 --- a/src/app/shared/nodes/resource-node/resource-node.css +++ b/src/app/shared/nodes/resource-node/resource-node.css @@ -284,9 +284,9 @@ /* The socket sizes itself as a square; here it is stretched into the bar. */ .resource-card__socket ::ng-deep > * { - width: 9px !important; + width: 7px !important; height: 100% !important; - border-radius: 999px !important; + border-radius: 4px !important; } .llm-spinner { diff --git a/src/app/shared/nodes/task-step-node/task-step-node.css b/src/app/shared/nodes/task-step-node/task-step-node.css index df22ac1..152f6be 100644 --- a/src/app/shared/nodes/task-step-node/task-step-node.css +++ b/src/app/shared/nodes/task-step-node/task-step-node.css @@ -504,6 +504,28 @@ button.llm-node-bias-summary:focus-visible { gap: 0; } +/* A storage Resource is selected through the whole labelled pill, not a separate handle. */ +.llm-row-resource { + position: relative; +} + +.llm-row-resource .llm-pill-resource { + position: absolute; + inset: 0; + left: 0; + right: 0; + z-index: 21; +} + +.llm-row-resource .llm-pill-resource ::ng-deep > * { + position: absolute !important; + inset: 0; + width: 100% !important; + height: 100% !important; + margin: 0 !important; + opacity: 0; +} + .llm-socket { position: relative; left: -16px; @@ -846,7 +868,26 @@ button.llm-node-bias-summary:focus-visible { } .llm-pill-resource { + width: 100%; + justify-content: center; + text-align: center; border-color: #5eead4; - background: #f0fdfa; + background: linear-gradient(135deg, #f0fdfa 0%, #ccfbf1 100%); color: #0f766e; + font-family: var(--font-sans); + font-size: 10px; + font-weight: 800; + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.llm-pill-resource--connected { + border-color: #14b8a6; + background: linear-gradient(135deg, #ccfbf1 0%, #99f6e4 100%); + box-shadow: inset 0 0 0 1px rgba(13, 148, 136, 0.14); +} + +.llm-pill-resource--connected .bi { + font-size: 14px; + line-height: 1; } diff --git a/src/app/shared/nodes/task-step-node/task-step-node.html b/src/app/shared/nodes/task-step-node/task-step-node.html index 9216e46..ed6f727 100644 --- a/src/app/shared/nodes/task-step-node/task-step-node.html +++ b/src/app/shared/nodes/task-step-node/task-step-node.html @@ -254,10 +254,12 @@
Storage
@if (storageLinkInput; as storageLinkInput) { -
-
+ -
- Works on + @if (storageLinkIsConnected) { + + Resource connected + } @else { + Resource + } +
}
diff --git a/src/app/shared/nodes/task-step-node/task-step-node.ts b/src/app/shared/nodes/task-step-node/task-step-node.ts index ab1905c..e176ce9 100644 --- a/src/app/shared/nodes/task-step-node/task-step-node.ts +++ b/src/app/shared/nodes/task-step-node/task-step-node.ts @@ -174,6 +174,13 @@ export class TaskStepNodeComponent { return input ? { key: FLOW_RESOURCE_LINK_PORT_KEY, socket: (input as any).socket as ClassicPreset.Socket } : null; } + get storageLinkIsConnected(): boolean { + const nodeData = this.data?.data as Record | undefined; + const field = nodeData?.['__resourceRefField']; + const value = typeof field === 'string' ? nodeData?.['specificConfiguration']?.[field] : null; + return typeof value === 'string' && value.trim().length > 0; + } + get dependantOutput(): { key: string; socket: ClassicPreset.Socket } | null { const output = this.data?.outputs?.[FLOW_DEPENDANT_PORT_KEY]; return output diff --git a/src/app/utilities/rete-editor.ts b/src/app/utilities/rete-editor.ts index 709bcfd..250b962 100644 --- a/src/app/utilities/rete-editor.ts +++ b/src/app/utilities/rete-editor.ts @@ -215,6 +215,13 @@ export async function createEditor( return context; }); + editor.addPipe(async (context) => { + if (context.type === "connectioncreated" || context.type === "connectionremoved") { + await syncResourceLinkPresentation(editor, area, runtime, context.data); + } + return context; + }); + area.addPipe((context: any) => { if (context?.type === 'nodetranslated') { // Any node moved can be one the way back has to clear, not only one of its two ends. @@ -838,6 +845,34 @@ async function replaceResourceLink(editor: NodeEditor, stepNodeId: st } } +/** Keep a node's visible Resource state in step with the one allowed resource connection. */ +async function syncResourceLinkPresentation( + editor: NodeEditor, + area: AreaPlugin, + runtime: ReteRuntimeContext, + connection: Pick, 'source' | 'sourceOutput' | 'target' | 'targetInput'> +) { + if (getGraphConnectionKind(connection.sourceOutput, connection.targetInput) !== 'resource') return; + + const target = editor.getNode(connection.target) as HFNode | undefined; + const reference = resourceRefOf(runtime, target); + if (!target?.data || !reference) return; + + const activeLink = editor.getConnections().find((candidate) => + candidate.target === target.id && candidate.targetInput === FLOW_RESOURCE_LINK_PORT_KEY); + const source = activeLink ? editor.getNode(activeLink.source) as HFNode | undefined : undefined; + const resourceId = typeof source?.data?.id === 'string' ? source.data.id : null; + const targetData = target.data as Record; + const configuration = targetData['specificConfiguration'] as Record | undefined; + if (configuration?.[reference.field] === resourceId) return; + + targetData['specificConfiguration'] = { + ...(configuration ?? {}), + [reference.field]: resourceId + }; + await area.update('node', target.id); +} + async function withRestoredConnections(runtime: ReteRuntimeContext | undefined, restore: () => Promise): Promise { if (!runtime) return restore(); runtime.restoringConnections = (runtime.restoringConnections ?? 0) + 1;