diff options
| author | eleanor-park <113556828+eleanor-park@users.noreply.github.com> | 2024-07-11 11:48:34 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-07-11 11:48:34 -0400 |
| commit | 4438e7fe202ff4091b26f073122e7866ec9abb46 (patch) | |
| tree | 19895e85364e58246ec2869459e9d29f6621526e /src/client/views/smartdraw | |
| parent | 59ca918ea0918b41f1e2fa4b6acb8725ca9b44af (diff) | |
| parent | f33e6c9e191092e6050f980892b4404ff0d0a1f2 (diff) | |
Merge branch 'eleanor-gptdraw' into master
Diffstat (limited to 'src/client/views/smartdraw')
| -rw-r--r-- | src/client/views/smartdraw/DrawingPalette.scss | 11 | ||||
| -rw-r--r-- | src/client/views/smartdraw/DrawingPalette.tsx | 89 | ||||
| -rw-r--r-- | src/client/views/smartdraw/SmartDrawHandler.tsx | 409 |
3 files changed, 509 insertions, 0 deletions
diff --git a/src/client/views/smartdraw/DrawingPalette.scss b/src/client/views/smartdraw/DrawingPalette.scss new file mode 100644 index 000000000..0f1152b71 --- /dev/null +++ b/src/client/views/smartdraw/DrawingPalette.scss @@ -0,0 +1,11 @@ +.drawing-palette { + display: grid; + grid-template-columns: auto; + position: absolute; + right: 14px; + width: 170px; + height: 170px; + top: 50px; + border-radius: 5px; + background-color: white; +} diff --git a/src/client/views/smartdraw/DrawingPalette.tsx b/src/client/views/smartdraw/DrawingPalette.tsx new file mode 100644 index 000000000..87a39bc85 --- /dev/null +++ b/src/client/views/smartdraw/DrawingPalette.tsx @@ -0,0 +1,89 @@ +import { computed, makeObservable, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { returnAll, returnFalse, returnOne, returnZero } from '../../../ClientUtils'; +import { Doc, StrListCast } from '../../../fields/Doc'; +import { emptyFunction } from '../../../Utils'; +import { CollectionViewType } from '../../documents/DocumentTypes'; +import { MarqueeView } from '../collections/collectionFreeForm'; +import { CollectionGridView } from '../collections/collectionGrid'; +import { CollectionStackingView } from '../collections/CollectionStackingView'; +import { DocumentView } from '../nodes/DocumentView'; +import { FieldViewProps } from '../nodes/FieldView'; +import { ObservableReactComponent } from '../ObservableReactComponent'; +import './DrawingPalette.scss'; + +@observer +export class DrawingPalette extends ObservableReactComponent<{}> { + @observable private _savedDrawings: Doc[] = []; + @observable _marqueeViewRef = React.createRef<MarqueeView>(); + private _stackRef = React.createRef<CollectionStackingView>(); + + constructor(props: any) { + super(props); + makeObservable(this); + } + + panelWidth = () => 100; + panelHeight = () => 100; + + getCollection = () => { + return this._marqueeViewRef.current?.collection(undefined, false, this._savedDrawings) || new Doc(); + }; + + @computed get savedDrawingAnnos() { + // const savedAnnos = Doc.MyDrawingAnnos; + return ( + <div className="collectionMenu-contMenuButtons" style={{ height: '100%' }}> + {/* <DocumentView PanelHeight={this.panelWidth} PanelWidth={this.panelHeight} Document={savedAnnos} renderDepth={2} isContentActive={returnFalse} childFilters={this.childFilters} /> */} + {/* <CollectionStackingView + {...this._props} + Document={savedAnnos} + // setContentViewBox={emptyFunction} + // NativeWidth={returnZero} + // NativeHeight={returnZero} + ref={this._stackRef} + PanelHeight={this.panelWidth} + PanelWidth={this.panelHeight} + // childFilters={this.childFilters} + // sortFunc={this.sortByLinkAnchorY} + // setHeight={this.setHeightCallback} + // isAnnotationOverlay={false} + // select={emptyFunction} + NativeDimScaling={returnOne} + // childlayout_showTitle={this.layout_showTitle} + isContentActive={returnFalse} + isSelected={returnFalse} + isAnyChildContentActive={returnFalse} + // childDocumentsActive={this._props.isContentActive} + whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged} + childHideDecorationTitle + // ScreenToLocalTransform={this.screenToLocalTransform} + renderDepth={this._props.renderDepth + 1} + type_collection={CollectionViewType.Stacking} + // fieldKey={'drawing-palette'} + pointerEvents={returnAll} + /> */} + </div> + ); + } + + render() { + return ( + <div className="drawing-palette"> + {/* {this._savedDrawings.map(doc => { + return <DocumentView + Document={doc} + renderDepth={0} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} + isContentActive={this.isContentActive} />; + })} */} + {/* <CollectionGridView {...this._props} /> */} + {} + {/* <DocumentView Document={this.getCollection()} /> */} + {this.savedDrawingAnnos} + </div> + ); + } +} diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx new file mode 100644 index 000000000..d24cc9d50 --- /dev/null +++ b/src/client/views/smartdraw/SmartDrawHandler.tsx @@ -0,0 +1,409 @@ +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, containerDoc?: Doc) => 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, 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: (e: React.PointerEvent<Element>, 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._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], this._selectedDoc); + } else { + 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 <></>; + } + } +} |
