aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/smartdraw/SmartDrawHandler.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/smartdraw/SmartDrawHandler.tsx')
-rw-r--r--src/client/views/smartdraw/SmartDrawHandler.tsx407
1 files changed, 407 insertions, 0 deletions
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
new file mode 100644
index 000000000..6d2cc0593
--- /dev/null
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -0,0 +1,407 @@
+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 './ImageLabelHandler.scss';
+import { gptAPICall, GPTCallType } from '../../apis/gpt/GPT';
+import { InkData } 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';
+
+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 private _showRegenerate: boolean = false;
+ @observable private _complexity: number = 5;
+ @observable private _size: number = 200;
+ @observable private _autoColor: boolean = true;
+ @observable private _regenInput: string = '';
+ private _addFunc: (e: React.PointerEvent<Element>, strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => void = () => {};
+ private _deleteFunc: (doc?: Doc) => void = () => {};
+ private _lastInput: DrawingOptions = { text: '', complexity: 5, size: 300, autoColor: true, x: 0, y: 0 };
+ private _lastResponse: string = '';
+ private _selectedDoc: Doc | undefined = undefined;
+
+ constructor(props: any) {
+ super(props);
+ makeObservable(this);
+ SmartDrawHandler.Instance = this;
+ }
+
+ @action
+ setUserInput = (input: string) => {
+ this._userInput = input;
+ };
+
+ @action
+ setRegenInput = (input: string) => {
+ this._regenInput = input;
+ };
+
+ @action
+ setShowOptions = () => {
+ this._showOptions = !this._showOptions;
+ };
+
+ @action
+ setComplexity = (val: number) => {
+ this._complexity = val;
+ };
+
+ @action
+ setSize = (val: number) => {
+ this._size = val;
+ };
+
+ @action
+ setAutoColor = () => {
+ this._autoColor = !this._autoColor;
+ };
+
+ @action
+ displaySmartDrawHandler = (x: number, y: number, addFunc: (e: React.PointerEvent<Element>, strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => 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: (e: React.PointerEvent<Element>, strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => void, deleteFunc: (doc?: Doc) => void) => {
+ const selectedDoc: Doc = DocumentView.SelectedDocs().lastElement();
+ const docData = selectedDoc[DocData];
+ this._addFunc = addFunc;
+ this._deleteFunc = deleteFunc;
+ this._pageX = x;
+ this._pageY = y;
+ this._showRegenerate = true;
+ this._lastResponse = docData.drawingData as string;
+ this._lastInput = { text: docData.drawingInput as string, complexity: docData.drawingComplexity as number, size: docData.drawingSize as number, autoColor: docData.drawingColored as boolean, 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 = 300;
+ this._autoColor = true;
+ // this._regenInput = ''
+ };
+
+ @action
+ hideRegenerate = () => {
+ this._showRegenerate = false;
+ this._isLoading = false;
+ this._regenInput = '';
+ };
+
+ _errorOccurredOnce = false;
+ @action
+ drawWithGPT = async (e: React.PointerEvent<Element>, input: string) => {
+ if (input === '') return;
+ this._lastInput = { text: input, complexity: this._complexity, size: this._size, autoColor: this._autoColor, x: e.clientX, y: e.clientY };
+ this._isLoading = true;
+ this._showOptions = false;
+ try {
+ const res = await gptAPICall(`"${input}", "${this._complexity}", "${this._size}"`, GPTCallType.DRAW, undefined, true);
+ if (!res) {
+ console.error('GPT call failed');
+ return;
+ }
+ console.log(res);
+ await this.parseResponse(e, res, { X: e.clientX, Y: e.clientY }, false);
+ this.hideSmartDrawHandler();
+ this._showRegenerate = true;
+ this._errorOccurredOnce = false;
+ } catch (err) {
+ if (this._errorOccurredOnce) {
+ console.error('GPT call failed', err);
+ this._errorOccurredOnce = false;
+ } else {
+ this._errorOccurredOnce = true;
+ this.drawWithGPT(e, input);
+ }
+ }
+ this._isLoading = false;
+ };
+
+ @action
+ edit = () => {
+ this._showEditBox = !this._showEditBox;
+ };
+
+ @action
+ regenerate = async (e: React.PointerEvent<Element>) => {
+ this._isLoading = true;
+ 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);
+ this.parseResponse(e, res, { X: this._lastInput.x, Y: this._lastInput.y }, true);
+ } catch (err) {
+ console.error('GPT call failed', err);
+ }
+ this._isLoading = false;
+ this._regenInput = '';
+ this._showEditBox = false;
+ };
+
+ @action
+ parseResponse = async (e: React.PointerEvent<Element>, res: string, startPoint: { X: number; Y: number }, regenerate: boolean) => {
+ const svg = res.match(/<svg[^>]*>([\s\S]*?)<\/svg>/g);
+ console.log('start point is', startPoint);
+ if (svg) {
+ this._lastResponse = svg[0];
+ const svgObject = await parse(svg[0]);
+ const svgStrokes: any = svgObject.children;
+ const strokeData: [InkData, string, string][] = [];
+ console.log('autocolor is', this._autoColor);
+ 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 : this._autoColor) ? child.attributes.stroke : undefined,
+ (regenerate ? this._lastInput.autoColor : this._autoColor) ? child.attributes.fill : undefined,
+ ]);
+ });
+ if (regenerate) {
+ this._deleteFunc(this._selectedDoc);
+ }
+ this._addFunc(e, strokeData, this._lastInput, svg[0]);
+ }
+ };
+
+ render() {
+ if (this._display) {
+ return (
+ <div
+ id="label-handler"
+ className="contextMenu-cont"
+ 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="label-input"
+ id="new-label"
+ type="text"
+ style={{ color: 'black' }}
+ value={this._userInput}
+ onChange={e => {
+ this.setUserInput(e.target.value);
+ }}
+ placeholder="Enter item to draw"
+ />
+ <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={e => {
+ this.drawWithGPT(e as React.PointerEvent<Element>, this._userInput);
+ }}
+ />
+ </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}
+ 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="contextMenu-cont"
+ 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={e => {
+ this.regenerate(e as React.PointerEvent<Element>);
+ }}
+ />
+ <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"
+ id="regen-input"
+ type="text"
+ style={{ color: 'black' }}
+ value={this._regenInput}
+ onChange={e => {
+ this.setRegenInput(e.target.value);
+ }}
+ 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={e => {
+ this.regenerate(e as React.PointerEvent<Element>);
+ }}
+ />
+ </div>
+ )}
+ </div>
+ </div>
+ );
+ } else {
+ return <></>;
+ }
+ }
+}