diff options
| author | Nathan-SR <144961007+Nathan-SR@users.noreply.github.com> | 2024-06-03 13:33:37 -0400 |
|---|---|---|
| committer | Nathan-SR <144961007+Nathan-SR@users.noreply.github.com> | 2024-06-03 13:33:37 -0400 |
| commit | 9e77f980e7704999ef0a1c1845d660bccb13ff8a (patch) | |
| tree | 14ca0da5915e4382a7bcb15f7d0b241941c8291f /src/client/views/nodes/DiagramBox.tsx | |
| parent | 1be63695875c9242fba43d580465e8765cf3991d (diff) | |
| parent | 202e994515392892676f8f080852db1e32b8dbd3 (diff) | |
Merge branch 'master' into nathan-starter
Diffstat (limited to 'src/client/views/nodes/DiagramBox.tsx')
| -rw-r--r-- | src/client/views/nodes/DiagramBox.tsx | 291 |
1 files changed, 291 insertions, 0 deletions
diff --git a/src/client/views/nodes/DiagramBox.tsx b/src/client/views/nodes/DiagramBox.tsx new file mode 100644 index 000000000..32969fa53 --- /dev/null +++ b/src/client/views/nodes/DiagramBox.tsx @@ -0,0 +1,291 @@ +import mermaid from 'mermaid'; +import { action, 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 { RichTextField } from '../../../fields/RichTextField'; +import { DocCast, NumCast } 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'; + +@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>(); + constructor(props: FieldViewProps) { + super(props); + makeObservable(this); + } + + @observable inputValue = ''; + @observable loading = false; + @observable errorMessage = ''; + @observable mermaidCode = ''; + + @action handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { + this.inputValue = e.target.value; + }; + async 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 + reaction( + () => DocListCast(this.Document.data), + () => this.convertDrawingToMermaidCode(), + { fireImmediately: true } + ); + } + 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; + if (bindFunctions) { + 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('```', ''); + } + // 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) + ';'; + } + } + } + } + } + } + // 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 = ''; + } + } + } + } + }); + } + } + } + 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 ?? '') + '))'; + } + } + } + } + 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> + </div> + ); + } +} + +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' }, +}); |
