aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorIEatChili <nanunguyen99@gmail.com>2024-06-06 15:23:12 -0400
committerIEatChili <nanunguyen99@gmail.com>2024-06-06 15:23:12 -0400
commitb440901843a930c6c87ec23c59f90f1349c25b50 (patch)
tree6ee534140d7a7714204f5ebe217a0f539c112a2c /src
parent202e994515392892676f8f080852db1e32b8dbd3 (diff)
feat: updated ui
Diffstat (limited to 'src')
-rw-r--r--src/client/documents/DocumentTypes.ts1
-rw-r--r--src/client/documents/Documents.ts4
-rw-r--r--src/client/util/CurrentUserUtils.ts7
-rw-r--r--src/client/views/Main.tsx2
-rw-r--r--src/client/views/MainView.tsx1
-rw-r--r--src/client/views/collections/collectionFreeForm/ImageLabelBox.scss26
-rw-r--r--src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx150
-rw-r--r--src/client/views/collections/collectionFreeForm/ImageLabelHandler.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx56
-rw-r--r--src/client/views/linking/LinkPopup.tsx1
-rw-r--r--src/fields/Doc.ts1
11 files changed, 230 insertions, 21 deletions
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<Doc>([]), 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<FieldViewProps>() {
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(ImageLabelBox, fieldKey);
+ }
+
+ public static Instance: ImageLabelBox | null = null;
+ private _inputRef = React.createRef<HTMLInputElement>();
+ @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 <div className="searchBox-container">Loading...</div>;
+ }
+
+ return (
+ <div className="searchBox-container">
+ <div className="searchBox-bar">
+ <input
+ defaultValue=""
+ autoComplete="off"
+ // onKeyDown={e => {
+ // 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}
+ />
+ <IconButton
+ tooltip={'Add group'}
+ onPointerDown={() => {
+ const input = document.getElementById('new-label') as HTMLInputElement;
+ const newLabel = input.value;
+ this.addLabel(newLabel);
+ this._currentLabel = '';
+ input.value = '';
+ }}
+ icon={<FontAwesomeIcon icon="plus" />}
+ color={MarqueeOptionsMenu.Instance.userColor}
+ style={{ width: '19px' }}
+ />
+ {this._labelGroups.length > 0 ? (
+ <IconButton tooltip={'Group Images'} onPointerDown={this.groupImages} icon={<FontAwesomeIcon icon="object-group" />} color={MarqueeOptionsMenu.Instance.userColor} style={{ width: '19px' }} />
+ ) : (
+ <div></div>
+ )}
+ </div>
+ <div>
+ <div className="image-label-list">
+ {this._labelGroups.map(group => {
+ return (
+ <div>
+ <p style={{ color: MarqueeOptionsMenu.Instance.userColor }}>{group}</p>
+ <IconButton
+ tooltip={'Remove Label'}
+ onPointerDown={() => {
+ this.removeLabel(group);
+ }}
+ icon={'x'}
+ color={MarqueeOptionsMenu.Instance.userColor}
+ style={{ width: '8px' }}
+ />
+ </div>
+ );
+ })}
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+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<{}> {
}}>
<div>
<IconButton tooltip={'Cancel'} onPointerDown={this.hideLabelhandler} icon={<FontAwesomeIcon icon="eye-slash" />} color={MarqueeOptionsMenu.Instance.userColor} style={{ width: '19px' }} />
- <input aria-label="label-input" id="new-label" type="text" style={{ color: 'black' }} />
+ <input aria-label="label-input" id="new-label" type="text" placeholder="Input a classification" style={{ color: 'black' }} />
<IconButton
tooltip={'Add Label'}
onPointerDown={() => {
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<void>;
}
+/**
+ * A component that deals with the marquee select in the freeform canvas.
+ */
@observer
export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps & MarqueeViewProps> {
public static CurViewBounds(pinDoc: Doc, panelWidth: number, panelHeight: number) {
@@ -60,9 +66,12 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
return { left: NumCast(pinDoc._freeform_panX) - panelWidth / 2 / ps, top: NumCast(pinDoc._freeform_panY) - panelHeight / 2 / ps, width: panelWidth / ps, height: panelHeight / ps };
}
+ static Instance: MarqueeView;
+
constructor(props: any) {
super(props);
makeObservable(this);
+ MarqueeView.Instance = this;
}
private _commandExecuted = false;
@@ -430,33 +439,46 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
/**
* Classifies images and assigns the labels as document fields.
- * TODO: Turn into lists of labels instead of individual fields.
*/
@undoBatch
classifyImages = action(async (e: React.MouseEvent | undefined) => {
- 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<number>(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<SubCollectionViewProps
*/
@undoBatch
groupImages = action(async () => {
- const labelGroups = ImageLabelHandler.Instance._labelGroups;
+ const labelGroups = ImageLabelBox.Instance!._labelGroups;
const labelToEmbedding = new Map<string, number[]>();
// 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<SubCollectionViewProps
labelGroups.map(label => ({ 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<LinkPopupProps> {
{/* <i></i>
<input defaultValue={""} autoComplete="off" type="text" placeholder="Search for Document..." id="search-input"
className="linkPopup-searchBox searchBox-input" /> */}
-
<SearchBox
Document={Doc.MySearcher}
docViewPath={returnEmptyDocViewList}
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 1b3d963e8..4abb23404 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -264,6 +264,7 @@ export class Doc extends RefField {
public static get MyUserDocView() { return DocCast(Doc.UserDoc().myUserDocView); } // prettier-ignore
public static get MyDockedBtns() { return DocCast(Doc.UserDoc().myDockedBtns); } // prettier-ignore
public static get MySearcher() { return DocCast(Doc.UserDoc().mySearcher); } // prettier-ignore
+ public static get MyImageGrouper() { return DocCast(Doc.UserDoc().myImageGrouper); } //prettier-ignore
public static get MyHeaderBar() { return DocCast(Doc.UserDoc().myHeaderBar); } // prettier-ignore
public static get MyLeftSidebarMenu() { return DocCast(Doc.UserDoc().myLeftSidebarMenu); } // prettier-ignore
public static get MyLeftSidebarPanel() { return DocCast(Doc.UserDoc().myLeftSidebarPanel); } // prettier-ignore