diff options
Diffstat (limited to 'src/client/views/smartdraw/SmartDrawHandler.tsx')
-rw-r--r-- | src/client/views/smartdraw/SmartDrawHandler.tsx | 255 |
1 files changed, 165 insertions, 90 deletions
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx index 7db9ef133..1cceabed3 100644 --- a/src/client/views/smartdraw/SmartDrawHandler.tsx +++ b/src/client/views/smartdraw/SmartDrawHandler.tsx @@ -1,19 +1,21 @@ +import { Button, IconButton } from '@dash/components'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Checkbox, FormControlLabel, Radio, RadioGroup, Slider, Switch } from '@mui/material'; -import { Button, IconButton } from '@dash/components'; import { action, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import React from 'react'; import { AiOutlineSend } from 'react-icons/ai'; import ReactLoading from 'react-loading'; import { INode, parse } from 'svgson'; -import { imageUrlToBase64 } from '../../../ClientUtils'; +import { imageUrlToBase64, setupMoveUpEvents } from '../../../ClientUtils'; import { unimplementedFunction } from '../../../Utils'; import { Doc, DocListCast } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { InkData, InkField, InkTool } from '../../../fields/InkField'; import { BoolCast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; +import { Networking } from '../../Network'; import { GPTCallType, gptAPICall, gptDrawingColor } from '../../apis/gpt/GPT'; +import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; import { SettingsManager } from '../../util/SettingsManager'; import { undoable } from '../../util/UndoManager'; @@ -21,22 +23,24 @@ import { SVGToBezier, SVGType } from '../../util/bezierFit'; import { InkingStroke } from '../InkingStroke'; import { ObservableReactComponent } from '../ObservableReactComponent'; import { MarqueeView } from '../collections/collectionFreeForm'; -import { ActiveInkArrowEnd, ActiveInkArrowStart, ActiveInkDash, ActiveInkFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView, DocumentViewInternal } from '../nodes/DocumentView'; +import { ActiveInkArrowEnd, ActiveInkArrowStart, ActiveInkBezierApprox, ActiveInkColor, ActiveInkDash, ActiveInkFillColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../nodes/DocumentView'; +import { FireflyDimensionsMap, FireflyImageData, FireflyImageDimensions } from './FireflyConstants'; import './SmartDrawHandler.scss'; -import { Networking } from '../../Network'; -import { OpenWhere } from '../nodes/OpenWhere'; -import { FireflyDimensionsMap, FireflyImageDimensions, FireflyImageData } from './FireflyConstants'; -import { DocumentType } from '../../documents/DocumentTypes'; +import { Upload } from '../../../server/SharedMediaTypes'; +import { PointData } from '../../../pen-gestures/GestureTypes'; +import { List } from '../../../fields/List'; export interface DrawingOptions { - text: string; - complexity: number; - size: number; - autoColor: boolean; - x: number; - y: number; + text?: string; + complexity?: number; + size?: number; + autoColor?: boolean; + x?: number; + y?: number; } +type svgparsedData = [PointData[], string, string]; + /** * The SmartDrawHandler allows users to generate drawings with GPT from text input. Users are able to enter * the item to draw, how complex they want the drawing to be, how large the drawing should be, and whether @@ -60,7 +64,6 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { private _lastInput: DrawingOptions = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 }; private _lastResponse: string = ''; private _selectedDocs: Doc[] = []; - private _errorOccurredOnce = false; @observable private _display: boolean = false; @observable private _pageX: number = 0; @@ -95,7 +98,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { CollectionFreeForm, FormattedTextBox, StickerPalette) to define how a drawing document should be added or removed in their respective locations (to the freeform canvas, to the sticker palette's preview, etc.) */ - public AddDrawing: (doc: Doc, opts: DrawingOptions, gptRes: string) => void = unimplementedFunction; + public AddDrawing: (doc: Doc, opts: DrawingOptions, gptRes: string, x?: number, y?: number) => void = unimplementedFunction; public RemoveDrawing: (useLastContainer: boolean, doc?: Doc) => void = unimplementedFunction; /** * This creates the ink document that represents a drawing, so it goes through the strokes that make up the drawing, @@ -103,7 +106,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { * classes to customize the way the drawing docs get created. For example, the freeform canvas has a different way of * defining document bounds, so CreateDrawingDoc is redefined when that class calls gpt draw functions. */ - public CreateDrawingDoc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => Doc | undefined = (strokeList: [InkData, string, string][], opts: DrawingOptions) => { + public static CreateDrawingDoc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => Doc | undefined = (strokeList: [InkData, string, string][], opts: DrawingOptions) => { const drawing: Doc[] = []; strokeList.forEach((stroke: [InkData, string, string]) => { const bounds = InkField.getBounds(stroke[0]); @@ -115,7 +118,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { y: bounds.top - inkWidth / 2, _width: bounds.width + inkWidth, _height: bounds.height + inkWidth, - stroke_showLabel: !BoolCast(Doc.UserDoc().activeHideTextLabels)}, // prettier-ignore + stroke_showLabel: false}, // prettier-ignore inkWidth, opts.autoColor ? stroke[1] : ActiveInkColor(), ActiveInkBezierApprox(), @@ -189,9 +192,9 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { /** * This allows users to press the return/enter key to send input. */ - handleKeyPress = (event: React.KeyboardEvent) => { - if (event.key === 'Enter') { - this.handleSendClick(); + handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + this.handleSendClick(this._pageX, this._pageY); } }; @@ -201,41 +204,24 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { * what the user sees. */ @action - handleSendClick = async () => { + handleSendClick = async (X: number, Y: number) => { if ((!this.ShowRegenerate && this._userInput == '') || (!this._generateImage && !this._generateDrawing)) return; this._isLoading = true; this._canInteract = false; if (this.ShowRegenerate) { - await this.regenerate(this._selectedDocs); - runInAction(() => { - this._selectedDocs = []; - this._regenInput = ''; - this._showEditBox = false; - }); + await this.regenerate(this._selectedDocs, undefined, undefined, this._regenInput).then(action(() => (this._showEditBox = false))); } else { - runInAction(() => { - this._showOptions = false; - }); + this._showOptions = false; try { if (this._generateImage) { await this.createImageWithFirefly(this._userInput); } if (this._generateDrawing) { - await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor); + await this.drawWithGPT({ X, Y }, this._userInput, this._complexity, this._size, this._autoColor); } this.hideSmartDrawHandler(); - - runInAction(() => { - this.ShowRegenerate = true; - }); } catch (err) { - if (this._errorOccurredOnce) { - console.error('GPT call failed', err); - this._errorOccurredOnce = false; - } else { - this._errorOccurredOnce = true; - await this.handleSendClick(); - } + console.error('GPT call failed', err); } } runInAction(() => { @@ -247,16 +233,15 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { /** * Calls GPT API to create a drawing based on user input. */ - drawWithGPT = async (startPt: { X: number; Y: number }, input: string, complexity: number, size: number, autoColor: boolean) => { + drawWithGPT = async (screenPt: { X: number; Y: number }, input: string, complexity: number, size: number, autoColor: boolean) => { if (input) { - this._lastInput = { text: input, complexity: complexity, size: size, autoColor: autoColor, x: startPt.X, y: startPt.Y }; + this._lastInput = { text: input, complexity: complexity, size: size, autoColor: autoColor, x: screenPt.X, y: screenPt.Y }; const res = await gptAPICall(`"${input}", "${complexity}", "${size}"`, GPTCallType.DRAW, undefined, true); if (res) { - const strokeData = await this.parseSvg(res, startPt, false, autoColor); - const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes); - drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res); + const strokeData = await this.parseSvg(res, { X: 0, Y: 0 }, false, autoColor); + const drawingDoc = strokeData && SmartDrawHandler.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes); + drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res, screenPt.X, screenPt.Y); drawingDoc && this._selectedDocs.push(drawingDoc); - this._errorOccurredOnce = false; return strokeData; } else { console.error('GPT call failed'); @@ -268,28 +253,64 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { /** * Calls Firefly API to create an image based on user input */ - createImageWithFirefly = (input: string, seed?: number, changeInPlace?: boolean): Promise<FireflyImageData> => { + createImageWithFirefly = (input: string, seed?: number): Promise<FireflyImageData | Doc | undefined> => { this._lastInput.text = input; - const dims = FireflyDimensionsMap[this._imgDims]; + return SmartDrawHandler.CreateWithFirefly(input, this._imgDims, seed).then(doc => { + doc instanceof Doc && this.AddDrawing(doc, this._lastInput, input, this._pageX, this._pageY); + return doc; + }); + }; /** + * Calls Firefly API to create an image based on user input + */ + recreateImageWithFirefly = (input: string, seed?: number): Promise<FireflyImageData | Doc | undefined> => { + this._lastInput.text = input; + return SmartDrawHandler.ReCreateWithFirefly(input, this._imgDims, seed); + }; + public static ReCreateWithFirefly(input: string, imgDims: FireflyImageDimensions, seed?: number): Promise<FireflyImageData | Doc | undefined> { + const dims = FireflyDimensionsMap[imgDims]; return Networking.PostToServer('/queryFireflyImage', { prompt: input, width: dims.width, height: dims.height, seed }) - .then(img => { - const newseed = img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1]; - if (!changeInPlace) { - const imgDoc: Doc = Docs.Create.ImageDocument(img.accessPaths.agnostic.client, { - title: input.match(/^(.*?)~~~.*$/)?.[1] || input, - nativeWidth: dims.width, - nativeHeight: dims.height, - ai: 'firefly', - ai_firefly_seed: newseed, - ai_firefly_prompt: input, - }); - DocumentViewInternal.addDocTabFunc(imgDoc, OpenWhere.addRight); - this._selectedDocs.push(imgDoc); + .then(res => { + const img = res as Upload.FileInformation; + const error = res as { error: string }; + if ('error' in error) { + alert('recreate image failed: ' + error.error); + return undefined; } return { prompt: input, seed, pathname: img.accessPaths.agnostic.client }; }) - .catch(e => alert('create image failed: ' + e.toString())); - }; + .catch(e => { + alert('recreate image failed: ' + e.toString()); + return undefined; + }); + } + public static CreateWithFirefly(input: string, imgDims: FireflyImageDimensions, seed?: number): Promise<FireflyImageData | Doc | undefined> { + const dims = FireflyDimensionsMap[imgDims]; + return Networking.PostToServer('/queryFireflyImage', { prompt: input, width: dims.width, height: dims.height, seed }) + .then(res => { + const img = res as Upload.FileInformation; + const error = res as { error: string }; + if ('error' in error) { + alert('create image failed: ' + error.error); + return undefined; + } + const newseed = img.accessPaths.agnostic.client.match(/\/(\d+)upload/)?.[1]; + return Docs.Create.ImageDocument(img.accessPaths.agnostic.client, { + title: input, + nativeWidth: dims.width, + nativeHeight: dims.height, + tags: new List<string>(['@ai']), + _width: Math.min(400, dims.width), + _height: (Math.min(400, dims.width) * dims.height) / dims.width, + ai: 'firefly', + ai_firefly_seed: +(newseed ?? 0), + ai_firefly_prompt: input, + }); + }) + .catch(e => { + alert('create image failed: ' + e.toString()); + return undefined; + }); + } /** * Regenerates drawings with the option to add a specific regenerate prompt/request. @@ -303,28 +324,25 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { return Promise.all( drawingDocs.map(async doc => { switch (doc.type) { - case DocumentType.IMG: - if (this._regenInput) { - // if (this._selectedDoc) { - const newPrompt = doc.ai_firefly_prompt ? `${doc.ai_firefly_prompt} ~~~ ${this._regenInput}` : this._regenInput; - return this.createImageWithFirefly(newPrompt, NumCast(doc?.ai_firefly_seed), changeInPlace); - // } - } - return this.createImageWithFirefly(this._lastInput.text || StrCast(doc.ai_firefly_prompt), undefined, changeInPlace); + case DocumentType.IMG: { + const func = changeInPlace ? this.recreateImageWithFirefly : this.createImageWithFirefly; + const newPrompt = doc.ai_firefly_prompt ? `${doc.ai_firefly_prompt} ~~~ ${this._regenInput}` : this._regenInput; + return this._regenInput ? func(newPrompt, NumCast(doc?.ai_firefly_seed)) : func(this._lastInput.text || StrCast(doc.ai_firefly_prompt)); + } case DocumentType.COL: { try { - let res; - if (this._regenInput) { - const prompt = `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); - } + const res = await (async () => { + if (this._regenInput) { + const prompt = `This is your previously generated svg code: ${this._lastResponse} for the user input "${this._lastInput.text}". Please regenerate it with the provided specifications.`; + this._lastInput.text = `${this._lastInput.text} ~~~ ${this._regenInput}`; + return gptAPICall(`"${this._regenInput}"`, GPTCallType.DRAW, prompt, true); + } + return gptAPICall(`"${this._lastInput.text}", "${this._lastInput.complexity}", "${this._lastInput.size}"`, GPTCallType.DRAW, undefined, true); + })(); if (res) { - const strokeData = await this.parseSvg(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor); + const strokeData = await this.parseSvg(res, { X: this._lastInput.x ?? 0, Y: this._lastInput.y ?? 0 }, true, lastInput?.autoColor || this._autoColor); this.RemoveDrawing !== unimplementedFunction && this.RemoveDrawing(true, doc); - const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes); + const drawingDoc = strokeData && SmartDrawHandler.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes); drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res); } else { console.error('GPT call failed'); @@ -344,20 +362,35 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { */ parseSvg = 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]); + console.log(res, svgObject); const svgStrokes: INode[] = svgObject.children; const strokeData: [InkData, string, string][] = []; + + const tl = { X: Number.MAX_SAFE_INTEGER, Y: Number.MAX_SAFE_INTEGER }; + let last: PointData = { X: 0, Y: 0 }; svgStrokes.forEach(child => { - const convertedBezier: InkData = SVGToBezier(child.name as SVGType, child.attributes); + const convertedBezier: InkData = SVGToBezier(child.name as SVGType, child.attributes, last); + last = convertedBezier.lastElement(); strokeData.push([ - convertedBezier.map(point => ({ X: startPoint.X + (point.X - startPoint.X) * this._scale, Y: startPoint.Y + (point.Y - startPoint.Y) * this._scale })), + convertedBezier.map(point => { + if (point.X < tl.X) tl.X = point.X; + if (point.Y < tl.Y) tl.Y = point.Y; + return { X: point.X, Y: point.Y }; + }), (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.stroke : '', (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.fill : '', ]); }); - return { data: strokeData, lastInput: this._lastInput, lastRes: svg[0] }; + const mapStroke = (pd: PointData): PointData => ({ X: startPoint.X + (pd.X - tl.X) * this._scale, Y: startPoint.Y + (pd.Y - tl.Y) * this._scale }); + return { + data: strokeData.map(sdata => [sdata[0].map(mapStroke), sdata[1], sdata[2]] as svgparsedData), + lastInput: this._lastInput, + lastRes: svg[0], + }; } }; @@ -427,7 +460,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { }, }} checked={this._generateImage} - onChange={() => this._canInteract && (this._generateImage = !this._generateImage)} + onChange={action(() => this._canInteract && (this._generateImage = !this._generateImage))} /> </div> </div> @@ -507,6 +540,19 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { return ( <div className="smart-draw-handler" + onPointerDown={e => + setupMoveUpEvents( + this, + e, + action(me => { + this._pageX = this._pageX + me.movementX; + this._pageY = this._pageY + me.movementY; + return false; + }), + () => {}, + () => {} + ) + } style={{ display: this._display ? '' : 'none', left: this._pageX, @@ -525,6 +571,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { color={SettingsManager.userColor} /> <input + style={{ color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }} aria-label="Smart Draw Input" className="smartdraw-input" type="text" @@ -541,7 +588,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />} iconPlacement="right" color={SettingsManager.userColor} - onClick={this.handleSendClick} + onClick={() => this.handleSendClick(this._pageX, this._pageY)} /> </div> {this._showOptions && ( @@ -565,6 +612,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { onChange={action(e => this._canInteract && (this._regenInput = e.target.value))} onKeyDown={this.handleKeyPress} placeholder="Edit instructions" + onPointerDown={e => e.stopPropagation()} /> <Button style={{ alignSelf: 'flex-end' }} @@ -572,15 +620,42 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />} iconPlacement="right" color={SettingsManager.userColor} - onClick={this.handleSendClick} + onClick={() => this.handleSendClick(this._pageX, this._pageY)} /> </div> ); + startDragging = (e: PointerEvent) => { + setupMoveUpEvents( + this, + e, + action(me => { + this._pageX = this._pageX + me.movementX; + this._pageY = this._pageY + me.movementY; + return false; + }), + () => {}, + () => {} + ); + }; renderRegenerate = () => ( <div className="smart-draw-handler" + onPointerDown={e => + setupMoveUpEvents( + this, + e, + action(me => { + this._pageX = this._pageX + me.movementX; + this._pageY = this._pageY + me.movementY; + return false; + }), + () => {}, + () => {} + ) + } style={{ + padding: 10, left: this._pageX, ...(this._yRelativeToTop ? { top: Math.max(0, this._pageY) } : { bottom: this._pageY }), background: SettingsManager.userBackgroundColor, @@ -591,7 +666,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { 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} + onClick={() => this.handleSendClick(this._pageX, this._pageY)} /> <IconButton tooltip="Edit with GPT" icon={<FontAwesomeIcon icon="pen-to-square" />} color={SettingsManager.userColor} onClick={action(() => (this._showEditBox = !this._showEditBox))} /> {this._showEditBox ? this.renderRegenerateEditBox() : null} |