diff options
-rw-r--r-- | src/ClientUtils.ts | 23 | ||||
-rw-r--r-- | src/client/util/SelectionManager.ts | 3 | ||||
-rw-r--r-- | src/client/views/GlobalKeyHandler.ts | 8 | ||||
-rw-r--r-- | src/client/views/PropertiesView.tsx | 48 | ||||
-rw-r--r-- | src/client/views/TagsView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss | 15 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 236 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.scss | 1 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 48 | ||||
-rw-r--r-- | src/client/views/nodes/ImageBox.scss | 19 | ||||
-rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 113 | ||||
-rw-r--r-- | src/client/views/smartdraw/DrawingFillHandler.tsx | 17 | ||||
-rw-r--r-- | src/client/views/smartdraw/SmartDrawHandler.tsx | 8 | ||||
-rw-r--r-- | src/server/ApiManagers/UploadManager.ts | 22 |
14 files changed, 316 insertions, 247 deletions
diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts index 8f62b9060..e5dbb81db 100644 --- a/src/ClientUtils.ts +++ b/src/ClientUtils.ts @@ -144,15 +144,20 @@ export namespace ClientUtils { export async function convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename: string | undefined = undefined) { try { const posting = ClientUtils.prepend('/uploadURI'); - const returnedUri = await rp.post(posting, { - body: { - uri: imageUri, - name: returnedFilename, - nosuffix, - replaceRootFilename, - }, - json: true, - }); + const returnedUri = await rp + .post(posting, { + body: { + uri: imageUri, + name: returnedFilename, + nosuffix, + replaceRootFilename, + }, + json: true, + }) + .catch(e => { + alert('Data URI Error: ' + e.toString()); + return undefined; + }); return returnedUri; } catch (e) { console.log('ConvertDataURI :' + e); diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 1ab84421c..a1f2849cd 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -88,7 +88,8 @@ ScriptingGlobals.add(function SelectedDocType(type: string, expertMode: boolean, return DocumentView.Selected().lastElement()?._props.renderDepth === 0; } const selected = (sel => (checkContext ? DocCast(sel?.embedContainer) : sel))(DocumentView.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement()); - return selected?.type === type || selected?.type_collection === type || !type; + const matchOverlayFreeform = type === CollectionViewType.Freeform && DocumentView.Selected().lastElement()?.ComponentView?.annotationKey; + return matchOverlayFreeform || selected?.type === type || selected?.type_collection === type || !type; }); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function deselectAll() { diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index f16338361..b200aff65 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -169,10 +169,10 @@ export class KeyManager { return { stopPropagation: true, preventDefault: true }; } break; - case 'arrowleft': return this.nudge(-1,0, 'nudge left') - case 'arrowright': return this.nudge(1,0, 'nudge right'); - case 'arrowup': return this.nudge(0, -1, 'nudge up'); - case 'arrowdown': return this.nudge(0, 1, 'nudge down'); + case 'arrowleft': return (e.target as any).type !== 'text' && this.nudge(-1, 0, 'nudge left') // if target is an input box, then we don't want to nudge any Docs since we're justing moving within the text itself. + case 'arrowright': return (e.target as any).type !== 'text' && this.nudge( 1, 0, 'nudge right'); + case 'arrowup': return (e.target as any).type !== 'text' && this.nudge(0, -1, 'nudge up'); + case 'arrowdown': return (e.target as any).type !== 'text' && this.nudge(0, 1, 'nudge down'); default: } // prettier-ignore diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index e52189f56..42aa6782f 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -111,6 +111,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps @observable openContexts: boolean = true; @observable openLinks: boolean = true; @observable openAppearance: boolean = true; + @observable openFirefly: boolean = true; @observable openTransform: boolean = true; @observable openFilters: boolean = false; @observable openStyling: boolean = true; @@ -982,26 +983,10 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps 10, 1 ); - const strength = this.getNumber('Reference Strength', '', 1, 100, this.refStrength, (val: number) => { - !isNaN(val) && (this.refStrength = val); - }); return ( <div> - {!targetDoc.layout_isSvg && this.containsInkDoc && ( - <div> - <div className="drawing-to-image"> - <Toggle - text={'Create Image'} - color={SettingsManager.userColor} - icon={<FontAwesomeIcon icon="fill-drip" />} - iconPlacement="left" - align="flex-start" - fillWidth - toggleType={ToggleType.BUTTON} - onClick={undoable(() => DrawingFillHandler.drawingToImage(targetDoc, this.refStrength, StrCast(targetDoc.title) !== 'grouping' ? StrCast(targetDoc.title) : ''), 'createImage')} - /> - </div> - <div className="strength-slider">{strength}</div> + <div> + {!targetDoc.layout_isSvg && this.containsInkDoc && ( <div className="color"> <Toggle text={'Color with GPT'} @@ -1016,8 +1001,8 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps }, 'colorWithGPT')} /> </div> - </div> - )} + )} + </div> <div className="smooth"> <Toggle text={'Smooth Ink Strokes'} @@ -1113,6 +1098,8 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps this.openTransform = false; this.openFields = false; this.openSharing = false; + this.openAppearance = false; + this.openFirefly = false; this.openLayout = false; this.openFilters = false; }; @@ -1323,11 +1310,32 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps } @computed get inkSubMenu() { + const strength = this.getNumber('Reference Strength', '', 1, 100, this.refStrength, (val: number) => { + !isNaN(val) && (this.refStrength = val); + }); + const targetDoc = this.selectedLayoutDoc; return ( <> <PropertiesSection title="Appearance" isOpen={this.openAppearance} setIsOpen={bool => { this.openAppearance = bool; }} onDoubleClick={this.CloseAll}> {this.selectedStrokes.length ? this.inkEditor : null} </PropertiesSection> + <PropertiesSection title="Firefly" isOpen={this.openFirefly} setIsOpen={bool => { this.openFirefly = bool; }} onDoubleClick={this.CloseAll}> + <> + <div className="drawing-to-image"> + <Toggle + text="Create Image" + color={SettingsManager.userColor} + icon={<FontAwesomeIcon icon="fill-drip" />} + iconPlacement="left" + align="flex-start" + fillWidth + toggleType={ToggleType.BUTTON} + onClick={undoable(() => DrawingFillHandler.drawingToImage(targetDoc, this.refStrength, StrCast(targetDoc.title) !== 'grouping' ? StrCast(targetDoc.title) : ''), 'createImage')} + /> + </div> + <div className="strength-slider">{strength}</div> + </> + </PropertiesSection> <PropertiesSection title="Transform" isOpen={this.openTransform} setIsOpen={bool => { this.openTransform = bool; }} onDoubleClick={this.CloseAll}> {this.transformEditor} </PropertiesSection> diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx index 21b6a76c7..0edd89204 100644 --- a/src/client/views/TagsView.tsx +++ b/src/client/views/TagsView.tsx @@ -366,7 +366,7 @@ export class TagsView extends ObservableReactComponent<TagViewProps> { backgroundColor: this.isEditing ? Colors.LIGHT_GRAY : Colors.TRANSPARENT, borderColor: this.isEditing ? Colors.BLACK : Colors.TRANSPARENT, position: 'relative', - top: this._props.Views.length > 1 ? 25 : `calc(-${this.InsetDist} * ${1 / this.currentScale}px)`, + top: this._props.Views.lastElement()._showAIEditor ? undefined : this._props.Views.length > 1 ? 25 : `calc(-${this.InsetDist} * ${1 / this.currentScale}px)`, }}> <div className="tagsView-content" style={{ width: '100%' }}> <div className="tagsView-list"> diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index dff2cb282..82887a7a5 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -312,10 +312,14 @@ .collectionfreeformview-aiView-prompt { height: 25px; + width: 100%; } .collectionFreeFormView-aiView-strength { text-align: center; + align-items: center; + display: flex; + width: 150px; } .collectionFreeformView-aiView-options-container, @@ -323,13 +327,20 @@ text-align: start; font-weight: normal; padding: 5px; + width: 100%; + display: flex; + .collectionFreeformView-aiView-subtitle { + margin: auto; + width: 40px; + } } .collectionFreeformView-aiView-options, .collectionFreeFormView-aiView-regenerate { display: flex; flex-direction: row; - gap: 10px; - justify-content: center; align-items: center; + align-items: center; + width: 100%; + gap: 10px; } } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 112bfd178..5b4f81379 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1877,8 +1877,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection : UpdateIcon( this.layoutDoc[Id] + '_icon_' + new Date().getTime(), contentDiv, - usePanelDimensions ? this._props.PanelWidth() : NumCast(this.layoutDoc._width), - usePanelDimensions ? this._props.PanelHeight() : NumCast(this.layoutDoc._height), + usePanelDimensions || true ? this._props.PanelWidth() : NumCast(this.layoutDoc._width), + usePanelDimensions || true ? this._props.PanelHeight() : NumCast(this.layoutDoc._height), this._props.PanelWidth(), this._props.PanelHeight(), 0, @@ -2228,45 +2228,20 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; componentAIView = () => { - const showRegenerate = this.Document[DocData].ai; return ( <div className="collectionfreeformview-aiView" onPointerDown={e => e.stopPropagation()}> - Edit Collection with AI - {showRegenerate && ( - <div className="collectionfreeformview-aiView-regenerate-container"> - <text className="collectionfreeformview-aiView-subtitle">Regenerate AI Image</text> - <div className="collectionfreeformview-aiView-regenerate"> - <input - className="collectionfreeformview-aiView-input" - aria-label="Edit instructions input" - type="text" - value={this._regenInput} - onChange={action(e => this._canInteract && (this._regenInput = e.target.value))} - placeholder="Prompt (Optional)" - /> - <Button - text="Regenerate" - type={Type.SEC} - icon={this._regenLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />} - iconPlacement="right" - onClick={action(async () => { - this._regenLoading = true; - SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc; - SmartDrawHandler.Instance.AddDrawing = this.addDrawing; - SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing; - await SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true); - this._regenLoading = false; - })} - /> - </div> - </div> - )} <div className="collectionfreeformview-aiView-options-container"> - <text className="collectionfreeformview-aiView-subtitle"> Create Image with Firefly </text> + <span className="collectionfreeformview-aiView-subtitle">Firefly:</span> <div className="collectionfreeformview-aiView-options"> - <input className="collectionfreeformview-aiView-prompt" placeholder="Prompt (Optional)" type="text" value={this._drawingFillInput} onChange={action(e => this._canInteract && (this._drawingFillInput = e.target.value))} /> + <input + className="collectionfreeformview-aiView-prompt" + placeholder={this._drawingFillInput || StrCast(this.Document.title) || 'Describe image'} + type="text" + value={this._drawingFillInput} + onChange={action(e => this._canInteract && (this._drawingFillInput = e.target.value))} + /> <div className="collectionfreeformview-aiView-strength"> - Reference Strength + Similarity <Slider className="collectionfreeformview-aiView-slider" sx={{ @@ -2286,24 +2261,49 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection <Button text="Send" type={Type.SEC} - icon={this._drawingFillLoading && this._drawingFillInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />} + icon={this._drawingFillLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />} iconPlacement="right" onClick={undoable( action(() => { this._drawingFillLoading = true; - DrawingFillHandler.drawingToImage( - this.props.Document, - this._fireflyRefStrength, - this._drawingFillInput !== '' ? this._drawingFillInput : StrCast(this.props.Document.title) !== 'grouping' ? StrCast(this.props.Document.title) : '' + DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._drawingFillInput || StrCast(this.Document.title))?.then( + action(() => { + this._drawingFillLoading = false; + }) ); - this._drawingFillInput = ''; - this._drawingFillLoading = false; }), 'create image' )} /> </div> </div> + <div className="collectionfreeformview-aiView-regenerate-container"> + <span className="collectionfreeformview-aiView-subtitle">Regenerate</span> + <div className="collectionfreeformview-aiView-regenerate"> + <input + className="collectionfreeformview-aiView-input" + aria-label="Edit instructions input" + type="text" + value={this._regenInput} + onChange={action(e => this._canInteract && (this._regenInput = e.target.value))} + placeholder="..under development.." + /> + <Button + text="Regenerate" + type={Type.SEC} + icon={this._regenLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />} + iconPlacement="right" + // onClick={action(async () => { + // this._regenLoading = true; + // SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc; + // SmartDrawHandler.Instance.AddDrawing = this.addDrawing; + // SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing; + // await SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true); + // this._regenLoading = false; + // })} + /> + </div> + </div> </div> ); }; @@ -2311,83 +2311,81 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection render() { TraceMobx(); return ( - <div> - <div - className="collectionfreeformview-container" - id={this._paintedId} - ref={r => { - this.createDashEventsTarget(r); - this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); - this._oldWheel = r; - // prevent wheel events from passivly propagating up through containers - r?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); - r?.addEventListener('mouseleave', this.onMouseLeave); - r?.addEventListener('mouseenter', this.onMouseEnter); - }} - onWheel={this.onPointerWheel} - onClick={this.onClick} - onPointerDown={this.onPointerDown} - onPointerMove={this.onCursorMove} - onDrop={this.onExternalDrop} - onDragOver={e => e.preventDefault()} - onContextMenu={this.onContextMenu} - style={{ - pointerEvents: this._props.isContentActive() && SnappingManager.IsDragging ? 'all' : this._props.pointerEvents?.(), - textAlign: this.isAnnotationOverlay ? 'initial' : undefined, - transform: `scale(${this.nativeDimScaling})`, - width: `${100 / this.nativeDimScaling}%`, - height: this._props.getScrollHeight?.() ?? `${100 / this.nativeDimScaling}%`, - }}> - {Doc.ActiveTool === InkTool.Eraser && Doc.ActiveEraser === InkEraserTool.Radius && this._showEraserCircle && ( - <div - onPointerMove={this.onCursorMove} - style={{ - position: 'fixed', - left: this._eraserX, - top: this._eraserY, - width: (ActiveEraserWidth() + 5) * 2, - height: (ActiveEraserWidth() + 5) * 2, - borderRadius: '50%', - border: '1px solid gray', - transform: 'translate(-50%, -50%)', - }} + <div + className="collectionfreeformview-container" + id={this._paintedId} + ref={r => { + this.createDashEventsTarget(r); + this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); + this._oldWheel = r; + // prevent wheel events from passivly propagating up through containers + r?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); + r?.addEventListener('mouseleave', this.onMouseLeave); + r?.addEventListener('mouseenter', this.onMouseEnter); + }} + onWheel={this.onPointerWheel} + onClick={this.onClick} + onPointerDown={this.onPointerDown} + onPointerMove={this.onCursorMove} + onDrop={this.onExternalDrop} + onDragOver={e => e.preventDefault()} + onContextMenu={this.onContextMenu} + style={{ + pointerEvents: this._props.isContentActive() && SnappingManager.IsDragging ? 'all' : this._props.pointerEvents?.(), + textAlign: this.isAnnotationOverlay ? 'initial' : undefined, + transform: `scale(${this.nativeDimScaling})`, + width: `${100 / this.nativeDimScaling}%`, + height: this._props.getScrollHeight?.() ?? `${100 / this.nativeDimScaling}%`, + }}> + {Doc.ActiveTool === InkTool.Eraser && Doc.ActiveEraser === InkEraserTool.Radius && this._showEraserCircle && ( + <div + onPointerMove={this.onCursorMove} + style={{ + position: 'fixed', + left: this._eraserX, + top: this._eraserY, + width: (ActiveEraserWidth() + 5) * 2, + height: (ActiveEraserWidth() + 5) * 2, + borderRadius: '50%', + border: '1px solid gray', + transform: 'translate(-50%, -50%)', + }} + /> + )} + {this.paintFunc ? ( + <FormattedTextBox {...this.props} /> // need this so that any live dashfieldviews will update the underlying text that the code eval reads + ) : this._lightboxDoc ? ( + <div style={{ padding: 15, width: '100%', height: '100%' }}> + <DocumentView + {...this._props} + Document={this._lightboxDoc} + containerViewPath={this.DocumentView?.().docViewPath} + TemplateDataDocument={undefined} + PanelWidth={this.lightboxPanelWidth} + PanelHeight={this.lightboxPanelHeight} + NativeWidth={returnZero} + NativeHeight={returnZero} + onClickScript={this.onChildClickHandler} + onKey={this.onKeyDown} + onDoubleClickScript={this.onChildDoubleClickHandler} + childFilters={this.childDocFilters} + childFiltersByRanges={this.childDocRangeFilters} + searchFilterDocs={this.searchFilterDocs} + isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this.isContentActive} + isContentActive={this._props.childContentsActive ?? emptyFunction} + addDocTab={this.addDocTab} + ScreenToLocalTransform={this.lightboxScreenToLocal} + fitContentsToBox={undefined} + focus={this.focus} /> - )} - {this.paintFunc ? ( - <FormattedTextBox {...this.props} /> // need this so that any live dashfieldviews will update the underlying text that the code eval reads - ) : this._lightboxDoc ? ( - <div style={{ padding: 15, width: '100%', height: '100%' }}> - <DocumentView - {...this._props} - Document={this._lightboxDoc} - containerViewPath={this.DocumentView?.().docViewPath} - TemplateDataDocument={undefined} - PanelWidth={this.lightboxPanelWidth} - PanelHeight={this.lightboxPanelHeight} - NativeWidth={returnZero} - NativeHeight={returnZero} - onClickScript={this.onChildClickHandler} - onKey={this.onKeyDown} - onDoubleClickScript={this.onChildDoubleClickHandler} - childFilters={this.childDocFilters} - childFiltersByRanges={this.childDocRangeFilters} - searchFilterDocs={this.searchFilterDocs} - isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this.isContentActive} - isContentActive={this._props.childContentsActive ?? emptyFunction} - addDocTab={this.addDocTab} - ScreenToLocalTransform={this.lightboxScreenToLocal} - fitContentsToBox={undefined} - focus={this.focus} - /> - </div> - ) : ( - <> - {this._firstRender ? this.placeholder : this.marqueeView} - {this._props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />} - {!this.GroupChildDrag ? null : <div className="collectionFreeForm-groupDropper" />} - </> - )} - </div> + </div> + ) : ( + <> + {this._firstRender ? this.placeholder : this.marqueeView} + {this._props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />} + {!this.GroupChildDrag ? null : <div className="collectionFreeForm-groupDropper" />} + </> + )} </div> ); } diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index a3d47290a..8e7307d23 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -280,7 +280,6 @@ .documentView-editorView { width: 100%; - overflow-y: scroll; justify-items: center; background-color: rgb(223, 223, 223); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 640d27aa1..e37658ca5 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -85,7 +85,7 @@ export interface DocumentViewProps extends FieldViewSharedProps { reactParent?: React.Component; // parent React component view (see CollectionFreeFormDocumentView) } @observer -export class DocumentViewInternal extends DocComponent<FieldViewProps & DocumentViewProps>() { +export class DocumentViewInternal extends DocComponent<FieldViewProps & DocumentViewProps & { showAIEditor: boolean }>() { // this makes mobx trace() statements more descriptive public get displayName() { return 'DocumentViewInternal(' + this.Document.title + ')'; } // prettier-ignore public static SelectAfterContextMenu = true; // whether a document should be selected after it's contextmenu is triggered. @@ -109,7 +109,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document private _mainCont = React.createRef<HTMLDivElement>(); private _titleRef = React.createRef<EditableView>(); private _dropDisposer?: DragManager.DragDropDisposer; - constructor(props: FieldViewProps & DocumentViewProps) { + constructor(props: FieldViewProps & DocumentViewProps & { showAIEditor: boolean }) { super(props); makeObservable(this); } @@ -122,12 +122,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document @observable _componentView: Opt<ViewBoxInterface<FieldViewProps>> = undefined; // needs to be accessed from DocumentView wrapper class @observable _animateScaleTime: Opt<number> = undefined; // milliseconds for animating between views. defaults to 300 if not uset @observable _animateScalingTo = 0; - @observable public _showAIEditor: boolean = false; - - @action - showAIEditor() { - this._showAIEditor = !this._showAIEditor; - } get _contentDiv() { return this._mainCont.current; } // prettier-ignore get _docView() { return this._props.DocumentView?.(); } // prettier-ignore @@ -689,7 +683,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document rootSelected = () => this._rootSelected; panelHeight = () => this._props.PanelHeight() - this.headerMargin; - screenToLocalContent = () => this._props.ScreenToLocalTransform().translate(0, -this.headerMargin); + screenToLocalContent = () => + this._props + .ScreenToLocalTransform() + .translate(0, -this.headerMargin) + .scale(this._props.showAIEditor ? (this._props.PanelHeight() || 1) / this.rph() : 1); onClickFunc = this.disableClickScriptFunc ? undefined : () => this.onClickHdlr; setHeight = (height: number) => { !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height)); } // prettier-ignore setContentView = action((view: ViewBoxInterface<FieldViewProps>) => { this._componentView = view; }); // prettier-ignore @@ -715,9 +713,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document return this._props.styleProvider?.(doc, props, property); }; - @observable _aiWinHeight = 100; - rpw = () => this._props.PanelWidth() - this._aiWinHeight; - rph = () => this.panelHeight() - this._aiWinHeight; + @observable _aiWinHeight = 95; + rpw = () => (this.rph() * (this._props.NativeWidth?.() || 1)) / (this._props.NativeHeight?.() || 1); + rph = () => Math.max(10, this._props.PanelHeight() - this._aiWinHeight); @computed get viewBoxContents() { TraceMobx(); const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString; @@ -728,8 +726,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document className="documentView-contentsView" style={{ pointerEvents: (isInk || noBackground ? 'none' : this.contentPointerEvents()) ?? (this._mounted ? 'all' : 'none'), - width: this._showAIEditor ? this.rpw() : undefined, - height: this._showAIEditor ? this.rph() : this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined, + width: this._props.showAIEditor ? this.rpw() : undefined, + height: this._props.showAIEditor ? this.rph() : this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined, }}> <DocumentContentsView {...this._props} @@ -737,8 +735,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document pointerEvents={this.contentPointerEvents} setContentViewBox={this.setContentView} childFilters={this.childFilters} - PanelHeight={this._showAIEditor ? this.rpw : this.panelHeight} - PanelWidth={this._showAIEditor ? this.rph : this._props.PanelWidth} + PanelWidth={this._props.showAIEditor ? this.rpw : this._props.PanelWidth} + PanelHeight={this._props.showAIEditor ? this.rph : this.panelHeight} setHeight={this.setHeight} isContentActive={this.isContentActive} ScreenToLocalTransform={this.screenToLocalContent} @@ -748,7 +746,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)} /> </div> - {!this._showAIEditor ? null : ( + {!this._props.showAIEditor ? null : ( <> <div className="documentView-editorView-history" @@ -1168,10 +1166,13 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { @computed private get nativeScaling() { if (this.shouldNotScale) return 1; const minTextScale = this.Document.type === DocumentType.RTF ? 0.1 : 0; - if (this.layout_fitWidth || this._props.PanelHeight() / (this.effectiveNativeHeight || 1) > this._props.PanelWidth() / (this.effectiveNativeWidth || 1)) { - return Math.max(minTextScale, this._props.PanelWidth() / (this.effectiveNativeWidth || 1)); // width-limited or layout_fitWidth + const ai = this._showAIEditor && this.nativeWidth === this.layoutDoc.width ? 95 : 0; + const effNW = Math.max(this.effectiveNativeWidth - ai, 1); + const effNH = Math.max(this.effectiveNativeHeight - ai, 1); + if (this.layout_fitWidth || (this._props.PanelHeight() - ai) / effNH > (this._props.PanelWidth() - ai) / effNW) { + return Math.max(minTextScale, (this._props.PanelWidth() - ai) / effNW); // width-limited or layout_fitWidth } - return Math.max(minTextScale, this._props.PanelHeight() / (this.effectiveNativeHeight || 1)); // height-limited or unscaled + return Math.max(minTextScale, (this._props.PanelHeight() - ai) / effNH); // height-limited or unscaled } @computed private get panelWidth() { return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this._props.PanelWidth(); @@ -1231,7 +1232,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { } @computed get layout_fitWidth() { - return this._props.fitWidth?.(this.layoutDoc) ?? this.layoutDoc?.layout_fitWidth; + return this._showAIEditor ? false : (this._props.fitWidth?.(this.layoutDoc) ?? this.layoutDoc?.layout_fitWidth); } @computed get anchorViewDoc() { return this._props.LayoutTemplateString?.includes('link_anchor_2') ? DocCast(this.Document.link_anchor_2) : this._props.LayoutTemplateString?.includes('link_anchor_1') ? DocCast(this.Document.link_anchor_1) : undefined; @@ -1327,9 +1328,11 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { } }; + @observable public _showAIEditor: boolean = false; + @action public toggleAIEditor = () => { - this._docViewInternal && this._docViewInternal.showAIEditor(); + this._showAIEditor = !this._showAIEditor; }; public setTextHtmlOverlay = action((text: string | undefined, effect?: Doc) => { @@ -1516,6 +1519,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { }}> <DocumentViewInternal {...this._props} + showAIEditor={this._showAIEditor} reactParent={undefined} isHovering={this.isHovering} fieldKey={this.LayoutFieldKey} diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 1b6b3c85a..a50320f5e 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -40,6 +40,7 @@ max-height: 100%; pointer-events: inherit; background: transparent; + z-index: -10000; img { height: auto; @@ -159,6 +160,7 @@ .imageBox-aiView { text-align: center; font-weight: bold; + width: 100%; .imageBox-aiView-subtitle { position: relative; @@ -168,20 +170,29 @@ .imageBox-aiView-regenerate-container, .imageBox-aiView-options-container { font-weight: normal; - margin: 5px; display: flex; } .imageBox-aiView-regenerate, .imageBox-aiView-options { display: flex; - flex-direction: row; - justify-content: center; + align-items: center; flex-direction: row; gap: 5px; + width: 100%; } + .imageBox-aiView-strength { + text-align: center; + align-items: center; + display: flex; + width: 125px; + } + .imageBox-aiView-slider { + width: 50px; + margin-left: 5px; + } .imageBox-aiView-input { - width: 50%; + width: 100%; } } diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 9838af4d0..9223fc180 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@mui/material'; +import { Slider, Tooltip } from '@mui/material'; import axios from 'axios'; import { Colors, Button, Type, Size } from '@dash/components'; import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx'; @@ -44,6 +44,7 @@ import { SmartDrawHandler } from '../smartdraw/SmartDrawHandler'; import { SettingsManager } from '../../util/SettingsManager'; import { AiOutlineSend } from 'react-icons/ai'; import { FireflyImageData } from '../smartdraw/FireflyConstants'; +import { DrawingFillHandler } from '../smartdraw/DrawingFillHandler'; export class ImageEditorData { // eslint-disable-next-line no-use-before-define @@ -98,7 +99,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @observable private _regenInput = ''; @observable private _canInteract = true; @observable private _regenerateLoading = false; - @observable private _prevImgs: FireflyImageData[] = []; + @observable private _prevImgs: FireflyImageData[] = StrCast(this.Document.ai_firefly_history) ? JSON.parse(StrCast(this.Document.ai_firefly_history)) : []; constructor(props: FieldViewProps) { super(props); @@ -386,8 +387,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { : UpdateIcon( this.layoutDoc[Id] + '_icon_' + new Date().getTime(), contentDiv, - usePanelDimensions ? this._props.PanelWidth() : NumCast(this.layoutDoc._width), - usePanelDimensions ? this._props.PanelHeight() : NumCast(this.layoutDoc._height), + usePanelDimensions || true ? this._props.PanelWidth() : NumCast(this.layoutDoc._width), + usePanelDimensions || true ? this._props.PanelHeight() : NumCast(this.layoutDoc._height), this._props.PanelWidth(), this._props.PanelHeight(), 0, @@ -543,65 +544,91 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { <div key={img.pathname}> <img className="imageBox-aiView-img" - src={img.href} + src={ClientUtils.prepend(img.pathname.replace(extname(img.pathname), '_s' + extname(img.pathname)))} onClick={() => { this.dataDoc[this.fieldKey] = new ImageField(img.pathname); this.dataDoc.ai_firefly_prompt = img.prompt; this.dataDoc.ai_firefly_seed = img.seed; }} /> - <text>{img.prompt}</text> + <span>{img.prompt}</span> </div> ))} </div> ); }; + @observable private _fireflyRefStrength = 0; componentAIView = () => { const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey])); - const showRegenerate = this.Document[DocData].ai; return ( <div className="imageBox-aiView"> - {showRegenerate && ( - <div className="imageBox-aiView-regenerate-container"> - <div className="imageBox-aiView-regenerate"> - <input - className="imageBox-aiView-input" - aria-label="Edit instructions input" - type="text" - value={this._regenInput} - onChange={action(e => this._canInteract && (this._regenInput = e.target.value))} - placeholder="Prompt (Optional)" - /> - <Button - text="Regenerate Image" - type={Type.SEC} - // style={{ alignSelf: 'flex-end' }} - icon={this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />} - iconPlacement="right" - onClick={action(async () => { - this._regenerateLoading = true; - SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true).then(newImgs => { - if (newImgs[0]) { - const url = newImgs[0].pathname; - const imgField = new ImageField(url); - this._prevImgs.length === 0 && - this._prevImgs.push({ prompt: StrCast(this.dataDoc.ai_firefly_prompt), seed: this.dataDoc.ai_firefly_seed as number, href: this.paths.lastElement(), pathname: field.url.pathname }); - this._prevImgs.unshift({ prompt: newImgs[0].prompt, seed: newImgs[0].seed, href: this.paths.lastElement(), pathname: url }); - this.dataDoc.ai_firefly_history = `${this._prevImgs}`; - this.dataDoc[this.fieldKey] = imgField; - this._regenerateLoading = false; - this._regenInput = ''; - } - }); - })} + <div className="imageBox-aiView-regenerate-container"> + <div className="imageBox-aiView-regenerate"> + Firefly: + <input + className="imageBox-aiView-input" + aria-label="Edit instructions input" + type="text" + value={this._regenInput} + onChange={action(e => this._canInteract && (this._regenInput = e.target.value))} + placeholder={this._regenInput || StrCast(this.Document.title)} + /> + <div className="imageBox-aiView-strength"> + <span style={{ width: 60 }}>Similarity</span> + <Slider + className="imageBox-aiView-slider" + sx={{ + '& .MuiSlider-track': { color: SettingsManager.userVariantColor }, + '& .MuiSlider-rail': { color: SettingsManager.userBackgroundColor }, + '& .MuiSlider-thumb': { color: SettingsManager.userVariantColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } }, + }} + min={0} + max={100} + step={1} + size="small" + value={this._fireflyRefStrength} + onChange={action((e, val) => this._canInteract && (this._fireflyRefStrength = val as number))} + valueLabelDisplay="auto" /> - <Button text="Get Variations" type={Type.SEC} iconPlacement="right" /> </div> + <Button + text="Create" + type={Type.SEC} + // style={{ alignSelf: 'flex-end' }} + icon={this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />} + iconPlacement="right" + onClick={action(async () => { + this._regenerateLoading = true; + if (this._fireflyRefStrength) { + DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._regenInput || StrCast(this.Document.title))?.then( + action(() => { + this._regenerateLoading = false; + }) + ); + } else + SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput || StrCast(this.Document.title), true).then( + action(newImgs => { + if (newImgs[0]) { + const url = newImgs[0].pathname; + const imgField = new ImageField(url); + this._prevImgs.length === 0 && + this._prevImgs.push({ prompt: StrCast(this.dataDoc.ai_firefly_prompt), seed: this.dataDoc.ai_firefly_seed as number, href: this.paths.lastElement(), pathname: field.url.pathname }); + this._prevImgs.unshift({ prompt: newImgs[0].prompt, seed: newImgs[0].seed, pathname: url }); + this.dataDoc.ai_firefly_history = JSON.stringify(this._prevImgs); + this.dataDoc.ai_firefly_prompt = newImgs[0].prompt; + this.dataDoc[this.fieldKey] = imgField; + this._regenerateLoading = false; + this._regenInput = ''; + } + }) + ); + })} + /> </div> - )} + </div> <div className="imageBox-aiView-options-container"> - {showRegenerate && <text className="imageBox-aiView-subtitle"> More Options: </text>} + <span className="imageBox-aiView-subtitle"> More Options: </span> <div className="imageBox-aiView-options"> <Button type={Type.TERT} diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx index 423e3b2ac..d9a01c436 100644 --- a/src/client/views/smartdraw/DrawingFillHandler.tsx +++ b/src/client/views/smartdraw/DrawingFillHandler.tsx @@ -15,7 +15,7 @@ export class DrawingFillHandler { const docData = drawing[DocData]; const tags: string[] = ((docData?.tags as unknown as string[]) ?? []).map(tag => tag.slice(1)) ?? []; const styles = tags.filter(tag => FireflyStylePresets.has(tag)); - DocumentView.GetDocImage(drawing)?.then(imageField => { + return DocumentView.GetDocImage(drawing)?.then(imageField => { if (imageField) { const aspectRatio = (drawing.width as number) / (drawing.height as number); let dims: { width: number; height: number }; @@ -31,18 +31,17 @@ export class DrawingFillHandler { const { href } = ImageCast(imageField).url; const hrefParts = href.split('.'); const structureUrl = `${hrefParts.slice(0, -1).join('.')}_o.${hrefParts.lastElement()}`; - imageUrlToBase64(structureUrl) - .then((hrefBase64: string) => gptDescribeImage(hrefBase64)) - .then(prompt => { + return imageUrlToBase64(structureUrl) + .then(gptDescribeImage) + .then(prompt => Networking.PostToServer('/queryFireflyImageFromStructure', - { prompt: `${user_prompt}, ${prompt}`, width: dims.width, height: dims.height, structureUrl, strength, styles }) + { prompt: `${user_prompt || prompt}`, width: dims.width, height: dims.height, structureUrl, strength, styles }) .then((info: Upload.ImageInformation) => DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, - { ai: 'firefly', ai_firefly_prompt: user_prompt || prompt, _width: 500, data_nativeWidth: info.nativeWidth, data_nativeHeight: info.nativeHeight }), OpenWhere.addRight) - ).catch(e => alert("create image failed: " + e.toString())); // prettier-ignore - }); + { ai: 'firefly', title: user_prompt || prompt, ai_firefly_prompt: user_prompt || prompt, _width: 500, data_nativeWidth: info.nativeWidth, data_nativeHeight: info.nativeHeight }), OpenWhere.addRight) + ).catch(e => alert("create image failed: " + e.toString())) + ); // prettier-ignore } - return false; }); }; } diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx index f635b5642..7db9ef133 100644 --- a/src/client/views/smartdraw/SmartDrawHandler.tsx +++ b/src/client/views/smartdraw/SmartDrawHandler.tsx @@ -271,16 +271,16 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { createImageWithFirefly = (input: string, seed?: number, changeInPlace?: boolean): Promise<FireflyImageData> => { this._lastInput.text = input; const dims = FireflyDimensionsMap[this._imgDims]; - return Networking.PostToServer('/queryFireflyImage', { prompt: input, width: dims.width, height: dims.height, seed: seed }) + return Networking.PostToServer('/queryFireflyImage', { prompt: input, width: dims.width, height: dims.height, seed }) .then(img => { - const seed = img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1]; + 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: seed, + ai_firefly_seed: newseed, ai_firefly_prompt: input, }); DocumentViewInternal.addDocTabFunc(imgDoc, OpenWhere.addRight); @@ -306,7 +306,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { case DocumentType.IMG: if (this._regenInput) { // if (this._selectedDoc) { - const newPrompt = `${doc.ai_firefly_prompt} ~~~ ${this._regenInput}`; + const newPrompt = doc.ai_firefly_prompt ? `${doc.ai_firefly_prompt} ~~~ ${this._regenInput}` : this._regenInput; return this.createImageWithFirefly(newPrompt, NumCast(doc?.ai_firefly_seed), changeInPlace); // } } diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts index 5a880901b..c9d5df547 100644 --- a/src/server/ApiManagers/UploadManager.ts +++ b/src/server/ApiManagers/UploadManager.ts @@ -274,14 +274,20 @@ export default class UploadManager extends ApiManager { .filter(f => regex.test(f)) .map(f => fs.unlinkSync(serverPath + f)); } - imageDataUri.outputFile(uri, serverPathToFile(Directory.images, InjectSize(filename, origSuffix))).then((savedName: string) => { - const ext = path.extname(savedName).toLowerCase(); - const outputPath = serverPathToFile(Directory.images, filename + ext); - if (AcceptableMedia.imageFormats.includes(ext)) { - workerResample(savedName, outputPath, origSuffix, false); - } - res.send(clientPathToFile(Directory.images, filename + ext)); - }); + imageDataUri + .outputFile(uri, serverPathToFile(Directory.images, InjectSize(filename, origSuffix))) + .then((savedName: string) => { + const ext = path.extname(savedName).toLowerCase(); + const outputPath = serverPathToFile(Directory.images, filename + ext); + if (AcceptableMedia.imageFormats.includes(ext)) { + workerResample(savedName, outputPath, origSuffix, false); + } + res.send(clientPathToFile(Directory.images, filename + ext)); + }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .catch((e: any) => { + res.status(404).json({ error: e.toString() }); + }); }, }); } |