diff options
| author | bobzel <zzzman@gmail.com> | 2025-03-16 19:02:56 -0400 |
|---|---|---|
| committer | bobzel <zzzman@gmail.com> | 2025-03-16 19:02:56 -0400 |
| commit | df708c90d8356934d2e3d9123129c761d328c1fe (patch) | |
| tree | 98b0588710ac8ca00c303960da0851614aacf597 /src/client/views/smartdraw/StickerPalette.tsx | |
| parent | 7d9fae09e8906e5636f6ea695ad560797b08d023 (diff) | |
| parent | f4042257be7359734a0dd35cedbf03fe4aa14cf1 (diff) | |
Merge branch 'DocCreatorMenu-work' of https://github.com/brown-dash/Dash-Web into DocCreatorMenu-work
Diffstat (limited to 'src/client/views/smartdraw/StickerPalette.tsx')
| -rw-r--r-- | src/client/views/smartdraw/StickerPalette.tsx | 352 |
1 files changed, 352 insertions, 0 deletions
diff --git a/src/client/views/smartdraw/StickerPalette.tsx b/src/client/views/smartdraw/StickerPalette.tsx new file mode 100644 index 000000000..e3305851a --- /dev/null +++ b/src/client/views/smartdraw/StickerPalette.tsx @@ -0,0 +1,352 @@ +import { Button } from '@dash/components'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Slider, Switch } from '@mui/material'; +import { action, makeObservable, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { AiOutlineSend } from 'react-icons/ai'; +import ReactLoading from 'react-loading'; +import { returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils'; +import { emptyFunction, numberRange } from '../../../Utils'; +import { Doc, DocListCast, returnEmptyDoclist } from '../../../fields/Doc'; +import { DocData } from '../../../fields/DocSymbols'; +import { ImageCast, NumCast } from '../../../fields/Types'; +import { ImageField } from '../../../fields/URLField'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { Docs } from '../../documents/Documents'; +import { makeUserTemplateButtonOrImage } from '../../util/DropConverter'; +import { SettingsManager } from '../../util/SettingsManager'; +import { Transform } from '../../util/Transform'; +import { undoBatch } from '../../util/UndoManager'; +import { ObservableReactComponent } from '../ObservableReactComponent'; +import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider'; +import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView'; +import { FieldView } from '../nodes/FieldView'; +import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler'; +import './StickerPalette.scss'; + +interface StickerPaletteProps { + Document: Doc; +} + +enum StickerPaletteMode { + create, + view, +} + +/** + * The StickerPalette can be toggled in the lightbox view of a document. The goal of the palette + * is to offer an easy way for users to create stickers and drag and drop them onto a document. + * These stickers can technically be of any document type and operate similarly to user templates. + * However, the palette is designed to be geared toward ink stickers and image stickers. + * + * On the "add" side of the palette, there is a way to create a drawing sticker with GPT. Users can + * enter the item to draw, toggle different settings, then GPT will generate three versions of the drawing + * to choose from. These drawings can then be saved to the palette as stickers. + */ +@observer +export class StickerPalette extends ObservableReactComponent<StickerPaletteProps> { + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(StickerPalette, fieldKey); + } + /** + * Adds a doc to the sticker palette. Gets a snapshot of the document to use as a preview in the palette. When this + * preview is dragged onto a parent document, a copy of that document is added as a sticker. + */ + public static addToPalette = async (doc: Doc) => { + if (!doc.savedAsSticker) { + const docView = DocumentView.getDocumentView(doc); + await docView?.ComponentView?.updateIcon?.(true); + const { clone } = await Doc.MakeClone(doc); + clone.title = doc.title; + const image = ImageCast(doc.icon, ImageCast(clone[Doc.LayoutFieldKey(clone)]))?.url?.href; + Doc.AddDocToList(Doc.MyStickers, 'data', makeUserTemplateButtonOrImage(clone, image)); + doc.savedAsSticker = true; + } + }; + + public static getIcon(group: Doc) { + const docView = DocumentView.getDocumentView(group); + docView?.ComponentView?.updateIcon?.(true); + return !docView ? undefined : new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000)); + } + + private _gptRes: string[] = []; + + @observable private _paletteMode = StickerPaletteMode.view; + @observable private _userInput: string = ''; + @observable private _isLoading: boolean = false; + @observable private _canInteract: boolean = true; + @observable private _showRegenerate: boolean = false; + @observable private _docView: DocumentView | null = null; + @observable private _docCarouselView: DocumentView | null = null; + @observable private _opts: DrawingOptions = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 }; + + constructor(props: StickerPaletteProps) { + super(props); + makeObservable(this); + } + + componentWillUnmount() { + this.resetPalette(true); + } + + Contains = (view: DocumentView) => + (this._docView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docView)) || // + (this._docCarouselView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docCarouselView)); + + return170 = () => 170; + + handleKeyPress = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + this.generateDrawings(); + } + }; + + setPaletteMode = action((mode: StickerPaletteMode) => { + this._paletteMode = mode; + }); + + setUserInput = action((input: string) => { + if (!this._isLoading) this._userInput = input; + }); + + setDetail = action((detail: number) => { + if (this._canInteract) this._opts.complexity = detail; + }); + + setColor = action((autoColor: boolean) => { + if (this._canInteract) this._opts.autoColor = autoColor; + }); + + setSize = action((size: number) => { + if (this._canInteract) this._opts.size = size; + }); + + resetPalette = action((changePaletteMode: boolean) => { + if (changePaletteMode) this.setPaletteMode(StickerPaletteMode.view); + this.setUserInput(''); + this.setDetail(5); + this.setColor(true); + this.setSize(200); + this._showRegenerate = false; + this._canInteract = true; + this._opts = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 }; + this._gptRes = []; + this._props.Document[DocData].data = undefined; + }); + + /** + * Calls the draw with AI functions in SmartDrawHandler to allow users to generate drawings straight from + * the sticker palette. + */ + @undoBatch + generateDrawings = action(() => { + this._isLoading = true; + const prevDrawings = DocListCast(this._props.Document[DocData].data); + this._props.Document[DocData].data = undefined; + SmartDrawHandler.Instance.AddDrawing = this.addDrawing; + this._canInteract = false; + Promise.all( + numberRange(3).map(i => { + return this._showRegenerate + ? SmartDrawHandler.Instance.regenerate(prevDrawings, this._opts, this._gptRes[i], this._userInput) + : SmartDrawHandler.Instance.drawWithGPT({ X: 0, Y: 0 }, this._userInput, this._opts.complexity || 0, this._opts.size || 0, !!this._opts.autoColor); + }) + ).then(() => { + this._opts.text !== '' ? (this._opts.text = `${this._opts.text} ~~~ ${this._userInput}`) : (this._opts.text = this._userInput); + this._userInput = ''; + this._isLoading = false; + this._showRegenerate = true; + }); + }); + + @action + addDrawing = (drawing: Doc, opts: DrawingOptions, gptRes: string) => { + this._gptRes.push(gptRes); + drawing[DocData].freeform_fitContentsToBox = true; + Doc.AddDocToList(this._props.Document, 'data', drawing); + }; + + /** + * Saves the currently showing, newly generated drawing to the sticker palette and sets the metadata. + * AddToPalette() is generically used to add any document to the palette, while this defines the behavior for when a user + * presses the "save drawing" button. + */ + saveDrawing = () => { + const cIndex = NumCast(this._props.Document.carousel_index); + const focusedDrawing = DocListCast(this._props.Document.data)[cIndex]; + const docData = focusedDrawing[DocData]; + docData.title = this._opts.text?.match(/^(.*?)~~~.*$/)?.[1] || this._opts.text; + docData.ai_drawing_input = this._opts.text; + docData.ai_drawing_complexity = this._opts.complexity; + docData.ai_drawing_colored = this._opts.autoColor; + docData.ai_drawing_size = this._opts.size; + docData.ai_drawing_data = this._gptRes[cIndex]; + docData.ai = 'gpt'; + focusedDrawing.width = this._opts.size; + docData.x = this._opts.x; + docData.y = this._opts.y; + StickerPalette.addToPalette(focusedDrawing).then(() => this.resetPalette(true)); + }; + + renderCreateInput = () => ( + <div className="palette-create"> + <input + className="palette-create-input" + aria-label="label-input" + id="new-label" + type="text" + value={this._userInput} + onChange={e => this.setUserInput(e.target.value)} + placeholder={this._showRegenerate ? '(Optional) Enter edits' : 'Enter item to draw'} + onKeyDown={this.handleKeyPress} + /> + <Button + style={{ alignSelf: 'flex-end' }} + tooltip={this._showRegenerate ? 'Regenerate' : 'Send'} + icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : this._showRegenerate ? <FontAwesomeIcon icon={'rotate'} /> : <AiOutlineSend />} + iconPlacement="right" + color={SettingsManager.userColor} + onClick={this.generateDrawings} + /> + </div> + ); + renderCreateOptions = () => ( + <div className="palette-create-options"> + <div className="palette-color"> + Color + <Switch + sx={{ + '& .MuiSwitch-switchBase.Mui-checked': { + color: SettingsManager.userColor, + }, + '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { + backgroundColor: SettingsManager.userVariantColor, + }, + }} + defaultChecked={true} + value={this._opts.autoColor} + size="small" + onChange={() => this.setColor(!this._opts.autoColor)} + /> + </div> + <div className="palette-detail"> + Detail + <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._opts.complexity} + onChange={(e, val) => typeof val === 'number' && this.setDetail(val)} + valueLabelDisplay="auto" + /> + </div> + <div className="palette-size"> + Size + <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={500} + step={10} + size="small" + value={this._opts.size} + onChange={(e, val) => typeof val === 'number' && this.setSize(val)} + valueLabelDisplay="auto" + /> + </div> + </div> + ); + renderDoc = (doc: Doc, refFunc: (r: DocumentView) => void) => { + return ( + <DocumentView + ref={refFunc} + Document={doc} + addDocument={undefined} + addDocTab={DocumentViewInternal.addDocTabFunc} + pinToPres={DocumentView.PinDoc} + containerViewPath={returnEmptyDocViewList} + styleProvider={DefaultStyleProvider} + removeDocument={returnFalse} + ScreenToLocalTransform={Transform.Identity} + PanelWidth={this.return170} + PanelHeight={this.return170} + renderDepth={1} + isContentActive={returnTrue} + focus={emptyFunction} + whenChildContentsActiveChanged={emptyFunction} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + /> + ); + }; + renderPaletteCreate = () => ( + <> + {this.renderCreateInput()} + {this.renderCreateOptions()} + {this.renderDoc(this._props.Document, (r: DocumentView) => { + this._docCarouselView = r; + })} + <div className="palette-buttons"> + <Button text="Back" tooltip="Back to All Stickers" icon={<FontAwesomeIcon icon="reply" />} color={SettingsManager.userColor} onClick={() => this.resetPalette(true)} /> + <div className="palette-save-reset"> + <Button tooltip="Save" icon={<FontAwesomeIcon icon="file-arrow-down" />} color={SettingsManager.userColor} onClick={this.saveDrawing} /> + <Button tooltip="Reset" icon={<FontAwesomeIcon icon="rotate-left" />} color={SettingsManager.userColor} onClick={() => this.resetPalette(false)} /> + </div> + </div> + </> + ); + renderPaletteView = () => ( + <> + {this.renderDoc(Doc.MyStickers, (r: DocumentView) => { + this._docView = r; + })} + <Button text="Add" icon={<FontAwesomeIcon icon="square-plus" />} color={SettingsManager.userColor} onClick={() => this.setPaletteMode(StickerPaletteMode.create)} /> + </> + ); + + render() { + return ( + <div className="sticker-palette" style={{ zIndex: 1000 }} onClick={e => e.stopPropagation()}> + {this._paletteMode === StickerPaletteMode.view ? this.renderPaletteView() : null} + {this._paletteMode === StickerPaletteMode.create ? this.renderPaletteCreate() : null} + </div> + ); + } +} + +Docs.Prototypes.TemplateMap.set(DocumentType.ANNOPALETTE, { + layout: { view: StickerPalette, dataField: 'data' }, + options: { acl: '' }, +}); |
