diff options
Diffstat (limited to 'src/client/views/nodes/ImageBox.tsx')
-rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 147 |
1 files changed, 100 insertions, 47 deletions
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index f7ad5c7e2..78bacdcac 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -8,7 +8,7 @@ import { extname } from 'path'; import * as React from 'react'; import { AiOutlineSend } from 'react-icons/ai'; import ReactLoading from 'react-loading'; -import { ClientUtils, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils'; +import { ClientUtils, imageUrlToBase64, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon, returnTrue } from '../../../ClientUtils'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; @@ -16,7 +16,7 @@ import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; import { ComputedField } from '../../../fields/ScriptField'; -import { Cast, DocCast, ImageCast, NumCast, RTFCast, StrCast } from '../../../fields/Types'; +import { Cast, DocCast, ImageCast, NumCast, RTFCast, StrCast, ImageCastWithSuffix } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction } from '../../../Utils'; @@ -45,6 +45,7 @@ import { FieldView, FieldViewProps } from './FieldView'; import { FocusViewOptions } from './FocusViewOptions'; import './ImageBox.scss'; import { OpenWhere } from './OpenWhere'; +import { gptImageLabel } from '../../apis/gpt/GPT'; const DefaultPath = '/assets/unknown-file-icon-hi.png'; export class ImageEditorData { @@ -71,7 +72,7 @@ export class ImageEditorData { public static set AddDoc(addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean>) { ImageEditorData.set(this.imageData.open, this.imageData.rootDoc, this.imageData.source, addDoc); } // prettier-ignore } -const API_URL = 'https://api.unsplash.com/search/photos'; +const UNSPLASH_API = 'https://api.unsplash.com/search/photos'; @observer export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { @@ -112,11 +113,69 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this._props.setContentViewBox?.(this); } + @computed get outpaintOriginalSize(): { width: number; height: number } { + return { + width: NumCast(this.Document[this.fieldKey + '_outpaintOriginalWidth']), + height: NumCast(this.Document[this.fieldKey + '_outpaintOriginalHeight']), + }; + } + set outpaintOriginalSize(prop: { width: number; height: number } | undefined) { + this.Document[this.fieldKey + '_outpaintOriginalWidth'] = prop?.width; + this.Document[this.fieldKey + '_outpaintOriginalHeight'] = prop?.height; + } + + @computed get imgNativeSize() { + return { + nativeWidth: NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth'], 500)), + nativeHeight: NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '_nativeHeight'], 500)), + }; + } + set imgNativeSize(prop: { nativeWidth: number; nativeHeight: number }) { + this.dataDoc[this.fieldKey + '_nativeWidth'] = prop.nativeWidth; + this.dataDoc[this.fieldKey + '_nativeHeight'] = prop.nativeHeight; + } + protected createDropTarget = (ele: HTMLDivElement) => { this._mainCont = ele; this._dropDisposer?.(); ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.Document)); }; + + autoTag = async () => { + if (this.Document.$tags_chat) return; + try { + // 1) grab the full-size URL + const layoutKey = Doc.LayoutDataKey(this.Document); + const url = ImageCastWithSuffix(this.Document[layoutKey], '_o'); + if (!url) throw new Error('No image URL found'); + + // 2) convert to base64 + const base64 = await imageUrlToBase64(url); + if (!base64) throw new Error('Failed to load image data'); + + // 3) ask GPT for labels one label: PERSON or LANDSCAPE + const label = await gptImageLabel( + base64, + `Classify this image as PERSON or LANDSCAPE. You may only respond with one of these two options. + Then provide five additional descriptive tags to describe the image for a total of 6 words outputted, delimited by spaces. + For example: "LANDSCAPE BUNNY NATURE FOREST PEACEFUL OUTDOORS". + Then add one final lengthier summary tag (separated by underscores) that describes the image.` + ).then(raw => raw.trim().toUpperCase()); + + const { nativeWidth, nativeHeight } = this.nativeSize; + const aspectRatio = ((nativeWidth || 1) / (nativeHeight || 1)).toFixed(2); + + // 5) stash it on the Doc + // overwrite any old tags so re-runs still work + this.Document.$tags_chat = new List<string>([...label.split(/\s+/), `ASPECT_${aspectRatio}`]); + + // 6) flip on “show tags” in the layout + this.Document._layout_showTags = true; + } catch (err) { + console.error('autoTag failed:', err); + } + }; + getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { const visibleAnchor = this._getAnchor?.(this._savedAnnotations, true); // use marquee anchor, otherwise, save zoom/pan as anchor const anchor = @@ -153,7 +212,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width), height: this.layoutDoc._height }), ({ nativeSize, width, height }) => { if (!this.layoutDoc._layout_nativeDimEditable || !height || this.layoutDoc.layout_resetNativeDim) { - this.layoutDoc.layout_resetNativeDim = undefined; // template images need to reset their dimensions when they are rendered with content. afterwards, remove this flag. + if (!this._props.TemplateDataDocument) this.layoutDoc._nativeWidth = this.layoutDoc._nativeHeight = undefined; + this.layoutDoc.layout_resetNativeDim = undefined; // reset dimensions of templates rendered with content or if image changes. afterwards, remove this flag. this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth; } }, @@ -170,7 +230,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { { fireImmediately: true } ); this._disposers.outpaint = reaction( - () => this.Document[this.fieldKey + '_outpaintOriginalWidth'] !== undefined && !SnappingManager.ShiftKey, + () => this.outpaintOriginalSize?.width && !SnappingManager.ShiftKey, complete => complete && this.openOutpaintPrompt(), { fireImmediately: true } ); @@ -185,7 +245,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { */ fetchImages = async () => { try { - const { data } = await axios.get(`${API_URL}?query=${this._searchInput}&page=1&per_page=${1}&client_id=${process.env.VITE_API_KEY}`); + const { data } = await axios.get(`${UNSPLASH_API}?query=${this._searchInput}&page=1&per_page=${1}&client_id=${process.env.VITE_API_KEY}`); const imageSnapshot = Docs.Create.ImageDocument(data.results[0].urls.small, { _nativeWidth: Doc.NativeWidth(this.layoutDoc), _nativeHeight: Doc.NativeHeight(this.layoutDoc), @@ -202,10 +262,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } }; - handleSelection = async (selection: string) => { - this._searchInput = selection; - }; - drop = undoable( action((e: Event, de: DragManager.DropEvent) => { if (de.complete.docDragData && !this._props.rejectDrop?.(de, this.DocumentView?.())) { @@ -231,14 +287,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { DrawingFillHandler.drawingToImage(this.Document, 90, newPrompt(descText), drag)?.then(action(() => (this._regenerateLoading = false))); added = false; } else if (de.altKey || !this.dataDoc[this.fieldKey]) { - const layoutDoc = de.complete.docDragData?.draggedDocuments[0]; - const targetField = Doc.LayoutDataKey(layoutDoc); - const targetDoc = layoutDoc[DocData]; - if (targetDoc[targetField] instanceof ImageField) { + const dropDoc = de.complete.docDragData?.draggedDocuments[0]; + const dropDocFieldKey = Doc.LayoutDataKey(dropDoc); + const dropDataDoc = dropDoc[DocData]; + if (dropDataDoc[dropDocFieldKey] instanceof ImageField) { added = true; - this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField); - Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(targetDoc), this.fieldKey); - Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(targetDoc), this.fieldKey); + this.dataDoc.layout_resetNativeDim = true; + this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(dropDataDoc[dropDocFieldKey] as ImageField); + Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(dropDataDoc), this.fieldKey); + Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(dropDataDoc), this.fieldKey); } } added === false && e.preventDefault(); @@ -257,18 +314,17 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @undoBatch setNativeSize = action(() => { - const oldnativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']); + const oldnativeWidth = this.imgNativeSize.nativeWidth; const nscale = NumCast(this._props.PanelWidth()) * NumCast(this.layoutDoc._freeform_scale, 1); const nw = nscale / oldnativeWidth; - this.dataDoc[this.fieldKey + '_nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) * nw; - this.dataDoc[this.fieldKey + '_nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) * nw; + this.imgNativeSize = { nativeWidth: this.imgNativeSize.nativeWidth * nw, nativeHeight: this.imgNativeSize.nativeHeight * nw }; this.dataDoc.freeform_panX = nw * NumCast(this.dataDoc.freeform_panX); this.dataDoc.freeform_panY = nw * NumCast(this.dataDoc.freeform_panY); this.dataDoc.freeform_panX_max = this.dataDoc.freeform_panX_max ? nw * NumCast(this.dataDoc.freeform_panX_max) : undefined; this.dataDoc.freeform_panX_min = this.dataDoc.freeform_panX_min ? nw * NumCast(this.dataDoc.freeform_panX_min) : undefined; this.dataDoc.freeform_panY_max = this.dataDoc.freeform_panY_max ? nw * NumCast(this.dataDoc.freeform_panY_max) : undefined; this.dataDoc.freeform_panY_min = this.dataDoc.freeform_panY_min ? nw * NumCast(this.dataDoc.freeform_panY_min) : undefined; - const newnativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']); + const newnativeWidth = this.imgNativeSize.nativeWidth; DocListCast(this.dataDoc[this.annotationKey]).forEach(doc => { doc.x = (NumCast(doc.x) / oldnativeWidth) * newnativeWidth; doc.y = (NumCast(doc.y) / oldnativeWidth) * newnativeWidth; @@ -280,13 +336,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }); @undoBatch rotate = action(() => { - const nw = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']); - const nh = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']); + const nativeSize = this.imgNativeSize; const w = this.layoutDoc._width; const h = this.layoutDoc._height; this.dataDoc[this.fieldKey + '_rotation'] = (NumCast(this.dataDoc[this.fieldKey + '_rotation']) + 90) % 360; - this.dataDoc[this.fieldKey + '_nativeWidth'] = nh; - this.dataDoc[this.fieldKey + '_nativeHeight'] = nw; + this.imgNativeSize = { nativeWidth: nativeSize.nativeHeight, nativeHeight: nativeSize.nativeWidth }; // swap width and height this.layoutDoc._width = h; this.layoutDoc._height = w; }); @@ -302,7 +356,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const anchy = NumCast(cropping.y); const anchw = NumCast(cropping._width); const anchh = NumCast(cropping._height); - const viewScale = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) / anchw; + const viewScale = this.nativeSize.nativeWidth / anchw; cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width); cropping.y = NumCast(this.Document.y); cropping.onClick = undefined; @@ -364,18 +418,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @action cancelOutpaintPrompt = () => { - const origWidth = NumCast(this.Document[this.fieldKey + '_outpaintOriginalWidth']); - const origHeight = NumCast(this.Document[this.fieldKey + '_outpaintOriginalHeight']); - this.Document._width = origWidth; - this.Document._height = origHeight; + [this.Document._width, this.Document._height] = [this.outpaintOriginalSize.width, this.outpaintOriginalSize.height]; this._outpaintingInProgress = false; + this.outpaintOriginalSize = undefined; this.closeOutpaintPrompt(); }; @action - handlePromptChange = (val: string | number) => { - this._outpaintPromptInput = '' + val; - }; + handlePromptChange = (val: string | number) => (this._outpaintPromptInput = '' + val); @action submitOutpaintPrompt = () => { @@ -416,8 +466,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { loadingOverlay.innerHTML = '<div style="color: white; font-size: 16px;">Generating outpainted image...</div>'; this._mainCont?.appendChild(loadingOverlay); - const origWidth = NumCast(this.Document[this.fieldKey + '_outpaintOriginalWidth']); - const origHeight = NumCast(this.Document[this.fieldKey + '_outpaintOriginalHeight']); + const { width: origWidth, height: origHeight } = this.outpaintOriginalSize; const response = await Networking.PostToServer('/outpaintImage', { imageUrl: currentPath, prompt: customPrompt, @@ -454,8 +503,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this.Document.$ai = true; this.Document.$ai_outpainted = true; this.Document.$ai_outpaint_prompt = customPrompt; - this.Document[this.fieldKey + '_outpaintOriginalWidth'] = undefined; - this.Document[this.fieldKey + '_outpaintOriginalHeight'] = undefined; + this.outpaintOriginalSize = undefined; } else { this.cancelOutpaintPrompt(); alert('Failed to receive a valid image URL from server.'); @@ -478,6 +526,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { return this._props.PanelWidth() / this._props.PanelHeight() < this.nativeSize.nativeWidth / this.nativeSize.nativeHeight; } + isOutpaintable = () => true; + componentUI = (/* boundsLeft: number, boundsTop: number*/) => !this._showOutpaintPrompt ? null : ( <div @@ -668,8 +718,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @computed get nativeSize() { TraceMobx(); if (this.paths.length && this.paths[0].includes(DefaultPath)) return { nativeWidth: NumCast(this.layoutDoc._width), nativeHeight: NumCast(this.layoutDoc._height), nativeOrientation: 0 }; - const nativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth'], 500)); - const nativeHeight = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '_nativeHeight'], 500)); + const { nativeWidth, nativeHeight } = this.imgNativeSize; const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + '_nativeOrientation'], 1); return { nativeWidth, nativeHeight, nativeOrientation }; } @@ -689,7 +738,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @computed get overlayImageIcon() { const usePath = this.layoutDoc[`_${this.fieldKey}_usePath`]; - return ( + return this._regenerateLoading ? null : ( <Tooltip title={ <div className="dash-tooltip"> @@ -731,7 +780,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { ); } @computed get regenerateImageIcon() { - return ( + return this._regenerateLoading ? null : ( <Tooltip title={'click to show AI generations. Drop an image on to create a new generation'}> <div className="imageBox-regenerateDropTarget" @@ -820,7 +869,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { transform, transformOrigin, width: this._outpaintAlign ? 'max-content' : this._outpaintAlign ? '100%' : undefined, - height: this._outpaintVAlign ? 'max-content' : this.Document[this.fieldKey + '_outpaintOriginalWidth'] !== undefined ? '100%' : undefined, + height: this._outpaintVAlign ? 'max-content' : this.outpaintOriginalSize?.width ? '100%' : undefined, }} onError={action(e => (this._error = e.toString()))} draggable={false} @@ -943,10 +992,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { return { width, height }; }; savedAnnotations = () => this._savedAnnotations; + showBorderRounding = returnTrue; + rejectDrop = (de: DragManager.DropEvent, subView?: DocumentView | undefined) => (this.dataDoc[this.fieldKey] === undefined ? true : (this._props.rejectDrop?.(de, subView) ?? false)); render() { TraceMobx(); - const borderRad = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding) as string; - const borderRadius = borderRad?.includes('px') ? `${Number(borderRad.split('px')[0]) / (this._props.NativeDimScaling?.() || 1)}px` : borderRad; + const borderRadius = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding) as string; return ( <> <div @@ -986,7 +1036,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { ScreenToLocalTransform={this.screenToLocalTransform} select={emptyFunction} focus={this.focus} - rejectDrop={this._props.rejectDrop} + rejectDrop={this.rejectDrop} getScrollHeight={this.getScrollHeight} NativeDimScaling={returnOne} isAnyChildContentActive={returnFalse} @@ -1048,8 +1098,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { if (result instanceof Error) { alert('Error uploading files - possibly due to unsupported file types'); } else { - this.dataDoc[this.fieldKey] = new ImageField(result.accessPaths.agnostic.client); - !(result instanceof Error) && DocUtils.assignUploadInfo(result, this.dataDoc); + runInAction(() => { + this.dataDoc.layout_resetNativeDim = true; + !(result instanceof Error) && DocUtils.assignUploadInfo(result, this.dataDoc, this.fieldKey); + this.dataDoc[this.fieldKey] = new ImageField(result.accessPaths.agnostic.client); + }); } disposer(); } else { |