Skip to content

Commit

Permalink
feat(frontend): add support for state editing
Browse files Browse the repository at this point in the history
add react component @microlink/react-json-view

work on #34
  • Loading branch information
bsorrentino committed Sep 27, 2024
1 parent af0d3d6 commit ccbe253
Show file tree
Hide file tree
Showing 7 changed files with 354 additions and 14 deletions.
2 changes: 2 additions & 0 deletions server-jetty/src/main/js/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
<link href="./app.css" type="text/css" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>LangGraph4j App</title>

<script type="module" src="/src/lg4j-node-output.js"></script>
<script type="module" src="/src/lg4j-workbench.js"></script>
<script type="module" src="/src/lg4j-executor.js"></script>
<script type="module" src="/src/lg4j-graph.js"></script>
Expand Down
5 changes: 3 additions & 2 deletions server-jetty/src/main/js/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "lit-vite",
"name": "langgraph4j-frontend",
"private": true,
"version": "0.0.0",
"scripts": {
Expand All @@ -12,11 +12,12 @@
"dependencies": {
"@alenaksu/json-viewer": "^2.0.1",
"@lit/task": "^1.0.1",
"@types/bun": "^1.1.8",
"@microlink/react-json-view": "^1.23.3",
"lit": "^3.2.0",
"mermaid": "^10.9.1"
},
"devDependencies": {
"@types/bun": "^1.1.8",
"@types/node": "^20.14.10",
"autoprefixer": "^10.4.20",
"crypto-browserify": "^3.12.0",
Expand Down
68 changes: 68 additions & 0 deletions server-jetty/src/main/js/src/lg4j-graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,74 @@ export class LG4jMermaid extends HTMLElement {

}

/**
* Handles the content event to update the diagram content.
*
* @param {CustomEvent} e - The event object containing the new content detail.
*/
#onContent(e) {
const { detail: newContent } = e;

this._content = newContent;
this.#renderDiagram();
}

/**
* Handles the active class event to update the active class in the diagram.
*
* @param {CustomEvent} e - The event object containing the active class detail.
*/
#onActive(e) {
const { detail: activeClass } = e;

this._activeClass = activeClass;
this.#renderDiagram();
}

/**
* Handles the resize event to re-render the diagram.
*/
#resizeHandler = () => this.#renderDiagram();

/**
* Called when the element is connected to the document's DOM.
* Sets up event listeners for graph content and active class updates, and window resize.
*/
connectedCallback() {
this.addEventListener('graph', this.#onContent);
this.addEventListener('graph-active', this.#onActive);
window.addEventListener('resize', this.#resizeHandler);
}

/**
* Called when the element is disconnected from the document's DOM.
* Cleans up event listeners for graph content and active class updates, and window resize.
*/
disconnectedCallback() {
this.removeEventListener('graph', this.#onContent);
this.removeEventListener('graph-active', this.#onActive);
window.removeEventListener('resize', this.#resizeHandler);
}

/**
* Renders the diagram with the current content and runs the mermaid library.
* This method is deprecated and should not be used in new code.
*
* @deprecated
* @returns {Promise<void>} A promise that resolves when the diagram rendering and mermaid run are complete.
*/
async #renderDiagramWithRun() {
const pres = this.shadowRoot.querySelectorAll('.mermaid');
pres[0].textContent = this.#textContent;

return mermaid.run({
nodes: pres,
suppressErrors: true
})
.then(() => console.debug("RUN COMPLETE"))
.then(() => this.#svgPanZoom())
.catch(e => console.error("RUN ERROR", e));
}
#onContent(e) {
const { detail: newContent } = e

Expand Down
124 changes: 124 additions & 0 deletions server-jetty/src/main/js/src/lg4j-node-output-with-schema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { JSONEditor } from '@json-editor/json-editor'

export class LG4JNodeOutput extends HTMLElement {

static get observedAttributes() {
return ['value'];
}

#editor

constructor() {
super()

const shadowRoot = this.attachShadow({ mode: "open" });

const style = document.createElement("style");
style.textContent = `
<style>
.je-indented-panel {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.je-child-editor-holder {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.je-object__controls {
display: none;
}
.je-header.je-object__title {
display: none;
}
select.je-switcher {
pointer-events: none;
cursor: not-allowed;
visibility: hidden;
}
</style>
`

shadowRoot.appendChild(style);


}

attributeChangedCallback(name, oldValue, newValue) {
if (name === 'value') {
if (newValue !== null) {
console.debug( "attributeChangedCallback.value", newValue )
this.#editor?.setValue( JSON.parse(newValue) );

}
}
}

connectedCallback() {

const value = this.textContent
console.debug( "value", value )
this.#initialize( JSON.parse(value) )

}

disconnectedCallback() {

this.#editor.destroy();

this.#editor = null
}

#initialize( value ) {
const schema = {
"type": "object",

"additionalProperties": {
"options": {
"collapsed": true,
},
"type": [
"array",
]
},

"additionalProperties": {
"options": {
},
"type": [
"string", "number", "boolean", "object", "array",
]
}

};

const container = document.createElement('div')

this.shadowRoot.appendChild( container );

this.#editor = new JSONEditor(container, {
schema: schema,
no_additional_properties: false,
required_by_default: true,
disable_edit_json: true,
disable_properties: true,
disable_array_delete_all_rows: true,
disable_array_add: true,
array_controls_top: true,
disable_array_delete: true,
disable_array_reorder: true,

startval: value
});

}
}


window.customElements.define('lg4j-node-output', LG4JNodeOutput);
99 changes: 99 additions & 0 deletions server-jetty/src/main/js/src/lg4j-node-output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React from 'react'
import ReactDOM from 'react-dom/client';
import ReactJson from '@microlink/react-json-view'

export class LG4JNodeOutput extends HTMLElement {

static get observedAttributes() {
return ['value'];
}

constructor() {
super()

const shadowRoot = this.attachShadow({ mode: "open" });

const style = document.createElement("style");
style.textContent = `
<style>
</style>
`

shadowRoot.appendChild(style);

}

attributeChangedCallback(name, oldValue, newValue) {
if (name === 'value') {
if (newValue !== null) {
console.debug( "attributeChangedCallback.value", newValue )
}
}
}

connectedCallback() {

const value = this.textContent

console.debug( "value", value )

this.root = this.#createRoot( JSON.parse(value) )

}

disconnectedCallback() {

this.root?.unmount()

}


/**
* Represents an event triggered when an edit occurs.
*
* @typedef {Object} EditEvent
* @property {Record<string, any>} existing_src - The original source object before the edit.
* @property {any} existing_value - The original value before the edit.
* @property {string} name - The name of the field that was edited.
* @property {string[]} namespace - The namespace path indicating where the edit occurred.
* @property {any} new_value - The new value after the edit.
* @property {Record<string, any>} updated_src - The updated source object after the edit.
*/

/**
*
* @param {EditEvent} e
*/
#onEdit( e ) {

console.dir( e )

}


#createRoot( value ) {

const mountPoint = document.createElement('span');
this.shadowRoot.appendChild(mountPoint);

const root = ReactDOM.createRoot(mountPoint);

const component = React.createElement( ReactJson, {
src: value,
enableClipboard: false,
displayDataTypes: false,
name: false,
collapsed: true,
theme: 'monokai',
onEdit: e => this.#onEdit(e)

} )

root.render( component )

return root
}
}


window.customElements.define('lg4j-node-output', LG4JNodeOutput);
Loading

0 comments on commit ccbe253

Please sign in to comment.