/* eslint-disable prettier/prettier */ /* eslint-disable jsx-a11y/control-has-associated-label */ import mermaid from 'mermaid'; import { action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { RichTextField } from '../../../fields/RichTextField'; import { Cast, DocCast, NumCast } from '../../../fields/Types'; import { Gestures } from '../../../pen-gestures/GestureTypes'; 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 { undoable } from '../../util/UndoManager'; 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'; @observer export class DiagramBox extends ViewBoxAnnotatableComponent() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DiagramBox, fieldKey); } static isPointInBox = (box: Doc, pt: number[]): boolean => { if (typeof pt[0] === 'number' && typeof box.x === 'number' && typeof box.y === 'number' && typeof pt[1] === 'number') { return pt[0] < box.x + NumCast(box.width) && pt[0] > box.x && pt[1] > box.y && pt[1] < box.y + NumCast(box.height); } return false; }; constructor(props: FieldViewProps) { super(props); makeObservable(this); } @observable _showCode = false; @observable _inputValue = ''; @observable _generating = false; @observable _errorMessage = ''; @computed get mermaidcode() { return Cast(this.Document[DocData].text, RichTextField, null)?.Text ?? ''; } 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 }, }); // when a new doc/text/ink/shape is created in the freeform view, this generates the corresponding mermaid diagram code reaction( () => DocListCast(this.Document.data), docArray => docArray.length && this.convertDrawingToMermaidCode(docArray), { fireImmediately: true } ); } renderMermaid = (str: string) => { try { return mermaid.render('graph' + Date.now(), str); } catch (error) { return { svg: '', bindFunctions: undefined }; } }; renderMermaidAsync = async (mermaidCode: string, dashDiv: HTMLDivElement) => { try { const { svg, bindFunctions } = await this.renderMermaid(mermaidCode); dashDiv.innerHTML = svg; bindFunctions?.(dashDiv); } catch (error) { console.error('Error rendering Mermaid:', error); } }; setMermaidCode = undoable((res: string) => { this.Document[DocData].text = new RichTextField( JSON.stringify({ doc: { type: 'doc', content: [ { type: 'code_block', content: [ { type: 'text', text: `^@mermaids\n` }, { type: 'text', text: this.removeWords(res) }, ], }, ], }, selection: { type: 'text', anchor: 1, head: 1 }, }), res ); }, 'set mermaid code'); generateMermaidCode = action(() => { this._generating = true; const prompt = 'Write this in mermaid code and only give me the mermaid code: ' + this._inputValue; gptAPICall(prompt, GPTCallType.MERMAID).then( action(res => { this._generating = false; if (res === 'Error connecting with API.') { this._errorMessage = 'GPT call failed; please try again.'; } // If GPT call succeeded, set mermaid code on Doc which will trigger a rendering if _showCode is false else if (res && this.isValidCode(res)) { this.setMermaidCode(res); this._errorMessage = ''; } else { this._errorMessage = 'GPT call succeeded but invalid html; please try again.'; } }) ); }); isValidCode = (html: string) => (html ? true : false); removeWords = (inputStrIn: string) => inputStrIn.replace('```mermaid', '').replace(`^@mermaids`, '').replace('```', ''); // method to convert the drawings on collection node side the mermaid code convertDrawingToMermaidCode = async (docArray: Doc[]) => { const rectangleArray = docArray.filter(doc => doc.title === Gestures.Rectangle || doc.title === Gestures.Circle); const lineArray = docArray.filter(doc => doc.title === Gestures.Line || doc.title === Gestures.Stroke); const textArray = docArray.filter(doc => doc.type === DocumentType.RTF); await new Promise(resolve => setTimeout(resolve)); const inkStrokeArray = lineArray.map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())).filter(inkView => inkView?.ComponentView instanceof InkingStroke); if (inkStrokeArray[0] && inkStrokeArray.length === lineArray.length) { let mermaidCode = `graph TD \n`; const inkingStrokeArray = inkStrokeArray.map(stroke => stroke?.ComponentView as InkingStroke).filter(stroke => stroke); for (const rectangle of rectangleArray) { for (const inkStroke of inkingStrokeArray) { const inkData = inkStroke.inkScaledData(); const { inkScaleX, inkScaleY } = inkData; const inkStrokeXArray = inkData.inkData.map(coord => coord.X * inkScaleX); const inkStrokeYArray = inkData.inkData.map(coord => coord.Y * 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 offX = Math.min(...inkStrokeXArray) - NumCast(inkStroke.Document.x); const offY = Math.min(...inkStrokeYArray) - NumCast(inkStroke.Document.y); const startX = inkStrokeXArray[0] - offX; const startY = inkStrokeYArray[0] - offY; const endX = inkStrokeXArray.lastElement() - offX; const endY = inkStrokeYArray.lastElement() - offY; if (DiagramBox.isPointInBox(rectangle, [startX, startY])) { for (const rectangle2 of rectangleArray) { if (DiagramBox.isPointInBox(rectangle2, [endX, endY])) { const linkedDocs = LinkManager.Instance.getAllRelatedLinks(inkStroke.Document).map(d => DocCast(LinkManager.getOppositeAnchor(d, inkStroke.Document))); const linkedDocText = Cast(linkedDocs[0]?.text, RichTextField, null)?.Text; const linkText = linkedDocText ? `|${linkedDocText}|` : ''; mermaidCode += ' ' + Math.abs(NumCast(rectangle.x)) + this.getTextInBox(rectangle, textArray) + '-->' + linkText + Math.abs(NumCast(rectangle2.x)) + this.getTextInBox(rectangle2, textArray) + `\n`; } } } } this.setMermaidCode(mermaidCode); } } }; getTextInBox = (box: Doc, richTextArray: Doc[]) => { for (const textDoc of richTextArray) { if (DiagramBox.isPointInBox(box, [NumCast(textDoc.x), NumCast(textDoc.y)])) { switch (box.title) { case Gestures.Rectangle: return '(' + ((textDoc.text as RichTextField)?.Text ?? '') + ')'; case Gestures.Circle: return '((' + ((textDoc.text as RichTextField)?.Text ?? '') + '))'; default: } // prettier-ignore } } return '( )'; }; renderGpt(): React.ReactNode { return (
e.key === 'Enter' && this.generateMermaidCode())} onChange={action(e => (this._inputValue = e.target.value))} /> (this._showCode = !this._showCode))} />
{this._showCode ? ( ) : this._generating ? (
) : (
r && this.renderMermaidAsync.call(this, this.removeWords(this.mermaidcode), r)}> {this._errorMessage || 'Type a prompt to generate a diagram'}
)}
); } exampleButton = () => { if (this.isExampleMenuOpen) { this.isExampleMenuOpen = false; } else { this.isExampleMenuOpen = true; } }; flowButton = () => { this.createInputValue = `flowchart TD A[Christmas] -->|Get money| B(Go shopping) B --> C{Let me think} C -->|One| D[Laptop] C -->|Two| E[iPhone] C -->|Three| F[fa:fa-car Car]`; this.renderMermaidAsync(this.createInputValue); }; pieButton = () => { this.createInputValue = `pie title Pets adopted by volunteers "Dogs" : 386 "Cats" : 85 "Rats" : 15`; this.renderMermaidAsync(this.createInputValue); }; timelineButton = () => { this.createInputValue = `gantt title A Gantt Diagram dateFormat YYYY-MM-DD section Section A task :a1, 2014-01-01, 30d Another task :after a1 , 20d section Another Task in sec :2014-01-12 , 12d another task : 24d`; this.renderMermaidAsync(this.createInputValue); }; classButton = () => { this.createInputValue = `classDiagram Animal <|-- Duck Animal <|-- Fish Animal <|-- Zebra Animal : +int age Animal : +String gender Animal: +isMammal() Animal: +mate() class Duck{ +String beakColor +swim() +quack() } class Fish{ -int sizeInFeet -canEat() } class Zebra{ +bool is_wild +run() }`; this.renderMermaidAsync(this.createInputValue); }; mindmapButton = () => { this.createInputValue = `mindmap root((mindmap)) Origins Long history ::icon(fa fa-book) Popularisation British popular psychology author Tony Buzan Research On effectivness
and features On Automatic creation Uses Creative techniques Strategic planning Argument mapping Tools Pen and paper Mermaid`; this.renderMermaidAsync(this.createInputValue); }; handleInputChangeEditor = (e: React.ChangeEvent) => { if (typeof e.target.value === 'string') { this.createInputValue = e.target.value; this.renderMermaidAsync(e.target.value); } }; removeWhitespace(str: string): string { return str.replace(/\s+/g, ''); } autoResize(element: HTMLTextAreaElement): void { element.style.height = '5px'; element.style.height = element.scrollHeight + 'px'; } timeline = `gantt title College Timeline dateFormat YYYY-MM-DD section Semester 1 Orientation :done, des1, 2023-08-01, 2023-08-03 Classes Start :active, des2, 2023-08-04, 2023-12-15 Midterm Exams : des3, 2023-10-15, 2023-10-20 End of Semester : des4, 2023-12-16, 2023-12-20 section Semester 2 Classes Start : des5, 2024-01-10, 2024-05-15 Spring Break : des6, 2024-03-15, 2024-03-22 Midterm Exams : des7, 2024-03-25, 2024-03-30 Final Exams : des8, 2024-05-10, 2024-05-15 section Summer Break Internship : des9, 2024-06-01, 2024-08-31 section Semester 3 Classes Start : des10, 2024-09-01, 2025-12-15 Midterm Exams : des11, 2024-11-15, 2024-11-20 End of Semester : des12, 2025-12-16, 2025-12-20 section Semester 4 Classes Start : des13, 2025-01-10, 2025-05-15 Spring Break : des14, 2025-03-15, 2025-03-22 Midterm Exams : des15, 2025-03-25, 2025-03-30 Final Exams : des16, 2025-05-10, 2025-05-15 Graduation : des17, 2025-05-20, 2025-05-21`; render() { this.switchRenderDiv(); return (
{this.renderDiv}
); } } Docs.Prototypes.TemplateMap.set(DocumentType.DIAGRAM, { layout: { view: DiagramBox, dataField: 'data' }, options: { _height: 300, // _layout_fitWidth: true, _layout_nativeDimEditable: true, _layout_reflowVertical: true, _layout_reflowHorizontal: true, waitForDoubleClickToClick: 'always', systemIcon: 'BsGlobe', }, });