Asset nodes

This commit is contained in:
Diego Imbert
2025-07-01 14:11:58 +02:00
parent c44e49ddbb
commit 2b9a397236
4 changed files with 113 additions and 9 deletions
@@ -143,7 +143,6 @@
let flowStateStore = $derived(flowEditorContext?.flowStateStore)
let assets = $derived(id ? $flowStateStore?.[id]?.assetsCache : undefined)
let containsSelectedAsset = $derived(assets?.some((a) => assetEq(a, selectedAssetStore?.val)))
let stepHistoryLoader = getStepHistoryLoaderContext()
@@ -276,8 +275,7 @@
class={classNames(
'w-full module flex rounded-sm cursor-pointer max-w-full ',
'flex relative',
deletable ? aiModuleActionToBgColor(action) : '',
containsSelectedAsset ? '!bg-surface-hover' : ''
deletable ? aiModuleActionToBgColor(action) : ''
)}
style="width: 275px; height: 34px; background-color: {hover && bgHoverColor
? bgHoverColor
@@ -292,8 +290,7 @@
<div
class={classNames(
'absolute rounded-sm outline-offset-0 outline-slate-500 dark:outline-gray-400',
selected || containsSelectedAsset ? 'outline outline-2' : 'active:outline active:outline-2',
containsSelectedAsset ? 'outline-slate-600 dark:outline-gray-300' : ''
selected ? 'outline outline-2' : 'active:outline active:outline-2'
)}
style={`width: 275px; height: ${outputPickerVisible ? (outputPickerBarOpen ? '51px' : '35px') : '34px'};`}
></div>
@@ -17,6 +17,7 @@
import {
graphBuilder,
isTriggerStep,
type AssetN,
type InlineScript,
type InsertKind,
type NodeLayout,
@@ -50,11 +51,18 @@
import SubflowBound from './renderers/nodes/SubflowBound.svelte'
import { deepEqual } from 'fast-equals'
import ViewportResizer from './ViewportResizer.svelte'
import type { FlowEditorContext } from '../flows/types'
import AssetNode from './renderers/nodes/AssetNode.svelte'
import { formatAsset } from '../assets/lib'
let useDataflow: Writable<boolean | undefined> = writable<boolean | undefined>(false)
const triggerContext = getContext<TriggerContext>('TriggerContext')
const flowEditorContextOpt: FlowEditorContext | undefined =
getContext<FlowEditorContext>('FlowEditorContext')
const flowStateStoreOpt = flowEditorContextOpt.flowStateStore
let fullWidth = 0
let width = $state(0)
@@ -220,9 +228,11 @@
.decross(nodes.length > 20 ? decrossTwoLayer() : decrossOpt())
.coord(coordCenter())
.nodeSize((d) => {
const id: string | undefined = d?.data?.['id'] ?? ''
const assetOffset = $flowStateStoreOpt?.[id]?.assetsCache?.length ? 100 : 0
return [
(nodeWidths[d?.data?.['id'] ?? ''] ?? 1) * (NODE.width + NODE.gap.horizontal * 1),
NODE.height + NODE.gap.vertical
(nodeWidths[id] ?? 1) * (NODE.width + NODE.gap.horizontal * 1),
NODE.height + NODE.gap.vertical + assetOffset
] as readonly [number, number]
})
boxSize = layout(dag as any)
@@ -256,6 +266,55 @@
return newNodes
}
function computeAssetNodes(nodes: Node[]) {
const ASSET_X_GAP = 20
const ASSET_WIDTH = 180
const ASSET_Y_OFFSET = 55
return nodes.flatMap((node) => {
const assets = $flowStateStoreOpt?.[node.id]?.assetsCache
const assetNodes = assets?.map(
(asset, assetIdx) =>
({
id: `${node.id}-asset-${formatAsset(asset)}`,
type: 'asset',
data: { asset },
position: {
x:
(ASSET_WIDTH + ASSET_X_GAP) * (assetIdx - assets.length / 2) +
(NODE.width + ASSET_X_GAP) / 2,
y: ASSET_Y_OFFSET
},
parentId: node.id,
width: ASSET_WIDTH
}) satisfies Node & AssetN
)
return assetNodes ?? []
})
}
function computeAssetEdges(edges: Edge[], assetNodes: (Node & AssetN)[]): Edge[] {
return assetNodes.map(
(n) =>
({
id: `${n.id}-edge`,
source: n.parentId ?? '',
target: n.id,
type: 'empty',
data: {
insertable: false,
sourceId: n.id,
targetId: n.parentId,
moving: moving,
eventHandlers: eventHandler,
index: 0,
enableTrigger: false,
disableAi: disableAi,
disableMoveIds: []
}
}) satisfies Edge
)
}
let eventHandler = {
deleteBranch: (detail, label) => {
$selectedId = label
@@ -344,7 +403,9 @@
let newGraph = graph
nodes = layoutNodes(newGraph.nodes)
edges = newGraph.edges
const assetNodes = computeAssetNodes(nodes)
nodes = [...nodes, ...assetNodes]
edges = [...newGraph.edges, ...computeAssetEdges(newGraph.edges, assetNodes)]
await tick()
height = Math.max(...nodes.map((n) => n.position.y + NODE.height + 100), minHeight)
}
@@ -363,7 +424,8 @@
branchOneEnd: BranchOneEndNode,
subflowBound: SubflowBound,
noBranch: NoBranchNode,
trigger: TriggersNode
trigger: TriggersNode,
asset: AssetNode
} as any
const edgeTypes = {
@@ -4,6 +4,7 @@ import { getDependeeAndDependentComponents } from '../flows/flowExplorer'
import { dfsByModule } from '../flows/previousResults'
import { defaultIfEmptyString } from '$lib/utils'
import type { GraphModuleState } from './model'
import type { Asset } from '../assets/lib'
export type InsertKind =
| 'script'
@@ -90,6 +91,7 @@ export type FlowNode =
| SubflowBoundN
| NoBranchN
| TriggerN
| AssetN
export type InputN = {
type: 'input2'
@@ -256,6 +258,13 @@ export type TriggerN = {
}
}
export type AssetN = {
type: 'asset'
data: {
asset: Asset
}
}
// input2: InputNode,
// module: ModuleNode,
// branchAllStart: BranchAllStart,
@@ -0,0 +1,36 @@
<script lang="ts">
import NodeWrapper from './NodeWrapper.svelte'
import type { AssetN } from '../../graphBuilder.svelte'
import { Pyramid } from 'lucide-svelte'
import { assetEq, formatAsset } from '$lib/components/assets/lib'
import { getContext } from 'svelte'
import type { FlowEditorContext } from '$lib/components/flows/types'
import { twMerge } from 'tailwind-merge'
interface Props {
data: AssetN['data']
}
const { selectedAssetStore } = getContext<FlowEditorContext>('FlowEditorContext') ?? {}
let { data }: Props = $props()
const isSelected = $derived(assetEq(selectedAssetStore.val, data.asset))
</script>
<NodeWrapper>
{#snippet children({ darkMode })}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class={twMerge(
'bg-surface py-1 px-1.5 flex gap-1.5 rounded-sm text-tertiary border',
isSelected ? 'bg-surface-hover border-surface-inverse' : 'border-transparent'
)}
onmouseenter={() => (selectedAssetStore.val = data.asset)}
onmouseleave={() => (selectedAssetStore.val = undefined)}
>
<Pyramid size={16} class="shrink-0" />
<span class="text-3xs truncate">
{formatAsset(data.asset)}
</span>
</div>
{/snippet}
</NodeWrapper>