diff options
Diffstat (limited to 'src/client/views')
| -rw-r--r-- | src/client/views/nodes/DiagramBox.scss | 88 | ||||
| -rw-r--r-- | src/client/views/nodes/DiagramBox.tsx | 310 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentContentsView.tsx | 2 |
3 files changed, 400 insertions, 0 deletions
diff --git a/src/client/views/nodes/DiagramBox.scss b/src/client/views/nodes/DiagramBox.scss new file mode 100644 index 000000000..d2749f1ad --- /dev/null +++ b/src/client/views/nodes/DiagramBox.scss @@ -0,0 +1,88 @@ +.DIYNodeBox { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + .DIYNodeBox-wrapper { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + .DIYNodeBox { + /* existing code */ + + .DIYNodeBox-iframe { + height: 100%; + width: 100%; + border: none; + + } + } + + .search-bar { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + padding: 10px; + + input[type="text"] { + flex: 1; + margin-right: 10px; + } + + button { + padding: 5px 10px; + } + } + + .content { + flex: 1; + display: flex; + justify-content: center; + align-items: center; + width:100%; + height:100%; + .diagramBox{ + flex: 1; + display: flex; + justify-content: center; + align-items: center; + width:100%; + height:100%; + svg{ + flex: 1; + display: flex; + justify-content: center; + align-items: center; + width:100%; + height:100%; + } + } + } + + .loading-circle { + position: relative; + width: 50px; + height: 50px; + border-radius: 50%; + border: 3px solid #ccc; + border-top-color: #333; + animation: spin 1s infinite linear; + } + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/DiagramBox.tsx b/src/client/views/nodes/DiagramBox.tsx new file mode 100644 index 000000000..1813e6097 --- /dev/null +++ b/src/client/views/nodes/DiagramBox.tsx @@ -0,0 +1,310 @@ +import { makeObservable, observable, action ,reaction} from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent'; +import { StyleProp } from '../StyleProvider'; +import './DiagramBox.scss'; +import { FieldView, FieldViewProps } from './FieldView'; +import { PinProps, PresBox } from './trails'; +import mermaid from 'mermaid'; +import { Doc, DocListCast } from '../../../fields/Doc'; +import { List } from '../../../fields/List'; +import { RichTextField } from '../../../fields/RichTextField'; +import { ContextMenu } from '../ContextMenu'; +import { gptAPICall, GPTCallType } from '../../apis/gpt/GPT'; +import { ChatCompletionMessageParam } from 'openai/resources/chat/completions'; +import OpenAI, { ClientOptions } from 'openai'; +import { line } from 'd3'; +import { InkingStroke } from '../InkingStroke'; +import { DocumentManager } from '../../util/DocumentManager'; +import { C } from '@fullcalendar/core/internal-common'; +import { Docs } from '../../documents/Documents'; +import { NumCast } from '../../../fields/Types'; + +@observer +export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { + 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'; + 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]){ + if(typeof mermaidCodeDoc[0].title=='string'){ + console.log(mermaidCodeDoc[0].title) + if(mermaidCodeDoc[0].title!=""){ + this.renderMermaidAsync(mermaidCodeDoc[0].title); + } + } + } + else{ + DocumentManager.Instance.AddViewRenderedCb(this.Document, (docViewForYourCollection) => { + if (docViewForYourCollection && docViewForYourCollection.ComponentView) { + if (docViewForYourCollection.ComponentView.addDocument&&docViewForYourCollection.ComponentView.removeDocument) { + let newDoc=Docs.Create.TextDocument("mermaidCodeTitle", { title: "", x: 9999 + NumCast(this.layoutDoc._width), y: 9999 }) + docViewForYourCollection.ComponentView?.addDocument(newDoc) + } + } + }); + } + reaction( + () => DocListCast(this.Document.data), + docs => { + console.log("reaction happened") + this.createMermaidCode(); + + }, + { 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) => { + return await mermaid.render('graph' + Date.now(), str); + }; + + async renderMermaidAsync(mermaidCode: string) { + try { + const { svg, bindFunctions } = await this.renderMermaid(mermaidCode); + const dashDiv = document.getElementById('dashDiv'); + if (dashDiv) { + dashDiv.innerHTML = svg; + if (bindFunctions) { + bindFunctions(dashDiv); + } + } + } catch (error) { + console.error('Error rendering Mermaid:', error); + } + } + @action handleRenderClick = () => { + // Call the GPT model and get the HTML output + // const modelOutput = getHtmlOutput(this.inputValue); + // this.htmlCode = modelOutput; + this.generateMermaidCode(); + }; + printTitle(){ + 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') + console.log(mermaidCodeDoc[0].title) + if(mermaidCodeDoc[0]){ + console.log(mermaidCodeDoc[0].title) + if(typeof mermaidCodeDoc[0].title=='string'){ + console.log(mermaidCodeDoc[0].title) + if(mermaidCodeDoc[0].title!=""){ + console.log("you have to see me") + } + } + } + } + @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") + //} + let 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) => { + return true; + }; + testInkingStroke = () => { + if (this.Document.data instanceof List) { + let docArray: Doc[] = DocListCast(this.Document.data); + let lineArray = docArray.filter(doc => doc.title == 'line' || doc.title == 'stroke'); + setTimeout(() => { + let inkStrokeArray = lineArray.map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())).filter(inkView => inkView?.ComponentView instanceof InkingStroke); + console.log(inkStrokeArray); + }); + } + }; + + removeWords(inputStr: string) { + inputStr = inputStr.replace('```mermaid', ''); + return inputStr.replace('```', ''); + } + + async createMermaidCode() { + let mermaidCode="" + let diagramExists=false + if (this.Document.data instanceof List) { + let docArray: Doc[] = DocListCast(this.Document.data); + let rectangleArray = docArray.filter(doc => doc.title == 'rectangle' || doc.title == 'circle'); + let lineArray = docArray.filter(doc => doc.title == 'line' || doc.title == 'stroke'); + let textArray = docArray.filter(doc => doc.type == 'rich text'); + const timeoutPromise = () => + new Promise(resolve => { + setTimeout(resolve, 0); + }); + await timeoutPromise(); + lineArray.map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())).forEach(inkView => { + const componentView = inkView?.ComponentView; + if (componentView) { + console.log(componentView.constructor.name, componentView); // Print instance type and object + } + }); + let inkStrokeArray = lineArray.map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())).filter(inkView => inkView?.ComponentView instanceof InkingStroke); + console.log(lineArray.length) + console.log(inkStrokeArray.length) + if (inkStrokeArray[0]) { + mermaidCode = 'graph LR;'; + let 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++) { + let inkScaleX = (inkingStrokeArray[j] as InkingStroke)?.inkScaledData().inkScaleX; + let inkScaleY = (inkingStrokeArray[j] as InkingStroke)?.inkScaledData().inkScaleY; + let inkStrokeXArray = (inkingStrokeArray[j] as InkingStroke)?.inkScaledData().inkData.map(coord => coord.X).map(doc=>doc*inkScaleX); + let inkStrokeYArray = (inkingStrokeArray[j] as InkingStroke)?.inkScaledData().inkData.map(coord => coord.Y).map(doc=>doc*inkScaleY); + //console.log(inkingStrokeArray.length) + //console.log(lineArray.length) + let minX: number = Math.min(...inkStrokeXArray); + let minY: number = Math.min(...inkStrokeYArray); + let startX = inkStrokeXArray[0] - minX + (lineArray[j]?.x as number); + let startY = inkStrokeYArray[0] - minY + (lineArray[j]?.y as number); + let endX = inkStrokeXArray[inkStrokeXArray.length - 1]- minX + (lineArray[j].x as number); + let 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 + mermaidCode += Math.abs(rectangle.x) + this.getTextInBox(rectangle, textArray) + '---' + Math.abs(rectangle2.x) + this.getTextInBox(rectangle2, textArray) + ';'; + + } + } + } + } + } + } + } + DocumentManager.Instance.AddViewRenderedCb(this.Document, (docViewForYourCollection) => { + if (docViewForYourCollection && docViewForYourCollection.ComponentView) { + if (docViewForYourCollection.ComponentView.addDocument&&docViewForYourCollection.ComponentView.removeDocument) { + let docArray: Doc[] = DocListCast(this.Document.data); + docArray=docArray.filter(doc => doc.type == 'rich text') + let mermaidCodeDoc = docArray.filter(doc => (doc.text as RichTextField).Text == 'mermaidCodeTitle') + if(mermaidCodeDoc[0]){ + if(diagramExists){ + mermaidCodeDoc[0].title=mermaidCode + } + else{ + mermaidCodeDoc[0].title="" + } + } + } + } + }); + let docArray: Doc[] = DocListCast(this._props.Document.data) + //console.log(mermaidCode) + + } + + getTextInBox = (box: Doc, richTextArray: Doc[]): string => { + for (let i = 0; i < richTextArray.length; i++) { + let 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; + } else { + 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 onClick={this.handleRenderClick}>Generate</button> + </div> + <div className="content"> + {this.mermaidCode ? ( + <div id="dashDiv" className="diagramBox"></div> + ) : ( + <div>{this.loading ? <div className="loading-circle"></div> : <div>{this.errorMessage ? this.errorMessage : 'Insert prompt to generate diagram'}</div>}</div> + )} + </div> + </div> + </div> + ); + } +} diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index e729e2fa2..f32466b8c 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -23,6 +23,7 @@ import { DashWebRTCVideo } from '../webcam/DashWebRTCVideo'; import { YoutubeBox } from './../../apis/youtube/YoutubeBox'; import { AudioBox } from './AudioBox'; import { ComparisonBox } from './ComparisonBox'; +import { DiagramBox } from './DiagramBox'; import { DataVizBox } from './DataVizBox/DataVizBox'; import './DocumentView.scss'; import { EquationBox } from './EquationBox'; @@ -264,6 +265,7 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte DataVizBox, HTMLtag, ComparisonBox, + DiagramBox, LoadingBox, PhysicsSimulationBox, SchemaRowBox, |
