diff options
Diffstat (limited to 'src/client/views/nodes')
56 files changed, 967 insertions, 696 deletions
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss index 933a383ea..c25c09af9 100644 --- a/src/client/views/nodes/AudioBox.scss +++ b/src/client/views/nodes/AudioBox.scss @@ -138,7 +138,7 @@ input[type='range']::-webkit-slider-thumb { box-shadow: 0; - border: 0; + border: 0px; height: 10px; width: 10px; border-radius: 10px; @@ -168,7 +168,7 @@ .audiobox-button { width: 15px; height: 15px; - margin: 0; + margin: 0px; svg { width: 10px; diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.scss b/src/client/views/nodes/CollectionFreeFormDocumentView.scss index 7f0a39550..300533df8 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.scss +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.scss @@ -3,7 +3,7 @@ position: absolute; background-color: transparent; touch-action: manipulation; - top: 0; - left: 0; + top: 0px; + left: 0px; pointer-events: none; } diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss index d2ba9796b..cbbd6bde3 100644 --- a/src/client/views/nodes/ComparisonBox.scss +++ b/src/client/views/nodes/ComparisonBox.scss @@ -18,7 +18,7 @@ } .input-box { position: absolute; - top: 50; + top: 50px; padding: 10px; width: 100%; height: 70%; @@ -33,7 +33,7 @@ padding-right: 5px; border-radius: 2px; height: 17%; - bottom: 0; + bottom: 0px; overflow: hidden; display: flex; width: 100%; @@ -101,7 +101,7 @@ position: absolute; display: inline-block; margin-top: 150px; - bottom: 0; + bottom: 0px; } .dropup-content { @@ -145,8 +145,8 @@ .clip-div { position: absolute; - top: 0; - left: 0; + top: 0px; + left: 0px; height: 100%; overflow: hidden; @@ -180,8 +180,8 @@ .afterBox-cont { position: absolute; - top: 0; - right: 0; + top: 0px; + right: 0px; height: 100%; width: 100%; overflow: hidden; @@ -331,8 +331,8 @@ justify-content: space-between; height: max-content; position: absolute; - bottom: 0; - right: 2; + bottom: 0px; + right: 2px; flex-direction: row-reverse; display: flex; cursor: pointer; diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.scss b/src/client/views/nodes/DataVizBox/DataVizBox.scss index 9825d926f..32a01355e 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.scss +++ b/src/client/views/nodes/DataVizBox/DataVizBox.scss @@ -37,25 +37,25 @@ margin-left: 10px; margin-bottom: -10px; } - + .displaySchemaLive { margin-bottom: 20px; } .dataviz-sidebar { position: absolute; - right: 0; - top: 0; + right: 0px; + top: 0px; height: 100%; } .button-container { pointer-events: unset; } - .dataVizBox-annotationLayer{ + .dataVizBox-annotationLayer { position: absolute; transform-origin: left top; - top: 0; + top: 0px; width: 100%; pointer-events: none; mix-blend-mode: multiply; diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss index 6eb7fa96a..e2261b9e2 100644 --- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss @@ -110,12 +110,12 @@ &::before { content: ''; position: absolute; - top: 0; - left: 0; + top: 0px; + left: 0px; border-bottom: 20px solid rgb(50, 50, 50); border-left: 12px solid transparent; border-right: 12px solid transparent; - height: 0; + height: 0px; width: 50px; } @@ -127,7 +127,7 @@ border-bottom: 22px solid rgb(180, 180, 180); border-left: 12px solid transparent; border-right: 12px solid transparent; - height: 0; + height: 0px; width: 52px; z-index: -1; } @@ -418,8 +418,8 @@ } .div { - width: 200; - height: 200; + width: 200px; + height: 200px; border: solid 1px white; } @@ -588,7 +588,7 @@ } .docCreatorMenu-configuration-bar { - width: 200; + width: 200px; gap: 5px; display: flex; flex-direction: row; @@ -709,8 +709,8 @@ width: 100%; aspect-ratio: 1; //height: auto; - // max-width: 240; - // max-height: 240; + // max-width: 240px; + // max-height: 240px; border: 1px solid rgb(180, 180, 180); border-radius: 5px; background-color: rgb(34, 34, 37); @@ -718,8 +718,8 @@ scrollbar-width: none; &.small { - max-width: 100; - max-height: 100; + max-width: 100px; + max-height: 100px; } .docCreatorMenu-layout-preview-item { @@ -1009,8 +1009,8 @@ } &:hover .operator-dropdown-current { - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; + border-bottom-right-radius: 0px; + border-bottom-left-radius: 0px; } &:hover .operator-dropdown-option { diff --git a/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.scss b/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.scss index 63a693918..0acc2c847 100644 --- a/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.scss +++ b/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.scss @@ -13,7 +13,7 @@ $highlightedText: #82e0ff; min-height: 200px; border-radius: 15px; padding: 15px; - padding-bottom: 0; + padding-bottom: 0px; z-index: 999; display: flex; flex-direction: column; @@ -40,7 +40,7 @@ $highlightedText: #82e0ff; font-size: 12px; font-weight: 400; letter-spacing: 1px; - margin: 0; + margin: 0px; padding-right: 5px; } @@ -124,8 +124,8 @@ $highlightedText: #82e0ff; .img-container::after { content: ''; position: absolute; - top: 0; - left: 0; + top: 0px; + left: 0px; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss index ff1fa343d..a22e1153c 100644 --- a/src/client/views/nodes/DataVizBox/components/Chart.scss +++ b/src/client/views/nodes/DataVizBox/components/Chart.scss @@ -91,7 +91,7 @@ margin: 5px; margin-left: 25px; margin-right: 10px; - margin-bottom: 0; + margin-bottom: 0px; .tableBox-table { height: 100%; width: 100%; @@ -101,7 +101,7 @@ text-overflow: ellipsis; width: 100%; white-space: pre; - max-width: 150; + max-width: 150px; overflow: hidden; margin-left: 2px; } diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index 9e0868cd5..cc08cf269 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -401,7 +401,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { } })}> <thead> - <tr style={{ height: this.startID * Number(DATA_VIZ_TABLE_ROW_HEIGHT) }} /> + <tr style={{ height: this.startID * Number(DATA_VIZ_TABLE_ROW_HEIGHT.replace("px","")) }} /> <tr> {this.columns.map((col, i) => ( <th @@ -470,7 +470,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { })} </tr> ))} - <tr style={{ display: this._tableDataIds.length - this.endID ? undefined : 'none', height: (this._tableDataIds.length - this.endID) * Number(DATA_VIZ_TABLE_ROW_HEIGHT) }} /> + <tr style={{ display: this._tableDataIds.length - this.endID ? undefined : 'none', height: (this._tableDataIds.length - this.endID) * Number(DATA_VIZ_TABLE_ROW_HEIGHT.replace("px","")) }} /> </tbody> </table> </div> diff --git a/src/client/views/nodes/DocumentLinksButton.scss b/src/client/views/nodes/DocumentLinksButton.scss index e1b83dc59..43b1e083f 100644 --- a/src/client/views/nodes/DocumentLinksButton.scss +++ b/src/client/views/nodes/DocumentLinksButton.scss @@ -17,8 +17,8 @@ } .documentLinksButton-cont { - min-width: 20; - min-height: 20; + min-width: 20px; + min-height: 20px; position: absolute; } diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index c4351a200..98ca76339 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -1,6 +1,7 @@ @use '../global/globalCssVariables.module.scss' as global; .documentView-effectsWrapper { + height: 100%; border-radius: inherit; transition: inherit; } @@ -14,13 +15,13 @@ width: 100%; height: 100%; position: absolute; - top: 0; + top: 0px; } .documentView-node { position: inherit; - top: 0; - left: 0; + top: 0px; + left: 0px; width: 100%; height: 100%; border-radius: inherit; @@ -55,7 +56,7 @@ .documentView-htmlOverlay { position: absolute; display: flex; - top: 0; + top: 0px; height: 100%; width: 100%; .documentView-htmlOverlayInner { @@ -79,9 +80,9 @@ .documentView-audioBackground { display: inline-block; width: 25px; - height: 25; + height: 25px; position: absolute; - top: 0; + top: 0px; left: 50%; border-radius: 25px; background: white; @@ -130,7 +131,7 @@ width: 30px; border-radius: 50%; position: absolute; - right: -15; + right: -15px; opacity: 0.9; pointer-events: auto; background-color: #9dca96; @@ -147,8 +148,8 @@ .documentView-anchorCont { position: absolute; - top: 0; - left: 0; + top: 0px; + left: 0px; width: 100%; height: 100%; display: inline-block; @@ -160,8 +161,8 @@ position: absolute; width: 100%; height: 100%; - top: 0; - left: 0; + top: 0px; + left: 0px; } .documentView-styleWrapper { @@ -183,9 +184,9 @@ .documentView-titleWrapper-hover { color: global.$black; transform-origin: top left; - top: 0; + top: 0px; width: 100%; - height: 14; + height: 14px; opacity: 0.5; text-align: center; text-overflow: ellipsis; @@ -211,7 +212,7 @@ .documentView-captionWrapper { position: absolute; - bottom: 0; + bottom: 0px; width: 100%; overflow-y: auto; transform-origin: bottom left; @@ -275,20 +276,20 @@ .documentView-noAIWidgets { transform-origin: top left; position: absolute; - bottom: 0; + bottom: 0px; pointer-events: none; } .documentView-widgetDecorations { transform-origin: top right; position: absolute; - top: 0; - right: 0; + top: 0px; + right: 0px; } .documentView-editorView-history { position: absolute; transform-origin: top right; - right: 0; + right: 0px; top: 0; overflow-y: scroll; scrollbar-width: thin; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 9b73cc073..fe95f15af 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -720,7 +720,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewProps & Field @computed get tagsOverlay() { return ( <div - className="documentView-noAiWidgets" + className="documentView-noAIWidgets" style={{ width: `${100 / this.uiBtnScaling}%`, // transform: `scale(${this.uiBtnScaling})`, diff --git a/src/client/views/nodes/FontIconBox/FontIconBadge.scss b/src/client/views/nodes/FontIconBox/FontIconBadge.scss index 2ff5c651f..e741936db 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBadge.scss +++ b/src/client/views/nodes/FontIconBox/FontIconBadge.scss @@ -6,7 +6,7 @@ color: black; display: block; position: absolute; - right: 5; + right: 5px; border-radius: 50%; text-align: center; } diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.scss b/src/client/views/nodes/FontIconBox/FontIconBox.scss index 8bc68c131..52eebba54 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.scss +++ b/src/client/views/nodes/FontIconBox/FontIconBox.scss @@ -42,7 +42,7 @@ letter-spacing: normal; background-color: inherit; border-radius: 8px; - padding: 0; + padding: 0px; width: 100%; font-family: 'system-ui'; text-transform: uppercase; @@ -96,22 +96,22 @@ display: inline-block; width: 100%; height: 25px; - margin: 0; + margin: 0px; } .switch input { opacity: 0; - width: 0; - height: 0; + width: 0px; + height: 0px; } .slider { position: absolute; cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; + top: 0px; + left: 0px; + right: 0px; + bottom: 0px; background-color: lightgrey; -webkit-transition: 0.4s; transition: 0.4s; @@ -223,7 +223,7 @@ height: fit-content; color: black; top: 100%; - left: 0; + left: 0px; z-index: 21; background-color: #e3e3e3; box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3); @@ -430,7 +430,7 @@ border-radius: 0px 7px 7px 0px; width: 13px; height: 100%; - right: 0; + right: 0px; } .menuButton-dropdown-header { diff --git a/src/client/views/nodes/IconTagBox.scss b/src/client/views/nodes/IconTagBox.scss index d6cf95958..c0977dfc5 100644 --- a/src/client/views/nodes/IconTagBox.scss +++ b/src/client/views/nodes/IconTagBox.scss @@ -15,7 +15,7 @@ width: 20px; height: 20px; margin: auto; - padding: 0; + padding: 0px; border-radius: 50%; background-color: global.$dark-gray; background-color: transparent; diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 5a6292fab..90ede69dc 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -3,14 +3,14 @@ width: 100%; height: 100%; position: absolute; - top: 0; - left: 0; + top: 0px; + left: 0px; transform-origin: top left; .imageBox-annotationLayer { position: absolute; transform-origin: left top; - top: 0; + top: 0px; width: 100%; pointer-events: none; mix-blend-mode: multiply; // bcz: makes text fuzzy! @@ -24,8 +24,8 @@ #upload-icon { position: absolute; - bottom: 0; - right: 0; + bottom: 0px; + right: 0px; width: 20px; height: 20px; } @@ -51,8 +51,8 @@ .imageBox-dot { position: absolute; - bottom: 10; - left: 0; + bottom: 10px; + left: 0px; border-radius: 10px; width: 20px; height: 20px; @@ -131,8 +131,8 @@ position: absolute; color: white; background: black; - right: 0; - bottom: 0; + right: 0px; + bottom: 0px; z-index: 2; transform-origin: bottom right; cursor: default; @@ -142,13 +142,13 @@ } } .imageBox-regenerateDropTarget { - right: 35; + right: 35px; transform-origin: 70px 35px; } .imageBox-fader img { position: absolute; - left: 0; + left: 0px; } .imageBox-fadeBlocker-hover { @@ -223,7 +223,7 @@ max-width: 90%; width: 100%; .imageBox-aiView-similarity { - max-width: 65; + max-width: 65px; overflow: hidden; text-overflow: ellipsis; width: 100%; @@ -250,7 +250,7 @@ z-index: 10000; h3 { - margin-top: 0; + margin-top: 0px; } input { diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 8ed59c6e1..5b738ee19 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 { @@ -112,7 +113,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this._props.setContentViewBox?.(this); } - @computed get oupaintOriginalSize(): { width: number; height: number } { + @computed get outpaintOriginalSize(): { width: number; height: number } { return { width: NumCast(this.Document[this.fieldKey + '_outpaintOriginalWidth']), height: NumCast(this.Document[this.fieldKey + '_outpaintOriginalHeight']), @@ -139,6 +140,42 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { 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 = @@ -225,9 +262,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } }; - handleSelection = async (selection: string) => { - this._searchInput = selection; - }; + handleSelection = (selection: string) => (this._searchInput = selection); drop = undoable( action((e: Event, de: DragManager.DropEvent) => { @@ -385,16 +420,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @action cancelOutpaintPrompt = () => { - [this.Document._width, this.Document._height] = [this.oupaintOriginalSize.width, this.oupaintOriginalSize.height]; + [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 = () => { @@ -435,7 +468,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 { width: origWidth, height: origHeight } = this.oupaintOriginalSize; + const { width: origWidth, height: origHeight } = this.outpaintOriginalSize; const response = await Networking.PostToServer('/outpaintImage', { imageUrl: currentPath, prompt: customPrompt, @@ -495,6 +528,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 @@ -959,11 +994,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 diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss index 441fceba4..80ace6ae0 100644 --- a/src/client/views/nodes/KeyValueBox.scss +++ b/src/client/views/nodes/KeyValueBox.scss @@ -93,9 +93,9 @@ $header-height: 30px; height: 30px; width: 5px; z-index: 20; - right: 0; - top: 0; - border-radius: 0; + right: 0px; + top: 0px; + border-radius: 0px; background: black; pointer-events: all; } @@ -105,8 +105,8 @@ $header-height: 30px; float: left; height: 37px; z-index: 20; - right: 0; - top: 0; + right: 0px; + top: 0px; background: transparent; pointer-events: none; } diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss index 913ab641c..154fbdcfa 100644 --- a/src/client/views/nodes/KeyValuePair.scss +++ b/src/client/views/nodes/KeyValuePair.scss @@ -17,7 +17,7 @@ } .keyValuePair-td-key-check { position: relative; - margin: 0; + margin: 0px; } .keyValuePair-keyField { width: 100%; diff --git a/src/client/views/nodes/LabelBox.scss b/src/client/views/nodes/LabelBox.scss index 889cdc0ca..e1974d6a0 100644 --- a/src/client/views/nodes/LabelBox.scss +++ b/src/client/views/nodes/LabelBox.scss @@ -24,8 +24,8 @@ .answer-icon { position: absolute; - right: 8; - bottom: 5; + right: 8px; + bottom: 5px; color: black; display: inline-block; font-size: 10px; @@ -36,8 +36,8 @@ .q-icon { position: absolute; - right: 6; - bottom: 5; + right: 6px; + bottom: 5px; color: white; display: inline-block; font-size: 10px; @@ -48,8 +48,8 @@ .edit-icon { position: absolute; - right: 20; - bottom: 5; + right: 20px; + bottom: 5px; display: inline-block; font-size: 10px; cursor: pointer; diff --git a/src/client/views/nodes/LinkDocPreview.scss b/src/client/views/nodes/LinkDocPreview.scss index 28216394d..7d99247e7 100644 --- a/src/client/views/nodes/LinkDocPreview.scss +++ b/src/client/views/nodes/LinkDocPreview.scss @@ -42,7 +42,7 @@ .linkDocPreview-button { display: inline-flex; - margin: 0; + margin: 0px; margin-right: 3px; border-radius: 50%; pointer-events: auto; diff --git a/src/client/views/nodes/LoadingBox.scss b/src/client/views/nodes/LoadingBox.scss index cabd4de05..5a8f49000 100644 --- a/src/client/views/nodes/LoadingBox.scss +++ b/src/client/views/nodes/LoadingBox.scss @@ -26,7 +26,7 @@ } .loadingBox-spinner { position: absolute; - top: 0; - left: 0; + top: 0px; + left: 0px; } } diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.scss b/src/client/views/nodes/MapBox/MapAnchorMenu.scss index c36d98afe..217576203 100644 --- a/src/client/views/nodes/MapBox/MapAnchorMenu.scss +++ b/src/client/views/nodes/MapBox/MapAnchorMenu.scss @@ -6,19 +6,19 @@ } .anchorMenu-highlighter { padding-right: 5px; - .antimodeMenu-button { - padding: 0; - padding: 0; + .antimodeMenu-button { + padding: 0px; + padding: 0px; padding-right: 0px; padding-left: 0px; width: 5px; } } -.anchor-color-preview-button { - width: 25px !important; +.anchor-color-preview-button { + width: 25px !important; .anchor-color-preview { display: flex; - flex-direction: column; + flex-direction: column; padding-right: 3px; width: unset !important; .color-preview { @@ -72,12 +72,11 @@ } } - .MuiInputBase-input{ + .MuiInputBase-input { color: white !important; } - - - .css-1t8l2tu-MuiInputBase-input-MuiOutlinedInput-input.Mui-disabled{ + + .css-1t8l2tu-MuiInputBase-input-MuiOutlinedInput-input.Mui-disabled { -webkit-text-fill-color: #b3b2b2 !important; } @@ -91,7 +90,7 @@ gap: 5px; } - .selected-route-details-container{ + .selected-route-details-container { display: flex; flex-direction: column; gap: 3px; @@ -99,33 +98,25 @@ align-items: flex-start; padding: 5px; } - - } - .customized-marker-container{ + .customized-marker-container { display: flex; flex-direction: column; gap: 10px; - .current-marker-container{ + .current-marker-container { display: flex; align-items: center; gap: 5px; } - .all-markers-container{ + .all-markers-container { display: flex; - align-items: center; + align-items: center; gap: 10px; flex-wrap: wrap; max-width: 400px; } } - - - - } - - diff --git a/src/client/views/nodes/MapBox/MapBox.scss b/src/client/views/nodes/MapBox/MapBox.scss index bd4b51038..89d381070 100644 --- a/src/client/views/nodes/MapBox/MapBox.scss +++ b/src/client/views/nodes/MapBox/MapBox.scss @@ -21,7 +21,7 @@ .mapBox-infoWindow { background-color: white; opacity: 0.75; - padding: 12; + padding: 12px; font-size: 17; } .mapBox-searchbar { @@ -119,7 +119,7 @@ width: 100%; label { - margin-bottom: 0; + margin-bottom: 0px; } .speed-label { @@ -197,12 +197,12 @@ } .mapBox-sidebar { position: absolute; - right: 0; + right: 0px; height: 100%; } .mapBox-sidebar-handle { - top: 0; + top: 0px; //top: calc(50% - 17.5px); // use this to center vertically -- make sure it looks okay for slide views width: 10px; height: 100%; @@ -215,7 +215,7 @@ left: 50%; margin-left: 120px; right: unset !important; - margin-top: -10; + margin-top: -10px; height: max-content; } .searchbox { diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index f09a2630a..e34ca61d4 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -17,8 +17,8 @@ height: 100%; z-index: 1; pointer-events: none; - top: 0; - left: 0; + top: 0px; + left: 0px; // glr: This should really be the same component as text and PDFs .pdfBox-sidebarBtn { @@ -72,16 +72,16 @@ align-items: center; height: 20px; background: none; - padding: 0; + padding: 0px; position: absolute; pointer-events: all; color: white; - bottom: 0; - right: 0; + bottom: 0px; + right: 0px; .pdfBox-overlayButton-arrow { - width: 0; - height: 0; + width: 0px; + height: 0px; border-top: 10px solid transparent; border-bottom: 10px solid transparent; border-right: 15px solid #121721; @@ -122,8 +122,8 @@ .pdfBox-settingsCont { position: absolute; - right: 0; - top: 3; + right: 0px; + top: 3px; pointer-events: all; .pdfBox-settingsButton { @@ -133,11 +133,11 @@ align-items: center; height: 20px; background: none; - padding: 0; + padding: 0px; .pdfBox-settingsButton-arrow { - width: 0; - height: 0; + width: 0px; + height: 0px; border-top: 10px solid transparent; border-bottom: 10px solid transparent; border-right: 15px solid #121721; @@ -189,7 +189,7 @@ width: calc(100% - 40px); height: 20px; background: #121721; - bottom: 0; + bottom: 0px; display: flex; justify-content: center; align-items: center; @@ -253,13 +253,13 @@ .pdfBox-container { position: absolute; transform-origin: top left; - top: 0; + top: 0px; } .pdfBox-sidebarContainer { position: absolute; height: 100%; - right: 0; - top: 0; + right: 0px; + top: 0px; } .pdfBox-interactive { @@ -290,7 +290,7 @@ } .pdfBox-settingsButton-arrow { - height: 60; + height: 60px; border-top: 30px solid transparent; border-bottom: 30px solid transparent; border-right: 30px solid #121721; diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 45fa5cc12..5501f0a31 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -33,6 +33,9 @@ import { ImageBox } from './ImageBox'; import { OpenWhere } from './OpenWhere'; import './PDFBox.scss'; import { CreateImage } from './WebBoxRenderer'; +import { gptAPICall } from '../../apis/gpt/GPT'; +import { List } from '../../../fields/List'; +import { GPTCallType } from '../../apis/gpt/GPT'; @observer export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @@ -78,6 +81,36 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } } + autoTag = async () => { + if (!this.Document.$tags_chat && this._pdf) { + if (!this.dataDoc.text) { + // 1) Extract text from the first few pages (e.g., first 2 pages) + const maxPages = Math.min(2, this._pdf.numPages); + const promises: Promise<string>[] = []; + for (let pageNum = 1; pageNum <= maxPages; pageNum++) { + promises.push( + this._pdf + .getPage(pageNum) + .then(page => page.getTextContent()) + .then(content => content.items.map(item => ('str' in item ? item.str : '')).join(' ')) + ); + } + this.dataDoc.text = (await Promise.all(promises)).join(' '); + } + + const text = StrCast(this.dataDoc.text).trim().slice(0, 2000); + if (text) { + // 2) Ask GPT to classify and provide descriptive tags, then normalize the results + const label = await gptAPICall(`"${text}"`, GPTCallType.CLASSIFYTEXTFULL).then(raw => raw.trim().toUpperCase()); + + this.Document.$tags_chat = new List<string>(label.split(/\s+/)); + + // 4) Show tags in layout + this.Document._layout_showTags = true; + } + } + }; + replaceCanvases = (oldDiv: HTMLElement, newDiv: HTMLElement) => { if (oldDiv.childNodes) { for (let i = 0; i < oldDiv.childNodes.length; i++) { diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.scss b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.scss index ac2c611c7..78aa526bf 100644 --- a/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.scss +++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.scss @@ -29,8 +29,8 @@ .wedge { pointer-events: none; position: absolute; - left: 0; - top: 0; + left: 0px; + top: 0px; } } diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.scss b/src/client/views/nodes/RecordingBox/ProgressBar.scss index 28ad25ffa..ec01f0241 100644 --- a/src/client/views/nodes/RecordingBox/ProgressBar.scss +++ b/src/client/views/nodes/RecordingBox/ProgressBar.scss @@ -1,36 +1,34 @@ - .progressbar { - touch-action: none; - vertical-align: middle; - text-align: center; - - align-items: center; - cursor: default; - - - position: absolute; - display: flex; - justify-content: flex-start; - bottom: 2px; - width: 99%; - height: 30px; - background-color: gray; - - &.done { - top: 0; - width: 0px; - height: 5px; - background-color: red; - z-index: 2; - } - - &.mark { - top: 0; - background-color: transparent; - border-right: 2px solid white; - z-index: 3; - pointer-events: none; - } + touch-action: none; + vertical-align: middle; + text-align: center; + + align-items: center; + cursor: default; + + position: absolute; + display: flex; + justify-content: flex-start; + bottom: 2px; + width: 99%; + height: 30px; + background-color: gray; + + &.done { + top: 0px; + width: 0px; + height: 5px; + background-color: red; + z-index: 2; + } + + &.mark { + top: 0px; + background-color: transparent; + border-right: 2px solid white; + z-index: 3; + pointer-events: none; + } } .progressbar-disabled { @@ -43,37 +41,41 @@ // citation: https://codepen.io/_Master_/pen/PRdjmQ @keyframes blinker { - from {opacity: 1.0;} - to {opacity: 0.0;} + from { + opacity: 1; + } + to { + opacity: 0; + } } .blink { - text-decoration: blink; - animation-name: blinker; - animation-duration: 0.6s; - animation-iteration-count:infinite; - animation-timing-function:ease-in-out; - animation-direction: alternate; + text-decoration: blink; + animation-name: blinker; + animation-duration: 0.6s; + animation-iteration-count: infinite; + animation-timing-function: ease-in-out; + animation-direction: alternate; } .segment { - border: 3px solid black; - background-color: red; - margin: 1px; - padding: 0; - cursor: pointer; - transition-duration: .5s; - user-select: none; - - vertical-align: middle; - text-align: center; + border: 3px solid black; + background-color: red; + margin: 1px; + padding: 0px; + cursor: pointer; + transition-duration: 0.5s; + user-select: none; + + vertical-align: middle; + text-align: center; } .segment-expanding { -border-color: red; - background-color: white; - transition-duration: 0s; - opacity: .75; - pointer-events: none; + border-color: red; + background-color: white; + transition-duration: 0s; + opacity: 0.75; + pointer-events: none; } .segment-expanding:hover { @@ -82,10 +84,10 @@ border-color: red; } .segment-disabled { - pointer-events: none; - opacity: 0.5; - transition-duration: 0s; - /* Hide the text. */ + pointer-events: none; + opacity: 0.5; + transition-duration: 0s; + /* Hide the text. */ text-indent: 100%; white-space: nowrap; overflow: hidden; @@ -99,25 +101,26 @@ border-color: red; } .segment:first-child { - margin-left: 2px; + margin-left: 2px; } .segment:last-child { - margin-right: 2px; + margin-right: 2px; } .segment:hover { background-color: white; } -.segment:hover, .segment-selected { - margin: 0px; - border: 4px solid red; - border-radius: 2px; +.segment:hover, +.segment-selected { + margin: 0px; + border: 4px solid red; + border-radius: 2px; } .segment-selected { - border: 4px solid #202020; - background-color: red; - opacity: .75; - cursor: grabbing; + border: 4px solid #202020; + background-color: red; + opacity: 0.75; + cursor: grabbing; } diff --git a/src/client/views/nodes/RecordingBox/RecordingView.scss b/src/client/views/nodes/RecordingBox/RecordingView.scss index f2d5a980d..15b48c111 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.scss +++ b/src/client/views/nodes/RecordingBox/RecordingView.scss @@ -28,7 +28,7 @@ video { justify-content: center; // overflow: hidden; border-radius: 10px; - margin: 0; + margin: 0px; } .video-wrapper:hover .controls { @@ -108,7 +108,7 @@ video { .timer { font-size: 15px; color: white; - margin: 0; + margin: 0px; } .dot { @@ -148,7 +148,7 @@ video { height: 80%; width: 80%; align-self: center; - margin: 0; + margin: 0px; &:hover { height: 85%; @@ -163,7 +163,7 @@ video { height: 70%; width: 70%; align-self: center; - margin: 0; + margin: 0px; // &:hover { // width: 40px; @@ -178,8 +178,8 @@ video { flex-direction: row; align-content: center; position: relative; - top: 0; - bottom: 0; + top: 0px; + bottom: 0px; &.video-edit-wrapper { // right: 50% - 15; diff --git a/src/client/views/nodes/ScreenshotBox.scss b/src/client/views/nodes/ScreenshotBox.scss index 1e9b64a0b..1714d87c2 100644 --- a/src/client/views/nodes/ScreenshotBox.scss +++ b/src/client/views/nodes/ScreenshotBox.scss @@ -32,10 +32,10 @@ .screenshotBox-uiButtons { position: absolute; - right: 25; - top: 0; - width: 22; - height: 25; + right: 25px; + top: 0px; + width: 22px; + height: 25px; .screenshotBox-recorder { color: white; diff --git a/src/client/views/nodes/ScriptingBox.scss b/src/client/views/nodes/ScriptingBox.scss index 9789da55a..de70dbe74 100644 --- a/src/client/views/nodes/ScriptingBox.scss +++ b/src/client/views/nodes/ScriptingBox.scss @@ -82,14 +82,14 @@ } .rta__autocomplete--top { - margin-top: 0; + margin-top: 0px; margin-bottom: 1em; max-height: 100px; } .rta__list { - margin: 0; - padding: 0; + margin: 0px; + padding: 0px; background: #fff; border: 1px solid #dfe2e5; border-radius: 3px; diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index b5405f0fb..27f419198 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -3,8 +3,8 @@ .mini-viewer { cursor: grab; position: absolute; - right: 10; - top: 10; + right: 10px; + top: 10px; opacity: 0.1; transition: all 0.4s; color: white; @@ -38,7 +38,7 @@ .videoBox-annotationLayer { position: relative; transform-origin: left top; - top: 0; + top: 0px; width: 100%; pointer-events: none; mix-blend-mode: multiply; // bcz: makes text fuzzy! @@ -81,8 +81,8 @@ // } .videoBox-ui-wrapper { - width: 0; - height: 0; + width: 0px; + height: 0px; position: relative; z-index: 2000; } diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index b3cb0e1db..f994bdbb5 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -30,6 +30,7 @@ import { StyleProp } from '../StyleProp'; import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { FocusViewOptions } from './FocusViewOptions'; +import { gptImageLabel } from '../../apis/gpt/GPT'; import './VideoBox.scss'; /** @@ -109,6 +110,52 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { return this._videoRef; } + autoTag = async () => { + if (this.Document.$tags_chat) return; + try { + if (!this.player) throw new Error('Video element not available.'); + + // 1) Extract a frame at the video's midpoint + const videoDuration = this.player.duration; + const snapshotTime = videoDuration / 2; + + // Seek the video element to the midpoint + await new Promise<void>(resolve => { + const onSeeked = () => { + this.player!.removeEventListener('seeked', onSeeked); + resolve(); + }; + this.player!.addEventListener('seeked', onSeeked); + this.player!.currentTime = snapshotTime; + }); + + // 2) Draw the frame onto a canvas and get a base64 representation + const canvas = document.createElement('canvas'); + canvas.width = this.player.videoWidth; + canvas.height = this.player.videoHeight; + const ctx = canvas.getContext('2d'); + if (!ctx) throw new Error('Failed to create canvas context.'); + ctx.drawImage(this.player, 0, 0, canvas.width, canvas.height); + const base64Image = canvas.toDataURL('image/png'); + + // 3) Send the image data to GPT for classification and descriptive tags + const label = await gptImageLabel( + base64Image, + `Classify this video frame as either a PERSON or LANDSCAPE. + Then provide five additional descriptive tags (single words) separated by spaces. + Finally, add one detailed summary phrase using underscores.` + ).then(raw => raw.trim().toUpperCase()); + + // 4) Normalize and store labels in the Document's tags + const aspect = this.player!.videoWidth / (this.player!.videoHeight || 1); + this.Document.$tags_chat = new List<string>([...label.split(/\s+/), `ASPECT_${aspect}`]); + // 5) Turn on tag display in layout + this.Document._layout_showTags = true; + } catch (err) { + console.error('Video autoTag failed:', err); + } + }; + componentDidMount() { this.unmounting = false; this._props.setContentViewBox?.(this); // this tells the DocumentView that this VideoBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the VideoBox when making a link. diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss index 05d5babf9..e7c9cf095 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -3,8 +3,8 @@ .webBox { height: 100%; width: 100%; - top: 0; - left: 0; + top: 0px; + left: 0px; position: relative; display: flex; overflow: hidden; @@ -28,8 +28,8 @@ height: 100%; z-index: 1; pointer-events: none; - top: 0; - left: 0; + top: 0px; + left: 0px; overflow: hidden; .webBox-overlayButton { @@ -39,16 +39,16 @@ align-items: center; height: 20px; background: none; - padding: 0; + padding: 0px; position: absolute; pointer-events: all; color: white; - bottom: 0; - right: 0; + bottom: 0px; + right: 0px; .webBox-overlayButton-arrow { - width: 0; - height: 0; + width: 0px; + height: 0px; border-top: 10px solid transparent; border-bottom: 10px solid transparent; border-right: 15px solid #121721; @@ -92,7 +92,7 @@ width: calc(100% - 40px); height: 20px; background: #121721; - bottom: 0; + bottom: 0px; display: flex; justify-content: center; align-items: center; @@ -137,7 +137,7 @@ .webBox-annotationLayer { position: absolute; transform-origin: left top; - top: 0; + top: 0px; width: 100%; pointer-events: none; mix-blend-mode: multiply; // bcz: makes text fuzzy! @@ -156,8 +156,8 @@ .webBox-htmlSpan { position: absolute; - top: 0; - left: 0; + top: 0px; + left: 0px; cursor: text; padding: 15px; height: 100%; @@ -171,8 +171,8 @@ .webBox-cont-interactive { padding: 0vw; position: absolute; - top: 0; - left: 0; + top: 0px; + left: 0px; width: 100%; height: 100%; transform-origin: top left; @@ -181,8 +181,8 @@ width: 100%; height: 100%; position: absolute; - top: 0; - left: 0; + top: 0px; + left: 0px; body { ::selection { color: white; @@ -203,8 +203,8 @@ height: 100%; position: absolute; transform-origin: top left; - top: 0; - left: 0; + top: 0px; + left: 0px; overflow: auto; .webBox-innerContent { @@ -224,7 +224,7 @@ } .webBox-buttons { - margin-left: 44; + margin-left: 44px; background: lightGray; width: 100%; } @@ -232,8 +232,8 @@ .webBox-annotationToggle { z-index: 901; position: absolute; - top: 2; - left: 2; + top: 2px; + left: 2px; cursor: pointer; box-shadow: black 0.3em 0.3em 1em; border-radius: 5px; diff --git a/src/client/views/nodes/audio/AudioWaveform.scss b/src/client/views/nodes/audio/AudioWaveform.scss index 6cbd1759a..c6b0da9c8 100644 --- a/src/client/views/nodes/audio/AudioWaveform.scss +++ b/src/client/views/nodes/audio/AudioWaveform.scss @@ -1,17 +1,17 @@ -.audioWaveform { +.audioWaveform { position: relative; width: 100%; height: 200%; overflow: hidden; z-index: -1000; - bottom: 0; + bottom: 0px; pointer-events: none; div { height: 100% !important; - width: 100% !important; + width: 100% !important; } - canvas { + canvas { height: 100% !important; - width: 100% !important; + width: 100% !important; } } diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss index 3d27fa887..4db5cec3d 100644 --- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss +++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss @@ -28,7 +28,7 @@ $transition: all 0.2s ease-in-out; box-shadow: 0 1px 4px $shadow-color; h2 { - margin: 0; + margin: 0px; font-size: 1.5em; font-weight: 500; } @@ -126,7 +126,7 @@ $transition: all 0.2s ease-in-out; animation: fadeIn 0.3s ease-in-out; p { - margin: 0; + margin: 0px; font-size: 14px; } @@ -263,10 +263,10 @@ $transition: all 0.2s ease-in-out; .uploading-overlay { position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; + top: 0px; + left: 0px; + right: 0px; + bottom: 0px; background-color: rgba(255, 255, 255, 0.8); display: flex; justify-content: center; @@ -285,7 +285,7 @@ $transition: all 0.2s ease-in-out; @media (max-width: 768px) { .chat-box { - border-radius: 0; + border-radius: 0px; } .message { diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ProgressBar.scss b/src/client/views/nodes/chatbot/chatboxcomponents/ProgressBar.scss index ff5be4a38..77d452830 100644 --- a/src/client/views/nodes/chatbot/chatboxcomponents/ProgressBar.scss +++ b/src/client/views/nodes/chatbot/chatboxcomponents/ProgressBar.scss @@ -21,8 +21,8 @@ background-color: #4a90e2; opacity: 0.6; position: absolute; - top: 0; - left: 0; + top: 0px; + left: 0px; animation: bounce 2s infinite ease-in-out; } @@ -42,10 +42,10 @@ .uploading-overlay { position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; + top: 0px; + left: 0px; + right: 0px; + bottom: 0px; background-color: rgba(255, 255, 255, 0.8); display: flex; align-items: center; diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss index 3734ad9cc..1b2f76bbe 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.scss +++ b/src/client/views/nodes/formattedText/DashFieldView.scss @@ -17,7 +17,7 @@ min-width: 12px; position: relative; display: inline-block; - margin: 0; + margin: 0px; transform: scale(0.7); background-color: rgba(155, 155, 155, 0.24); } diff --git a/src/client/views/nodes/formattedText/EquationEditor.scss b/src/client/views/nodes/formattedText/EquationEditor.scss index b0c17e56e..602135a30 100644 --- a/src/client/views/nodes/formattedText/EquationEditor.scss +++ b/src/client/views/nodes/formattedText/EquationEditor.scss @@ -32,7 +32,7 @@ margin-left: -1px; position: relative; z-index: 1; - padding: 0; + padding: 0px; display: -moz-inline-box; display: inline-block; } @@ -128,8 +128,8 @@ .mq-math-mode * { font-size: inherit; line-height: inherit; - margin: 0; - padding: 0; + margin: 0px; + padding: 0px; border-color: black; -webkit-user-select: none; -moz-user-select: none; @@ -178,7 +178,7 @@ margin-left: 0.1em; } .mq-math-mode .mq-roman var.mq-f { - margin: 0; + margin: 0px; } .mq-math-mode big { font-size: 200%; @@ -323,7 +323,7 @@ padding: 0.1em; } .mq-math-mode .mq-sqrt-prefix { - padding-top: 0; + padding-top: 0px; position: relative; top: 0.1em; vertical-align: top; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 547a2efa8..d5e566226 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -46,7 +46,7 @@ } audiotag { - left: 0; + left: 0px; position: absolute; cursor: pointer; border-radius: 10px; @@ -62,7 +62,7 @@ audiotag:hover { .formattedTextBox { touch-action: none; background: inherit; - padding: 0; + padding: 0px; border-width: 0px; border-color: global.$medium-gray; box-sizing: border-box; @@ -77,14 +77,14 @@ audiotag:hover { width: 100%; position: relative; transform-origin: left top; - top: 0; - left: 0; + top: 0px; + left: 0px; } .formattedTextBox-cont { touch-action: none; background: inherit; - padding: 0; + padding: 0px; border-width: 0px; border-radius: inherit; border-color: global.$medium-gray; @@ -111,7 +111,7 @@ audiotag:hover { .answer-tooltip { font-size: 15px; padding: 2px; - max-width: 150; + max-width: 150px; line-height: 150%; position: relative; } @@ -122,10 +122,10 @@ audiotag:hover { position: absolute; color: white; background: black; - right: 0; - bottom: 0; - width: 15; - height: 22; + right: 0px; + bottom: 0px; + width: 15px; + height: 22px; cursor: default; } @@ -139,8 +139,8 @@ audiotag:hover { .formattedTextBox-sidebar-handle { position: absolute; - top: 0; - right: 0; + top: 0px; + right: 0px; width: 20px; height: 20px; font-size: 11px; @@ -168,7 +168,7 @@ audiotag:hover { height: 100%; display: inline-block; position: absolute; - right: 0; + right: 0px; overflow: hidden; .collectionfreeformview-container { @@ -302,8 +302,8 @@ footnote::before { position: absolute; top: -0.5em; content: ' '; - height: 0; - width: 0; + height: 0px; + width: 0px; } .formattedTextBox-inlineComment { @@ -346,7 +346,7 @@ footnote::before { .prosemirror-linkBtn { background: unset; color: unset; - padding: 0; + padding: 0px; text-transform: unset; letter-spacing: unset; font-size: unset; @@ -357,7 +357,7 @@ footnote::before { background-color: dimgray; margin-top: 1.5em; z-index: 1; - padding: 5; + padding: 5px; border-radius: 2px; } .prosemirror-hrefoptions { @@ -396,7 +396,7 @@ footnote::before { blockquote { padding: 10px 10px; font-size: smaller; - margin: 0; + margin: 0px; font-style: italic; background: lightgray; border-left: solid 2px dimgray; @@ -415,7 +415,7 @@ footnote::before { p { font-family: inherit; } - margin-left: 0; + margin-left: 0px; } .bullet1 { p { @@ -439,7 +439,7 @@ footnote::before { display: inline-block; font-family: inherit; } - margin-left: 0; + margin-left: 0px; background-color: inherit; } .decimal2-ol { @@ -506,7 +506,7 @@ footnote::before { display: inline-block; font-family: inherit; } - margin-left: 0; + margin-left: 0px; padding-left: 1.2em; background-color: inherit; } @@ -661,7 +661,7 @@ footnote::before { .formattedTextBox-cont { touch-action: none; background: inherit; - padding: 0; + padding: 0px; border-width: 0px; border-radius: inherit; border-color: global.$medium-gray; @@ -706,7 +706,7 @@ footnote::before { height: 100%; display: inline-block; position: absolute; - right: 0; + right: 0px; .collectionfreeformview-container { position: relative; @@ -832,8 +832,8 @@ footnote::before { position: absolute; top: -0.5em; content: ' '; - height: 0; - width: 0; + height: px; + width: 0px; } .formattedTextBox-inlineComment { @@ -892,7 +892,7 @@ footnote::before { display: inline; font-family: inherit; } - margin-left: 0; + margin-left: 0px; } .decimal2-ol { counter-reset: deci2; @@ -952,7 +952,7 @@ footnote::before { display: inline; font-family: inherit; } - margin-left: 0; + margin-left: 0px; padding-left: 1.2em; } .multi2-ol { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index c8df6e50f..07cb795f1 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -13,7 +13,7 @@ import { EditorState, NodeSelection, Plugin, Selection, TextSelection, Transacti import { EditorView, NodeViewConstructor } from 'prosemirror-view'; import * as React from 'react'; import { BsMarkdownFill } from 'react-icons/bs'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, removeStyleSheet, returnFalse, returnZero, setupMoveUpEvents, simMouseEvent, smoothScroll, StopEvent } from '../../../../ClientUtils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, removeStyleSheet, returnFalse, returnTrue, returnZero, setupMoveUpEvents, simMouseEvent, smoothScroll, StopEvent } from '../../../../ClientUtils'; import { DateField } from '../../../../fields/DateField'; import { CreateLinkToActiveAudio, Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc'; import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols'; @@ -308,6 +308,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } }; + autoTag = () => { + const rawText = RTFCast(this.Document[this.fieldKey])?.Text ?? StrCast(this.Document[this.fieldKey]); + if (rawText && !this.Document.$tags_chat) { + const callType = rawText.includes('[placeholder]') ? GPTCallType.CLASSIFYTEXTMINIMAL : GPTCallType.CLASSIFYTEXTFULL; + + gptAPICall(rawText, callType).then( + action(desc => { + // Split GPT response into tokens and push individually & clear existing tags + this.Document.$tags_chat = new List<string>(desc.trim().split(/\s+/)); + this.Document._layout_showTags = true; + }) + ); + } + }; + leafText = (node: Node) => { if (node.type === this.EditorView?.state.schema.nodes.dashField) { const refDoc = !node.attrs.docId ? this.rootDoc : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc); @@ -369,6 +384,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : newText || (DocCast(dataDoc.proto)?.[this.fieldKey] === undefined && this.layoutDoc[this.fieldKey] === undefined) ? new RichTextField(newJson, newText) : undefined; textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.Document, text: newText }); + if (textChange) this.dataDoc.$tags_chat = undefined; this.ApplyingChange = ''; // turning this off here allows a Doc to retrieve data from template if noTemplate below is changed to false unchanged = false; } @@ -1072,6 +1088,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return anchorDoc ?? this.Document; } + showBorderRounding = returnTrue; getView = (doc: Doc, options: FocusViewOptions) => { if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) { return SidebarAnnos.getView(this._sidebarRef.current, this.SidebarShown, () => this.toggleSidebar(false), doc, options); diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss index bc0810f22..92f3e3290 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss @@ -11,8 +11,8 @@ -webkit-transform: translateX(-50%); transform: translateX(-50%); box-shadow: 3px 3px 1.5px grey; - max-width: 400; - max-height: 235; + max-width: 400px; + max-height: 235px; height: max-content; .formattedTextBox-tooltipText { height: max-content; @@ -22,26 +22,26 @@ .formattedTextBox-tooltip:before { content: ''; - height: 0; - width: 0; + height: 0px; + width: 0px; position: absolute; left: 50%; margin-left: -5px; bottom: -6px; border: 5px solid transparent; - border-bottom-width: 0; + border-bottom-width: 0px; border-top-color: silver; } .formattedTextBox-tooltip:after { content: ''; - height: 0; - width: 0; + height: 0px; + width: 0px; position: absolute; left: 50%; margin-left: -5px; bottom: -4.5px; border: 5px solid transparent; - border-bottom-width: 0; + border-bottom-width: 0px; border-top-color: white; } diff --git a/src/client/views/nodes/formattedText/RichTextMenu.scss b/src/client/views/nodes/formattedText/RichTextMenu.scss index fcc816447..7c747de1e 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.scss +++ b/src/client/views/nodes/formattedText/RichTextMenu.scss @@ -23,7 +23,7 @@ .dropdown { position: absolute; top: 35px; - left: 0; + left: 0px; background-color: #323232; color: global.$light-gray; border: 1px solid #4d4d4d; @@ -47,7 +47,7 @@ } &:last-child { - margin-bottom: 0; + margin-bottom: 0px; } } } diff --git a/src/client/views/nodes/formattedText/TooltipTextMenu.scss b/src/client/views/nodes/formattedText/TooltipTextMenu.scss index 87320943d..8980a93a2 100644 --- a/src/client/views/nodes/formattedText/TooltipTextMenu.scss +++ b/src/client/views/nodes/formattedText/TooltipTextMenu.scss @@ -195,7 +195,7 @@ left: 1px; width: 24px; height: 4px; - margin-top: 0; + margin-top: 0px; } } @@ -221,7 +221,7 @@ display: inline-block; width: 1em; height: 1em; - stroke-width: 0; + stroke-width: 0px; stroke: currentColor; fill: currentColor; margin-right: 15px; @@ -231,7 +231,7 @@ display: inline-block; width: 1em; height: 1em; - stroke-width: 3; + stroke-width: 3px; fill: greenyellow; margin-right: 15px; } @@ -270,7 +270,7 @@ &.ProseMirror-menu-dropdown { width: 10px; height: 25px; - margin: 0; + margin: 0px; padding: 0 2px; background-color: #323232; text-align: center; diff --git a/src/client/views/nodes/imageEditor/ImageEditor.scss b/src/client/views/nodes/imageEditor/ImageEditor.scss index c691e6a18..942a7d4c6 100644 --- a/src/client/views/nodes/imageEditor/ImageEditor.scss +++ b/src/client/views/nodes/imageEditor/ImageEditor.scss @@ -4,8 +4,8 @@ $scale: 0.5; .imageEditorContainer { position: absolute; - top: 0; - left: 0; + top: 0px; + left: 0px; z-index: 9999; height: 100vh; width: 100vw; @@ -78,7 +78,7 @@ $scale: 0.5; .sideControlsContainer { width: 160px; position: absolute; - left: 0; + left: 0px; height: 100%; .sideControls { @@ -129,8 +129,8 @@ $scale: 0.5; .originalImageLabel { position: absolute; - bottom: 10; - left: 10; + bottom: 10px; + left: 10px; color: #ffffff; font-size: 0.8rem; letter-spacing: 1px; diff --git a/src/client/views/nodes/scrapbook/AIPresetGenerator.ts b/src/client/views/nodes/scrapbook/AIPresetGenerator.ts new file mode 100644 index 000000000..1f159222b --- /dev/null +++ b/src/client/views/nodes/scrapbook/AIPresetGenerator.ts @@ -0,0 +1,31 @@ +import { ScrapbookItemConfig } from './ScrapbookPreset'; +import { GPTCallType, gptAPICall } from '../../../apis/gpt/GPT'; + +// Represents the descriptor for each document +export interface DocumentDescriptor { + type: string; + tags: string[]; +} + +// Main function to request AI-generated presets +export async function requestAiGeneratedPreset(descriptors: DocumentDescriptor[]): Promise<ScrapbookItemConfig[]> { + const prompt = createPrompt(descriptors); + let aiResponse = await gptAPICall(prompt, GPTCallType.GENERATESCRAPBOOK); + // Strip out ```json and ``` if the model wrapped its answer in fences + aiResponse = aiResponse + .trim() + .replace(/^```(?:json)?\s*/, "") // remove leading ``` or ```json + .replace(/\s*```$/, ""); // remove trailing ``` + const parsedPreset = JSON.parse(aiResponse) as ScrapbookItemConfig[]; + return parsedPreset; +} + +// Helper to generate prompt text for AI +function createPrompt(descriptors: DocumentDescriptor[]): string { + let prompt = ""; + descriptors.forEach((desc, index) => { + prompt += `${index + 1}. Type: ${desc.type}, Tags: ${desc.tags.join(', ')}\n`; + }); + + return prompt; +} diff --git a/src/client/views/nodes/scrapbook/EmbeddedDocView.tsx b/src/client/views/nodes/scrapbook/EmbeddedDocView.tsx deleted file mode 100644 index e99bf67c7..000000000 --- a/src/client/views/nodes/scrapbook/EmbeddedDocView.tsx +++ /dev/null @@ -1,52 +0,0 @@ -//IGNORE FOR NOW, CURRENTLY NOT USED IN SCRAPBOOK IMPLEMENTATION -import * as React from "react"; -import { observer } from "mobx-react"; -import { Doc } from "../../../../fields/Doc"; -import { DocumentView } from "../DocumentView"; -import { Transform } from "../../../util/Transform"; - -interface EmbeddedDocViewProps { - doc: Doc; - width?: number; - height?: number; - slotId?: string; -} - -@observer -export class EmbeddedDocView extends React.Component<EmbeddedDocViewProps> { - render() { - const { doc, width = 300, height = 200, slotId } = this.props; - - // Use either an existing embedding or create one - let docToDisplay = doc; - - // If we need an embedding, create or use one - if (!docToDisplay.isEmbedding) { - docToDisplay = Doc.BestEmbedding(doc) || Doc.MakeEmbedding(doc); - // Set the container to the slot's ID so we can track it - if (slotId) { - docToDisplay.embedContainer = `scrapbook-slot-${slotId}`; - } - } - - return ( - <DocumentView - Document={docToDisplay} - renderDepth={0} - // Required sizing functions - NativeWidth={() => width} - NativeHeight={() => height} - PanelWidth={() => width} - PanelHeight={() => height} - // Required state functions - isContentActive={() => true} - childFilters={() => []} - ScreenToLocalTransform={() => new Transform()} - // Display options - hideDeleteButton={true} - hideDecorations={true} - hideResizeHandles={true} - /> - ); - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.scss b/src/client/views/nodes/scrapbook/ScrapbookBox.scss new file mode 100644 index 000000000..6ac2220f9 --- /dev/null +++ b/src/client/views/nodes/scrapbook/ScrapbookBox.scss @@ -0,0 +1,66 @@ +.scrapbook-box { + /* Make sure the container fills its parent, and set a base background */ + position: relative; /* so that absolute children (loading overlay, etc.) are positioned relative to this */ + width: 100%; + height: 100%; + background: beige; + overflow: hidden; /* prevent scrollbars if children overflow */ +} + +/* Loading overlay that covers the entire scrapbook while AI-generation is in progress */ +.scrapbook-box-loading-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + background: rgba(255, 255, 255, 0.8); + z-index: 10; /* sits above the ImageBox and other content */ +} + +/* The <select> dropdown for choosing presets */ +.scrapbook-box-preset-select { + position: relative; + top: 8px; + left: 8px; + z-index: 20; + padding: 4px 8px; + font-size: 14px; + border: 1px solid #ccc; + border-radius: 4px; + background: white; +} + +/* Container for the “Regenerate Background” button */ +.scrapbook-box-ui { + position: relative; + top: 8px; + right: 8px; + z-index: 20; + background: white; + width: 40px; + display: flex; + justify-content: center; +} + +/* The button itself */ +.scrapbook-box-ui-button { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + font-size: 14px; + color: black; + background: white; + border: 1px solid #ccc; + border-radius: 4px; + cursor: pointer; + white-space: nowrap; +} + +.scrapbook-box-ui-button:hover { + background: #f5f5f5; +} diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx index 6cfe9a62c..d0ae6194f 100644 --- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx +++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx @@ -1,130 +1,260 @@ -import { action, makeObservable, observable } from 'mobx'; +import { IconButton, Size } from '@dash/components'; +import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; +import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCast } from '../../../../fields/Doc'; +import ReactLoading from 'react-loading'; +import { Doc, DocListCast, Opt, StrListCast } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; +import { DateCast, DocCast, NumCast, toList } from '../../../../fields/Types'; import { emptyFunction } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; import { DocumentType } from '../../../documents/DocumentTypes'; +import { DragManager } from '../../../util/DragManager'; +import { SnappingManager } from '../../../util/SnappingManager'; +import { undoable } from '../../../util/UndoManager'; import { CollectionView } from '../../collections/CollectionView'; import { ViewBoxAnnotatableComponent } from '../../DocComponent'; +import { AspectRatioLimits, FireflyImageDimensions } from '../../smartdraw/FireflyConstants'; +import { SmartDrawHandler } from '../../smartdraw/SmartDrawHandler'; import { DocumentView } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; -import { DragManager } from '../../../util/DragManager'; -import { RTFCast, StrCast, toList } from '../../../../fields/Types'; -import { undoable } from '../../../util/UndoManager'; -// Scrapbook view: a container that lays out its child items in a grid/template -export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { - @observable createdDate: string; +import { ImageBox } from '../ImageBox'; +import './ScrapbookBox.scss'; +import { ScrapbookItemConfig } from './ScrapbookPreset'; +import { createPreset, getPresetNames } from './ScrapbookPresetRegistry'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { DocUtils } from '../../../documents/DocUtils'; +import { returnTrue } from '../../../../ClientUtils'; - constructor(props: FieldViewProps) { - super(props); - makeObservable(this); - this.createdDate = this.getFormattedDate(); +function createPlaceholder(cfg: ScrapbookItemConfig, doc: Doc) { + const placeholder = new Doc(); + placeholder.proto = doc; + placeholder.original = doc; + placeholder.x = cfg.x; + placeholder.y = cfg.y; + if (cfg.width !== null) placeholder._width = cfg.width; + if (cfg.height !== null) placeholder._height = cfg.height; + return placeholder; +} - // ensure we always have a List<Doc> in dataDoc['items'] - if (!this.dataDoc[this.fieldKey]) { - this.dataDoc[this.fieldKey] = new List<Doc>(); +function createMessagePlaceholder(cfg: ScrapbookItemConfig) { + return createPlaceholder(cfg, + Docs.Create.TextDocument(cfg.message ?? ('[placeholder] ' + cfg.acceptTags?.[0]), { placeholder: "", placeholder_docType: cfg.type, placeholder_acceptTags: new List<string>(cfg.acceptTags) }) + ); // prettier-ignore +} +export function buildPlaceholdersFromConfigs(configs: ScrapbookItemConfig[]) { + return configs.map(cfg => { + if (cfg.children?.length) { + const childDocs = cfg.children.map(createMessagePlaceholder); + const protoW = cfg.containerWidth ?? cfg.width; + const protoH = cfg.containerHeight ?? cfg.height; + // Create a stacking document with the child placeholders + const containerProto = Docs.Create.StackingDocument(childDocs, { + ...(protoW !== null ? { _width: protoW } : {}), + ...(protoH !== null ? { _height: protoH } : {}), + title: cfg.message, + }); + return createPlaceholder(cfg, containerProto); } - this.createdDate = this.getFormattedDate(); - this.setTitle(); + return createMessagePlaceholder(cfg); + }); +} +export async function slotRealDocIntoPlaceholders(realDoc: Doc, placeholders: Doc[]) { + if (!realDoc.$tags_chart) { + await DocumentView.getFirstDocumentView(realDoc)?.ComponentView?.autoTag?.(); } + const realTags = new Set<string>(StrListCast(realDoc.$tags_chat).map(t => t.toLowerCase?.() ?? '')); + // Find placeholder with most matching tags + let bestMatch: Doc | null = null; + let maxMatches = 0; + + // match fields based on type, or by analyzing content .. simple example of matching text in placeholder to dropped doc's type + placeholders + .filter(ph => ph.placeholder_docType === realDoc.$type) // Skip this placeholder entirely if types do not match. + .forEach(ph => { + const matches = StrListCast(ph.placeholder_acceptTags) + .map(t => t.toLowerCase?.()) + .filter(tag => realTags.has(tag)); + + if (matches.length > maxMatches) { + maxMatches = matches.length; + bestMatch = ph; + } + }); + + if (bestMatch && maxMatches > 0) { + setTimeout(undoable(() => (bestMatch!.proto = realDoc), 'Scrapbook add')); + return true; + } + + return false; +} + +// Scrapbook view: a container that lays out its child items in a template +@observer +export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { public static LayoutString(fieldStr: string) { return FieldView.LayoutString(ScrapbookBox, fieldStr); } + private _disposers: { [name: string]: IReactionDisposer } = {}; + private _imageBoxRef = React.createRef<ImageBox>(); + + constructor(props: FieldViewProps) { + super(props); + makeObservable(this); + } + + @observable _selectedPreset = getPresetNames()[0]; + @observable _loading = false; - getFormattedDate(): string { - return new Date().toLocaleDateString(undefined, { + @computed get createdDate() { + return DateCast(this.dataDoc.$author_date)?.date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric', }); } + @computed get ScrapbookLayoutDocs() { return DocListCast(this.dataDoc[this.fieldKey]); } // prettier-ignore + @computed get BackgroundDoc() { return DocCast(this.dataDoc[this.fieldKey + '_background']); } // prettier-ignore + set ScrapbookLayoutDocs(doc: Doc[]) { this.dataDoc[this.fieldKey] = new List(doc); } // prettier-ignore + set BackgroundDoc(doc: Opt<Doc>) { this.dataDoc[this.fieldKey + '_background'] = doc; } // prettier-ignore @action - setTitle() { - const title = `Scrapbook - ${this.createdDate}`; - if (this.dataDoc.title !== title) { - this.dataDoc.title = title; - - const image = Docs.Create.TextDocument('image'); - image.accepts_docType = DocumentType.IMG; - const placeholder = new Doc(); - placeholder.proto = image; - placeholder.original = image; - placeholder._width = 250; - placeholder._height = 200; - placeholder.x = 0; - placeholder.y = -100; - //placeholder.overrideFields = new List<string>(['x', 'y']); // shouldn't need to do this for layout fields since the placeholder already overrides its protos - - const summary = Docs.Create.TextDocument('summary'); - summary.accepts_docType = DocumentType.RTF; - summary.accepts_textType = 'one line'; - const placeholder2 = new Doc(); - placeholder2.proto = summary; - placeholder2.original = summary; - placeholder2.x = 0; - placeholder2.y = 200; - placeholder2._width = 250; - //placeholder2.overrideFields = new List<string>(['x', 'y', '_width']); // shouldn't need to do this for layout fields since the placeholder already overrides its protos - this.dataDoc[this.fieldKey] = new List<Doc>([placeholder, placeholder2]); - } - } + setDefaultPlaceholder = () => { + this.ScrapbookLayoutDocs = [ + createMessagePlaceholder({ + message: 'To create a scrapbook from existing documents, marquee select. For existing scrapbook arrangements, select a preset from the dropdown.', + type: DocumentType.RTF, + width: 250, + height: 200, + x: 0, + y: 0, + }), + ]; + + const placeholder1 = createMessagePlaceholder({ acceptTags: ['PERSON'], type: DocumentType.IMG, width: 250, height: 200, x: 0, y: -100 }); + const placeholder2 = createMessagePlaceholder({ acceptTags: ['lengthy description'], type: DocumentType.RTF, width: 250, height: undefined, x: 0, y: 200 }); + const placeholder3 = createMessagePlaceholder({ acceptTags: ['title'], type: DocumentType.RTF, width: 50, height: 200, x: 280, y: -50 }); + const placeholder4 = createPlaceholder( { width: 100, height: 200, x: -200, y: -100 }, Docs.Create.StackingDocument([ + createMessagePlaceholder({ acceptTags: ['LANDSCAPE'], type: DocumentType.IMG, width: 50, height: 100, x: 0, y: -100 }) + ], { _width: 300, _height: 300, title: 'internal coll' })); // prettier-ignore + console.log('UNUSED', placeholder4, placeholder3, placeholder2, placeholder1); + /* note-to-self + would doing: + const collection = Docs.Create.ScrapbookDocument([placeholder, placeholder2, placeholder3]); + create issues with references to the same object? */ + + /*note-to-self + Should we consider that there are more collections than just COL type collections? + when spreading */ + + /*note-to-self + difference between passing a new List<Doc> versus just the raw array? */ + }; + + selectPreset = action((presetName: string) => (this.ScrapbookLayoutDocs = buildPlaceholdersFromConfigs(createPreset(presetName)))); componentDidMount() { - this.setTitle(); + const title = `Scrapbook - ${this.createdDate}`; + if (!this.ScrapbookLayoutDocs.length) this.setDefaultPlaceholder(); + if (!this.BackgroundDoc) this.generateAiImage(this.regenPrompt); + if (this.dataDoc.title !== title) this.dataDoc.title = title; // ensure title is set + + this._disposers.propagateResize = reaction( + () => ({ w: this.layoutDoc._width, h: this.layoutDoc._height }), + (dims, prev) => { + const imageBox = this._imageBoxRef.current; + // prev is undefined on the first run + if (prev && SnappingManager.ShiftKey && this.BackgroundDoc && imageBox) { + this.BackgroundDoc[imageBox.fieldKey + '_outpaintOriginalWidth'] = prev.w; + this.BackgroundDoc[imageBox.fieldKey + '_outpaintOriginalHeight'] = prev.h; + imageBox.layoutDoc._width = dims.w; + imageBox.layoutDoc._height = dims.h; + } + } + ); } - childRejectDrop = (de: DragManager.DropEvent, subView?: DocumentView) => { - return true; // disable dropping documents onto any child of the scrapbook. - }; - rejectDrop = (de: DragManager.DropEvent, subView?: DocumentView) => { - // Test to see if the dropped doc is dropped on an acceptable location (anywerhe? on a specific box). - // const draggedDocs = de.complete.docDragData?.draggedDocuments; - return false; // allow all Docs to be dropped onto scrapbook -- let filterAddDocument make the final decision. + isOutpaintable = returnTrue; + showBorderRounding = returnTrue; + + @action + generateAiImage = (prompt: string) => { + this._loading = true; + + const ratio = NumCast(this.layoutDoc._width, 1) / NumCast(this.layoutDoc._height, 1); // Measure the scrapbook’s current aspect + const choosePresetForDimensions = (() => { // Pick the Firefly preset that best matches the aspect ratio + if (ratio > AspectRatioLimits[FireflyImageDimensions.Widescreen]) return FireflyImageDimensions.Widescreen; + if (ratio > AspectRatioLimits[FireflyImageDimensions.Landscape]) return FireflyImageDimensions.Landscape; + if (ratio < AspectRatioLimits[FireflyImageDimensions.Portrait]) return FireflyImageDimensions.Portrait; + return FireflyImageDimensions.Square; + })(); // prettier-ignore + + SmartDrawHandler.CreateWithFirefly(prompt, choosePresetForDimensions) // Call exactly the same CreateWithFirefly that ImageBox uses + .then(action(doc => { + if (doc instanceof Doc) { + this.BackgroundDoc = doc; // set the background image directly on the scrapbook + } else { + alert('Failed to generate document.'); + } + })) + .catch(e => alert(`Generation error: ${e}`)) + .finally(action(() => (this._loading = false))); // prettier-ignore }; - filterAddDocument = (docIn: Doc | Doc[]) => { - const docs = toList(docIn); - if (docs?.length === 1) { - const placeholder = DocListCast(this.dataDoc[this.fieldKey]).find(d => - (d.accepts_docType === docs[0].$type || // match fields based on type, or by analyzing content .. simple example of matching text in placeholder to dropped doc's type - RTFCast(d[Doc.LayoutDataKey(d)])?.Text.includes(StrCast(docs[0].$type))) - ); // prettier-ignore - - if (placeholder) { - // ugh. we have to tell the underlying view not to add the Doc so that we can add it where we want it. - // However, returning 'false' triggers an undo. so this settimeout is needed to make the assignment happen after the undo. - setTimeout( - undoable(() => { - //StrListCast(placeholder.overrideFields).map(field => (docs[0][field] = placeholder[field])); // // shouldn't need to do this for layout fields since the placeholder already overrides its protos - placeholder.proto = docs[0]; - }, 'Scrapbook add') - ); - return false; - } - } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + childRejectDrop = (de: DragManager.DropEvent, subView?: DocumentView) => true; // disable dropping documents onto any child of the scrapbook. + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + rejectDrop = (de: DragManager.DropEvent, subView?: DocumentView) => false; // allow all Docs to be dropped onto scrapbook -- let filterAddDocument make the final decision. + + /** + * Filter function to determine if a document can be added to the scrapbook. + * This checks if the document matches any of the placeholder slots in the scrapbook. + * @param docs - The document(s) being added to the scrapbook. + * @returns true if the document can be added, false otherwise. + */ + filterAddDocument = (docs: Doc | Doc[]) => { + toList(docs).forEach(doc => slotRealDocIntoPlaceholders(doc, DocUtils.unwrapPlaceholders(this.ScrapbookLayoutDocs))); return false; }; + @computed get regenPrompt() { + const allDocs = DocUtils.unwrapPlaceholders(this.ScrapbookLayoutDocs); // find all non-collections in scrapbook (e.g., placeholder content docs) + const internalTagsSet = new Set<string>(allDocs.flatMap(doc => StrListCast(doc.$tags_chat).filter(tag => !tag.startsWith?.('ASPECT_')))); + const internalTags = Array.from(internalTagsSet).join(', '); + + return internalTags ? `Create a new scrapbook background featuring: ${internalTags}` : 'A serene mountain landscape at sunrise, ultra-wide, pastel sky, abstract, scrapbook background'; + } + render() { return ( - <div style={{ background: 'beige', width: '100%', height: '100%' }}> - <CollectionView - {...this._props} // - setContentViewBox={emptyFunction} - rejectDrop={this.rejectDrop} - childRejectDrop={this.childRejectDrop} - filterAddDocument={this.filterAddDocument} - /> - {/* <div style={{ border: '1px black', borderStyle: 'dotted', position: 'absolute', top: '50%', width: '100%', textAlign: 'center' }}>Drop an image here</div> */} + <div className="scrapbook-box"> + <div style={{ display: this._loading ? undefined : 'none' }} className="scrapbook-box-loading-overlay"> + <ReactLoading type="spin" width={50} height={50} /> + </div> + + {this.BackgroundDoc && <ImageBox ref={this._imageBoxRef} {...this._props} Document={this.BackgroundDoc} fieldKey="data" />} + <div style={{ display: this._props.isContentActive() ? 'flex' : 'none', alignItems: 'center', justifyContent: 'space-between', padding: '0 10px' }}> + <select className="scrapbook-box-preset-select" value={this._selectedPreset} onChange={action(e => this.selectPreset((this._selectedPreset = e.currentTarget.value)))}> + {getPresetNames().map(name => ( + <option key={name} value={name}> + {name} + </option> + ))} + </select> + <div className="scrapbook-box-ui" style={{ opacity: this._loading ? 0.5 : 1 }}> + <IconButton size={Size.SMALL} tooltip="regenerate a new background" label="back-ground" icon={<FontAwesomeIcon icon="redo-alt" size="sm" />} onClick={() => !this._loading && this.generateAiImage(this.regenPrompt)} /> + </div> + </div> + + <CollectionView {...this._props} setContentViewBox={emptyFunction} rejectDrop={this.rejectDrop} childRejectDrop={this.childRejectDrop} filterAddDocument={this.filterAddDocument} /> </div> ); } } -// Register scrapbook Docs.Prototypes.TemplateMap.set(DocumentType.SCRAPBOOK, { layout: { view: ScrapbookBox, dataField: 'items' }, options: { diff --git a/src/client/views/nodes/scrapbook/ScrapbookContent.tsx b/src/client/views/nodes/scrapbook/ScrapbookContent.tsx deleted file mode 100644 index ad1d308e8..000000000 --- a/src/client/views/nodes/scrapbook/ScrapbookContent.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from "react"; -import { observer } from "mobx-react-lite"; -// Import the Doc type from your actual module. -import { Doc } from "../../../../fields/Doc"; - -export interface ScrapbookContentProps { - doc: Doc; -} - -// A simple view that displays a document's title and content. -// Adjust how you extract the text if your Doc fields are objects. -export const ScrapbookContent: React.FC<ScrapbookContentProps> = observer(({ doc }) => { - // If doc.title or doc.content are not plain strings, convert them. - const titleText = doc.title ? doc.title.toString() : "Untitled"; - const contentText = doc.content ? doc.content.toString() : "No content available."; - - return ( - <div className="scrapbook-content"> - <h3>{titleText}</h3> - <p>{contentText}</p> - </div> - ); -}); diff --git a/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx new file mode 100644 index 000000000..a3405083b --- /dev/null +++ b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx @@ -0,0 +1,94 @@ +import { DocumentType } from '../../../documents/DocumentTypes'; + +export enum ScrapbookPresetType { + None = 'None', + Collage = 'Collage', + Spotlight = 'Spotlight', + Gallery = 'Gallery', + Default = 'Default', + Classic = 'Classic', +} + +export interface ScrapbookItemConfig { + x: number; + y: number; + + message?: string; // optional text to display instead of [placeholder] + acceptTags[0] + type?: DocumentType; + /** what this slot actually accepts (defaults to `tag`) */ + acceptTags?: string[]; + /** the frame this placeholder occupies */ + width?: number; + height?: number; + /** if this is a container with children, use these for the proto’s own size */ + containerWidth?: number; + containerHeight?: number; + children?: ScrapbookItemConfig[]; +} + +export class ScrapbookPreset { + static createPreset(presetType: ScrapbookPresetType): ScrapbookItemConfig[] { + switch (presetType) { + case ScrapbookPresetType.None: return ScrapbookPreset.createNonePreset(); + case ScrapbookPresetType.Classic: return ScrapbookPreset.createClassicPreset(); + case ScrapbookPresetType.Collage: return ScrapbookPreset.createCollagePreset(); + case ScrapbookPresetType.Spotlight: return ScrapbookPreset.createSpotlightPreset(); + case ScrapbookPresetType.Default: return ScrapbookPreset.createDefaultPreset(); + case ScrapbookPresetType.Gallery: return ScrapbookPreset.createGalleryPreset(); + default: + throw new Error(`Unknown preset type: ${presetType}`); + } // prettier-ignore + } + + private static createNonePreset(): ScrapbookItemConfig[] { + return [{ message: 'To create a scrapbook from existing documents, marquee select. For existing scrapbook arrangements, select a preset from the dropdown.', type: DocumentType.RTF, acceptTags: [], x: 0, y: 0, width: 250, height: 200 }]; + } + + private static createClassicPreset(): ScrapbookItemConfig[] { + return [ + { type: DocumentType.IMG, message: '[placeholder] landscape', acceptTags: ['LANDSCAPE'], x: 0, y: -100, width: 250, height: 200 }, + { type: DocumentType.RTF, message: '[placeholder] lengthy caption', acceptTags: ['paragraphs'], x: 0, y: 138, width: 250, height: 172 }, + { type: DocumentType.RTF, message: '[placeholder] brief description', acceptTags: ['sentence'], x: 280, y: -50, width: 50, height: 200 }, + { type: DocumentType.IMG, message: '[placeholder] person', acceptTags: ['PERSON'], x: -200, y: -100, width: 167, height: 200 }, + ]; + } + + private static createGalleryPreset(): ScrapbookItemConfig[] { + return [ + { type: DocumentType.IMG, message: 'Gallery 1 <drop person images into the gallery!>', acceptTags: ['PERSON'], x: -150, y: -150, width: 150, height: 150 }, + { type: DocumentType.IMG, message: 'Gallery 2', acceptTags: ['PERSON'], x: 0, y: -150, width: 150, height: 150 }, + { type: DocumentType.IMG, message: 'Gallery 3', acceptTags: ['PERSON'], x: 150, y: -150, width: 150, height: 150 }, + { type: DocumentType.IMG, message: 'Gallery 4', acceptTags: ['PERSON'], x: -150, y: 0, width: 150, height: 150 }, + { type: DocumentType.IMG, message: 'Gallery 5', acceptTags: ['PERSON'], x: 0, y: 0, width: 150, height: 150 }, + { type: DocumentType.IMG, message: 'Gallery 6', acceptTags: ['PERSON'], x: 150, y: 0, width: 150, height: 150 }, + ]; + } + + private static createDefaultPreset(): ScrapbookItemConfig[] { + return [ + { type: DocumentType.IMG, message: 'drop a landscape image', acceptTags: ['LANDSCAPE'], x: 44, y: -50, width: 200, height: 120 }, + { type: DocumentType.PDF, message: 'summary pdf', acceptTags: ['word', 'sentence', 'paragraphs'], x: 45, y: 93, width: 184, height: 273 }, + { type: DocumentType.RTF, message: 'sidebar text', acceptTags: ['paragraphs'], x: 250, y: -50, width: 100, height: 200 }, + { containerWidth: 200, containerHeight: 425, x: -171, y: -54, width: 200, height: 425, + children: [{ type: DocumentType.IMG, message: 'drop a person image', acceptTags: ['PERSON'], x: -350, y: 200, width: 162, height: 137 }], }, + ]; // prettier-ignore + } + + private static createCollagePreset(): ScrapbookItemConfig[] { + return [ + { type: DocumentType.IMG, message: 'landscape image', acceptTags: ['LANDSCAPE'], x: -174, y: 100, width: 160, height: 150 }, + { type: DocumentType.IMG, message: 'person image', acceptTags: ['PERSON'], x: 0, y: 100, width: 150, height: 150 }, + { type: DocumentType.RTF, message: 'caption', acceptTags: ['sentence'], x: -174, y: 50, width: 150, height: 40 }, + { type: DocumentType.RTF, message: 'caption', acceptTags: ['sentence'], x: 0, y: 50, width: 150, height: 40 }, + { type: DocumentType.RTF, message: 'lengthy description', acceptTags: ['paragraphs'], x: -180, y: -60, width: 350, height: 100 }, + ]; // prettier-ignore + } + + private static createSpotlightPreset(): ScrapbookItemConfig[] { + return [ + { type: DocumentType.RTF, message: 'title text', acceptTags: ['word'], x: 0, y: -30, width: 300, height: 40 }, + { type: DocumentType.IMG, message: 'drop a landscape image', acceptTags: ['LANDSCAPE'], x: 0, y: 20, width: 300, height: 200 }, + { type: DocumentType.RTF, message: 'caption text', acceptTags: ['sentence'], x: 0, y: 230, width: 300, height: 50 }, + ]; + } +} diff --git a/src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts b/src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts new file mode 100644 index 000000000..3a2189d00 --- /dev/null +++ b/src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts @@ -0,0 +1,36 @@ +import { ScrapbookItemConfig } from './ScrapbookPreset'; +import { ScrapbookPresetType } from './ScrapbookPreset'; + +type PresetGenerator = () => ScrapbookItemConfig[]; + +// Internal map of preset name to generator +const presetRegistry = new Map<string, PresetGenerator>(); + +/** + * Register a new scrapbook preset under the given name. + */ +export function registerPreset(name: string, gen: PresetGenerator) { + presetRegistry.set(name, gen); +} + +/** + * List all registered preset names. + */ +export function getPresetNames(): string[] { + return Array.from(presetRegistry.keys()); +} + +/** + * Create the config array for the named preset. + */ +export function createPreset(name: string): ScrapbookItemConfig[] { + const gen = presetRegistry.get(name); + if (!gen) throw new Error(`Unknown scrapbook preset: ${name}`); + return gen(); +} + +// ------------------------ +// Register built-in presets +import { ScrapbookPreset } from './ScrapbookPreset'; + +Object.keys(ScrapbookPresetType).forEach(key => registerPreset(key, () => ScrapbookPreset.createPreset(key as ScrapbookPresetType))); // pretter-ignore diff --git a/src/client/views/nodes/scrapbook/ScrapbookSlot.scss b/src/client/views/nodes/scrapbook/ScrapbookSlot.scss deleted file mode 100644 index ae647ad36..000000000 --- a/src/client/views/nodes/scrapbook/ScrapbookSlot.scss +++ /dev/null @@ -1,85 +0,0 @@ -//IGNORE FOR NOW, CURRENTLY NOT USED IN SCRAPBOOK IMPLEMENTATION -.scrapbook-slot { - position: absolute; - background-color: rgba(245, 245, 245, 0.7); - border: 2px dashed #ccc; - border-radius: 5px; - box-sizing: border-box; - transition: all 0.2s ease; - overflow: hidden; - - &.scrapbook-slot-over { - border-color: #4a90e2; - background-color: rgba(74, 144, 226, 0.1); - } - - &.scrapbook-slot-filled { - border-style: solid; - border-color: rgba(0, 0, 0, 0.1); - background-color: transparent; - - &.scrapbook-slot-over { - border-color: #4a90e2; - background-color: rgba(74, 144, 226, 0.1); - } - } - - .scrapbook-slot-empty { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - } - - .scrapbook-slot-placeholder { - text-align: center; - color: #888; - } - - .scrapbook-slot-title { - font-weight: bold; - margin-bottom: 5px; - } - - .scrapbook-slot-instruction { - font-size: 0.9em; - font-style: italic; - } - - .scrapbook-slot-content { - width: 100%; - height: 100%; - position: relative; - } - - .scrapbook-slot-controls { - position: absolute; - top: 5px; - right: 5px; - z-index: 10; - opacity: 0; - transition: opacity 0.2s ease; - - .scrapbook-slot-remove-btn { - background-color: rgba(255, 255, 255, 0.8); - border: 1px solid #ccc; - border-radius: 50%; - width: 20px; - height: 20px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - font-size: 10px; - - &:hover { - background-color: rgba(255, 0, 0, 0.1); - } - } - } - - &:hover .scrapbook-slot-controls { - opacity: 1; - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/scrapbook/ScrapbookSlot.tsx b/src/client/views/nodes/scrapbook/ScrapbookSlot.tsx deleted file mode 100644 index 2c8f93778..000000000 --- a/src/client/views/nodes/scrapbook/ScrapbookSlot.tsx +++ /dev/null @@ -1,28 +0,0 @@ - -//IGNORE FOR NOW, CURRENTLY NOT USED IN SCRAPBOOK IMPLEMENTATION -export interface SlotDefinition { - id: string; - x: number; y: number; - defaultWidth: number; - defaultHeight: number; - } - - export interface SlotContentMap { - slotId: string; - docId?: string; - } - - export interface ScrapbookConfig { - slots: SlotDefinition[]; - contents?: SlotContentMap[]; - } - - export const DEFAULT_SCRAPBOOK_CONFIG: ScrapbookConfig = { - slots: [ - { id: "slot1", x: 10, y: 10, defaultWidth: 180, defaultHeight: 120 }, - { id: "slot2", x: 200, y: 10, defaultWidth: 180, defaultHeight: 120 }, - // …etc - ], - contents: [] - }; -
\ No newline at end of file diff --git a/src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts b/src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts deleted file mode 100644 index 686917d9a..000000000 --- a/src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts +++ /dev/null @@ -1,25 +0,0 @@ -// ScrapbookSlotTypes.ts -export interface SlotDefinition { - id: string; - title: string; - x: number; - y: number; - defaultWidth: number; - defaultHeight: number; - } - - export interface ScrapbookConfig { - slots: SlotDefinition[]; - contents?: { slotId: string; docId: string }[]; - } - - // give it three slots by default: - export const DEFAULT_SCRAPBOOK_CONFIG: ScrapbookConfig = { - slots: [ - { id: "main", title: "Main Content", x: 20, y: 20, defaultWidth: 360, defaultHeight: 200 }, - { id: "notes", title: "Notes", x: 20, y: 240, defaultWidth: 360, defaultHeight: 160 }, - { id: "resources", title: "Resources", x: 400, y: 20, defaultWidth: 320, defaultHeight: 380 }, - ], - contents: [], - }; -
\ No newline at end of file diff --git a/src/client/views/nodes/trails/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss index e24b47bd1..8875f7012 100644 --- a/src/client/views/nodes/trails/PresBox.scss +++ b/src/client/views/nodes/trails/PresBox.scss @@ -229,7 +229,7 @@ .toolbar-transition { display: flex; font-size: 10; - width: 100; + width: 100px; background-color: rgba(0, 0, 0, 0); min-width: max-content; @@ -358,7 +358,7 @@ font-weight: 200; padding: 8px; border-radius: 4px; - // height: 20; + // height: 20px; // display: flex; // margin-left: 5px; // margin-top: 5px; @@ -371,8 +371,8 @@ } .ribbon-propertyUpDown { - height: 20; - width: 20; + height: 20px; + width: 20px; margin-top: 5px; display: grid; grid-template-rows: 10px 10px; @@ -486,7 +486,7 @@ -moz-user-select: none; -ms-user-select: none; user-select: none; - margin: 0; + margin: 0px; margin-right: 3px; border-radius: 100%; height: 15px; @@ -658,9 +658,9 @@ height: 25px; color: white; width: 100%; - max-width: 120; - padding-left: 10; - padding-right: 10; + max-width: 120px; + padding-left: 10px; + padding-right: 10px; border-radius: 10px; background-color: global.$medium-gray; } @@ -683,9 +683,9 @@ height: 25px; color: global.$light-gray; width: 100%; - max-width: 120; - padding-left: 10; - padding-right: 10; + max-width: 120px; + padding-left: 10px; + padding-right: 10px; border-radius: 10px; background-color: global.$black; } @@ -745,9 +745,9 @@ .selectedList { display: block; - min-width: 50; - max-width: 120; - height: 70; + min-width: 50px; + max-width: 120px; + height: 70px; overflow-y: scroll; .selectedList-items { @@ -760,7 +760,7 @@ cursor: pointer; font-size: 10.5; font-weight: 300; - height: 20; + height: 20px; background-color: global.$medium-gray; color: white; display: flex; @@ -788,7 +788,7 @@ cursor: pointer; font-size: 10.5; font-weight: 200; - height: 20; + height: 20px; background-color: global.$white; display: inline-flex; color: global.$black; @@ -813,7 +813,7 @@ } svg.svg-inline--fa.fa-thumbtack.fa-w-12.toolbar-thumbtack { - right: 40; + right: 40px; position: absolute; transform: rotate(45deg); } @@ -850,7 +850,7 @@ background-color: global.$light-gray; border-radius: 5px; font-size: 10; - height: 25; + height: 25px; color: global.$black; padding-left: 5px; align-items: center; @@ -868,8 +868,8 @@ display: block; padding-left: 10px; padding-right: 5px; - padding-top: 3; - padding-bottom: 3; + padding-top: 3px; + padding-bottom: 3px; opacity: 0.8; } @@ -959,8 +959,8 @@ border-radius: 4px; padding-left: 7px; padding-right: 7px; - border-bottom-right-radius: 0; - border-top-right-radius: 0; + border-bottom-right-radius: 0px; + border-top-right-radius: 0px; } .presBox-button-right { @@ -976,8 +976,8 @@ border-radius: 4px; padding-left: 7px; padding-right: 7px; - border-bottom-left-radius: 0; - border-top-left-radius: 0; + border-bottom-left-radius: 0px; + border-top-left-radius: 0px; } .presBox-button-right.active { @@ -1043,8 +1043,8 @@ cursor: pointer; align-self: center; justify-self: center; - margin-top: 5; - margin-bottom: 5; + margin-top: 5px; + margin-bottom: 5px; position: relative; height: 55px; min-width: 90px; @@ -1063,7 +1063,7 @@ padding-left: 3px; margin-left: 3px; margin-right: 3px; - height: 13; + height: 13px; font-size: 12; display: flex; background-color: global.$white; @@ -1076,7 +1076,7 @@ margin-left: 3px; margin-right: 3px; font-weight: 400; - height: 13; + height: 13px; font-size: 9; display: flex; background-color: global.$white; @@ -1089,11 +1089,11 @@ padding-left: 3px; margin-left: 3px; margin-right: 3px; - height: 13; + height: 13px; font-size: 10; display: flex; background-color: global.$white; - height: 33; + height: 33px; text-align: left; font-size: 8px; } @@ -1112,7 +1112,7 @@ .presBox-viewPicker { cursor: pointer; - height: 25; + height: 25px; position: relative; display: inline-block; grid-column: 1; @@ -1184,9 +1184,9 @@ } .collectionViewBaseChrome-viewPicker { - min-width: 50; + min-width: 50px; width: 5%; - height: 25; + height: 25px; position: relative; display: inline-block; left: 8px; @@ -1210,11 +1210,11 @@ } .presBox-backward { - left: 5; + left: 5px; } .presBox-forward { - right: 5; + right: 5px; } // CSS adjusted for mobile devices @@ -1233,8 +1233,8 @@ .presBox-button { margin-top: 5%; - height: 250; - width: 300; + height: 250px; + width: 300px; font-size: 100; display: flex; align-items: center; @@ -1256,7 +1256,7 @@ } .presBox-cont .presBox-listCont { - top: 50; + top: 50px; height: calc(100% - 80px); } @@ -1269,8 +1269,8 @@ .miniPres { cursor: grab; position: absolute; - top: 0; - left: 0; + top: 0px; + left: 0px; opacity: 0.5; transition: all 0.4s; color: global.$white; @@ -1298,7 +1298,7 @@ .presPanel-button-text { cursor: pointer; display: flex; - height: 20; + height: 20px; width: max-content; font-family: Roboto; font-weight: 400; @@ -1327,8 +1327,8 @@ grid-template-columns: auto auto auto; justify-content: space-around; font-size: 11; - margin-left: 7; - width: 30; + margin-left: 7px; + width: 30px; height: 85%; background-color: rgba(91, 157, 221, 0.4); border-radius: 5px; @@ -1337,8 +1337,8 @@ .presPanel-button { cursor: pointer; display: flex; - height: 20; - min-width: 20; + height: 20px; + min-width: 20px; margin-left: 3px; margin-right: 3px; border-radius: 100%; @@ -1361,8 +1361,8 @@ // cursor: grab; // position: absolute; // overflow: hidden; -// right: 10; -// top: 10; +// right: 10px; +// top: 10px; // opacity: 0.1; // transition: all 0.4s; // /* border: solid 1px; */ @@ -1380,7 +1380,7 @@ // .miniPres-button-text { // cursor: pointer; // display: flex; -// height: 20; +// height: 20px; // font-weight: 400; // min-width: 100%; // border-radius: 5px; @@ -1397,8 +1397,8 @@ // grid-template-columns: auto auto auto; // justify-content: space-around; // font-size: 11; -// margin-left: 7; -// width: 30; +// margin-left: 7px; +// width: 30px; // height: 85%; // background-color: rgba(91, 157, 221, 0.4); // border-radius: 5px; @@ -1413,8 +1413,8 @@ // .miniPres-button { // cursor: pointer; // display: flex; -// height: 20; -// min-width: 20; +// height: 20px; +// min-width: 20px; // border-radius: 100%; // align-items: center; // justify-content: center; diff --git a/src/client/views/nodes/trails/PresSlideBox.scss b/src/client/views/nodes/trails/PresSlideBox.scss index 9ac2b5a94..740bcae5f 100644 --- a/src/client/views/nodes/trails/PresSlideBox.scss +++ b/src/client/views/nodes/trails/PresSlideBox.scss @@ -117,8 +117,8 @@ $slide-active: #5b9fdd; height: 100%; position: absolute; border-radius: 3px; - top: 0; - left: 0; + top: 0px; + left: 0px; z-index: 1; overflow: hidden; } @@ -209,7 +209,7 @@ $slide-active: #5b9fdd; position: absolute; /* grid-row: 3; */ /* grid-column: 1/8; */ - top: 28; + top: 28px; display: block; background: #92adb9; width: 100%; @@ -276,7 +276,7 @@ $slide-active: #5b9fdd; cursor: pointer; position: absolute; border-radius: 100px; - bottom: 0; + bottom: 0px; left: -18; z-index: 300; width: 15px; @@ -302,7 +302,7 @@ $slide-active: #5b9fdd; color: #d5dce2; position: absolute; left: -15px; - top: 1; + top: 1px; font-weight: 600; font-size: 12; } |
