/** * Copyright (c) 2023 MERCENARIES.AI PTE. LTD. * All rights reserved. */ import Alpine from 'alpinejs'; import { getRenderTemplate } from './nodes/nodes.js'; import '../styles/rete.scss'; import { omnilog } from 'omni-shared'; window.Alpine = Alpine; export function kebab(str) { const replace = (s) => s.toLowerCase().replace(/ /g, '-'); return Array.isArray(str) ? str.map(replace) : replace(str); } export class AlpineRenderPlugin { constructor(options = {}) { this.name = 'alpine-render'; this.options = options; this.editor = null; } setupAlpineNode(el, reteNode, bindSocket, bindControl) { if (reteNode == null) { throw new Error('Invalid Rete Node (null)'); } reteNode._alpine = el; const name = 'retenode' + reteNode.id; el.setAttribute('x-data', name); el.setAttribute('id', name); // TODO: Is this possibly leaking when nodes are deleted? new ResizeObserver(() => { this.onNodeResize(reteNode); }).observe(el); reteNode.update = async (skipRedraw) => await this.update(reteNode, skipRedraw); const reteEditor = this.editor; Alpine.data(name, () => ({ node: null, editor: null, bindSocket: null, bindControl: null, showEditor: false, classNameString: '', //blockClassString: '', tabButtonsClassString: '', showHelp: true, copyNotification: false, getTitle(){ return this.node.title || this.node.name }, selected(node) { return this.editor?.selected.contains(node) ? 'selected' : ''; }, className(node) { return kebab([ 'rete-block', // Required for rete.scss to apply error and selected styles to blocks. this.selected(node), node.name, node.errors.length ? 'error' : '', node.active ? 'active' : '' ]); }, async rename() { let newName = prompt('New Name', this.node.title) || ''; if (newName === this.title) { newName = ''; // Restore default name. } const prevName = this.node.data['x-omni-title'] || ''; this.node.data['x-omni-title'] = newName || undefined; this.node.title = this.node.data['x-omni-title'] || this.title; return prevName !== newName }, async startEditComment(el) { const commentEl = document.getElementById(`comment_${this.node.id}`); commentEl.innerText = this.node.data['x-omni-comment'] || this.node.summary; }, async getHelpText(node) { return window.client.markdownEngine.render(this.node.data['x-omni-comment'] || this.node.summary); }, async setComment(el) { const commentEl = document.getElementById(`comment_${this.node.id}`); const newText = commentEl.innerText.trim(); const prevText = this.node.data['x-omni-comment'] || ''; if (newText) { this.node.data['x-omni-comment'] = newText; } else { delete this.node.data['x-omni-comment']; } commentEl.innerHTML = await window.client.markdownEngine.render( this.node.data['x-omni-comment'] || this.node.summary ); return prevText !== newText; }, outputs(node) { if (!node || !node.outputs) { return []; } return Array.from(node?.outputs?.values?.() || []); }, inputs(node) { if (!node || !node.inputs) { return []; } return Array.from(node?.inputs?.values?.() || []).filter((x) => x != null); }, controls(node) { return Array.from(node?.controls?.values?.() || []).filter((x) => { if (x == null) { console.warn('Null control for node - skipping render', node); return false; } if (x.component == null) { console.warn('Invalid component for control - skipping render', Alpine.raw(node), Alpine.raw(x)); return false; } return true; }); }, onClickClose(node) { // Delete the node and the component (!) this.editor.removeNode(node); }, toggleInfo(node) { this.showHelp = !this.showHelp; if (!this.showHelp) { window.localStorage.setItem('omni/workbench/help_seen_' + node.name, 1); } else { window.localStorage.removeItem('omni/workbench/help_seen_' + node.name); } window.client.sendSystemMessage( Object.assign({ name: node.name, title: node.title }, node.meta || {}, node.patch?.meta || {}), 'omni/component-meta', { commands: [ { title: 'Add to Workbench', id: 'add', args: [node.name] } ] }, ['no-picture'] ); }, onClickCopy(node) { const copyNode = { data: node.data, name: node.name }; const s = JSON.stringify(copyNode); navigator.clipboard.writeText(s).then( function () { // console.log('Copying to clipboard was successful!'); }, function (err) { console.error('Could not copy node: ', err); } ); this.copyNotification = true; const that = this; setTimeout(function () { that.copyNotification = false; }, 3000); }, getContentHeight() {}, init() { if (!reteNode) { throw new Error('reteNode is not defined'); } this.node = reteNode; this.node.namespace ??= ''; this.node.category ??= ''; this.node.title ??= ''; this.node.summary ??= ''; this.node.meta ??= {}; this.node.data.xOmniEnabled ??= true; this.showHelp = window.localStorage.getItem('omni/workbench/help_seen_' + this.node.name) == null; this.editor = reteEditor; this.bindSocket = bindSocket; this.bindControl = bindControl; //this.classNameString = this.blockClassString = this.tabButtonsClassString = this.className(reteNode) } })); } // eslint-disable-next-line no-unused-vars createControl(el, control) { const data = Alpine.$data(el); return data; } setInnerHTML(node) { //node._alpine.setAttribute('class', 'block') //node._alpine.setAttribute(':class', 'blockClassString') // POC: This will bec if (!node) { throw new Error('Invalid node passed into setInnerHtml'); } node._alpine.innerHTML = node.renderTemplate ? getRenderTemplate(node.renderTemplate) : getRenderTemplate('default'); } updateConnections(reteNode) { reteNode.outputs.forEach((x) => { x.connections.forEach((connection) => { this.editor.view.connections.get(connection)?.update(); }); }); reteNode.inputs.forEach((x) => { x.connections.forEach((connection) => { this.editor.view.connections.get(connection)?.update(); }); }); } onNodeResize(reteNode) { if (!reteNode) { throw new Error('Invalid reteNode passed to onNodeResize'); } this.updateConnections(reteNode); } async update(reteNode, skipRedraw = false) { if (!reteNode) { throw new Error('Invalid reteNode passed to update'); } const ref = Alpine.$data(reteNode._alpine); if (ref?.className) { ref.classNameString = ref.className(reteNode); //ref.blockClassString = ref.classNameString ref.tabButtonsClassString = ref.classNameString; } if (!skipRedraw) { this.setInnerHTML(reteNode); } return await new Promise((res) => { // update() is often called with .reduce(...) // This Promise attempts to minimize excessive updates by only updating one node per frame. // TODO: Measure difference in battery usage. if (!reteNode._alpine) { res(); return; } Alpine.effect(() => { requestAnimationFrame(res); }); }); } install(editor) { this.editor = editor; this.editor.on('rendernode', ({ el, node, component, bindSocket, bindControl }) => { if (!component.render || component.render === 'alpine') { // Create Alpine component for the node and update it this.setupAlpineNode(el, node, bindSocket, bindControl); this.setInnerHTML(node); node.update(); } }); this.editor.on('rendercontrol', ({ el, control }) => { if (control.render && control.render !== 'alpine') return; // Create Alpine component for the control and update it control._alpine = this.createControl(el, control); control.update = () => control._alpine.update(); }); this.editor.on('connectioncreated connectionremoved', (connection) => { connection.input.node.update(); connection.output.node.update(); }); this.editor.on('nodeselected', (node) => { if (node._alpine !== editor.previousSelectedNode?._alpine) { editor.previousSelectedNode?.update(true); // Redraw previously selected node editor.previousSelectedNode = node; node.update(true); } }); } } document.addEventListener('alpine:init', () => { omnilog.status_start('Renderer Alpine Init'); Alpine.directive('socket', (el, { expression, value }, { evaluate, effect }) => { const foundEl = el.closest('[x-data]'); const data = Alpine.$data(foundEl); // Alpine directive x-socket:value=expression const io = evaluate(expression); const currentClass = el.getAttribute('class'); el.setAttribute('class', currentClass + ' ' + io.socket.name); const prefix = value === 'output' ? 'o_' : 'i_'; foundEl.socketDict = { ...(foundEl.socketDict || {}), [prefix + io.key]: el }; effect(() => { data.bindSocket(el, value, Alpine.raw(io)); }); }); Alpine.directive('control', (el, { expression, value }, { evaluate, effect }) => { // Alpine directive x-control:value=expression const foundEl = el.closest('[x-data]'); const data = Alpine.$data(foundEl); const control = evaluate(expression); effect(() => { data.bindControl(el, Alpine.raw(control)); }); }); });