import { makeObservable, observable, action } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent'; import { StyleProp } from '../StyleProvider'; import './DiagramBox.scss'; import { FieldView, FieldViewProps } from './FieldView'; import { PinProps, PresBox } from './trails'; import mermaid from 'mermaid'; import { Doc, DocListCast } from '../../../fields/Doc'; import { List } from '../../../fields/List'; import { RichTextField } from '../../../fields/RichTextField'; import { ContextMenu } from '../ContextMenu'; import { gptAPICall, GPTCallType } from '../../apis/gpt/GPT'; import { ChatCompletionMessageParam } from 'openai/resources/chat/completions'; import OpenAI, { ClientOptions } from 'openai'; @observer export class DiagramBox extends ViewBoxAnnotatableComponent() implements ViewBoxInterface { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DiagramBox, fieldKey); } private _ref: React.RefObject = React.createRef(); private _dragRef = React.createRef(); constructor(props: FieldViewProps) { super(props); makeObservable(this); //this.chartContent = 'flowchart LR;A-->B:::wide;B-->C:::wide; B-->D[hello];'; } @observable inputValue = ''; @observable loading = false; @observable errorMessage = ''; @observable mermaidCode = ''; @action handleInputChange = (e: React.ChangeEvent) => { this.inputValue = e.target.value; }; componentDidMount() { this._props.setContentViewBox?.(this); mermaid.initialize({ securityLevel: 'loose', startOnLoad: true, flowchart: { useMaxWidth: true, htmlLabels: true, curve: 'cardinal' }, }); } renderMermaid = async (str:string) => { try { const { svg, bindFunctions } = await this.mermaidDiagram(str); return { svg, bindFunctions }; } catch (error) { console.error('Error rendering mermaid diagram:', error); return { svg: '', bindFunctions: undefined }; } }; mermaidDiagram = async (str:string) => { return await mermaid.render('graph' + Date.now(), str); }; async renderMermaidAsync(mermaidCode:string) { try { const { svg, bindFunctions } = await this.renderMermaid(mermaidCode); const dashDiv = document.getElementById('dashDiv'); if (dashDiv) { dashDiv.innerHTML = svg; if (bindFunctions) { bindFunctions(dashDiv); } } } catch (error) { console.error('Error rendering Mermaid:', error); } } @action handleRenderClick = () => { // Call the GPT model and get the HTML output // const modelOutput = getHtmlOutput(this.inputValue); // this.htmlCode = modelOutput; this.generateMermaidCode(); }; @action generateMermaidCode=async()=>{ console.log('Generating Mermaid Code'); this.loading = true; let res = await gptAPICall(this.inputValue, GPTCallType.MERMAID); this.loading = false; if (res == 'Error connecting with API.') { // If GPT call failed console.error('GPT call failed'); this.errorMessage = 'GPT call failed; please try again.'; } else if(res!=null){ // If GPT call succeeded, set htmlCode;;; TODO: check if valid html if (this.isValidCode(res)) { this.mermaidCode = res; console.log('GPT call succeeded:' + res); this.errorMessage = ''; } else { console.error('GPT call succeeded but invalid html; please try again.'); this.errorMessage = 'GPT call succeeded but invalid html; please try again.'; } } this.renderMermaidAsync.call(this,this.removeWords(this.mermaidCode)); this.loading = false; } isValidCode = (html: string) => { return true; }; removeWords(inputStr:string) { inputStr=inputStr.replace("```mermaid","") return inputStr.replace("```",""); } createMermaidCode = async (): Promise => { let mermaidCode = 'graph LR;'; if (this.Document.data instanceof List) { let docArray: Doc[] = DocListCast(this.Document.data); let rectangleArray = docArray.filter(doc => doc.title == 'rectangle' || doc.title == 'circle'); let lineArray = docArray.filter(doc => doc.title == 'line' || doc.title == 'stroke'); let textArray = docArray.filter(doc => doc.type == 'rich text'); for (let i = 0; i < rectangleArray.length; i++) { const rectangle = rectangleArray[i]; for (let j = 0; j < lineArray.length; j++) { const line = lineArray[j]; if (this.isLineInFirstBox(rectangle, line)) { for (let k = 0; k < rectangleArray.length; k++) { const rectangle2 = rectangleArray[k]; if (this.isLineInSecondBox(rectangle2, line) && typeof rectangle.x === 'number' && typeof rectangle2.x === 'number') { mermaidCode += Math.abs(rectangle.x) + this.getTextInBox(rectangle, textArray) + '---' + Math.abs(rectangle2.x) + this.getTextInBox(rectangle2, textArray) + ';'; } } } } } } //console.log(mermaidCode); return mermaidCode; }; getTextInBox = (box: Doc, richTextArray: Doc[]): string => { for (let i = 0; i < richTextArray.length; i++) { let textDoc = richTextArray[i]; if (typeof textDoc.x === 'number' && typeof textDoc.y === 'number' && typeof box.x === 'number' && typeof box.height === 'number' && typeof box.width === 'number' && typeof box.y === 'number') { if (textDoc.x > box.x && textDoc.x < box.x + box.width && textDoc.y > box.y && textDoc.y < box.y + box.height) { if (box.title == 'rectangle') { return '(' + (textDoc.text as RichTextField)?.Text + ')'; } if (box.title == 'circle') { return '((' + (textDoc.text as RichTextField)?.Text + '))'; } } } } return '( )'; }; isLineInFirstBox = (box: Doc, line: Doc): boolean => { if (typeof line.x === 'number' && typeof box.x === 'number' && typeof box.width === 'number' && typeof box.height === 'number' && typeof box.y === 'number' && typeof line.y === 'number') { return line.x < box.x + box.width + (box.width + box.x) * 0.1 && line.x > box.x && line.y > box.y && line.y < box.y + box.height; } else { return false; } }; isLineInSecondBox = (box: Doc, line: Doc): boolean => { if (typeof line.x === 'number' && typeof line.width === 'number' && typeof box.x === 'number' && typeof box.width === 'number' && typeof box.y === 'number' && typeof box.height === 'number' && typeof line.y === 'number') { return line.x + line.width > box.x - (box.x - box.width) * 0.1 && line.x + line.width < box.x + box.width && line.y > box.y && line.y < box.y + box.height; } else { return false; } }; render() { console.log(this.loading) return(
{this.mermaidCode ? (
) : (
{this.loading ?
:
{this.errorMessage ? this.errorMessage : 'Insert prompt to generate diagram'}
}
)}
); } }