Improve GraphFX UI editing and image previews

This commit is contained in:
2026-06-19 10:22:04 +02:00
parent 7927b34c5f
commit 8c58aaae2b
13 changed files with 1151 additions and 659 deletions

View File

@@ -1,12 +1,12 @@
import { MarkerType, type Edge, type Node } from '@vue-flow/core'
import { ioTypeFor, type GraphFxIoType } from './nodeDefinitions'
import { ioOptionsFor, ioTypeFor, type GraphFxIoType } from './nodeDefinitions'
export type GraphFxIoValue = {
label?: string | null
value?: unknown
output?: string | null
type?: GraphFxIoType
definition?: { type?: GraphFxIoType }
definition?: { type?: GraphFxIoType; enum?: string[]; options?: string[] }
}
export type GraphFxSerializedNode = {
@@ -32,7 +32,25 @@ export type GraphFxPort = {
label: string
type: GraphFxIoType
output?: string | null
connectedDisplay?: string | null
value?: unknown
options?: string[]
previewUrl?: string | null
}
export type GraphFxImageOption = {
id: string
name: string
}
export type GraphFxNodeActions = {
renameNodeId: (nodeId: string, nextId: string) => void
updateIoLabel: (nodeId: string, direction: 'in' | 'out', ioName: string, label: string) => void
updateInputValue: (nodeId: string, ioName: string, value: string | boolean) => void
disconnectInput: (nodeId: string, ioName: string) => void
deleteNode: (nodeId: string) => void
duplicateNode: (nodeId: string) => void
selectInputImage: (nodeId: string, ioName: string, imageId: string) => void | Promise<void>
}
export type GraphFxNodeData = {
@@ -42,6 +60,11 @@ export type GraphFxNodeData = {
outputs: GraphFxPort[]
hasSavedPosition: boolean
raw: GraphFxSerializedNode
actions?: GraphFxNodeActions
imageOptions?: GraphFxImageOption[]
selectedImages?: Record<string, string>
imagePreviews?: Record<string, string>
runtimeReady?: boolean
}
export type GraphFxFlowWarning = {
@@ -89,14 +112,35 @@ function labelFor(name: string, io?: GraphFxIoValue) {
return io?.label || name
}
function imagePreviewUrl(value: unknown): string | null {
if (!value) return null
if (typeof value === 'string') {
if (value.startsWith('data:image/')) return value
if (/^[A-Za-z0-9+/=\r\n]+$/.test(value) && value.length > 80) return `data:image/png;base64,${value.replace(/\s+/g, '')}`
return null
}
if (typeof value === 'object') {
const record = value as { src?: unknown; dataUrl?: unknown; base64?: unknown }
if (typeof record.src === 'string') return imagePreviewUrl(record.src)
if (typeof record.dataUrl === 'string') return imagePreviewUrl(record.dataUrl)
if (typeof record.base64 === 'string') return imagePreviewUrl(record.base64)
}
return null
}
function toPorts(nodeName: string, record: Record<string, GraphFxIoValue> | undefined, direction: 'in' | 'out') {
return Object.entries(record || {}).map(([name, io]) => ({
name,
label: labelFor(name, io),
type: ioTypeFor(nodeName, direction, name, io?.type || io?.definition?.type),
output: direction === 'in' ? io?.output || null : null,
value: direction === 'in' ? io?.value : undefined,
}))
return Object.entries(record || {}).map(([name, io]) => {
const type = ioTypeFor(nodeName, direction, name, io?.type || io?.definition?.type)
return {
name,
label: labelFor(name, io),
type,
output: direction === 'in' ? io?.output || null : null,
value: direction === 'in' ? io?.value : undefined,
options: ioOptionsFor(nodeName, direction, name, io?.definition),
previewUrl: type === 'Image' ? imagePreviewUrl(io?.value) : null,
}
})
}
function savedPosition(item: GraphFxSerializedNode, index: number) {
@@ -105,7 +149,7 @@ function savedPosition(item: GraphFxSerializedNode, index: number) {
return {
hasSavedPosition: hasX && hasY,
position: hasX && hasY ? { x: item.x as number, y: item.y as number } : { x: index * 280, y: 0 },
position: hasX && hasY ? { x: item.x as number, y: item.y as number } : { x: index * 430, y: 0 },
}
}
@@ -189,6 +233,11 @@ export function graphFxToVueFlow(graph: GraphFxSerializedGraph): GraphFxFlowMode
const sourceNode = nodes.find((node) => node.id === source.nodeId)
const sourcePort = sourceNode?.data?.outputs.find((output) => output.name === source.outputName)
const targetNode = nodes.find((node) => node.id === item.node.id)
const targetPort = targetNode?.data?.inputs.find((port) => port.name === inputName)
if (targetPort) {
targetPort.connectedDisplay = `${sourceNode?.data?.name || source.nodeId}.${sourcePort?.label || source.outputName}`
}
const edgeColor = ioTypeColors[sourcePort?.type || 'Unknown']
edges.push({