aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/DiagramBox.tsx
diff options
context:
space:
mode:
authorZachary Zhang <zacharyzhang7@gmail.com>2024-08-31 00:46:29 -0400
committerZachary Zhang <zacharyzhang7@gmail.com>2024-08-31 00:46:29 -0400
commit196294f331496262bef256da8b8e9dbc80288bea (patch)
tree85ff27b7a8070585f9a5ef71dff63566e03232ba /src/client/views/nodes/DiagramBox.tsx
parent0cf61501ec9be34294935f01973c1bd9cad6d267 (diff)
parentc36607691e0b7f5c04f3209a64958f5e51ddd785 (diff)
Merge branch 'master' into zach-starter
Diffstat (limited to 'src/client/views/nodes/DiagramBox.tsx')
-rw-r--r--src/client/views/nodes/DiagramBox.tsx643
1 files changed, 140 insertions, 503 deletions
diff --git a/src/client/views/nodes/DiagramBox.tsx b/src/client/views/nodes/DiagramBox.tsx
index 5a712b8b0..79cf39152 100644
--- a/src/client/views/nodes/DiagramBox.tsx
+++ b/src/client/views/nodes/DiagramBox.tsx
@@ -1,45 +1,42 @@
/* 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 { 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, BoolCast } 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 { PointData } from '../../../pen-gestures/GestureTypes';
-import { InkField } from '../../../fields/InkField';
-
-enum menuState {
- option,
- mermaidCode,
- drawing,
- gpt,
- justCreated,
-}
+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 menuState = menuState.justCreated;
@observable renderDiv: React.ReactNode;
@observable inputValue = '';
@observable createInputValue = '';
@@ -47,12 +44,21 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@observable errorMessage = '';
@observable mermaidCode = '';
@observable isExampleMenuOpen = false;
+ @observable _showCode = false;
+ @observable _inputValue = '';
+ @observable _generating = false;
+ @observable _errorMessage = '';
@action handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
this.inputValue = e.target.value;
console.log(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',
@@ -61,510 +67,147 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
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();
- }
+ // 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 = 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;
- // this.changeHeightWidth(svg);
- if (bindFunctions) {
- bindFunctions(dashDiv);
- }
- }
+ dashDiv.innerHTML = svg;
+ 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);
+ 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');
- 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) + ';';
- }
- }
- }
- }
- }
+ 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.';
}
- // this will save the text
- if (diagramExists) {
- this.Document.drawingMermaidCode = mermaidCode;
+ // 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.Document.drawingMermaidCode = '';
+ this._errorMessage = 'GPT call succeeded but invalid html; please try again.';
}
- }
- }
- }
- 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)
+ });
+ isValidCode = (html: string) => (html ? true : false);
+ removeWords = (inputStrIn: string) => inputStrIn.replace('```mermaid', '').replace(`^@mermaids`, '').replace('```', '');
- 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);
+ // 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 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())
+ 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`;
}
}
- });
- }
- }
- }
- }
- 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 ?? '') + '))';
}
}
+ this.setMermaidCode(mermaidCode);
}
}
- 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 (
- <div className="buttonCollections">
- <button type="button" onClick={this.drawingButton}>
- Drawing - Create diagram from ink drawing
- </button>
- <button type="button" onClick={this.gptButton}>
- GPT - Generate diagram with AI prompt
- </button>
- <button type="button" onClick={this.mermaidButton}>
- Mermaid Editor - Create diagram with mermaid code
- </button>
- </div>
- );
- }
- renderDrawing(): React.ReactNode {
- return (
- <div ref={this._dragRef} className="DiagramBox-wrapper">
- <div className="content">
- <div className="topBar">
- <button className="backButtonDrawing" type="button" onClick={this.optionButton}>
- Back
- </button>
- {!this.Document.mermaidCode && <p>Click the red pen icon to flip onto the collection side and draw a diagram with ink</p>}
- </div>
- <div id={'dashDiv' + this.Document.title} className="diagramBox" />
- </div>
- </div>
- );
- }
- renderGpt(): React.ReactNode {
- return (
- <div ref={this._dragRef} className="DiagramBox-wrapper">
- <div className="content">
- <div className="search-bar">
- <button className="backButton" type="button" onClick={this.optionButton}>
- Back
- </button>
- <textarea value={this.inputValue} placeholder="Enter GPT prompt" onChange={this.handleInputChange} onInput={e => this.autoResize(e.target as HTMLTextAreaElement)} />
- <div className="rightButtons">
- <button className="generateButton" type="button" onClick={this.handleRenderClick}>
- Generate
- </button>
- <button className="convertButton" type="button" onClick={this.handleConvertButton}>
- Edit
- </button>
- </div>
- </div>
- {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>
- );
- }
- handleConvertButton = () => {
- this.Document.menuState = 'mermaidCode';
- if (typeof this.Document.gptMermaidCode === 'string') {
- this.createInputValue = this.removeFirstEmptyLine(this.Document.gptMermaidCode);
- console.log(this.Document.gptMermaidCode);
- this.renderMermaidAsync(this.Document.gptMermaidCode);
+ 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 '( )';
};
- removeFirstEmptyLine(input: string): string {
- const lines = input.split('\n');
- let emptyLineRemoved = false;
- const resultLines = lines.filter(line => {
- if (!emptyLineRemoved && line.trim() === '') {
- emptyLineRemoved = true;
- return false;
- }
- return true;
- });
- return resultLines.join('\n');
- }
- renderMermaidCode(): React.ReactNode {
+ render() {
return (
- <div ref={this._dragRef} className="DiagramBox-wrapper">
- <div className="contentCode">
- <div className="search-bar">
- <button className="backButton" type="button" onClick={this.optionButton}>
- Back
- </button>
- <button className="exampleButton" type="button" onClick={this.exampleButton}>
- Examples
- </button>
- </div>
- {this.isExampleMenuOpen && (
- <div className="exampleButtonContainer">
- <button type="button" onClick={this.flowButton}>
- Flow
- </button>
- <button type="button" onClick={this.pieButton}>
- Pie
- </button>
- <button type="button" onClick={this.timelineButton}>
- Timeline
- </button>
- <button type="button" onClick={this.classButton}>
- Class
- </button>
- <button type="button" onClick={this.mindmapButton}>
- Mindmap
- </button>
+ <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>
)}
- <textarea value={this.createInputValue} placeholder="Enter Mermaid Code" onChange={this.handleInputChangeEditor} />
- <div id={'dashDiv' + this.Document.title} className="diagramBox" />
</div>
</div>
);
@@ -583,14 +226,12 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
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
@@ -602,7 +243,6 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
section Another
Task in sec :2014-01-12 , 12d
another task : 24d`;
- this.renderMermaidAsync(this.createInputValue);
};
classButton = () => {
this.createInputValue = `classDiagram
@@ -626,7 +266,6 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
+bool is_wild
+run()
}`;
- this.renderMermaidAsync(this.createInputValue);
};
mindmapButton = () => {
this.createInputValue = `mindmap
@@ -646,12 +285,10 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
Tools
Pen and paper
Mermaid`;
- this.renderMermaidAsync(this.createInputValue);
};
handleInputChangeEditor = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
if (typeof e.target.value === 'string') {
this.createInputValue = e.target.value;
- this.renderMermaidAsync(e.target.value);
}
};
removeWhitespace(str: string): string {
@@ -686,17 +323,17 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
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 (
- <div ref={this._ref} className="DiagramBox">
- {this.renderDiv}
- </div>
- );
- }
}
Docs.Prototypes.TemplateMap.set(DocumentType.DIAGRAM, {
- layout: { view: DiagramBox, dataField: 'dadta' },
- options: { _height: 700, _width: 700, _layout_fitWidth: false, _layout_nativeDimEditable: true, _layout_reflowVertical: true, waitForDoubleClickToClick: 'always', _layout_reflowHorizontal: true, 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',
+ },
});