/* eslint-disable prettier/prettier */ /* eslint-disable jsx-a11y/control-has-associated-label */ import mermaid from 'mermaid'; import { action, makeObservable, observable, reaction, computed } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast } from '../../../fields/Doc'; import { List } from '../../../fields/List'; import { RichTextField } from '../../../fields/RichTextField'; import { DocCast, BoolCast } from '../../../fields/Types'; import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT'; import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; import { LinkManager } from '../../util/LinkManager'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { InkingStroke } from '../InkingStroke'; import './DiagramBox.scss'; import { FieldView, FieldViewProps } from './FieldView'; import { PointData } from '../../../pen-gestures/GestureTypes'; import { InkField } from '../../../fields/InkField'; enum menuState { option, mermaidCode, drawing, gpt, justCreated, } @observer export class DiagramBox extends ViewBoxAnnotatableComponent() { 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); } @observable menuState = menuState.justCreated; @observable renderDiv: React.ReactNode; @observable inputValue = ''; @observable createInputValue = ''; @observable loading = false; @observable errorMessage = ''; @observable mermaidCode = ''; @observable isExampleMenuOpen = false; @action handleInputChange = (e: React.ChangeEvent) => { this.inputValue = e.target.value; console.log(e.target.value); }; async componentDidMount() { this._props.setContentViewBox?.(this); mermaid.initialize({ securityLevel: 'loose', startOnLoad: true, darkMode: true, flowchart: { useMaxWidth: false, htmlLabels: true, curve: 'cardinal' }, gantt: { useMaxWidth: true, useWidth: 2000 }, }); if (!this.Document.testValue) { this.Document.height = 500; this.Document.width = 500; } this.Document.testValue = 'a'; this.mermaidCode = 'a'; if (typeof this.Document.drawingMermaidCode === 'string' && this.Document.menuState === 'drawing') { this.renderMermaidAsync(this.Document.drawingMermaidCode); } // this is so that ever time a new doc, text node or ink node, is created, this.createMermaidCode will run which will create a save // reaction( // () => DocListCast(this.Document.data), // () => this.lockInkStroke(), // { fireImmediately: true } // ); // reaction( // () => // DocListCast(this.Document.data) // .filter(doc => doc.type === 'rich text') // .map(doc => (doc.text as RichTextField).Text), // () => this.convertDrawingToMermaidCode(), // { fireImmediately: true } // ); // const rectangleXValues = computed(() => // DocListCast(this.Document.data) // .filter(doc => doc.title === 'rectangle') // .map(doc => doc.x) // ); // reaction( // () => rectangleXValues.get(), // () => this.lockInkStroke(), // { fireImmediately: true } // ); this.lockInkStroke(); } componentDidUpdate = () => { if (typeof this.Document.drawingMermaidCode === 'string' && this.Document.menuState === 'drawing') { this.renderMermaidAsync(this.Document.drawingMermaidCode); } if (typeof this.Document.gptMermaidCode === 'string' && this.Document.menuState === 'gpt') { this.renderMermaidAsync(this.Document.gptMermaidCode); } }; switchRenderDiv() { switch (this.Document.menuState) { case 'option': this.renderDiv = this.renderOption(); break; case 'drawing': this.renderDiv = this.renderDrawing(); break; case 'gpt': this.renderDiv = this.renderGpt(); break; case 'mermaidCode': this.renderDiv = this.renderMermaidCode(); break; default: this.menuState = menuState.option; this.renderDiv = this.renderOption(); } } 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) => mermaid.render('graph' + Date.now(), str); async renderMermaidAsync(mermaidCode: string) { try { const { svg, bindFunctions } = await this.renderMermaid(mermaidCode); const dashDiv = document.getElementById('dashDiv' + this.Document.title); if (dashDiv) { dashDiv.innerHTML = svg; // this.changeHeightWidth(svg); if (bindFunctions) { bindFunctions(dashDiv); } } } catch (error) { console.error('Error rendering Mermaid:', error); } } changeHeightWidth(svgString: string) { const pattern = /width="([\d.]+)"\s*height="([\d.]+)"/; const match = svgString.match(pattern); if (match) { const width = parseFloat(match[1]); const height = parseFloat(match[2]); console.log(width); console.log(height); this.Document.width = width; this.Document.height = height; } } @action handleRenderClick = () => { this.mermaidCode = ''; if (this.inputValue) { this.generateMermaidCode(); } }; @action async generateMermaidCode() { console.log('Generating Mermaid Code'); const dashDiv = document.getElementById('dashDiv' + this.Document.title); if (dashDiv) { dashDiv.innerHTML = ''; } this.loading = true; let prompt = ''; prompt = 'Write this in mermaid code and only give me the mermaid code: ' + this.inputValue; // } const res = await gptAPICall(prompt, GPTCallType.MERMAID); this.loading = true; 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 this.mermaidCode = res; console.log('GPT call succeeded:' + res); this.errorMessage = ''; } this.renderMermaidAsync.call(this, this.removeWords(this.mermaidCode)); this.Document.gptMermaidCode = this.removeWords(this.mermaidCode); } removeWords(inputStrIn: string) { const inputStr = inputStrIn.replace('```mermaid', ''); return inputStr.replace('```', ''); } // method to convert the drawings on collection node side the mermaid code async convertDrawingToMermaidCode() { let mermaidCode = ''; let diagramExists = false; if (this.Document.data instanceof List) { const docArray: Doc[] = DocListCast(this.Document.data); const rectangleArray = docArray.filter(doc => doc.title === 'rectangle' || doc.title === 'circle'); const lineArray = docArray.filter(doc => doc.title === 'line' || doc.title === 'stroke'); const textArray = docArray.filter(doc => doc.type === 'rich text'); const timeoutPromise = () => new Promise(resolve => { setTimeout(resolve, 0); }); await timeoutPromise(); const inkStrokeArray = lineArray.map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())).filter(inkView => inkView?.ComponentView instanceof InkingStroke); if (inkStrokeArray[0] && inkStrokeArray.length === lineArray.length) { // if (this.isLeftRightDiagram(docArray)) { // mermaidCode = 'graph LR;'; // } else { // mermaidCode = 'graph TD;'; // } const 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++) { const inkScaleX = (inkingStrokeArray[j] as InkingStroke)?.inkScaledData().inkScaleX; const inkScaleY = (inkingStrokeArray[j] as InkingStroke)?.inkScaledData().inkScaleY; const inkStrokeXArray = (inkingStrokeArray[j] as InkingStroke) ?.inkScaledData() .inkData.map(coord => coord.X) .map(doc => doc * inkScaleX); const inkStrokeYArray = (inkingStrokeArray[j] as InkingStroke) ?.inkScaledData() .inkData.map(coord => coord.Y) .map(doc => doc * inkScaleY); // need to minX and minY to since the inkStroke.x and.y is not relative to the doc. so I have to do some calcluations const minX: number = Math.min(...inkStrokeXArray); const minY: number = Math.min(...inkStrokeYArray); const startX = inkStrokeXArray[0] - minX + (lineArray[j]?.x as number); const startY = inkStrokeYArray[0] - minY + (lineArray[j]?.y as number); const endX = inkStrokeXArray[inkStrokeXArray.length - 1] - minX + (lineArray[j].x as number); const endY = inkStrokeYArray[inkStrokeYArray.length - 1] - minY + (lineArray[j].y as number); 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') { diagramExists = true; const linkedDocs: Doc[] = LinkManager.Instance.getAllRelatedLinks(lineArray[j]).map(d => DocCast(LinkManager.getOppositeAnchor(d, lineArray[j]))); if (linkedDocs.length !== 0) { const linkedText = (linkedDocs[0].text as RichTextField).Text; mermaidCode += Math.abs(rectangle.x) + this.getTextInBox(rectangle, textArray) + '---|' + linkedText + '|' + Math.abs(rectangle2.x) + this.getTextInBox(rectangle2, textArray) + ';'; } else { mermaidCode += Math.abs(rectangle.x) + this.getTextInBox(rectangle, textArray) + '---' + Math.abs(rectangle2.x) + this.getTextInBox(rectangle2, textArray) + ';'; } } } } } } // this will save the text if (diagramExists) { this.Document.drawingMermaidCode = mermaidCode; } else { this.Document.drawingMermaidCode = ''; } } } } async lockInkStroke() { console.log('hello'); console.log( DocListCast(this.Document.data) .filter(doc => doc.title === 'rectangle') .map(doc => doc.x) ); if (this.Document.data instanceof List) { const docArray: Doc[] = DocListCast(this.Document.data); const rectangleArray = docArray.filter(doc => doc.title === 'rectangle' || doc.title === 'circle'); if (rectangleArray[0]) { console.log(rectangleArray[0].x); } const lineArray = docArray.filter(doc => doc.title === 'line' || doc.title === 'stroke'); const timeoutPromise = () => new Promise(resolve => { setTimeout(resolve, 0); }); await timeoutPromise(); const inkStrokeArray = lineArray.map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())).filter(inkView => inkView?.ComponentView instanceof InkingStroke); const inkingStrokeArray = inkStrokeArray.map(stroke => stroke?.ComponentView); for (let j = 0; j < lineArray.length; j++) { const inkScaleX = (inkingStrokeArray[j] as InkingStroke)?.inkScaledData().inkScaleX; const inkScaleY = (inkingStrokeArray[j] as InkingStroke)?.inkScaledData().inkScaleY; const inkStrokeXArray = (inkingStrokeArray[j] as InkingStroke) ?.inkScaledData() .inkData.map(coord => coord.X) .map(doc => doc * inkScaleX); const inkStrokeYArray = (inkingStrokeArray[j] as InkingStroke) ?.inkScaledData() .inkData.map(coord => coord.Y) .map(doc => doc * inkScaleY); // need to minX and minY to since the inkStroke.x and.y is not relative to the doc. so I have to do some calcluations const minX: number = Math.min(...inkStrokeXArray); const minY: number = Math.min(...inkStrokeYArray); const startX = inkStrokeXArray[0] - minX + (lineArray[j]?.x as number); const startY = inkStrokeYArray[0] - minY + (lineArray[j]?.y as number); const endX = inkStrokeXArray[inkStrokeXArray.length - 1] - minX + (lineArray[j].x as number); const endY = inkStrokeYArray[inkStrokeYArray.length - 1] - minY + (lineArray[j].y as number); let closestStartRect: Doc = lineArray[0]; let closestStartDistance = 9999999; let closestEndRect: Doc = lineArray[0]; let closestEndDistance = 9999999; rectangleArray.forEach(rectangle => { const midPoint = this.getMidPoint(rectangle); if (this.euclideanDistance(midPoint.X, midPoint.Y, startX, startY) < closestStartDistance && this.euclideanDistance(midPoint.X, midPoint.Y, endX, endY) < closestEndDistance) { if (this.euclideanDistance(midPoint.X, midPoint.Y, startX, startY) < this.euclideanDistance(midPoint.X, midPoint.Y, endX, endY)) { closestStartDistance = this.euclideanDistance(midPoint.X, midPoint.Y, startX, startY); closestStartRect = rectangle; } else { closestEndDistance = this.euclideanDistance(midPoint.X, midPoint.Y, startX, startY); closestEndRect = rectangle; } } else if (this.euclideanDistance(midPoint.X, midPoint.Y, startX, startY) < closestStartDistance) { closestStartDistance = this.euclideanDistance(midPoint.X, midPoint.Y, startX, startY); closestStartRect = rectangle; } else if (this.euclideanDistance(midPoint.X, midPoint.Y, endX, endY) < closestEndDistance) { closestEndDistance = this.euclideanDistance(midPoint.X, midPoint.Y, startX, startY); closestEndRect = rectangle; } }); const inkToDelete: Doc = lineArray[j]; if ( typeof closestStartRect.x === 'number' && typeof closestStartRect.y === 'number' && typeof closestEndRect.x === 'number' && typeof closestEndRect.y === 'number' && typeof closestStartRect.width === 'number' && typeof closestStartRect.height === 'number' && typeof closestEndRect.height === 'number' && typeof closestEndRect.width === 'number' ) { const points: PointData[] = [ { X: closestStartRect.x, Y: closestStartRect.y }, { X: closestStartRect.x, Y: closestStartRect.y }, { X: closestEndRect.x, Y: closestEndRect.y }, { X: closestEndRect.x, Y: closestEndRect.y }, ]; let inkX = 0; let inkY = 0; if (this.getMidPoint(closestEndRect).X < this.getMidPoint(closestStartRect).X) { inkX = this.getMidPoint(closestEndRect).X; } else { inkX = this.getMidPoint(closestStartRect).X; } if (this.getMidPoint(closestEndRect).Y < this.getMidPoint(closestStartRect).Y) { inkY = this.getMidPoint(closestEndRect).Y; } else { inkY = this.getMidPoint(closestStartRect).Y; } const newInkDoc = Docs.Create.AudioDocument(''); // get rid of this!! // const newInkDoc:Doc=Docs.Create.InkDocument( // points, // { title: 'stroke', // x: inkX, // y: inkY, // strokeWidth: Math.abs(closestEndRect.x+closestEndRect.width/2-closestStartRect.x-closestStartRect.width/2), // _height: Math.abs(closestEndRect.y+closestEndRect.height/2-closestStartRect.y-closestStartRect.height/2), // stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore // 1) DocumentManager.Instance.AddViewRenderedCb(this.Document, docViewForYourCollection => { if (docViewForYourCollection && docViewForYourCollection.ComponentView) { if (docViewForYourCollection.ComponentView.addDocument && docViewForYourCollection.ComponentView.removeDocument) { docViewForYourCollection.ComponentView?.removeDocument(inkToDelete); docViewForYourCollection.ComponentView?.addDocument(newInkDoc); // const bruh2= DocListCast(this.Document.data).filter(doc => doc.title === 'line' || doc.title === 'stroke').map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())).filter(inkView => inkView?.ComponentView instanceof InkingStroke).map(stroke => stroke?.ComponentView); // console.log(bruh2) // console.log((bruh2[0] as InkingStroke)?.inkScaledData()) } } }); } } } } getMidPoint(rectangle: Doc) { let midPoint = { X: 0, Y: 0 }; if (typeof rectangle.x === 'number' && typeof rectangle.width === 'number' && typeof rectangle.y === 'number' && typeof rectangle.height === 'number') { midPoint = { X: rectangle.x + rectangle.width / 2, Y: rectangle.y + rectangle.height / 2 }; } return midPoint; } euclideanDistance(x1: number, y1: number, x2: number, y2: number): number { const deltaX = x2 - x1; const deltaY = y2 - y1; return Math.sqrt(deltaX * deltaX + deltaY * deltaY); } // isLeftRightDiagram = (docArray: Doc[]) => { // const filteredDocs = docArray.filter(doc => doc.title === 'rectangle' || doc.title === 'circle'); // const xDoc = filteredDocs.map(doc => doc.x) as number[]; // const minX = Math.min(...xDoc); // const xWidthDoc = filteredDocs.map(doc => { // if (typeof doc.x === 'number' && typeof doc.width === 'number') { // return doc.x + doc.width; // } // }) as number[]; // const maxX = Math.max(...xWidthDoc); // const yDoc = filteredDocs.map(doc => doc.y) as number[]; // const minY = Math.min(...yDoc); // const yHeightDoc = filteredDocs.map(doc => { // if (typeof doc.x === 'number' && typeof doc.width === 'number') { // return doc.x + doc.width; // } // }) as number[]; // const maxY = Math.max(...yHeightDoc); // if (maxX - minX > maxY - minY) { // return true; // } // return false; // }; getTextInBox = (box: Doc, richTextArray: Doc[]): string => { for (let i = 0; i < richTextArray.length; i++) { const 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; } return false; }; drawingButton = () => { this.Document.menuState = 'drawing'; }; gptButton = () => { this.Document.menuState = 'gpt'; }; mermaidButton = () => { this.Document.menuState = 'mermaidCode'; }; optionButton = () => { this.Document.menuState = 'option'; }; renderOption(): React.ReactNode { return (
); } renderDrawing(): React.ReactNode { return (
{!this.Document.mermaidCode &&

Click the red pen icon to flip onto the collection side and draw a diagram with ink

}
); } renderGpt(): React.ReactNode { return (