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.tsx255
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}