aboutsummaryrefslogtreecommitdiff
path: root/src/client/views
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views')
-rw-r--r--src/client/views/DocumentButtonBar.tsx22
-rw-r--r--src/client/views/DocumentDecorations.tsx5
-rw-r--r--src/client/views/GestureOverlay.tsx8
-rw-r--r--src/client/views/GlobalKeyHandler.ts2
-rw-r--r--src/client/views/InkTranscription.scss5
-rw-r--r--src/client/views/InkTranscription.tsx765
-rw-r--r--src/client/views/InkingStroke.tsx4
-rw-r--r--src/client/views/LightboxView.tsx22
-rw-r--r--src/client/views/MainView.tsx14
-rw-r--r--src/client/views/ViewBoxInterface.ts2
-rw-r--r--src/client/views/collections/CollectionCarousel3DView.tsx2
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx11
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx113
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx3
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx13
-rw-r--r--src/client/views/global/globalScripts.ts6
-rw-r--r--src/client/views/nodes/DiagramBox.scss250
-rw-r--r--src/client/views/nodes/DiagramBox.tsx619
-rw-r--r--src/client/views/nodes/ScreenshotBox.tsx2
-rw-r--r--src/client/views/smartdraw/AnnotationPalette.scss10
-rw-r--r--src/client/views/smartdraw/AnnotationPalette.tsx382
-rw-r--r--src/client/views/smartdraw/SmartDrawHandler.scss3
-rw-r--r--src/client/views/smartdraw/SmartDrawHandler.tsx437
23 files changed, 2149 insertions, 551 deletions
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 487868169..15e90ac2a 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -12,7 +12,7 @@ import * as React from 'react';
import { FaEdit } from 'react-icons/fa';
import { returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick } from '../../ClientUtils';
import { emptyFunction } from '../../Utils';
-import { Doc } from '../../fields/Doc';
+import { Doc, DocListCast } from '../../fields/Doc';
import { Cast, DocCast } from '../../fields/Types';
import { DocUtils, IsFollowLinkScript } from '../documents/DocUtils';
import { CalendarManager } from '../util/CalendarManager';
@@ -31,6 +31,8 @@ import { DocumentLinksButton } from './nodes/DocumentLinksButton';
import { DocumentView } from './nodes/DocumentView';
import { OpenWhere } from './nodes/OpenWhere';
import { DashFieldView } from './nodes/formattedText/DashFieldView';
+import { AnnotationPalette } from './smartdraw/AnnotationPalette';
+import { DocData } from '../../fields/DocSymbols';
@observer
export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (DocumentView | undefined)[]; stack?: any }> {
@@ -241,6 +243,23 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
);
}
+ @undoBatch
+ saveAnno = action(async (targetDoc: Doc) => {
+ await AnnotationPalette.addToPalette(targetDoc);
+ });
+
+ @computed
+ get saveAnnoButton() {
+ const targetDoc = this.view0?.Document;
+ return !targetDoc ? null : (
+ <Tooltip title={<div className="dash-tooltip">{targetDoc.savedAsAnno ? 'Saved as Annotation!' : 'Save to Annotation Palette'}</div>}>
+ <div className="documentButtonBar-icon" style={{ color: 'white' }} onClick={() => this.saveAnno(targetDoc)}>
+ <FontAwesomeIcon className="documentdecorations-icon" icon={targetDoc.savedAsAnno ? 'clipboard-check' : 'file-arrow-down'} />
+ </div>
+ </Tooltip>
+ );
+ }
+
@computed
get shareButton() {
const targetDoc = this.view0?.Document;
@@ -450,6 +469,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
<div className="documentButtonBar-button">{this.templateButton}</div>
{!DocumentView.Selected().some(v => v.allLinks.length) ? null : <div className="documentButtonBar-button">{this.followLinkButton}</div>}
<div className="documentButtonBar-button">{this.pinButton}</div>
+ <div className="documentButtonBar-button">{this.saveAnnoButton}</div>
<div className="documentButtonBar-button">{this.recordButton}</div>
<div className="documentButtonBar-button">{this.calendarButton}</div>
{!Doc.UserDoc().documentLinksButton_fullMenu ? null : <div className="documentButtonBar-button">{this.shareButton}</div>}
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 93c3e3338..ae4f5b98b 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -229,6 +229,11 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
if (iconViewDoc.activeFrame) {
iconViewDoc.opacity = 0; // bcz: hacky ... allows inkMasks and other documents to be "turned off" without removing them from the animated collection which allows them to function properly in a presenation.
} else {
+ // to mark annotations as no longer saved if they're deleted from the palette
+ const dragFactory: Doc = DocCast(iconView.Document.dragFactory);
+ if (dragFactory && DocCast(dragFactory.cloneOf).savedAsAnno) {
+ DocCast(dragFactory.cloneOf).savedAsAnno = undefined;
+ }
iconView._props.removeDocument?.(iconView.Document);
}
});
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index e3e252593..e961bc031 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -94,6 +94,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
@action
onPointerDown = (e: React.PointerEvent) => {
+ console.log('pointerdown');
if (!(e.target as any)?.className?.toString().startsWith('lm_')) {
if ([InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
this._points.push({ X: e.clientX, Y: e.clientY });
@@ -132,6 +133,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
}
@action
onPointerUp = () => {
+ console.log('pointer up');
DocumentView.DownDocView = undefined;
if (this._points.length > 1) {
const B = this.svgBounds;
@@ -147,7 +149,9 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
else {
// need to decide when to turn gestures back on
const result = points.length > 2 && GestureUtils.GestureRecognizer.Recognize([points]);
+ console.log(points);
let actionPerformed = false;
+ console.log(result);
if (Doc.UserDoc().recognizeGestures && result && result.Score > 0.7) {
switch (result.Name) {
case Gestures.Line:
@@ -156,10 +160,14 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
case Gestures.Circle:
this.makeBezierPolygon(result.Name, true);
actionPerformed = this.dispatchGesture(result.Name);
+ console.log(result.Name);
+ console.log();
break;
case Gestures.Scribble:
console.log('scribble');
break;
+ case Gestures.RightAngle:
+ console.log('RightAngle');
default:
}
}
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 7d01bbabb..562827db5 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -162,7 +162,7 @@ export class KeyManager {
case 'delete':
case 'backspace':
if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
- if (DocumentView.LightboxDoc()) {
+ if (DocumentView.LightboxDoc() && !DocumentView.Selected().length) {
DocumentView.SetLightboxDoc(undefined);
DocumentView.DeselectAll();
} else if (!window.getSelection()?.toString()) DocumentDecorations.Instance.onCloseClick(true);
diff --git a/src/client/views/InkTranscription.scss b/src/client/views/InkTranscription.scss
index bbb0a1afa..18d6b8b10 100644
--- a/src/client/views/InkTranscription.scss
+++ b/src/client/views/InkTranscription.scss
@@ -2,4 +2,9 @@
.error-msg {
display: none !important;
}
+ .ms-editor{
+ .smartguide{
+ top:1000px;
+ }
+ }
} \ No newline at end of file
diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx
index 1ed8de1be..3f90df7d1 100644
--- a/src/client/views/InkTranscription.tsx
+++ b/src/client/views/InkTranscription.tsx
@@ -1,349 +1,416 @@
-// import * as iink from 'iink-js';
-// import { action, observable } from 'mobx';
-// import * as React from 'react';
-// import { Doc, DocListCast } from '../../fields/Doc';
-// import { InkData, InkField, InkTool } from '../../fields/InkField';
-// import { Cast, DateCast, NumCast } from '../../fields/Types';
-// import { aggregateBounds } from '../../Utils';
-// import { DocumentType } from '../documents/DocumentTypes';
-// import { CollectionFreeFormView } from './collections/collectionFreeForm';
-// import { InkingStroke } from './InkingStroke';
-// import './InkTranscription.scss';
-
-// /**
-// * Class component that handles inking in writing mode
-// */
-// export class InkTranscription extends React.Component {
-// static Instance: InkTranscription;
-
-// @observable _mathRegister: any= undefined;
-// @observable _mathRef: any= undefined;
-// @observable _textRegister: any= undefined;
-// @observable _textRef: any= undefined;
-// private lastJiix: any;
-// private currGroup?: Doc;
-
-// constructor(props: Readonly<{}>) {
-// super(props);
-
-// InkTranscription.Instance = this;
-// }
-
-// componentWillUnmount() {
-// this._mathRef.removeEventListener('exported', (e: any) => this.exportInk(e, this._mathRef));
-// this._textRef.removeEventListener('exported', (e: any) => this.exportInk(e, this._textRef));
-// }
-
-// @action
-// setMathRef = (r: any) => {
-// if (!this._mathRegister) {
-// this._mathRegister = r
-// ? iink.register(r, {
-// recognitionParams: {
-// type: 'MATH',
-// protocol: 'WEBSOCKET',
-// server: {
-// host: 'cloud.myscript.com',
-// applicationKey: process.env.IINKJS_APP,
-// hmacKey: process.env.IINKJS_HMAC,
-// websocket: {
-// pingEnabled: false,
-// autoReconnect: true,
-// },
-// },
-// iink: {
-// math: {
-// mimeTypes: ['application/x-latex', 'application/vnd.myscript.jiix'],
-// },
-// export: {
-// jiix: {
-// strokes: true,
-// },
-// },
-// },
-// },
-// })
-// : null;
-// }
-
-// r?.addEventListener('exported', (e: any) => this.exportInk(e, this._mathRef));
-
-// return (this._mathRef = r);
-// };
-
-// @action
-// setTextRef = (r: any) => {
-// if (!this._textRegister) {
-// this._textRegister = r
-// ? iink.register(r, {
-// recognitionParams: {
-// type: 'TEXT',
-// protocol: 'WEBSOCKET',
-// server: {
-// host: 'cloud.myscript.com',
-// applicationKey: '7277ec34-0c2e-4ee1-9757-ccb657e3f89f',
-// hmacKey: 'f5cb18f2-1f95-4ddb-96ac-3f7c888dffc1',
-// websocket: {
-// pingEnabled: false,
-// autoReconnect: true,
-// },
-// },
-// iink: {
-// text: {
-// mimeTypes: ['text/plain'],
-// },
-// export: {
-// jiix: {
-// strokes: true,
-// },
-// },
-// },
-// },
-// })
-// : null;
-// }
-
-// r?.addEventListener('exported', (e: any) => this.exportInk(e, this._textRef));
-
-// return (this._textRef = r);
-// };
-
-// /**
-// * Handles processing Dash Doc data for ink transcription.
-// *
-// * @param groupDoc the group which contains the ink strokes we want to transcribe
-// * @param inkDocs the ink docs contained within the selected group
-// * @param math boolean whether to do math transcription or not
-// */
-// transcribeInk = (groupDoc: Doc | undefined, inkDocs: Doc[], math: boolean) => {
-// if (!groupDoc) return;
-// const validInks = inkDocs.filter(s => s.type === DocumentType.INK);
-
-// const strokes: InkData[] = [];
-// const times: number[] = [];
-// validInks
-// .filter(i => Cast(i[Doc.LayoutFieldKey(i)], InkField))
-// .forEach(i => {
-// const d = Cast(i[Doc.LayoutFieldKey(i)], InkField, null);
-// const inkStroke = DocumentManager.Instance.getDocumentView(i)?.ComponentView as InkingStroke;
-// strokes.push(d.inkData.map(pd => inkStroke.ptToScreen({ X: pd.X, Y: pd.Y })));
-// times.push(DateCast(i.author_date).getDate().getTime());
-// });
-
-// this.currGroup = groupDoc;
-
-// const pointerData = { events: strokes.map((stroke, i) => this.inkJSON(stroke, times[i])) };
-// const processGestures = false;
-
-// if (math) {
-// this._mathRef.editor.pointerEvents(pointerData, processGestures);
-// } else {
-// this._textRef.editor.pointerEvents(pointerData, processGestures);
-// }
-// };
-
-// /**
-// * Converts the Dash Ink Data to JSON.
-// *
-// * @param stroke The dash ink data
-// * @param time the time of the stroke
-// * @returns json object representation of ink data
-// */
-// inkJSON = (stroke: InkData, time: number) => {
-// return {
-// pointerType: 'PEN',
-// pointerId: 1,
-// x: stroke.map(point => point.X),
-// y: stroke.map(point => point.Y),
-// t: new Array(stroke.length).fill(time),
-// p: new Array(stroke.length).fill(1.0),
-// };
-// };
-
-// /**
-// * Creates subgroups for each word for the whole text transcription
-// * @param wordInkDocMap the mapping of words to ink strokes (Ink Docs)
-// */
-// subgroupsTranscriptions = (wordInkDocMap: Map<string, Doc[]>) => {
-// // iterate through the keys of wordInkDocMap
-// wordInkDocMap.forEach(async (inkDocs: Doc[], word: string) => {
-// const selected = inkDocs.slice();
-// if (!selected) {
-// return;
-// }
-// const ctx = await Cast(selected[0].embedContainer, Doc);
-// if (!ctx) {
-// return;
-// }
-// const docView: CollectionFreeFormView = DocumentManager.Instance.getDocumentView(ctx)?.ComponentView as CollectionFreeFormView;
-
-// if (!docView) return;
-// const marqViewRef = docView._marqueeViewRef.current;
-// if (!marqViewRef) return;
-// this.groupInkDocs(selected, docView, word);
-// });
-// };
-
-// /**
-// * Event listener function for when the 'exported' event is heard.
-// *
-// * @param e the event objects
-// * @param ref the ref to the editor
-// */
-// exportInk = (e: any, ref: any) => {
-// const exports = e.detail.exports;
-// if (exports) {
-// if (exports['application/x-latex']) {
-// const latex = exports['application/x-latex'];
-// if (this.currGroup) {
-// this.currGroup.text = latex;
-// this.currGroup.title = latex;
-// }
-
-// ref.editor.clear();
-// } else if (exports['text/plain']) {
-// if (exports['application/vnd.myscript.jiix']) {
-// this.lastJiix = JSON.parse(exports['application/vnd.myscript.jiix']);
-// // map timestamp to strokes
-// const timestampWord = new Map<number, string>();
-// this.lastJiix.words.map((word: any) => {
-// if (word.items) {
-// word.items.forEach((i: { id: string; timestamp: string; X: Array<number>; Y: Array<number>; F: Array<number> }) => {
-// const ms = Date.parse(i.timestamp);
-// timestampWord.set(ms, word.label);
-// });
-// }
-// });
-
-// const wordInkDocMap = new Map<string, Doc[]>();
-// if (this.currGroup) {
-// const docList = DocListCast(this.currGroup.data);
-// docList.forEach((inkDoc: Doc) => {
-// // just having the times match up and be a unique value (actual timestamp doesn't matter)
-// const ms = DateCast(inkDoc.author_date).getDate().getTime() + 14400000;
-// const word = timestampWord.get(ms);
-// if (!word) {
-// return;
-// }
-// const entry = wordInkDocMap.get(word);
-// if (entry) {
-// entry.push(inkDoc);
-// wordInkDocMap.set(word, entry);
-// } else {
-// const newEntry = [inkDoc];
-// wordInkDocMap.set(word, newEntry);
-// }
-// });
-// if (this.lastJiix.words.length > 1) this.subgroupsTranscriptions(wordInkDocMap);
-// }
-// }
-// const text = exports['text/plain'];
-
-// if (this.currGroup) {
-// this.currGroup.transcription = text;
-// this.currGroup.title = text.split('\n')[0];
-// }
-
-// ref.editor.clear();
-// }
-// }
-// };
-
-// /**
-// * Creates the ink grouping once the user leaves the writing mode.
-// */
-// createInkGroup() {
-// // TODO nda - if document being added to is a inkGrouping then we can just add to that group
-// if (Doc.ActiveTool === InkTool.Write) {
-// CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => {
-// // TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those
-// const selected = ffView.unprocessedDocs;
-// const newCollection = this.groupInkDocs(
-// selected.filter(doc => doc.embedContainer),
-// ffView
-// );
-// ffView.unprocessedDocs = [];
-
-// InkTranscription.Instance.transcribeInk(newCollection, selected, false);
-// });
-// }
-// CollectionFreeFormView.collectionsWithUnprocessedInk.clear();
-// }
-
-// /**
-// * Creates the groupings for a given list of ink docs on a specific doc view
-// * @param selected: the list of ink docs to create a grouping of
-// * @param docView: the view in which we want the grouping to be created
-// * @param word: optional param if the group we are creating is a word (subgrouping individual words)
-// * @returns a new collection Doc or undefined if the grouping fails
-// */
-// groupInkDocs(selected: Doc[], docView: CollectionFreeFormView, word?: string): Doc | undefined {
-// const bounds: { x: number; y: number; width?: number; height?: number }[] = [];
-
-// // calculate the necessary bounds from the selected ink docs
-// selected.map(
-// action(d => {
-// const x = NumCast(d.x);
-// const y = NumCast(d.y);
-// const width = NumCast(d._width);
-// const height = NumCast(d._height);
-// bounds.push({ x, y, width, height });
-// })
-// );
-
-// // calculate the aggregated bounds
-// const aggregBounds = aggregateBounds(bounds, 0, 0);
-// const marqViewRef = docView._marqueeViewRef.current;
-
-// // set the vals for bounds in marqueeView
-// if (marqViewRef) {
-// marqViewRef._downX = aggregBounds.x;
-// marqViewRef._downY = aggregBounds.y;
-// marqViewRef._lastX = aggregBounds.r;
-// marqViewRef._lastY = aggregBounds.b;
-// }
-
-// // map through all the selected ink strokes and create the groupings
-// selected.map(
-// action(d => {
-// const dx = NumCast(d.x);
-// const dy = NumCast(d.y);
-// delete d.x;
-// delete d.y;
-// delete d.activeFrame;
-// delete d._timecodeToShow; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
-// delete d._timecodeToHide; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
-// // calculate pos based on bounds
-// if (marqViewRef?.Bounds) {
-// d.x = dx - marqViewRef.Bounds.left - marqViewRef.Bounds.width / 2;
-// d.y = dy - marqViewRef.Bounds.top - marqViewRef.Bounds.height / 2;
-// }
-// return d;
-// })
-// );
-// docView.props.removeDocument?.(selected);
-// // Gets a collection based on the selected nodes using a marquee view ref
-// const newCollection = marqViewRef?.getCollection(selected, undefined, true);
-// if (newCollection) {
-// newCollection.width = NumCast(newCollection._width);
-// newCollection.height = NumCast(newCollection._height);
-// // if the grouping we are creating is an individual word
-// if (word) {
-// newCollection.title = word;
-// }
-// }
-
-// // nda - bug: when deleting a stroke before leaving writing mode, delete the stroke from unprocessed ink docs
-// newCollection && docView.props.addDocument?.(newCollection);
-// return newCollection;
-// }
-
-// render() {
-// return (
-// <div className="ink-transcription">
-// <div className="math-editor" ref={this.setMathRef} touch-action="none"></div>
-// <div className="text-editor" ref={this.setTextRef} touch-action="none"></div>
-// </div>
-// );
-// }
-// }
+import * as iink from 'iink-ts';
+import { action, observable } from 'mobx';
+import * as React from 'react';
+import { Doc, DocListCast } from '../../fields/Doc';
+import { InkData, InkField, InkTool } from '../../fields/InkField';
+import { Cast, DateCast, ImageCast, NumCast, StrCast } from '../../fields/Types';
+import { aggregateBounds } from '../../Utils';
+import { DocumentType } from '../documents/DocumentTypes';
+import { CollectionFreeFormView } from './collections/collectionFreeForm';
+import { InkingStroke } from './InkingStroke';
+import './InkTranscription.scss';
+import { Docs } from '../documents/Documents';
+import { DocumentView } from './nodes/DocumentView';
+import { Number } from 'mongoose';
+import { NumberArray } from 'd3';
+import { ImageField } from '../../fields/URLField';
+import { gptHandwriting } from '../apis/gpt/GPT';
+import * as fs from 'fs';
+import { URLField } from '../../fields/URLField';
+/**
+ * Class component that handles inking in writing mode
+ */
+export class InkTranscription extends React.Component {
+ static Instance: InkTranscription;
+
+ @observable _mathRegister: any = undefined;
+ @observable _mathRef: any = undefined;
+ @observable _textRegister: any = undefined;
+ @observable _textRef: any = undefined;
+ @observable iinkEditor: any = undefined;
+ private lastJiix: any;
+ private currGroup?: Doc;
+ private collectionFreeForm?: CollectionFreeFormView;
+
+ constructor(props: Readonly<{}>) {
+ super(props);
+
+ InkTranscription.Instance = this;
+ }
+
+ componentWillUnmount() {
+ // this._mathRef.removeEventListener('exported', (e: any) => this.exportInk(e, this._mathRef));
+ // this._textRef.removeEventListener('exported', (e: any) => this.exportInk(e, this._textRef));
+ }
+
+ @action
+ setMathRef = async (r: any) => {
+ if (!this._textRegister && r) {
+ let editor;
+ const options = {
+ configuration: {
+ server: {
+ scheme: 'https',
+ host: 'cloud.myscript.com',
+ applicationKey: 'c0901093-5ac5-4454-8e64-0def0f13f2ca',
+ hmacKey: 'f6465cca-1856-4492-a6a4-e2395841be2f',
+ protocol: 'WEBSOCKET',
+ },
+ recognition: {
+ type: 'TEXT',
+ },
+ },
+ };
+
+ editor = new iink.Editor(r, options as any);
+
+ await editor.initialize();
+
+ this._textRegister = r;
+ r?.addEventListener('exported', (e: any) => this.exportInk(e, this._textRef));
+
+ return (this._textRef = r);
+ }
+ };
+ @action
+ setTextRef = async (r: any) => {
+ if (!this._textRegister && r) {
+ let editor;
+ const options = {
+ configuration: {
+ server: {
+ scheme: 'https',
+ host: 'cloud.myscript.com',
+ applicationKey: 'c0901093-5ac5-4454-8e64-0def0f13f2ca',
+ hmacKey: 'f6465cca-1856-4492-a6a4-e2395841be2f',
+ protocol: 'WEBSOCKET',
+ },
+ recognition: {
+ type: 'TEXT',
+ },
+ },
+ };
+
+ editor = new iink.Editor(r, options as any);
+
+ await editor.initialize();
+ this.iinkEditor = editor;
+ this._textRegister = r;
+ r?.addEventListener('exported', (e: any) => this.exportInk(e, this._textRef));
+
+ return (this._textRef = r);
+ }
+ };
+
+ /**
+ * Handles processing Dash Doc data for ink transcription.
+ *
+ * @param groupDoc the group which contains the ink strokes we want to transcribe
+ * @param inkDocs the ink docs contained within the selected group
+ * @param math boolean whether to do math transcription or not
+ */
+ transcribeInk = (groupDoc: Doc | undefined, inkDocs: Doc[], math: boolean) => {
+ if (!groupDoc) return;
+ const validInks = inkDocs.filter(s => s.type === DocumentType.INK);
+
+ const strokes: InkData[] = [];
+ const times: number[] = [];
+ validInks
+ .filter(i => Cast(i[Doc.LayoutFieldKey(i)], InkField))
+ .forEach(i => {
+ const d = Cast(i[Doc.LayoutFieldKey(i)], InkField, null);
+ const inkStroke = DocumentView.getDocumentView(i)?.ComponentView as InkingStroke;
+ strokes.push(d.inkData.map(pd => inkStroke.ptToScreen({ X: pd.X, Y: pd.Y })));
+ times.push(DateCast(i.author_date).getDate().getTime());
+ });
+ console.log(strokes);
+ console.log(this.convertPointsToString(strokes));
+ console.log(this.convertPointsToString2(strokes));
+ this.currGroup = groupDoc;
+ const pointerData = strokes.map((stroke, i) => this.inkJSON(stroke, times[i]));
+ const processGestures = false;
+ if (math) {
+ console.log('math');
+ this.iinkEditor.importPointEvents(pointerData);
+ } else {
+ this.iinkEditor.importPointEvents(pointerData);
+ }
+ };
+ convertPointsToString(points: InkData[]): string {
+ return points[0].map(point => `new Point(${point.X}, ${point.Y})`).join(',\n ');
+ }
+ convertPointsToString2(points: InkData[]): string {
+ return points[0].map(point => `(${point.X},${point.Y})`).join(',');
+ }
+
+ /**
+ * Converts the Dash Ink Data to JSON.
+ *
+ * @param stroke The dash ink data
+ * @param time the time of the stroke
+ * @returns json object representation of ink data
+ */
+ inkJSON = (stroke: InkData, time: number) => {
+ interface strokeData {
+ x: number;
+ y: number;
+ t: number;
+ p: number;
+ }
+ let strokeObjects: strokeData[] = [];
+ stroke.forEach(point => {
+ let tempObject: strokeData = {
+ x: point.X,
+ y: point.Y,
+ t: time,
+ p: 1.0,
+ };
+ strokeObjects.push(tempObject);
+ });
+ return {
+ pointerType: 'PEN',
+ pointerId: 1,
+ pointers: strokeObjects,
+ };
+ };
+
+ /**
+ * Creates subgroups for each word for the whole text transcription
+ * @param wordInkDocMap the mapping of words to ink strokes (Ink Docs)
+ */
+ subgroupsTranscriptions = (wordInkDocMap: Map<string, Doc[]>) => {
+ // iterate through the keys of wordInkDocMap
+ wordInkDocMap.forEach(async (inkDocs: Doc[], word: string) => {
+ const selected = inkDocs.slice();
+ if (!selected) {
+ return;
+ }
+ const ctx = await Cast(selected[0].embedContainer, Doc);
+ if (!ctx) {
+ return;
+ }
+ const docView: CollectionFreeFormView = DocumentView.getDocumentView(ctx)?.ComponentView as CollectionFreeFormView;
+ // DocumentManager.Instance.getDocumentView(ctx)?.ComponentView as CollectionFreeFormView;
+
+ if (!docView) return;
+ const marqViewRef = docView._marqueeViewRef.current;
+ if (!marqViewRef) return;
+ this.groupInkDocs(selected, docView, word);
+ });
+ };
+
+ /**
+ * Event listener function for when the 'exported' event is heard.
+ *
+ * @param e the event objects
+ * @param ref the ref to the editor
+ */
+ exportInk = async (e: any, ref: any) => {
+ const exports = e.detail['application/vnd.myscript.jiix'];
+ if (exports) {
+ if (exports['type'] == 'Math') {
+ const latex = exports['application/x-latex'];
+ if (this.currGroup) {
+ this.currGroup.text = latex;
+ this.currGroup.title = latex;
+ }
+
+ ref.editor.clear();
+ } else if (exports['type'] == 'Text') {
+ if (exports['application/vnd.myscript.jiix']) {
+ this.lastJiix = JSON.parse(exports['application/vnd.myscript.jiix']);
+ // map timestamp to strokes
+ const timestampWord = new Map<number, string>();
+ this.lastJiix.words.map((word: any) => {
+ if (word.items) {
+ word.items.forEach((i: { id: string; timestamp: string; X: Array<number>; Y: Array<number>; F: Array<number> }) => {
+ const ms = Date.parse(i.timestamp);
+ timestampWord.set(ms, word.label);
+ });
+ }
+ });
+
+ const wordInkDocMap = new Map<string, Doc[]>();
+ if (this.currGroup) {
+ const docList = DocListCast(this.currGroup.data);
+ docList.forEach((inkDoc: Doc) => {
+ // just having the times match up and be a unique value (actual timestamp doesn't matter)
+ const ms = DateCast(inkDoc.author_date).getDate().getTime() + 14400000;
+ const word = timestampWord.get(ms);
+ if (!word) {
+ return;
+ }
+ const entry = wordInkDocMap.get(word);
+ if (entry) {
+ entry.push(inkDoc);
+ wordInkDocMap.set(word, entry);
+ } else {
+ const newEntry = [inkDoc];
+ wordInkDocMap.set(word, newEntry);
+ }
+ });
+ if (this.lastJiix.words.length > 1) this.subgroupsTranscriptions(wordInkDocMap);
+ }
+ }
+ const text = exports['label'];
+
+ if (this.currGroup && text) {
+ DocumentView.getDocumentView(this.currGroup)?.ComponentView?.updateIcon?.();
+ this.currGroup.transcription = text;
+ this.currGroup.title = text;
+ let image = await this.getIcon();
+ const pathname = image?.url.href as string;
+ console.log(image?.url);
+ console.log(image);
+ const { href } = (image as URLField).url;
+ const hrefParts = href.split('.');
+ const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
+ let response;
+ try {
+ const hrefBase64 = await this.imageUrlToBase64(hrefComplete);
+ response = await gptHandwriting(hrefBase64);
+ console.log(response);
+ } catch (error) {
+ console.log('bad things have happened');
+ }
+ const textBoxText = 'iink: ' + text + '\n' + '\n' + 'ChatGPT: ' + response;
+ if (!this.currGroup.hasTextBox) {
+ const newDoc = Docs.Create.TextDocument(textBoxText, { title: '', x: this.currGroup.x as number, y: (this.currGroup.y as number) + (this.currGroup.height as number) });
+ newDoc.height = 200;
+ this.collectionFreeForm?.addDocument(newDoc);
+ this.currGroup.hasTextBox = true;
+ }
+ ref.editor.clear();
+ }
+ }
+ }
+ };
+ async getIcon() {
+ const docView = DocumentView.getDocumentView(this.currGroup);
+ console.log(this.currGroup);
+ if (docView) {
+ console.log(docView);
+ docView.ComponentView?.updateIcon?.();
+ return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000));
+ }
+ return undefined;
+ }
+ imageUrlToBase64 = async (imageUrl: string): Promise<string> => {
+ try {
+ const response = await fetch(imageUrl);
+ const blob = await response.blob();
+
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.readAsDataURL(blob);
+ reader.onloadend = () => resolve(reader.result as string);
+ reader.onerror = error => reject(error);
+ });
+ } catch (error) {
+ console.error('Error:', error);
+ throw error;
+ }
+ };
+
+ /**
+ * Creates the ink grouping once the user leaves the writing mode.
+ */
+ createInkGroup() {
+ // TODO nda - if document being added to is a inkGrouping then we can just add to that group
+ if (Doc.ActiveTool === InkTool.Write) {
+ CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => {
+ // TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those
+ const selected = ffView.unprocessedDocs;
+ const newCollection = this.groupInkDocs(
+ selected.filter(doc => doc.embedContainer),
+ ffView
+ );
+ ffView.unprocessedDocs = [];
+
+ InkTranscription.Instance.transcribeInk(newCollection, selected, false);
+ });
+ }
+ CollectionFreeFormView.collectionsWithUnprocessedInk.clear();
+ }
+
+ /**
+ * Creates the groupings for a given list of ink docs on a specific doc view
+ * @param selected: the list of ink docs to create a grouping of
+ * @param docView: the view in which we want the grouping to be created
+ * @param word: optional param if the group we are creating is a word (subgrouping individual words)
+ * @returns a new collection Doc or undefined if the grouping fails
+ */
+ groupInkDocs(selected: Doc[], docView: CollectionFreeFormView, word?: string): Doc | undefined {
+ this.collectionFreeForm = docView;
+ const bounds: { x: number; y: number; width?: number; height?: number }[] = [];
+
+ // calculate the necessary bounds from the selected ink docs
+ selected.map(
+ action(d => {
+ const x = NumCast(d.x);
+ const y = NumCast(d.y);
+ const width = NumCast(d._width);
+ const height = NumCast(d._height);
+ bounds.push({ x, y, width, height });
+ })
+ );
+
+ // calculate the aggregated bounds
+ const aggregBounds = aggregateBounds(bounds, 0, 0);
+ const marqViewRef = docView._marqueeViewRef.current;
+
+ // set the vals for bounds in marqueeView
+ if (marqViewRef) {
+ marqViewRef._downX = aggregBounds.x;
+ marqViewRef._downY = aggregBounds.y;
+ marqViewRef._lastX = aggregBounds.r;
+ marqViewRef._lastY = aggregBounds.b;
+ }
+
+ // map through all the selected ink strokes and create the groupings
+ selected.map(
+ action(d => {
+ const dx = NumCast(d.x);
+ const dy = NumCast(d.y);
+ delete d.x;
+ delete d.y;
+ delete d.activeFrame;
+ delete d._timecodeToShow; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
+ delete d._timecodeToHide; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
+ // calculate pos based on bounds
+ if (marqViewRef?.Bounds) {
+ d.x = dx - marqViewRef.Bounds.left - marqViewRef.Bounds.width / 2;
+ d.y = dy - marqViewRef.Bounds.top - marqViewRef.Bounds.height / 2;
+ }
+ return d;
+ })
+ );
+ docView.props.removeDocument?.(selected);
+ // Gets a collection based on the selected nodes using a marquee view ref
+ const newCollection = marqViewRef?.getCollection(selected, undefined, true);
+ if (newCollection) {
+ newCollection.width = NumCast(newCollection._width);
+ newCollection.height = NumCast(newCollection._height);
+ // if the grouping we are creating is an individual word
+ if (word) {
+ newCollection.title = word;
+ }
+ }
+
+ // nda - bug: when deleting a stroke before leaving writing mode, delete the stroke from unprocessed ink docs
+ newCollection && docView.props.addDocument?.(newCollection);
+ if (newCollection) {
+ newCollection.hasTextBox = false;
+ }
+ return newCollection;
+ }
+
+ render() {
+ return (
+ <div className="ink-transcription">
+ <div className="math-editor" ref={this.setMathRef} touch-action="none"></div>
+ <div className="text-editor" ref={this.setTextRef} touch-action="none"></div>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 55f28f415..ce1c07f2f 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -104,8 +104,8 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
* analyzes the ink stroke and saves the analysis of the stroke to the 'inkAnalysis' field,
* and the recognized words to the 'handwriting'
*/
- analyzeStrokes() {
- const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
+ analyzeStrokes=()=> {
+ const data: InkData = this.inkScaledData().inkData ?? [];
CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ['inkAnalysis', 'handwriting'], [data]);
}
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 7198c7f05..a0284e55a 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -12,7 +12,7 @@ import { emptyFunction } from '../../Utils';
import { CreateLinkToActiveAudio, Doc, DocListCast, FieldResult, Opt } from '../../fields/Doc';
import { Id } from '../../fields/FieldSymbols';
import { InkTool } from '../../fields/InkField';
-import { Cast, NumCast, toList } from '../../fields/Types';
+import { Cast, DocCast, NumCast, toList } from '../../fields/Types';
import { SnappingManager } from '../util/SnappingManager';
import { Transform } from '../util/Transform';
import { GestureOverlay } from './GestureOverlay';
@@ -23,6 +23,8 @@ import { DocumentView } from './nodes/DocumentView';
import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
import { ScriptingGlobals } from '../util/ScriptingGlobals';
import { OverlayView } from './OverlayView';
+import { AnnotationPalette } from './smartdraw/AnnotationPalette';
+import { DocData } from '../../fields/DocSymbols';
interface LightboxViewProps {
PanelWidth: number;
@@ -40,7 +42,12 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
* @param view
* @returns true if a DocumentView is descendant of the lightbox view
*/
- public static Contains(view?:DocumentView) { return view && LightboxView.Instance?._docView && (view.containerViewPath?.() ?? []).concat(view).includes(LightboxView.Instance?._docView); } // prettier-ignore
+ public static Contains(view?: DocumentView) {
+ return (
+ (view && LightboxView.Instance?._docView && (view.containerViewPath?.() ?? []).concat(view).includes(LightboxView.Instance?._docView)) ||
+ (view && LightboxView.Instance?._annoPaletteView?.Contains(view))
+ );
+ } // prettier-ignore
public static LightboxDoc = () => LightboxView.Instance?._doc;
// eslint-disable-next-line no-use-before-define
static Instance: LightboxView;
@@ -59,6 +66,8 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
@observable private _doc: Opt<Doc> = undefined;
@observable private _docTarget: Opt<Doc> = undefined;
@observable private _docView: Opt<DocumentView> = undefined;
+ @observable private _showPalette: boolean = false;
+ private _annoPaletteView: AnnotationPalette | null = null;
@computed get leftBorder() { return Math.min(this._props.PanelWidth / 4, this._props.maxBorder[0]); } // prettier-ignore
@computed get topBorder() { return Math.min(this._props.PanelHeight / 4, this._props.maxBorder[1]); } // prettier-ignore
@@ -71,6 +80,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
DocumentView._lightboxContains = LightboxView.Contains;
DocumentView._lightboxDoc = LightboxView.LightboxDoc;
}
+
/**
* Sets the root Doc to render in the lightbox view.
* @param doc
@@ -103,6 +113,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
this._history = [];
Doc.ActiveTool = InkTool.None;
SnappingManager.SetExploreMode(false);
+ this._showPalette = false;
}
DocumentView.DeselectAll();
if (future) {
@@ -202,6 +213,9 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
toggleFitWidth = () => {
this._doc && (this._doc._layout_fitWidth = !this._doc._layout_fitWidth);
};
+ togglePalette = () => {
+ this._showPalette = !this._showPalette;
+ };
togglePen = () => {
Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
};
@@ -306,6 +320,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
</GestureOverlay>
</div>
+ {this._showPalette && <AnnotationPalette ref={r => (this._annoPaletteView = r)} Document={DocCast(Doc.UserDoc().myLightboxDrawings)} />}
{this.renderNavBtn(0, undefined, this._props.PanelHeight / 2 - 12.5, 'chevron-left', this._doc && this._history.length, this.previous)}
{this.renderNavBtn(
this._props.PanelWidth - Math.min(this._props.PanelWidth / 4, this._props.maxBorder[0]),
@@ -318,7 +333,8 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
)}
<LightboxTourBtn lightboxDoc={this.lightboxDoc} navBtn={this.renderNavBtn} future={this.future} stepInto={this.stepInto} />
{toggleBtn('lightboxView-navBtn', 'toggle reading view', this._doc?._layout_fitWidth, 'book-open', 'book', this.toggleFitWidth)}
- {toggleBtn('lightboxView-tabBtn', 'open document in a tab', false, 'file-download', '', this.downloadDoc)}
+ {toggleBtn('lightboxView-tabBtn', 'open document in a tab', false, 'file-export', '', this.downloadDoc)}
+ {toggleBtn('lightboxView-paletteBtn', 'toggle annotation palette', this._showPalette === true, 'palette', '', this.togglePalette)}
{toggleBtn('lightboxView-penBtn', 'toggle pen annotation', Doc.ActiveTool === InkTool.Pen, 'pen', '', this.togglePen)}
{toggleBtn('lightboxView-exploreBtn', 'toggle navigate only mode', SnappingManager.ExploreMode, 'globe-americas', '', this.toggleExplore)}
</div>
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index ef1bcfb64..e6d6b20a4 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -76,6 +76,9 @@ import { PresBox } from './nodes/trails';
import { AnchorMenu } from './pdf/AnchorMenu';
import { GPTPopup } from './pdf/GPTPopup/GPTPopup';
import { TopBar } from './topbar/TopBar';
+import { SmartDrawHandler } from './smartdraw/SmartDrawHandler';
+import { AnnotationPalette } from './smartdraw/AnnotationPalette';
+import { InkTranscription } from './InkTranscription';
const { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
const _global = (window /* browser */ || global) /* node */ as any;
@@ -339,6 +342,7 @@ export class MainView extends ObservableReactComponent<{}> {
fa.faTerminal,
fa.faToggleOn,
fa.faFile,
+ fa.faFileExport,
fa.faLocationArrow,
fa.faSearch,
fa.faFileDownload,
@@ -378,6 +382,7 @@ export class MainView extends ObservableReactComponent<{}> {
fa.faXmark,
fa.faExclamation,
fa.faFileAlt,
+ fa.faFileArrowDown,
fa.faFileAudio,
fa.faFileVideo,
fa.faFilePdf,
@@ -431,6 +436,7 @@ export class MainView extends ObservableReactComponent<{}> {
fa.faBold,
fa.faItalic,
fa.faClipboard,
+ fa.faClipboardCheck,
fa.faUnderline,
fa.faStrikethrough,
fa.faSuperscript,
@@ -477,6 +483,8 @@ export class MainView extends ObservableReactComponent<{}> {
fa.faHashtag,
fa.faAlignJustify,
fa.faCheckSquare,
+ fa.faSquarePlus,
+ fa.faReply,
fa.faListUl,
fa.faWindowMinimize,
fa.faWindowRestore,
@@ -572,6 +580,9 @@ export class MainView extends ObservableReactComponent<{}> {
Doc.linkFollowUnhighlight();
AudioBox.Enabled = true;
const targets = document.elementsFromPoint(e.x, e.y);
+ const targetClasses: string[] = targets.map(target => {
+ return target.className.toString();
+ });
if (targets.length) {
let targClass = targets[0].className.toString();
for (let i = 0; i < targets.length - 1; i++) {
@@ -579,6 +590,8 @@ export class MainView extends ObservableReactComponent<{}> {
else break;
}
!targClass.includes('contextMenu') && ContextMenu.Instance.closeMenu();
+ !targetClasses.includes('marqueeView') && !targetClasses.includes('smart-draw-handler') && SmartDrawHandler.Instance.hideSmartDrawHandler();
+ !targetClasses.includes('smart-draw-handler') && SmartDrawHandler.Instance.hideRegenerate();
!['timeline-menu-desc', 'timeline-menu-item', 'timeline-menu-input'].includes(targClass) && TimelineMenu.Instance.closeMenu();
}
});
@@ -1096,6 +1109,7 @@ export class MainView extends ObservableReactComponent<{}> {
<MarqueeOptionsMenu />
<TimelineMenu />
<RichTextMenu />
+ <InkTranscription />
{this.snapLines}
<LightboxView key="lightbox" PanelWidth={this._windowWidth} addSplit={CollectionDockingView.AddSplit} PanelHeight={this._windowHeight} maxBorder={this.lightboxMaxBorder} />
<GPTPopup key="gptpopup" />
diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts
index c633f34fb..ea5f3dd27 100644
--- a/src/client/views/ViewBoxInterface.ts
+++ b/src/client/views/ViewBoxInterface.ts
@@ -19,7 +19,7 @@ export abstract class ViewBoxInterface<P> extends ObservableReactComponent<React
abstract get dataDoc(): Doc;
abstract get fieldKey(): string;
promoteCollection?: () => void; // moves contents of collection to parent
- updateIcon?: () => void; // updates the icon representation of the document
+ updateIcon?: (usePanelDimensions?: boolean) => void; // updates the icon representation of the document
getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
restoreView?: (viewSpec: Doc) => boolean;
scrollPreview?: (docView: DocumentView, doc: Doc, focusSpeed: number, options: FocusViewOptions) => Opt<number>; // returns the duration of the focus
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index 38f681e87..4f3ce9d9b 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -79,7 +79,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
// suppressSetHeight={true}
NativeWidth={returnZero}
NativeHeight={returnZero}
- fitWidth={undefined}
+ fitWidth={this._props.childLayoutFitWidth}
onDoubleClickScript={this.onChildDoubleClick}
renderDepth={this._props.renderDepth + 1}
LayoutTemplate={this._props.childLayoutTemplate}
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 2adad68e0..8b083de15 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -8,7 +8,7 @@ import * as React from 'react';
import { StopEvent, returnFalse, returnOne, returnTrue, returnZero } from '../../../ClientUtils';
import { emptyFunction } from '../../../Utils';
import { Doc, Opt } from '../../../fields/Doc';
-import { DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { DocumentType } from '../../documents/DocumentTypes';
import { DragManager } from '../../util/DragManager';
import { ContextMenu } from '../ContextMenu';
@@ -144,6 +144,7 @@ export class CollectionCarouselView extends CollectionSubView() {
revealItems.push({description: 'Quiz Cards', event: () => {this.layoutDoc.filterOp = cardMode.QUIZ;}, icon: 'pencil',}); // prettier-ignore
!revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
};
+ childFitWidth = (doc: Doc) => Cast(this.Document.childLayoutFitWidth, 'boolean', this._props.childLayoutFitWidth?.(doc) ?? Cast(doc.layout_fitWidth, 'boolean', null));
@computed get content() {
const index = NumCast(this.layoutDoc._carousel_index);
const curDoc = this.carouselItems?.[index];
@@ -154,10 +155,11 @@ export class CollectionCarouselView extends CollectionSubView() {
<div className="collectionCarouselView-image" key="image">
<DocumentView
{...this._props}
- NativeWidth={returnZero}
- NativeHeight={returnZero}
- fitWidth={undefined}
+ // NativeWidth={returnZero}
+ // NativeHeight={returnZero}
+ fitWidth={returnTrue}
setContentViewBox={undefined}
+ containerViewPath={this.DocumentView?.().docViewPath}
onDoubleClickScript={this.onContentDoubleClick}
onClickScript={this.onContentClick}
isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive}
@@ -169,6 +171,7 @@ export class CollectionCarouselView extends CollectionSubView() {
Document={curDoc.layout}
TemplateDataDocument={DocCast(curDoc.layout.resolvedDataDoc)}
PanelHeight={this.panelHeight}
+ PanelWidth={this._props.PanelWidth}
/>
</div>
{!carouselShowsCaptions ? null : (
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 5b7f09be3..62632e8c2 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -55,6 +55,8 @@ import { CollectionFreeFormPannableContents } from './CollectionFreeFormPannable
import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors';
import './CollectionFreeFormView.scss';
import { MarqueeView } from './MarqueeView';
+import { DrawingOptions, SmartDrawHandler } from '../../smartdraw/SmartDrawHandler';
+import { AnnotationPalette } from '../../smartdraw/AnnotationPalette';
@observer
class CollectionFreeFormOverlayView extends React.Component<{ elements: () => ViewDefResult[] }> {
@@ -1239,6 +1241,69 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
};
@action
+ showSmartDraw = (e: PointerEvent, doubleTap?: boolean) => {
+ SmartDrawHandler.Instance.displaySmartDrawHandler(e.pageX, e.pageY, this.createDrawing, this.removeDrawing);
+ };
+
+ _drawing: Doc[] = [];
+ _drawingContainer: Doc | undefined = undefined;
+ @undoBatch
+ createDrawing = (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => {
+ this._drawing = [];
+ const xf = this.screenToFreeformContentsXf;
+ // this._drawingContainer = undefined;
+ strokeData.forEach((stroke: [InkData, string, string]) => {
+ const bounds = InkField.getBounds(stroke[0]);
+ const B = xf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
+ const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
+ const inkDoc = Docs.Create.InkDocument(
+ stroke[0],
+ { title: 'stroke',
+ x: B.x - inkWidth / 2,
+ y: B.y - inkWidth / 2,
+ _width: B.width + inkWidth,
+ _height: B.height + inkWidth,
+ stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+ inkWidth,
+ opts.autoColor ? stroke[1] : ActiveInkColor(),
+ ActiveInkBezierApprox(),
+ stroke[2] === 'none' ? ActiveFillColor() : stroke[2],
+ ActiveArrowStart(),
+ ActiveArrowEnd(),
+ ActiveDash(),
+ ActiveIsInkMask()
+ );
+ this._drawing.push(inkDoc);
+ this.addDocument(inkDoc);
+ });
+ const collection = this._marqueeViewRef.current?.collection(undefined, true, this._drawing);
+ if (collection) {
+ const docData = collection[DocData];
+ docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
+ docData.drawingInput = opts.text;
+ docData.drawingComplexity = opts.complexity;
+ docData.drawingColored = opts.autoColor;
+ docData.drawingSize = opts.size;
+ docData.drawingData = gptRes;
+ this._drawingContainer = collection;
+ }
+ this._batch?.end();
+ };
+
+ removeDrawing = (doc?: Doc) => {
+ this._batch = UndoManager.StartBatch('regenerateDrawing');
+ if (doc) {
+ const docData = doc[DocData];
+ const children = DocListCast(docData.data);
+ this._props.removeDocument?.(doc);
+ this._props.removeDocument?.(children);
+ } else {
+ if (this._drawingContainer) this._props.removeDocument?.(this._drawingContainer);
+ }
+ this._drawing = [];
+ };
+
+ @action
zoom = (pointX: number, pointY: number, deltaY: number): void => {
if (this.Document.isGroup || this.Document[(this._props.viewField ?? '_') + 'freeform_noZoom']) return;
let deltaScale = deltaY > 0 ? 1 / 1.05 : 1.05;
@@ -1688,7 +1753,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
.forEach(entry =>
elements.push({
ele: this.getChildDocView(entry[1]),
- bounds: (entry[1].opacity === 0 ? { payload:undefined, type:"", ...entry[1], width: 0, height: 0 } : { payload:undefined, type:"",...entry[1] }),
+ bounds: entry[1].opacity === 0 ? { payload: undefined, type: '', ...entry[1], width: 0, height: 0 } : { payload: undefined, type: '', ...entry[1] },
inkMask: BoolCast(entry[1].pair.layout.stroke_isInkMask) ? NumCast(entry[1].pair.layout.opacity, 1) : -1,
})
);
@@ -1810,26 +1875,27 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
Object.values(this._disposers).forEach(disposer => disposer?.());
}
- updateIcon = () => {
+ updateIcon = (usePanelDimensions?: boolean) => {
const contentDiv = this.DocumentView?.().ContentDiv;
- contentDiv && UpdateIcon(
- this.layoutDoc[Id] + '-icon' + new Date().getTime(),
- contentDiv,
- NumCast(this.layoutDoc._width),
- NumCast(this.layoutDoc._height),
- this._props.PanelWidth(),
- this._props.PanelHeight(),
- 0,
- 1,
- false,
- '',
- (iconFile, nativeWidth, nativeHeight) => {
- this.dataDoc.icon = new ImageField(iconFile);
- this.dataDoc.icon_nativeWidth = nativeWidth;
- this.dataDoc.icon_nativeHeight = nativeHeight;
- }
- );
- }
+ contentDiv &&
+ UpdateIcon(
+ this.layoutDoc[Id] + '-icon' + new Date().getTime(),
+ contentDiv,
+ usePanelDimensions ? this._props.PanelWidth() : NumCast(this.layoutDoc._width),
+ usePanelDimensions ? this._props.PanelHeight() : NumCast(this.layoutDoc._height),
+ this._props.PanelWidth(),
+ this._props.PanelHeight(),
+ 0,
+ 1,
+ false,
+ '',
+ (iconFile, nativeWidth, nativeHeight) => {
+ this.dataDoc.icon = new ImageField(iconFile);
+ this.dataDoc.icon_nativeWidth = nativeWidth;
+ this.dataDoc.icon_nativeHeight = nativeHeight;
+ }
+ );
+ };
@action
onCursorMove = (e: React.PointerEvent) => {
@@ -1941,6 +2007,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}),
icon: 'eye',
});
+ optionItems.push({
+ description: 'Show Drawing Editor',
+ event: action(() => {
+ !SmartDrawHandler.Instance._showRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10, this.createDrawing, this.removeDrawing) : SmartDrawHandler.Instance.hideRegenerate();
+ }),
+ icon: 'pen-to-square',
+ });
this._props.renderDepth &&
optionItems.push({
description: 'Use Background Color as Default',
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
index f02cd9d45..b3fdd9379 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
@@ -3,6 +3,7 @@ import { IconButton } from 'browndash-components';
import { computed, makeObservable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
+import { Doc } from '../../../../fields/Doc';
import { unimplementedFunction } from '../../../../Utils';
import { SettingsManager } from '../../../util/SettingsManager';
import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
@@ -12,7 +13,7 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> {
// eslint-disable-next-line no-use-before-define
static Instance: MarqueeOptionsMenu;
- public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined, group?: boolean) => void = unimplementedFunction;
+ public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined, group?: boolean, selection?: Doc[]) => Doc | void = unimplementedFunction;
public delete: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
public summarize: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
public showMarquee: () => void = unimplementedFunction;
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index dc15c83c5..63e36fc31 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -362,7 +362,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
this.hideMarquee();
});
- getCollection = action((selected: Doc[], creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, makeGroup: Opt<boolean>) => {
+ public static getCollection = action((selected: Doc[], creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, makeGroup: Opt<boolean>, bounds: MarqueeViewBounds) => {
const newCollection = creator
? creator(selected, { title: 'nested stack' })
: ((doc: Doc) => {
@@ -374,14 +374,13 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
return doc;
})(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true));
newCollection.isSystem = undefined;
- newCollection._width = this.Bounds.width;
- newCollection._height = this.Bounds.height;
+ newCollection._width = bounds.width || 1; // if width/height are unset/0, then groups won't autoexpand to contain their children
+ newCollection._height = bounds.height || 1;
newCollection._dragWhenActive = makeGroup;
- newCollection.x = this.Bounds.left;
- newCollection.y = this.Bounds.top;
+ newCollection.x = bounds.left;
+ newCollection.y = bounds.top;
newCollection.layout_fitWidth = true;
selected.forEach(d => Doc.SetContainer(d, newCollection));
- this.hideMarquee();
return newCollection;
});
@@ -418,7 +417,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
this._props.removeDocument?.(selected);
}
- const newCollection = this.getCollection(selected, (e as KeyboardEvent)?.key === 't' ? Docs.Create.StackingDocument : undefined, group);
+ const newCollection = MarqueeView.getCollection(selected, (e as KeyboardEvent)?.key === 't' ? Docs.Create.StackingDocument : undefined, group, this.Bounds);
newCollection._freeform_panX = this.Bounds.left + this.Bounds.width / 2;
newCollection._freeform_panY = this.Bounds.top + this.Bounds.height / 2;
newCollection._currentFrame = activeFrame;
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 2b7de5082..bba34e302 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -35,6 +35,7 @@ import { ImageBox } from '../nodes/ImageBox';
import { VideoBox } from '../nodes/VideoBox';
import { WebBox } from '../nodes/WebBox';
import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
+import { InkTranscription } from '../InkTranscription';
// import { InkTranscription } from '../InkTranscription';
@@ -364,6 +365,7 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?:
export function createInkGroup(/* inksToGroup?: Doc[], isSubGroup?: boolean */) {
// TODO nda - if document being added to is a inkGrouping then we can just add to that group
if (Doc.ActiveTool === InkTool.Write) {
+ console.log('create inking group ');
CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => {
// TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those
const selected = ffView.unprocessedDocs;
@@ -421,14 +423,14 @@ export function createInkGroup(/* inksToGroup?: Doc[], isSubGroup?: boolean */)
// TODO: nda - will probably need to go through and only remove the unprocessed selected docs
ffView.unprocessedDocs = [];
- // InkTranscription.Instance.transcribeInk(newCollection, selected, false);
+ InkTranscription.Instance.transcribeInk(newCollection, selected, false);
});
}
CollectionFreeFormView.collectionsWithUnprocessedInk.clear();
}
function setActiveTool(tool: InkTool | Gestures, keepPrim: boolean, checkResult?: boolean) {
- // InkTranscription.Instance?.createInkGroup();
+ InkTranscription.Instance?.createInkGroup();
if (checkResult) {
return (Doc.ActiveTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool
? GestureOverlay.Instance?.KeepPrimitiveMode || ![Gestures.Circle, Gestures.Line, Gestures.Rectangle].includes(tool as Gestures)
diff --git a/src/client/views/nodes/DiagramBox.scss b/src/client/views/nodes/DiagramBox.scss
index d2749f1ad..4bfd4f7cb 100644
--- a/src/client/views/nodes/DiagramBox.scss
+++ b/src/client/views/nodes/DiagramBox.scss
@@ -1,87 +1,229 @@
-.DIYNodeBox {
+.DiagramBox {
+ overflow:hidden;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
- align-items: center;
- justify-content: center;
-
- .DIYNodeBox-wrapper {
+ .buttonCollections{
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ height:100%;
+ padding:20px;
+ padding-right:40px;
+ button{
+ font-size:15px;
+ height:100%;
+ width:100%;
+ border: none;
+ border-radius: 5px;
+ cursor: pointer;
+ background-color: #007bff;
+ color: #fff;
+ transition: background-color 0.3s ease;
+ }
+ button:hover {
+ background-color: #0056b3;
+ }
+
+ }
+ .DiagramBox-wrapper {
+ overflow:hidden;
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 {
+ .contentCode{
+ overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
- width: 100%;
- padding: 10px;
-
- input[type="text"] {
- flex: 1;
- margin-right: 10px;
+ flex-direction:row;
+ padding:10px;
+ width:100%;
+ height:100%;
+ .topbar{
+ .backButtonDrawing{
+ padding: 5px 10px;
+ height:23px;
+ border-radius: 10px;
+ text-align: center;
+ padding:0;
+ width:50px;
+ font-size:10px;
+ position:absolute;
+ top:10px;
+ left:10px;
+ }
+ p{
+ margin-left:60px
+ }
}
+ .search-bar {
+ overflow:hidden;
+ position:absolute;
+ top:0;
+ .backButton{
+ text-align: center;
+ padding:0;
+ width:50px;
+ font-size:10px;
- button {
- padding: 5px 10px;
+ }
+ .exampleButton{
+ width:100px;
+ height:30px;
+ }
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ button {
+ padding: 5px 10px;
+ width:80px;
+ height:23px;
+ border-radius: 3px;
+ }
+ }
+ .exampleButtonContainer{
+ display:flex;
+ flex-direction: column;
+ position: absolute;
+ top:37px;
+ right:30px;
+ width:50px;
+ z-index: 200;
+ button{
+ width:70px;
+ margin:2px;
+ padding:0px;
+ height:15px;
+ border-radius: 3px;
+ }
+ }
+ textarea {
+ position:relative;
+ width:40%;
+ height: 100%;
+ height: calc(100% - 25px);
+ top:15px;
+ resize:none;
+ overflow: hidden;
+ }
+ .diagramBox{
+ flex: 1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ svg{
+ position: relative;
+ top:25;
+ max-width: none !important;
+ height: calc(100% - 50px);
+ }
}
}
-
.content {
- flex: 1;
+ overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
+ flex-direction: column;
+ padding:10px;
width:100%;
height:100%;
- .diagramBox{
- flex: 1;
+ .topbar{
+ .backButtonDrawing{
+ padding: 5px 10px;
+ height:23px;
+ border-radius: 10px;
+ text-align: center;
+ padding:0;
+ width:50px;
+ font-size:10px;
+ position:absolute;
+ top:10px;
+ left:10px;
+ }
+ p{
+ margin-left:60px
+ }
+ }
+ .search-bar {
+ overflow:hidden;
+ position:absolute;
+ top:0;
+ .backButton{
+ text-align: center;
+ padding:0;
+ width:50px;
+ font-size:10px;
+
+ }
display: flex;
+ flex-wrap: wrap;
justify-content: center;
align-items: center;
- width:100%;
- height:100%;
- svg{
+ width: 100%;
+ textarea {
flex: 1;
- display: flex;
- justify-content: center;
- align-items: center;
- width:100%;
- height:100%;
+ height: 5px;
+ min-height: 20px;
+ resize:none;
+ overflow: hidden;
+ }
+ button {
+ padding: 5px 10px;
+ width:80px;
+ height:23px;
+ border-radius: 10px;
+ }
+ .rightButtons{
+ display:flex;
+ flex-direction: column;
+ button {
+ padding: 5px 10px;
+ width:80px;
+ height:23px;
+ margin:2;
+ border-radius: 10px;
+ }
}
}
- }
-
- .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);
+ .loading-circle {
+ position: absolute;
+ display:flex;
+ align-items: center;
+ justify-content: center;
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ border: 3px solid #ccc;
+ border-top-color: #333;
+ animation: spin 1s infinite linear;
}
- 100% {
- transform: rotate(360deg);
+ @keyframes spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+ }
+ .diagramBox{
+ flex: 1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ svg{
+ position: relative;
+ top:25;
+ max-width: none !important;
+ height: calc(100% - 50px);
+ }
}
}
}
diff --git a/src/client/views/nodes/DiagramBox.tsx b/src/client/views/nodes/DiagramBox.tsx
index 32969fa53..5a712b8b0 100644
--- a/src/client/views/nodes/DiagramBox.tsx
+++ b/src/client/views/nodes/DiagramBox.tsx
@@ -1,11 +1,13 @@
+/* eslint-disable prettier/prettier */
+/* eslint-disable jsx-a11y/control-has-associated-label */
import mermaid from 'mermaid';
-import { action, makeObservable, observable, reaction } from 'mobx';
+import { action, makeObservable, observable, reaction, computed } 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 { DocCast, BoolCast } from '../../../fields/Types';
import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT';
import { DocumentType } from '../../documents/DocumentTypes';
import { Docs } from '../../documents/Documents';
@@ -15,6 +17,16 @@ 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,
+}
@observer
export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@@ -27,60 +39,97 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
super(props);
makeObservable(this);
}
-
+ @observable menuState = menuState.justCreated;
+ @observable renderDiv: React.ReactNode;
@observable inputValue = '';
+ @observable createInputValue = '';
@observable loading = false;
@observable errorMessage = '';
@observable mermaidCode = '';
+ @observable isExampleMenuOpen = false;
- @action handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ @action handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
this.inputValue = e.target.value;
+ console.log(e.target.value);
};
async componentDidMount() {
this._props.setContentViewBox?.(this);
mermaid.initialize({
securityLevel: 'loose',
startOnLoad: true,
- flowchart: { useMaxWidth: true, htmlLabels: true, curve: 'cardinal' },
+ darkMode: true,
+ flowchart: { useMaxWidth: false, htmlLabels: true, curve: 'cardinal' },
+ gantt: { useMaxWidth: true, useWidth: 2000 },
});
- 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);
- }
- }
+ if (!this.Document.testValue) {
+ this.Document.height = 500;
+ this.Document.width = 500;
}
- // 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);
- }
- }
- });
+ this.Document.testValue = 'a';
+ this.mermaidCode = 'a';
+ if (typeof this.Document.drawingMermaidCode === 'string' && this.Document.menuState === 'drawing') {
+ this.renderMermaidAsync(this.Document.drawingMermaidCode);
}
- 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 }
- );
+ // 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();
+ }
}
renderMermaid = async (str: string) => {
try {
const { svg, bindFunctions } = await this.mermaidDiagram(str);
return { svg, bindFunctions };
} catch (error) {
- console.error('Error rendering mermaid diagram:', error);
+ // console.error('Error rendering mermaid diagram:', error);
return { svg: '', bindFunctions: undefined };
}
};
@@ -92,6 +141,7 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const dashDiv = document.getElementById('dashDiv' + this.Document.title);
if (dashDiv) {
dashDiv.innerHTML = svg;
+ // this.changeHeightWidth(svg);
if (bindFunctions) {
bindFunctions(dashDiv);
}
@@ -100,51 +150,51 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
console.error('Error rendering Mermaid:', error);
}
}
+ changeHeightWidth(svgString: string) {
+ const pattern = /width="([\d.]+)"\s*height="([\d.]+)"/;
+
+ const match = svgString.match(pattern);
+
+ 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.generateMermaidCode();
+ 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 = '';
- // 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;
+ 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
- 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.mermaidCode = res;
+ console.log('GPT call succeeded:' + res);
+ this.errorMessage = '';
}
this.renderMermaidAsync.call(this, this.removeWords(this.mermaidCode));
- this.loading = false;
+ this.Document.gptMermaidCode = this.removeWords(this.mermaidCode);
}
- isValidCode = (html: string) => true;
removeWords(inputStrIn: string) {
const inputStr = inputStrIn.replace('```mermaid', '');
return inputStr.replace('```', '');
@@ -164,10 +214,12 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
});
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;';
+ // 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];
@@ -182,8 +234,6 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
?.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);
@@ -197,12 +247,11 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
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) + ';';
+ 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) + ';';
+ mermaidCode += Math.abs(rectangle.x) + this.getTextInBox(rectangle, textArray) + '---' + Math.abs(rectangle2.x) + this.getTextInBox(rectangle2, textArray) + ';';
}
}
}
@@ -210,35 +259,166 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
}
// 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 = '';
- }
- }
- }
- }
- });
+ if (diagramExists) {
+ this.Document.drawingMermaidCode = mermaidCode;
+ } else {
+ this.Document.drawingMermaidCode = '';
+ }
}
}
}
- testInkingStroke = () => {
+ 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');
- setTimeout(() => {
- const inkStrokeArray = lineArray.map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())).filter(inkView => inkView?.ComponentView instanceof InkingStroke);
- console.log(inkStrokeArray);
- });
+ 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)
+
+ 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);
+
+ // 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())
+ }
+ }
+ });
+ }
+ }
}
- };
+ }
+ 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];
@@ -261,31 +441,262 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
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>
+ );
+ }
- render() {
+ renderGpt(): React.ReactNode {
return (
- <div ref={this._ref} className="DIYNodeBox">
- <div ref={this._dragRef} className="DIYNodeBox-wrapper">
+ <div ref={this._dragRef} className="DiagramBox-wrapper">
+ <div className="content">
<div className="search-bar">
- <input type="text" value={this.inputValue} onChange={this.handleInputChange} />
- <button type="button" onClick={this.handleRenderClick}>
- Generate
+ <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>
- <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>
- )}
+ {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);
+ }
+ };
+ 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 {
+ 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>
+ )}
+ <textarea value={this.createInputValue} placeholder="Enter Mermaid Code" onChange={this.handleInputChangeEditor} />
+ <div id={'dashDiv' + this.Document.title} className="diagramBox" />
</div>
</div>
);
}
+ 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<br/>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<HTMLTextAreaElement>) => {
+ 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 (
+ <div ref={this._ref} className="DiagramBox">
+ {this.renderDiv}
+ </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' },
+ options: { _height: 700, _width: 700, _layout_fitWidth: false, _layout_nativeDimEditable: true, _layout_reflowVertical: true, waitForDoubleClickToClick: 'always', _layout_reflowHorizontal: true, systemIcon: 'BsGlobe' },
});
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 3be50f5e6..e6590958b 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -149,7 +149,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
componentDidMount() {
this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = 0;
- this._props.setContentViewBox?.(this); // this tells the DocumentView that this ScreenshotBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
+ this._props.setContentViewBox?.(this); // this tells the DocumentView that this Box is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
// this.layoutDoc.videoWall && reaction(() => ({ width: this._props.PanelWidth(), height: this._props.PanelHeight() }),
// ({ width, height }) => {
// if (this._camera) {
diff --git a/src/client/views/smartdraw/AnnotationPalette.scss b/src/client/views/smartdraw/AnnotationPalette.scss
new file mode 100644
index 000000000..9f875f61a
--- /dev/null
+++ b/src/client/views/smartdraw/AnnotationPalette.scss
@@ -0,0 +1,10 @@
+.annotation-palette {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ position: absolute;
+ right: 14px;
+ top: 50px;
+ border-radius: 5px;
+ margin: auto;
+}
diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx
new file mode 100644
index 000000000..ec4279e3e
--- /dev/null
+++ b/src/client/views/smartdraw/AnnotationPalette.tsx
@@ -0,0 +1,382 @@
+import { faLaptopHouse } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Slider, Switch } from '@mui/material';
+import { Button, IconButton } from 'browndash-components';
+import { action, computed, makeObservable, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { AiOutlineSend } from 'react-icons/ai';
+import ReactLoading from 'react-loading';
+import { returnAll, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero } from '../../../ClientUtils';
+import { ActiveInkWidth, Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc';
+import { DocData } from '../../../fields/DocSymbols';
+import { InkData, InkField } from '../../../fields/InkField';
+import { BoolCast, DocCast, ImageCast } from '../../../fields/Types';
+import { emptyFunction, unimplementedFunction } from '../../../Utils';
+import { Docs } from '../../documents/Documents';
+import { makeUserTemplateButton, makeUserTemplateImage } from '../../util/DropConverter';
+import { SettingsManager } from '../../util/SettingsManager';
+import { Transform } from '../../util/Transform';
+import { undoable, undoBatch } from '../../util/UndoManager';
+import { CollectionFreeFormView, MarqueeOptionsMenu, MarqueeView } from '../collections/collectionFreeForm';
+import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveIsInkMask, DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
+import { FieldView } from '../nodes/FieldView';
+import { ObservableReactComponent } from '../ObservableReactComponent';
+import { DefaultStyleProvider } from '../StyleProvider';
+import './AnnotationPalette.scss';
+import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { ImageField } from '../../../fields/URLField';
+import { CollectionCarousel3DView } from '../collections/CollectionCarousel3DView';
+import { Copy } from '../../../fields/FieldSymbols';
+
+interface AnnotationPaletteProps {
+ Document: Doc;
+}
+
+@observer
+export class AnnotationPalette extends ObservableReactComponent<AnnotationPaletteProps> {
+ @observable private _paletteMode: 'create' | 'view' = 'view';
+ @observable private _userInput: string = '';
+ @observable private _isLoading: boolean = false;
+ @observable private _canInteract: boolean = true;
+ @observable private _showRegenerate: boolean = false;
+ @observable private _docView: DocumentView | null = null;
+ @observable private _docCarouselView: DocumentView | null = null;
+ @observable private _opts: DrawingOptions = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 };
+ private _gptRes: string[] = [];
+
+ constructor(props: any) {
+ super(props);
+ makeObservable(this);
+ }
+
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(AnnotationPalette, fieldKey);
+ }
+
+ Contains = (view: DocumentView) => {
+ return (this._docView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docView)) || (this._docCarouselView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docCarouselView));
+ };
+
+ return170 = () => 170;
+
+ @action
+ handleKeyPress = async (event: React.KeyboardEvent) => {
+ if (event.key === 'Enter') {
+ await this.generateDrawing();
+ }
+ };
+
+ @action
+ setPaletteMode = (mode: 'create' | 'view') => {
+ this._paletteMode = mode;
+ };
+
+ @action
+ setUserInput = (input: string) => {
+ if (!this._isLoading) this._userInput = input;
+ };
+
+ @action
+ setDetail = (detail: number) => {
+ if (this._canInteract) this._opts.complexity = detail;
+ };
+
+ @action
+ setColor = (autoColor: boolean) => {
+ if (this._canInteract) this._opts.autoColor = autoColor;
+ };
+
+ @action
+ setSize = (size: number) => {
+ if (this._canInteract) this._opts.size = size;
+ };
+
+ @action
+ resetPalette = (changePaletteMode: boolean) => {
+ if (changePaletteMode) this.setPaletteMode('view');
+ this.setUserInput('');
+ this.setDetail(5);
+ this.setColor(true);
+ this.setSize(200);
+ this._showRegenerate = false;
+ this._canInteract = true;
+ this._opts = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 };
+ this._gptRes = [];
+ this._props.Document[DocData].data = undefined;
+ };
+
+ public static addToPalette = async (doc: Doc) => {
+ if (!doc.savedAsAnno) {
+ const clone = await Doc.MakeClone(doc);
+ clone.clone.title = doc.title;
+ const image = (await AnnotationPalette.getIcon(doc))?.[Copy]();
+ if (image) {
+ const imageTemplate = makeUserTemplateImage(clone.clone, image);
+ Doc.AddDocToList(Doc.MyAnnos, 'data', imageTemplate);
+ doc.savedAsAnno = true;
+ }
+ }
+ };
+
+ public static getIcon(group: Doc) {
+ const docView = DocumentView.getDocumentView(group);
+ if (docView) {
+ docView.ComponentView?.updateIcon?.(true);
+ return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000));
+ }
+ return undefined;
+ }
+
+ @undoBatch
+ generateDrawing = action(async () => {
+ this._isLoading = true;
+ this._props.Document[DocData].data = undefined;
+ for (var i = 0; i < 3; i++) {
+ try {
+ SmartDrawHandler.Instance._addFunc = this.createDrawing;
+ this._canInteract = false;
+ if (this._showRegenerate) {
+ SmartDrawHandler.Instance._deleteFunc = unimplementedFunction;
+ await SmartDrawHandler.Instance.regenerate(this._opts, this._gptRes[i], this._userInput);
+ } else {
+ await SmartDrawHandler.Instance.drawWithGPT({ X: 0, Y: 0 }, this._userInput, this._opts.complexity, this._opts.size, this._opts.autoColor);
+ }
+ } catch (e) {
+ console.log('Error generating drawing');
+ }
+ }
+ this._opts.text !== '' ? (this._opts.text = `${this._opts.text} ~~~ ${this._userInput}`) : (this._opts.text = this._userInput);
+ this._userInput = '';
+ this._isLoading = false;
+ this._showRegenerate = true;
+ });
+
+ @action
+ createDrawing = (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => {
+ this._opts = opts;
+ this._gptRes.push(gptRes);
+ const drawing: Doc[] = [];
+
+ strokeList.forEach((stroke: [InkData, string, string]) => {
+ const bounds = InkField.getBounds(stroke[0]);
+ const inkWidth = Math.min(5, ActiveInkWidth());
+ const inkDoc = Docs.Create.InkDocument(
+ stroke[0],
+ { title: 'stroke',
+ x: bounds.left - inkWidth / 2,
+ y: bounds.top - inkWidth / 2,
+ _width: bounds.width + inkWidth,
+ _height: bounds.height + inkWidth,
+ stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+ inkWidth,
+ opts.autoColor ? stroke[1] : ActiveInkColor(),
+ ActiveInkBezierApprox(),
+ stroke[2] === 'none' ? ActiveFillColor() : stroke[2],
+ ActiveArrowStart(),
+ ActiveArrowEnd(),
+ ActiveDash(),
+ ActiveIsInkMask()
+ );
+ drawing.push(inkDoc);
+ });
+
+ const collection = MarqueeView.getCollection(drawing, undefined, true, { left: 1, top: 1, width: 1, height: 1 });
+ if (collection) {
+ collection[DocData].freeform_fitContentsToBox = true;
+ Doc.AddDocToList(this._props.Document, 'data', collection);
+ }
+ };
+
+ saveDrawing = async () => {
+ const cIndex: number = this._props.Document.carousel_index as number;
+ const focusedDrawing = DocListCast(this._props.Document.data)[cIndex];
+ const docData = focusedDrawing[DocData];
+ docData.title = this._opts.text.match(/^(.*?)~~~.*$/)?.[1] || this._opts.text;
+ docData.drawingInput = this._opts.text;
+ docData.drawingComplexity = this._opts.complexity;
+ docData.drawingColored = this._opts.autoColor;
+ docData.drawingSize = this._opts.size;
+ docData.drawingData = this._gptRes[cIndex];
+ docData.width = this._opts.size;
+ await AnnotationPalette.addToPalette(focusedDrawing);
+ this.resetPalette(true);
+ };
+
+ async getIcon(group: Doc) {
+ const docView = DocumentView.getDocumentView(group);
+ if (docView) {
+ docView.ComponentView?.updateIcon?.(true);
+ return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000));
+ }
+ return undefined;
+ }
+
+ render() {
+ return (
+ <div className="annotation-palette" style={{ zIndex: 1000 }} onClick={e => e.stopPropagation()}>
+ {this._paletteMode === 'view' && (
+ <>
+ <DocumentView
+ ref={r => (this._docView = r)}
+ Document={Doc.MyAnnos}
+ addDocument={undefined}
+ addDocTab={DocumentViewInternal.addDocTabFunc}
+ pinToPres={DocumentView.PinDoc}
+ containerViewPath={returnEmptyDoclist}
+ styleProvider={DefaultStyleProvider}
+ removeDocument={returnFalse}
+ ScreenToLocalTransform={Transform.Identity}
+ PanelWidth={this.return170}
+ PanelHeight={this.return170}
+ renderDepth={0}
+ isContentActive={returnTrue}
+ focus={emptyFunction}
+ whenChildContentsActiveChanged={emptyFunction}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ />
+ <Button text="Add" icon={<FontAwesomeIcon icon="square-plus" />} color={SettingsManager.userColor} onClick={() => this.setPaletteMode('create')} />
+ </>
+ )}
+ {this._paletteMode === 'create' && (
+ <>
+ <div style={{ display: 'flex', flexDirection: 'row', width: '170px' }}>
+ <input
+ aria-label="label-input"
+ id="new-label"
+ type="text"
+ style={{ color: 'black', width: '170px' }}
+ value={this._userInput}
+ onChange={e => {
+ this.setUserInput(e.target.value);
+ }}
+ placeholder={this._showRegenerate ? '(Optional) Enter edits' : 'Enter item to draw'}
+ onKeyDown={this.handleKeyPress}
+ />
+ <Button
+ style={{ alignSelf: 'flex-end' }}
+ tooltip={this._showRegenerate ? 'Regenerate' : 'Send'}
+ icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : this._showRegenerate ? <FontAwesomeIcon icon={'rotate'} /> : <AiOutlineSend />}
+ iconPlacement="right"
+ color={SettingsManager.userColor}
+ onClick={this.generateDrawing}
+ />
+ </div>
+ <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', width: '170px', marginTop: '5px' }}>
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: '40px' }}>
+ Color
+ <Switch
+ sx={{
+ '& .MuiSwitch-switchBase.Mui-checked': {
+ color: SettingsManager.userColor,
+ },
+ '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': {
+ backgroundColor: SettingsManager.userVariantColor,
+ },
+ }}
+ defaultChecked={true}
+ value={this._opts.autoColor}
+ size="small"
+ onChange={() => this.setColor(!this._opts.autoColor)}
+ />
+ </div>
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: '60px' }}>
+ Detail
+ <Slider
+ sx={{
+ '& .MuiSlider-thumb': {
+ color: SettingsManager.userColor,
+ '&.Mui-focusVisible, &:hover, &.Mui-active': {
+ boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10`,
+ },
+ },
+ '& .MuiSlider-track': {
+ color: SettingsManager.userVariantColor,
+ },
+ '& .MuiSlider-rail': {
+ color: SettingsManager.userColor,
+ },
+ }}
+ style={{ width: '80%' }}
+ min={1}
+ max={10}
+ step={1}
+ size="small"
+ value={this._opts.complexity}
+ onChange={(e, val) => {
+ this.setDetail(val as number);
+ }}
+ valueLabelDisplay="auto"
+ />
+ </div>
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: '60px' }}>
+ Size
+ <Slider
+ sx={{
+ '& .MuiSlider-thumb': {
+ color: SettingsManager.userColor,
+ '&.Mui-focusVisible, &:hover, &.Mui-active': {
+ boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}20`,
+ },
+ },
+ '& .MuiSlider-track': {
+ color: SettingsManager.userVariantColor,
+ },
+ '& .MuiSlider-rail': {
+ color: SettingsManager.userColor,
+ },
+ }}
+ style={{ width: '80%' }}
+ min={50}
+ max={500}
+ step={10}
+ size="small"
+ value={this._opts.size}
+ onChange={(e, val) => {
+ this.setSize(val as number);
+ }}
+ valueLabelDisplay="auto"
+ />
+ </div>
+ </div>
+ <DocumentView
+ ref={r => (this._docCarouselView = r)}
+ Document={this._props.Document}
+ addDocument={undefined}
+ addDocTab={DocumentViewInternal.addDocTabFunc}
+ pinToPres={DocumentView.PinDoc}
+ containerViewPath={returnEmptyDoclist}
+ styleProvider={DefaultStyleProvider}
+ removeDocument={returnFalse}
+ ScreenToLocalTransform={Transform.Identity}
+ PanelWidth={this.return170}
+ PanelHeight={this.return170}
+ renderDepth={1}
+ isContentActive={returnTrue}
+ focus={emptyFunction}
+ whenChildContentsActiveChanged={emptyFunction}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ />
+ <div style={{ width: '100%', display: 'flex', flexDirection: 'row', justifyContent: 'space-between' }}>
+ <Button text="Back" tooltip="Back to All Annotations" icon={<FontAwesomeIcon icon="reply" />} color={SettingsManager.userColor} onClick={() => this.resetPalette(true)} />
+ <div style={{ display: 'flex', flexDirection: 'row' }}>
+ <Button tooltip="Save" icon={<FontAwesomeIcon icon="file-arrow-down" />} color={SettingsManager.userColor} onClick={this.saveDrawing} />
+ <Button tooltip="Reset" icon={<FontAwesomeIcon icon="rotate-left" />} color={SettingsManager.userColor} onClick={() => this.resetPalette(false)} />
+ </div>
+ </div>
+ </>
+ )}
+ </div>
+ );
+ }
+}
+
+Docs.Prototypes.TemplateMap.set(DocumentType.ANNOPALETTE, {
+ layout: { view: AnnotationPalette, dataField: 'data' },
+ options: { acl: '' },
+});
diff --git a/src/client/views/smartdraw/SmartDrawHandler.scss b/src/client/views/smartdraw/SmartDrawHandler.scss
new file mode 100644
index 000000000..6d402a80f
--- /dev/null
+++ b/src/client/views/smartdraw/SmartDrawHandler.scss
@@ -0,0 +1,3 @@
+.smart-draw-handler {
+ position: absolute;
+}
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
new file mode 100644
index 000000000..c842551c3
--- /dev/null
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -0,0 +1,437 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, makeObservable, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import React from 'react';
+import { SettingsManager } from '../../util/SettingsManager';
+import { ObservableReactComponent } from '../ObservableReactComponent';
+import { Button, IconButton } from 'browndash-components';
+import ReactLoading from 'react-loading';
+import { AiOutlineSend } from 'react-icons/ai';
+import { gptAPICall, GPTCallType } from '../../apis/gpt/GPT';
+import { InkData, InkTool } from '../../../fields/InkField';
+import { SVGToBezier } from '../../util/bezierFit';
+const { parse } = require('svgson');
+import { Slider, Switch } from '@mui/material';
+import { Doc } from '../../../fields/Doc';
+import { DocData } from '../../../fields/DocSymbols';
+import { DocumentView } from '../nodes/DocumentView';
+import { BoolCast, NumCast, StrCast } from '../../../fields/Types';
+import './SmartDrawHandler.scss';
+import { unimplementedFunction } from '../../../Utils';
+
+export interface DrawingOptions {
+ text: string;
+ complexity: number;
+ size: number;
+ autoColor: boolean;
+ x: number;
+ y: number;
+}
+
+@observer
+export class SmartDrawHandler extends ObservableReactComponent<{}> {
+ static Instance: SmartDrawHandler;
+
+ @observable private _display: boolean = false;
+ @observable private _pageX: number = 0;
+ @observable private _pageY: number = 0;
+ @observable private _yRelativeToTop: boolean = true;
+ @observable private _isLoading: boolean = false;
+ @observable private _userInput: string = '';
+ @observable private _showOptions: boolean = false;
+ @observable private _showEditBox: boolean = false;
+ @observable public _showRegenerate: boolean = false;
+ @observable private _complexity: number = 5;
+ @observable private _size: number = 200;
+ @observable private _autoColor: boolean = true;
+ @observable private _regenInput: string = '';
+ @observable private _canInteract: boolean = true;
+ public _addFunc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void = () => {};
+ public _deleteFunc: (doc?: Doc) => void = () => {};
+ private _lastInput: DrawingOptions = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 };
+ private _lastResponse: string = '';
+ private _selectedDoc: Doc | undefined = undefined;
+ private _errorOccurredOnce = false;
+
+ constructor(props: any) {
+ super(props);
+ makeObservable(this);
+ SmartDrawHandler.Instance = this;
+ }
+
+ @action
+ setUserInput = (input: string) => {
+ if (this._canInteract) this._userInput = input;
+ };
+
+ @action
+ setRegenInput = (input: string) => {
+ if (this._canInteract) this._regenInput = input;
+ };
+
+ @action
+ setShowOptions = () => {
+ this._showOptions = !this._showOptions;
+ };
+
+ @action
+ setComplexity = (val: number) => {
+ if (this._canInteract) this._complexity = val;
+ };
+
+ @action
+ setSize = (val: number) => {
+ if (this._canInteract) this._size = val;
+ };
+
+ @action
+ setAutoColor = () => {
+ if (this._canInteract) this._autoColor = !this._autoColor;
+ };
+
+ @action
+ displaySmartDrawHandler = (x: number, y: number, addFunc: (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void, deleteFunc: (doc?: Doc) => void) => {
+ this._pageX = x;
+ this._pageY = y;
+ this._display = true;
+ this._addFunc = addFunc;
+ this._deleteFunc = deleteFunc;
+ };
+
+ @action
+ displayRegenerate = (x: number, y: number, addFunc: (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void, deleteFunc: (doc?: Doc) => void) => {
+ this._selectedDoc = DocumentView.SelectedDocs()?.lastElement();
+ const docData = this._selectedDoc[DocData];
+ this._addFunc = addFunc;
+ this._deleteFunc = deleteFunc;
+ this._pageX = x;
+ this._pageY = y;
+ this._display = false;
+ this._showRegenerate = true;
+ this._showEditBox = false;
+ this._lastResponse = StrCast(docData.drawingData);
+ this._lastInput = { text: StrCast(docData.drawingInput), complexity: NumCast(docData.drawingComplexity), size: NumCast(docData.drawingSize), autoColor: BoolCast(docData.drawingColored), x: this._pageX, y: this._pageY };
+ };
+
+ @action
+ hideSmartDrawHandler = () => {
+ this._showRegenerate = false;
+ this._display = false;
+ this._isLoading = false;
+ this._showOptions = false;
+ this._userInput = '';
+ this._complexity = 5;
+ this._size = 350;
+ this._autoColor = true;
+ Doc.ActiveTool = InkTool.None;
+ };
+
+ @action
+ hideRegenerate = () => {
+ if (!this._isLoading) {
+ this._showRegenerate = false;
+ this._isLoading = false;
+ this._regenInput = '';
+ this._lastInput = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 };
+ }
+ };
+
+ @action
+ handleKeyPress = async (event: React.KeyboardEvent) => {
+ if (event.key === 'Enter') {
+ await this.handleSendClick();
+ }
+ };
+
+ @action
+ handleSendClick = async () => {
+ this._isLoading = true;
+ this._canInteract = false;
+ if (this._showRegenerate) {
+ await this.regenerate();
+ this._regenInput = '';
+ this._showEditBox = false;
+ } else {
+ this._showOptions = false;
+ try {
+ await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor);
+ this._showRegenerate = true;
+ this.hideSmartDrawHandler();
+ } catch (err) {
+ if (this._errorOccurredOnce) {
+ console.error('GPT call failed', err);
+ this._errorOccurredOnce = false;
+ } else {
+ this._errorOccurredOnce = true;
+ await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor);
+ }
+ }
+ }
+ this._isLoading = false;
+ this._canInteract = true;
+ };
+
+ @action
+ drawWithGPT = async (startPt: { X: number; Y: number }, input: string, complexity: number, size: number, autoColor: boolean) => {
+ if (input === '') return;
+ this._lastInput = { text: input, complexity: complexity, size: size, autoColor: autoColor, x: startPt.X, y: startPt.Y };
+ const res = await gptAPICall(`"${input}", "${complexity}", "${size}"`, GPTCallType.DRAW, undefined, true);
+ if (!res) {
+ console.error('GPT call failed');
+ return;
+ }
+ console.log(res);
+ const strokeData = await this.parseResponse(res, startPt, false, autoColor);
+ this._errorOccurredOnce = false;
+ return strokeData;
+ };
+
+ @action
+ edit = () => {
+ this._showEditBox = !this._showEditBox;
+ };
+
+ @action
+ regenerate = async (lastInput?: DrawingOptions, lastResponse?: string, regenInput?: string) => {
+ if (lastInput) this._lastInput = lastInput;
+ if (lastResponse) this._lastResponse = lastResponse;
+ if (regenInput) this._regenInput = regenInput;
+
+ try {
+ let res;
+ if (this._regenInput !== '') {
+ const prompt: string = `This is your previously generated svg code: ${this._lastResponse} for the user input "${this._lastInput.text}". Please regenerate it with the provided specifications.`;
+ res = await gptAPICall(`"${this._regenInput}"`, GPTCallType.DRAW, prompt, true);
+ this._lastInput.text = `${this._lastInput.text} ~~~ ${this._regenInput}`;
+ } else {
+ res = await gptAPICall(`"${this._lastInput.text}", "${this._lastInput.complexity}", "${this._lastInput.size}"`, GPTCallType.DRAW, undefined, true);
+ }
+ if (!res) {
+ console.error('GPT call failed');
+ return;
+ }
+ console.log(res);
+ await this.parseResponse(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor);
+ } catch (err) {
+ console.error('GPT call failed', err);
+ }
+ };
+
+ @action
+ parseResponse = async (res: string, startPoint: { X: number; Y: number }, regenerate: boolean, autoColor: boolean) => {
+ const svg = res.match(/<svg[^>]*>([\s\S]*?)<\/svg>/g);
+ if (svg) {
+ this._lastResponse = svg[0];
+ const svgObject = await parse(svg[0]);
+ const svgStrokes: any = svgObject.children;
+ const strokeData: [InkData, string, string][] = [];
+ svgStrokes.forEach((child: any) => {
+ const convertedBezier: InkData = SVGToBezier(child.name, child.attributes);
+ strokeData.push([
+ convertedBezier.map(point => {
+ return { X: point.X + startPoint.X - this._size / 1.5, Y: point.Y + startPoint.Y - this._size / 2 };
+ }),
+ (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.stroke : undefined,
+ (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.fill : undefined,
+ ]);
+ });
+ if (regenerate) {
+ if (this._deleteFunc !== unimplementedFunction) this._deleteFunc(this._selectedDoc);
+ this._addFunc(strokeData, this._lastInput, svg[0]);
+ } else {
+ this._addFunc(strokeData, this._lastInput, svg[0]);
+ }
+ return { data: strokeData, lastInput: this._lastInput, lastRes: svg[0] };
+ }
+ };
+
+ render() {
+ if (this._display) {
+ return (
+ <div
+ id="label-handler"
+ className="smart-draw-handler"
+ style={{
+ display: this._display ? '' : 'none',
+ left: this._pageX,
+ ...(this._yRelativeToTop ? { top: Math.max(0, this._pageY) } : { bottom: this._pageY }),
+ background: SettingsManager.userBackgroundColor,
+ color: SettingsManager.userColor,
+ }}>
+ <div>
+ <IconButton
+ tooltip={'Cancel'}
+ onClick={() => {
+ this.hideSmartDrawHandler();
+ this.hideRegenerate();
+ }}
+ icon={<FontAwesomeIcon icon="xmark" />}
+ color={SettingsManager.userColor}
+ style={{ width: '19px' }}
+ />
+ <input
+ aria-label="Smart Draw Input"
+ className="smartdraw-input"
+ id="smartdraw-input"
+ type="text"
+ style={{ color: 'black' }}
+ value={this._userInput}
+ onChange={e => {
+ this.setUserInput(e.target.value);
+ }}
+ placeholder="Enter item to draw"
+ onKeyDown={this.handleKeyPress}
+ />
+ <IconButton tooltip="Advanced Options" icon={<FontAwesomeIcon icon={this._showOptions ? 'caret-down' : 'caret-right'} />} color={SettingsManager.userColor} style={{ width: '14px' }} onClick={this.setShowOptions} />
+ <Button
+ style={{ alignSelf: 'flex-end' }}
+ text="Send"
+ icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+ iconPlacement="right"
+ color={SettingsManager.userColor}
+ onClick={this.handleSendClick}
+ />
+ </div>
+ {this._showOptions && (
+ <>
+ <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-around' }}>
+ <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '30%' }}>
+ Auto color
+ <Switch
+ sx={{
+ '& .MuiSwitch-switchBase.Mui-checked': {
+ color: SettingsManager.userColor,
+ },
+ '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': {
+ backgroundColor: SettingsManager.userVariantColor,
+ },
+ }}
+ defaultChecked={true}
+ value={this._autoColor}
+ size="small"
+ onChange={this.setAutoColor}
+ />
+ </div>
+ <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '31%' }}>
+ Complexity
+ <Slider
+ sx={{
+ '& .MuiSlider-thumb': {
+ color: SettingsManager.userColor,
+ '&.Mui-focusVisible, &:hover, &.Mui-active': {
+ boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10`,
+ },
+ },
+ '& .MuiSlider-track': {
+ color: SettingsManager.userVariantColor,
+ },
+ '& .MuiSlider-rail': {
+ color: SettingsManager.userColor,
+ },
+ }}
+ style={{ width: '80%' }}
+ min={1}
+ max={10}
+ step={1}
+ size="small"
+ value={this._complexity}
+ onChange={(e, val) => {
+ this.setComplexity(val as number);
+ }}
+ valueLabelDisplay="auto"
+ />
+ </div>
+ <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '39%' }}>
+ Size (in pixels)
+ <Slider
+ sx={{
+ '& .MuiSlider-thumb': {
+ color: SettingsManager.userColor,
+ '&.Mui-focusVisible, &:hover, &.Mui-active': {
+ boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}20`,
+ },
+ },
+ '& .MuiSlider-track': {
+ color: SettingsManager.userVariantColor,
+ },
+ '& .MuiSlider-rail': {
+ color: SettingsManager.userColor,
+ },
+ }}
+ style={{ width: '80%' }}
+ min={50}
+ max={700}
+ step={10}
+ size="small"
+ value={this._size}
+ onChange={(e, val) => {
+ this.setSize(val as number);
+ }}
+ valueLabelDisplay="auto"
+ />
+ </div>
+ </div>
+ </>
+ )}
+ </div>
+ );
+ } else if (this._showRegenerate) {
+ return (
+ <div
+ id="smartdraw-options-menu"
+ className="smart-draw-handler"
+ style={{
+ left: this._pageX,
+ ...(this._yRelativeToTop ? { top: Math.max(0, this._pageY) } : { bottom: this._pageY }),
+ background: SettingsManager.userBackgroundColor,
+ color: SettingsManager.userColor,
+ }}>
+ <div
+ style={{
+ display: 'flex',
+ flexDirection: 'row',
+ }}>
+ <IconButton
+ tooltip="Regenerate"
+ icon={this._isLoading && this._regenInput === '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <FontAwesomeIcon icon={'rotate'} />}
+ color={SettingsManager.userColor}
+ onClick={this.handleSendClick}
+ />
+ <IconButton tooltip="Edit with GPT" icon={<FontAwesomeIcon icon="pen-to-square" />} color={SettingsManager.userColor} onClick={this.edit} />
+ {this._showEditBox && (
+ <div
+ style={{
+ display: 'flex',
+ flexDirection: 'row',
+ }}>
+ <input
+ aria-label="Edit instructions input"
+ className="smartdraw-input"
+ id="regen-input"
+ type="text"
+ style={{ color: 'black' }}
+ value={this._regenInput}
+ onChange={e => {
+ this.setRegenInput(e.target.value);
+ }}
+ onKeyDown={this.handleKeyPress}
+ placeholder="Edit instructions"
+ />
+ <Button
+ style={{ alignSelf: 'flex-end' }}
+ text="Send"
+ icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+ iconPlacement="right"
+ color={SettingsManager.userColor}
+ onClick={this.handleSendClick}
+ />
+ </div>
+ )}
+ </div>
+ </div>
+ );
+ } else {
+ return <></>;
+ }
+ }
+}