Skip to content

Commit 1d7016b

Browse files
Merge pull request #15 from CodeSignal/codex/ui-final-polish
feat(web): final UI polish for run controls and node connectors
2 parents ff75502 + 50bf8b7 commit 1d7016b

3 files changed

Lines changed: 243 additions & 35 deletions

File tree

apps/web/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ <h2>Run Console</h2>
8989
type="button"
9090
aria-label="Cancel run"
9191
>
92-
<span class="icon icon-rectangle-2698 icon-small" aria-hidden="true"></span>
92+
<span class="icon icon-theme-light-state-open icon-small" aria-hidden="true"></span>
9393
</button>
9494
</div>
9595
</div>

apps/web/src/app/workflow-editor.ts

Lines changed: 90 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ const SUBAGENT_HANDLE = 'subagent';
2323
const SUBAGENT_TARGET_HANDLE = 'subagent-target';
2424
const IF_PORT_BASE_TOP = 45;
2525
const IF_PORT_STEP = 30;
26-
const IF_COLLAPSED_MULTI_CONDITION_PORT_TOP = 18;
27-
const IF_COLLAPSED_MULTI_FALLBACK_PORT_TOP = 45;
2826
const SUBAGENT_PORT_MIN_TOP = 42;
27+
const DEFAULT_HEADER_CENTER_Y = 24;
28+
const DEFAULT_SECONDARY_CENTER_Y = 81;
29+
const PORT_RADIUS = 6;
30+
const AGGREGATE_PORT_RADIUS = 8;
2931
const PREVIOUS_OUTPUT_TEMPLATE = '{{PREVIOUS_OUTPUT}}';
3032
const GENERIC_AGENT_SPINNER_KEY = '__generic_agent_spinner__';
3133
const IF_CONDITION_OPERATORS = [
@@ -650,7 +652,7 @@ export class WorkflowEditor {
650652
}
651653
return {
652654
x: targetNode.x,
653-
y: targetNode.y + 24
655+
y: targetNode.y + this.getNodeHeaderCenterYOffset(targetNode)
654656
};
655657
}
656658

@@ -852,7 +854,7 @@ export class WorkflowEditor {
852854

853855
getIfConditionPortTop(node: EditorNode, index: number): number {
854856
if (node.data?.collapsed) {
855-
return this.getIfPortTop(index);
857+
return this.getNodeHeaderPortTop(node);
856858
}
857859

858860
const nodeEl = document.getElementById(node.id);
@@ -872,7 +874,7 @@ export class WorkflowEditor {
872874
getIfFallbackPortTop(node: EditorNode): number {
873875
const conditions = this.getIfConditions(node);
874876
if (node.data?.collapsed) {
875-
return this.getIfPortTop(conditions.length);
877+
return this.getNodeSecondaryPortTop(node);
876878
}
877879

878880
const nodeEl = document.getElementById(node.id);
@@ -959,11 +961,11 @@ export class WorkflowEditor {
959961
if (node.type === 'if') {
960962
if (this.shouldAggregateCollapsedIfPorts(node)) {
961963
if (sourceHandle === IF_FALLBACK_HANDLE) {
962-
return IF_COLLAPSED_MULTI_FALLBACK_PORT_TOP + 6;
964+
return this.getNodeSecondaryCenterYOffset(node);
963965
}
964966
const conditionIndex = this.getIfConditionIndexFromHandle(sourceHandle);
965967
if (conditionIndex !== null) {
966-
return IF_COLLAPSED_MULTI_CONDITION_PORT_TOP + 6;
968+
return this.getNodeHeaderCenterYOffset(node);
967969
}
968970
}
969971
if (sourceHandle === IF_FALLBACK_HANDLE) {
@@ -975,9 +977,35 @@ export class WorkflowEditor {
975977
}
976978
}
977979

978-
if (sourceHandle === 'approve') return 51;
979-
if (sourceHandle === 'reject') return 81;
980-
return 24;
980+
if (sourceHandle === 'approve') return this.getNodeHeaderCenterYOffset(node);
981+
if (sourceHandle === 'reject') return this.getNodeSecondaryCenterYOffset(node);
982+
return this.getNodeHeaderCenterYOffset(node);
983+
}
984+
985+
getNodeHeaderCenterYOffset(node: EditorNode): number {
986+
const nodeEl = document.getElementById(node.id);
987+
const headerEl = nodeEl?.querySelector('.node-header');
988+
if (!(headerEl instanceof HTMLElement)) return DEFAULT_HEADER_CENTER_Y;
989+
return Math.round(headerEl.offsetTop + (headerEl.offsetHeight / 2));
990+
}
991+
992+
getNodeHeaderPortTop(node: EditorNode): number {
993+
return this.getNodeHeaderCenterYOffset(node) - PORT_RADIUS;
994+
}
995+
996+
getNodeSecondaryCenterYOffset(node: EditorNode): number {
997+
const nodeEl = document.getElementById(node.id);
998+
const headerEl = nodeEl?.querySelector('.node-header');
999+
if (!(nodeEl instanceof HTMLElement) || !(headerEl instanceof HTMLElement)) {
1000+
return DEFAULT_SECONDARY_CENTER_Y;
1001+
}
1002+
const bodyTop = headerEl.offsetTop + headerEl.offsetHeight;
1003+
const bodyHeight = Math.max(nodeEl.offsetHeight - bodyTop, PORT_RADIUS * 2);
1004+
return Math.round(bodyTop + (bodyHeight / 2));
1005+
}
1006+
1007+
getNodeSecondaryPortTop(node: EditorNode): number {
1008+
return this.getNodeSecondaryCenterYOffset(node) - PORT_RADIUS;
9811009
}
9821010

9831011
setWorkflowState(state: WorkflowState): void {
@@ -1003,6 +1031,15 @@ export class WorkflowEditor {
10031031
}
10041032
}
10051033

1034+
setCancelRunButtonHint(reason: string | null): void {
1035+
if (!this.cancelRunButton) return;
1036+
if (reason) {
1037+
this.cancelRunButton.setAttribute('data-tooltip', reason);
1038+
} else {
1039+
this.cancelRunButton.removeAttribute('data-tooltip');
1040+
}
1041+
}
1042+
10061043
setCanvasValidationMessage(message: string | null): void {
10071044
if (this.canvasValidationTimeout !== null) {
10081045
clearTimeout(this.canvasValidationTimeout);
@@ -1138,6 +1175,7 @@ export class WorkflowEditor {
11381175
const showCancel = this.workflowState === 'running';
11391176
this.cancelRunButton.style.display = showCancel ? 'inline-flex' : 'none';
11401177
this.cancelRunButton.disabled = !showCancel;
1178+
this.setCancelRunButtonHint(showCancel ? 'Cancel workflow' : null);
11411179
}
11421180

11431181
if (this.clearButton) {
@@ -2209,14 +2247,14 @@ export class WorkflowEditor {
22092247

22102248
} else if (node.type === 'approval') {
22112249
container.appendChild(buildLabel('Approval Message'));
2212-
const pInput = document.createElement('input');
2213-
pInput.type = 'text';
2214-
pInput.className = 'input';
2250+
const pInput = document.createElement('textarea');
2251+
pInput.className = 'input textarea-input';
2252+
pInput.rows = 4;
22152253
pInput.value = data.prompt || '';
22162254
pInput.placeholder = 'Message shown to user when approval is required';
22172255
pInput.addEventListener('input', (e: any) => {
22182256
data.prompt = e.target.value;
2219-
this.scheduleSave();
2257+
this.updatePreview(node);
22202258
});
22212259
container.appendChild(pInput);
22222260

@@ -2269,52 +2307,49 @@ export class WorkflowEditor {
22692307
node.id,
22702308
SUBAGENT_TARGET_HANDLE,
22712309
'port-subagent-target',
2272-
'Subagent target'
2310+
'Set as subagent'
22732311
)
22742312
);
22752313
}
22762314

22772315
if (node.type !== 'start') {
2278-
const portIn = this.createPort(node.id, 'input', 'port-in');
2316+
const inputTooltip = node.type === 'end' ? 'End input' : 'Input';
2317+
const portIn = this.createPort(node.id, 'input', 'port-in', inputTooltip, this.getNodeHeaderPortTop(node));
22792318
el.appendChild(portIn);
22802319
}
22812320

22822321
if (node.type !== 'end') {
22832322
if (node.type === 'if') {
22842323
const conditions = this.getIfConditions(node);
22852324
if (this.shouldAggregateCollapsedIfPorts(node)) {
2286-
const title = `${conditions.length} condition branches (expand to wire specific branches)`;
22872325
const aggregateConditionPort = this.createPort(
22882326
node.id,
22892327
this.getIfConditionHandle(0),
22902328
'port-out port-condition port-condition-aggregate',
2291-
title,
2292-
IF_COLLAPSED_MULTI_CONDITION_PORT_TOP,
2329+
'Expand to connect',
2330+
this.getNodeHeaderCenterYOffset(node) - AGGREGATE_PORT_RADIUS,
22932331
false
22942332
);
22952333
aggregateConditionPort.textContent = String(conditions.length);
2296-
aggregateConditionPort.setAttribute('aria-label', `${conditions.length} conditions`);
2334+
aggregateConditionPort.setAttribute('aria-label', 'Expand to connect');
22972335
el.appendChild(aggregateConditionPort);
22982336
el.appendChild(
22992337
this.createPort(
23002338
node.id,
23012339
IF_FALLBACK_HANDLE,
23022340
'port-out port-condition-fallback',
2303-
'False fallback',
2304-
IF_COLLAPSED_MULTI_FALLBACK_PORT_TOP
2341+
'Fallback path',
2342+
this.getNodeSecondaryPortTop(node)
23052343
)
23062344
);
23072345
} else {
2308-
conditions.forEach((condition: any, index: any) => {
2309-
const operatorLabel = condition.operator === 'contains' ? 'Contains' : 'Equal';
2310-
const conditionValue = condition.value || '';
2311-
const title = `Condition ${index + 1}: ${operatorLabel} "${conditionValue}"`;
2346+
conditions.forEach((_condition: any, index: any) => {
23122347
el.appendChild(
23132348
this.createPort(
23142349
node.id,
23152350
this.getIfConditionHandle(index),
23162351
'port-out port-condition',
2317-
title,
2352+
`Condition ${index + 1}`,
23182353
this.getIfConditionPortTop(node, index)
23192354
)
23202355
);
@@ -2324,28 +2359,45 @@ export class WorkflowEditor {
23242359
node.id,
23252360
IF_FALLBACK_HANDLE,
23262361
'port-out port-condition-fallback',
2327-
'False fallback',
2362+
'Fallback path',
23282363
this.getIfFallbackPortTop(node)
23292364
)
23302365
);
23312366
}
23322367
} else if (node.type === 'agent') {
2333-
el.appendChild(this.createPort(node.id, 'output', 'port-out'));
2368+
el.appendChild(this.createPort(node.id, 'output', 'port-out', 'Output', this.getNodeHeaderPortTop(node)));
23342369
if (node.data?.tools?.subagents) {
23352370
el.appendChild(
23362371
this.createPort(
23372372
node.id,
23382373
SUBAGENT_HANDLE,
23392374
'port-subagent',
2340-
'Subagent'
2375+
'Add subagent'
23412376
)
23422377
);
23432378
}
23442379
} else if (node.type === 'approval') {
2345-
el.appendChild(this.createPort(node.id, 'approve', 'port-out port-true', 'Approve'));
2346-
el.appendChild(this.createPort(node.id, 'reject', 'port-out port-false', 'Reject'));
2380+
el.appendChild(
2381+
this.createPort(
2382+
node.id,
2383+
'approve',
2384+
'port-out port-true',
2385+
'Approve path',
2386+
this.getNodeHeaderPortTop(node)
2387+
)
2388+
);
2389+
el.appendChild(
2390+
this.createPort(
2391+
node.id,
2392+
'reject',
2393+
'port-out port-false',
2394+
'Reject path',
2395+
this.getNodeSecondaryPortTop(node)
2396+
)
2397+
);
23472398
} else {
2348-
el.appendChild(this.createPort(node.id, 'output', 'port-out'));
2399+
const outputTooltip = node.type === 'start' ? 'Next step' : 'Output';
2400+
el.appendChild(this.createPort(node.id, 'output', 'port-out', outputTooltip, this.getNodeHeaderPortTop(node)));
23492401
}
23502402
}
23512403
}
@@ -2360,7 +2412,11 @@ export class WorkflowEditor {
23602412
): HTMLDivElement {
23612413
const port = document.createElement('div');
23622414
port.className = `port ${className}${connectable ? '' : ' port-disabled'}`;
2363-
if (title) port.title = title;
2415+
if (title) {
2416+
port.title = title;
2417+
port.setAttribute('data-tooltip', title);
2418+
port.setAttribute('aria-label', title);
2419+
}
23642420
if (typeof top === 'number') {
23652421
port.style.top = `${top}px`;
23662422
}

0 commit comments

Comments
 (0)