From 4574b7f03ccc85c4bebdbfd9475788456086704f Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 8 Aug 2024 12:27:40 -0400 Subject: many changes to add typing in place of 'any's etc --- src/client/documents/DocUtils.ts | 64 +++++++++++++++++++---------------- src/client/documents/DocumentTypes.ts | 1 - src/client/documents/Documents.ts | 33 ++++++++++-------- 3 files changed, 52 insertions(+), 46 deletions(-) (limited to 'src/client/documents') diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index 0c9fe0315..a503d732b 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -35,14 +35,16 @@ import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox'; import { DocumentType } from './DocumentTypes'; import { Docs, DocumentOptions } from './Documents'; +// eslint-disable-next-line @typescript-eslint/no-var-requires const { DFLT_IMAGE_NATIVE_DIM } = require('../views/global/globalCssVariables.module.scss'); // prettier-ignore const defaultNativeImageDim = Number(DFLT_IMAGE_NATIVE_DIM.replace('px', '')); +// eslint-disable-next-line @typescript-eslint/no-namespace export namespace DocUtils { - function matchFieldValue(doc: Doc, key: string, valueIn: any): boolean { + function matchFieldValue(doc: Doc, key: string, valueIn: unknown): boolean { let value = valueIn; - const hasFunctionFilter = ClientUtils.HasFunctionFilter(value); + const hasFunctionFilter = ClientUtils.HasFunctionFilter(value as string); if (hasFunctionFilter) { return hasFunctionFilter(StrCast(doc[key])); } @@ -57,8 +59,8 @@ export namespace DocUtils { // prettier-ignore return (value === Doc.FilterNone && !allLinks.length) || (value === Doc.FilterAny && !!allLinks.length) || - (allLinks.some(link => matchLink(value,DocCast(link.link_anchor_1)) || - matchLink(value,DocCast(link.link_anchor_2)) )); + (allLinks.some(link => matchLink(value as string, DocCast(link.link_anchor_1)) || + matchLink(value as string, DocCast(link.link_anchor_2)) )); } if (typeof value === 'string') { value = value.replace(`,${ClientUtils.noRecursionHack}`, ''); @@ -71,9 +73,9 @@ export namespace DocUtils { } const vals = StrListCast(fieldVal); // list typing is very imperfect. casting to a string list doesn't mean that the entries will actually be strings if (vals.length) { - return vals.some(v => typeof v === 'string' && v.includes(value)); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring + return vals.some(v => typeof v === 'string' && v.includes(value as string)); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring } - return Field.toString(fieldVal as FieldType).includes(value); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring + return Field.toString(fieldVal as FieldType).includes(value as string); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring } /** * @param docs @@ -215,13 +217,13 @@ export namespace DocUtils { { acl_Guest: SharingPermissions.Augment, _acl_Guest: SharingPermissions.Augment, - title: ComputedField.MakeFunction('generateLinkTitle(this)') as any, + title: ComputedField.MakeFunction('generateLinkTitle(this)') as unknown as string, // title can accept functions even though type says it can't link_anchor_1_useSmallAnchor: source.useSmallAnchor ? true : undefined, link_anchor_2_useSmallAnchor: target.useSmallAnchor ? true : undefined, link_relationship: linkSettings.link_relationship, link_description: linkSettings.link_description, - x: ComputedField.MakeFunction(`((this.${a}?.x||0)+(this.${b}?.x||0))/2`) as any, - y: ComputedField.MakeFunction(`((this.${a}?.y||0)+(this.${b}?.y||0))/2`) as any, + x: ComputedField.MakeFunction(`((this.${a}?.x||0)+(this.${b}?.x||0))/2`) as unknown as number, // x can accept functions even though type says it can't + y: ComputedField.MakeFunction(`((this.${a}?.y||0)+(this.${b}?.y||0))/2`) as unknown as number, // y can accept functions even though type says it can't link_autoMoveAnchors: true, _lockedPosition: true, _layout_showCaption: '', // removed since they conflict with showing a link with a LinkBox (ie, line, not comparison box) @@ -235,10 +237,10 @@ export namespace DocUtils { ); } - export function AssignScripts(doc: Doc, scripts?: { [key: string]: string | undefined }, funcs?: { [key: string]: string }) { + export function AssignScripts(doc: Doc, scripts?: { [key: string]: string | undefined }, funcs?: { [key: string]: string | undefined }) { scripts && Object.keys(scripts).forEach(key => { - const script = scripts[key]; + const script = scripts[key] as string; if (ScriptCast(doc[key])?.script.originalScript !== scripts[key] && script) { (key.startsWith('_') ? doc : Doc.GetProto(doc))[key] = ScriptField.MakeScript(script, { this: Doc.name, @@ -261,16 +263,17 @@ export namespace DocUtils { .filter(key => !key.endsWith('-setter')) .forEach(key => { const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); - if (ScriptCast(cfield)?.script.originalScript !== funcs[key]) { + const func = funcs[key]; + if (ScriptCast(cfield)?.script.originalScript !== func) { const setFunc = Cast(funcs[key + '-setter'], 'string', null); - (key.startsWith('_') ? doc : Doc.GetProto(doc))[key] = funcs[key] ? ComputedField.MakeFunction(funcs[key], { dragData: Doc.DocDragDataName }, { _readOnly_: true }, setFunc) : undefined; + (key.startsWith('_') ? doc : Doc.GetProto(doc))[key] = func ? ComputedField.MakeFunction(func, { dragData: Doc.DocDragDataName }, { _readOnly_: true }, setFunc) : undefined; } }); return doc; } export function AssignOpts(doc: Doc | undefined, reqdOpts: DocumentOptions, items?: Doc[]) { if (doc) { - const compareValues = (val1: any, val2: any) => { + const compareValues = (val1: unknown, val2: unknown) => { if (val1 instanceof List && val2 instanceof List && val1.length === val2.length) { return !val1.some(v => !val2.includes(v)) || !val2.some(v => val1.includes(v)); } @@ -334,7 +337,7 @@ export namespace DocUtils { if (path.includes(window.location.hostname)) { const s = path.split('/'); const id = s[s.length - 1]; - return DocServer.GetRefField(id).then(field => { + return DocServer.GetRefField(id)?.then(field => { if (field instanceof Doc) { const embedding = Doc.MakeEmbedding(field); embedding.x = (options.x as number) || 0; @@ -354,7 +357,7 @@ export namespace DocUtils { return ctor ? ctor(path, overwriteDoc ? { ...options, title: StrCast(overwriteDoc.title, path) } : options, overwriteDoc) : undefined; } - export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number, simpleMenu: boolean = false, pivotField?: string, pivotValue?: string): void { + export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number, simpleMenu: boolean = false, pivotField?: string, pivotValue?: string | number | boolean): void { const documentList: ContextMenuProps[] = DocListCast(DocListCast(Doc.MyTools?.data)[0]?.data) .filter(btnDoc => !btnDoc.hidden) .map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)) @@ -451,7 +454,7 @@ export namespace DocUtils { batch.end(); return doc; } - export function findTemplate(templateName: string, type: string) { + export function findTemplate(templateName: string, doc: Doc) { let docLayoutTemplate: Opt; const iconViews = DocListCast(Cast(Doc.UserDoc().template_icons, Doc, null)?.data); const templBtns = DocListCast(Cast(Doc.UserDoc().template_buttons, Doc, null)?.data); @@ -469,7 +472,8 @@ export namespace DocUtils { // first try to find a template that matches the specific document type (_). otherwise, fallback to a general match on !docLayoutTemplate && allTemplates.forEach(tempDoc => { - StrCast(tempDoc.title) === templateName + '_' + type && (docLayoutTemplate = tempDoc); + const templateType = StrCast(doc[templateName + '_fieldKey'] || doc.type); + StrCast(tempDoc.title) === templateName + '_' + templateType && (docLayoutTemplate = tempDoc); }); !docLayoutTemplate && allTemplates.forEach(tempDoc => { @@ -481,7 +485,7 @@ export namespace DocUtils { const templateName = templateSignature.replace(/\(.*\)/, ''); doc.layout_fieldKey = 'layout_' + (templateSignature || (docLayoutTemplate?.title ?? '')); // eslint-disable-next-line no-param-reassign - docLayoutTemplate = docLayoutTemplate || findTemplate(templateName, StrCast(doc.isGroup && doc.transcription ? 'transcription' : doc.type)); + docLayoutTemplate = docLayoutTemplate || findTemplate(templateName, doc); const customName = 'layout_' + templateSignature; const _width = NumCast(doc._width); @@ -619,7 +623,7 @@ export namespace DocUtils { const proto = protoIn; if (Upload.isImageInformation(result)) { const maxNativeDim = Math.min(Math.max(result.nativeHeight, result.nativeWidth), defaultNativeImageDim); - const exifRotation = StrCast((result.exifData?.data as any)?.Orientation).toLowerCase(); + const exifRotation = StrCast(result.exifData?.data?.Orientation).toLowerCase(); proto.data_nativeOrientation = result.exifData?.data?.image?.Orientation ?? (exifRotation.includes('rotate 90') || exifRotation.includes('rotate 270') ? 5 : undefined); proto.data_nativeWidth = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim; proto.data_nativeHeight = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight); @@ -697,10 +701,10 @@ export namespace DocUtils { source: { newFilename, mimetype }, result, } = upfiles.lastElement(); - if ((result as any).message) { + if (result instanceof Error) { if (overwriteDoc) { overwriteDoc.isLoading = false; - overwriteDoc.loadingError = (result as any).message; + overwriteDoc.loadingError = result.message; Doc.removeCurrentlyLoading(overwriteDoc); } } else newFilename && processFileupload(generatedDocuments, newFilename, mimetype ?? '', result, options, overwriteDoc); @@ -736,9 +740,9 @@ export namespace DocUtils { source: { newFilename, mimetype }, result, } = upfiles.lastElement() ?? { source: { newFilename: '', mimetype: '' }, result: { message: 'upload failed' } }; - if ((result as any).message) { + if (result instanceof Error) { if (overwriteDoc) { - overwriteDoc.loadingError = (result as any).message; + overwriteDoc.loadingError = result.message; Doc.removeCurrentlyLoading(overwriteDoc); } } else newFilename && mimetype && processFileupload(generatedDocuments, newFilename, mimetype, result, options, overwriteDoc); @@ -768,7 +772,7 @@ export namespace DocUtils { export async function Zip(doc: Doc, zipFilename = 'dashExport.zip') { const { clone, map, linkMap } = await Doc.MakeClone(doc); const proms = new Set(); - function replacer(key: any, value: any) { + function replacer(key: string, value: { url: string; [key: string]: unknown }) { if (key && ['branchOf', 'cloneOf', 'cursors'].includes(key)) return undefined; if (value?.__type === 'image') { const extension = value.url.replace(/.*\./, ''); @@ -804,8 +808,8 @@ export namespace DocUtils { return value; } - const docs: { [id: string]: any } = {}; - const links: { [id: string]: any } = {}; + const docs: { [id: string]: unknown } = {}; + const links: { [id: string]: unknown } = {}; Array.from(map.entries()).forEach(f => { docs[f[0]] = f[1]; }); @@ -826,13 +830,13 @@ export namespace DocUtils { } else promArr.forEach((url, i) => { // loading a file and add it in a zip file - JSZipUtils.getBinaryContent(window.location.origin + '/' + url, (err: any, data: any) => { + JSZipUtils.getBinaryContent(window.location.origin + '/' + url, (err: unknown, data: unknown) => { if (err) throw err; // or handle the error // // Generate a directory within the Zip file structure // const assets = zip.folder("assets"); // assets.file(filename, data, {binary: true}); const assetPathOnServer = promArr[i].replace(window.location.origin + '/', '').replace(/\//g, '%%%'); - zip.file(assetPathOnServer, data, { binary: true }); + zip.file(assetPathOnServer, data as string, { binary: true }); console.log(' => ' + url); if (++count === promArr.length) { zip.file('docs.json', jsonDocs); @@ -862,7 +866,7 @@ ScriptingGlobals.add(function copyDragFactory(dragFactory: Doc, asDelegate?: boo return dragFactory instanceof Doc ? (asDelegate ? DocUtils.delegateDragFactory(dragFactory) : DocUtils.copyDragFactory(dragFactory)) : dragFactory; }); // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function makeDelegate(proto: any) { +ScriptingGlobals.add(function makeDelegate(proto: Doc) { const d = Docs.Create.DelegateDocument(proto, { title: 'child of ' + proto.title }); return d; }); diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 8f95068db..53edb2e31 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -31,7 +31,6 @@ export enum DocumentType { // special purpose wrappers that either take no data or are compositions of lower level types LINK = 'link', - IMPORT = 'import', PRES = 'presentation', PRESELEMENT = 'preselement', COMPARISON = 'comparison', diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index b96fdb4bd..d7fdf016c 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -141,12 +141,12 @@ class RtfInfo extends FInfo { } class ListInfo extends FInfo { fieldType? = FInfoFieldType.list; - values?: List[] = []; + values?: List[] = []; } type BOOLt = BoolInfo | boolean; type NUMt = NumInfo | number; type STRt = StrInfo | string; -type LISTt = ListInfo | List; +type LISTt = ListInfo | List; type DOCt = DocInfo | Doc; type RTFt = RtfInfo | RichTextField; type DIMt = DimInfo; // | typeof DimUnit.Pixel | typeof DimUnit.Ratio; @@ -358,7 +358,7 @@ export class DocumentOptions { presentation_duration?: NUMt = new NumInfo('the duration of the slide in presentation view', false); presentation_zoomText?: BOOLt = new BoolInfo('whether text anchors should shown in a larger box when following links to make them stand out', false); - data?: any; + data?: FieldType; data_useCors?: BOOLt = new BoolInfo('whether CORS protocol should be used for web page'); columnHeaders?: List; // headers for stacking views schemaHeaders?: List; // headers for schema view @@ -464,13 +464,14 @@ export class DocumentOptions { sidebar_color?: string; // background color of text sidebar sidebar_type_collection?: string; // collection type of text sidebar - data_dashboards?: List; // list of dashboards used in shareddocs; + data_dashboards?: List; // list of dashboards used in shareddocs; textTransform?: string; letterSpacing?: string; iconTemplate?: string; // name of icon template style + icon_fieldKey?: string; // specifies the icon template to use (e.g., icon_fieldKey='george', then the icon template's name is icon_george; otherwise, the template's name would be icon_ where type is the Doc's type(pdf,rich text, etc)) selectedIndex?: NUMt = new NumInfo("which item in a linear view has been selected using the 'thumb doc' ui"); - fieldValues?: List; // possible values a field can have (used by FieldInfo's only) + fieldValues?: List; // possible values a field can have (used by FieldInfo's only) fieldType?: string; // display type of a field, e.g. string, number, enumeration (used by FieldInfo's only) clipboard?: Doc; @@ -484,7 +485,10 @@ export class DocumentOptions { } export const DocOptions = new DocumentOptions(); + +// eslint-disable-next-line @typescript-eslint/no-namespace export namespace Docs { + // eslint-disable-next-line @typescript-eslint/no-namespace export namespace Prototypes { type LayoutSource = { LayoutString: (key: string) => string }; type PrototypeTemplate = { @@ -492,7 +496,6 @@ export namespace Docs { view: LayoutSource; dataField: string; }; - data?: any; options?: Partial; }; type TemplateMap = Map; @@ -554,7 +557,7 @@ export namespace Docs { const actualProtos = await DocServer.GetRefFields(prototypeIds); // update this object to include any default values: DocumentOptions for all prototypes prototypeIds.forEach(id => { - const existing = actualProtos[id] as Doc; + const existing = actualProtos.get(id); const type = id.replace(suffix, '') as DocumentType; // get or create prototype of the specified type... const target = buildPrototype(type, id, existing); @@ -627,16 +630,15 @@ export namespace Docs { acl_Guest: SharingPermissions.View, ...(template.options || {}), layout: layout.view?.LayoutString(layout.dataField), - data: template.data, }; Object.entries(options) .filter(pair => typeof pair[1] === 'string' && pair[1].startsWith('@')) .forEach(pair => { if (!existing || ScriptCast(existing[pair[0]])?.script.originalScript !== pair[1].substring(1)) { - (options as any)[pair[0]] = ComputedField.MakeFunction(pair[1].substring(1)); + (options as { [key: string]: unknown })[pair[0]] = ComputedField.MakeFunction(pair[1].substring(1)); } }); - return Doc.assign(existing ?? new Doc(prototypeId, true), OmitKeys(options, Object.keys(existing ?? {})).omit, undefined, true); + return Doc.assign(existing ?? new Doc(prototypeId, true), OmitKeys(options, Object.keys(existing ?? {})).omit as { [key: string]: FieldType }, undefined, true); } } @@ -644,6 +646,7 @@ export namespace Docs { * Encapsulates the factory used to create new document instances * delegated from top-level prototypes */ + // eslint-disable-next-line @typescript-eslint/no-namespace export namespace Create { /** * This function receives the relevant document prototype and uses @@ -667,10 +670,10 @@ export namespace Docs { function InstanceFromProto(proto: Doc, data: FieldType | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = 'data', protoId?: string, placeholderDocIn?: Doc, noView?: boolean) { const placeholderDoc = placeholderDocIn; const viewKeys = ['x', 'y', 'isSystem']; // keys that should be addded to the view document even though they don't begin with an "_" - const { omit: dataProps, extract: viewProps } = OmitKeys(options, viewKeys, '^_'); + const { omit: dataProps, extract: viewProps } = OmitKeys(options, viewKeys, '^_') as { omit: { [key: string]: FieldType | undefined }; extract: { [key: string]: FieldType | undefined } }; // dataProps.acl_Override = SharingPermissions.Unset; - dataProps.acl_Guest = options.acl_Guest ?? (Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View); + dataProps.acl_Guest = options.acl_Guest?.toString() ?? (Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View); dataProps.isSystem = viewProps.isSystem; dataProps.isDataDoc = true; dataProps.author = ClientUtils.CurrentUserEmail(); @@ -693,7 +696,7 @@ export namespace Docs { } if (!noView) { - const viewFirstProps: { [id: string]: any } = { author: ClientUtils.CurrentUserEmail() }; + const viewFirstProps: { [id: string]: FieldType } = { author: ClientUtils.CurrentUserEmail() }; viewFirstProps.acl_Guest = options._acl_Guest ?? (Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View); let viewDoc: Doc; // determines whether viewDoc should be created using placeholder Doc or default @@ -710,7 +713,7 @@ export namespace Docs { viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true); } Doc.assign(viewDoc, viewProps, true, true); - if (![DocumentType.LINK, DocumentType.CONFIG, DocumentType.LABEL].includes(viewDoc.type as any)) { + if (![DocumentType.LINK, DocumentType.CONFIG, DocumentType.LABEL].includes(viewDoc.type as DocumentType)) { CreateLinkToActiveAudio(() => viewDoc); } updateCachedAcls(dataDoc); @@ -909,7 +912,7 @@ export namespace Docs { } export function ConfigDocument(options: DocumentOptions, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), options?.data, options, id, '', undefined, undefined, true); + return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), undefined, options, id, '', undefined, undefined, true); } export function PileDocument(documents: Array, options: DocumentOptions, id?: string) { -- cgit v1.2.3-70-g09d2 From 0e975569e5686138e52bdc554b3f0391f42aeead Mon Sep 17 00:00:00 2001 From: IEatChili Date: Thu, 15 Aug 2024 14:13:02 -0400 Subject: feat: added face recogntion box --- src/client/apis/gpt/GPT.ts | 2 +- src/client/documents/DocumentTypes.ts | 1 + src/client/documents/Documents.ts | 4 + src/client/util/CurrentUserUtils.ts | 13 +- src/client/views/KeywordBox.tsx | 7 +- src/client/views/Main.tsx | 2 + .../collectionFreeForm/FaceCollectionBox.scss | 74 +++++++ .../collectionFreeForm/FaceCollectionBox.tsx | 217 +++++++++++++++++++++ .../collectionFreeForm/ImageLabelBox.tsx | 5 +- src/client/views/search/FaceRecognitionHandler.tsx | 74 +++++-- src/fields/Doc.ts | 1 + 11 files changed, 375 insertions(+), 25 deletions(-) create mode 100644 src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss create mode 100644 src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx (limited to 'src/client/documents') diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts index 05007960d..8dd3fd6e2 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -128,7 +128,7 @@ const gptImageLabel = async (src: string): Promise => { { role: 'user', content: [ - { type: 'text', text: 'Give three to five labels to describe this image.' }, + { type: 'text', text: 'Give three labels to describe this image.' }, { type: 'image_url', image_url: { diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index a9ea889b3..b66a29ac2 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -17,6 +17,7 @@ export enum DocumentType { FONTICON = 'fonticonbox', SEARCH = 'search', // search query IMAGEGROUPER = 'imagegrouper', + FACECOLLECTION = 'facecollection', 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 3737aa0b5..ecea74fab 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -795,6 +795,10 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.IMAGEGROUPER), undefined, options); } + export function FaceCollectionDocument(options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.FACECOLLECTION), 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 cb3d9df62..db0de83b9 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -458,7 +458,8 @@ 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: false } + { title: "Image Grouper", toolTip: "Image Grouper", target: this.setupImageGrouper(doc, "myImageGrouper"), ignoreClick: true, icon: "folder-open", hidden: false }, + { title: "Face Collection", toolTip: "Face Collection", target: this.setupFaceCollection(doc, "myFaceCollection"), ignoreClick: true, icon: "face-smile", hidden: false }, ].map(tuple => ({...tuple, scripts:{onClick: 'selectMainMenu(this)'}})); } @@ -500,6 +501,12 @@ pie title Minerals in my tap water _lockedPosition: true, _type_collection: CollectionViewType.Schema }); } + static setupFaceCollection(doc: Doc, field: string) { + return DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.FaceCollectionDocument(opts), { + dontRegisterView: true, backgroundColor: "dimgray", ignoreClick: true, title: "Face Collection", 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); @@ -702,8 +709,8 @@ pie title Minerals in my tap water { title: "Fit All", icon: "object-group", toolTip: "Fit Docs to View (double click to make sticky)",btnType: ButtonType.ToggleButton, ignoreClick:true, expertMode: false, toolType:"viewAll", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}', onDoubleClick: '{ return showFreeform(this.toolType, _readOnly_, true);}'}}, // Only when floating document is selected in freeform { title: "Clusters", icon: "braille", toolTip: "Show Doc Clusters", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"clusters", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform { title: "Cards", icon: "brain", toolTip: "Flashcards", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"flashcards", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform - { title: "Arrange", icon:"arrow-down-short-wide",toolTip:"Toggle Auto Arrange", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"arrange", funcs: {hidden: 'IsNoviceMode()'}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform - + { title: "Arrange", icon:"arrow-down-short-wide",toolTip:"Auto Arrange", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"arrange", funcs: {hidden: 'IsNoviceMode()'}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform + ] } static textTools():Button[] { diff --git a/src/client/views/KeywordBox.tsx b/src/client/views/KeywordBox.tsx index 68584a7fa..fc9c38a11 100644 --- a/src/client/views/KeywordBox.tsx +++ b/src/client/views/KeywordBox.tsx @@ -7,7 +7,7 @@ import { Doc, DocListCast } from '../../fields/Doc'; import { DocData } from '../../fields/DocSymbols'; import { List } from '../../fields/List'; import { NumCast, StrCast } from '../../fields/Types'; -import { emptyFunction } from '../../Utils'; +import { emptyFunction, Utils } from '../../Utils'; import { DocumentType } from '../documents/DocumentTypes'; import { DragManager } from '../util/DragManager'; import { SnappingManager } from '../util/SnappingManager'; @@ -124,7 +124,7 @@ export class KeywordItem extends ObservableReactComponent { render() { return ( -
+
{this._props.keyword} {this.props.isEditing && }
@@ -297,7 +297,7 @@ export class KeywordBox extends ObservableReactComponent {
{(keywordsList as List).map(keyword => { - return ; + return ; })}
{this._props.isEditing ? ( @@ -331,6 +331,7 @@ export class KeywordBox extends ObservableReactComponent { onClick={() => { this.submitLabel(keyword); }} + key={Utils.GenerateGuid()} /> ); })} diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index ada934aea..85c2b3d47 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -62,6 +62,7 @@ import { PresBox, PresElementBox } from './nodes/trails'; import { SearchBox } from './search/SearchBox'; import { ImageLabelBox } from './collections/collectionFreeForm/ImageLabelBox'; import { FaceRecognitionHandler } from './search/FaceRecognitionHandler'; +import { FaceCollectionBox } from './collections/collectionFreeForm/FaceCollectionBox'; dotenv.config(); @@ -135,6 +136,7 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' }; PresElementBox, SearchBox, ImageLabelBox, //Here! + FaceCollectionBox, FunctionPlotBox, InkingStroke, LinkBox, diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss new file mode 100644 index 000000000..480d109c8 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss @@ -0,0 +1,74 @@ +.face-document-item { + background: #555555; + margin-top: 10px; + margin-bottom: 10px; + padding: 10px; + border-radius: 10px; + position: relative; + + h1 { + color: white; + font-size: 24px; + text-align: center; + } + + .face-collection-buttons { + position: absolute; + top: 10px; + right: 10px; + } + + .face-document-image-container { + display: flex; + justify-content: center; + flex-wrap: wrap; + + .image-wrapper { + position: relative; + width: 70px; + height: 70px; + margin: 10px; + display: flex; + align-items: center; // Center vertically + justify-content: center; // Center horizontally + + img { + width: 100%; + height: 100%; + object-fit: cover; // This ensures the image covers the container without stretching + border-radius: 5px; + border: 2px solid white; + transition: border-color 0.4s; + + &:hover { + border-color: orange; // Change this to your desired hover border color + } + } + + .remove-item { + position: absolute; + bottom: -5; + right: -5; + background-color: rgba(0, 0, 0, 0.5); // Optional: to add a background behind the icon for better visibility + border-radius: 30%; + width: 10px; // Adjust size as needed + height: 10px; // Adjust size as needed + display: flex; + align-items: center; + justify-content: center; + } + } + + // img { + // max-width: 60px; + // margin: 10px; + // border-radius: 5px; + // border: 2px solid white; + // transition: 0.4s; + + // &:hover { + // border-color: orange; + // } + // } + } +} diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx new file mode 100644 index 000000000..1d3f88df1 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx @@ -0,0 +1,217 @@ +import { observer } from 'mobx-react'; +import React from 'react'; +import { Docs } from '../../../documents/Documents'; +import { DocumentType } from '../../../documents/DocumentTypes'; +import { ViewBoxBaseComponent } from '../../DocComponent'; +import { FieldView, FieldViewProps } from '../../nodes/FieldView'; +import 'ldrs/ring'; +import { SnappingManager } from '../../../util/SnappingManager'; +import { action, computed, makeObservable, observable, reaction } from 'mobx'; +import { Doc, DocListCast, NumListCast } from '../../../../fields/Doc'; +import { DocData } from '../../../../fields/DocSymbols'; +import { ImageCast, StrCast } from '../../../../fields/Types'; +import { ObservableReactComponent } from '../../ObservableReactComponent'; +import './FaceCollectionBox.scss'; +import { IconButton, Size } from 'browndash-components'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { MarqueeOptionsMenu } from './MarqueeOptionsMenu'; +import { List } from '../../../../fields/List'; +import { DocumentView } from '../../nodes/DocumentView'; +import { Utils } from '../../../../Utils'; +import { DragManager } from '../../../util/DragManager'; +import * as faceapi from 'face-api.js'; +import { FaceMatcher } from 'face-api.js'; + +interface FaceDocumentProps { + faceDoc: Doc; +} + +/** + * A componenent to visually represent a Face Document. + */ +@observer +export class FaceDocumentItem extends ObservableReactComponent { + private ref: React.RefObject; + @observable _displayImages: boolean = true; + private _dropDisposer?: DragManager.DragDropDisposer; + private _inputRef = React.createRef(); + + constructor(props: any) { + super(props); + makeObservable(this); + this.ref = React.createRef(); + } + + protected createDropTarget = (ele: HTMLDivElement) => { + this._dropDisposer?.(); + ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this._props.faceDoc)); + }; + + protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean { + const { docDragData } = de.complete; + if (docDragData) { + const filteredDocs = docDragData.droppedDocuments.filter(doc => doc.type === DocumentType.IMG); + filteredDocs.forEach(doc => { + // If the current Face Document has no items, and the doc has more than one face descriptor, don't let the user add the document first. + if ((this._props.faceDoc[DocData].faceDescriptors as List>).length === 0 && (doc[DocData].faces as List>).length > 1) { + alert('Cannot add a document with multiple faces as the first item!'); + } else { + // Loop through the documents' face descriptors. + // Choose the face with the smallest distance to add. + const float32Array = (this._props.faceDoc[DocData].faceDescriptors as List>).map(faceDescriptor => new Float32Array(Array.from(faceDescriptor))); + const labeledFaceDescriptor = new faceapi.LabeledFaceDescriptors(StrCast(this._props.faceDoc[DocData].label), float32Array); + const faceDescriptors: faceapi.LabeledFaceDescriptors[] = [labeledFaceDescriptor]; + + const faceMatcher = new FaceMatcher(faceDescriptors, 1); + let cur_lowest_distance = 1; + let cur_matching_face = new List(); + + (doc[DocData].faces as List>).forEach(face => { + // If the face has the current lowest distance, mark it as such + // Once that lowest distance is found, add the face descriptor to the faceDoc, and add the associated doc + const convered_32_array: Float32Array = new Float32Array(Array.from(face)); + const match = faceMatcher.matchDescriptor(convered_32_array); + + if (match.distance < cur_lowest_distance) { + cur_lowest_distance = match.distance; + cur_matching_face = face; + } + }); + + if (doc[DocData][`FACE DESCRIPTOR - ${this._props.faceDoc[DocData].label}`]) { + doc[DocData][`FACE DESCRIPTOR - ${this._props.faceDoc[DocData].label}`] = new List>([...(doc[DocData][`FACE DESCRIPTOR - ${this._props.faceDoc[DocData].label}`] as List>), cur_matching_face]); + } else { + doc[DocData][`FACE DESCRIPTOR - ${this._props.faceDoc[DocData].label}`] = new List>([cur_matching_face]); + } + + this._props.faceDoc[DocData].associatedDocs = new List([...DocListCast(this._props.faceDoc[DocData].associatedDocs), doc]); + this._props.faceDoc[DocData].faceDescriptors = new List>([...(this._props.faceDoc[DocData].faceDescriptors as List>), cur_matching_face]); + + //const match = faceMatcher.findBestMatch(cur_descriptor); + } + }); + return false; + } + return false; + } + + /** + * Toggles whether a Face Document displays its associated docs. + */ + @action + onDisplayClick() { + this._displayImages = !this._displayImages; + } + + /** + * Deletes a Face Document. + */ + @action + deleteFaceDocument = () => { + if (Doc.ActiveDashboard) { + Doc.ActiveDashboard[DocData].faceDocuments = new List(DocListCast(Doc.ActiveDashboard[DocData].faceDocuments).filter(doc => doc !== this._props.faceDoc)); + } + }; + + /** + * Deletes a document from a Face Document's associated docs list. + * @param doc + */ + @action + deleteAssociatedDoc = (doc: Doc) => { + this._props.faceDoc[DocData].faceDescriptors = new List>( + (this._props.faceDoc[DocData].faceDescriptors as List>).filter(fd => !(doc[DocData][`FACE DESCRIPTOR - ${this._props.faceDoc[DocData].label}`] as List>).includes(fd)) + ); + doc[DocData][`FACE DESCRIPTOR - ${this._props.faceDoc[DocData].label}`] = new List>(); + this._props.faceDoc[DocData].associatedDocs = new List(DocListCast(this._props.faceDoc[DocData].associatedDocs).filter(associatedDoc => associatedDoc !== doc)); + }; + + render() { + return ( +
this.createDropTarget(ele!)}> +
+ +
+
+

{StrCast(this._props.faceDoc[DocData].label)}

+
+ this.onDisplayClick()} + icon={this._displayImages ? : } + color={MarqueeOptionsMenu.Instance.userColor} + style={{ width: '19px' }} + /> + {this._displayImages ? ( +
+ {DocListCast(this._props.faceDoc[DocData].associatedDocs).map(doc => { + const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.'); + return ( +
+ { + await DocumentView.showDocument(doc, { willZoomCentered: true }); + }} + style={{ maxWidth: '60px', margin: '10px' }} + src={`${name}_o.${type}`} + /> +
+ { + this.deleteAssociatedDoc(doc); + }} + icon={'x'} + style={{ width: '4px' }} + size={Size.XSMALL} + /> +
+
+ ); + })} +
+ ) : ( +
+ )} +
+ ); + } +} + +@observer +export class FaceCollectionBox extends ViewBoxBaseComponent() { + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(FaceCollectionBox, fieldKey); + } + + public static Instance: FaceCollectionBox; + + @computed get currentDocs() { + if (Doc.ActiveDashboard) { + return DocListCast(Doc.ActiveDashboard[DocData].faceDocuments); + } else { + return []; + } + } + + constructor(props: any) { + super(props); + makeObservable(this); + FaceCollectionBox.Instance = this; + } + + render() { + return ( +
+ {this.currentDocs.map(doc => { + return ; + })} +
+ ); + } +} + +Docs.Prototypes.TemplateMap.set(DocumentType.FACECOLLECTION, { + layout: { view: FaceCollectionBox, dataField: 'data' }, + options: { acl: '', _width: 400 }, +}); diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx index af01d6cbc..421b5d0a6 100644 --- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx +++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx @@ -179,6 +179,7 @@ export class ImageLabelBox extends ViewBoxBaseComponent() { const labels = imageInfo.labels.split('\n'); labels.forEach(label => { label = label.replace(/^\d+\.\s*|-|\*/, '').trim(); + console.log(label); imageInfo.doc[DocData][`${label}`] = true; (imageInfo.doc[DocData].data_labels as List).push(label); }); @@ -198,7 +199,7 @@ export class ImageLabelBox extends ViewBoxBaseComponent() { for (let index = 0; index < (doc[DocData].data_labels as List).length; index++) { const label = (doc[DocData].data_labels as List)[index]; const embedding = await gptGetEmbedding(label); - doc[`data_labels_embedding_${index + 1}`] = new List(embedding); + doc[DocData][`data_labels_embedding_${index + 1}`] = new List(embedding); } } @@ -209,7 +210,7 @@ export class ImageLabelBox extends ViewBoxBaseComponent() { // For each image, loop through the labels, and calculate similarity. Associate it with the // most similar one. this._selectedImages.forEach(doc => { - const embedLists = numberRange((doc[DocData].data_labels as List).length).map(n => Array.from(NumListCast(doc[`data_labels_embedding_${n + 1}`]))); + const embedLists = numberRange((doc[DocData].data_labels as List).length).map(n => Array.from(NumListCast(doc[DocData][`data_labels_embedding_${n + 1}`]))); const bestEmbedScore = (embedding: Opt) => 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)) })) diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx index fcd38c42f..ef4622ea2 100644 --- a/src/client/views/search/FaceRecognitionHandler.tsx +++ b/src/client/views/search/FaceRecognitionHandler.tsx @@ -1,14 +1,13 @@ import * as faceapi from 'face-api.js'; -import { FaceMatcher, TinyFaceDetectorOptions } from 'face-api.js'; -import { Doc, DocListCast, NumListCast } from '../../../fields/Doc'; +import { FaceMatcher } from 'face-api.js'; +import { Doc, DocListCast } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { List } from '../../../fields/List'; -import { ObjectField } from '../../../fields/ObjectField'; -import { ImageCast, StrCast } from '../../../fields/Types'; -import { DocUtils } from '../../documents/DocUtils'; -import { Deserializable } from '../../util/SerializationHelper'; -import { DocumentView } from '../nodes/DocumentView'; +import { ImageCast, NumCast, StrCast } from '../../../fields/Types'; +/** + * A class that handles face recognition. + */ export class FaceRecognitionHandler { static _instance: FaceRecognitionHandler; private loadedModels: boolean = false; @@ -18,8 +17,12 @@ export class FaceRecognitionHandler { constructor() { FaceRecognitionHandler._instance = this; this.loadModels(); + this.examinedDocs = new Set(DocListCast(Doc.UserDoc()[DocData].examinedFaceDocs, [])); } + /** + * Loads the face detection models. + */ async loadModels() { const MODEL_URL = `/models`; await faceapi.loadFaceDetectionModel(MODEL_URL); @@ -32,19 +35,31 @@ export class FaceRecognitionHandler { return FaceRecognitionHandler._instance ?? new FaceRecognitionHandler(); } + /** + * When a document is added, look for matching face documents. + * @param doc The document being analyzed. + */ public async findMatches(doc: Doc) { if (this.loadedModels) { + // If the Dashboard doesn't have a list of face documents yet, initialize the list. if (!Doc.ActiveDashboard![DocData].faceDocuments) { Doc.ActiveDashboard![DocData].faceDocuments = new List(); } + // If the doc is currently already been examined, or it is being processed, stop examining the document. if (this.examinedDocs.has(doc) || this.processingDocs.has(doc)) { return; } + // Mark the document as being processed. this.processingDocs.add(doc); try { + if (!Doc.UserDoc()[DocData].faceDocNum) { + Doc.UserDoc()[DocData].faceDocNum = 0; + } + + // Get the image the document contains and analyze for faces. const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.'); const imageURL = `${name}_o.${type}`; @@ -52,30 +67,51 @@ export class FaceRecognitionHandler { const fullFaceDescriptions = await faceapi.detectAllFaces(img).withFaceLandmarks().withFaceDescriptors(); + doc[DocData].faces = new List>(); + + // For each face detected, find a match. for (const fd of fullFaceDescriptions) { - const match = this.findMatch(fd.descriptor); + let match = this.findMatch(fd.descriptor); + let converted_list = new List(); + if (match) { + // If a matching Face Document has been found, add the document to the Face Document's associated docs and append the face + // descriptor to the Face Document's descriptor list. const converted_array = Array.from(fd.descriptor); - const converted_list = new List(converted_array); + converted_list = new List(converted_array); match[DocData].associatedDocs = new List([...DocListCast(match[DocData].associatedDocs), doc]); match[DocData].faceDescriptors = new List>([...(match[DocData].faceDescriptors as List>), converted_list]); } else { + // If a matching Face Document has not been found, create a new Face Document. const newFaceDocument = new Doc(); const converted_array = Array.from(fd.descriptor); - const converted_list = new List(converted_array); + converted_list = new List(converted_array); newFaceDocument[DocData].faceDescriptors = new List>(); (newFaceDocument[DocData].faceDescriptors as List>).push(converted_list); - newFaceDocument[DocData].label = `Person ${DocListCast(Doc.ActiveDashboard![DocData].faceDocuments).length + 1}`; + Doc.UserDoc()[DocData].faceDocNum = NumCast(Doc.UserDoc()[DocData].faceDocNum) + 1; + newFaceDocument[DocData].label = `Face ${Doc.UserDoc()[DocData].faceDocNum}`; newFaceDocument[DocData].associatedDocs = new List([doc]); Doc.ActiveDashboard![DocData].faceDocuments = new List([...DocListCast(Doc.ActiveDashboard![DocData].faceDocuments), newFaceDocument]); + match = newFaceDocument; } + + // Assign a field in the document of the matching Face Document. + if (doc[DocData][`FACE DESCRIPTOR - ${match[DocData].label}`]) { + doc[DocData][`FACE DESCRIPTOR - ${match[DocData].label}`] = new List>([...(doc[DocData][`FACE DESCRIPTOR - ${match[DocData].label}`] as List>), converted_list]); + } else { + doc[DocData][`FACE DESCRIPTOR - ${match[DocData].label}`] = new List>([converted_list]); + } + + doc[DocData].faces = new List>([...(doc[DocData].faces as List>), converted_list]); } + // Updates the examined docs field. this.examinedDocs.add(doc); - console.log(this.examinedDocs); - - DocListCast(Doc.ActiveDashboard![DocData].faceDocuments).forEach(doc => console.log(DocListCast(doc[DocData].associatedDocs))); + if (!Doc.UserDoc()[DocData].examinedFaceDocs) { + Doc.UserDoc()[DocData].examinedFaceDocs = new List(); + } + Doc.UserDoc()[DocData].examinedFaceDocs = new List([...DocListCast(Doc.UserDoc()[DocData].examinedFaceDocs), doc]); } catch (error) { console.error('Error processing document:', error); } finally { @@ -84,6 +120,11 @@ export class FaceRecognitionHandler { } } + /** + * Finds a matching Face Document given a descriptor + * @param cur_descriptor The current descriptor whose match is being searched for. + * @returns The most similar Face Document. + */ private findMatch(cur_descriptor: Float32Array) { if (DocListCast(Doc.ActiveDashboard![DocData].faceDocuments).length < 1) { return null; @@ -95,19 +136,20 @@ export class FaceRecognitionHandler { }); const faceMatcher = new FaceMatcher(faceDescriptors, 0.6); const match = faceMatcher.findBestMatch(cur_descriptor); - if (match.label == 'unknown') { return null; } else { for (const doc of DocListCast(Doc.ActiveDashboard![DocData].faceDocuments)) { if (doc[DocData].label === match.label) { - console.log(match.label); return doc; } } } } + /** + * Loads an image + */ private loadImage = (src: string): Promise => { return new Promise((resolve, reject) => { const img = new Image(); diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 4abb23404..ebd30ceba 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -265,6 +265,7 @@ export class Doc extends RefField { 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 MyFaceCollection() { return DocCast(Doc.UserDoc().myFaceCollection); } //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 -- cgit v1.2.3-70-g09d2 From e57584a1be9d428fb40fc789494a7ac0ac14fb84 Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 19 Aug 2024 16:42:56 -0400 Subject: extensive fixes to DiagramBox --- src/client/documents/Documents.ts | 1 + src/client/util/CurrentUserUtils.ts | 16 +- .../collectionGrid/CollectionGridView.tsx | 6 +- src/client/views/nodes/DiagramBox.scss | 112 +++---- src/client/views/nodes/DiagramBox.tsx | 360 ++++++++------------- .../views/nodes/formattedText/FormattedTextBox.tsx | 3 +- 6 files changed, 206 insertions(+), 292 deletions(-) (limited to 'src/client/documents') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index d7fdf016c..b108b73db 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -256,6 +256,7 @@ export class DocumentOptions { _layout_nativeDimEditable?: BOOLt = new BoolInfo('native dimensions can be modified using document decoration reizers', false); _layout_reflowVertical?: BOOLt = new BoolInfo('permit vertical resizing with content "reflow"'); _layout_reflowHorizontal?: BOOLt = new BoolInfo('permit horizontal resizing with content reflow'); + _layout_noSidebar?: BOOLt = new BoolInfo('whether to display the sidebar toggle button'); layout_boxShadow?: string; // box-shadow css string OR "standard" to use dash standard box shadow layout_maxShown?: NUMt = new NumInfo('maximum number of children to display at one time (see multicolumnview)'); _layout_autoHeight?: BOOLt = new BoolInfo('whether document automatically resizes vertically to display contents'); diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index c99ce1832..311b86fa4 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -321,20 +321,16 @@ export class CurrentUserUtils { const rtfield = new RichTextField(JSON.stringify( {doc: {type:"doc",content:[ {type:"code_block",content:[ - {type:"text",text:"^@mermaids"}, - {type:"text",text:"\n\n"}, - {type:"text",text:"pie "}, - {type:"text",text:"title"}, - {type:"text",text:" "}, - {type:"text",text:"Minerals in my tap water"}, - {type:"text",text:"\n \"Calcium\" : "}, + {type:"text",text:`^@mermaids\n`}, + {type:"text",text:`\n pie title Minerals in my tap water`}, + {type:"text",text:`\n "Calcium" : `}, {type:"dashField",attrs:{fieldKey:"calcium",docId:"",hideKey:true,hideValue:false,editable:true}}, - {type:"text",text:"\n \"Potassium\" : "}, + {type:"text",text:`\n "Potassium" : `}, {type:"dashField",attrs:{fieldKey:"pot",docId:"",hideKey:true,hideValue:false,editable:true}}, - {type:"text",text:"\n \"Magnesium\" : 10.01"} + {type:"text",text:`\n "Magnesium" : 10.01`} ]} ]}, - selection:{type:"text",anchor:109,head:109} + selection:{type:"text",anchor:1,head:1} }), `^@mermaids pie title Minerals in my tap water diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx index 2d9191dd7..61bd0241c 100644 --- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx +++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx @@ -13,7 +13,7 @@ import { undoBatch } from '../../../util/UndoManager'; import { ContextMenu } from '../../ContextMenu'; import { ContextMenuProps } from '../../ContextMenuItem'; import { DocumentView } from '../../nodes/DocumentView'; -import { CollectionSubView } from '../CollectionSubView'; +import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView'; import './CollectionGridView.scss'; import Grid, { Layout } from './Grid'; @@ -26,7 +26,7 @@ export class CollectionGridView extends CollectionSubView() { @observable private _scroll: number = 0; // required to make sure the decorations box container updates on scroll private dropLocation: object = {}; // sets the drop location for external drops - constructor(props: any) { + constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); } @@ -200,7 +200,7 @@ export class CollectionGridView extends CollectionSubView() { whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged} onClickScript={this.onChildClickHandler} renderDepth={this._props.renderDepth + 1} - dontCenter={StrCast(this.layoutDoc.layout_dontCenter) as any} // 'y', 'x', 'xy' + dontCenter={StrCast(this.layoutDoc.layout_dontCenter) as 'x' | 'y' | 'xy'} /> ); } diff --git a/src/client/views/nodes/DiagramBox.scss b/src/client/views/nodes/DiagramBox.scss index d2749f1ad..323638bff 100644 --- a/src/client/views/nodes/DiagramBox.scss +++ b/src/client/views/nodes/DiagramBox.scss @@ -1,3 +1,5 @@ +$searchbarHeight: 50px; + .DIYNodeBox { width: 100%; height: 100%; @@ -6,83 +8,75 @@ align-items: center; justify-content: center; - .DIYNodeBox-wrapper { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - .DIYNodeBox { - /* existing code */ - - .DIYNodeBox-iframe { - height: 100%; - width: 100%; - border: none; + .DIYNodeBox { + /* existing code */ - } + .DIYNodeBox-iframe { + height: 100%; + width: 100%; + border: none; } + } - .search-bar { - display: flex; - justify-content: center; - align-items: center; - width: 100%; - padding: 10px; + .DIYNodeBox-searchbar { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: $searchbarHeight; + padding: 10px; - input[type="text"] { - flex: 1; - margin-right: 10px; - } + input[type='text'] { + flex: 1; + margin-right: 10px; + } - button { - padding: 5px 10px; - } + button { + padding: 5px 10px; } + } - .content { + .DIYNodeBox-content { + flex: 1; + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: calc(100% - $searchbarHeight); + .diagramBox { flex: 1; display: flex; justify-content: center; align-items: center; - width:100%; - height:100%; - .diagramBox{ + width: 100%; + height: 100%; + svg { flex: 1; display: flex; justify-content: center; align-items: center; - width:100%; - height:100%; - svg{ - flex: 1; - display: flex; - justify-content: center; - align-items: center; - width:100%; - height:100%; - } + width: 100%; + height: 100%; } } + } - .loading-circle { - position: relative; - width: 50px; - height: 50px; - border-radius: 50%; - border: 3px solid #ccc; - border-top-color: #333; - animation: spin 1s infinite linear; - } + .loading-circle { + position: relative; + width: 50px; + height: 50px; + border-radius: 50%; + border: 3px solid #ccc; + border-top-color: #333; + animation: spin 1s infinite linear; + } - @keyframes spin { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } + @keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); } } -} \ No newline at end of file +} diff --git a/src/client/views/nodes/DiagramBox.tsx b/src/client/views/nodes/DiagramBox.tsx index 32969fa53..36deb2d8d 100644 --- a/src/client/views/nodes/DiagramBox.tsx +++ b/src/client/views/nodes/DiagramBox.tsx @@ -1,284 +1,198 @@ import mermaid from 'mermaid'; -import { action, makeObservable, observable, reaction } from 'mobx'; +import { action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast } from '../../../fields/Doc'; -import { List } from '../../../fields/List'; +import { DocData } from '../../../fields/DocSymbols'; import { RichTextField } from '../../../fields/RichTextField'; -import { DocCast, NumCast } from '../../../fields/Types'; +import { Cast, DocCast, NumCast } from '../../../fields/Types'; +import { Gestures } from '../../../pen-gestures/GestureTypes'; import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT'; import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; import { LinkManager } from '../../util/LinkManager'; +import { undoable } from '../../util/UndoManager'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { InkingStroke } from '../InkingStroke'; import './DiagramBox.scss'; import { FieldView, FieldViewProps } from './FieldView'; +import { FormattedTextBox } from './formattedText/FormattedTextBox'; @observer export class DiagramBox extends ViewBoxAnnotatableComponent() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DiagramBox, fieldKey); } - private _ref: React.RefObject = React.createRef(); - private _dragRef = React.createRef(); + static isPointInBox = (box: Doc, pt: number[]): boolean => { + if (typeof pt[0] === 'number' && typeof box.x === 'number' && typeof box.y === 'number' && typeof pt[1] === 'number') { + return pt[0] < box.x + NumCast(box.width) && pt[0] > box.x && pt[1] > box.y && pt[1] < box.y + NumCast(box.height); + } + return false; + }; + constructor(props: FieldViewProps) { super(props); makeObservable(this); } - @observable inputValue = ''; - @observable loading = false; - @observable errorMessage = ''; - @observable mermaidCode = ''; + @observable _showCode = false; + @observable _inputValue = ''; + @observable _generating = false; + @observable _errorMessage = ''; - @action handleInputChange = (e: React.ChangeEvent) => { - this.inputValue = e.target.value; - }; - async componentDidMount() { + @computed get mermaidcode() { + return Cast(this.Document[DocData].text, RichTextField, null)?.Text ?? ''; + } + + componentDidMount() { this._props.setContentViewBox?.(this); mermaid.initialize({ securityLevel: 'loose', startOnLoad: true, flowchart: { useMaxWidth: true, htmlLabels: true, curve: 'cardinal' }, }); - this.mermaidCode = 'asdasdasd'; - const docArray: Doc[] = DocListCast(this.Document.data); - let mermaidCodeDoc = docArray.filter(doc => doc.type === 'rich text'); - mermaidCodeDoc = mermaidCodeDoc.filter(doc => (doc.text as RichTextField).Text === 'mermaidCodeTitle'); - if (mermaidCodeDoc[0]) { - if (typeof mermaidCodeDoc[0].title === 'string') { - console.log(mermaidCodeDoc[0].title); - if (mermaidCodeDoc[0].title !== '') { - this.renderMermaidAsync(mermaidCodeDoc[0].title); - } - } - } - // this will create a text doc far away where the user cant to save the mermaid code, where it will then be accessed when flipped to the diagram box side - // the code is stored in the title since it is much easier to change than in the text - else { - DocumentManager.Instance.AddViewRenderedCb(this.Document, docViewForYourCollection => { - if (docViewForYourCollection && docViewForYourCollection.ComponentView) { - if (docViewForYourCollection.ComponentView.addDocument && docViewForYourCollection.ComponentView.removeDocument) { - const newDoc = Docs.Create.TextDocument('mermaidCodeTitle', { title: '', x: 9999 + NumCast(this.layoutDoc._width), y: 9999 }); - docViewForYourCollection.ComponentView?.addDocument(newDoc); - } - } - }); - } - console.log(this.Document.title); - // this is so that ever time a new doc, text node or ink node, is created, this.createMermaidCode will run which will create a save + // when a new doc/text/ink/shape is created in the freeform view, this generates the corresponding mermaid diagram code reaction( () => DocListCast(this.Document.data), - () => this.convertDrawingToMermaidCode(), + docArray => docArray.length && this.convertDrawingToMermaidCode(docArray), { fireImmediately: true } ); } - renderMermaid = async (str: string) => { + renderMermaid = (str: string) => { try { - const { svg, bindFunctions } = await this.mermaidDiagram(str); - return { svg, bindFunctions }; + return mermaid.render('graph' + Date.now(), str); } catch (error) { - console.error('Error rendering mermaid diagram:', error); return { svg: '', bindFunctions: undefined }; } }; - mermaidDiagram = async (str: string) => mermaid.render('graph' + Date.now(), str); - async renderMermaidAsync(mermaidCode: string) { + renderMermaidAsync = async (mermaidCode: string, dashDiv: HTMLDivElement) => { try { const { svg, bindFunctions } = await this.renderMermaid(mermaidCode); - const dashDiv = document.getElementById('dashDiv' + this.Document.title); - if (dashDiv) { - dashDiv.innerHTML = svg; - if (bindFunctions) { - bindFunctions(dashDiv); - } - } + dashDiv.innerHTML = svg; + bindFunctions?.(dashDiv); } catch (error) { console.error('Error rendering Mermaid:', error); } - } - @action handleRenderClick = () => { - this.generateMermaidCode(); }; - @action async generateMermaidCode() { - console.log('Generating Mermaid Code'); - this.loading = true; - let prompt = ''; - // let docArray: Doc[] = DocListCast(this.Document.data); - // let mermaidCodeDoc = docArray.filter(doc => doc.type == 'rich text') - // mermaidCodeDoc=mermaidCodeDoc.filter(doc=>(doc.text as RichTextField).Text=='mermaidCodeTitle') - // if(mermaidCodeDoc[0]){ - // console.log(mermaidCodeDoc[0].title) - // if(typeof mermaidCodeDoc[0].title=='string'){ - // console.log(mermaidCodeDoc[0].title) - // if(mermaidCodeDoc[0].title!=""){ - // prompt="Edit this code "+this.inputValue+": "+mermaidCodeDoc[0].title - // console.log("you have to see me") - // } - // } - // } - // else{ - prompt = 'Write this in mermaid code and only give me the mermaid code: ' + this.inputValue; - console.log('there is no text save'); - // } - const res = await gptAPICall(prompt, GPTCallType.MERMAID); - this.loading = false; - if (res === 'Error connecting with API.') { - // If GPT call failed - console.error('GPT call failed'); - this.errorMessage = 'GPT call failed; please try again.'; - } else if (res !== null) { - // If GPT call succeeded, set htmlCode;;; TODO: check if valid html - if (this.isValidCode(res)) { - this.mermaidCode = res; - console.log('GPT call succeeded:' + res); - this.errorMessage = ''; - } else { - console.error('GPT call succeeded but invalid html; please try again.'); - this.errorMessage = 'GPT call succeeded but invalid html; please try again.'; - } - } - this.renderMermaidAsync.call(this, this.removeWords(this.mermaidCode)); - this.loading = false; - } - isValidCode = (html: string) => true; - removeWords(inputStrIn: string) { - const inputStr = inputStrIn.replace('```mermaid', ''); - return inputStr.replace('```', ''); - } + + setMermaidCode = undoable((res: string) => { + this.Document[DocData].text = new RichTextField( + JSON.stringify({ + doc: { + type: 'doc', + content: [ + { + type: 'code_block', + content: [ + { type: 'text', text: `^@mermaids\n` }, + { type: 'text', text: this.removeWords(res) }, + ], + }, + ], + }, + selection: { type: 'text', anchor: 1, head: 1 }, + }), + res + ); + }, 'set mermaid code'); + + generateMermaidCode = action(() => { + this._generating = true; + const prompt = 'Write this in mermaid code and only give me the mermaid code: ' + this._inputValue; + gptAPICall(prompt, GPTCallType.MERMAID).then( + action(res => { + this._generating = false; + if (res === 'Error connecting with API.') { + this._errorMessage = 'GPT call failed; please try again.'; + } + // If GPT call succeeded, set mermaid code on Doc which will trigger a rendering if _showCode is false + else if (res && this.isValidCode(res)) { + this.setMermaidCode(res); + this._errorMessage = ''; + } else { + this._errorMessage = 'GPT call succeeded but invalid html; please try again.'; + } + }) + ); + }); + isValidCode = (html: string) => (html ? true : false); + removeWords = (inputStrIn: string) => inputStrIn.replace('```mermaid', '').replace(`^@mermaids`, '').replace('```', ''); + // method to convert the drawings on collection node side the mermaid code - async convertDrawingToMermaidCode() { - let mermaidCode = ''; - let diagramExists = false; - if (this.Document.data instanceof List) { - const docArray: Doc[] = DocListCast(this.Document.data); - const rectangleArray = docArray.filter(doc => doc.title === 'rectangle' || doc.title === 'circle'); - const lineArray = docArray.filter(doc => doc.title === 'line' || doc.title === 'stroke'); - const textArray = docArray.filter(doc => doc.type === 'rich text'); - const timeoutPromise = () => - new Promise(resolve => { - setTimeout(resolve, 0); - }); - await timeoutPromise(); - const inkStrokeArray = lineArray.map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())).filter(inkView => inkView?.ComponentView instanceof InkingStroke); - console.log(inkStrokeArray.length); - console.log(lineArray.length); - if (inkStrokeArray[0] && inkStrokeArray.length === lineArray.length) { - mermaidCode = 'graph TD;'; - const inkingStrokeArray = inkStrokeArray.map(stroke => stroke?.ComponentView); - for (let i = 0; i < rectangleArray.length; i++) { - const rectangle = rectangleArray[i]; - for (let j = 0; j < lineArray.length; j++) { - const inkScaleX = (inkingStrokeArray[j] as InkingStroke)?.inkScaledData().inkScaleX; - const inkScaleY = (inkingStrokeArray[j] as InkingStroke)?.inkScaledData().inkScaleY; - const inkStrokeXArray = (inkingStrokeArray[j] as InkingStroke) - ?.inkScaledData() - .inkData.map(coord => coord.X) - .map(doc => doc * inkScaleX); - const inkStrokeYArray = (inkingStrokeArray[j] as InkingStroke) - ?.inkScaledData() - .inkData.map(coord => coord.Y) - .map(doc => doc * inkScaleY); - console.log(inkingStrokeArray.length); - console.log(lineArray.length); - // need to minX and minY to since the inkStroke.x and.y is not relative to the doc. so I have to do some calcluations - const minX: number = Math.min(...inkStrokeXArray); - const minY: number = Math.min(...inkStrokeYArray); - const startX = inkStrokeXArray[0] - minX + (lineArray[j]?.x as number); - const startY = inkStrokeYArray[0] - minY + (lineArray[j]?.y as number); - const endX = inkStrokeXArray[inkStrokeXArray.length - 1] - minX + (lineArray[j].x as number); - const endY = inkStrokeYArray[inkStrokeYArray.length - 1] - minY + (lineArray[j].y as number); - if (this.isPointInBox(rectangle, [startX, startY])) { - for (let k = 0; k < rectangleArray.length; k++) { - const rectangle2 = rectangleArray[k]; - if (this.isPointInBox(rectangle2, [endX, endY]) && typeof rectangle.x === 'number' && typeof rectangle2.x === 'number') { - diagramExists = true; - const linkedDocs: Doc[] = LinkManager.Instance.getAllRelatedLinks(lineArray[j]).map(d => DocCast(LinkManager.getOppositeAnchor(d, lineArray[j]))); - console.log(linkedDocs.length); - if (linkedDocs.length !== 0) { - const linkedText = (linkedDocs[0].text as RichTextField).Text; - mermaidCode += Math.abs(rectangle.x) + this.getTextInBox(rectangle, textArray) + '-->|' + linkedText + '|' + Math.abs(rectangle2.x) + this.getTextInBox(rectangle2, textArray) + ';'; - } else { - mermaidCode += Math.abs(rectangle.x) + this.getTextInBox(rectangle, textArray) + '-->' + Math.abs(rectangle2.x) + this.getTextInBox(rectangle2, textArray) + ';'; - } - } + convertDrawingToMermaidCode = async (docArray: Doc[]) => { + const rectangleArray = docArray.filter(doc => doc.title === Gestures.Rectangle || doc.title === Gestures.Circle); + const lineArray = docArray.filter(doc => doc.title === Gestures.Line || doc.title === Gestures.Stroke); + const textArray = docArray.filter(doc => doc.type === DocumentType.RTF); + await new Promise(resolve => setTimeout(resolve)); + const inkStrokeArray = lineArray.map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())).filter(inkView => inkView?.ComponentView instanceof InkingStroke); + if (inkStrokeArray[0] && inkStrokeArray.length === lineArray.length) { + let mermaidCode = `graph TD \n`; + const inkingStrokeArray = inkStrokeArray.map(stroke => stroke?.ComponentView as InkingStroke).filter(stroke => stroke); + for (const rectangle of rectangleArray) { + for (const inkStroke of inkingStrokeArray) { + const inkData = inkStroke.inkScaledData(); + const { inkScaleX, inkScaleY } = inkData; + const inkStrokeXArray = inkData.inkData.map(coord => coord.X * inkScaleX); + const inkStrokeYArray = inkData.inkData.map(coord => coord.Y * inkScaleY); + // need to minX and minY to since the inkStroke.x and.y is not relative to the doc. so I have to do some calcluations + const offX = Math.min(...inkStrokeXArray) - NumCast(inkStroke.Document.x); + const offY = Math.min(...inkStrokeYArray) - NumCast(inkStroke.Document.y); + + const startX = inkStrokeXArray[0] - offX; + const startY = inkStrokeYArray[0] - offY; + const endX = inkStrokeXArray.lastElement() - offX; + const endY = inkStrokeYArray.lastElement() - offY; + if (DiagramBox.isPointInBox(rectangle, [startX, startY])) { + for (const rectangle2 of rectangleArray) { + if (DiagramBox.isPointInBox(rectangle2, [endX, endY])) { + const linkedDocs = LinkManager.Instance.getAllRelatedLinks(inkStroke.Document).map(d => DocCast(LinkManager.getOppositeAnchor(d, inkStroke.Document))); + const linkedDocText = Cast(linkedDocs[0]?.text, RichTextField, null)?.Text; + const linkText = linkedDocText ? `|${linkedDocText}|` : ''; + mermaidCode += ' ' + Math.abs(NumCast(rectangle.x)) + this.getTextInBox(rectangle, textArray) + '-->' + linkText + Math.abs(NumCast(rectangle2.x)) + this.getTextInBox(rectangle2, textArray) + `\n`; } } } } - // this will save the text - DocumentManager.Instance.AddViewRenderedCb(this.Document, docViewForYourCollection => { - if (docViewForYourCollection && docViewForYourCollection.ComponentView) { - if (docViewForYourCollection.ComponentView.addDocument && docViewForYourCollection.ComponentView.removeDocument) { - let docs: Doc[] = DocListCast(this.Document.data); - docs = docs.filter(doc => doc.type === 'rich text'); - const mermaidCodeDoc = docs.filter(doc => (doc.text as RichTextField).Text === 'mermaidCodeTitle'); - if (mermaidCodeDoc[0]) { - if (diagramExists) { - mermaidCodeDoc[0].title = mermaidCode; - } else { - mermaidCodeDoc[0].title = ''; - } - } - } - } - }); + this.setMermaidCode(mermaidCode); } } - } - testInkingStroke = () => { - if (this.Document.data instanceof List) { - const docArray: Doc[] = DocListCast(this.Document.data); - const lineArray = docArray.filter(doc => doc.title === 'line' || doc.title === 'stroke'); - setTimeout(() => { - const inkStrokeArray = lineArray.map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())).filter(inkView => inkView?.ComponentView instanceof InkingStroke); - console.log(inkStrokeArray); - }); - } }; - getTextInBox = (box: Doc, richTextArray: Doc[]): string => { - for (let i = 0; i < richTextArray.length; i++) { - const textDoc = richTextArray[i]; - if (typeof textDoc.x === 'number' && typeof textDoc.y === 'number' && typeof box.x === 'number' && typeof box.height === 'number' && typeof box.width === 'number' && typeof box.y === 'number') { - if (textDoc.x > box.x && textDoc.x < box.x + box.width && textDoc.y > box.y && textDoc.y < box.y + box.height) { - if (box.title === 'rectangle') { - return '(' + ((textDoc.text as RichTextField)?.Text ?? '') + ')'; - } - if (box.title === 'circle') { - return '((' + ((textDoc.text as RichTextField)?.Text ?? '') + '))'; - } - } + + getTextInBox = (box: Doc, richTextArray: Doc[]) => { + for (const textDoc of richTextArray) { + if (DiagramBox.isPointInBox(box, [NumCast(textDoc.x), NumCast(textDoc.y)])) { + switch (box.title) { + case Gestures.Rectangle: return '(' + ((textDoc.text as RichTextField)?.Text ?? '') + ')'; + case Gestures.Circle: return '((' + ((textDoc.text as RichTextField)?.Text ?? '') + '))'; + default: + } // prettier-ignore } } return '( )'; }; - isPointInBox = (box: Doc, line: number[]): boolean => { - if (typeof line[0] === 'number' && typeof box.x === 'number' && typeof box.width === 'number' && typeof box.height === 'number' && typeof box.y === 'number' && typeof line[1] === 'number') { - return line[0] < box.x + box.width && line[0] > box.x && line[1] > box.y && line[1] < box.y + box.height; - } - return false; - }; render() { return ( -
-
-
- - -
-
- {this.mermaidCode ? ( -
- ) : ( -
{this.loading ?
:
{this.errorMessage ? this.errorMessage : 'Insert prompt to generate diagram'}
}
- )} -
+
+
+ e.key === 'Enter' && this.generateMermaidCode())} onChange={action(e => (this._inputValue = e.target.value))} /> + + (this._showCode = !this._showCode))} /> +
+
+ {this._showCode ? ( + + ) : this._generating ? ( +
+ ) : ( +
r && this.renderMermaidAsync.call(this, this.removeWords(this.mermaidcode), r)}> + {this._errorMessage || 'Type a prompt to generate a diagram'} +
+ )}
); @@ -286,6 +200,14 @@ export class DiagramBox extends ViewBoxAnnotatableComponent() { } Docs.Prototypes.TemplateMap.set(DocumentType.DIAGRAM, { - layout: { view: DiagramBox, dataField: 'dadta' }, - options: { _height: 300, _layout_fitWidth: true, _layout_nativeDimEditable: true, _layout_reflowVertical: true, waitForDoubleClickToClick: 'always', systemIcon: 'BsGlobe' }, + layout: { view: DiagramBox, dataField: 'data' }, + options: { + _height: 300, // + _layout_fitWidth: true, + _layout_nativeDimEditable: true, + _layout_reflowVertical: true, + _layout_reflowHorizontal: true, + waitForDoubleClickToClick: 'always', + systemIcon: 'BsGlobe', + }, }); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index e21902fdd..a88bd8920 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1464,7 +1464,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - this._editorView?.dispatch(tx.deleteSelection().addStoredMark(mark)); + this._editorView?.dispatch(tx.addStoredMark(mark)); }); this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data } else { @@ -2090,6 +2090,7 @@ Docs.Prototypes.TemplateMap.set(DocumentType.RTF, { _layout_nativeDimEditable: true, _layout_reflowVertical: true, _layout_reflowHorizontal: true, + _layout_noSidebar: true, defaultDoubleClick: 'ignore', systemIcon: 'BsFileEarmarkTextFill', }, -- cgit v1.2.3-70-g09d2 From cdb21e036ff65d63991a53798133407be1d5755f Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 20 Aug 2024 21:30:32 -0400 Subject: fixed error handling of images too big to load. cleaned up facecollectionbox. changed metadata field naming to match conventions. --- src/client/Network.ts | 11 +-- src/client/documents/DocUtils.ts | 2 +- .../collectionFreeForm/FaceCollectionBox.tsx | 88 ++++++++------------ .../collections/collectionFreeForm/MarqueeView.tsx | 97 +++------------------- src/client/views/nodes/FaceRectangles.tsx | 3 +- src/client/views/search/FaceRecognitionHandler.tsx | 64 +++++++------- 6 files changed, 87 insertions(+), 178 deletions(-) (limited to 'src/client/documents') diff --git a/src/client/Network.ts b/src/client/Network.ts index 8876d8190..17f8a6534 100644 --- a/src/client/Network.ts +++ b/src/client/Network.ts @@ -1,3 +1,4 @@ +import formidable from 'formidable'; import * as requestPromise from 'request-promise'; import { ClientUtils } from '../ClientUtils'; import { Utils } from '../Utils'; @@ -49,15 +50,9 @@ export namespace Networking { if (!fileguidpairs.length) { return []; } - const maxFileSize = 5000000; + const maxFileSize = 6000000; if (fileguidpairs.some(f => f.file.size > maxFileSize)) { - return new Promise[]>(res => { - res([{ - source: { size: 0, filepath: '', mimetype: '', originalFilename: '', newFilename: '',hashAlgorithm: false, - toJSON: () => ({ size: 0, filepath: '', mimetype: '', originalFilename: '', newFilename: '',name: '', length: 0, mtime: new Date(), type: '' }) }, - result: { name: '', message: `max file size (${maxFileSize / 1000000}MB) exceeded` } - }]) // prettier-ignore - }); + return new Promise[]>(res => res([{ source: { newFilename: '', mimetype: '' } as formidable.File, result: new Error(`max file size (${maxFileSize / 1000000}MB) exceeded`) }])); } formData.set('fileguids', fileguidpairs.map(pair => pair.guid).join(';')); formData.set('filesize', fileguidpairs.reduce((sum, pair) => sum + pair.file.size, 0).toString()); diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index a503d732b..35d835f1f 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -739,7 +739,7 @@ export namespace DocUtils { const { source: { newFilename, mimetype }, result, - } = upfiles.lastElement() ?? { source: { newFilename: '', mimetype: '' }, result: { message: 'upload failed' } }; + } = upfiles.lastElement() ?? { source: { newFilename: '', mimetype: '' }, result: new Error('upload failed') }; if (result instanceof Error) { if (overwriteDoc) { overwriteDoc.loadingError = result.message; diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx index 50b91e8fe..d5a2809dc 100644 --- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx +++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx @@ -11,17 +11,20 @@ import { Doc, DocListCast } from '../../../../fields/Doc'; import { DocData } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; -import { ImageCast, StrCast } from '../../../../fields/Types'; +import { listSpec } from '../../../../fields/Schema'; +import { Cast, ImageCast, StrCast } from '../../../../fields/Types'; import { DocumentType } from '../../../documents/DocumentTypes'; import { Docs } from '../../../documents/Documents'; import { DragManager } from '../../../util/DragManager'; import { SnappingManager } from '../../../util/SnappingManager'; +import { undoable } from '../../../util/UndoManager'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { DocumentView } from '../../nodes/DocumentView'; import { FieldView, FieldViewProps } from '../../nodes/FieldView'; import './FaceCollectionBox.scss'; import { MarqueeOptionsMenu } from './MarqueeOptionsMenu'; +import { FaceRecognitionHandler } from '../../search/FaceRecognitionHandler'; interface FaceDocumentProps { faceDoc: Doc; @@ -32,17 +35,15 @@ interface FaceDocumentProps { */ @observer export class FaceDocumentItem extends ObservableReactComponent { - private ref: React.RefObject; - @observable _displayImages: boolean = true; private _dropDisposer?: DragManager.DragDropDisposer; - private _inputRef = React.createRef(); constructor(props: FaceDocumentProps) { super(props); makeObservable(this); - this.ref = React.createRef(); } + @observable _displayImages: boolean = true; + protected createDropTarget = (ele: HTMLDivElement) => { this._dropDisposer?.(); ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this._props.faceDoc)); @@ -54,20 +55,20 @@ export class FaceDocumentItem extends ObservableReactComponent doc.type === DocumentType.IMG); filteredDocs.forEach(doc => { // If the current Face Document has no items, and the doc has more than one face descriptor, don't let the user add the document first. - if ((this._props.faceDoc[DocData].faceDescriptors as List>).length === 0 && (doc[DocData].faces as List>).length > 1) { + if ((this._props.faceDoc[DocData].face_descriptors as List>).length === 0 && (doc[DocData][FaceRecognitionHandler.FacesField(doc)] as List>).length > 1) { alert('Cannot add a document with multiple faces as the first item!'); } else { // Loop through the documents' face descriptors. // Choose the face with the smallest distance to add. - const float32Array = (this._props.faceDoc[DocData].faceDescriptors as List>).map(faceDescriptor => new Float32Array(Array.from(faceDescriptor))); - const labeledFaceDescriptor = new faceapi.LabeledFaceDescriptors(StrCast(this._props.faceDoc[DocData].label), float32Array); + const float32Array = (this._props.faceDoc[DocData].face_descriptors as List>).map(faceDescriptor => new Float32Array(Array.from(faceDescriptor))); + const labeledFaceDescriptor = new faceapi.LabeledFaceDescriptors(StrCast(this._props.faceDoc[DocData].face_label), float32Array); const faceDescriptors: faceapi.LabeledFaceDescriptors[] = [labeledFaceDescriptor]; const faceMatcher = new FaceMatcher(faceDescriptors, 1); let cur_lowest_distance = 1; let cur_matching_face = new List(); - (doc[DocData].faces as List>).forEach(face => { + (doc[DocData][FaceRecognitionHandler.FacesField(doc)] as List>).forEach(face => { // If the face has the current lowest distance, mark it as such // Once that lowest distance is found, add the face descriptor to the faceDoc, and add the associated doc const convered_32_array: Float32Array = new Float32Array(Array.from(face)); @@ -79,16 +80,15 @@ export class FaceDocumentItem extends ObservableReactComponent>([...(doc[DocData][`FACE DESCRIPTOR - ${this._props.faceDoc[DocData].label}`] as List>), cur_matching_face]); + const faceFieldKey = FaceRecognitionHandler.FaceField(doc, this._props.faceDoc); + if (doc[DocData][faceFieldKey]) { + Cast(doc[DocData][faceFieldKey], listSpec('number'), null).push(cur_matching_face as unknown as number); // items are lists of numbers, not numbers, but type system can't handle that } else { - doc[DocData][`FACE DESCRIPTOR - ${this._props.faceDoc[DocData].label}`] = new List>([cur_matching_face]); + doc[DocData][faceFieldKey] = new List>([cur_matching_face]); } - this._props.faceDoc[DocData].associatedDocs = new List([...DocListCast(this._props.faceDoc[DocData].associatedDocs), doc]); - this._props.faceDoc[DocData].faceDescriptors = new List>([...(this._props.faceDoc[DocData].faceDescriptors as List>), cur_matching_face]); - - //const match = faceMatcher.findBestMatch(cur_descriptor); + Doc.AddDocToList(this._props.faceDoc[DocData], 'face_docList', doc); + Cast(this._props.faceDoc[DocData].face_descriptors, listSpec('number'), null).push(cur_matching_face as unknown as number); // items are lists of numbers, not numbers, but type system can't handle that } }); return false; @@ -107,12 +107,11 @@ export class FaceDocumentItem extends ObservableReactComponent { + deleteFaceDocument = undoable(() => { if (Doc.ActiveDashboard) { - Doc.ActiveDashboard[DocData].faceDocuments = new List(DocListCast(Doc.ActiveDashboard[DocData].faceDocuments).filter(doc => doc !== this._props.faceDoc)); + Doc.RemoveDocFromList(Doc.ActiveDashboard[DocData], 'faceDocuments', this._props.faceDoc); } - }; + }, 'remove face'); /** * Deletes a document from a Face Document's associated docs list. @@ -120,60 +119,44 @@ export class FaceDocumentItem extends ObservableReactComponent { - this._props.faceDoc[DocData].faceDescriptors = new List>( - (this._props.faceDoc[DocData].faceDescriptors as List>).filter(fd => !(doc[DocData][`FACE DESCRIPTOR - ${this._props.faceDoc[DocData].label}`] as List>).includes(fd)) + this._props.faceDoc[DocData].face_descriptors = new List>( + (this._props.faceDoc[DocData].face_descriptors as List>).filter(fd => !(doc[DocData][FaceRecognitionHandler.FaceField(doc, this._props.faceDoc)] as List>).includes(fd)) ); - doc[DocData][`FACE DESCRIPTOR - ${this._props.faceDoc[DocData].label}`] = new List>(); - this._props.faceDoc[DocData].associatedDocs = new List(DocListCast(this._props.faceDoc[DocData].associatedDocs).filter(associatedDoc => associatedDoc !== doc)); + doc[DocData][FaceRecognitionHandler.FaceField(doc, this._props.faceDoc)] = new List>(); + Doc.RemoveDocFromList(this._props.faceDoc[DocData], 'face_docList', doc); }; render() { return (
this.createDropTarget(ele!)}>
- +
-

{StrCast(this._props.faceDoc[DocData].label)}

+

{StrCast(this._props.faceDoc[DocData].face_label)}

this.onDisplayClick()} - icon={this._displayImages ? : } + icon={} color={MarqueeOptionsMenu.Instance.userColor} style={{ width: '19px' }} /> {this._displayImages ? (
- {DocListCast(this._props.faceDoc[DocData].associatedDocs).map(doc => { + {DocListCast(this._props.faceDoc[DocData].face_docList).map(doc => { const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.'); return (
- { - await DocumentView.showDocument(doc, { willZoomCentered: true }); - }} - style={{ maxWidth: '60px', margin: '10px' }} - src={`${name}_o.${type}`} - /> + DocumentView.showDocument(doc, { willZoomCentered: true })} style={{ maxWidth: '60px', margin: '10px' }} src={`${name}_o.${type}`} />
- { - this.deleteAssociatedDoc(doc); - }} - icon={'x'} - style={{ width: '4px' }} - size={Size.XSMALL} - /> + this.deleteAssociatedDoc(doc)} icon={'x'} style={{ width: '4px' }} size={Size.XSMALL} />
); })}
- ) : ( -
- )} + ) : null}
); } @@ -190,9 +173,8 @@ export class FaceCollectionBox extends ViewBoxBaseComponent() { @computed get currentDocs() { if (Doc.ActiveDashboard) { return DocListCast(Doc.ActiveDashboard[DocData].faceDocuments); - } else { - return []; } + return []; } constructor(props: FieldViewProps) { @@ -204,9 +186,9 @@ export class FaceCollectionBox extends ViewBoxBaseComponent() { render() { return (
- {this.currentDocs.map(doc => { - return ; - })} + {this.currentDocs.map(doc => ( + + ))}
); } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index d7a41df64..6fee076ee 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,28 +1,24 @@ -/* eslint-disable jsx-a11y/no-static-element-interactions */ -import similarity from 'compute-cosine-similarity'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { ClientUtils, lightOrDark, returnFalse } from '../../../../ClientUtils'; -import { intersectRect, numberRange } from '../../../../Utils'; -import { Doc, DocListCast, NumListCast, Opt } from '../../../../fields/Doc'; +import { intersectRect } from '../../../../Utils'; +import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; import { AclAdmin, AclAugment, AclEdit, DocData } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; -import { InkData, InkField, InkTool } from '../../../../fields/InkField'; +import { InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; -import { RichTextField } from '../../../../fields/RichTextField'; -import { Cast, FieldValue, ImageCast, NumCast, StrCast } from '../../../../fields/Types'; +import { Cast, NumCast, StrCast } from '../../../../fields/Types'; import { ImageField } from '../../../../fields/URLField'; import { GetEffectiveAcl } from '../../../../fields/util'; -import { gptGetEmbedding, gptImageLabel } from '../../../apis/gpt/GPT'; -import { CognitiveServices } from '../../../cognitive_services/CognitiveServices'; import { DocUtils } from '../../../documents/DocUtils'; -import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; +import { DocumentType } from '../../../documents/DocumentTypes'; import { Docs, DocumentOptions } from '../../../documents/Documents'; import { SnappingManager, freeformScrollMode } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; import { UndoManager, undoBatch } from '../../../util/UndoManager'; import { ContextMenu } from '../../ContextMenu'; +import { MainView } from '../../MainView'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { MarqueeViewBounds } from '../../PinFuncs'; import { PreviewCursor } from '../../PreviewCursor'; @@ -30,15 +26,10 @@ import { DocumentView } from '../../nodes/DocumentView'; import { OpenWhere } from '../../nodes/OpenWhere'; import { pasteImageBitmap } from '../../nodes/WebBoxRenderer'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; -import { CollectionCardView } from '../CollectionCardDeckView'; import { SubCollectionViewProps } from '../CollectionSubView'; -import { CollectionFreeFormView } from './CollectionFreeFormView'; -import { ImageLabelHandler } from './ImageLabelHandler'; +import { ImageLabelBoxData } from './ImageLabelBox'; import { MarqueeOptionsMenu } from './MarqueeOptionsMenu'; import './MarqueeView.scss'; -import { MainView } from '../../MainView'; -import { ImageLabelBox, ImageLabelBoxData } from './ImageLabelBox'; -import { SearchBox } from '../../search/SearchBox'; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -68,7 +59,7 @@ export class MarqueeView extends ObservableReactComponent { - const selected = this.marqueeSelect(false); - if (e instanceof KeyboardEvent ? e.key === 'i' : true) { - const inks = selected.filter(s => s.type === DocumentType.INK); - const setDocs = selected.filter(s => s.type === DocumentType.RTF && s.color); - const sets = setDocs.map(sd => Cast(sd.data, RichTextField)?.Text as string); - const colors = setDocs.map(sd => FieldValue(sd.color) as string); - const wordToColor = new Map(); - sets.forEach((st: string, i: number) => st.split(',').forEach(word => wordToColor.set(word, colors[i]))); - const strokes: InkData[] = []; - inks.filter(i => Cast(i.data, InkField)).forEach(i => { - const d = Cast(i.data, InkField, null); - const left = Math.min(...(d?.inkData.map(pd => pd.X) ?? [0])); - const top = Math.min(...(d?.inkData.map(pd => pd.Y) ?? [0])); - strokes.push(d.inkData.map(pd => ({ X: pd.X + NumCast(i.x) - left, Y: pd.Y + NumCast(i.y) - top }))); - }); - CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then(results => { - // const wordResults = results.filter((r: any) => r.category === "inkWord"); - // for (const word of wordResults) { - // const indices: number[] = word.strokeIds; - // indices.forEach(i => { - // if (wordToColor.has(word.recognizedText.toLowerCase())) { - // inks[i].color = wordToColor.get(word.recognizedText.toLowerCase()); - // } - // else { - // for (const alt of word.alternates) { - // if (wordToColor.has(alt.recognizedString.toLowerCase())) { - // inks[i].color = wordToColor.get(alt.recognizedString.toLowerCase()); - // break; - // } - // } - // } - // }) - // } - // const wordResults = results.filter((r: any) => r.category === "inkWord"); - // for (const word of wordResults) { - // const indices: number[] = word.strokeIds; - // indices.forEach(i => { - // const otherInks: Doc[] = []; - // indices.forEach(i2 => i2 !== i && otherInks.push(inks[i2])); - // inks[i].relatedInks = new List(otherInks); - // const uniqueColors: string[] = []; - // Array.from(wordToColor.values()).forEach(c => uniqueColors.indexOf(c) === -1 && uniqueColors.push(c)); - // inks[i].alternativeColors = new List(uniqueColors); - // if (wordToColor.has(word.recognizedText.toLowerCase())) { - // inks[i].color = wordToColor.get(word.recognizedText.toLowerCase()); - // } - // else if (word.alternates) { - // for (const alt of word.alternates) { - // if (wordToColor.has(alt.recognizedString.toLowerCase())) { - // inks[i].color = wordToColor.get(alt.recognizedString.toLowerCase()); - // break; - // } - // } - // } - // }); - // } - const lines = results.filter((r: any) => r.category === 'line'); - const text = lines.map((l: any) => l.recognizedText).join('\r\n'); - this._props.addDocument?.(Docs.Create.TextDocument(text, { _width: this.Bounds.width, _height: this.Bounds.height, x: this.Bounds.left + this.Bounds.width, y: this.Bounds.top, title: text })); - }); - } - }); - @undoBatch summary = action(() => { const selected = this.marqueeSelect(false).map(d => { @@ -704,8 +630,8 @@ export class MarqueeView extends ObservableReactComponent {' '} @@ -714,7 +640,7 @@ export class MarqueeView extends ObservableReactComponent s + pt[0] + ',' + pt[1] + ' ', '')} fill="none" - stroke={lightOrDark(this._props.Document?.backgroundColor ?? 'white')} + stroke={lightOrDark((this._props.Document?.backgroundColor as string) ?? 'white')} strokeWidth="1" strokeDasharray="3" /> @@ -753,7 +679,6 @@ export class MarqueeView extends ObservableReactComponent { diff --git a/src/client/views/nodes/FaceRectangles.tsx b/src/client/views/nodes/FaceRectangles.tsx index ade4225d9..19aa90a8b 100644 --- a/src/client/views/nodes/FaceRectangles.tsx +++ b/src/client/views/nodes/FaceRectangles.tsx @@ -4,6 +4,7 @@ import { Doc, DocListCast } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { Cast, NumCast } from '../../../fields/Types'; import FaceRectangle from './FaceRectangle'; +import { FaceRecognitionHandler } from '../search/FaceRecognitionHandler'; interface FaceRectanglesProps { document: Doc; @@ -19,7 +20,7 @@ export interface RectangleTemplate { @observer export class FaceRectangles extends React.Component { render() { - const faces = DocListCast(this.props.document.faces); + const faces = DocListCast(this.props.document[FaceRecognitionHandler.FacesField(this.props.document)]); const templates: RectangleTemplate[] = faces.map(faceDoc => { const rectangle = Cast(faceDoc.faceRectangle, Doc) as Doc; const style = { diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx index 29ca6e797..dc271fe73 100644 --- a/src/client/views/search/FaceRecognitionHandler.tsx +++ b/src/client/views/search/FaceRecognitionHandler.tsx @@ -1,42 +1,46 @@ import * as faceapi from 'face-api.js'; import { FaceMatcher } from 'face-api.js'; +import { computed } from 'mobx'; import { Doc, DocListCast } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { List } from '../../../fields/List'; +import { listSpec } from '../../../fields/Schema'; import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; -import { DocumentManager } from '../../util/DocumentManager'; -import { computed } from 'mobx'; import { DocumentType } from '../../documents/DocumentTypes'; -import { listSpec } from '../../../fields/Schema'; +import { DocumentManager } from '../../util/DocumentManager'; /** * A class that handles face recognition. */ export class FaceRecognitionHandler { static _instance: FaceRecognitionHandler; - private loadedModels: boolean = false; - @computed get examinedFaceDocs() { - return DocListCast(Doc.UserDoc().examinedFaceDocs); - } - processingDocs: Set = new Set(); - pendingLoadDocs: Doc[] = []; + private _loadedModels: boolean = false; + private _processingDocs: Set = new Set(); + private _pendingLoadDocs: Doc[] = []; + + public static FaceField = (target: Doc, doc: Doc) => `${Doc.LayoutFieldKey(target)}_${doc.face_label}`; + public static FacesField = (target: Doc) => `${Doc.LayoutFieldKey(target)}_Faces`; constructor() { FaceRecognitionHandler._instance = this; - this.loadModels().then(() => this.pendingLoadDocs.forEach(this.findMatches)); + this.loadModels().then(() => this._pendingLoadDocs.forEach(this.findMatches)); DocumentManager.Instance.AddAnyViewRenderedCB(dv => FaceRecognitionHandler.Instance.findMatches(dv.Document)); } + @computed get examinedFaceDocs() { + return DocListCast(Doc.UserDoc().examinedFaceDocs); + } + /** * Loads the face detection models. */ - async loadModels() { + loadModels = async () => { const MODEL_URL = `/models`; await faceapi.loadFaceDetectionModel(MODEL_URL); await faceapi.loadFaceLandmarkModel(MODEL_URL); await faceapi.loadFaceRecognitionModel(MODEL_URL); - this.loadedModels = true; - } + this._loadedModels = true; + }; public static get Instance() { return FaceRecognitionHandler._instance ?? new FaceRecognitionHandler(); @@ -47,8 +51,8 @@ export class FaceRecognitionHandler { * @param doc The document being analyzed. */ public findMatches = async (doc: Doc) => { - if (!this.loadedModels || !Doc.ActiveDashboard) { - this.pendingLoadDocs.push(doc); + if (!this._loadedModels || !Doc.ActiveDashboard) { + this._pendingLoadDocs.push(doc); return; } @@ -59,12 +63,12 @@ export class FaceRecognitionHandler { const imgUrl = ImageCast(doc[Doc.LayoutFieldKey(doc)]); // If the doc isn't an image or currently already been examined or is being processed, stop examining the document. - if (!imgUrl || this.examinedFaceDocs.includes(doc) || this.processingDocs.has(doc)) { + if (!imgUrl || this.examinedFaceDocs.includes(doc) || this._processingDocs.has(doc)) { return; } // Mark the document as being processed. - this.processingDocs.add(doc); + this._processingDocs.add(doc); // Get the image the document contains and analyze for faces. const [name, type] = imgUrl.url.href.split('.'); @@ -74,7 +78,7 @@ export class FaceRecognitionHandler { const fullFaceDescriptions = await faceapi.detectAllFaces(img).withFaceLandmarks().withFaceDescriptors(); - doc[DocData].faces = new List>(); + doc[DocData][FaceRecognitionHandler.FacesField(doc)] = new List>(); // For each face detected, find a match. for (const fd of fullFaceDescriptions) { @@ -84,34 +88,36 @@ export class FaceRecognitionHandler { if (match) { // If a matching Face Document has been found, add the document to the Face Document's associated docs and append the face // descriptor to the Face Document's descriptor list. - Doc.AddDocToList(match, 'associatedDocs', doc); - Cast(match.faceDescriptors, listSpec('number'), null).push(converted_list as unknown as number); // items are lists of numbers, not numbers, but type system can't handle that + Doc.AddDocToList(match, 'face_docList', doc); + Cast(match.face_descriptors, listSpec('number'), null).push(converted_list as unknown as number); // items are lists of numbers, not numbers, but type system can't handle that } else { // If a matching Face Document has not been found, create a new Face Document. Doc.UserDoc().faceDocNum = NumCast(Doc.UserDoc().faceDocNum) + 1; const newFaceDocument = new Doc(); - newFaceDocument.label = `Face ${Doc.UserDoc().faceDocNum}`; - newFaceDocument.associatedDocs = new List([doc]); - newFaceDocument.faceDescriptors = new List>([converted_list]); + newFaceDocument.title = `Face ${Doc.UserDoc().faceDocNum}`; + newFaceDocument.face = ''; // just to make prettyprinting look better + newFaceDocument.face_label = `Face${Doc.UserDoc().faceDocNum}`; + newFaceDocument.face_docList = new List([doc]); + newFaceDocument.face_descriptors = new List>([converted_list]); Doc.AddDocToList(Doc.ActiveDashboard[DocData], 'faceDocuments', newFaceDocument); match = newFaceDocument; } // Assign a field in the document of the matching Face Document. - const faceDescripField = `FACE DESCRIPTOR - ${match[DocData].label}`; + const faceDescripField = FaceRecognitionHandler.FaceField(doc, match); if (doc[DocData][faceDescripField]) { Cast(doc[DocData][faceDescripField], listSpec('number'), null).push(converted_list as unknown as number); // items are lists of numbers, not numbers, but type system can't handle that } else { doc[DocData][faceDescripField] = new List>([converted_list]); } - Cast(doc[DocData].faces, listSpec('number'), null).push(converted_list as unknown as number); // items are lists of numbers, not numbers, but type system can't handle that + Cast(doc[DocData][FaceRecognitionHandler.FacesField(doc)], listSpec('number'), null).push(converted_list as unknown as number); // items are lists of numbers, not numbers, but type system can't handle that Doc.AddDocToList(Doc.UserDoc(), 'examinedFaceDocs', doc); } - this.processingDocs.delete(doc); + this._processingDocs.delete(doc); }; /** @@ -125,14 +131,14 @@ export class FaceRecognitionHandler { } const faceDescriptors: faceapi.LabeledFaceDescriptors[] = DocListCast(Doc.ActiveDashboard[DocData].faceDocuments).map(faceDocument => { - const float32Array = (faceDocument[DocData].faceDescriptors as List>).map(faceDescriptor => new Float32Array(Array.from(faceDescriptor))); - return new faceapi.LabeledFaceDescriptors(StrCast(faceDocument[DocData].label), float32Array); + const float32Array = (faceDocument[DocData].face_descriptors as List>).map(faceDescriptor => new Float32Array(Array.from(faceDescriptor))); + return new faceapi.LabeledFaceDescriptors(StrCast(faceDocument[DocData].face_label), float32Array); }); const faceMatcher = new FaceMatcher(faceDescriptors, 0.6); const match = faceMatcher.findBestMatch(cur_descriptor); if (match.label !== 'unknown') { for (const doc of DocListCast(Doc.ActiveDashboard[DocData].faceDocuments)) { - if (doc[DocData].label === match.label) { + if (doc[DocData].face_label === match.label) { return doc; } } -- cgit v1.2.3-70-g09d2 From 19a947cbd253ff7fb9da88c27af7645219446d0a Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 20 Aug 2024 22:02:54 -0400 Subject: from last --- .eslintrc.json | 2 +- src/client/documents/DocUtils.ts | 2 +- src/client/views/nodes/FaceRectangle.tsx | 34 ---------------------- src/client/views/nodes/FaceRectangles.tsx | 47 ------------------------------- src/server/server_Initialization.ts | 2 +- 5 files changed, 3 insertions(+), 84 deletions(-) delete mode 100644 src/client/views/nodes/FaceRectangle.tsx delete mode 100644 src/client/views/nodes/FaceRectangles.tsx (limited to 'src/client/documents') diff --git a/.eslintrc.json b/.eslintrc.json index 6202561d5..d4f43f04c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -40,6 +40,7 @@ "react/jsx-filename-extension": [2, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }], "import/prefer-default-export": "off", "no-unused-expressions": "off", + "@typescript-eslint/no-unused-expressions": "off", "prefer-template": "off", "no-inner-declarations": "off", "no-plusplus": "off", @@ -54,7 +55,6 @@ "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/no-namespace": 0, - "@typescript-eslint/no-unused-expressions": "off", "react/destructuring-assignment": 0, "no-restricted-globals": ["error", "event"], "no-param-reassign": ["error", { "props": false }], diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index 35d835f1f..0168e0a3b 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -35,7 +35,7 @@ import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox'; import { DocumentType } from './DocumentTypes'; import { Docs, DocumentOptions } from './Documents'; -// eslint-disable-next-line @typescript-eslint/no-var-requires +// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports const { DFLT_IMAGE_NATIVE_DIM } = require('../views/global/globalCssVariables.module.scss'); // prettier-ignore const defaultNativeImageDim = Number(DFLT_IMAGE_NATIVE_DIM.replace('px', '')); diff --git a/src/client/views/nodes/FaceRectangle.tsx b/src/client/views/nodes/FaceRectangle.tsx deleted file mode 100644 index 2b66b83fe..000000000 --- a/src/client/views/nodes/FaceRectangle.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { observable, runInAction } from 'mobx'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { RectangleTemplate } from './FaceRectangles'; - -@observer -export default class FaceRectangle extends React.Component<{ rectangle: RectangleTemplate }> { - @observable private opacity = 0; - - componentDidMount() { - setTimeout( - () => - runInAction(() => { - this.opacity = 1; - }), - 500 - ); - } - - render() { - const { rectangle } = this.props; - return ( -
- ); - } -} diff --git a/src/client/views/nodes/FaceRectangles.tsx b/src/client/views/nodes/FaceRectangles.tsx deleted file mode 100644 index 19aa90a8b..000000000 --- a/src/client/views/nodes/FaceRectangles.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { Doc, DocListCast } from '../../../fields/Doc'; -import { Id } from '../../../fields/FieldSymbols'; -import { Cast, NumCast } from '../../../fields/Types'; -import FaceRectangle from './FaceRectangle'; -import { FaceRecognitionHandler } from '../search/FaceRecognitionHandler'; - -interface FaceRectanglesProps { - document: Doc; - color: string; - backgroundColor: string; -} - -export interface RectangleTemplate { - id: string; - style: Partial; -} - -@observer -export class FaceRectangles extends React.Component { - render() { - const faces = DocListCast(this.props.document[FaceRecognitionHandler.FacesField(this.props.document)]); - const templates: RectangleTemplate[] = faces.map(faceDoc => { - const rectangle = Cast(faceDoc.faceRectangle, Doc) as Doc; - const style = { - top: NumCast(rectangle.top), - left: NumCast(rectangle.left), - width: NumCast(rectangle.width), - height: NumCast(rectangle.height), - backgroundColor: `${this.props.backgroundColor}33`, - border: `solid 2px ${this.props.color}`, - } as React.CSSProperties; - return { - id: rectangle[Id], - style: style, - }; - }); - return ( -
- {templates.map(rectangle => ( - - ))} -
- ); - } -} diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts index 8d3afc3ad..97c63a93e 100644 --- a/src/server/server_Initialization.ts +++ b/src/server/server_Initialization.ts @@ -115,7 +115,7 @@ function registerEmbeddedBrowseRelativePathHandler(server: express.Express) { // eslint-disable-next-line @typescript-eslint/no-explicit-any function proxyServe(req: any, requrl: string, response: any) { - // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + // eslint-disable-next-line global-require, @typescript-eslint/no-require-imports const htmlBodyMemoryStream = new (require('memorystream'))(); let wasinBrFormat = false; const sendModifiedBody = () => { -- cgit v1.2.3-70-g09d2 From 166d40bd856b85bd4bbde9f9ffe2c1ec0fb648d5 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 21 Aug 2024 10:57:53 -0400 Subject: more lint fixes after update --- eslint.config.mjs | 45 ++++++++++++++++++++++++- package-lock.json | 46 ++++++++++---------------- src/ClientUtils.ts | 1 - src/ServerUtils.ts | 1 - src/client/Network.ts | 2 +- src/client/documents/DocUtils.ts | 3 +- src/client/documents/Documents.ts | 4 +-- src/client/util/DictationManager.ts | 4 --- src/client/util/DragManager.ts | 1 - src/client/util/History.ts | 1 - src/client/util/InteractionUtils.tsx | 1 - src/client/util/ScriptingGlobals.ts | 2 +- src/client/util/SearchUtil.ts | 1 - src/client/util/SerializationHelper.ts | 1 - src/client/util/UndoManager.ts | 1 - src/client/views/DocViewUtils.ts | 1 - src/pen-gestures/GestureUtils.ts | 1 - src/server/DashStats.ts | 2 +- src/server/DashUploadUtils.ts | 1 - src/server/Message.ts | 1 - src/server/Search.ts | 1 - src/server/SharedMediaTypes.ts | 2 -- src/server/apis/google/GoogleApiServerUtils.ts | 2 +- src/server/server_Initialization.ts | 9 +++++ src/server/websocket.ts | 1 - 25 files changed, 77 insertions(+), 58 deletions(-) (limited to 'src/client/documents') diff --git a/eslint.config.mjs b/eslint.config.mjs index b35601abd..04655ce13 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -3,4 +3,47 @@ import pluginReactConfig from 'eslint-plugin-react/configs/recommended.js'; import globals from 'globals'; import tseslint from 'typescript-eslint'; -export default [{ languageOptions: { globals: { ...globals.browser, ...globals.node } } }, pluginJs.configs.recommended, ...tseslint.configs.recommended, pluginReactConfig]; +export default [ + { + languageOptions: { globals: { ...globals.browser, ...globals.node } }, + }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + { + rules: { + 'node/no-missing-import': 0, + 'no-console': 'off', + 'func-names': 'off', + 'no-process-exit': 'off', + 'object-shorthand': 'off', + 'class-methods-use-this': 'off', + 'single-quote': 'off', + 'max-classes-per-file': 0, + + 'react/jsx-filename-extension': [ + 2, + { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + ], + + 'import/prefer-default-export': 'off', + 'no-unused-expressions': 'off', + 'prefer-template': 'off', + 'no-inner-declarations': 'off', + 'no-plusplus': 'off', + 'no-multi-assign': 'off', + 'no-underscore-dangle': 'off', + 'no-nested-ternary': 'off', + 'lines-between-class-members': 'off', + 'no-shadow': 'off', + '@typescript-eslint/no-shadow': 'warn', + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': 'error', + '@typescript-eslint/no-namespace': 'off', + 'react/destructuring-assignment': 0, + 'no-restricted-globals': ['error', 'event'], + }, + }, + pluginReactConfig, +]; diff --git a/package-lock.json b/package-lock.json index 41922228a..565463980 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2423,14 +2423,14 @@ "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" }, "node_modules/@emotion/react": { - "version": "11.13.0", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.0.tgz", - "integrity": "sha512-WkL+bw1REC2VNV1goQyfxjx1GYJkcc23CRQkXX+vZNLINyfI7o+uUn/rTGPt/xJ3bJHd5GcljgnxHf4wRw5VWQ==", + "version": "11.13.3", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.3.tgz", + "integrity": "sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==", "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.12.0", "@emotion/cache": "^11.13.0", - "@emotion/serialize": "^1.3.0", + "@emotion/serialize": "^1.3.1", "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", "@emotion/utils": "^1.4.0", "@emotion/weak-memoize": "^0.4.0", @@ -2446,13 +2446,13 @@ } }, "node_modules/@emotion/serialize": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.0.tgz", - "integrity": "sha512-jACuBa9SlYajnpIVXB+XOXnfJHyckDfe6fOpORIM6yhBDlqGuExvDdZYHDQGoDf3bZXGv7tNr+LpLjJqiEQ6EA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.1.tgz", + "integrity": "sha512-dEPNKzBPU+vFPGa+z3axPRn8XVDetYORmDC0wAiej+TNcOZE70ZMJa0X7JdeoM6q/nWTMZeLpN/fTnD9o8MQBA==", "dependencies": { "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", - "@emotion/unitless": "^0.9.0", + "@emotion/unitless": "^0.10.0", "@emotion/utils": "^1.4.0", "csstype": "^3.0.2" } @@ -2491,9 +2491,9 @@ "peer": true }, "node_modules/@emotion/unitless": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.9.0.tgz", - "integrity": "sha512-TP6GgNZtmtFaFcsOgExdnfxLLpRDla4Q66tnenA9CktvVSdNKDvMVuUah4QvWPIpNjrWsGg3qeGo9a43QooGZQ==" + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==" }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { "version": "1.1.0", @@ -17245,9 +17245,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.5.12", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.12.tgz", - "integrity": "sha512-tIhPkdlEoCL1Y+PToq3zRNehUaKp3wBX/sr7aclAWdIWjvqAe/Im/H0SiCM4c1Q8BLPHCdoJTol+ZblflydehA==" + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz", + "integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==" }, "node_modules/elkjs": { "version": "0.9.3", @@ -20304,14 +20304,6 @@ "node": ">= 12.20" } }, - "node_modules/formdata-node/node_modules/web-streams-polyfill": { - "version": "4.0.0-beta.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", - "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", - "engines": { - "node": ">= 14" - } - }, "node_modules/formidable": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.1.tgz", @@ -41328,13 +41320,11 @@ } }, "node_modules/web-streams-polyfill": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0.tgz", - "integrity": "sha512-0zJXHRAYEjM2tUfZ2DiSOHAa2aw1tisnnhU3ufD57R8iefL+DcdJyRBRyJpG+NUimDgbTI/lH+gAE1PAvV3Cgw==", - "optional": true, - "peer": true, + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", "engines": { - "node": ">= 8" + "node": ">= 14" } }, "node_modules/web-worker": { diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts index fc048b155..55801df81 100644 --- a/src/ClientUtils.ts +++ b/src/ClientUtils.ts @@ -82,7 +82,6 @@ export function returnEmptyFilter() { return [] as string[]; } -// eslint-disable-next-line @typescript-eslint/no-namespace export namespace ClientUtils { export const CLICK_TIME = 300; export const DRAG_THRESHOLD = 4; diff --git a/src/ServerUtils.ts b/src/ServerUtils.ts index 8b2d0b9f6..715341ab3 100644 --- a/src/ServerUtils.ts +++ b/src/ServerUtils.ts @@ -2,7 +2,6 @@ import { Socket } from 'socket.io'; import { Message } from './server/Message'; import { Utils } from './Utils'; -// eslint-disable-next-line @typescript-eslint/no-namespace export namespace ServerUtils { export function Emit(socket: Socket, message: Message, args: T) { Utils.log('Emit', message.Name, args, false); diff --git a/src/client/Network.ts b/src/client/Network.ts index 8876d8190..6c60c4151 100644 --- a/src/client/Network.ts +++ b/src/client/Network.ts @@ -8,7 +8,7 @@ import { Upload } from '../server/SharedMediaTypes'; * mainly provides methods that the client can use to begin the process of * interacting with the server, such as fetching or uploading files. */ -// eslint-disable-next-line @typescript-eslint/no-namespace + export namespace Networking { export async function FetchFromServer(relativeRoute: string) { return (await fetch(relativeRoute)).text(); diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index a503d732b..5ee7d0f9c 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -40,7 +40,6 @@ const { DFLT_IMAGE_NATIVE_DIM } = require('../views/global/globalCssVariables.mo const defaultNativeImageDim = Number(DFLT_IMAGE_NATIVE_DIM.replace('px', '')); -// eslint-disable-next-line @typescript-eslint/no-namespace export namespace DocUtils { function matchFieldValue(doc: Doc, key: string, valueIn: unknown): boolean { let value = valueIn; @@ -467,7 +466,7 @@ export namespace DocUtils { .concat(userTypes) .concat(clickFuncs) .map(btnDoc => (btnDoc.dragFactory as Doc) || btnDoc) - .filter(doc => doc.isTemplateDoc); + .filter(d => d.isTemplateDoc); // bcz: this is hacky -- want to have different templates be applied depending on the "type" of a document. but type is not reliable and there could be other types of template searches so this should be generalized // first try to find a template that matches the specific document type (_). otherwise, fallback to a general match on !docLayoutTemplate && diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index b108b73db..41c6ce39b 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -487,9 +487,7 @@ export class DocumentOptions { export const DocOptions = new DocumentOptions(); -// eslint-disable-next-line @typescript-eslint/no-namespace export namespace Docs { - // eslint-disable-next-line @typescript-eslint/no-namespace export namespace Prototypes { type LayoutSource = { LayoutString: (key: string) => string }; type PrototypeTemplate = { @@ -647,7 +645,7 @@ export namespace Docs { * Encapsulates the factory used to create new document instances * delegated from top-level prototypes */ - // eslint-disable-next-line @typescript-eslint/no-namespace + export namespace Create { /** * This function receives the relevant document prototype and uses diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts index 16a0df120..a0e1413b6 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -32,14 +32,12 @@ import { UndoManager } from './UndoManager'; * to add new commands as classes or components are constructed. */ -// eslint-disable-next-line @typescript-eslint/no-namespace export namespace DictationManager { /** * Some type maneuvering to access Webkit's built-in * speech recognizer. */ - // eslint-disable-next-line @typescript-eslint/no-namespace namespace CORE { export interface IWindow extends Window { webkitSpeechRecognition: { new (): SpeechRecognition }; @@ -48,7 +46,6 @@ export namespace DictationManager { const { webkitSpeechRecognition }: CORE.IWindow = window as unknown as CORE.IWindow; export const placeholder = 'Listening...'; - // eslint-disable-next-line @typescript-eslint/no-namespace export namespace Controls { export const Infringed = 'unable to process: dictation manager still involved in previous session'; const browser = (() => { @@ -230,7 +227,6 @@ export namespace DictationManager { }; } - // eslint-disable-next-line @typescript-eslint/no-namespace export namespace Commands { export const dictationFadeDuration = 2000; diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index c237a75de..7db13689d 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -70,7 +70,6 @@ export function SetupDrag(_reference: React.RefObject, docFunc: () return onItemDown; } -// eslint-disable-next-line @typescript-eslint/no-namespace export namespace DragManager { export const dragClassName = 'collectionFreeFormDocumentView-container'; let dragDiv: HTMLDivElement; diff --git a/src/client/util/History.ts b/src/client/util/History.ts index 067c28c6b..0d0c056a4 100644 --- a/src/client/util/History.ts +++ b/src/client/util/History.ts @@ -10,7 +10,6 @@ import { OmitKeys, ClientUtils } from '../../ClientUtils'; import { DocServer } from '../DocServer'; import { DashboardView } from '../views/DashboardView'; -// eslint-disable-next-line @typescript-eslint/no-namespace export namespace HistoryUtil { export interface DocInitializerList { [key: string]: string | number; diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index f3ede596d..4231c2ca8 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -4,7 +4,6 @@ import { Utils } from '../../Utils'; import { Gestures } from '../../pen-gestures/GestureTypes'; import './InteractionUtils.scss'; -// eslint-disable-next-line @typescript-eslint/no-namespace export namespace InteractionUtils { export const MOUSETYPE = 'mouse'; export const TOUCHTYPE = 'touch'; diff --git a/src/client/util/ScriptingGlobals.ts b/src/client/util/ScriptingGlobals.ts index 4a1a70633..444e8fc0a 100644 --- a/src/client/util/ScriptingGlobals.ts +++ b/src/client/util/ScriptingGlobals.ts @@ -6,7 +6,7 @@ const _scriptingGlobals: { [name: string]: unknown } = {}; const _scriptingDescriptions: { [name: string]: string } = {}; const _scriptingParams: { [name: string]: string } = {}; export let scriptingGlobals: { [name: string]: unknown } = _scriptingGlobals; -// eslint-disable-next-line @typescript-eslint/no-namespace + export namespace ScriptingGlobals { export function getGlobals() { return Object.keys(_scriptingGlobals); } // prettier-ignore export function getGlobalObj() { return _scriptingGlobals; } // prettier-ignore diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index 6cad7060b..733eae5f4 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -5,7 +5,6 @@ import { StrCast } from '../../fields/Types'; import { DocumentType } from '../documents/DocumentTypes'; import { DocOptions, FInfo } from '../documents/Documents'; -// eslint-disable-next-line @typescript-eslint/no-namespace export namespace SearchUtil { export type HighlightingResult = { [id: string]: { [key: string]: string[] } }; diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts index 0386b2455..ccb02fb79 100644 --- a/src/client/util/SerializationHelper.ts +++ b/src/client/util/SerializationHelper.ts @@ -12,7 +12,6 @@ export function afterDocDeserialize(cb: (err: unknown, val: unknown) => void, er const serializationTypes: { [name: string]: { ctor: { new (): unknown }; afterDeserialize?: (obj: unknown) => void | Promise } } = {}; const reverseMap: { [ctor: string]: string } = {}; -// eslint-disable-next-line @typescript-eslint/no-namespace export namespace SerializationHelper { export function IsSerializing() { return serializing > 0; diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts index 5fd935370..ce0e7768b 100644 --- a/src/client/util/UndoManager.ts +++ b/src/client/util/UndoManager.ts @@ -83,7 +83,6 @@ export function undoBatch(target: any, key?: string | symbol, descriptor?: Typed return descriptor; } -// eslint-disable-next-line @typescript-eslint/no-namespace export namespace UndoManager { export interface UndoEvent { undo: () => void; diff --git a/src/client/views/DocViewUtils.ts b/src/client/views/DocViewUtils.ts index 49a30aa08..1f5f29c7e 100644 --- a/src/client/views/DocViewUtils.ts +++ b/src/client/views/DocViewUtils.ts @@ -6,7 +6,6 @@ import { Doc, SetActiveAudioLinker } from '../../fields/Doc'; import { DocUtils } from '../documents/DocUtils'; import { FieldViewProps } from './nodes/FieldView'; -// eslint-disable-next-line @typescript-eslint/no-namespace export namespace DocViewUtils { export const ActiveRecordings: { props: FieldViewProps; getAnchor: (addAsAnnotation: boolean) => Doc }[] = []; diff --git a/src/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts index bf5475042..c7051c87c 100644 --- a/src/pen-gestures/GestureUtils.ts +++ b/src/pen-gestures/GestureUtils.ts @@ -2,7 +2,6 @@ import { Rect } from 'react-measure'; import { Gestures, PointData } from './GestureTypes'; import { NDollarRecognizer } from './ndollar'; -// eslint-disable-next-line @typescript-eslint/no-namespace export namespace GestureUtils { export class GestureEvent { readonly gesture: Gestures; diff --git a/src/server/DashStats.ts b/src/server/DashStats.ts index 6b9fb8971..8e1d4661f 100644 --- a/src/server/DashStats.ts +++ b/src/server/DashStats.ts @@ -9,7 +9,7 @@ import { socketMap, timeMap, userOperations } from './SocketData'; * This includes time connected, number of operations, and * the rate of their operations */ -// eslint-disable-next-line @typescript-eslint/no-namespace + export namespace DashStats { export const SAMPLING_INTERVAL = 1000; // in milliseconds (ms) - Time interval to update the frontend. export const RATE_INTERVAL = 10; // in seconds (s) - Used to calculate rate diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index 5e58db103..8f012f783 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -47,7 +47,6 @@ function usingAzure() { return process.env.USE_AZURE === 'true'; } -// eslint-disable-next-line @typescript-eslint/no-namespace export namespace DashUploadUtils { export interface Size { width: number; diff --git a/src/server/Message.ts b/src/server/Message.ts index b904a5ba3..01a42fc68 100644 --- a/src/server/Message.ts +++ b/src/server/Message.ts @@ -43,7 +43,6 @@ export interface RoomMessage { readonly room: string; } -// eslint-disable-next-line @typescript-eslint/no-namespace export namespace MessageStore { export const Foo = new Message('Foo'); export const Bar = new Message('Bar'); diff --git a/src/server/Search.ts b/src/server/Search.ts index 06af18776..b21ee853a 100644 --- a/src/server/Search.ts +++ b/src/server/Search.ts @@ -3,7 +3,6 @@ import * as rp from 'request-promise'; const pathTo = (relative: string) => `http://localhost:8983/solr/dash/${relative}`; -// eslint-disable-next-line @typescript-eslint/no-namespace export namespace Search { export async function updateDocument(document: any) { try { diff --git a/src/server/SharedMediaTypes.ts b/src/server/SharedMediaTypes.ts index 680db9cd0..9aa4b120f 100644 --- a/src/server/SharedMediaTypes.ts +++ b/src/server/SharedMediaTypes.ts @@ -1,7 +1,6 @@ import { ExifData } from 'exif'; import { File } from 'formidable'; -// eslint-disable-next-line @typescript-eslint/no-namespace export namespace AcceptableMedia { export const gifs = ['.gif']; export const pngs = ['.png']; @@ -19,7 +18,6 @@ export enum AudioAnnoState { playing = 'playing', } -// eslint-disable-next-line @typescript-eslint/no-namespace export namespace Upload { export function isImageInformation(uploadResponse: Upload.FileInformation): uploadResponse is Upload.ImageInformation { return 'nativeWidth' in uploadResponse; diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 47206f415..21c405bee 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -21,7 +21,7 @@ const scope = ['documents.readonly', 'documents', 'presentations', 'presentation * This namespace manages server side authentication for Google API queries, either * from the standard v1 APIs or the Google Photos REST API. */ -// eslint-disable-next-line @typescript-eslint/no-namespace + export namespace GoogleApiServerUtils { /** * As we expand out to more Google APIs that are accessible from diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts index 2190e27c7..8d3afc3ad 100644 --- a/src/server/server_Initialization.ts +++ b/src/server/server_Initialization.ts @@ -113,6 +113,7 @@ function registerEmbeddedBrowseRelativePathHandler(server: express.Express) { }); } +// eslint-disable-next-line @typescript-eslint/no-explicit-any function proxyServe(req: any, requrl: string, response: any) { // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires const htmlBodyMemoryStream = new (require('memorystream'))(); @@ -137,8 +138,10 @@ function proxyServe(req: any, requrl: string, response: any) { response.send(header?.includes('gzip') ? zlib.gzipSync(htmlText) : htmlText); } else { req.pipe(request(requrl)) + // eslint-disable-next-line @typescript-eslint/no-explicit-any .on('error', (e: any) => console.log('requrl ', e)) .pipe(response) + // eslint-disable-next-line @typescript-eslint/no-explicit-any .on('error', (e: any) => console.log('response pipe error', e)); console.log('EMPTY body:' + req.url); } @@ -147,14 +150,17 @@ function proxyServe(req: any, requrl: string, response: any) { } } else { req.pipe(htmlBodyMemoryStream) + // eslint-disable-next-line @typescript-eslint/no-explicit-any .on('error', (e: any) => console.log('html body memorystream error', e)) .pipe(response) + // eslint-disable-next-line @typescript-eslint/no-explicit-any .on('error', (e: any) => console.log('html body memory stream response error', e)); } }; const retrieveHTTPBody = () => { // req.headers.cookie = ''; req.pipe(request(requrl)) + // eslint-disable-next-line @typescript-eslint/no-explicit-any .on('error', (e: any) => { console.log(`CORS url error: ${requrl}`, e); response.send(` @@ -163,6 +169,7 @@ function proxyServe(req: any, requrl: string, response: any) {

${e}

`); }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any .on('response', (res: any) => { res.headers; const headers = Object.keys(res.headers); @@ -187,6 +194,7 @@ function proxyServe(req: any, requrl: string, response: any) { }) .on('end', sendModifiedBody) .pipe(htmlBodyMemoryStream) + // eslint-disable-next-line @typescript-eslint/no-explicit-any .on('error', (e: any) => console.log('http body pipe error', e)); }; retrieveHTTPBody(); @@ -244,6 +252,7 @@ export default async function InitializeServer(routeSetter: RouteSetter) { app.use(whm(compiler)); app.get(/^\/+$/, (req, res) => res.redirect(req.user ? '/home' : '/login')); // target urls that consist of one or more '/'s with nothing in between app.use(express.static(publicDirectory, { setHeaders: res => res.setHeader('Access-Control-Allow-Origin', '*') })); // all urls that start with dash's public directory: /files/ (e.g., /files/images, /files/audio, etc) + // eslint-disable-next-line @typescript-eslint/no-explicit-any app.use(cors({ origin: (_origin: any, callback: any) => callback(null, true) })); registerAuthenticationRoutes(app); // this adds routes to authenticate a user (login, etc) registerCorsProxy(app); // this adds a /corsProxy/ route to allow clients to get to urls that would otherwise be blocked by cors policies diff --git a/src/server/websocket.ts b/src/server/websocket.ts index ccbcb1c5f..1e25a8a27 100644 --- a/src/server/websocket.ts +++ b/src/server/websocket.ts @@ -15,7 +15,6 @@ import { resolvedPorts, socketMap, timeMap, userOperations } from './SocketData' import { initializeGuest } from './authentication/DashUserModel'; import { Database } from './database'; -// eslint-disable-next-line @typescript-eslint/no-namespace export namespace WebSocket { let CurUser: string | undefined; export let _socket: Socket; -- cgit v1.2.3-70-g09d2 From a52cda0a098716c24b3c6aafe23d2bd9267c72d9 Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 26 Aug 2024 10:43:43 -0400 Subject: moved KeywordsBox to TagsView. changed imagellable to use tags --- src/client/documents/Documents.ts | 8 +- src/client/views/DocumentButtonBar.tsx | 2 +- src/client/views/DocumentDecorations.tsx | 6 +- src/client/views/KeywordBox.tsx | 316 ------------------- src/client/views/StyleProvider.scss | 64 ---- src/client/views/StyleProvider.tsx | 6 +- src/client/views/TagsView.scss | 63 ++++ src/client/views/TagsView.tsx | 344 +++++++++++++++++++++ .../collectionFreeForm/ImageLabelBox.tsx | 58 ++-- src/client/views/nodes/DocumentView.tsx | 1 + .../views/nodes/formattedText/RichTextRules.ts | 2 +- src/fields/Doc.ts | 4 +- src/fields/DocSymbols.ts | 1 - 13 files changed, 447 insertions(+), 428 deletions(-) delete mode 100644 src/client/views/KeywordBox.tsx create mode 100644 src/client/views/TagsView.scss create mode 100644 src/client/views/TagsView.tsx (limited to 'src/client/documents') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 6b5469cca..c0e4e961c 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -43,7 +43,7 @@ export class FInfo { description: string = ''; readOnly: boolean = false; fieldType?: FInfoFieldType; - values?: FieldType[] | Map; + values?: FieldType[]; filterable?: boolean = true; // can be used as a Filter in FilterPanel // format?: string; // format to display values (e.g, decimal places, $, etc) @@ -144,10 +144,6 @@ class ListInfo extends FInfo { fieldType? = FInfoFieldType.list; values?: List[] = []; } -class MapInfo extends FInfo { - fieldType? = FInfoFieldType.map; - values?: Map = new Map(); -} type BOOLt = BoolInfo | boolean; type NUMt = NumInfo | number; type STRt = StrInfo | string; @@ -160,7 +156,6 @@ type COLLt = CTypeInfo | CollectionViewType; type DROPt = DAInfo | dropActionType; type DATEt = DateInfo | number; type DTYPEt = DTypeInfo | string; -type MAPt = MapInfo | Map; export class DocumentOptions { // coordinate and dimensions depending on view x?: NUMt = new NumInfo('horizontal coordinate in freeform view', false); @@ -489,7 +484,6 @@ export class DocumentOptions { cardSort?: STRt = new StrInfo('way cards are sorted in deck view'); cardSort_customField?: STRt = new StrInfo('field key used for sorting cards'); cardSort_visibleSortGroups?: List; // which sorting values are being filtered (shown) - keywords?: MAPt = new MapInfo('keywords', true); } export const DocOptions = new DocumentOptions(); diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index eb157b9ab..58b7f207c 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -284,7 +284,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( get keywordButton() { return !DocumentView.Selected().length ? null : ( Open keyword menu
}> -
DocumentView.Selected().map(dv => (dv.dataDoc.showLabels = !dv.dataDoc.showLabels))}> +
DocumentView.Selected().map(dv => (dv.dataDoc.showTags = !dv.dataDoc.showTags))}>
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 19c4a097b..da35459bb 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,3 +1,4 @@ +import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { IconButton } from 'browndash-components'; @@ -9,7 +10,7 @@ import { lightOrDark, returnFalse, setupMoveUpEvents } from '../../ClientUtils'; import { Utils, emptyFunction, numberValue } from '../../Utils'; import { DateField } from '../../fields/DateField'; import { Doc, DocListCast, Field, FieldType, HierarchyMapping, ReverseHierarchyMap } from '../../fields/Doc'; -import { AclAdmin, AclAugment, AclEdit, DocData, KeywordsHeight } from '../../fields/DocSymbols'; +import { AclAdmin, AclAugment, AclEdit, DocData } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { InkField } from '../../fields/InkField'; import { ScriptField } from '../../fields/ScriptField'; @@ -34,7 +35,6 @@ import { DocumentView } from './nodes/DocumentView'; import { ImageBox } from './nodes/ImageBox'; import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; -import { IconProp } from '@fortawesome/fontawesome-svg-core'; interface DocumentDecorationsProps { PanelWidth: number; @@ -835,7 +835,7 @@ export class DocumentDecorations extends ObservableReactComponent DocumentView.Selected()} /> diff --git a/src/client/views/KeywordBox.tsx b/src/client/views/KeywordBox.tsx deleted file mode 100644 index 841c5394b..000000000 --- a/src/client/views/KeywordBox.tsx +++ /dev/null @@ -1,316 +0,0 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button, Colors, IconButton } from 'browndash-components'; -import { action, computed, makeObservable, observable } from 'mobx'; -import { observer } from 'mobx-react'; -import React from 'react'; -import ResizeObserver from 'resize-observer-polyfill'; -import { returnFalse, setupMoveUpEvents } from '../../ClientUtils'; -import { Utils, emptyFunction } from '../../Utils'; -import { Doc, DocListCast, StrListCast } from '../../fields/Doc'; -import { DocData, KeywordsHeight } from '../../fields/DocSymbols'; -import { List } from '../../fields/List'; -import { NumCast, StrCast } from '../../fields/Types'; -import { DocumentType } from '../documents/DocumentTypes'; -import { DragManager } from '../util/DragManager'; -import { SnappingManager } from '../util/SnappingManager'; -import { undoable } from '../util/UndoManager'; -import { ObservableReactComponent } from './ObservableReactComponent'; -import { DocumentView } from './nodes/DocumentView'; -import { FontIconBox } from './nodes/FontIconBox/FontIconBox'; - -interface KeywordItemProps { - doc: Doc; - keyword: string; - keywordDoc?: Doc; - setToEditing: () => void; - isEditing: boolean; -} - -/** - * A component that handles individual keywords. - */ -@observer -export class KeywordItem extends ObservableReactComponent { - /** - * return list of all Docs that collect Docs with specified keywords - */ - public static get AllKeywordCollections() { - return DocListCast(Doc.ActiveDashboard?.myKeywordCollections); - } - /** - * Find Doc that collects all Docs with given keyword - * @param keyword keyword string - * @returns keyword collection Doc or undefined - */ - public static findKeywordCollectionDoc = (keyword: String) => KeywordItem.AllKeywordCollections.find(doc => doc.title === keyword); - - /** - * Creates a Doc that collects Docs with the specified keyword - * @param keyword keyword string - * @returns collection Doc - */ - public static createKeywordCollectionDoc = (keyword: string) => { - const newKeywordCol = new Doc(); - newKeywordCol.title = keyword; - newKeywordCol.collections = new List(); - newKeywordCol[DocData].docs = new List(); - // If the active Dashboard does not have a keyword collection, create it. - if (Doc.ActiveDashboard) { - if (!Doc.ActiveDashboard.myKeywordCollections) Doc.ActiveDashboard.myKeywordCollections = new List(); - Doc.AddDocToList(Doc.ActiveDashboard, 'myKeywordCollections', newKeywordCol); - } - - return newKeywordCol; - }; - /** - * Gets all Docs that have the specified keyword - * @param keyword keyword string - * @returns An array of documents that contain the keyword. - */ - public static allDocsWithKeyword = (keyword: string) => DocListCast(KeywordItem.findKeywordCollectionDoc(keyword)?.[DocData].docs); - - /** - * Adds a keyword to the metadata of this document - * @param keyword keyword string - */ - public static addLabelToDoc = (doc: Doc, keyword: string) => { - // If the keyword collection is not in active Dashboard, add it as a new doc, with the keyword as its title. - const keywordCollection = KeywordItem.findKeywordCollectionDoc(keyword) ?? KeywordItem.createKeywordCollectionDoc(keyword); - - // If the document is of type COLLECTION, make it a smart collection, otherwise, add the keyword to the document. - if (doc.type === DocumentType.COL) { - Doc.AddDocToList(keywordCollection[DocData], 'collections', doc); - - // Iterate through the keyword Doc collections and add a copy of the document to each collection - for (const cdoc of DocListCast(keywordCollection[DocData].docs)) { - if (!DocListCast(doc[DocData].data).find(d => Doc.AreProtosEqual(d, cdoc))) { - const newEmbedding = Doc.MakeEmbedding(cdoc); - Doc.AddDocToList(doc[DocData], 'data', newEmbedding); - Doc.SetContainer(newEmbedding, doc); - } - } - } else { - // Add this document to the keyword's collection of associated documents. - Doc.AddDocToList(keywordCollection[DocData], 'docs', doc); - - // Iterate through the keyword document's collections and add a copy of the document to each collection - for (const collection of DocListCast(keywordCollection.collections)) { - if (!DocListCast(collection[DocData].data).find(d => Doc.AreProtosEqual(d, doc))) { - const newEmbedding = Doc.MakeEmbedding(doc); - Doc.AddDocToList(collection[DocData], 'data', newEmbedding); - Doc.SetContainer(newEmbedding, collection); - } - } - } - - if (!doc[DocData].tags) doc[DocData].tags = new List(); - const tagList = doc[DocData].tags as List; - if (!tagList.includes(keyword)) tagList.push(keyword); - }; - - public static RemoveLabel = (doc: Doc, keyword: string, keywordDoc?: Doc) => { - if (doc[DocData].tags) { - if (doc.type === DocumentType.COL) { - keywordDoc && Doc.RemoveDocFromList(keywordDoc[DocData], 'collections', doc); - - for (const cur_doc of KeywordItem.allDocsWithKeyword(keyword)) { - doc[DocData].data = new List(DocListCast(doc[DocData].data).filter(d => !Doc.AreProtosEqual(cur_doc, d))); - } - } else { - keywordDoc && Doc.RemoveDocFromList(keywordDoc[DocData], 'docs', doc); - - for (const collection of DocListCast(keywordDoc?.collections)) { - collection[DocData].data = new List(DocListCast(collection[DocData].data).filter(d => !Doc.AreProtosEqual(doc, d))); - } - } - } - doc[DocData].tags = new List((doc[DocData].tags as List).filter(label => label !== keyword)); - }; - - private _ref: React.RefObject; - - constructor(props: any) { - super(props); - makeObservable(this); - this._ref = React.createRef(); - } - - /** - * Creates a smart collection. - * @returns - */ - createCollection = () => { - // Get the documents that contain the keyword. - const newEmbeddings = KeywordItem.allDocsWithKeyword(this._props.keyword).map(doc => Doc.MakeEmbedding(doc)); - - // Create a new collection and set up configurations. - const newCollection = ((doc: Doc) => { - const docData = doc[DocData]; - docData.data = new List(newEmbeddings); - docData.title = this._props.keyword; - docData.tags = new List([this._props.keyword]); - docData.showLabels = true; - docData.freeform_fitContentsToBox = true; - doc._freeform_panX = doc._freeform_panY = 0; - doc._width = 900; - doc._height = 900; - doc.layout_fitWidth = true; - return doc; - })(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true)); - newEmbeddings.forEach(embed => Doc.SetContainer(embed, newCollection)); - - // Add the collection to the keyword document's list of associated smart collections. - this._props.keywordDoc && Doc.AddDocToList(this._props.keywordDoc, 'collections', newCollection); - return newCollection; - }; - - @action - handleDragStart = (e: React.PointerEvent) => { - setupMoveUpEvents( - this, - e, - () => { - const dragData = new DragManager.DocumentDragData([this.createCollection()]); - DragManager.StartDocumentDrag([this._ref.current!], dragData, e.clientX, e.clientY, {}); - return true; - }, - returnFalse, - emptyFunction - ); - e.preventDefault(); - }; - - render() { - setTimeout(() => KeywordItem.addLabelToDoc(this._props.doc, this._props.keyword)); // bcz: hack to make sure that Docs are added to their keyword Doc collection since metadata can get set anywhere without a guard triggering an add to the collection - const keyword = this._props.keyword.replace(/^#/, ''); - const metadata = keyword.startsWith('@') ? keyword.replace(/^@/, '') : ''; - return ( -
- {metadata ? ( - - {keyword}  - {this._props.doc[metadata] as string} - - ) : ( - keyword - )} - {this.props.isEditing && ( - KeywordItem.RemoveLabel(this._props.doc, this._props.keyword, this._props.keywordDoc), `remove label ${this._props.keyword}`)} - icon={} - style={{ width: '8px', height: '8px', marginLeft: '10px' }} - /> - )} -
- ); - } -} - -interface KeywordBoxProps { - View: DocumentView; -} - -/** - * A component that handles the keyword display for documents. - */ -@observer -export class KeywordBox extends ObservableReactComponent { - private _ref: React.RefObject; - - constructor(props: any) { - super(props); - makeObservable(this); - this._ref = React.createRef(); - } - - @observable _currentInput = ''; - @observable _isEditing = !StrListCast(this._props.View.dataDoc.tags).length; - - @computed get currentScale() { - return NumCast((this._props.View.Document.embedContainer as Doc)?._freeform_scale, 1); - } - @computed get isEditing() { - return this._isEditing && DocumentView.SelectedDocs().includes(this._props.View.Document); - } - - @action - setToEditing = (editing = true) => { - this._isEditing = editing; - editing && this._props.View.select(false); - }; - - /** - * Adds the keyword to the document. - * @param keyword - */ - submitLabel = undoable((keyword: string) => { - const submittedLabel = keyword.trim(); - submittedLabel && KeywordItem.addLabelToDoc(this._props.View.Document, '#' + submittedLabel.replace(/^#/, '')); - this._currentInput = ''; // Clear the input box - }, 'added doc label'); - - render() { - const keywordsList = StrListCast(this._props.View.dataDoc.tags); - - return !this._props.View.Document.showLabels ? null : ( -
r && new ResizeObserver(action(() => (this._props.View.Document[KeywordsHeight] = r?.getBoundingClientRect().height ?? 0))).observe(r)} - style={{ - transformOrigin: 'top left', - maxWidth: `${100 * this.currentScale}%`, - width: 'max-content', - transform: `scale(${1 / this.currentScale})`, - backgroundColor: this.isEditing ? Colors.LIGHT_GRAY : Colors.TRANSPARENT, - borderColor: this.isEditing ? Colors.BLACK : Colors.TRANSPARENT, - }}> -
-
- {!keywordsList.length ? null : ( // - this.setToEditing(!this._isEditing)} icon={} /> - )} - {keywordsList.map(keyword => ( - - ))} -
- {this.isEditing ? ( -
-
- (this._currentInput = e.target.value))} - onKeyDown={e => { - e.key === 'Enter' ? this.submitLabel(this._currentInput) : null; - e.stopPropagation(); - }} - type="text" - placeholder="Input keywords for document..." - aria-label="keyword-input" - className="keyword-input" - style={{ width: '100%', borderRadius: '5px' }} - /> -
-
- {KeywordItem.AllKeywordCollections.map(doc => { - const keyword = StrCast(doc.title); - return ( -
-
- ) : null} -
-
- ); - } -} diff --git a/src/client/views/StyleProvider.scss b/src/client/views/StyleProvider.scss index 6f6939d54..ce00f6101 100644 --- a/src/client/views/StyleProvider.scss +++ b/src/client/views/StyleProvider.scss @@ -53,67 +53,3 @@ .styleProvider-treeView-icon { opacity: 0; } - -.keywords-container { - display: flex; - flex-wrap: wrap; - flex-direction: column; - border: 1px solid; - border-radius: 4px; -} - -.keywords-list { - display: flex; - flex-wrap: wrap; - .iconButton-container { - min-height: unset !important; - } -} - -.keyword { - padding: 1px 5px; - background-color: lightblue; - border: 1px solid black; - border-radius: 5px; - white-space: nowrap; - display: flex; - align-items: center; -} - -.keyword-suggestions-box { - display: flex; - flex-wrap: wrap; - margin: auto; - align-self: center; - width: 90%; - border: 1px solid black; - border-radius: 2px; - margin-top: 8px; -} - -.keyword-suggestion { - cursor: pointer; - padding: 1px 1px; - margin: 2px 2px; - 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 { - 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 a841ec63a..e48994586 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -19,7 +19,7 @@ import { SnappingManager } from '../util/SnappingManager'; import { undoable, UndoManager } from '../util/UndoManager'; import { TreeSort } from './collections/TreeSort'; import { Colors } from './global/globalEnums'; -import { KeywordBox } from './KeywordBox'; +import { TagsView } from './TagsView'; import { CollectionFreeFormDocumentView } from './nodes/CollectionFreeFormDocumentView'; import { DocumentView, DocumentViewProps } from './nodes/DocumentView'; import { FieldViewProps } from './nodes/FieldView'; @@ -363,14 +363,14 @@ export function DefaultStyleProvider(doc: Opt, props: Opt ); }; - const keywords = () => props?.DocumentView?.() && CollectionFreeFormDocumentView.from(props.DocumentView()) ? : null; + const tags = () => props?.DocumentView?.() && CollectionFreeFormDocumentView.from(props.DocumentView()) ? : null; return ( <> {paint()} {lock()} {filter()} {audio()} - {keywords()} + {tags()} ); } diff --git a/src/client/views/TagsView.scss b/src/client/views/TagsView.scss new file mode 100644 index 000000000..f7365a51b --- /dev/null +++ b/src/client/views/TagsView.scss @@ -0,0 +1,63 @@ +.tagsView-container { + display: flex; + flex-wrap: wrap; + flex-direction: column; + border: 1px solid; + border-radius: 4px; +} + +.tagsView-list { + display: flex; + flex-wrap: wrap; + .iconButton-container { + min-height: unset !important; + } +} + +.tagItem { + padding: 1px 5px; + background-color: lightblue; + border: 1px solid black; + border-radius: 5px; + white-space: nowrap; + display: flex; + align-items: center; +} + +.tagsView-suggestions-box { + display: flex; + flex-wrap: wrap; + margin: auto; + align-self: center; + width: 90%; + border: 1px solid black; + border-radius: 2px; + margin-top: 8px; +} + +.tagsView-suggestion { + cursor: pointer; + padding: 1px 1px; + margin: 2px 2px; + background-color: lightblue; + border: 1px solid black; + border-radius: 5px; + white-space: nowrap; + display: flex; + align-items: center; +} + +.tagsView-editing-box { + margin-top: 8px; +} + +.tagsView-input-box { + margin: auto; + align-self: center; + width: 90%; +} + +.tagsView-buttons { + margin-left: auto; + width: 10%; +} diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx new file mode 100644 index 000000000..dffd1e096 --- /dev/null +++ b/src/client/views/TagsView.tsx @@ -0,0 +1,344 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Button, Colors, IconButton } from 'browndash-components'; +import { action, computed, makeObservable, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import React from 'react'; +import ResizeObserver from 'resize-observer-polyfill'; +import { returnFalse, setupMoveUpEvents } from '../../ClientUtils'; +import { emptyFunction } from '../../Utils'; +import { Doc, DocListCast, Opt, StrListCast } from '../../fields/Doc'; +import { DocData } from '../../fields/DocSymbols'; +import { List } from '../../fields/List'; +import { NumCast, StrCast } from '../../fields/Types'; +import { DocumentType } from '../documents/DocumentTypes'; +import { DragManager } from '../util/DragManager'; +import { SnappingManager } from '../util/SnappingManager'; +import { undoable } from '../util/UndoManager'; +import { ObservableReactComponent } from './ObservableReactComponent'; +import './TagsView.scss'; +import { DocumentView } from './nodes/DocumentView'; + +/** + * The TagsView is a metadata input/display panel shown at the bottom of a DocumentView in a freeform collection. + * + * This panel allow sthe user to add metadata tags to a Doc, and to display those tags, or any metadata field + * in a panel of 'buttons' (TagItems) just below the DocumentView. TagItems are interactive - + * the user can drag them off in order to display a collection of all documents that share the tag value. + * + * The tags that are added using the panel are the same as the #tags that can entered in a text Doc. + * Note that tags starting with #@ display a metadata key/value pair instead of the tag itself. + * e.g., '#@author' shows the document author + * + */ + +interface TagItemProps { + doc: Doc; + tag: string; + tagDoc: Opt; + showRemoveUI: boolean; + setToEditing: () => void; +} + +/** + * Interactive component that display a single metadata tag or value. + * + * These items can be dragged and dropped to create a collection of Docs that + * share the same metadata tag / value. + */ +@observer +export class TagItem extends ObservableReactComponent { + /** + * return list of all tag Docs (ie, Doc that are collections of Docs sharing a specific tag / value) + */ + public static get AllTagCollectionDocs() { + return DocListCast(Doc.ActiveDashboard?.myTagCollections); + } + /** + * Find tag Doc that collects all Docs with given tag / value + * @param tag tag string + * @returns tag collection Doc or undefined + */ + public static findTagCollectionDoc = (tag: String) => TagItem.AllTagCollectionDocs.find(doc => doc.title === tag); + + /** + * Creates a Doc that collects Docs with the specified tag / value + * @param tag tag string + * @returns tag collection Doc + */ + public static createTagCollectionDoc = (tag: string) => { + const newTagCol = new Doc(); + newTagCol.title = tag; + newTagCol.collections = new List(); + newTagCol[DocData].docs = new List(); + // If the active Dashboard does not have a tag Doc collection, create it. + if (Doc.ActiveDashboard) { + if (!Doc.ActiveDashboard.myTagCollections) Doc.ActiveDashboard.myTagCollections = new List(); + Doc.AddDocToList(Doc.ActiveDashboard, 'myTagCollections', newTagCol); + } + + return newTagCol; + }; + /** + * Gets all Docs that have the specified tag / value + * @param tag tag string + * @returns An array of documents that contain the tag. + */ + public static allDocsWithTag = (tag: string) => DocListCast(TagItem.findTagCollectionDoc(tag)?.[DocData].docs); + + /** + * Adds a tag to the metadata of this document and adds the Doc to the corresponding tag collection Doc (or creates it) + * @param tag tag string + */ + public static addTagToDoc = (doc: Doc, tag: string) => { + // If the tag collection is not in active Dashboard, add it as a new doc, with the tag as its title. + const tagCollection = TagItem.findTagCollectionDoc(tag) ?? TagItem.createTagCollectionDoc(tag); + + // If the document is of type COLLECTION, make it a smart collection, otherwise, add the tag to the document. + if (doc.type === DocumentType.COL) { + Doc.AddDocToList(tagCollection[DocData], 'collections', doc); + + // Iterate through the tag Doc collections and add a copy of the document to each collection + for (const cdoc of DocListCast(tagCollection[DocData].docs)) { + if (!DocListCast(doc[DocData].data).find(d => Doc.AreProtosEqual(d, cdoc))) { + const newEmbedding = Doc.MakeEmbedding(cdoc); + Doc.AddDocToList(doc[DocData], 'data', newEmbedding); + Doc.SetContainer(newEmbedding, doc); + } + } + } else { + // Add this document to the tag's collection of associated documents. + Doc.AddDocToList(tagCollection[DocData], 'docs', doc); + + // Iterate through the tag document's collections and add a copy of the document to each collection + for (const collection of DocListCast(tagCollection.collections)) { + if (!DocListCast(collection[DocData].data).find(d => Doc.AreProtosEqual(d, doc))) { + const newEmbedding = Doc.MakeEmbedding(doc); + Doc.AddDocToList(collection[DocData], 'data', newEmbedding); + Doc.SetContainer(newEmbedding, collection); + } + } + } + + if (!doc[DocData].tags) doc[DocData].tags = new List(); + const tagList = doc[DocData].tags as List; + if (!tagList.includes(tag)) tagList.push(tag); + }; + + /** + * Removes a tag from a Doc and removes the Doc from the corresponding tag collection Doc + * @param doc Doc to add tag + * @param tag tag string + * @param tagDoc doc that collections the Docs with the tag + */ + public static removeTagFromDoc = (doc: Doc, tag: string, tagDoc?: Doc) => { + if (doc[DocData].tags) { + if (doc.type === DocumentType.COL) { + tagDoc && Doc.RemoveDocFromList(tagDoc[DocData], 'collections', doc); + + for (const cur_doc of TagItem.allDocsWithTag(tag)) { + doc[DocData].data = new List(DocListCast(doc[DocData].data).filter(d => !Doc.AreProtosEqual(cur_doc, d))); + } + } else { + tagDoc && Doc.RemoveDocFromList(tagDoc[DocData], 'docs', doc); + + for (const collection of DocListCast(tagDoc?.collections)) { + collection[DocData].data = new List(DocListCast(collection[DocData].data).filter(d => !Doc.AreProtosEqual(doc, d))); + } + } + } + doc[DocData].tags = new List((doc[DocData].tags as List).filter(label => label !== tag)); + }; + + private _ref: React.RefObject; + + constructor(props: any) { + super(props); + makeObservable(this); + this._ref = React.createRef(); + } + + /** + * Creates a smart collection. + * @returns + */ + createTagCollection = () => { + // Get the documents that contain the tag. + const newEmbeddings = TagItem.allDocsWithTag(this._props.tag).map(doc => Doc.MakeEmbedding(doc)); + + // Create a new collection and set up configurations. + const newCollection = ((doc: Doc) => { + const docData = doc[DocData]; + docData.data = new List(newEmbeddings); + docData.title = this._props.tag; + docData.tags = new List([this._props.tag]); + docData.showTags = true; + docData.freeform_fitContentsToBox = true; + doc._freeform_panX = doc._freeform_panY = 0; + doc._width = 900; + doc._height = 900; + doc.layout_fitWidth = true; + return doc; + })(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true)); + newEmbeddings.forEach(embed => Doc.SetContainer(embed, newCollection)); + + // Add the collection to the tag document's list of associated smart collections. + this._props.tagDoc && Doc.AddDocToList(this._props.tagDoc, 'collections', newCollection); + return newCollection; + }; + + @action + handleDragStart = (e: React.PointerEvent) => { + setupMoveUpEvents( + this, + e, + () => { + const dragData = new DragManager.DocumentDragData([this.createTagCollection()]); + DragManager.StartDocumentDrag([this._ref.current!], dragData, e.clientX, e.clientY, {}); + return true; + }, + returnFalse, + emptyFunction + ); + e.preventDefault(); + }; + + render() { + setTimeout(() => TagItem.addTagToDoc(this._props.doc, this._props.tag)); // bcz: hack to make sure that Docs are added to their tag Doc collection since metadata can get set anywhere without a guard triggering an add to the collection + const tag = this._props.tag.replace(/^#/, ''); + const metadata = tag.startsWith('@') ? tag.replace(/^@/, '') : ''; + return ( +
+ {metadata ? ( + + {tag}  + {this._props.doc[metadata] as string} + + ) : ( + tag + )} + {this.props.showRemoveUI && ( + TagItem.removeTagFromDoc(this._props.doc, this._props.tag, this._props.tagDoc), `remove tag ${this._props.tag}`)} + icon={} + style={{ width: '8px', height: '8px', marginLeft: '10px' }} + /> + )} +
+ ); + } +} + +interface TagViewProps { + View: DocumentView; +} + +/** + * Displays a panel of tags that have been added to a Doc. Also allows for editing the applied tags through a dropdown UI. + */ +@observer +export class TagsView extends ObservableReactComponent { + private _ref: React.RefObject; + + constructor(props: any) { + super(props); + makeObservable(this); + this._ref = React.createRef(); + } + + @observable _currentInput = ''; + @observable _isEditing = !StrListCast(this._props.View.dataDoc.tags).length; + + @computed get currentScale() { + return NumCast((this._props.View.Document.embedContainer as Doc)?._freeform_scale, 1); + } + @computed get isEditing() { + return this._isEditing && DocumentView.SelectedDocs().includes(this._props.View.Document); + } + + @action + setToEditing = (editing = true) => { + this._isEditing = editing; + editing && this._props.View.select(false); + }; + + /** + * Adds the specified tag to the Doc. If the tag is not prefixed with '#', then a '#' prefix is added. + * Whne the tag (after the '#') begins with '@', then a metadata key/value pair is displayed instead of + * just the tag. + * @param tag tag string to add + */ + submitTag = undoable((tag: string) => { + const submittedLabel = tag.trim(); + submittedLabel && TagItem.addTagToDoc(this._props.View.Document, '#' + submittedLabel.replace(/^#/, '')); + this._currentInput = ''; // Clear the input box + }, 'added doc label'); + + /** + * When 'showTags' is set on a Doc, this displays a wrapping panel of tagItemViews corresponding to all the tags set on the Doc). + * When the dropdown is clicked, this will toggle an extended UI that allows additional tags to be added/removed. + */ + render() { + const tagsList = StrListCast(this._props.View.dataDoc.tags); + + return !this._props.View.Document.showTags ? null : ( +
r && new ResizeObserver(action(() => (this._props.View.TagPanelHeight = r?.getBoundingClientRect().height ?? 0))).observe(r)} + style={{ + transformOrigin: 'top left', + maxWidth: `${100 * this.currentScale}%`, + width: 'max-content', + transform: `scale(${1 / this.currentScale})`, + backgroundColor: this.isEditing ? Colors.LIGHT_GRAY : Colors.TRANSPARENT, + borderColor: this.isEditing ? Colors.BLACK : Colors.TRANSPARENT, + }}> +
+
+ {!tagsList.length ? null : ( // + this.setToEditing(!this._isEditing)} icon={} /> + )} + {tagsList.map(tag => ( + + ))} +
+ {this.isEditing ? ( +
+
+ (this._currentInput = e.target.value))} + onKeyDown={e => { + e.key === 'Enter' ? this.submitTag(this._currentInput) : null; + e.stopPropagation(); + }} + type="text" + placeholder="Input tags for document..." + aria-label="tagsView-input" + className="tagsView-input" + style={{ width: '100%', borderRadius: '5px' }} + /> +
+
+ {TagItem.AllTagCollectionDocs.map((doc, i) => { + const tag = StrCast(doc.title); + return ( +
+
+ ) : null} +
+
+ ); + } +} diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx index 421b5d0a6..6eb3eb784 100644 --- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx +++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx @@ -1,30 +1,30 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Colors, IconButton } from 'browndash-components'; +import similarity from 'compute-cosine-similarity'; +import { ring } from 'ldrs'; +import 'ldrs/ring'; import { action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import React from 'react'; +import { Utils, numberRange } from '../../../../Utils'; import { Doc, NumListCast, Opt } 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 'ldrs/ring'; -import { ring } from 'ldrs'; -import { SnappingManager } from '../../../util/SnappingManager'; -import { ImageCast } from '../../../../fields/Types'; import { DocData } from '../../../../fields/DocSymbols'; -import { SettingsManager } from '../../../util/SettingsManager'; -import { CollectionCardView } from '../CollectionCardDeckView'; -import { gptGetEmbedding, gptImageLabel } from '../../../apis/gpt/GPT'; -import { numberRange, Utils } from '../../../../Utils'; import { List } from '../../../../fields/List'; +import { ImageCast } from '../../../../fields/Types'; +import { gptGetEmbedding, gptImageLabel } from '../../../apis/gpt/GPT'; +import { DocumentType } from '../../../documents/DocumentTypes'; +import { Docs } from '../../../documents/Documents'; import { DragManager } from '../../../util/DragManager'; -import { OpenWhere } from '../../nodes/OpenWhere'; -import similarity from 'compute-cosine-similarity'; +import { SettingsManager } from '../../../util/SettingsManager'; +import { SnappingManager } from '../../../util/SnappingManager'; +import { ViewBoxBaseComponent } from '../../DocComponent'; +import { MainView } from '../../MainView'; import { DocumentView } from '../../nodes/DocumentView'; +import { FieldView, FieldViewProps } from '../../nodes/FieldView'; +import { OpenWhere } from '../../nodes/OpenWhere'; +import { CollectionCardView } from '../CollectionCardDeckView'; +import './ImageLabelBox.scss'; +import { MarqueeOptionsMenu } from './MarqueeOptionsMenu'; export class ImageInformationItem {} @@ -139,9 +139,9 @@ export class ImageLabelBox extends ViewBoxBaseComponent() { toggleDisplayInformation = () => { this._displayImageInformation = !this._displayImageInformation; if (this._displayImageInformation) { - this._selectedImages.forEach(doc => (doc[DocData].showLabels = true)); + this._selectedImages.forEach(doc => (doc[DocData].showTags = true)); } else { - this._selectedImages.forEach(doc => (doc[DocData].showLabels = false)); + this._selectedImages.forEach(doc => (doc[DocData].showTags = false)); } }; @@ -163,7 +163,7 @@ export class ImageLabelBox extends ViewBoxBaseComponent() { // 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) { + if (!doc[DocData].tags) { const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.'); return CollectionCardView.imageUrlToBase64(`${name}_o.${type}`).then(hrefBase64 => !hrefBase64 ? undefined : @@ -174,14 +174,14 @@ export class ImageLabelBox extends ViewBoxBaseComponent() { (await Promise.all(imageInfos)).forEach(imageInfo => { if (imageInfo) { - imageInfo.doc[DocData].data_labels = new List(); + imageInfo.doc[DocData].tags = (imageInfo.doc[DocData].tags as List) ?? new List(); const labels = imageInfo.labels.split('\n'); labels.forEach(label => { - label = label.replace(/^\d+\.\s*|-|\*/, '').trim(); + label = label.replace(/^\d+\.\s*|-|f\*/, '').trim(); console.log(label); - imageInfo.doc[DocData][`${label}`] = true; - (imageInfo.doc[DocData].data_labels as List).push(label); + imageInfo.doc[DocData][label] = true; + (imageInfo.doc[DocData].tags as List).push(label); }); } }); @@ -196,10 +196,10 @@ export class ImageLabelBox extends ViewBoxBaseComponent() { this.startLoading(); for (const doc of this._selectedImages) { - for (let index = 0; index < (doc[DocData].data_labels as List).length; index++) { - const label = (doc[DocData].data_labels as List)[index]; + for (let index = 0; index < (doc[DocData].tags as List).length; index++) { + const label = (doc[DocData].tags as List)[index]; const embedding = await gptGetEmbedding(label); - doc[DocData][`data_labels_embedding_${index + 1}`] = new List(embedding); + doc[DocData][`tags_embedding_${index + 1}`] = new List(embedding); } } @@ -210,7 +210,7 @@ export class ImageLabelBox extends ViewBoxBaseComponent() { // For each image, loop through the labels, and calculate similarity. Associate it with the // most similar one. this._selectedImages.forEach(doc => { - const embedLists = numberRange((doc[DocData].data_labels as List).length).map(n => Array.from(NumListCast(doc[DocData][`data_labels_embedding_${n + 1}`]))); + const embedLists = numberRange((doc[DocData].tags as List).length).map(n => Array.from(NumListCast(doc[DocData][`tags_embedding_${n + 1}`]))); const bestEmbedScore = (embedding: Opt) => 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)) })) @@ -317,7 +317,7 @@ export class ImageLabelBox extends ViewBoxBaseComponent() { await DocumentView.showDocument(doc, { willZoomCentered: true }); }}>
this._props.addDocTab(doc, OpenWhere.addRightKeyvalue)}> - {(doc[DocData].data_labels as List).map(label => { + {(doc[DocData].tags as List).map(label => { return (
{label} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 5efdb3df4..4c357cf45 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1124,6 +1124,7 @@ export class DocumentView extends DocComponent() { @observable private _isHovering = false; @observable private _selected = false; @observable public static CurrentlyPlaying: DocumentView[] = []; // audio or video media views that are currently playing + @observable public TagPanelHeight = 0; @computed private get shouldNotScale() { return (this.layout_fitWidth && !this.nativeWidth) || this.ComponentView?.isUnstyledView?.(); diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 79c118490..e0d6c7c05 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -404,7 +404,7 @@ export class RichTextRules { if (!tags.includes(tag)) { tags.push(tag); this.Document[DocData].tags = new List(tags); - this.Document[DocData].showLabels = true; + this.Document[DocData].showTags = true; } const fieldView = state.schema.nodes.dashField.create({ fieldKey: '#' + tag }); return state.tr diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index f246f49e5..ffb5aab79 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -11,7 +11,7 @@ import { ClientUtils, incrementTitleCopy } from '../ClientUtils'; import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, Animation, AudioPlay, Brushed, CachedUpdates, DirectLinks, DocAcl, DocCss, DocData, DocLayout, DocViews, FieldKeys, FieldTuples, ForceServerWrite, Height, Highlight, - Initializing, KeywordsHeight, Self, SelfProxy, TransitionTimer, UpdatingFromServer, Width + Initializing, Self, SelfProxy, TransitionTimer, UpdatingFromServer, Width } from './DocSymbols'; // prettier-ignore import { Copy, FieldChanged, HandleUpdate, Id, Parent, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; import { InkTool } from './InkField'; @@ -303,7 +303,6 @@ export class Doc extends RefField { Height, Highlight, Initializing, - KeywordsHeight, Self, SelfProxy, UpdatingFromServer, @@ -369,7 +368,6 @@ export class Doc extends RefField { @observable public [Highlight]: boolean = false; @observable public [Brushed]: boolean = false; @observable public [DocViews] = new ObservableSet(); - @observable public [KeywordsHeight]: number = 0; private [Self] = this; private [SelfProxy]: Doc; diff --git a/src/fields/DocSymbols.ts b/src/fields/DocSymbols.ts index 9e091ab29..dc18d8638 100644 --- a/src/fields/DocSymbols.ts +++ b/src/fields/DocSymbols.ts @@ -32,6 +32,5 @@ export const DocViews = Symbol('DocViews'); export const Brushed = Symbol('DocBrushed'); export const DocCss = Symbol('DocCss'); export const TransitionTimer = Symbol('DocTransitionTimer'); -export const KeywordsHeight = Symbol('DocKeywordsHeight'); export const DashVersion = 'v0.8.0'; -- cgit v1.2.3-70-g09d2 From 2e345c1ebd498e3f7e088e6fc3e1ca17082b23c1 Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 26 Aug 2024 13:53:26 -0400 Subject: converted unique faces to be a Doc type similar to a collection. --- src/client/documents/DocumentTypes.ts | 1 + src/client/documents/Documents.ts | 4 + src/client/views/DashboardView.tsx | 1 + src/client/views/Main.tsx | 3 +- .../views/collections/CollectionStackingView.tsx | 3 +- src/client/views/collections/CollectionSubView.tsx | 1 + .../collectionFreeForm/FaceCollectionBox.scss | 24 ++++- .../collectionFreeForm/FaceCollectionBox.tsx | 113 +++++++++++++-------- src/client/views/search/FaceRecognitionHandler.tsx | 34 +++++-- 9 files changed, 123 insertions(+), 61 deletions(-) (limited to 'src/client/documents') diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 49df943d8..b055546fc 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -18,6 +18,7 @@ export enum DocumentType { SEARCH = 'search', // search query IMAGEGROUPER = 'imagegrouper', FACECOLLECTION = 'facecollection', + UFACE = 'uniqueface', // unique face collection doc 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 c0e4e961c..c2211fb80 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -795,6 +795,10 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.FACECOLLECTION), undefined, options); } + export function UniqeFaceDocument(options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.UFACE), 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/views/DashboardView.tsx b/src/client/views/DashboardView.tsx index 4616e15e5..33e905a54 100644 --- a/src/client/views/DashboardView.tsx +++ b/src/client/views/DashboardView.tsx @@ -432,6 +432,7 @@ export class DashboardView extends ObservableReactComponent { dashboardDoc.myOverlayDocs = new List(); dashboardDoc[DocData].myPublishedDocs = new List(); dashboardDoc[DocData].myTagCollections = new List(); + dashboardDoc[DocData].myUniqueFaces = new List(); dashboardDoc[DocData].myTrails = DashboardView.SetupDashboardTrails(dashboardDoc); dashboardDoc[DocData].myCalendars = DashboardView.SetupDashboardCalendars(dashboardDoc); // open this new dashboard diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 023324881..f7cd0e925 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -63,7 +63,7 @@ import { PresBox, PresElementBox } from './nodes/trails'; import { SearchBox } from './search/SearchBox'; import { ImageLabelBox } from './collections/collectionFreeForm/ImageLabelBox'; import { FaceRecognitionHandler } from './search/FaceRecognitionHandler'; -import { FaceCollectionBox } from './collections/collectionFreeForm/FaceCollectionBox'; +import { FaceCollectionBox, UniqueFaceBox } from './collections/collectionFreeForm/FaceCollectionBox'; import { Node } from 'prosemirror-model'; import { EditorView } from 'prosemirror-view'; @@ -140,6 +140,7 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' }; SearchBox, ImageLabelBox, FaceCollectionBox, + UniqueFaceBox, FunctionPlotBox, InkingStroke, LinkBox, diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 3f8aee792..6402ef16c 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -98,7 +98,7 @@ export class CollectionStackingView extends CollectionSubView Doc | undefined; // specify a layout Doc template to use for children of the collection childHideDecorationTitle?: boolean; childHideResizeHandles?: boolean; + childHideDecorations?: boolean; childDragAction?: dropActionType; childXPadding?: number; childYPadding?: number; diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss index 480d109c8..86120f966 100644 --- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss +++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss @@ -1,10 +1,11 @@ .face-document-item { - background: #555555; margin-top: 10px; margin-bottom: 10px; padding: 10px; - border-radius: 10px; + border-radius: inherit; position: relative; + width: 100%; + height: 100%; h1 { color: white; @@ -14,14 +15,31 @@ .face-collection-buttons { position: absolute; - top: 10px; + top: 0px; right: 10px; } + .face-collection-toggle { + position: absolute; + top: 0px; + left: 10px; + } + .face-document-top { + position: absolute; + top: 0; + margin: auto; + width: 100%; + left: 0; + } .face-document-image-container { display: flex; justify-content: center; flex-wrap: wrap; + overflow-x: hidden; + overflow-y: auto; + position: absolute; + top: 75px; + height: calc(100% - 75px); .image-wrapper { position: relative; diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx index 1cc1f59dc..6a0d51e32 100644 --- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx +++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx @@ -6,23 +6,21 @@ import 'ldrs/ring'; import { action, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import React from 'react'; -import { setupMoveUpEvents } from '../../../../ClientUtils'; -import { Utils, emptyFunction } from '../../../../Utils'; +import { lightOrDark, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils'; +import { emptyFunction } from '../../../../Utils'; import { Doc, Opt } from '../../../../fields/Doc'; -import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; -import { ImageCast } from '../../../../fields/Types'; +import { ImageCast, StrCast } from '../../../../fields/Types'; import { DocumentType } from '../../../documents/DocumentTypes'; import { Docs } from '../../../documents/Documents'; import { DragManager } from '../../../util/DragManager'; import { dropActionType } from '../../../util/DropActionTypes'; -import { SnappingManager } from '../../../util/SnappingManager'; import { undoable } from '../../../util/UndoManager'; import { ViewBoxBaseComponent } from '../../DocComponent'; -import { ObservableReactComponent } from '../../ObservableReactComponent'; import { DocumentView } from '../../nodes/DocumentView'; import { FieldView, FieldViewProps } from '../../nodes/FieldView'; import { FaceRecognitionHandler } from '../../search/FaceRecognitionHandler'; +import { CollectionStackingView } from '../CollectionStackingView'; import './FaceCollectionBox.scss'; import { MarqueeOptionsMenu } from './MarqueeOptionsMenu'; @@ -31,31 +29,29 @@ import { MarqueeOptionsMenu } from './MarqueeOptionsMenu'; * unique face in turn displays the set of images that correspond to the face. */ -interface UniqueFaceProps { - faceDoc: Doc; -} - /** - * React component for rendering a unique face and its collection of image Docs. + * Viewer for unique face Doc collections. * * This both displays a collection of images corresponding tp a unique face, and * allows for editing the face collection by removing an image, or drag-and-dropping * an image that was not recognized. */ @observer -export class UniqueFaceView extends ObservableReactComponent { +export class UniqueFaceBox extends ViewBoxBaseComponent() { + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(UniqueFaceBox, fieldKey); + } private _dropDisposer?: DragManager.DragDropDisposer; + private _oldWheel: HTMLElement | null = null; - constructor(props: UniqueFaceProps) { + constructor(props: FieldViewProps) { super(props); makeObservable(this); } - @observable _displayImages: boolean = true; - protected createDropTarget = (ele: HTMLDivElement) => { this._dropDisposer?.(); - ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this._props.faceDoc)); + ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this._props.Document)); }; protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean { @@ -63,12 +59,12 @@ export class UniqueFaceView extends ObservableReactComponent { ?.filter(doc => doc.type === DocumentType.IMG) .forEach(imgDoc => { // If the current Face Document has no faces, and the doc has more than one face descriptor, don't let the user add the document first. Or should we just use the first face ? - if (FaceRecognitionHandler.UniqueFaceDescriptors(this._props.faceDoc).length === 0 && FaceRecognitionHandler.ImageDocFaceDescriptors(imgDoc).length > 1) { + if (FaceRecognitionHandler.UniqueFaceDescriptors(this._props.Document).length === 0 && FaceRecognitionHandler.ImageDocFaceDescriptors(imgDoc).length > 1) { alert('Cannot add a document with multiple faces as the first item!'); } else { // Loop through the documents' face descriptors and choose the face in the iage with the smallest distance (most similar to the face colleciton) - const faceDescriptorsAsFloat32Array = FaceRecognitionHandler.UniqueFaceDescriptors(this._props.faceDoc).map(fd => new Float32Array(Array.from(fd))); - const labeledFaceDescriptor = new faceapi.LabeledFaceDescriptors(FaceRecognitionHandler.UniqueFaceLabel(this._props.faceDoc), faceDescriptorsAsFloat32Array); + const faceDescriptorsAsFloat32Array = FaceRecognitionHandler.UniqueFaceDescriptors(this._props.Document).map(fd => new Float32Array(Array.from(fd))); + const labeledFaceDescriptor = new faceapi.LabeledFaceDescriptors(FaceRecognitionHandler.UniqueFaceLabel(this._props.Document), faceDescriptorsAsFloat32Array); const faceMatcher = new FaceMatcher([labeledFaceDescriptor], 1); const { face_match } = FaceRecognitionHandler.ImageDocFaceDescriptors(imgDoc).reduce( (prev, face) => { @@ -80,11 +76,12 @@ export class UniqueFaceView extends ObservableReactComponent { // assign the face in the image that's closest to the face collection to be the face that's assigned to the collection if (face_match) { - FaceRecognitionHandler.UniqueFaceAddFaceImage(imgDoc, face_match, this._props.faceDoc); + FaceRecognitionHandler.UniqueFaceAddFaceImage(imgDoc, face_match, this._props.Document); } } }); - return false; + e.stopPropagation(); + return true; } /** @@ -92,14 +89,15 @@ export class UniqueFaceView extends ObservableReactComponent { */ @action onDisplayClick() { - this._displayImages = !this._displayImages; + this._props.Document._displayImages = !this.props.Document._displayImages; + this._props.Document.height = this._props.Document._displayImages ? 400 : 100; } /** * Removes a unique face Doc from the colelction of unique faces. */ deleteUniqueFace = undoable(() => { - FaceRecognitionHandler.DeleteUniqueFace(this._props.faceDoc); + FaceRecognitionHandler.DeleteUniqueFace(this._props.Document); }, 'delete face'); /** @@ -107,9 +105,11 @@ export class UniqueFaceView extends ObservableReactComponent { * @param imgDoc - image Doc to remove */ removeFaceImageFromUniqueFace = undoable((imgDoc: Doc) => { - FaceRecognitionHandler.UniqueFaceRemoveFaceImage(imgDoc, this._props.faceDoc); + FaceRecognitionHandler.UniqueFaceRemoveFaceImage(imgDoc, this._props.Document); }, 'remove doc from face'); + onPassiveWheel = (e: WheelEvent) => e.stopPropagation(); + render() { return (
this.createDropTarget(ele!)}> @@ -117,23 +117,35 @@ export class UniqueFaceView extends ObservableReactComponent {
-

{FaceRecognitionHandler.UniqueFaceLabel(this._props.faceDoc)}

+

{FaceRecognitionHandler.UniqueFaceLabel(this._props.Document)}

- this.onDisplayClick()} - icon={} - color={MarqueeOptionsMenu.Instance.userColor} - style={{ width: '19px' }} - /> - {this._displayImages ? ( -
- {FaceRecognitionHandler.UniqueFaceImages(this._props.faceDoc).map(doc => { +
+ this.onDisplayClick()} + icon={} + color={MarqueeOptionsMenu.Instance.userColor} + style={{ width: '19px' }} + /> +
+ {this.props.Document._displayImages ? ( +
{ + this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); + this._oldWheel = ele; + // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling + ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); + }}> + {FaceRecognitionHandler.UniqueFaceImages(this._props.Document).map((doc, i) => { const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.'); return (
setupMoveUpEvents( this, @@ -165,8 +177,8 @@ export class UniqueFaceView extends ObservableReactComponent { * * Since the collection of recognized faces is stored on the active dashboard, this class * does not itself store any Docs, but accesses the myUniqueFaces field of the current - * dashboard. Each Face collection Doc is rendered using a FaceCollectionDocView which - * is not a CollectionView or even a DocumentView (but probably should be). + * dashboard. (This should probably go away as Doc type in favor of it just being a + * stacking collection of uniqueFace docs) */ @observer export class FaceCollectionBox extends ViewBoxBaseComponent() { @@ -179,18 +191,29 @@ export class FaceCollectionBox extends ViewBoxBaseComponent() { makeObservable(this); } + moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => !!(this._props.removeDocument?.(doc) && addDocument?.(doc)); + render() { - return ( -
- {FaceRecognitionHandler.UniqueFaces().map(doc => ( - - ))} -
+ return !Doc.ActiveDashboard ? null : ( + ); } } Docs.Prototypes.TemplateMap.set(DocumentType.FACECOLLECTION, { layout: { view: FaceCollectionBox, dataField: 'data' }, - options: { acl: '', _width: 400 }, + options: { acl: '', _width: 400, dropAction: dropActionType.embed }, +}); + +Docs.Prototypes.TemplateMap.set(DocumentType.UFACE, { + layout: { view: UniqueFaceBox, dataField: 'face_images' }, + options: { acl: '', _width: 400, _height: 400 }, }); diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx index a17e4c54a..1ab084eaa 100644 --- a/src/client/views/search/FaceRecognitionHandler.tsx +++ b/src/client/views/search/FaceRecognitionHandler.tsx @@ -8,6 +8,8 @@ import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; import { DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; import { ImageField } from '../../../fields/URLField'; +import { UniqueFaceBox } from '../collections/collectionFreeForm/FaceCollectionBox'; +import { Docs } from '../../documents/Documents'; /** * A singleton class that handles face recognition and manages face Doc collections for each face found. @@ -169,15 +171,25 @@ export class FaceRecognitionHandler { * @returns a unique face Doc */ private createUniqueFaceDoc = (dashboard: Doc) => { - const faceDocNum = NumCast(dashboard.myUniqueFaces_count) + 1; - dashboard.myUniqueFaces_count = faceDocNum; // TODO: improve to a better name - - const uniqueFaceDoc = new Doc(); - uniqueFaceDoc.title = `Face ${faceDocNum}`; - uniqueFaceDoc.face = ''; // just to make prettyprinting look better - uniqueFaceDoc.face_label = `Face${faceDocNum}`; - uniqueFaceDoc.face_images = new List(); - uniqueFaceDoc.face_descriptors = new List>(); + const faceDocNum = NumCast(dashboard[DocData].myUniqueFaces_count) + 1; + dashboard[DocData].myUniqueFaces_count = faceDocNum; // TODO: improve to a better name + + const uniqueFaceDoc = Docs.Create.UniqeFaceDocument({ + title: `Face ${faceDocNum}`, + _layout_reflowHorizontal: true, + _layout_reflowVertical: true, + _layout_nativeDimEditable: true, + _layout_borderRounding: '20px', + backgroundColor: '#555555', + _width: 400, + _height: 400, + }); + const uface = uniqueFaceDoc[DocData]; + uface.face = ''; // just to make prettyprinting look better + uface.face_label = `Face${faceDocNum}`; + uface.face_images = new List(); + uface.face_descriptors = new List>(); + Doc.SetContainer(uniqueFaceDoc, Doc.MyFaceCollection); Doc.ActiveDashboard && Doc.AddDocToList(Doc.ActiveDashboard[DocData], 'myUniqueFaces', uniqueFaceDoc); return uniqueFaceDoc; @@ -222,8 +234,8 @@ export class FaceRecognitionHandler { setTimeout(() => this.classifyFacesInImage(imgDoc), 1000); } else { const imgUrl = ImageCast(imgDoc[Doc.LayoutFieldKey(imgDoc)]); - if (imgUrl && !DocListCast(Doc.MyFaceCollection.examinedFaceDocs).includes(imgDoc)) { // only examine Docs that have an image and that haven't already been examined. - Doc.AddDocToList(Doc.MyFaceCollection, 'examinedFaceDocs', imgDoc); + if (imgUrl && !DocListCast(Doc.MyFaceCollection.examinedFaceDocs).includes(imgDoc[DocData])) { // only examine Docs that have an image and that haven't already been examined. + Doc.AddDocToList(Doc.MyFaceCollection, 'examinedFaceDocs', imgDoc[DocData]); FaceRecognitionHandler.initImageDocFaceDescriptors(imgDoc); FaceRecognitionHandler.loadImage(imgUrl).then( // load image and analyze faces img => faceapi.detectAllFaces(img).withFaceLandmarks().withFaceDescriptors() -- cgit v1.2.3-70-g09d2 From d12b43191e01e837d5a144fb19d9d3cd8eadd777 Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 26 Aug 2024 15:17:14 -0400 Subject: from last --- src/client/documents/Documents.ts | 1 + .../collectionFreeForm/FaceCollectionBox.scss | 19 ++++--- .../collectionFreeForm/FaceCollectionBox.tsx | 60 ++++++++++++++-------- src/client/views/search/FaceRecognitionHandler.tsx | 5 +- 4 files changed, 54 insertions(+), 31 deletions(-) (limited to 'src/client/documents') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index c2211fb80..ac271526f 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -362,6 +362,7 @@ export class DocumentOptions { data?: FieldType; data_useCors?: BOOLt = new BoolInfo('whether CORS protocol should be used for web page'); + _face_showImages?: BOOLt = new BoolInfo('whether to show images in uniqe face Doc'); columnHeaders?: List; // headers for stacking views schemaHeaders?: List; // headers for schema view dockingConfig?: STRt = new StrInfo('configuration of golden layout windows (applies only if doc is rendered as a CollectionDockingView)', false); diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss index 86120f966..00297a97d 100644 --- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss +++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss @@ -1,11 +1,10 @@ .face-document-item { - margin-top: 10px; - margin-bottom: 10px; - padding: 10px; - border-radius: inherit; - position: relative; + display: flex; + height: max-content; + flex-direction: column; + top: 0; + position: absolute; width: 100%; - height: 100%; h1 { color: white; @@ -24,11 +23,12 @@ left: 10px; } .face-document-top { - position: absolute; + position: relative; top: 0; margin: auto; width: 100%; left: 0; + height: 60px; } .face-document-image-container { @@ -37,9 +37,8 @@ flex-wrap: wrap; overflow-x: hidden; overflow-y: auto; - position: absolute; - top: 75px; - height: calc(100% - 75px); + position: relative; + padding: 10px; .image-wrapper { position: relative; diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx index 6a0d51e32..33fc3c210 100644 --- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx +++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx @@ -3,14 +3,14 @@ import { IconButton, Size } from 'browndash-components'; import * as faceapi from 'face-api.js'; import { FaceMatcher } from 'face-api.js'; import 'ldrs/ring'; -import { action, makeObservable, observable } from 'mobx'; +import { IReactionDisposer, action, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import React from 'react'; -import { lightOrDark, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils'; +import { DivHeight, lightOrDark, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils'; import { emptyFunction } from '../../../../Utils'; import { Doc, Opt } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; -import { ImageCast, StrCast } from '../../../../fields/Types'; +import { ImageCast, NumCast, StrCast } from '../../../../fields/Types'; import { DocumentType } from '../../../documents/DocumentTypes'; import { Docs } from '../../../documents/Documents'; import { DragManager } from '../../../util/DragManager'; @@ -48,10 +48,29 @@ export class UniqueFaceBox extends ViewBoxBaseComponent() { super(props); makeObservable(this); } + @observable _headerRef: HTMLDivElement | null = null; + @observable _listRef: HTMLDivElement | null = null; + _disposers: { [key: string]: IReactionDisposer } = {}; + _lastHeight = 0; + observer = new ResizeObserver(() => this._props.setHeight?.((this.props.Document._face_showImages ? 20 : 0) + (!this._headerRef ? 0 : DivHeight(this._headerRef)) + (!this._listRef ? 0 : DivHeight(this._listRef)))); + + componentDidMount(): void { + this._disposers.refList = reaction( + () => ({ refList: [this._headerRef, this._listRef], autoHeight: this.layoutDoc._layout_autoHeight && !DocumentView.LightboxContains(this.DocumentView?.()) }), + ({ refList, autoHeight }) => { + this.observer.disconnect(); + if (autoHeight) refList.filter(r => r).forEach(r => this.observer.observe(r!)); + }, + { fireImmediately: true } + ); + } + componentWillUnmount(): void { + //this._disposers?.(); + } protected createDropTarget = (ele: HTMLDivElement) => { this._dropDisposer?.(); - ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this._props.Document)); + ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.Document)); }; protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean { @@ -59,12 +78,12 @@ export class UniqueFaceBox extends ViewBoxBaseComponent() { ?.filter(doc => doc.type === DocumentType.IMG) .forEach(imgDoc => { // If the current Face Document has no faces, and the doc has more than one face descriptor, don't let the user add the document first. Or should we just use the first face ? - if (FaceRecognitionHandler.UniqueFaceDescriptors(this._props.Document).length === 0 && FaceRecognitionHandler.ImageDocFaceDescriptors(imgDoc).length > 1) { + if (FaceRecognitionHandler.UniqueFaceDescriptors(this.Document).length === 0 && FaceRecognitionHandler.ImageDocFaceDescriptors(imgDoc).length > 1) { alert('Cannot add a document with multiple faces as the first item!'); } else { // Loop through the documents' face descriptors and choose the face in the iage with the smallest distance (most similar to the face colleciton) - const faceDescriptorsAsFloat32Array = FaceRecognitionHandler.UniqueFaceDescriptors(this._props.Document).map(fd => new Float32Array(Array.from(fd))); - const labeledFaceDescriptor = new faceapi.LabeledFaceDescriptors(FaceRecognitionHandler.UniqueFaceLabel(this._props.Document), faceDescriptorsAsFloat32Array); + const faceDescriptorsAsFloat32Array = FaceRecognitionHandler.UniqueFaceDescriptors(this.Document).map(fd => new Float32Array(Array.from(fd))); + const labeledFaceDescriptor = new faceapi.LabeledFaceDescriptors(FaceRecognitionHandler.UniqueFaceLabel(this.Document), faceDescriptorsAsFloat32Array); const faceMatcher = new FaceMatcher([labeledFaceDescriptor], 1); const { face_match } = FaceRecognitionHandler.ImageDocFaceDescriptors(imgDoc).reduce( (prev, face) => { @@ -76,7 +95,7 @@ export class UniqueFaceBox extends ViewBoxBaseComponent() { // assign the face in the image that's closest to the face collection to be the face that's assigned to the collection if (face_match) { - FaceRecognitionHandler.UniqueFaceAddFaceImage(imgDoc, face_match, this._props.Document); + FaceRecognitionHandler.UniqueFaceAddFaceImage(imgDoc, face_match, this.Document); } } }); @@ -87,17 +106,17 @@ export class UniqueFaceBox extends ViewBoxBaseComponent() { /** * Toggles whether a Face Document displays its associated docs. */ - @action onDisplayClick() { - this._props.Document._displayImages = !this.props.Document._displayImages; - this._props.Document.height = this._props.Document._displayImages ? 400 : 100; + this.Document._face_showImages && (this._lastHeight = NumCast(this.Document.height)); + this.Document._face_showImages = !this.Document._face_showImages; + setTimeout(action(() => (!this.Document.layout_autoHeight || !this.Document._face_showImages) && (this.Document.height = this.Document._face_showImages ? this._lastHeight : 60))); } /** * Removes a unique face Doc from the colelction of unique faces. */ deleteUniqueFace = undoable(() => { - FaceRecognitionHandler.DeleteUniqueFace(this._props.Document); + FaceRecognitionHandler.DeleteUniqueFace(this.Document); }, 'delete face'); /** @@ -105,7 +124,7 @@ export class UniqueFaceBox extends ViewBoxBaseComponent() { * @param imgDoc - image Doc to remove */ removeFaceImageFromUniqueFace = undoable((imgDoc: Doc) => { - FaceRecognitionHandler.UniqueFaceRemoveFaceImage(imgDoc, this._props.Document); + FaceRecognitionHandler.UniqueFaceRemoveFaceImage(imgDoc, this.Document); }, 'remove doc from face'); onPassiveWheel = (e: WheelEvent) => e.stopPropagation(); @@ -116,31 +135,32 @@ export class UniqueFaceBox extends ViewBoxBaseComponent() {
-
-

{FaceRecognitionHandler.UniqueFaceLabel(this._props.Document)}

+
(this._headerRef = r))}> +

{FaceRecognitionHandler.UniqueFaceLabel(this.Document)}

this.onDisplayClick()} - icon={} + icon={} color={MarqueeOptionsMenu.Instance.userColor} style={{ width: '19px' }} />
- {this.props.Document._displayImages ? ( + {this.props.Document._face_showImages ? (
{ + ref={action((ele: HTMLDivElement | null) => { + this._listRef = ele; this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); this._oldWheel = ele; // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); - }}> - {FaceRecognitionHandler.UniqueFaceImages(this._props.Document).map((doc, i) => { + })}> + {FaceRecognitionHandler.UniqueFaceImages(this.Document).map((doc, i) => { const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.'); return (
Date: Mon, 26 Aug 2024 23:29:47 -0400 Subject: fixed naming of face annotations --- src/client/documents/Documents.ts | 1 + src/client/views/search/FaceRecognitionHandler.tsx | 56 ++++++++++++++-------- 2 files changed, 38 insertions(+), 19 deletions(-) (limited to 'src/client/documents') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index ac271526f..235f54fe8 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -363,6 +363,7 @@ export class DocumentOptions { data?: FieldType; data_useCors?: BOOLt = new BoolInfo('whether CORS protocol should be used for web page'); _face_showImages?: BOOLt = new BoolInfo('whether to show images in uniqe face Doc'); + face?: DOCt = new DocInfo('face document'); columnHeaders?: List; // headers for stacking views schemaHeaders?: List; // headers for schema view dockingConfig?: STRt = new StrInfo('configuration of golden layout windows (applies only if doc is rendered as a CollectionDockingView)', false); diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx index 4e500d24e..4906ba4b6 100644 --- a/src/client/views/search/FaceRecognitionHandler.tsx +++ b/src/client/views/search/FaceRecognitionHandler.tsx @@ -9,6 +9,7 @@ import { ImageField } from '../../../fields/URLField'; import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; +import { ComputedField } from '../../../fields/ScriptField'; /** * A singleton class that handles face recognition and manages face Doc collections for each face found. @@ -176,7 +177,7 @@ export class FaceRecognitionHandler { dashboard[DocData].myUniqueFaces_count = faceDocNum; // TODO: improve to a better name const uniqueFaceDoc = Docs.Create.UniqeFaceDocument({ - title: `Face ${faceDocNum}`, + title: ComputedField.MakeFunction('this.face') as unknown as string, _layout_reflowHorizontal: true, _layout_reflowVertical: true, _layout_nativeDimEditable: true, @@ -237,27 +238,44 @@ export class FaceRecognitionHandler { setTimeout(() => this.classifyFacesInImage(imgDoc), 1000); } else { const imgUrl = ImageCast(imgDoc[Doc.LayoutFieldKey(imgDoc)]); - if (imgUrl && !DocListCast(Doc.MyFaceCollection.examinedFaceDocs).includes(imgDoc[DocData])) { // only examine Docs that have an image and that haven't already been examined. + if (imgUrl && !DocListCast(Doc.MyFaceCollection.examinedFaceDocs).includes(imgDoc[DocData])) { + // only examine Docs that have an image and that haven't already been examined. Doc.AddDocToList(Doc.MyFaceCollection, 'examinedFaceDocs', imgDoc[DocData]); FaceRecognitionHandler.initImageDocFaceDescriptors(imgDoc); - FaceRecognitionHandler.loadImage(imgUrl).then( // load image and analyze faces - img => faceapi.detectAllFaces(img).withFaceLandmarks().withFaceDescriptors() - .then(imgDocFaceDescriptions => { // For each face detected, find a match. - const annos = [] as Doc[]; - const scale = NumCast(imgDoc.data_nativeWidth) / img.width; - for (const fd of imgDocFaceDescriptions) { - const faceDescriptor = new List(Array.from(fd.descriptor)); - annos.push(Docs.Create.FreeformDocument([], {backgroundColor: "#55555555", x: fd.alignedRect.box.left*scale, y: fd.alignedRect.box.top*scale, _width: fd.alignedRect.box.width*scale, _height: fd.alignedRect.box.height*scale})) - FaceRecognitionHandler.ImageDocAddFaceDescriptor(imgDoc, faceDescriptor); // add face descriptor to image's list of descriptors - const matchedUniqueFace = this.findMatchingFaceDoc(fd.descriptor) ?? this.createUniqueFaceDoc(activeDashboard); - FaceRecognitionHandler.UniqueFaceAddFaceImage(imgDoc, faceDescriptor, matchedUniqueFace); // add image/faceDescriptor to matched unique face - } + FaceRecognitionHandler.loadImage(imgUrl).then( + // load image and analyze faces + img => faceapi + .detectAllFaces(img) + .withFaceLandmarks() + .withFaceDescriptors() + .then(imgDocFaceDescriptions => { // For each face detected, find a match. + const annos = [] as Doc[]; + const scale = NumCast(imgDoc.data_nativeWidth) / img.width; + imgDocFaceDescriptions.forEach((fd, i) => { + const faceDescriptor = new List(Array.from(fd.descriptor)); + FaceRecognitionHandler.ImageDocAddFaceDescriptor(imgDoc, faceDescriptor); // add face descriptor to image's list of descriptors + const matchedUniqueFace = this.findMatchingFaceDoc(fd.descriptor) ?? this.createUniqueFaceDoc(activeDashboard); + FaceRecognitionHandler.UniqueFaceAddFaceImage(imgDoc, faceDescriptor, matchedUniqueFace); // add image/faceDescriptor to matched unique face + annos.push( + Docs.Create.FreeformDocument([], { + title: ComputedField.MakeFunction(`this.face.face`) as unknown as string, // + annotationOn: imgDoc, + face: matchedUniqueFace, + backgroundColor: '#55555555', + x: fd.alignedRect.box.left * scale, + y: fd.alignedRect.box.top * scale, + _width: fd.alignedRect.box.width * scale, + _height: fd.alignedRect.box.height * scale, + }) + ); + Doc.SetContainer(annos.lastElement(), imgDoc[DocData]); + }); - imgDoc[DocData].data_annotations = new List(annos); - return imgDocFaceDescriptions; - }) - ); - } // prettier-ignore + imgDoc[DocData].data_annotations = new List(annos); + return imgDocFaceDescriptions; + }) + ); // prettier-ignore + } } }; } -- cgit v1.2.3-70-g09d2 From cf10fbb80a022ddb141e41203b9135b6f1ed9527 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 27 Aug 2024 13:49:07 -0400 Subject: uniying image labels => tags. made face tags distinguishable from other tags - and drag off to place face collection. --- src/client/documents/Documents.ts | 1 + src/client/views/TagsView.scss | 4 + src/client/views/TagsView.tsx | 61 +++++++---- .../collectionFreeForm/FaceCollectionBox.tsx | 33 ++++-- .../collectionFreeForm/ImageLabelBox.tsx | 10 +- src/client/views/search/FaceRecognitionHandler.tsx | 112 +++++++-------------- src/fields/Doc.ts | 6 +- 7 files changed, 117 insertions(+), 110 deletions(-) (limited to 'src/client/documents') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 235f54fe8..af181b031 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -364,6 +364,7 @@ export class DocumentOptions { data_useCors?: BOOLt = new BoolInfo('whether CORS protocol should be used for web page'); _face_showImages?: BOOLt = new BoolInfo('whether to show images in uniqe face Doc'); face?: DOCt = new DocInfo('face document'); + faceDescriptor?: List; columnHeaders?: List; // headers for stacking views schemaHeaders?: List; // headers for schema view dockingConfig?: STRt = new StrInfo('configuration of golden layout windows (applies only if doc is rendered as a CollectionDockingView)', false); diff --git a/src/client/views/TagsView.scss b/src/client/views/TagsView.scss index f7365a51b..24f9e86bc 100644 --- a/src/client/views/TagsView.scss +++ b/src/client/views/TagsView.scss @@ -24,6 +24,10 @@ align-items: center; } +.faceItem { + background-color: lightGreen; +} + .tagsView-suggestions-box { display: flex; flex-wrap: wrap; diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx index 82c85ddf0..da1add55b 100644 --- a/src/client/views/TagsView.tsx +++ b/src/client/views/TagsView.tsx @@ -6,10 +6,10 @@ import React from 'react'; import ResizeObserver from 'resize-observer-polyfill'; import { returnFalse, setupMoveUpEvents } from '../../ClientUtils'; import { emptyFunction } from '../../Utils'; -import { Doc, DocListCast, Opt, StrListCast } from '../../fields/Doc'; +import { Doc, DocListCast, Field, Opt, StrListCast } from '../../fields/Doc'; import { DocData } from '../../fields/DocSymbols'; import { List } from '../../fields/List'; -import { NumCast, StrCast } from '../../fields/Types'; +import { DocCast, NumCast, StrCast } from '../../fields/Types'; import { DocumentType } from '../documents/DocumentTypes'; import { DragManager } from '../util/DragManager'; import { SnappingManager } from '../util/SnappingManager'; @@ -17,6 +17,7 @@ import { undoable } from '../util/UndoManager'; import { ObservableReactComponent } from './ObservableReactComponent'; import './TagsView.scss'; import { DocumentView } from './nodes/DocumentView'; +import { FaceRecognitionHandler } from './search/FaceRecognitionHandler'; /** * The TagsView is a metadata input/display panel shown at the bottom of a DocumentView in a freeform collection. @@ -158,6 +159,10 @@ export class TagItem extends ObservableReactComponent { * @returns */ createTagCollection = () => { + if (!this._props.tagDoc) { + const face = FaceRecognitionHandler.FindUniqueFaceByName(this._props.tag); + return face ? Doc.MakeEmbedding(face) : undefined; + } // Get the documents that contain the tag. const newEmbeddings = TagItem.allDocsWithTag(this._props.tag).map(doc => Doc.MakeEmbedding(doc)); @@ -188,9 +193,13 @@ export class TagItem extends ObservableReactComponent { this, e, () => { - const dragData = new DragManager.DocumentDragData([this.createTagCollection()]); - DragManager.StartDocumentDrag([this._ref.current!], dragData, e.clientX, e.clientY, {}); - return true; + const dragCollection = this.createTagCollection(); + if (dragCollection) { + const dragData = new DragManager.DocumentDragData([dragCollection]); + DragManager.StartDocumentDrag([this._ref.current!], dragData, e.clientX, e.clientY, {}); + return true; + } + return false; }, returnFalse, emptyFunction @@ -199,20 +208,20 @@ export class TagItem extends ObservableReactComponent { }; render() { - setTimeout(() => TagItem.addTagToDoc(this._props.doc, this._props.tag)); // bcz: hack to make sure that Docs are added to their tag Doc collection since metadata can get set anywhere without a guard triggering an add to the collection + this._props.tagDoc && setTimeout(() => TagItem.addTagToDoc(this._props.doc, this._props.tag)); // bcz: hack to make sure that Docs are added to their tag Doc collection since metadata can get set anywhere without a guard triggering an add to the collection const tag = this._props.tag.replace(/^#/, ''); const metadata = tag.startsWith('@') ? tag.replace(/^@/, '') : ''; return ( -
+
{metadata ? ( {tag}  - {this._props.doc[metadata] as string} + {Field.toString(this._props.doc[metadata])} ) : ( tag )} - {this.props.showRemoveUI && ( + {this.props.showRemoveUI && this._props.tagDoc && ( TagItem.removeTagFromDoc(this._props.doc, this._props.tag, this._props.tagDoc), `remove tag ${this._props.tag}`)} @@ -234,12 +243,9 @@ interface TagViewProps { */ @observer export class TagsView extends ObservableReactComponent { - private _ref: React.RefObject; - constructor(props: any) { super(props); makeObservable(this); - this._ref = React.createRef(); } @observable _currentInput = ''; @@ -268,18 +274,26 @@ export class TagsView extends ObservableReactComponent { * just the tag. * @param tag tag string to add */ - submitTag = undoable((tag: string) => { - const submittedLabel = tag.trim(); - submittedLabel && TagItem.addTagToDoc(this._props.View.Document, '#' + submittedLabel.replace(/^#/, '')); - this._currentInput = ''; // Clear the input box - }, 'added doc label'); + submitTag = undoable( + action((tag: string) => { + const submittedLabel = tag.trim(); + submittedLabel && TagItem.addTagToDoc(this._props.View.Document, '#' + submittedLabel.replace(/^#/, '')); + this._currentInput = ''; // Clear the input box + }), + 'added doc label' + ); /** * When 'showTags' is set on a Doc, this displays a wrapping panel of tagItemViews corresponding to all the tags set on the Doc). * When the dropdown is clicked, this will toggle an extended UI that allows additional tags to be added/removed. */ render() { - const tagsList = StrListCast(this._props.View.dataDoc.tags); + const tagsList = new Set(StrListCast(this._props.View.dataDoc.tags)); + const facesList = new Set( + DocListCast(this._props.View.dataDoc[Doc.LayoutFieldKey(this._props.View.Document) + '_annotations']) + .filter(d => d.face) + .map(doc => StrCast(DocCast(doc.face)?.title)) + ); return !this._props.View.Document.showTags ? null : (
{ }}>
- {!tagsList.length ? null : ( // + {!tagsList.size ? null : ( // this.setToEditing(!this._isEditing)} icon={} /> )} - {tagsList.map(tag => ( - + {Array.from(tagsList).map((tag, i) => ( + + ))} + {Array.from(facesList).map((tag, i) => ( + ))}
{this.isEditing ? ( @@ -330,7 +347,7 @@ export class TagsView extends ObservableReactComponent { color={SnappingManager.userVariantColor} tooltip="Add existing tag" onClick={() => this.submitTag(tag)} - key={i} + key={tag} /> ); })} diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx index ea1c22962..c62303dc0 100644 --- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx +++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx @@ -9,8 +9,9 @@ import React from 'react'; import { DivHeight, lightOrDark, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils'; import { emptyFunction } from '../../../../Utils'; import { Doc, Opt } from '../../../../fields/Doc'; +import { DocData } from '../../../../fields/DocSymbols'; import { List } from '../../../../fields/List'; -import { ImageCast, NumCast, StrCast } from '../../../../fields/Types'; +import { DocCast, ImageCast, NumCast, StrCast } from '../../../../fields/Types'; import { DocumentType } from '../../../documents/DocumentTypes'; import { Docs } from '../../../documents/Documents'; import { DragManager } from '../../../util/DragManager'; @@ -87,23 +88,27 @@ export class UniqueFaceBox extends ViewBoxBaseComponent() { ?.filter(doc => doc.type === DocumentType.IMG) .forEach(imgDoc => { // If the current Face Document has no faces, and the doc has more than one face descriptor, don't let the user add the document first. Or should we just use the first face ? - if (FaceRecognitionHandler.UniqueFaceDescriptors(this.Document).length === 0 && FaceRecognitionHandler.ImageDocFaceDescriptors(imgDoc).length > 1) { + if (FaceRecognitionHandler.UniqueFaceDescriptors(this.Document).length === 0 && FaceRecognitionHandler.ImageDocFaceAnnos(imgDoc).length > 1) { alert('Cannot add a document with multiple faces as the first item!'); } else { // Loop through the documents' face descriptors and choose the face in the iage with the smallest distance (most similar to the face colleciton) const faceDescriptorsAsFloat32Array = FaceRecognitionHandler.UniqueFaceDescriptors(this.Document).map(fd => new Float32Array(Array.from(fd))); const labeledFaceDescriptor = new faceapi.LabeledFaceDescriptors(FaceRecognitionHandler.UniqueFaceLabel(this.Document), faceDescriptorsAsFloat32Array); const faceMatcher = new FaceMatcher([labeledFaceDescriptor], 1); - const { face_match } = FaceRecognitionHandler.ImageDocFaceDescriptors(imgDoc).reduce( - (prev, face) => { - const match = faceMatcher.matchDescriptor(new Float32Array(Array.from(face))); - return match.distance < prev.dist ? { dist: match.distance, face_match: face } : prev; + const { faceAnno } = FaceRecognitionHandler.ImageDocFaceAnnos(imgDoc).reduce( + (prev, faceAnno) => { + const match = faceMatcher.matchDescriptor(new Float32Array(Array.from(faceAnno.faceDescriptor as List))); + return match.distance < prev.dist ? { dist: match.distance, faceAnno } : prev; }, - { dist: 1, face_match: undefined as Opt> } + { dist: 1, faceAnno: undefined as Opt } ); // assign the face in the image that's closest to the face collection's face - FaceRecognitionHandler.UniqueFaceAddFaceImage(imgDoc, face_match, this.Document); + if (faceAnno) { + FaceRecognitionHandler.UniqueFaceRemoveFaceImage(faceAnno, DocCast(faceAnno.face)); + FaceRecognitionHandler.UniqueFaceAddFaceImage(faceAnno, this.Document); + faceAnno.face = this.Document; + } } }); e.stopPropagation(); @@ -224,7 +229,15 @@ export class FaceCollectionBox extends ViewBoxBaseComponent() { } moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => !!(this._props.removeDocument?.(doc) && addDocument?.(doc)); - + addDocument = (doc: Doc | Doc[], annotationKey?: string) => { + const uniqueFaceDoc = doc instanceof Doc ? doc : doc[0]; + const added = uniqueFaceDoc.type === DocumentType.UFACE; + if (added) { + Doc.SetContainer(uniqueFaceDoc, Doc.MyFaceCollection); + Doc.ActiveDashboard && Doc.AddDocToList(Doc.ActiveDashboard[DocData], 'myUniqueFaces', uniqueFaceDoc); + } + return added; + }; /** * this changes style provider requests that target the dashboard to requests that target the face collection box which is what's actually being rendered. * This is needed, for instance, to get the default background color from the face collection, not the dashboard. @@ -233,6 +246,7 @@ export class FaceCollectionBox extends ViewBoxBaseComponent() { if (doc === Doc.ActiveDashboard) return this._props.styleProvider?.(this.Document, this._props, property); return this._props.styleProvider?.(doc, this._props, property); }; + render() { return !Doc.ActiveDashboard ? null : (
@@ -245,6 +259,7 @@ export class FaceCollectionBox extends ViewBoxBaseComponent() { Document={Doc.ActiveDashboard} fieldKey="myUniqueFaces" moveDocument={this.moveDocument} + addDocument={this.addDocument} isContentActive={returnTrue} isAnyChildContentActive={returnTrue} childHideDecorations={true} diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx index 6eb3eb784..5d3154e3c 100644 --- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx +++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx @@ -51,7 +51,7 @@ export class ImageLabelBoxData { label = label.toUpperCase().trim(); if (label.length > 0) { if (!this._labelGroups.includes(label)) { - this._labelGroups = [...this._labelGroups, label]; + this._labelGroups = [...this._labelGroups, label.startsWith('#') ? label : '#' + label]; } } }; @@ -178,8 +178,12 @@ export class ImageLabelBox extends ViewBoxBaseComponent() { const labels = imageInfo.labels.split('\n'); labels.forEach(label => { - label = label.replace(/^\d+\.\s*|-|f\*/, '').trim(); - console.log(label); + label = + '#' + + label + .replace(/^\d+\.\s*|-|f\*/, '') + .replace(/^#/, '') + .trim(); imageInfo.doc[DocData][label] = true; (imageInfo.doc[DocData].tags as List).push(label); }); diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx index 021802061..4c10307e6 100644 --- a/src/client/views/search/FaceRecognitionHandler.tsx +++ b/src/client/views/search/FaceRecognitionHandler.tsx @@ -1,15 +1,14 @@ import * as faceapi from 'face-api.js'; import { FaceMatcher } from 'face-api.js'; -import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { Doc, DocListCast } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { List } from '../../../fields/List'; -import { listSpec } from '../../../fields/Schema'; -import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; +import { ComputedField } from '../../../fields/ScriptField'; +import { DocCast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; -import { ComputedField } from '../../../fields/ScriptField'; /** * A singleton class that handles face recognition and manages face Doc collections for each face found. @@ -18,16 +17,17 @@ import { ComputedField } from '../../../fields/ScriptField'; * as a face collection Doc). If the face matches a face collection Doc, then it will be added to that * collection along with the numerical representation of the face, its face descriptor. * - * Image Doc's that are added to one or more face collection Docs will be given these metadata fields: - * _faceDescriptors - list of all the numerical face representations found in the image. - * _faces - list of unique face Docs corresponding to recognized faces in the image. + * Image Doc's that are added to one or more face collection Docs will be given an annotation rectangle that + * highlights where the face is, and the annotation will have these fields: + * faceDescriptor - the numerical face representations found in the image. + * face - the unique face Docs corresponding to recognized face in the image. + * annotationOn - the image where the face was found * * unique face Doc's are created for each person identified and are stored in the Dashboard's myUniqueFaces field * * Each unique face Doc represents a unique face and collects all matching face images for that person. It has these fields: * face - a string label for the person that was recognized (TODO: currently it's just a 'face#') - * face_descriptors - a list of all the face descriptors for different images of the person - * face_docList - a list of all image Docs that contain a face for the person + * face_annos - a list of face annotations, where each anno has */ export class FaceRecognitionHandler { static _instance: FaceRecognitionHandler; @@ -54,42 +54,12 @@ export class FaceRecognitionHandler { }); }; - /** - * return the metadata field name where unique face Docs are stored - * @param imgDoc image with faces - * @returns name of field - */ - private static ImageDocFaceField = (imgDoc: Doc) => `${Doc.LayoutFieldKey(imgDoc)}_faces`; - /** * Returns an array of faceDocs for each face recognized in the image * @param imgDoc image with faces * @returns faceDoc array */ - private static ImageDocFaces = (imgDoc: Doc) => DocListCast(imgDoc[`${Doc.LayoutFieldKey(imgDoc)}_faces`]); - - /** - * initializes an image with an empty list of face descriptors - * @param imgDoc image to initialize - */ - private static initImageDocFaceDescriptors = (imgDoc: Doc) => { - imgDoc[DocData][`${Doc.LayoutFieldKey(imgDoc)}_faceDescriptors`] = new List>(); - }; - /** - * returns the face descriptors for each face found on an image Doc - * @param imgDoc - * @returns list of face descriptors - */ - public static ImageDocFaceDescriptors = (imgDoc: Doc) => imgDoc[DocData][`${Doc.LayoutFieldKey(imgDoc)}_faceDescriptors`] as List>; - - /** - * Adds a face descriptor for a face found in an image - * @param imgDoc image Doc with face - * @param faceDescriptor descriptor of a face - */ - public static ImageDocAddFaceDescriptor = (imgDoc: Doc, faceDescriptor: List) => { - Cast(imgDoc[DocData][`${Doc.LayoutFieldKey(imgDoc)}_faceDescriptors`], listSpec('number'), null).push(faceDescriptor as unknown as number); - }; + public static ImageDocFaceAnnos = (imgDoc: Doc) => DocListCast(imgDoc[`${Doc.LayoutFieldKey(imgDoc)}_annotations`]).filter(doc => doc.face); /** * returns a list of all face collection Docs on the current dashboard @@ -97,6 +67,13 @@ export class FaceRecognitionHandler { */ public static UniqueFaces = () => DocListCast(Doc.ActiveDashboard?.[DocData].myUniqueFaces); + /** + * Find a unique face from its name + * @param name name of unique face + * @returns unique face or undefined + */ + public static FindUniqueFaceByName = (name: string) => FaceRecognitionHandler.UniqueFaces().find(faceDoc => faceDoc.title === name); + /** * Removes a unique face from the set of recognized unique faces * @param faceDoc unique face Doc @@ -117,28 +94,23 @@ export class FaceRecognitionHandler { * @param faceDoc unique face Doc * @returns face descriptors */ - public static UniqueFaceDescriptors = (faceDoc: Doc) => faceDoc[DocData].face_descriptors as List>; + public static UniqueFaceDescriptors = (faceDoc: Doc) => DocListCast(faceDoc[DocData].face_annos).map(face => face.faceDescriptor as List); /** * Returns a list of all face image Docs associated with a unique face Doc * @param faceDoc unique face Doc * @returns image Docs */ - public static UniqueFaceImages = (faceDoc: Doc) => DocListCast(faceDoc[DocData].face_images); + public static UniqueFaceImages = (faceDoc: Doc) => DocListCast(faceDoc[DocData].face_annos).map(face => DocCast(face.annotationOn)); /** * Adds a face image to a unique face Doc, adds the unique face Doc to the images list of reognized faces, * and updates the unique face's set of face image descriptors * @param img - image with faces to add to a face collection Doc - * @param faceDescriptor - the face descriptor for the face in the image to add - * @param faceDoc - unique face Doc + * @param faceAnno - a face annotation */ - public static UniqueFaceAddFaceImage = (img: Doc, faceDescriptor: Opt>, faceDoc: Doc) => { - Doc.AddDocToList(faceDoc, 'face_images', img); - if (faceDescriptor) { - Cast(faceDoc.face_descriptors, listSpec('number'), null).push(faceDescriptor as unknown as number); // items are lists of numbers, not numbers, but type system can't handle that - Doc.AddDocToList(img[DocData], FaceRecognitionHandler.ImageDocFaceField(img), faceDoc); - } + public static UniqueFaceAddFaceImage = (faceAnno: Doc, faceDoc: Doc) => { + Doc.AddDocToList(faceDoc, 'face_annos', faceAnno); }; /** @@ -146,11 +118,9 @@ export class FaceRecognitionHandler { * @param img - image with faces to remove * @param faceDoc - unique face Doc */ - public static UniqueFaceRemoveFaceImage = (img: Doc, faceDoc: Doc) => { - Doc.RemoveDocFromList(faceDoc[DocData], 'face_images', img); - const descriptorsEqual = (a: List, b: List) => (a === b ? true : a.length === b.length ? a.every((element, index) => element === b[index]) : false); - faceDoc[DocData].face_descriptors = new List>(FaceRecognitionHandler.UniqueFaceDescriptors(faceDoc).filter(fd => !FaceRecognitionHandler.ImageDocFaceDescriptors(img).some(desc => descriptorsEqual(fd, desc)))); - Doc.RemoveDocFromList(img[DocData], FaceRecognitionHandler.ImageDocFaceField(img), faceDoc); + public static UniqueFaceRemoveFaceImage = (faceAnno: Doc, faceDoc: Doc) => { + Doc.RemoveDocFromList(faceDoc[DocData], 'face_annos', faceAnno); + faceAnno.face = undefined; }; constructor() { @@ -192,8 +162,7 @@ export class FaceRecognitionHandler { }); const uface = uniqueFaceDoc[DocData]; uface.face = `Face${faceDocNum}`; - uface.face_images = new List(); - uface.face_descriptors = new List>(); + uface.face_annos = new List(); Doc.SetContainer(uniqueFaceDoc, Doc.MyFaceCollection); Doc.ActiveDashboard && Doc.AddDocToList(Doc.ActiveDashboard[DocData], 'myUniqueFaces', uniqueFaceDoc); @@ -243,7 +212,6 @@ export class FaceRecognitionHandler { if (imgUrl && !DocListCast(Doc.MyFaceCollection.examinedFaceDocs).includes(imgDoc[DocData])) { // only examine Docs that have an image and that haven't already been examined. Doc.AddDocToList(Doc.MyFaceCollection, 'examinedFaceDocs', imgDoc[DocData]); - FaceRecognitionHandler.initImageDocFaceDescriptors(imgDoc); FaceRecognitionHandler.loadImage(imgUrl).then( // load image and analyze faces img => faceapi @@ -255,22 +223,20 @@ export class FaceRecognitionHandler { const scale = NumCast(imgDoc.data_nativeWidth) / img.width; imgDocFaceDescriptions.forEach((fd, i) => { const faceDescriptor = new List(Array.from(fd.descriptor)); - FaceRecognitionHandler.ImageDocAddFaceDescriptor(imgDoc, faceDescriptor); // add face descriptor to image's list of descriptors const matchedUniqueFace = this.findMatchingFaceDoc(fd.descriptor) ?? this.createUniqueFaceDoc(activeDashboard); - FaceRecognitionHandler.UniqueFaceAddFaceImage(imgDoc, faceDescriptor, matchedUniqueFace); // add image/faceDescriptor to matched unique face - annos.push( - Docs.Create.FreeformDocument([], { - title: ComputedField.MakeFunction(`this.face.face`, undefined, undefined, 'this.face.face = value') as unknown as string, // - annotationOn: imgDoc, - face: matchedUniqueFace[DocData], - backgroundColor: '#55555555', - x: fd.alignedRect.box.left * scale, - y: fd.alignedRect.box.top * scale, - _width: fd.alignedRect.box.width * scale, - _height: fd.alignedRect.box.height * scale, - }) - ); - Doc.SetContainer(annos.lastElement(), imgDoc[DocData]); + const faceAnno = Docs.Create.FreeformDocument([], { + title: ComputedField.MakeFunction(`this.face.face`, undefined, undefined, 'this.face.face = value') as unknown as string, // + annotationOn: imgDoc, + face: matchedUniqueFace[DocData], + faceDescriptor: faceDescriptor, + backgroundColor: 'transparent', + x: fd.alignedRect.box.left * scale, + y: fd.alignedRect.box.top * scale, + _width: fd.alignedRect.box.width * scale, + _height: fd.alignedRect.box.height * scale, + }) + FaceRecognitionHandler.UniqueFaceAddFaceImage(faceAnno, matchedUniqueFace); // add image/faceDescriptor to matched unique face + annos.push(faceAnno); }); imgDoc[DocData].data_annotations = new List(annos); diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index e3178f20c..7abba7679 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -791,9 +791,9 @@ export namespace Doc { linkMap.set(link[Id], await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates)); } }); - if (Doc.Get(copy, 'title', true)) copy.title = '>:' + doc.title; - // Doc.SetInPlace(copy, 'title', '>:' + doc.title, true); copy.cloneOf = doc; + const cfield = ComputedField.WithoutComputed(() => FieldValue(doc.title)); + if (Doc.Get(copy, 'title', true) && !(cfield instanceof ComputedField)) copy.title = '>:' + doc.title; cloneMap.set(doc[Id], copy); return copy; @@ -1424,7 +1424,7 @@ export namespace Doc { export function Paste(docids: string[], clone: boolean, addDocument: (doc: Doc | Doc[]) => boolean, ptx?: number, pty?: number, newPoint?: number[]) { DocServer.GetRefFields(docids).then(async fieldlist => { - const list = Array.from(Object.values(fieldlist)) + const list = Array.from(fieldlist.values()) .map(d => DocCast(d)) .filter(d => d); const docs = clone ? (await Promise.all(Doc.MakeClones(list, false, false))).map(res => res.clone) : list; -- cgit v1.2.3-70-g09d2