diff options
author | IEatChili <nanunguyen99@gmail.com> | 2024-06-26 13:58:20 -0400 |
---|---|---|
committer | IEatChili <nanunguyen99@gmail.com> | 2024-06-26 13:58:20 -0400 |
commit | 3190f1eb07a47a5e1ccdd20e346b47094118292d (patch) | |
tree | 7b8dab1763720ee71bc89e888b0296fdd31054b5 /src | |
parent | 376ff1626b24cbac12b27ad072690424549f05c7 (diff) |
feat: worked more on keyword input for docs
Diffstat (limited to 'src')
-rw-r--r-- | src/client/views/DocumentButtonBar.tsx | 19 | ||||
-rw-r--r-- | src/client/views/DocumentDecorations.tsx | 1 | ||||
-rw-r--r-- | src/client/views/KeywordBox.tsx | 168 | ||||
-rw-r--r-- | src/client/views/StyleProvider.scss | 31 | ||||
-rw-r--r-- | src/client/views/StyleProvider.tsx | 6 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx | 53 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/MarqueeView.tsx | 28 | ||||
-rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 7 |
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' }); } }; |