Last active
August 23, 2024 22:17
-
-
Save bsorrentino/ac30aace026483d9688117f596e9d265 to your computer and use it in GitHub Desktop.
Mermaid Preview web component
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en" data-theme="dark"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title>Mermaid preview</title> | |
<style> | |
.h-screen { | |
height: 100vh; | |
} | |
</style> | |
<script type="module" src="./mermaid-preview.js"></script> | |
</head> | |
<body> | |
<mermaid-preview class="h-screen" id="preview1" theme="base"> | |
--- | |
title: ADAPTIVE RAG EXECUTOR | |
--- | |
flowchart TD | |
start((start)) | |
stop((stop)) | |
web_search("web_search") | |
retrieve("retrieve") | |
grade_documents("grade_documents") | |
generate("generate") | |
transform_query("transform_query") | |
%% condition1{"check state"} | |
%% condition2{"check state"} | |
%% startcondition{"check state"} | |
%% start:::start --> startcondition:::startcondition | |
%% startcondition:::startcondition -->|web_search| web_search:::web_search | |
start:::start -->|web_search| web_search:::web_search | |
%% startcondition:::startcondition -->|vectorstore| retrieve:::retrieve | |
start:::start -->|vectorstore| retrieve:::retrieve | |
web_search:::web_search --> generate:::generate | |
retrieve:::retrieve --> grade_documents:::grade_documents | |
%% grade_documents:::grade_documents --> condition1:::condition1 | |
%% condition1:::condition1 -->|transform_query| transform_query:::transform_query | |
grade_documents:::grade_documents -->|transform_query| transform_query:::transform_query | |
%% condition1:::condition1 -->|generate| generate:::generate | |
grade_documents:::grade_documents -->|generate| generate:::generate | |
transform_query:::transform_query --> retrieve:::retrieve | |
%% generate:::generate --> condition2:::condition2 | |
%% condition2:::condition2 -->|not supported| generate:::generate | |
generate:::generate -->|not supported| generate:::generate | |
%% condition2:::condition2 -->|not useful| transform_query:::transform_query | |
generate:::generate -->|not useful| transform_query:::transform_query | |
%% condition2:::condition2 -->|useful| stop:::stop | |
generate:::generate -->|useful| stop:::stop | |
</mermaid-preview> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const d3moduleUrl = 'https://cdn.jsdelivr.net/npm/[email protected]/+esm' | |
const mermaidModuleUrl = 'https://cdn.jsdelivr.net/npm/[email protected]/+esm' | |
Promise.all([ import(mermaidModuleUrl), import(d3moduleUrl)]).then( ([mermaidModule, d3]) => { | |
const mermaid = mermaidModule.default | |
class MermaidPreview extends HTMLElement { | |
static get observedAttributes() { | |
return ['theme']; | |
} | |
attributeChangedCallback(name, oldValue, newValue) { | |
console.debug( 'attributeChangedCallback', name, oldValue, newValue ) | |
if( name === 'theme' && oldValue !== newValue ) { | |
this.#init() | |
this.#renderDiagram() | |
} | |
} | |
get useMaxWidth() { | |
return this.getAttribute('useMaxWidth') === 'true' | |
} | |
constructor() { | |
super(); | |
this._content = null | |
this._activeClass = null | |
this._lastTransform = null | |
const shadowRoot = this.attachShadow({ mode: "open" }); | |
const style = document.createElement("style"); | |
style.textContent = ` | |
:host { | |
display: block; | |
width: 100%; | |
height: 100%; | |
} | |
.h-full { | |
height: 100%; | |
} | |
.w-full { | |
width: 100%; | |
} | |
.flex { | |
display: flex; | |
} | |
.items-center { | |
align-items: center; | |
} | |
.justify-center { | |
justify-content: center; | |
} | |
.bg-neutral { | |
--tw-bg-opacity: 1; | |
background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); | |
} | |
` | |
shadowRoot.appendChild(style); | |
const container = document.createElement('div') | |
container.classList.add("h-full"); | |
container.classList.add("w-full"); | |
container.classList.add("flex"); | |
container.classList.add("items-center"); | |
container.classList.add("justify-center"); | |
container.classList.add("bg-neutral"); | |
container.classList.add("mermaid"); | |
const errorSlot = document.createElement('slot') | |
errorSlot.name = 'error' | |
container.appendChild(errorSlot) | |
shadowRoot.appendChild(container); | |
this.#renderDiagram() | |
} | |
/** | |
* @returns {ChildNode[]} | |
* @private | |
*/ | |
get #textNodesContent() { | |
return Array.from(this.childNodes) | |
.filter(node => node.nodeType === this.TEXT_NODE) | |
.map(node => node.textContent?.trim()) | |
.join(''); | |
} | |
/** | |
* @returns {string} | |
* @private | |
*/ | |
get #textContent() { | |
if (this._content) { | |
if (this._activeClass) { | |
return ` | |
${this._content} | |
classDef ${this._activeClass} fill:#f96 | |
` | |
} | |
return this._content | |
} | |
return this.#textNodesContent; | |
} | |
#updateSVGSize( svg, { right: width, bottom: height } ) { | |
if( this.useMaxWidth ) { // no update required | |
return svg | |
} | |
console.debug( `new svg size: width:${width} height:${height}`); | |
const maxWitdhRegex = /style="max-width:\s*[\d\.]+px;"/ | |
if( maxWitdhRegex.test(svg) ) { // fix useMaxWidth === false doesn't work (to be investigated) | |
console.warn( 'useMaxWitdh is false, but found max-width in svg!' ) | |
return svg.replace( maxWitdhRegex, `height="${height}" width="${width}" `) | |
} | |
return svg | |
.replace(/height="[\d\.]+"/, `height="${height}"`) | |
.replace(/width="[\d\.]+"/, `width="${width}"`) | |
; | |
} | |
async #renderDiagram() { | |
if( !this.#textContent ) return | |
// console.debug( this.#textContent ) | |
const svgContainer = this.shadowRoot.querySelector('.mermaid') | |
return mermaid.render(`graph`, this.#textContent) | |
.then(res => svgContainer.innerHTML = this.#updateSVGSize(res.svg, svgContainer.getBoundingClientRect()) ) | |
.then(() => this.#svgPanZoom()) | |
.catch(e => { | |
console.error("RENDER ERROR", e) | |
const errorSlot = this.shadowRoot.querySelector('slot[name="error"]') | |
if( errorSlot ) { | |
const slotElements = errorSlot.assignedElements(); | |
slotElements[0].textContent = e.message | |
} | |
}) | |
} | |
#svgPanZoom() { | |
// console.debug( '_lastTransform', this._lastTransform ) | |
const svgs = d3.select(this.shadowRoot).select(".mermaid svg"); | |
// console.debug( 'svgs', svgs ) | |
const self = this; | |
svgs.each(function () { | |
// 'this' refers to the current DOM element | |
const svg = d3.select(this); | |
// console.debug( 'svg', svg ); | |
svg.html("<g>" + svg.html() + "</g>"); | |
const inner = svg.select("g"); | |
// console.debug( 'inner', inner ) | |
const zoom = d3.zoom().on("zoom", event => { | |
inner.attr("transform", event.transform); | |
self._lastTransform = event.transform; | |
}); | |
const selection = svg.call(zoom); | |
if (self._lastTransform !== null) { | |
inner.attr("transform", self._lastTransform) | |
// [D3.js Set initial zoom level](https://stackoverflow.com/a/46437252/521197) | |
selection.call(zoom.transform, self._lastTransform); | |
} | |
}); | |
} | |
#onContent(e) { | |
const { detail: newContent } = e | |
this._content = newContent | |
this.#renderDiagram() | |
} | |
#onActive(e) { | |
const { detail: activeClass } = e | |
this._activeClass = activeClass; | |
this.#renderDiagram() | |
} | |
#init() { | |
console.debug( '#init', this.attributes.theme ) | |
mermaid.initialize({ | |
logLevel: 'none', | |
startOnLoad: false, | |
theme: this.getAttribute('theme') ?? 'dark', | |
useMaxWidth: this.useMaxWidth, | |
flowchart: { useMaxWidth: this.useMaxWidth }, | |
sequence: { useMaxWidth: this.useMaxWidth }, | |
gantt: { useMaxWidth: this.useMaxWidth }, | |
journey: { useMaxWidth: this.useMaxWidth }, | |
timeline: { useMaxWidth: this.useMaxWidth }, | |
class: { useMaxWidth: this.useMaxWidth }, | |
state: { useMaxWidth: this.useMaxWidth }, | |
er: { useMaxWidth: this.useMaxWidth }, | |
pie: { useMaxWidth: this.useMaxWidth }, | |
quadrantChart: { useMaxWidth: this.useMaxWidth }, | |
xyChart: { useMaxWidth: this.useMaxWidth }, | |
requirement: { useMaxWidth: this.useMaxWidth }, | |
mindmap: { useMaxWidth: this.useMaxWidth }, | |
gitGraph: { useMaxWidth: this.useMaxWidth }, | |
c4: { useMaxWidth: this.useMaxWidth }, | |
sankey: { useMaxWidth: this.useMaxWidth }, | |
block: { useMaxWidth: this.useMaxWidth }, | |
}); | |
} | |
#resizeHandler = () => this.#renderDiagram() | |
connectedCallback() { | |
this.addEventListener('graph', this.#onContent) | |
this.addEventListener('graph-active', this.#onActive) | |
window.addEventListener('resize', this.#resizeHandler) | |
} | |
disconnectedCallback() { | |
this.removeEventListener('graph', this.#onContent) | |
this.removeEventListener('graph-active', this.#onActive) | |
window.removeEventListener('resize', this.#resizeHandler) | |
} | |
} | |
if (!window.customElements.get('mermaid-preview')) { | |
window.customElements.define('mermaid-preview', MermaidPreview); | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment