aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/smartdraw/AnnotationPalette.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/smartdraw/AnnotationPalette.tsx')
-rw-r--r--src/client/views/smartdraw/AnnotationPalette.tsx363
1 files changed, 363 insertions, 0 deletions
diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx
new file mode 100644
index 000000000..0c8dbf12d
--- /dev/null
+++ b/src/client/views/smartdraw/AnnotationPalette.tsx
@@ -0,0 +1,363 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Slider, Switch } from '@mui/material';
+import { Button } from 'browndash-components';
+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 } from '../../../Utils';
+import { Doc, DocListCast, returnEmptyDoclist } from '../../../fields/Doc';
+import { DocData } from '../../../fields/DocSymbols';
+import { ImageCast } from '../../../fields/Types';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { Docs } from '../../documents/Documents';
+import { makeUserTemplateImage } 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 './AnnotationPalette.scss';
+import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler';
+import { ImageField } from '../../../fields/URLField';
+import { Copy } from '../../../fields/FieldSymbols';
+
+interface AnnotationPaletteProps {
+ Document: Doc;
+}
+
+/**
+ * The AnnotationPalette can be toggled in the lightbox view of a document. The goal of the palette
+ * is to offer an easy way for users to save then drag and drop repeated annotations onto a document.
+ * These annotations can be of any annotation type and operate similarly to user templates.
+ *
+ * On the "add" side of the palette, there is a way to create a drawing annotation 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 annotations.
+ */
+@observer
+export class AnnotationPalette extends ObservableReactComponent<AnnotationPaletteProps> {
+ @observable private _paletteMode: 'create' | 'view' = '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 };
+ private _gptRes: string[] = [];
+
+ constructor(props: AnnotationPaletteProps) {
+ super(props);
+ makeObservable(this);
+ }
+
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(AnnotationPalette, fieldKey);
+ }
+
+ Contains = (view: DocumentView) => {
+ return (this._docView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docView)) || (this._docCarouselView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docCarouselView));
+ };
+
+ return170 = () => 170;
+
+ @action
+ handleKeyPress = async (event: React.KeyboardEvent) => {
+ if (event.key === 'Enter') {
+ await this.generateDrawings();
+ }
+ };
+
+ @action
+ setPaletteMode = (mode: 'create' | 'view') => {
+ this._paletteMode = mode;
+ };
+
+ @action
+ setUserInput = (input: string) => {
+ if (!this._isLoading) this._userInput = input;
+ };
+
+ @action
+ setDetail = (detail: number) => {
+ if (this._canInteract) this._opts.complexity = detail;
+ };
+
+ @action
+ setColor = (autoColor: boolean) => {
+ if (this._canInteract) this._opts.autoColor = autoColor;
+ };
+
+ @action
+ setSize = (size: number) => {
+ if (this._canInteract) this._opts.size = size;
+ };
+
+ @action
+ resetPalette = (changePaletteMode: boolean) => {
+ if (changePaletteMode) this.setPaletteMode('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;
+ };
+
+ /**
+ * Adds a doc to the annotation 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 an annotation.
+ */
+ public static addToPalette = async (doc: Doc) => {
+ if (!doc.savedAsAnno) {
+ 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.MyAnnos, 'data', makeUserTemplateImage(clone, image));
+ doc.savedAsAnno = true;
+ }
+ };
+
+ public static getIcon(group: Doc) {
+ const docView = DocumentView.getDocumentView(group);
+ if (docView) {
+ docView.ComponentView?.updateIcon?.(true);
+ return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000));
+ }
+ return undefined;
+ }
+
+ /**
+ * Calls the draw with GPT functions in SmartDrawHandler to allow users to generate drawings straight from
+ * the annotation palette.
+ */
+ @undoBatch
+ generateDrawings = action(async () => {
+ this._isLoading = true;
+ this._props.Document[DocData].data = undefined;
+ for (let i = 0; i < 3; i++) {
+ try {
+ SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
+ this._canInteract = false;
+ if (this._showRegenerate) {
+ await SmartDrawHandler.Instance.regenerate(this._opts, this._gptRes[i], this._userInput);
+ } else {
+ await SmartDrawHandler.Instance.drawWithGPT({ X: 0, Y: 0 }, this._userInput, this._opts.complexity, this._opts.size, this._opts.autoColor);
+ }
+ } catch (e) {
+ console.log('Error generating drawing', e);
+ }
+ }
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
+ 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 annotation 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 = async () => {
+ const cIndex: number = this._props.Document.carousel_index as number;
+ const focusedDrawing = DocListCast(this._props.Document.data)[cIndex];
+ const docData = focusedDrawing[DocData];
+ docData.title = this._opts.text.match(/^(.*?)~~~.*$/)?.[1] || this._opts.text;
+ docData.drawingInput = this._opts.text;
+ docData.drawingComplexity = this._opts.complexity;
+ docData.drawingColored = this._opts.autoColor;
+ docData.drawingSize = this._opts.size;
+ docData.drawingData = this._gptRes[cIndex];
+ docData.width = this._opts.size;
+ docData.x = this._opts.x;
+ docData.y = this._opts.y;
+ await AnnotationPalette.addToPalette(focusedDrawing);
+ this.resetPalette(true);
+ };
+
+ render() {
+ return (
+ <div className="annotation-palette" style={{ zIndex: 1000 }} onClick={e => e.stopPropagation()}>
+ {this._paletteMode === 'view' && (
+ <>
+ <DocumentView
+ ref={r => (this._docView = r)}
+ Document={Doc.MyAnnos}
+ addDocument={undefined}
+ addDocTab={DocumentViewInternal.addDocTabFunc}
+ pinToPres={DocumentView.PinDoc}
+ containerViewPath={returnEmptyDocViewList}
+ styleProvider={DefaultStyleProvider}
+ removeDocument={returnFalse}
+ ScreenToLocalTransform={Transform.Identity}
+ PanelWidth={this.return170}
+ PanelHeight={this.return170}
+ renderDepth={0}
+ isContentActive={returnTrue}
+ focus={emptyFunction}
+ whenChildContentsActiveChanged={emptyFunction}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ />
+ <Button text="Add" icon={<FontAwesomeIcon icon="square-plus" />} color={SettingsManager.userColor} onClick={() => this.setPaletteMode('create')} />
+ </>
+ )}
+ {this._paletteMode === 'create' && (
+ <>
+ <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>
+ <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) => {
+ this.setDetail(val as number);
+ }}
+ 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) => {
+ this.setSize(val as number);
+ }}
+ valueLabelDisplay="auto"
+ />
+ </div>
+ </div>
+ <DocumentView
+ ref={r => (this._docCarouselView = r)}
+ Document={this._props.Document}
+ 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}
+ />
+ <div className="palette-buttons">
+ <Button text="Back" tooltip="Back to All Annotations" 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>
+ </>
+ )}
+ </div>
+ );
+ }
+}
+
+Docs.Prototypes.TemplateMap.set(DocumentType.ANNOPALETTE, {
+ layout: { view: AnnotationPalette, dataField: 'data' },
+ options: { acl: '' },
+});