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) {
-
}
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) {
-
}
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;