From e57584a1be9d428fb40fc789494a7ac0ac14fb84 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 19 Aug 2024 16:42:56 -0400
Subject: extensive fixes to DiagramBox

---
 src/client/views/nodes/DiagramBox.tsx | 360 +++++++++++++---------------------
 1 file changed, 141 insertions(+), 219 deletions(-)

(limited to 'src/client/views/nodes/DiagramBox.tsx')

diff --git a/src/client/views/nodes/DiagramBox.tsx b/src/client/views/nodes/DiagramBox.tsx
index 32969fa53..36deb2d8d 100644
--- a/src/client/views/nodes/DiagramBox.tsx
+++ b/src/client/views/nodes/DiagramBox.tsx
@@ -1,284 +1,198 @@
 import mermaid from 'mermaid';
-import { action, makeObservable, observable, reaction } from 'mobx';
+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 { List } from '../../../fields/List';
+import { DocData } from '../../../fields/DocSymbols';
 import { RichTextField } from '../../../fields/RichTextField';
-import { DocCast, NumCast } from '../../../fields/Types';
+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 { FormattedTextBox } from './formattedText/FormattedTextBox';
 
 @observer
 export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     public static LayoutString(fieldKey: string) {
         return FieldView.LayoutString(DiagramBox, fieldKey);
     }
-    private _ref: React.RefObject<HTMLDivElement> = React.createRef();
-    private _dragRef = React.createRef<HTMLDivElement>();
+    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 inputValue = '';
-    @observable loading = false;
-    @observable errorMessage = '';
-    @observable mermaidCode = '';
+    @observable _showCode = false;
+    @observable _inputValue = '';
+    @observable _generating = false;
+    @observable _errorMessage = '';
 
-    @action handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-        this.inputValue = e.target.value;
-    };
-    async componentDidMount() {
+    @computed get mermaidcode() {
+        return Cast(this.Document[DocData].text, RichTextField, null)?.Text ?? '';
+    }
+
+    componentDidMount() {
         this._props.setContentViewBox?.(this);
         mermaid.initialize({
             securityLevel: 'loose',
             startOnLoad: true,
             flowchart: { useMaxWidth: true, htmlLabels: true, curve: 'cardinal' },
         });
-        this.mermaidCode = 'asdasdasd';
-        const docArray: Doc[] = DocListCast(this.Document.data);
-        let mermaidCodeDoc = docArray.filter(doc => doc.type === 'rich text');
-        mermaidCodeDoc = mermaidCodeDoc.filter(doc => (doc.text as RichTextField).Text === 'mermaidCodeTitle');
-        if (mermaidCodeDoc[0]) {
-            if (typeof mermaidCodeDoc[0].title === 'string') {
-                console.log(mermaidCodeDoc[0].title);
-                if (mermaidCodeDoc[0].title !== '') {
-                    this.renderMermaidAsync(mermaidCodeDoc[0].title);
-                }
-            }
-        }
-        // this will create a text doc far away where the user cant to save the mermaid code, where it will then be accessed when flipped to the diagram box side
-        // the code is stored in the title since it is much easier to change than in the text
-        else {
-            DocumentManager.Instance.AddViewRenderedCb(this.Document, docViewForYourCollection => {
-                if (docViewForYourCollection && docViewForYourCollection.ComponentView) {
-                    if (docViewForYourCollection.ComponentView.addDocument && docViewForYourCollection.ComponentView.removeDocument) {
-                        const newDoc = Docs.Create.TextDocument('mermaidCodeTitle', { title: '', x: 9999 + NumCast(this.layoutDoc._width), y: 9999 });
-                        docViewForYourCollection.ComponentView?.addDocument(newDoc);
-                    }
-                }
-            });
-        }
-        console.log(this.Document.title);
-        // 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
+        // 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),
-            () => this.convertDrawingToMermaidCode(),
+            docArray => docArray.length && this.convertDrawingToMermaidCode(docArray),
             { fireImmediately: true }
         );
     }
-    renderMermaid = async (str: string) => {
+    renderMermaid = (str: string) => {
         try {
-            const { svg, bindFunctions } = await this.mermaidDiagram(str);
-            return { svg, bindFunctions };
+            return mermaid.render('graph' + Date.now(), str);
         } 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) {
+    renderMermaidAsync = async (mermaidCode: string, dashDiv: HTMLDivElement) => {
         try {
             const { svg, bindFunctions } = await this.renderMermaid(mermaidCode);
-            const dashDiv = document.getElementById('dashDiv' + this.Document.title);
-            if (dashDiv) {
-                dashDiv.innerHTML = svg;
-                if (bindFunctions) {
-                    bindFunctions(dashDiv);
-                }
-            }
+            dashDiv.innerHTML = svg;
+            bindFunctions?.(dashDiv);
         } catch (error) {
             console.error('Error rendering Mermaid:', error);
         }
-    }
-    @action handleRenderClick = () => {
-        this.generateMermaidCode();
     };
-    @action async generateMermaidCode() {
-        console.log('Generating Mermaid Code');
-        this.loading = true;
-        let prompt = '';
-        // let docArray: Doc[] = DocListCast(this.Document.data);
-        // let mermaidCodeDoc = docArray.filter(doc => doc.type == 'rich text')
-        // mermaidCodeDoc=mermaidCodeDoc.filter(doc=>(doc.text as RichTextField).Text=='mermaidCodeTitle')
-        // if(mermaidCodeDoc[0]){
-        //     console.log(mermaidCodeDoc[0].title)
-        //     if(typeof mermaidCodeDoc[0].title=='string'){
-        //         console.log(mermaidCodeDoc[0].title)
-        //         if(mermaidCodeDoc[0].title!=""){
-        //             prompt="Edit this code "+this.inputValue+": "+mermaidCodeDoc[0].title
-        //             console.log("you have to see me")
-        //         }
-        //     }
-        // }
-        // else{
-        prompt = 'Write this in mermaid code and only give me the mermaid code: ' + this.inputValue;
-        console.log('there is no text save');
-        // }
-        const res = await gptAPICall(prompt, 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) => true;
-    removeWords(inputStrIn: string) {
-        const inputStr = inputStrIn.replace('```mermaid', '');
-        return inputStr.replace('```', '');
-    }
+
+    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
-    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);
-            console.log(inkStrokeArray.length);
-            console.log(lineArray.length);
-            if (inkStrokeArray[0] && inkStrokeArray.length === lineArray.length) {
-                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);
-                        console.log(inkingStrokeArray.length);
-                        console.log(lineArray.length);
-                        // 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])));
-                                    console.log(linkedDocs.length);
-                                    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) + ';';
-                                    }
-                                }
+    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 will save the text
-                DocumentManager.Instance.AddViewRenderedCb(this.Document, docViewForYourCollection => {
-                    if (docViewForYourCollection && docViewForYourCollection.ComponentView) {
-                        if (docViewForYourCollection.ComponentView.addDocument && docViewForYourCollection.ComponentView.removeDocument) {
-                            let docs: Doc[] = DocListCast(this.Document.data);
-                            docs = docs.filter(doc => doc.type === 'rich text');
-                            const mermaidCodeDoc = docs.filter(doc => (doc.text as RichTextField).Text === 'mermaidCodeTitle');
-                            if (mermaidCodeDoc[0]) {
-                                if (diagramExists) {
-                                    mermaidCodeDoc[0].title = mermaidCode;
-                                } else {
-                                    mermaidCodeDoc[0].title = '';
-                                }
-                            }
-                        }
-                    }
-                });
+                this.setMermaidCode(mermaidCode);
             }
         }
-    }
-    testInkingStroke = () => {
-        if (this.Document.data instanceof List) {
-            const docArray: Doc[] = DocListCast(this.Document.data);
-            const lineArray = docArray.filter(doc => doc.title === 'line' || doc.title === 'stroke');
-            setTimeout(() => {
-                const inkStrokeArray = lineArray.map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())).filter(inkView => inkView?.ComponentView instanceof InkingStroke);
-                console.log(inkStrokeArray);
-            });
-        }
     };
-    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 ?? '') + '))';
-                    }
-                }
+
+    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 '( )';
     };
-    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;
-    };
 
     render() {
         return (
-            <div ref={this._ref} className="DIYNodeBox">
-                <div ref={this._dragRef} className="DIYNodeBox-wrapper">
-                    <div className="search-bar">
-                        <input type="text" value={this.inputValue} onChange={this.handleInputChange} />
-                        <button type="button" onClick={this.handleRenderClick}>
-                            Generate
-                        </button>
-                    </div>
-                    <div className="content">
-                        {this.mermaidCode ? (
-                            <div id={'dashDiv' + this.Document.title} className="diagramBox" />
-                        ) : (
-                            <div>{this.loading ? <div className="loading-circle" /> : <div>{this.errorMessage ? this.errorMessage : 'Insert prompt to generate diagram'}</div>}</div>
-                        )}
-                    </div>
+            <div className="DIYNodeBox">
+                <div className="DIYNodeBox-searchbar">
+                    <input type="text" value={this._inputValue} onKeyDown={action(e => e.key === 'Enter' && this.generateMermaidCode())} onChange={action(e => (this._inputValue = e.target.value))} />
+                    <button type="button" onClick={this.generateMermaidCode}>
+                        Gen
+                    </button>
+                    <input type="checkbox" onClick={action(() => (this._showCode = !this._showCode))} />
+                </div>
+                <div className="DIYNodeBox-content">
+                    {this._showCode ? (
+                        <FormattedTextBox {...this._props} fieldKey="text" />
+                    ) : this._generating ? (
+                        <div className="loading-circle" />
+                    ) : (
+                        <div className="diagramBox" ref={r => r && this.renderMermaidAsync.call(this, this.removeWords(this.mermaidcode), r)}>
+                            {this._errorMessage || 'Type a prompt to generate a diagram'}
+                        </div>
+                    )}
                 </div>
             </div>
         );
@@ -286,6 +200,14 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 }
 
 Docs.Prototypes.TemplateMap.set(DocumentType.DIAGRAM, {
-    layout: { view: DiagramBox, dataField: 'dadta' },
-    options: { _height: 300, _layout_fitWidth: true, _layout_nativeDimEditable: true, _layout_reflowVertical: true, waitForDoubleClickToClick: 'always', systemIcon: 'BsGlobe' },
+    layout: { view: DiagramBox, dataField: 'data' },
+    options: {
+        _height: 300, //
+        _layout_fitWidth: true,
+        _layout_nativeDimEditable: true,
+        _layout_reflowVertical: true,
+        _layout_reflowHorizontal: true,
+        waitForDoubleClickToClick: 'always',
+        systemIcon: 'BsGlobe',
+    },
 });
-- 
cgit v1.2.3-70-g09d2

