From b440901843a930c6c87ec23c59f90f1349c25b50 Mon Sep 17 00:00:00 2001 From: IEatChili Date: Thu, 6 Jun 2024 15:23:12 -0400 Subject: feat: updated ui --- src/client/documents/DocumentTypes.ts | 1 + src/client/documents/Documents.ts | 4 + src/client/util/CurrentUserUtils.ts | 7 + src/client/views/Main.tsx | 2 + src/client/views/MainView.tsx | 1 + .../collectionFreeForm/ImageLabelBox.scss | 26 ++++ .../collectionFreeForm/ImageLabelBox.tsx | 150 +++++++++++++++++++++ .../collectionFreeForm/ImageLabelHandler.tsx | 2 +- .../collections/collectionFreeForm/MarqueeView.tsx | 56 +++++--- src/client/views/linking/LinkPopup.tsx | 1 - src/fields/Doc.ts | 1 + 11 files changed, 230 insertions(+), 21 deletions(-) create mode 100644 src/client/views/collections/collectionFreeForm/ImageLabelBox.scss create mode 100644 src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx (limited to 'src') diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 8f95068db..a9ea889b3 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -16,6 +16,7 @@ export enum DocumentType { SCREENSHOT = 'screenshot', FONTICON = 'fonticonbox', SEARCH = 'search', // search query + IMAGEGROUPER = 'imagegrouper', LABEL = 'label', // simple text label BUTTON = 'button', // onClick button WEBCAM = 'webcam', // webcam diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index a67e6b4f6..449347403 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -784,6 +784,10 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.SEARCH), new List([]), options); } + export function ImageGrouperDocument(options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.IMAGEGROUPER), undefined, options); + } + export function LoadingDocument(file: File | string, options: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.LOADING), undefined, { _height: 150, _width: 200, title: typeof file === 'string' ? file : file.name, ...options }, undefined, ''); } diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index e095bc659..486b6815a 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -458,6 +458,7 @@ pie title Minerals in my tap water { title: "Shared", toolTip: "Shared Docs", target: Doc.MySharedDocs, ignoreClick: true, icon: "users", funcs: {badgeValue: badgeValue}}, { title: "Trails", toolTip: "Trails ⌘R", target: Doc.UserDoc(), ignoreClick: true, icon: "pres-trail", funcs: {target: getActiveDashTrails}}, { title: "User Doc", toolTip: "User Doc", target: this.setupUserDocView(doc, "myUserDocView"), ignoreClick: true, icon: "address-card",funcs: {hidden: "IsNoviceMode()"} }, + { title: "Image Grouper", toolTip: "Image Grouper", target: this.setupImageGrouper(doc, "myImageGrouper"), ignoreClick: true, icon: "folder-open", hidden: true } ].map(tuple => ({...tuple, scripts:{onClick: 'selectMainMenu(this)'}})); } @@ -493,6 +494,12 @@ pie title Minerals in my tap water _lockedPosition: true, _type_collection: CollectionViewType.Schema }); } + static setupImageGrouper(doc: Doc, field: string) { + return DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.ImageGrouperDocument(opts), { + dontRegisterView: true, backgroundColor: "dimgray", ignoreClick: true, title: "Image Grouper", isSystem: true, childDragAction: dropActionType.embed, + _lockedPosition: true, _type_collection: CollectionViewType.Schema }); + } + /// Initializes the panel of draggable tools that is opened from the left sidebar. static setupToolsBtnPanel(doc: Doc, field:string) { const allTools = DocListCast(DocCast(doc[field])?.data); diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 43b9a6b39..8242e7c27 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -60,6 +60,7 @@ import { SummaryView } from './nodes/formattedText/SummaryView'; import { ImportElementBox } from './nodes/importBox/ImportElementBox'; import { PresBox, PresElementBox } from './nodes/trails'; import { SearchBox } from './search/SearchBox'; +import { ImageLabelBox } from './collections/collectionFreeForm/ImageLabelBox'; dotenv.config(); @@ -131,6 +132,7 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' }; PresBox, PresElementBox, SearchBox, + ImageLabelBox, //Here! FunctionPlotBox, InkingStroke, LinkBox, diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 31d88fb87..5b4c2b5ba 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -76,6 +76,7 @@ import { PresBox } from './nodes/trails'; import { AnchorMenu } from './pdf/AnchorMenu'; import { GPTPopup } from './pdf/GPTPopup/GPTPopup'; import { TopBar } from './topbar/TopBar'; +import { ImageLabelBox } from './collections/collectionFreeForm/ImageLabelBox'; const { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } = require('./global/globalCssVariables.module.scss'); // prettier-ignore const _global = (window /* browser */ || global) /* node */ as any; diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.scss b/src/client/views/collections/collectionFreeForm/ImageLabelBox.scss new file mode 100644 index 000000000..d0c12814c --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.scss @@ -0,0 +1,26 @@ +.image-label-list { + display: flex; + flex-direction: column; + align-items: center; // Centers the content vertically in the flex container + width: 100%; + + > div { + display: flex; + justify-content: space-between; // Puts the content and delete button on opposite ends + align-items: center; + width: 100%; + margin-top: 8px; // Adds space between label rows + background-color: black; + + p { + text-align: center; // Centers the text of the paragraph + font-size: large; + vertical-align: middle; + } + + .IconButton { + // Styling for the delete button + margin-left: auto; // Pushes the button to the far right + } + } +} diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx new file mode 100644 index 000000000..1c0035f0d --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx @@ -0,0 +1,150 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Colors, IconButton } from 'browndash-components'; +import { action, makeObservable, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import React from 'react'; +import { Doc } from '../../../../fields/Doc'; +import { Docs } from '../../../documents/Documents'; +import { DocumentType } from '../../../documents/DocumentTypes'; +import { ViewBoxBaseComponent } from '../../DocComponent'; +import { FieldView, FieldViewProps } from '../../nodes/FieldView'; +import { MarqueeOptionsMenu } from './MarqueeOptionsMenu'; +import './ImageLabelBox.scss'; +import { MainView } from '../../MainView'; +import { MarqueeView } from './MarqueeView'; + +@observer +export class ImageLabelBox extends ViewBoxBaseComponent() { + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(ImageLabelBox, fieldKey); + } + + public static Instance: ImageLabelBox | null = null; + private _inputRef = React.createRef(); + @observable _loading: boolean = true; + @observable _currentLabel: string = ''; + @observable _labelGroups: string[] = []; + + constructor(props: any) { + super(props); + makeObservable(this); + ImageLabelBox.Instance = this; + + console.log('Image Box Has Been Initialized'); + } + + /** + * This method is called when the SearchBox component is first mounted. When the user opens + * the search panel, the search input box is automatically selected. This allows the user to + * type in the search input box immediately, without needing clicking on it first. + */ + componentDidMount() { + // if (this._inputRef.current) { + // this._inputRef.current.focus(); + // } + } + + @action + addLabel = (label: string) => { + label = label.toUpperCase().trim(); + if (label.length > 0) { + if (!this._labelGroups.includes(label)) { + this._labelGroups = [...this._labelGroups, label]; + } + } + }; + + @action + removeLabel = (label: string) => { + const labelUp = label.toUpperCase(); + this._labelGroups = this._labelGroups.filter(group => group !== labelUp); + }; + + @action + groupImages = () => { + MarqueeOptionsMenu.Instance.groupImages(); + this._labelGroups = []; + MainView.Instance.closeFlyout(); + }; + + @action + startLoading = () => { + this._loading = true; + }; + + @action + endLoading = () => { + this._loading = false; + }; + + render() { + if (this._loading) { + return
Loading...
; + } + + return ( +
+
+ { + // e.key === 'Enter' ? this.submitSearch() : null; + // e.stopPropagation(); + // }} + type="text" + placeholder="Input a group to put images into..." + aria-label="label-input" + id="new-label" + className="searchBox-input" + style={{ width: '100%', borderRadius: '5px' }} + ref={this._inputRef} + /> + { + const input = document.getElementById('new-label') as HTMLInputElement; + const newLabel = input.value; + this.addLabel(newLabel); + this._currentLabel = ''; + input.value = ''; + }} + icon={} + color={MarqueeOptionsMenu.Instance.userColor} + style={{ width: '19px' }} + /> + {this._labelGroups.length > 0 ? ( + } color={MarqueeOptionsMenu.Instance.userColor} style={{ width: '19px' }} /> + ) : ( +
+ )} +
+
+
+ {this._labelGroups.map(group => { + return ( +
+

{group}

+ { + this.removeLabel(group); + }} + icon={'x'} + color={MarqueeOptionsMenu.Instance.userColor} + style={{ width: '8px' }} + /> +
+ ); + })} +
+
+
+ ); + } +} + +Docs.Prototypes.TemplateMap.set(DocumentType.IMAGEGROUPER, { + layout: { view: ImageLabelBox, dataField: 'data' }, + options: { acl: '', _width: 400 }, +}); diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelHandler.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelHandler.tsx index 7f27c6b5c..73befb205 100644 --- a/src/client/views/collections/collectionFreeForm/ImageLabelHandler.tsx +++ b/src/client/views/collections/collectionFreeForm/ImageLabelHandler.tsx @@ -77,7 +77,7 @@ export class ImageLabelHandler extends ObservableReactComponent<{}> { }}>
} color={MarqueeOptionsMenu.Instance.userColor} style={{ width: '19px' }} /> - + { diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index dc15c83c5..bb5a2a66e 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -5,7 +5,7 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { ClientUtils, lightOrDark, returnFalse } from '../../../../ClientUtils'; import { intersectRect, numberRange } from '../../../../Utils'; -import { Doc, NumListCast, Opt } from '../../../../fields/Doc'; +import { Doc, DocListCast, NumListCast, Opt } from '../../../../fields/Doc'; import { AclAdmin, AclAugment, AclEdit, DocData } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { InkData, InkField, InkTool } from '../../../../fields/InkField'; @@ -36,6 +36,9 @@ import { CollectionFreeFormView } from './CollectionFreeFormView'; import { ImageLabelHandler } from './ImageLabelHandler'; import { MarqueeOptionsMenu } from './MarqueeOptionsMenu'; import './MarqueeView.scss'; +import { MainView } from '../../MainView'; +import { ImageLabelBox } from './ImageLabelBox'; +import { SearchBox } from '../../search/SearchBox'; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -53,6 +56,9 @@ interface MarqueeViewProps { slowLoadDocuments: (files: File[] | string, options: DocumentOptions, generatedDocuments: Doc[], text: string, completed: ((doc: Doc[]) => void) | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => Promise; } +/** + * A component that deals with the marquee select in the freeform canvas. + */ @observer export class MarqueeView extends ObservableReactComponent { public static CurViewBounds(pinDoc: Doc, panelWidth: number, panelHeight: number) { @@ -60,9 +66,12 @@ export class MarqueeView extends ObservableReactComponent { - this._selectedDocs = this.marqueeSelect(false, DocumentType.IMG); + if (e) { + const groupButton = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MyImageGrouper); + if (groupButton) { + MainView.Instance.expandFlyout(groupButton); + while (!ImageLabelBox.Instance) { + await new Promise(resolve => setTimeout(resolve, 1000)).then(() => { + console.log('Waiting for Image Label Box'); + }); + } + ImageLabelBox.Instance.startLoading(); + } + } + + this._selectedDocs = this.marqueeSelect(false, DocumentType.IMG); // Get the selected documents from the marquee select. const imageInfos = this._selectedDocs.map(async doc => { - const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.'); - return CollectionCardView.imageUrlToBase64(`${name}_o.${type}`).then(hrefBase64 => + if (!doc[DocData].data_labels) { + const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.'); + return CollectionCardView.imageUrlToBase64(`${name}_o.${type}`).then(hrefBase64 => !hrefBase64 ? undefined : gptImageLabel(hrefBase64).then(labels => Promise.all(labels.split('\n').map(label => gptGetEmbedding(label))).then(embeddings => ({ doc, embeddings, labels }))) ); // prettier-ignore - }); + } + }); // Converts the images into a Base64 format, afterwhich the information is sent to GPT to label them. (await Promise.all(imageInfos)).forEach(imageInfo => { - if (imageInfo && Array.isArray(imageInfo.embeddings)) { + if (imageInfo && imageInfo.embeddings && Array.isArray(imageInfo.embeddings)) { imageInfo.doc[DocData].data_labels = imageInfo.labels; numberRange(3).forEach(n => { imageInfo.doc[`data_labels_embedding_${n + 1}`] = new List(imageInfo.embeddings[n]); }); } - }); + }); // Add the labels as fields to each image. - if (e) { - ImageLabelHandler.Instance.displayLabelHandler(e.pageX, e.pageY); - } + ImageLabelBox.Instance!.endLoading(); + console.log('Complete!'); }); /** @@ -464,7 +486,7 @@ export class MarqueeView extends ObservableReactComponent { - const labelGroups = ImageLabelHandler.Instance._labelGroups; + const labelGroups = ImageLabelBox.Instance!._labelGroups; const labelToEmbedding = new Map(); // Create embeddings for the labels. await Promise.all(labelGroups.map(async label => gptGetEmbedding(label).then(labelEmbedding => labelToEmbedding.set(label, labelEmbedding)))); @@ -478,14 +500,10 @@ export class MarqueeView extends ObservableReactComponent ({ label, similarityScore: bestEmbedScore(labelToEmbedding.get(label)) })) .reduce((prev, cur) => cur.similarityScore < 0.3 || cur.similarityScore <= prev.similarityScore ? prev: cur, { label: '', similarityScore: 0, }); // prettier-ignore - - numberRange(3).forEach(n => { - doc[`data_labels_embedding_${n + 1}`] = undefined; - }); - doc[DocData].data_label = mostSimilarLabelCollect; + doc[DocData].data_label = mostSimilarLabelCollect; // The label most similar to the image's contents. }); - this._props.Document._type_collection = CollectionViewType.Time; - this._props.Document.pivotField = 'data_label'; + this._props.Document._type_collection = CollectionViewType.Time; // Change the collection view to a Time view. + this._props.Document.pivotField = 'data_label'; // Sets the pivot to be the 'data_label'. }); @undoBatch diff --git a/src/client/views/linking/LinkPopup.tsx b/src/client/views/linking/LinkPopup.tsx index 76a8396ff..2405e375d 100644 --- a/src/client/views/linking/LinkPopup.tsx +++ b/src/client/views/linking/LinkPopup.tsx @@ -45,7 +45,6 @@ export class LinkPopup extends React.Component { {/* */} -