Download src/js/workflow/editor.ts from AUXteam/Bentopdf: direct link, hf CLI and curl.
- Browser
- Download file 13.8 kB
-
https://huggingface.co/spaces/AUXteam/Bentopdf/resolve/main/src/js/workflow/editor.ts
- Command line
-
hf download hf://spaces/AUXteam/Bentopdf/src/js/workflow/editor.ts
-
curl -L -o editor.ts https://huggingface.co/spaces/AUXteam/Bentopdf/resolve/main/src/js/workflow/editor.ts
13.8 kB
| import { NodeEditor } from 'rete'; | |
| import { AreaPlugin, AreaExtensions } from 'rete-area-plugin'; | |
| import { | |
| ConnectionPlugin, | |
| Presets as ConnectionPresets, | |
| } from 'rete-connection-plugin'; | |
| import { LitPlugin, Presets as LitPresets } from '@retejs/lit-plugin'; | |
| import type { ClassicScheme, LitArea2D } from '@retejs/lit-plugin'; | |
| import { DataflowEngine } from 'rete-engine'; | |
| import type { DataflowEngineScheme } from 'rete-engine'; | |
| import { html } from 'lit'; | |
| import type { BaseWorkflowNode } from './nodes/base-node'; | |
| // @ts-ignore -- Vite ?inline import for injecting into Shadow DOM | |
| import phosphorCSS from '@phosphor-icons/web/regular?inline'; | |
| // Shared stylesheet for Phosphor icons (font-face already loaded globally, strip it) | |
| const phosphorSheet = new CSSStyleSheet(); | |
| phosphorSheet.replaceSync(phosphorCSS.replace(/@font-face[^}]*\}/g, '')); | |
| type AreaExtra = LitArea2D<ClassicScheme>; | |
| export interface WorkflowEditor { | |
| editor: NodeEditor<ClassicScheme>; | |
| area: AreaPlugin<ClassicScheme, AreaExtra>; | |
| engine: DataflowEngine<DataflowEngineScheme>; | |
| destroy: () => void; | |
| } | |
| const categoryColors: Record<string, string> = { | |
| Input: '#60a5fa', | |
| 'Edit & Annotate': '#a5b4fc', | |
| 'Organize & Manage': '#c4b5fd', | |
| 'Optimize & Repair': '#fcd34d', | |
| 'Secure PDF': '#fda4af', | |
| Output: '#5eead4', | |
| }; | |
| function getStatusInfo(status: string, connected: boolean) { | |
| if (status === 'running') | |
| return { color: '#eab308', label: 'Running...', animate: true }; | |
| if (status === 'completed') | |
| return { color: '#22c55e', label: 'Complete', animate: false }; | |
| if (status === 'error') | |
| return { color: '#ef4444', label: 'Failed', animate: false }; | |
| return { | |
| color: connected ? '#22c55e' : '#6b7280', | |
| label: connected ? 'Connected' : 'Not connected', | |
| animate: false, | |
| }; | |
| } | |
| export function updateNodeDisplay( | |
| nodeId: string, | |
| editor: NodeEditor<ClassicScheme>, | |
| area: AreaPlugin<ClassicScheme, AreaExtra> | |
| ) { | |
| const view = area.nodeViews.get(nodeId); | |
| if (!view) return; | |
| const el = view.element; | |
| const node = editor.getNode(nodeId) as BaseWorkflowNode; | |
| if (!node) return; | |
| const conns = editor.getConnections(); | |
| const connected = conns.some( | |
| (c) => c.target === nodeId || c.source === nodeId | |
| ); | |
| const status = node.execStatus || 'idle'; | |
| const st = getStatusInfo(status, connected); | |
| const bar = el.querySelector<HTMLElement>('[data-wf="bar"]'); | |
| const dot = el.querySelector<HTMLElement>('[data-wf="dot"]'); | |
| const label = el.querySelector<HTMLElement>('[data-wf="label"]'); | |
| if (bar) { | |
| bar.className = st.animate ? 'wf-bar-slide' : ''; | |
| bar.style.background = st.animate | |
| ? `linear-gradient(90deg, #1f2937 0%, ${st.color} 50%, #1f2937 100%)` | |
| : st.color; | |
| bar.style.opacity = | |
| status === 'idle' && !connected | |
| ? '0.25' | |
| : status === 'idle' | |
| ? '0.5' | |
| : '1'; | |
| bar.style.backgroundSize = st.animate ? '200% 100%' : ''; | |
| } | |
| if (dot) { | |
| dot.className = st.animate ? 'wf-dot-pulse' : ''; | |
| dot.style.background = st.color; | |
| dot.style.boxShadow = 'none'; | |
| } | |
| if (label) { | |
| label.style.color = st.color; | |
| label.textContent = st.label; | |
| } | |
| } | |
| export async function createWorkflowEditor( | |
| container: HTMLElement | |
| ): Promise<WorkflowEditor> { | |
| const editor = new NodeEditor<ClassicScheme>(); | |
| const area = new AreaPlugin<ClassicScheme, AreaExtra>(container); | |
| const connection = new ConnectionPlugin<ClassicScheme, AreaExtra>(); | |
| const litPlugin = new LitPlugin<ClassicScheme, AreaExtra>(); | |
| const engine = new DataflowEngine<DataflowEngineScheme>(); | |
| litPlugin.addPreset( | |
| LitPresets.classic.setup({ | |
| customize: { | |
| node(data) { | |
| return ({ emit }: { emit: (data: unknown) => void }) => { | |
| const node = data.payload as BaseWorkflowNode; | |
| const inputs = Object.entries(node.inputs || {}); | |
| const outputs = Object.entries(node.outputs || {}); | |
| const color = categoryColors[node.category] || '#6b7280'; | |
| return html` | |
| <div | |
| style=" | |
| position: relative; display: flex; flex-direction: column; | |
| align-items: center; width: 280px; | |
| " | |
| > | |
| ${inputs.length > 0 | |
| ? html` | |
| <div | |
| style="display: flex; justify-content: center; gap: 8px; position: relative; z-index: 1; margin-bottom: -7px;" | |
| > | |
| ${inputs.map(([key, input]) => | |
| input | |
| ? html` | |
| <div | |
| style="display: flex; align-items: center; justify-content: center;" | |
| > | |
| <rete-ref | |
| .data=${{ | |
| type: 'socket', | |
| side: 'input', | |
| key, | |
| nodeId: node.id, | |
| payload: input.socket, | |
| }} | |
| .emit=${emit} | |
| ></rete-ref> | |
| </div> | |
| ` | |
| : null | |
| )} | |
| </div> | |
| ` | |
| : null} | |
| <div | |
| style=" | |
| background: #1f2937; border: 1px solid #374151; | |
| border-radius: 12px; width: 100%; overflow: hidden; | |
| " | |
| > | |
| <div | |
| style="height: 3px; border-radius: 10px 10px 0 0; overflow: hidden;" | |
| > | |
| <div | |
| data-wf="bar" | |
| style=" | |
| height: 100%; width: 100%; | |
| background: #6b7280; opacity: 0.25; | |
| " | |
| ></div> | |
| </div> | |
| <div | |
| style="padding: 6px 14px; display: flex; align-items: center; gap: 6px;" | |
| > | |
| <span | |
| data-wf="dot" | |
| style=" | |
| width: 7px; height: 7px; border-radius: 50%; background: #6b7280; flex-shrink: 0; | |
| " | |
| ></span> | |
| <span | |
| data-wf="label" | |
| style="font-size: 10px; color: #6b7280; font-weight: 500; flex: 1;" | |
| >Not connected</span | |
| > | |
| <span | |
| data-wf-delete="${node.id}" | |
| style=" | |
| cursor: pointer; display: flex; align-items: center; justify-content: center; | |
| width: 18px; height: 18px; border-radius: 4px; | |
| color: #6b7280; transition: all 0.15s; | |
| " | |
| > | |
| <svg | |
| width="12" | |
| height="12" | |
| viewBox="0 0 24 24" | |
| fill="none" | |
| stroke="currentColor" | |
| stroke-width="2" | |
| stroke-linecap="round" | |
| stroke-linejoin="round" | |
| > | |
| <line x1="18" y1="6" x2="6" y2="18"></line> | |
| <line x1="6" y1="6" x2="18" y2="18"></line> | |
| </svg> | |
| </span> | |
| </div> | |
| <div | |
| style="height: 1px; background: #374151; margin: 0 14px;" | |
| ></div> | |
| <div | |
| style="padding: 10px 14px 12px; display: flex; align-items: flex-start; gap: 10px;" | |
| > | |
| <i | |
| class="ph ${node.icon}" | |
| style="font-size: 18px; color: ${color}; flex-shrink: 0; margin-top: 1px; line-height: 1;" | |
| ></i> | |
| <div style="flex: 1; min-width: 0;"> | |
| <div | |
| style="font-size: 13px; font-weight: 600; color: #f3f4f6; line-height: 1.3;" | |
| > | |
| ${node.label} | |
| </div> | |
| <div | |
| style="font-size: 11px; color: #9ca3af; margin-top: 2px; line-height: 1.3;" | |
| > | |
| ${node.description} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ${outputs.length > 0 | |
| ? html` | |
| <div | |
| style="display: flex; justify-content: center; gap: 8px; position: relative; z-index: 1; margin-top: -7px;" | |
| > | |
| ${outputs.map(([key, output]) => | |
| output | |
| ? html` | |
| <div | |
| style="display: flex; align-items: center; justify-content: center;" | |
| > | |
| <rete-ref | |
| .data=${{ | |
| type: 'socket', | |
| side: 'output', | |
| key, | |
| nodeId: node.id, | |
| payload: output.socket, | |
| }} | |
| .emit=${emit} | |
| ></rete-ref> | |
| </div> | |
| ` | |
| : null | |
| )} | |
| </div> | |
| ` | |
| : null} | |
| </div> | |
| `; | |
| }; | |
| }, | |
| socket() { | |
| return () => { | |
| return html`<div | |
| style=" | |
| width: 14px; height: 14px; border-radius: 50%; | |
| background: #6366f1; border: 2px solid #1f2937; | |
| box-shadow: 0 0 0 1px #6366f1; cursor: crosshair; | |
| " | |
| ></div>`; | |
| }; | |
| }, | |
| }, | |
| }) | |
| ); | |
| connection.addPreset(ConnectionPresets.classic.setup()); | |
| // Override connection path to use vertical bezier curves (top-to-bottom flow) | |
| litPlugin.addPipe((context) => { | |
| if ((context as any).type === 'connectionpath') { | |
| const { points } = (context as any).data; | |
| const [start, end] = points as [ | |
| { x: number; y: number }, | |
| { x: number; y: number }, | |
| ]; | |
| const curvature = 0.3; | |
| const horizontal = Math.abs(start.x - end.x); | |
| const dy = | |
| Math.max(horizontal / 2, Math.abs(end.y - start.y)) * curvature; | |
| const path = `M ${start.x} ${start.y} C ${start.x} ${start.y + dy} ${end.x} ${end.y - dy} ${end.x} ${end.y}`; | |
| return { | |
| ...context, | |
| data: { ...(context as any).data, path }, | |
| } as typeof context; | |
| } | |
| return context; | |
| }); | |
| editor.use(area); | |
| area.use(connection); | |
| area.use(litPlugin); | |
| (editor as NodeEditor<DataflowEngineScheme>).use(engine); | |
| AreaExtensions.selectableNodes(area, AreaExtensions.selector(), { | |
| accumulating: AreaExtensions.accumulateOnCtrl(), | |
| }); | |
| AreaExtensions.simpleNodesOrder(area); | |
| // Inject Phosphor icon styles into Shadow DOM roots created by the Lit plugin | |
| let phosphorTimer: ReturnType<typeof setTimeout> | null = null; | |
| const injectPhosphor = () => { | |
| if (phosphorTimer) return; | |
| phosphorTimer = setTimeout(() => { | |
| phosphorTimer = null; | |
| for (const el of container.querySelectorAll('*')) { | |
| const sr = (el as HTMLElement).shadowRoot; | |
| if (sr && !sr.adoptedStyleSheets.includes(phosphorSheet)) { | |
| sr.adoptedStyleSheets = [...sr.adoptedStyleSheets, phosphorSheet]; | |
| } | |
| } | |
| }, 50); | |
| }; | |
| const observer = new MutationObserver(injectPhosphor); | |
| observer.observe(container, { childList: true, subtree: true }); | |
| const onPointerDown = (e: Event) => { | |
| const target = (e.target as HTMLElement).closest<HTMLElement>( | |
| '[data-wf-delete]' | |
| ); | |
| if (!target) return; | |
| e.stopPropagation(); | |
| e.preventDefault(); | |
| const nodeId = target.getAttribute('data-wf-delete'); | |
| if (nodeId) { | |
| document.dispatchEvent( | |
| new CustomEvent('wf-delete-node', { detail: { nodeId } }) | |
| ); | |
| } | |
| }; | |
| const onMouseEnter = (e: Event) => { | |
| const target = (e.target as HTMLElement).closest<HTMLElement>( | |
| '[data-wf-delete]' | |
| ); | |
| if (!target) return; | |
| target.style.color = '#f87171'; | |
| target.style.background = 'rgba(248,113,113,0.1)'; | |
| }; | |
| const onMouseLeave = (e: Event) => { | |
| const target = (e.target as HTMLElement).closest<HTMLElement>( | |
| '[data-wf-delete]' | |
| ); | |
| if (!target) return; | |
| target.style.color = '#6b7280'; | |
| target.style.background = 'transparent'; | |
| }; | |
| container.addEventListener('pointerdown', onPointerDown, true); | |
| container.addEventListener('mouseenter', onMouseEnter, true); | |
| container.addEventListener('mouseleave', onMouseLeave, true); | |
| return { | |
| editor, | |
| area, | |
| engine, | |
| destroy: () => { | |
| observer.disconnect(); | |
| if (phosphorTimer) { | |
| clearTimeout(phosphorTimer); | |
| phosphorTimer = null; | |
| } | |
| container.removeEventListener('pointerdown', onPointerDown, true); | |
| container.removeEventListener('mouseenter', onMouseEnter, true); | |
| container.removeEventListener('mouseleave', onMouseLeave, true); | |
| area.destroy(); | |
| }, | |
| }; | |
| } | |