aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/views/DocumentButtonBar.tsx19
-rw-r--r--src/client/views/DocumentDecorations.tsx1
-rw-r--r--src/client/views/KeywordBox.tsx168
-rw-r--r--src/client/views/StyleProvider.scss31
-rw-r--r--src/client/views/StyleProvider.tsx6
-rw-r--r--src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx53
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx28
-rw-r--r--src/client/views/nodes/ImageBox.tsx7
8 files changed, 233 insertions, 80 deletions
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 487868169..a75c7098c 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -31,6 +31,7 @@ import { DocumentLinksButton } from './nodes/DocumentLinksButton';
import { DocumentView } from './nodes/DocumentView';
import { OpenWhere } from './nodes/OpenWhere';
import { DashFieldView } from './nodes/formattedText/DashFieldView';
+import { DocData } from '../../fields/DocSymbols';
@observer
export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (DocumentView | undefined)[]; stack?: any }> {
@@ -282,6 +283,23 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
);
}
+ @computed
+ get keywordButton() {
+ const targetDoc = this.view0?.Document;
+ return !targetDoc ? null : (
+ <Tooltip title={<div className="dash-keyword-button">Open keyword menu</div>}>
+ <div
+ className="documentButtonBar-icon"
+ style={{ color: 'white' }}
+ onClick={() => {
+ targetDoc[DocData].showLabels = !targetDoc[DocData].showLabels;
+ }}>
+ <FontAwesomeIcon className="documentdecorations-icon" icon="tag" />
+ </div>
+ </Tooltip>
+ );
+ }
+
@observable _isRecording = false;
_stopFunc: () => void = emptyFunction;
@computed
@@ -452,6 +470,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
<div className="documentButtonBar-button">{this.pinButton}</div>
<div className="documentButtonBar-button">{this.recordButton}</div>
<div className="documentButtonBar-button">{this.calendarButton}</div>
+ <div className="documentButtonBar-button">{this.keywordButton}</div>
{!Doc.UserDoc().documentLinksButton_fullMenu ? null : <div className="documentButtonBar-button">{this.shareButton}</div>}
<div className="documentButtonBar-button">{this.menuButton}</div>
</div>
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 93c3e3338..20bf8fd9f 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -88,6 +88,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
(this._showNothing = !inputting && !DocumentButtonBar.Instance?._tooltipOpen && !(this.Bounds.x !== Number.MAX_VALUE && //
(this.Bounds.x > center.x+x || this.Bounds.r < center.x+x ||
this.Bounds.y > center.y+y || this.Bounds.b < center.y+y )));
+
})); // prettier-ignore
}
diff --git a/src/client/views/KeywordBox.tsx b/src/client/views/KeywordBox.tsx
index 3faddeb64..8c69f446d 100644
--- a/src/client/views/KeywordBox.tsx
+++ b/src/client/views/KeywordBox.tsx
@@ -1,64 +1,170 @@
-import { action, makeObservable } from 'mobx';
+import { Colors, IconButton } from 'browndash-components';
+import { action, computed, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import React from 'react';
import { Doc } from '../../fields/Doc';
import { DocData } from '../../fields/DocSymbols';
import { List } from '../../fields/List';
+import { DragManager, SetupDrag } from '../util/DragManager';
+import { SnappingManager } from '../util/SnappingManager';
+import { DocumentView } from './nodes/DocumentView';
import { ObservableReactComponent } from './ObservableReactComponent';
+interface KeywordItemProps {
+ doc: Doc;
+ label: string;
+ setToEditing: () => void;
+ isEditing: boolean;
+}
+
+@observer
+export class KeywordItem extends ObservableReactComponent<KeywordItemProps> {
+ constructor(props: any) {
+ super(props);
+ makeObservable(this);
+ this.ref = React.createRef();
+ }
+
+ private _dropDisposer?: DragManager.DragDropDisposer;
+ private ref: React.RefObject<HTMLDivElement>;
+
+ protected createDropTarget = (ele: HTMLDivElement) => {
+ this._dropDisposer?.();
+ SetupDrag(this.ref, () => undefined);
+ //ele && (this._dropDisposer = DragManager. (ele, this.onInternalDrop.bind(this), this.layoutDoc));
+ //ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc));
+ };
+
+ @action
+ removeLabel = () => {
+ if (this._props.doc[DocData].data_labels) {
+ this._props.doc[DocData].data_labels = (this._props.doc[DocData].data_labels as List<string>).filter(label => label !== this._props.label) as List<string>;
+ }
+ };
+
+ render() {
+ return (
+ <div className="keyword" onClick={this._props.setToEditing} onPointerDown={() => {}} ref={this.ref}>
+ {this._props.label}
+ {this.props.isEditing && <IconButton tooltip={'Remove label'} onPointerDown={this.removeLabel} icon={'X'} style={{ width: '8px', height: '8px', marginLeft: '10px' }} />}
+ </div>
+ );
+ }
+}
+
interface KeywordBoxProps {
- _doc: Doc;
- _isEditing: boolean;
+ doc: Doc;
+ isEditing: boolean;
}
@observer
export class KeywordBox extends ObservableReactComponent<KeywordBoxProps> {
+ @observable _currentInput: string = '';
+ //private disposer: () => void;
+
constructor(props: any) {
super(props);
makeObservable(this);
}
+ // componentDidMount(): void {
+ // reaction(
+ // () => ({
+ // isDragging: SnappingManager.IsDragging,
+ // selectedDoc: DocumentView.SelectedDocs().lastElement(),
+ // isEditing: this._props.isEditing,
+ // }),
+ // ({ isDragging, selectedDoc, isEditing }) => {
+ // if (isDragging || selectedDoc !== this._props.doc || !isEditing) {
+ // this.setToView();
+ // }
+ // }
+ // );
+ // }
+
+ // componentWillUnmount() {
+ // this.disposer();
+ // }
+
@action
setToEditing = () => {
- this._props._isEditing = true;
+ this._props.isEditing = true;
};
@action
setToView = () => {
- this._props._isEditing = false;
+ this._props.isEditing = false;
};
- submitLabel = () => {};
+ submitLabel = () => {
+ if (this._currentInput.trim()) {
+ if (!this._props.doc[DocData].data_labels) {
+ this._props.doc[DocData].data_labels = new List<string>();
+ }
- onInputChange = () => {};
+ (this._props.doc![DocData].data_labels! as List<string>).push(this._currentInput.trim());
+ this._currentInput = ''; // Clear the input box
+ }
+ };
+
+ @action
+ onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._currentInput = e.target.value;
+ };
render() {
- const keywordsList = this._props._doc![DocData].data_labels;
+ const keywordsList = this._props.doc[DocData].data_labels ? this._props.doc[DocData].data_labels : new List<string>();
+ const seldoc = DocumentView.SelectedDocs().lastElement();
+ if (SnappingManager.IsDragging || !(seldoc === this._props.doc) || !this._props.isEditing) {
+ setTimeout(
+ action(() => {
+ if ((keywordsList as List<string>).length === 0) {
+ this._props.doc[DocData].showLabels = false;
+ }
+ this.setToView();
+ })
+ );
+ }
+
return (
- <div className="keywords-container">
- {(keywordsList as List<string>).map(label => {
- return (
- <div className="keyword" onClick={this.setToEditing}>
- {label}
+ <div className="keywords-container" style={{ backgroundColor: this._props.isEditing ? Colors.LIGHT_GRAY : Colors.TRANSPARENT, borderColor: this._props.isEditing ? Colors.BLACK : Colors.TRANSPARENT }}>
+ <div className="keywords-list">
+ {(keywordsList as List<string>).map(label => {
+ return <KeywordItem doc={this._props.doc} label={label} setToEditing={this.setToEditing} isEditing={this._props.isEditing}></KeywordItem>;
+ })}
+ </div>
+ {this._props.isEditing ? (
+ <div className="keyword-editing-box">
+ <div className="keyword-input-box">
+ <input
+ value={this._currentInput}
+ autoComplete="off"
+ onChange={this.onInputChange}
+ onKeyDown={e => {
+ e.key === 'Enter' ? this.submitLabel() : null;
+ e.stopPropagation();
+ }}
+ type="text"
+ placeholder="Input keywords for document..."
+ aria-label="keyword-input"
+ className="keyword-input"
+ style={{ width: '100%', borderRadius: '5px' }}
+ />
+ </div>
+ <div className="keyword-buttons">
+ <IconButton
+ tooltip={'Close Menu'}
+ onPointerDown={() => {
+ if ((keywordsList as List<string>).length === 0) {
+ this._props.doc[DocData].showLabels = false;
+ } else {
+ this.setToView();
+ }
+ }}
+ icon={'x'}
+ style={{ width: '4px' }}
+ />
</div>
- );
- })}
- {this._props._isEditing ? (
- <div>
- <input
- defaultValue=""
- autoComplete="off"
- onChange={this.onInputChange}
- onKeyDown={e => {
- e.key === 'Enter' ? this.submitLabel() : null;
- e.stopPropagation();
- }}
- type="text"
- placeholder="Input keywords for document..."
- aria-label="keyword-input"
- className="keyword-input"
- style={{ width: '100%', borderRadius: '5px' }}
- />
</div>
) : (
<div></div>
diff --git a/src/client/views/StyleProvider.scss b/src/client/views/StyleProvider.scss
index 1e2af9a3a..7cc06f922 100644
--- a/src/client/views/StyleProvider.scss
+++ b/src/client/views/StyleProvider.scss
@@ -57,12 +57,41 @@
.keywords-container {
display: flex;
flex-wrap: wrap;
+ flex-direction: column;
+ padding-bottom: 4px;
+ border: 1px solid;
+ border-radius: 4px;
+}
+
+.keywords-list {
+ display: flex;
+ flex-wrap: wrap;
}
.keyword {
- padding: 5px 10px;
+ padding: 5px 5px;
background-color: lightblue;
border: 1px solid black;
border-radius: 5px;
white-space: nowrap;
+ display: flex;
+ align-items: center;
+}
+
+.keyword-editing-box {
+ margin-top: 8px;
+}
+
+.keyword-input-box {
+ // display: flex;
+ // align-items: center;
+ // align-content: center;
+ margin: auto;
+ align-self: center;
+ width: 90%;
+}
+
+.keyword-buttons {
+ margin-left: auto;
+ width: 10%;
}
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index fb509516a..f4d73cd1d 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -371,8 +371,10 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
);
};
const keywords = () => {
- if (doc && doc![DocData].data_labels && doc![DocData].showLabels) {
- return (<KeywordBox _isEditing={false} _doc={doc}></KeywordBox>)
+ if (doc && doc![DocData].showLabels && (!doc[DocData].data_labels || (doc[DocData].data_labels as List<string>).length === 0)){
+ return (<KeywordBox isEditing={true} doc={doc}></KeywordBox>)
+ } else if (doc && doc![DocData].data_labels && doc![DocData].showLabels) {
+ return (<KeywordBox isEditing={false} doc={doc}></KeywordBox>)
}
}
return (
diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
index cfb81e1a0..fec4d3e12 100644
--- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
@@ -123,7 +123,6 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
@action
groupImages = () => {
this.groupImagesInBox();
- MainView.Instance.closeFlyout();
};
@action
@@ -161,16 +160,7 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
classifyImagesInBox = async () => {
this.startLoading();
- // const imageInfos = this._selectedImages.map(async doc => {
- // 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.
+ // Converts the images into a Base64 format, afterwhich the information is sent to GPT to label them.
const imageInfos = this._selectedImages.map(async doc => {
if (!doc[DocData].data_labels) {
@@ -178,8 +168,7 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
return CollectionCardView.imageUrlToBase64(`${name}_o.${type}`).then(hrefBase64 =>
!hrefBase64 ? undefined :
gptImageLabel(hrefBase64).then(labels =>
-
- ({ doc, labels }))) ; // prettier-ignore
+ ({ doc, labels }))) ; // prettier-ignore
}
});
@@ -194,24 +183,7 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
(imageInfo.doc[DocData].data_labels as List<string>).push(label);
});
}
- }); // Add the labels as fields to each image.
-
- // (await Promise.all(imageInfos)).forEach(imageInfo => {
- // if (imageInfo && imageInfo.embeddings && Array.isArray(imageInfo.embeddings)) {
- // imageInfo.doc[DocData].data_labels = new List<string>();
-
- // const labels = imageInfo.labels.split('\n');
- // labels.forEach(label => {
- // label = label.replace(/^\d+\.\s*|-|\*/, '').trim();
- // imageInfo.doc[DocData][`${label}`] = true;
- // (imageInfo.doc[DocData].data_labels as List<string>).push(label);
- // });
-
- // numberRange(5).forEach(n => {
- // imageInfo.doc[`data_labels_embedding_${n + 1}`] = new List<number>(imageInfo.embeddings[n]);
- // });
- // }
- // }); // Add the labels as fields to each image.
+ });
this.endLoading();
};
@@ -220,12 +192,15 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
* Groups images to most similar labels.
*/
groupImagesInBox = action(async () => {
- this._selectedImages.forEach(doc => {
- (doc[DocData].data_labels as List<string>).forEach(async (label, index) => {
+ this.startLoading();
+
+ for (const doc of this._selectedImages) {
+ for (let index = 0; index < (doc[DocData].data_labels as List<string>).length; index++) {
+ const label = (doc[DocData].data_labels as List<string>)[index];
const embedding = await gptGetEmbedding(label);
doc[`data_labels_embedding_${index + 1}`] = new List<number>(embedding);
- });
- });
+ }
+ }
const labelToEmbedding = new Map<string, number[]>();
// Create embeddings for the labels.
@@ -234,8 +209,8 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
// For each image, loop through the labels, and calculate similarity. Associate it with the
// most similar one.
this._selectedImages.forEach(doc => {
- const embedLists = numberRange(5).map(n => Array.from(NumListCast(doc[`data_labels_embedding_${n + 1}`])));
- const bestEmbedScore = (embedding: Opt<number[]>) => Math.max(...embedLists.map((l, index) => (embedding && (1 - index * 0.1) * similarity(Array.from(embedding), l)!) || 0));
+ const embedLists = numberRange((doc[DocData].data_labels as List<string>).length).map(n => Array.from(NumListCast(doc[`data_labels_embedding_${n + 1}`])));
+ const bestEmbedScore = (embedding: Opt<number[]>) => Math.max(...embedLists.map((l, index) => (embedding && similarity(Array.from(embedding), l)!) || 0));
const {label: mostSimilarLabelCollect} =
this._labelGroups.map(label => ({ label, similarityScore: bestEmbedScore(labelToEmbedding.get(label)) }))
.reduce((prev, cur) => cur.similarityScore < 0.3 || cur.similarityScore <= prev.similarityScore ? prev: cur,
@@ -243,9 +218,13 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
doc[DocData].data_label = mostSimilarLabelCollect; // The label most similar to the image's contents.
});
+ this.endLoading();
+
if (this._selectedImages) {
MarqueeOptionsMenu.Instance.groupImages();
}
+
+ MainView.Instance.closeFlyout();
});
render() {
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index f03a9d62d..197681f62 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -455,8 +455,32 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
*/
@undoBatch
groupImages = action(async () => {
- 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'.
+ const labelGroups: string[] = ImageLabelBoxData.Instance._labelGroups;
+ const labelToCollection: Map<string, Doc> = new Map();
+ const selectedImages = ImageLabelBoxData.Instance._docs;
+
+ // Create new collections associated with each label and get the embeddings for the labels.
+ let x_offset = 0;
+ for (const label of labelGroups) {
+ const newCollection = this.getCollection([], undefined, false);
+ newCollection._freeform_panX = this.Bounds.left + this.Bounds.width / 2;
+ newCollection._freeform_panY = this.Bounds.top + this.Bounds.height / 2;
+ console.log(newCollection._x);
+ labelToCollection.set(label, newCollection);
+ this._props.addDocument?.(newCollection);
+ //newCollection._x = (newCollection._x as number) + x_offset;
+ //x_offset += newCollection._width as number;
+ }
+
+ for (const doc of selectedImages) {
+ if (doc[DocData].data_label) {
+ Doc.AddDocToList(labelToCollection.get(doc[DocData].data_label as string)!, undefined, doc);
+ this._props.removeDocument?.(doc);
+ }
+ }
+
+ //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/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index fb90f907f..1c90fae9e 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -283,13 +283,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}),
icon: 'pencil-alt',
});
- funcs.push({
- description: 'Toggle Keywords',
- event: () => {
- this.Document[DocData].showLabels = !this.Document[DocData].showLabels;
- },
- icon: 'eye',
- });
ContextMenu.Instance?.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' });
}
};