diff options
author | eleanor-park <eleanor_park@brown.edu> | 2024-06-20 16:42:44 -0400 |
---|---|---|
committer | eleanor-park <eleanor_park@brown.edu> | 2024-06-20 16:42:44 -0400 |
commit | 3d14f06ad1297a6adc851945804211efb9dff7ff (patch) | |
tree | e48202e43999230b5e202af53d31df9aaf1901c7 /src | |
parent | bd64bbd29a38ae4979b2165d1fa9b9c76c2600d5 (diff) |
user customization added
Diffstat (limited to 'src')
5 files changed, 209 insertions, 83 deletions
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts index 06c562562..b5f4c7fe9 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -58,7 +58,7 @@ const callTypeMap: { [type: string]: GPTCallOpts } = { model: 'gpt-4o', maxTokens: 1024, temp: 0.5, - prompt: 'Given an item, generate a detailed line drawing representation of it. The drawing should be in SVG format with no additional text or comments. For path coordinates, make sure you format with a comma between numbers, like M100,200 C150,250 etc. The only supported commands are line, ellipse, circle, rect, and path with M, Q, C, and L so only use those.', + prompt: 'Given an item, a level of complexity from 1-10, and a size in pixels, generate a detailed and colored line drawing representation of it. More complex drawings will have much more detail and strokes. The drawing should be in SVG format with no additional text or comments. For path coordinates, make sure you format with a comma between numbers, like M100,200 C150,250 etc. The only supported commands are line, ellipse, circle, rect, and path with M, Q, C, and L so only use those.', // prompt: 'I would like you to generate me vector art with Bezier curves. Given a prompt, generate a sequence of cubic Bezier coordinates in the range of 0 to 200 (unless specified larger/smaller) that creates a line drawing of the object. Format your response like this: M (100,30) C (75,10) (25,10) (50,50) C (25,75) (10,125) (50,150) C (25,75) (10,125) (50,150) and give no additional text. If a disconnected stroke is required, repeat that pattern with a new M marker', }, }; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index cbc337860..67b875ecb 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -442,6 +442,7 @@ export class MainView extends ObservableReactComponent<{}> { fa.faEyeDropper, fa.faPaintRoller, fa.faBars, + fa.faBarsStaggered, fa.faBrush, fa.faShapes, fa.faEllipsisH, diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 93b63ac4c..b8257ff31 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1268,23 +1268,25 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; @undoBatch - createInkStrokes = (strokeList: InkData[], alpha?: number) => { - console.log(strokeList.length); - strokeList.forEach(inkData => { + createInkStrokes = (strokeData: [InkData, string, string][]) => { + strokeData.forEach((stroke: [InkData, string, string]) => { // const points: InkData = FitCurve(inkData, 20) as InkData; // const allPts = GenerateControlPoints(inkData, alpha); - const bounds = InkField.getBounds(inkData); + const bounds = InkField.getBounds(stroke[0]); const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height); const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale; const inkDoc = Docs.Create.InkDocument( - inkData, + 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 + inkWidth, + stroke[1], + undefined, + stroke[2] === 'none' ? undefined : stroke[2] ); this.addDocument(inkDoc); }); diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelHandler.scss b/src/client/views/collections/collectionFreeForm/ImageLabelHandler.scss index e7413bf8e..9b8727e1a 100644 --- a/src/client/views/collections/collectionFreeForm/ImageLabelHandler.scss +++ b/src/client/views/collections/collectionFreeForm/ImageLabelHandler.scss @@ -42,3 +42,17 @@ } } } + +.complexity-slider { + width: 50%; /* Full-width */ + height: 25px; /* Specified height */ + background: #d3d3d3; /* Grey background */ + outline: none; /* Remove outline */ + opacity: 0.7; /* Set transparency (for mouse-over effects on hover) */ + -webkit-transition: 0.2s; /* 0.2 seconds transition on hover */ + transition: opacity 0.2s; + + :hover { + opacity: 1; /* Fully shown on mouse-over */ + } +} diff --git a/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx b/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx index 956a8d7e9..edb814172 100644 --- a/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx +++ b/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx @@ -4,16 +4,20 @@ 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 { Button, IconButton, Size } from 'browndash-components'; import ReactLoading from 'react-loading'; import { AiOutlineSend } from 'react-icons/ai'; -import { MarqueeOptionsMenu } from './MarqueeOptionsMenu'; import './ImageLabelHandler.scss'; import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT'; import { InkData } from '../../../../fields/InkField'; -import { ButtonType } from '../../nodes/FontIconBox/FontIconBox'; import { SVGToBezier } from '../../../util/bezierFit'; -const { parse, stringify } = require('svgson'); +const { parse } = require('svgson'); +import { Slider, Switch } from '@mui/material'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { Flex } from '@adobe/react-spectrum'; +import { Row } from 'react-aria-components'; +import { UndoManager } from '../../../util/UndoManager'; +import e from 'cors'; @observer export class SmartDrawHandler extends ObservableReactComponent<{}> { @@ -25,11 +29,15 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> { @observable private _yRelativeToTop: boolean = true; @observable private _isLoading: boolean = false; @observable private _userInput: string = ''; - @observable private _drawingTypeToolTip = 'Create Geometric Drawing'; - @observable private _drawingTypeIcon: 'star' | 'splotch' = 'star'; - @observable private _alpha: number | undefined = undefined; // number between 0 and 1 that determines how rounded a drawing will be - // @observable public strokes: InkData[] = []; - private _addToDocFunc: (strokeList: InkData[], alpha?: number) => void = () => {}; + @observable private _showOptions: boolean = false; + @observable private _menuIcon: string = 'caret-right'; + @observable private _complexity: number = 5; + @observable private _size: number = 300; + @observable private _autoColor: boolean = true; + @observable private _showRegenerate: boolean = false; + private _addToDocFunc: (strokeList: [InkData, string, string][]) => void = () => {}; + private _lastX: number = 0; + private _lastY: number = 0; constructor(props: any) { super(props); @@ -38,96 +46,81 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> { } @action - setIsLoading = (isLoading: boolean) => { - this._isLoading = isLoading; - }; - - @action setUserInput = (input: string) => { this._userInput = input; }; @action - displaySmartDrawHandler = (x: number, y: number, addToDoc: (strokeList: InkData[], alpha?: number) => void) => { + displaySmartDrawHandler = (x: number, y: number, addToDoc: (strokeData: [InkData, string, string][]) => void) => { this._pageX = x; this._pageY = y; this._display = true; this._addToDocFunc = addToDoc; }; - @action hideSmartDrawHandler = () => { + this._showRegenerate = false; this._display = false; + this._isLoading = false; + this._showOptions = false; + this._menuIcon = 'caret-right'; + }; + + hideRegenerate = () => { + this._showRegenerate = false; + this._userInput = ''; + this._complexity = 5; + this._size = 300; + this._autoColor = true; + this._isLoading = false; + }; + + toggleMenu = () => { + this._showOptions = !this._showOptions; + this._menuIcon === 'caret-right' ? (this._menuIcon = 'caret-down') : (this._menuIcon = 'caret-right'); }; @action - drawWithGPT = async (startPoint: { X: number; Y: number }, input: string) => { - console.log('start point is', startPoint); - this.setIsLoading(true); + drawWithGPT = async (e: React.MouseEvent<Element, MouseEvent>, startPoint: { X: number; Y: number }, input: string, regenerate: boolean = false) => { + if (this._userInput === '') return; + e.stopPropagation(); + this._lastX = startPoint.X; + this._lastY = startPoint.Y; + this._isLoading = true; + this._showOptions = false; try { - const res = await gptAPICall(input, GPTCallType.DRAW); + const res = await gptAPICall(`"${input}", "${this._complexity}", "${this._size}"`, GPTCallType.DRAW); if (!res) { console.error('GPT call failed'); return; } - console.log('GPT response:', res); const svg = res.match(/<svg[^>]*>([\s\S]*?)<\/svg>/g); - console.log('svg', svg); if (svg) { const svgObject = await parse(svg[0]); - console.log('svg object', svgObject); const svgStrokes: any = svgObject.children; - const beziers: InkData[] = []; - svgStrokes.forEach((stroke: any) => { - const convertedBezier: InkData = SVGToBezier(stroke.name, stroke.attributes); - beziers.push( + 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, Y: point.Y + startPoint.Y }; - }) - ); + return { X: point.X + startPoint.X - this._size / 1.5, Y: point.Y + startPoint.Y - this._size / 2 }; + }), + this._autoColor ? child.attributes.stroke : undefined, + this._autoColor ? child.attributes.fill : undefined, + ]); }); - this._addToDocFunc(beziers); + if (regenerate) UndoManager.Undo(); + this._addToDocFunc(strokeData); } - - // const strokes = res.trim().split(/\s*(?=\s*M)/); // prettier-ignore - // const parsedSegments: InkData[] = []; - // console.log('strokes', strokes); - // strokes.forEach(stroke => { - // stroke = stroke.replace(/C\s*\((\d+,\d+)\)\s*\((\d+,\d+)\)\s*\((\d+,\d+)\)/g, (c, p1, p2, p3) => { - // return `C (${p1}) (${p2}) (${p3}) (${p3})`; - // }); - // const coordStrings = stroke.match(/(\d+,\d+)/g); - // const coords: InkData = []; - // if (coordStrings) { - // coordStrings.forEach(coord => { - // const xy = coord.split(','); - // coords.push({ X: parseInt(xy[0]), Y: parseInt(xy[1]) }); - // }); - // coords.pop(); - // parsedSegments.push(coords); - // } - // console.log('coords', coords); - // }); - // this._addToDocFunc(parsedSegments); } catch (err) { console.error('GPT call failed', err); } - - this.setIsLoading(false); - this.setUserInput(''); this.hideSmartDrawHandler(); + this._showRegenerate = true; }; - changeDrawingType = () => { - if (this._drawingTypeIcon === 'star') { - this._drawingTypeIcon = 'splotch'; - this._drawingTypeToolTip = 'Create Rounded Drawing'; - this._alpha = 0.2; - } else { - this._drawingTypeIcon = 'star'; - this._drawingTypeToolTip = 'Create Geometric Drawing'; - this._alpha = 0; - } + regenerate = (e: React.MouseEvent<Element, MouseEvent>) => { + this.drawWithGPT(e, { X: this._lastX, Y: this._lastY }, `Regenerate the item "${this._userInput}"`, true); }; render() { @@ -144,7 +137,16 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> { color: SettingsManager.userColor, }}> <div> - <IconButton tooltip={'Cancel'} onClick={this.hideSmartDrawHandler} icon={<FontAwesomeIcon icon="xmark" />} color={MarqueeOptionsMenu.Instance.userColor} style={{ width: '19px' }} /> + <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" @@ -156,24 +158,131 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> { }} placeholder="Enter item to draw" /> - <IconButton tooltip={this._drawingTypeToolTip} icon={<FontAwesomeIcon icon={this._drawingTypeIcon} />} color={MarqueeOptionsMenu.Instance.userColor} style={{ width: '14px' }} onClick={this.changeDrawingType} /> - {/* <IconButton - tooltip="Create Geometric Drawing" - icon={<FontAwesomeIcon icon="star" />} - color={MarqueeOptionsMenu.Instance.userColor} + <IconButton + tooltip="Advanced Options" + icon={<FontAwesomeIcon icon={this._showOptions ? 'caret-down' : 'caret-right'} />} + color={SettingsManager.userColor} style={{ width: '14px' }} onClick={() => { - this._alpha = 0; + this._showOptions = !this._showOptions; }} - /> */} + /> <Button style={{ alignSelf: 'flex-end' }} text="Send" - icon={this._isLoading ? <ReactLoading type="spin" color="#ffffff" width={20} height={20} /> : <AiOutlineSend />} + icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />} iconPlacement="right" - color={MarqueeOptionsMenu.Instance.userColor} + color={SettingsManager.userColor} + onClick={e => { + this.drawWithGPT(e, { X: e.clientX, Y: e.clientY }, 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._autoColor = !this._autoColor)} + /> + </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._complexity = 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._size = 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="Cancel" onClick={this.hideRegenerate} icon={<FontAwesomeIcon icon="xmark" />} color={SettingsManager.userColor} style={{ width: '19px' }} /> + <IconButton + tooltip="Regenerate" + icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <FontAwesomeIcon icon={'rotate'} />} + color={SettingsManager.userColor} onClick={e => { - this.drawWithGPT({ X: e.clientX, Y: e.clientY }, this._userInput); + this.regenerate(e); }} /> </div> |