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'; import { line } from 'd3'; import { InkingStroke } from '../InkingStroke'; import { DocumentManager } from '../../util/DocumentManager'; import { C } from '@fullcalendar/core/internal-common'; @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(); private chartContent: string; 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' }, }); this.renderMermaidAsync.call(this, this.chartContent); this.mermaidCode = this.chartContent; this.chartContent= this.createMermaidCode(); } 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() { 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'); setTimeout(() => { let inkStrokeArray=lineArray.map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())) .filter(inkView => inkView?.ComponentView instanceof InkingStroke) console.log(inkStrokeArray.length) let inkingStrokeArray=inkStrokeArray.map(stroke=>stroke?.ComponentView) for (let i = 0; i < rectangleArray.length; i++) { const rectangle = rectangleArray[i]; for (let j = 0; j < lineArray.length; j++) { let inkStrokeXArray=(inkingStrokeArray[j] as InkingStroke)?.inkScaledData().inkData.map(coord=>coord.X) let inkStrokeYArray=(inkingStrokeArray[j] as InkingStroke)?.inkScaledData().inkData.map(coord=>coord.Y) let minX=Math.min(...inkStrokeXArray) let minY=Math.min(...inkStrokeYArray) let inkScaleX=(inkingStrokeArray[j] as InkingStroke)?.inkScaledData().inkScaleX let inkScaleY=(inkingStrokeArray[j] as InkingStroke)?.inkScaledData().inkScaleY let StartX:number=0 let StartY:number=0 let EndX:number=0 let EndY:number=0 if(typeof docArray[j].x==='number'&&typeof docArray[j].y==='number'&&typeof (inkingStrokeArray[j] as InkingStroke)?.inkScaledData().inkData[j]==='number'){ StartX=(inkingStrokeArray[j] as InkingStroke)?.inkScaledData().inkData[j].X*inkScaleX-minX*inkScaleX StartY=(inkingStrokeArray[j] as InkingStroke)?.inkScaledData().inkData[j].Y*inkScaleY-minY*inkScaleY+lineArray[j]?.y EndX=(inkingStrokeArray[j] as InkingStroke)?.inkScaledData().inkData[(inkingStrokeArray[j] as InkingStroke)?.inkScaledData().inkData.length-1].X*inkScaleX-minX*inkScaleX+docArray[j].x EndY=(inkingStrokeArray[j] as InkingStroke)?.inkScaledData().inkData[(inkingStrokeArray[j] as InkingStroke)?.inkScaledData().inkData.length-1].Y*inkScaleY-minY*inkScaleY+docArray[j].y } console.log((inkingStrokeArray[j] as InkingStroke)?.inkScaledData().inkData) console.log((inkingStrokeArray[j] as InkingStroke)?.inkScaledData()) console.log(EndX) console.log(EndY) console.log(docArray[j].title) console.log(docArray[j].x) console.log(docArray[j].y) console.log(docArray[j].width) console.log(docArray[j].height) if (this.isPointInBox(rectangle, [StartX,StartY])) { for (let k = 0; k < rectangleArray.length; k++) { const rectangle2 = rectangleArray[k]; if (this.isPointInBox(rectangle2, [EndX,EndY]) && 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 '( )'; }; isPointInBox = (box: Doc, line: number[]): boolean => { if (typeof line[0] === 'number' && typeof box.x === 'number' && typeof box.width === 'number' && typeof box.height === 'number' && typeof box.y === 'number' && typeof line[1] === 'number') { return line[0] < box.x + box.width&& line[0] > box.x && line[1] > box.y && line[1] < box.y + box.height; } else { return false; } }; render() { return (
{this.mermaidCode ? (
) : (
{this.loading ?
:
{this.errorMessage ? this.errorMessage : 'Insert prompt to generate diagram'}
}
)}
); } }