diff options
Diffstat (limited to 'src/client')
| -rw-r--r-- | src/client/ClientRecommender.scss | 12 | ||||
| -rw-r--r-- | src/client/ClientRecommender.tsx | 361 | ||||
| -rw-r--r-- | src/client/apis/IBM_Recommender.ts | 40 | ||||
| -rw-r--r-- | src/client/cognitive_services/CognitiveServices.ts | 91 | ||||
| -rw-r--r-- | src/client/documents/DocumentTypes.ts | 1 | ||||
| -rw-r--r-- | src/client/documents/Documents.ts | 11 | ||||
| -rw-r--r-- | src/client/util/SearchUtil.ts | 25 | ||||
| -rw-r--r-- | src/client/util/TooltipTextMenu.scss | 2 | ||||
| -rw-r--r-- | src/client/views/GlobalKeyHandler.ts | 2 | ||||
| -rw-r--r-- | src/client/views/MainView.tsx | 9 | ||||
| -rw-r--r-- | src/client/views/RecommendationsBox.scss | 68 | ||||
| -rw-r--r-- | src/client/views/RecommendationsBox.tsx | 199 | ||||
| -rw-r--r-- | src/client/views/collections/CollectionSchemaCells.tsx | 13 | ||||
| -rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 11 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentContentsView.tsx | 3 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 133 | ||||
| -rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 7 |
17 files changed, 975 insertions, 13 deletions
diff --git a/src/client/ClientRecommender.scss b/src/client/ClientRecommender.scss new file mode 100644 index 000000000..3f9102f15 --- /dev/null +++ b/src/client/ClientRecommender.scss @@ -0,0 +1,12 @@ +// @import "/views/globalCssVariables.scss"; + +.space{ + border: 1px dashed blue; + border-spacing: 25px; + border-collapse: separate; + align-content: center; +} + +.wrapper{ + text-align: -webkit-center; +}
\ No newline at end of file diff --git a/src/client/ClientRecommender.tsx b/src/client/ClientRecommender.tsx new file mode 100644 index 000000000..364ec0fe0 --- /dev/null +++ b/src/client/ClientRecommender.tsx @@ -0,0 +1,361 @@ +import { Doc, FieldResult } from "../new_fields/Doc"; +import { StrCast, Cast } from "../new_fields/Types"; +import { List } from "../new_fields/List"; +import { CognitiveServices, Confidence, Tag, Service } from "./cognitive_services/CognitiveServices"; +import React = require("react"); +import { observer } from "mobx-react"; +import { observable, action, computed, reaction } from "mobx"; +var assert = require('assert'); +var sw = require('stopword'); +var FeedParser = require('feedparser'); +import "./ClientRecommender.scss"; +import { JSXElement } from "babel-types"; +import { RichTextField } from "../new_fields/RichTextField"; +import { ToPlainText } from "../new_fields/FieldSymbols"; +import { listSpec } from "../new_fields/Schema"; +import { Identified } from "./Network"; +import { ComputedField } from "../new_fields/ScriptField"; +import { ImageField } from "../new_fields/URLField"; + +export interface RecommenderProps { + title: string; +} + +/** + * actualDoc: datadoc + * vectorDoc: mean vector of text + * score: similarity score to main doc + */ + +export interface RecommenderDocument { + actualDoc: Doc; + vectorDoc: number[]; + score: number; +} + +const fieldkey = "data"; + +@observer +export class ClientRecommender extends React.Component<RecommenderProps> { + + + + static Instance: ClientRecommender; + private mainDoc?: RecommenderDocument; + private docVectors: Set<RecommenderDocument> = new Set(); + + @observable private corr_matrix = [[0, 0], [0, 0]]; // for testing + + constructor(props: RecommenderProps) { + //console.log("creating client recommender..."); + super(props); + if (!ClientRecommender.Instance) ClientRecommender.Instance = this; + ClientRecommender.Instance.docVectors = new Set(); + //ClientRecommender.Instance.corr_matrix = [[0, 0], [0, 0]]; + } + + @action + public reset_docs() { + ClientRecommender.Instance.docVectors = new Set(); + ClientRecommender.Instance.mainDoc = undefined; + ClientRecommender.Instance.corr_matrix = [[0, 0], [0, 0]]; + } + + /*** + * Computes the cosine similarity between two vectors in Euclidean space. + */ + + private distance(vector1: number[], vector2: number[], metric: string = "cosine") { + assert(vector1.length === vector2.length, "Vectors are not the same length"); + let similarity: number; + switch (metric) { + case "cosine": + var dotproduct = 0; + var mA = 0; + var mB = 0; + for (let i = 0; i < vector1.length; i++) { // here you missed the i++ + dotproduct += (vector1[i] * vector2[i]); + mA += (vector1[i] * vector1[i]); + mB += (vector2[i] * vector2[i]); + } + mA = Math.sqrt(mA); + mB = Math.sqrt(mB); + similarity = (dotproduct) / ((mA) * (mB)); // here you needed extra brackets + return similarity; + case "euclidian": + var sum = 0; + for (let i = 0; i < vector1.length; i++) { + sum += Math.pow(vector1[i] - vector2[i], 2); + } + similarity = Math.sqrt(sum); + return similarity; + default: + return 0; + } + } + + /** + * Returns list of {doc, similarity (to main doc)} in increasing score + */ + + public computeSimilarities(distance_metric: string) { + const parameters: any = {}; + Identified.PostToServer("/IBMAnalysis", parameters).then(response => { + console.log("ANALYSIS RESULTS! ", response); + }); + ClientRecommender.Instance.docVectors.forEach((doc: RecommenderDocument) => { + if (ClientRecommender.Instance.mainDoc) { + const distance = ClientRecommender.Instance.distance(ClientRecommender.Instance.mainDoc.vectorDoc, doc.vectorDoc, distance_metric); + doc.score = distance; + } + } + ); + let doclist = Array.from(ClientRecommender.Instance.docVectors); + if (distance_metric == "euclidian") { + doclist.sort((a: RecommenderDocument, b: RecommenderDocument) => a.score - b.score); + } + else { + doclist.sort((a: RecommenderDocument, b: RecommenderDocument) => b.score - a.score); + } + return doclist; + } + + /*** + * Computes the mean of a set of vectors + */ + + public mean(paragraph: Set<number[]>) { + const n = 512; + const num_words = paragraph.size; + let meanVector = new Array<number>(n).fill(0); // mean vector + if (num_words > 0) { // check to see if paragraph actually was vectorized + paragraph.forEach((wordvec: number[]) => { + for (let i = 0; i < n; i++) { + meanVector[i] += wordvec[i]; + } + }); + meanVector = meanVector.map(x => x / num_words); + } + return meanVector; + } + + public processVector(vector: number[], dataDoc: Doc, isMainDoc: boolean) { + if (vector.length > 0) { + const internalDoc: RecommenderDocument = { actualDoc: dataDoc, vectorDoc: vector, score: 0 }; + ClientRecommender.Instance.addToDocSet(internalDoc, isMainDoc); + } + } + + private addToDocSet(internalDoc: RecommenderDocument, isMainDoc: boolean) { + if (ClientRecommender.Instance.docVectors) { + if (isMainDoc) ClientRecommender.Instance.mainDoc = internalDoc; + ClientRecommender.Instance.docVectors.add(internalDoc); + } + } + + /*** + * Generates tags for an image using Cognitive Services + */ + + generateMetadata = async (dataDoc: Doc, extDoc: Doc, threshold: Confidence = Confidence.Excellent) => { + let converter = (results: any) => { + let tagDoc = new Doc; + let tagsList = new List(); + results.tags.map((tag: Tag) => { + tagsList.push(tag.name); + let sanitized = tag.name.replace(" ", "_"); + tagDoc[sanitized] = ComputedField.MakeFunction(`(${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`); + }); + extDoc.generatedTags = tagsList; + tagDoc.title = "Generated Tags Doc"; + tagDoc.confidence = threshold; + return tagDoc; + }; + const url = this.url(dataDoc); + if (url) { + return CognitiveServices.Image.Appliers.ProcessImage(extDoc, ["generatedTagsDoc"], url, Service.ComputerVision, converter); + } + } + + /*** + * Gets URL of image + */ + + private url(dataDoc: Doc) { + let data = Cast(Doc.GetProto(dataDoc)[fieldkey], ImageField); + return data ? data.url.href : undefined; + } + + /*** + * Uses Cognitive Services to extract keywords from a document + */ + + public async extractText(dataDoc: Doc, extDoc: Doc, internal: boolean = true, isMainDoc: boolean = false, image: boolean = false) { + let data: string = ""; + let taglist: FieldResult<List<string>> = undefined; + if (image) { + if (!extDoc.generatedTags) await this.generateMetadata(dataDoc, extDoc); // TODO: Automatically generate tags. Need to ask Sam about this. + if (extDoc.generatedTags) { + taglist = Cast(extDoc.generatedTags, listSpec("string")); + taglist!.forEach(tag => { + data += tag + ", "; + }); + } + } + else { + let fielddata = Cast(dataDoc.data, RichTextField); + fielddata ? data = fielddata[ToPlainText]() : data = ""; + } + let converter = async (results: any, data: string, isImage: boolean = false) => { + let keyterms = new List<string>(); // raw keywords + // let keyterms_counted = new List<string>(); // keywords, where each keyword is repeated. input to w2v + let kp_string: string = ""; // keywords*frequency concatenated into a string. input into TF + let highKP: string[] = [""]; // most frequent keyphrase + let high = 0; + + if (isImage) { + kp_string = data; + if (taglist) { + keyterms = taglist; + highKP = [taglist[0]]; + } + } + else { // text processing + results.documents.forEach((doc: any) => { + let keyPhrases = doc.keyPhrases; + keyPhrases.map((kp: string) => { + keyterms.push(kp); + const frequency = this.countFrequencies(kp, data); // frequency of keyphrase in paragraph + kp_string += kp + ", "; // ensures that if frequency is 0 for some reason kp is still added + for (let i = 0; i < frequency - 1; i++) { + kp_string += kp + ", "; // weights repeated keywords higher + } + // replaces highKP with new one + if (frequency > high) { + high = frequency; + highKP = [kp]; + } + // appends to current highKP phrase + else if (frequency === high) { + highKP.push(kp); + } + }); + }); + } + if (kp_string.length > 2) kp_string = kp_string.substring(0, kp_string.length - 2); + console.log("kp string: ", kp_string); + let values = ""; + if (!internal) values = await this.sendRequest(highKP); + return { keyterms: keyterms, external_recommendations: values, kp_string: [kp_string] }; + }; + if (data !== "") { + return CognitiveServices.Text.Appliers.analyzer(dataDoc, extDoc, ["key words"], data, converter, isMainDoc, internal); + } + return; + } + + /** + * + * Counts frequencies of keyphrase in paragraph. + */ + + private countFrequencies(keyphrase: string, paragraph: string) { + let data = paragraph.split(/ |\n/); // splits by new lines and spaces + let kp_array = keyphrase.split(" "); + let num_keywords = kp_array.length; + let par_length = data.length; + let frequency = 0; + // slides keyphrase windows across paragraph and checks if it matches with corresponding paragraph slice + for (let i = 0; i <= par_length - num_keywords; i++) { + const window = data.slice(i, i + num_keywords); + if (JSON.stringify(window).toLowerCase() === JSON.stringify(kp_array).toLowerCase() || kp_array.every(val => window.includes(val))) { + frequency++; + } + } + return frequency; + } + + /** + * + * Removes stopwords from list of strings representing a sentence + */ + + private removeStopWords(word_array: string[]) { + //console.log(sw.removeStopwords(word_array)); + return sw.removeStopwords(word_array); + } + + /** + * + * API for sending arXiv request. + */ + + private async sendRequest(keywords: string[]) { + let query = ""; + keywords.forEach((kp: string) => query += " " + kp); + return new Promise<any>(resolve => { + this.arxivrequest(query).then(resolve); + }); + } + + /** + * Actual request to the arXiv server for ML articles. + */ + + arxivrequest = async (query: string) => { + let xhttp = new XMLHttpRequest(); + let serveraddress = "http://export.arxiv.org/api"; + const maxresults = 5; + let endpoint = serveraddress + "/query?search_query=all:" + query + "&start=0&max_results=" + maxresults.toString(); + let promisified = (resolve: any, reject: any) => { + xhttp.onreadystatechange = function () { + if (this.readyState === 4) { + let result = xhttp.response; + let xml = xhttp.responseXML; + console.log(xml); + switch (this.status) { + case 200: + let title_vals: string[] = []; + let url_vals: string[] = []; + //console.log(result); + if (xml) { + let titles = xml.getElementsByTagName("title"); + let counter = 1; + if (titles && titles.length > 1) { + while (counter <= maxresults) { + const title = titles[counter].childNodes[0].nodeValue!; + console.log(title) + title_vals.push(title); + counter++; + } + } + let ids = xml.getElementsByTagName("id"); + counter = 1; + if (ids && ids.length > 1) { + while (counter <= maxresults) { + const url = ids[counter].childNodes[0].nodeValue!; + console.log(url); + url_vals.push(url); + counter++; + } + } + } + return resolve({ title_vals, url_vals }); + case 400: + default: + return reject(result); + } + } + }; + xhttp.open("GET", endpoint, true); + xhttp.send(); + }; + return new Promise<any>(promisified); + } + + render() { + return (<div className="wrapper"> + </div>); + } + +}
\ No newline at end of file diff --git a/src/client/apis/IBM_Recommender.ts b/src/client/apis/IBM_Recommender.ts new file mode 100644 index 000000000..5f80f5126 --- /dev/null +++ b/src/client/apis/IBM_Recommender.ts @@ -0,0 +1,40 @@ +import { Opt } from "../../new_fields/Doc"; + +const NaturalLanguageUnderstandingV1 = require('ibm-watson/natural-language-understanding/v1'); +const { IamAuthenticator } = require('ibm-watson/auth'); + +export namespace IBM_Recommender { + + // pass to IBM account is Browngfx1 + + const naturalLanguageUnderstanding = new NaturalLanguageUnderstandingV1({ + version: '2019-07-12', + authenticator: new IamAuthenticator({ + apikey: 'tLiYwbRim3CnBcCO4phubpf-zEiGcub1uh0V-sD9OKhw', + }), + url: 'https://gateway-wdc.watsonplatform.net/natural-language-understanding/api' + }); + + const analyzeParams = { + 'url': 'www.ibm.com', + 'features': { + 'keywords': { + 'sentiment': true, + 'emotion': true, + 'limit': 3 + } + } + }; + + export const analyze = async (_parameters: any): Promise<Opt<string>> => { + try { + const response = await naturalLanguageUnderstanding.analyze(analyzeParams); + console.log(response); + return (JSON.stringify(response, null, 2)); + } catch (err) { + console.log('error: ', err); + return undefined; + } + }; + +}
\ No newline at end of file diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts index 08fcb4883..d496b442e 100644 --- a/src/client/cognitive_services/CognitiveServices.ts +++ b/src/client/cognitive_services/CognitiveServices.ts @@ -6,12 +6,17 @@ import { RouteStore } from "../../server/RouteStore"; import { Utils } from "../../Utils"; import { InkData } from "../../new_fields/InkField"; import { UndoManager } from "../util/UndoManager"; +import requestPromise = require("request-promise"); +import { List } from "../../new_fields/List"; +import { ClientRecommender } from "../ClientRecommender"; +import { ImageBox } from "../views/nodes/ImageBox"; type APIManager<D> = { converter: BodyConverter<D>, requester: RequestExecutor }; type RequestExecutor = (apiKey: string, body: string, service: Service) => Promise<string>; type AnalysisApplier<D> = (target: Doc, relevantKeys: string[], data: D, ...args: any) => any; type BodyConverter<D> = (data: D) => string; type Converter = (results: any) => Field; +type TextConverter = (results: any, data: string) => Promise<{ keyterms: Field, external_recommendations: any, kp_string: string[] }>; export type Tag = { name: string, confidence: number }; export type Rectangle = { top: number, left: number, width: number, height: number }; @@ -19,7 +24,8 @@ export type Rectangle = { top: number, left: number, width: number, height: numb export enum Service { ComputerVision = "vision", Face = "face", - Handwriting = "handwriting" + Handwriting = "handwriting", + Text = "text" } export enum Confidence { @@ -219,4 +225,87 @@ export namespace CognitiveServices { } + export namespace Text { + export const Manager: APIManager<string> = { + converter: (data: string) => { + return JSON.stringify({ + documents: [{ + id: 1, + language: "en", + text: data + }] + }); + }, + requester: async (apiKey: string, body: string, service: Service) => { + let serverAddress = "https://eastus.api.cognitive.microsoft.com"; + let endpoint = serverAddress + "/text/analytics/v2.1/keyPhrases"; + let sampleBody = { + "documents": [ + { + "language": "en", + "id": 1, + "text": "Hello world. This is some input text that I love." + } + ] + }; + let actualBody = body; + const options = { + uri: endpoint, + body: actualBody, + headers: { + 'Content-Type': 'application/json', + 'Ocp-Apim-Subscription-Key': apiKey + } + + }; + console.log("requested!"); + return request.post(options); + } + }; + + export namespace Appliers { + + export async function vectorize(keyterms: any, dataDoc: Doc, mainDoc: boolean = false) { + console.log("vectorizing..."); + //keyterms = ["father", "king"]; + + let args = { method: 'POST', uri: Utils.prepend("/recommender"), body: { keyphrases: keyterms }, json: true }; + await requestPromise.post(args).then(async (wordvecs) => { + if (wordvecs) { + let indices = Object.keys(wordvecs); + console.log("successful vectorization!"); + var vectorValues = new List<number>(); + indices.forEach((ind: any) => { + //console.log(wordvec.word); + vectorValues.push(wordvecs[ind]); + }); + ClientRecommender.Instance.processVector(vectorValues, dataDoc, mainDoc); + } // adds document to internal doc set + else { + console.log("unsuccessful :( word(s) not in vocabulary"); + } + //console.log(vectorValues.size); + } + ); + } + + export const analyzer = async (dataDoc: Doc, target: Doc, keys: string[], data: string, converter: TextConverter, isMainDoc: boolean = false, isInternal: boolean = true) => { + let results = await ExecuteQuery(Service.Text, Manager, data); + console.log(results); + let { keyterms, external_recommendations, kp_string } = await converter(results, data); + target[keys[0]] = keyterms; + if (isInternal) { + //await vectorize([data], dataDoc, isMainDoc); + await vectorize(kp_string, dataDoc, isMainDoc); + } else { + return external_recommendations; + } + }; + + // export async function countFrequencies() + } + + } + + }
\ No newline at end of file diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 12501065a..32c3e6e80 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -19,6 +19,7 @@ export enum DocumentType { YOUTUBE = "youtube", FONTICON = "fonticonbox", PRES = "presentation", + RECOMMENDATION = "recommendation", LINKFOLLOW = "linkfollow", PRESELEMENT = "preselement", QUERY = "search", diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 1a9d67d83..97f7f8bf0 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -43,6 +43,9 @@ import { PresBox } from "../views/nodes/PresBox"; import { ComputedField, ScriptField } from "../../new_fields/ScriptField"; import { ProxyField } from "../../new_fields/Proxy"; import { DocumentType } from "./DocumentTypes"; +import { RecommendationsBox } from "../views/RecommendationsBox"; +//import { PresBox } from "../views/nodes/PresBox"; +//import { PresField } from "../../new_fields/PresField"; import { LinkFollowBox } from "../views/linking/LinkFollowBox"; import { PresElementBox } from "../views/presentationview/PresElementBox"; import { QueryBox } from "../views/nodes/QueryBox"; @@ -204,6 +207,10 @@ export namespace Docs { layout: { view: FontIconBox, dataField: data }, options: { width: 40, height: 40, borderRounding: "100%" }, }], + [DocumentType.RECOMMENDATION, { + layout: { view: RecommendationsBox }, + options: { width: 200, height: 200 }, + }], [DocumentType.LINKFOLLOW, { layout: { view: LinkFollowBox, dataField: data } }], @@ -515,6 +522,10 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.IMPORT), new List<Doc>(), options); } + export function RecommendationsDocument(data: Doc[], options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.RECOMMENDATION), new List<Doc>(data), options); + } + export type DocConfig = { doc: Doc, initialWidth?: number diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index 2cf13680a..a8f4013f4 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -118,4 +118,27 @@ export namespace SearchUtil { aliasContexts.forEach(result => contexts.aliasContexts.push(...result.ids)); return contexts; } -}
\ No newline at end of file + + export async function GetAllDocs() { + const query = "*"; + let response = await rp.get(Utils.prepend('/search'), { + qs: + { start: 0, rows: 10000, q: query }, + + }); + let result: IdSearchResult = JSON.parse(response); + const { ids, numFound, highlighting } = result; + //console.log(ids.length); + const docMap = await DocServer.GetRefFields(ids); + const docs: Doc[] = []; + for (const id of ids) { + const field = docMap[id]; + if (field instanceof Doc) { + docs.push(field); + } + } + return docs; + // const docs = ids.map((id: string) => docMap[id]).filter((doc: any) => doc instanceof Doc); + // return docs as Doc[]; + } +} diff --git a/src/client/util/TooltipTextMenu.scss b/src/client/util/TooltipTextMenu.scss index 8310a0da6..e2149e9c1 100644 --- a/src/client/util/TooltipTextMenu.scss +++ b/src/client/util/TooltipTextMenu.scss @@ -369,4 +369,4 @@ button.colorPicker { &.active { border: 2px solid white !important; } -}
\ No newline at end of file +} diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 8f397e331..a61a1d8cb 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -6,6 +6,7 @@ import { DragManager } from "../util/DragManager"; import { action, runInAction } from "mobx"; import { Doc } from "../../new_fields/Doc"; import { DictationManager } from "../util/DictationManager"; +import { RecommendationsBox } from "./RecommendationsBox"; import SharingManager from "../util/SharingManager"; import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; import { Cast, PromiseValue } from "../../new_fields/Types"; @@ -78,6 +79,7 @@ export default class KeyManager { } SelectionManager.DeselectAll(); DictationManager.Controls.stop(); + // RecommendationsBox.Instance.closeMenu(); SharingManager.Instance.close(); break; case "delete": diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 83dbb433b..825856ff3 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -33,9 +33,14 @@ import KeyManager from './GlobalKeyHandler'; import "./MainView.scss"; import { MainViewNotifs } from './MainViewNotifs'; import { DocumentView } from './nodes/DocumentView'; -import { OverlayView } from './OverlayView'; import PDFMenu from './pdf/PDFMenu'; import { PreviewCursor } from './PreviewCursor'; +import { FilterBox } from './search/FilterBox'; +import { SchemaHeaderField, RandomPastel } from '../../new_fields/SchemaHeaderField'; +//import { DocumentManager } from '../util/DocumentManager'; +import { RecommendationsBox } from './RecommendationsBox'; +import { PresBox } from './nodes/PresBox'; +import { OverlayView } from './OverlayView'; import { Scripting } from '../util/Scripting'; import { AudioBox } from './nodes/AudioBox'; @@ -512,4 +517,4 @@ export class MainView extends React.Component { </div >); } } -Scripting.addGlobal(function freezeSidebar() { MainView.expandFlyout(); });
\ No newline at end of file +Scripting.addGlobal(function freezeSidebar() { MainView.expandFlyout(); }); diff --git a/src/client/views/RecommendationsBox.scss b/src/client/views/RecommendationsBox.scss new file mode 100644 index 000000000..dd8a105f6 --- /dev/null +++ b/src/client/views/RecommendationsBox.scss @@ -0,0 +1,68 @@ +@import "globalCssVariables"; + +.rec-content *{ + display: inline-block; + margin: auto; + width: 50; + height: 150px; + border: 1px dashed grey; + padding: 10px 10px; +} + +.rec-content { + float: left; + width: inherit; + align-content: center; +} + +.rec-scroll { + overflow-y: scroll; + overflow-x: hidden; + position: absolute; + pointer-events: all; + // display: flex; + z-index: 10000; + box-shadow: gray 0.2vw 0.2vw 0.4vw; + // flex-direction: column; + background: whitesmoke; + padding-bottom: 10px; + padding-top: 20px; + // border-radius: 15px; + border: solid #BBBBBBBB 1px; + width: 100%; + text-align: center; + // max-height: 250px; + height: 100%; + text-transform: uppercase; + color: grey; + letter-spacing: 2px; +} + +.content { + padding: 10px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; +} + +.image-background { + pointer-events: none; + background-color: transparent; + width: 50%; + text-align: center; + margin-left: 5px; +} + +img{ + width: 100%; + height: 100%; +} + +.score { + // margin-left: 15px; + width: 50%; + height: 100%; + text-align: center; + margin-left: 10px; +} diff --git a/src/client/views/RecommendationsBox.tsx b/src/client/views/RecommendationsBox.tsx new file mode 100644 index 000000000..0e3cfd729 --- /dev/null +++ b/src/client/views/RecommendationsBox.tsx @@ -0,0 +1,199 @@ +import { observer } from "mobx-react"; +import React = require("react"); +import { observable, action, computed, runInAction } from "mobx"; +import Measure from "react-measure"; +import "./RecommendationsBox.scss"; +import { Doc, DocListCast, WidthSym, HeightSym } from "../../new_fields/Doc"; +import { DocumentIcon } from "./nodes/DocumentIcon"; +import { StrCast, NumCast } from "../../new_fields/Types"; +import { returnFalse, emptyFunction, returnEmptyString, returnOne } from "../../Utils"; +import { Transform } from "../util/Transform"; +import { ObjectField } from "../../new_fields/ObjectField"; +import { DocumentView } from "./nodes/DocumentView"; +import { DocumentType } from '../documents/DocumentTypes'; +import { ClientRecommender } from "../ClientRecommender"; +import { DocServer } from "../DocServer"; +import { Id } from "../../new_fields/FieldSymbols"; +import { FieldView, FieldViewProps } from "./nodes/FieldView"; +import { DocumentManager } from "../util/DocumentManager"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { library } from "@fortawesome/fontawesome-svg-core"; +import { faBullseye, faLink } from "@fortawesome/free-solid-svg-icons"; +import { DocUtils } from "../documents/Documents"; + +export interface RecProps { + documents: { preview: Doc, similarity: number }[]; + node: Doc; +} + +library.add(faBullseye, faLink); + +@observer +export class RecommendationsBox extends React.Component<FieldViewProps> { + + public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(RecommendationsBox, fieldKey); } + + // @observable private _display: boolean = false; + @observable private _pageX: number = 0; + @observable private _pageY: number = 0; + @observable private _width: number = 0; + @observable private _height: number = 0; + @observable.shallow private _docViews: JSX.Element[] = []; + // @observable private _documents: { preview: Doc, score: number }[] = []; + private previewDocs: Doc[] = []; + + constructor(props: FieldViewProps) { + super(props); + } + + @action + private DocumentIcon(doc: Doc) { + let layoutresult = StrCast(doc.type); + let renderDoc = doc; + //let box: number[] = []; + if (layoutresult.indexOf(DocumentType.COL) !== -1) { + renderDoc = Doc.MakeDelegate(renderDoc); + } + let returnXDimension = () => 150; + let returnYDimension = () => 150; + let scale = () => returnXDimension() / NumCast(renderDoc.nativeWidth, returnXDimension()); + //let scale = () => 1; + let newRenderDoc = Doc.MakeAlias(renderDoc); /// newRenderDoc -> renderDoc -> render"data"Doc -> TextProt + newRenderDoc.height = NumCast(this.props.Document.documentIconHeight); + newRenderDoc.autoHeight = false; + const docview = <div> + <DocumentView + fitToBox={StrCast(doc.type).indexOf(DocumentType.COL) !== -1} + Document={newRenderDoc} + addDocument={returnFalse} + removeDocument={returnFalse} + ruleProvider={undefined} + ScreenToLocalTransform={Transform.Identity} + addDocTab={returnFalse} + pinToPres={returnFalse} + renderDepth={1} + PanelWidth={returnXDimension} + PanelHeight={returnYDimension} + focus={emptyFunction} + backgroundColor={returnEmptyString} + parentActive={returnFalse} + whenActiveChanged={returnFalse} + bringToFront={emptyFunction} + zoomToScale={emptyFunction} + getScale={returnOne} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + ContentScaling={scale} + /> + </div>; + return docview; + + } + + // @action + // closeMenu = () => { + // this._display = false; + // this.previewDocs.forEach(doc => DocServer.DeleteDocument(doc[Id])); + // this.previewDocs = []; + // } + + // @action + // resetDocuments = () => { + // this._documents = []; + // } + + // @action + // displayRecommendations(x: number, y: number) { + // this._pageX = x; + // this._pageY = y; + // this._display = true; + // } + + static readonly buffer = 20; + + // get pageX() { + // const x = this._pageX; + // if (x < 0) { + // return 0; + // } + // const width = this._width; + // if (x + width > window.innerWidth - RecommendationsBox.buffer) { + // return window.innerWidth - RecommendationsBox.buffer - width; + // } + // return x; + // } + + // get pageY() { + // const y = this._pageY; + // if (y < 0) { + // return 0; + // } + // const height = this._height; + // if (y + height > window.innerHeight - RecommendationsBox.buffer) { + // return window.innerHeight - RecommendationsBox.buffer - height; + // } + // return y; + // } + + // get createDocViews() { + // return DocListCast(this.props.Document.data).map(doc => { + // return ( + // <div className="content"> + // <span style={{ height: NumCast(this.props.Document.documentIconHeight) }} className="image-background"> + // {this.DocumentIcon(doc)} + // </span> + // <span className="score">{NumCast(doc.score).toFixed(4)}</span> + // <div style={{ marginRight: 50 }} onClick={() => DocumentManager.Instance.jumpToDocument(doc, false)}> + // <FontAwesomeIcon className="documentdecorations-icon" icon={"bullseye"} size="sm" /> + // </div> + // <div style={{ marginRight: 50 }} onClick={() => DocUtils.MakeLink({ doc: this.props.Document.sourceDoc as Doc }, { doc: doc }, "User Selected Link", "Generated from Recommender", undefined)}> + // <FontAwesomeIcon className="documentdecorations-icon" icon={"link"} size="sm" /> + // </div> + // </div> + // ); + // }); + // } + + componentDidMount() { //TODO: invoking a computedFn from outside an reactive context won't be memoized, unless keepAlive is set + runInAction(() => { + if (this._docViews.length === 0) { + this._docViews = DocListCast(this.props.Document.data).map(doc => { + return ( + <div className="content"> + <span style={{ height: NumCast(this.props.Document.documentIconHeight) }} className="image-background"> + {this.DocumentIcon(doc)} + </span> + <span className="score">{NumCast(doc.score).toFixed(4)}</span> + <div style={{ marginRight: 50 }} onClick={() => DocumentManager.Instance.jumpToDocument(doc, false)}> + <FontAwesomeIcon className="documentdecorations-icon" icon={"bullseye"} size="sm" /> + </div> + <div style={{ marginRight: 50 }} onClick={() => DocUtils.MakeLink({ doc: this.props.Document.sourceDoc as Doc }, { doc: doc }, "User Selected Link", "Generated from Recommender", undefined)}> + <FontAwesomeIcon className="documentdecorations-icon" icon={"link"} size="sm" /> + </div> + </div> + ); + }); + } + }); + } + + render() { //TODO: Invariant violation: max depth exceeded error. Occurs when images are rendered. + // if (!this._display) { + // return null; + // } + // let style = { left: this.pageX, top: this.pageY }; + //const transform = "translate(" + (NumCast(this.props.node.x) + 350) + "px, " + NumCast(this.props.node.y) + "px" + let title = StrCast((this.props.Document.sourceDoc as Doc).title); + if (title.length > 15) { + title = title.substring(0, 15) + "..."; + } + return ( + <div className="rec-scroll"> + <p>Recommendations for "{title}"</p> + {this._docViews} + </div> + ); + } + // + // +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index 54a36f691..102dad31f 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -23,6 +23,7 @@ import { faExpand } from '@fortawesome/free-solid-svg-icons'; import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; import { KeyCodes } from "../../northstar/utils/KeyCodes"; import { undoBatch } from "../../util/UndoManager"; +import { List } from "lodash"; library.add(faExpand); @@ -82,10 +83,20 @@ export class CollectionSchemaCell extends React.Component<CellProps> { } @action - onPointerDown = (e: React.PointerEvent): void => { + onPointerDown = async (e: React.PointerEvent): Promise<void> => { this.props.changeFocusedCellByIndex(this.props.row, this.props.col); this.props.setPreviewDoc(this.props.rowProps.original); + let url: string; + if (url = StrCast(this.props.rowProps.row.href)) { + try { + new URL(url); + const temp = window.open(url)!; + temp.blur(); + window.focus(); + } catch { } + } + // this._isEditing = true; // this.props.setIsEditing(true); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 0c5f4ec80..bb5d99a28 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -12,7 +12,6 @@ import { BoolCast, Cast, DateCast, NumCast, StrCast } from "../../../../new_fiel import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; import { aggregateBounds, emptyFunction, intersectRect, returnOne, Utils } from "../../../../Utils"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; -import { DocServer } from "../../../DocServer"; import { Docs } from "../../../documents/Documents"; import { DocumentType } from "../../../documents/DocumentTypes"; import { DocumentManager } from "../../../util/DocumentManager"; @@ -26,7 +25,7 @@ import { ContextMenu } from "../../ContextMenu"; import { ContextMenuProps } from "../../ContextMenuItem"; import { InkingCanvas } from "../../InkingCanvas"; import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; -import { DocumentViewProps } from "../../nodes/DocumentView"; +import { DocumentContentsView } from "../../nodes/DocumentContentsView"; import { FormattedTextBox } from "../../nodes/FormattedTextBox"; import { pageSchema } from "../../nodes/ImageBox"; import { CollectionSubView } from "../CollectionSubView"; @@ -35,7 +34,14 @@ import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCurso import "./CollectionFreeFormView.scss"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); +import v5 = require("uuid/v5"); +import { ClientRecommender } from "../../../ClientRecommender"; +import { SearchUtil } from "../../../util/SearchUtil"; +import { RouteStore } from "../../../../server/RouteStore"; +import { string, number, elementType } from "prop-types"; +import { DocServer } from "../../../DocServer"; import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas"; +import { DocumentViewProps } from "../../nodes/DocumentView"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); @@ -717,6 +723,7 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF const zoom = this.props.zoomScaling(); return <div className={freeformclass} style={{ borderRadius: "inherit", transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)` }}> {this.props.children} + {/* <ClientRecommender title="Distance Matrix" /> */} </div>; } }
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 779d25cdd..594eda8ff 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -31,6 +31,7 @@ import { PresElementBox } from "../presentationview/PresElementBox"; import { VideoBox } from "./VideoBox"; import { WebBox } from "./WebBox"; import React = require("react"); +import { RecommendationsBox } from "../RecommendationsBox"; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? type BindingProps = Without<FieldViewProps, 'fieldKey'>; @@ -97,7 +98,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, FontIconBox: FontIconBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox, - PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, PresElementBox, QueryBox, ColorBox, DocuLinkBox + PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, PresElementBox, QueryBox, ColorBox, DocuLinkBox, RecommendationsBox }} bindings={this.CreateBindings()} jsx={this.layout} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 7132f181e..55063a52c 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -41,6 +41,35 @@ import { DocumentContentsView } from "./DocumentContentsView"; import "./DocumentView.scss"; import { FormattedTextBox } from './FormattedTextBox'; import React = require("react"); +import requestPromise = require('request-promise'); +import { RecommendationsBox } from '../RecommendationsBox'; +import { SearchUtil } from '../../util/SearchUtil'; +import { ClientRecommender } from '../../ClientRecommender'; +import { SchemaHeaderField } from '../../../new_fields/SchemaHeaderField'; + +library.add(fa.faBrain); +library.add(fa.faEdit); +library.add(fa.faTrash); +library.add(fa.faShare); +library.add(fa.faDownload); +library.add(fa.faExpandArrowsAlt); +library.add(fa.faCompressArrowsAlt); +library.add(fa.faLayerGroup); +library.add(fa.faExternalLinkAlt); +library.add(fa.faAlignCenter); +library.add(fa.faCaretSquareRight); +library.add(fa.faSquare); +library.add(fa.faConciergeBell); +library.add(fa.faWindowRestore); +library.add(fa.faFolder); +library.add(fa.faMapPin); +library.add(fa.faLink); +library.add(fa.faFingerprint); +library.add(fa.faCrosshairs); +library.add(fa.faDesktop); +library.add(fa.faUnlock); +library.add(fa.faLock); +library.add(fa.faLaptopCode, fa.faMale, fa.faCopy, fa.faHandPointRight, fa.faCompass, fa.faSnowflake, fa.faMicrophone); library.add(fa.faEdit, fa.faTrash, fa.faShare, fa.faDownload, fa.faExpandArrowsAlt, fa.faCompressArrowsAlt, fa.faLayerGroup, fa.faExternalLinkAlt, fa.faAlignCenter, fa.faCaretSquareRight, fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faLink, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, fa.faLock, fa.faLaptopCode, fa.faMale, @@ -138,7 +167,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) { e.stopPropagation(); let preventDefault = true; - if (this._doubleTap && this.props.renderDepth && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click + if (this._doubleTap && this.props.renderDepth && !this.onClickHandler ?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click let fullScreenAlias = Doc.MakeAlias(this.props.Document); if (StrCast(fullScreenAlias.layoutKey) !== "layoutCustom" && fullScreenAlias.layoutCustom !== undefined) { fullScreenAlias.layoutKey = "layoutCustom"; @@ -352,7 +381,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @undoBatch @action setCustomView = (custom: boolean): void => { - if (this.props.ContainingCollectionView?.props.DataDoc || this.props.ContainingCollectionView?.props.Document.isTemplateDoc) { + if (this.props.ContainingCollectionView ?.props.DataDoc || this.props.ContainingCollectionView ?.props.Document.isTemplateDoc) { Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.ContainingCollectionView.props.Document); } else { custom ? DocumentView.makeCustomViewClicked(this.props.Document, this.props.DataDoc) : DocumentView.makeNativeViewClicked(this.props.Document); @@ -481,6 +510,22 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu // a.download = `DocExport-${this.props.Document[Id]}.zip`; // a.click(); }); + let recommender_subitems: ContextMenuProps[] = []; + + recommender_subitems.push({ + description: "Internal recommendations", + event: () => this.recommender(e), + icon: "brain" + }); + + recommender_subitems.push({ + description: "External recommendations", + event: () => this.externalRecommendation(e), + icon: "brain" + }); + + cm.addItem({ description: "Recommender System", subitems: recommender_subitems, icon: "brain" }); + cm.addItem({ description: "Publish", event: () => DocUtils.Publish(this.props.Document, this.Document.title || "", this.props.addDocument, this.props.removeDocument), icon: "file" }); cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" }); @@ -527,6 +572,90 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu }); } + recommender = async (e: React.MouseEvent) => { + if (!ClientRecommender.Instance) new ClientRecommender({ title: "Client Recommender" }); + let documents: Doc[] = []; + let allDocs = await SearchUtil.GetAllDocs(); + // allDocs.forEach(doc => console.log(doc.title)); + // clears internal representation of documents as vectors + ClientRecommender.Instance.reset_docs(); + //ClientRecommender.Instance.arxivrequest("electrons"); + await Promise.all(allDocs.map((doc: Doc) => { + let isMainDoc: boolean = false; + const dataDoc = Doc.GetDataDoc(doc); + if (doc.type === DocumentType.TEXT) { + if (dataDoc === Doc.GetDataDoc(this.props.Document)) { + isMainDoc = true; + } + if (!documents.includes(dataDoc)) { + documents.push(dataDoc); + const extdoc = doc.data_ext as Doc; + return ClientRecommender.Instance.extractText(doc, extdoc ? extdoc : doc, true, isMainDoc); + } + } + if (doc.type === DocumentType.IMG) { + if (dataDoc === Doc.GetDataDoc(this.props.Document)) { + isMainDoc = true; + } + if (!documents.includes(dataDoc)) { + documents.push(dataDoc); + const extdoc = doc.data_ext as Doc; + return ClientRecommender.Instance.extractText(doc, extdoc ? extdoc : doc, true, isMainDoc, true); + } + } + })); + const doclist = ClientRecommender.Instance.computeSimilarities("cosine"); + let recDocs: { preview: Doc, score: number }[] = []; + // tslint:disable-next-line: prefer-for-of + for (let i = 0; i < doclist.length; i++) { + recDocs.push({ preview: doclist[i].actualDoc, score: doclist[i].score }); + } + + const data = recDocs.map(unit => { + unit.preview.score = unit.score; + return unit.preview; + }); + + console.log(recDocs.map(doc => doc.score)); + + const title = `Showing ${data.length} recommendations for "${StrCast(this.props.Document.title)}"`; + const recommendations = Docs.Create.RecommendationsDocument(data, { title }); + recommendations.documentIconHeight = 150; + recommendations.sourceDoc = this.props.Document; + recommendations.sourceDocContext = this.props.ContainingCollectionView!.props.Document; + CollectionDockingView.AddRightSplit(recommendations, undefined); + + // RecommendationsBox.Instance.displayRecommendations(e.pageX + 100, e.pageY); + } + + externalRecommendation = async (e: React.MouseEvent) => { + if (!ClientRecommender.Instance) new ClientRecommender({ title: "Client Recommender" }); + ClientRecommender.Instance.reset_docs(); + const doc = Doc.GetDataDoc(this.props.Document); + const extdoc = doc.data_ext as Doc; + const values = await ClientRecommender.Instance.extractText(doc, extdoc ? extdoc : doc, false); + const headers = [new SchemaHeaderField("title"), new SchemaHeaderField("href")]; + let bodies: Doc[] = []; + const titles = values.title_vals; + const urls = values.url_vals; + for (let i = 0; i < 5; i++) { + const body = Docs.Create.FreeformDocument([], { title: titles[i] }); + body.href = urls[i]; + bodies.push(body); + } + CollectionDockingView.AddRightSplit(Docs.Create.SchemaDocument(headers, bodies, { title: `Showing External Recommendations for "${StrCast(doc.title)}"` }), undefined); + } + + onPointerEnter = (e: React.PointerEvent): void => { Doc.BrushDoc(this.props.Document); }; + onPointerLeave = (e: React.PointerEvent): void => { Doc.UnBrushDoc(this.props.Document); }; + + // the document containing the view layout information - will be the Document itself unless the Document has + // a layout field. In that case, all layout information comes from there unless overriden by Document + get layoutDoc(): Document { + return Document(Doc.Layout(this.props.Document)); + } + + // does Document set a layout prop // does Document set a layout prop setsLayoutProp = (prop: string) => this.props.Document[prop] !== this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)]; // get the a layout prop by first choosing the prop from Document, then falling back to the layout doc otherwise. diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 212c99f9d..fa8eb7736 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,6 +1,6 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEye } from '@fortawesome/free-regular-svg-icons'; -import { faAsterisk, faFileAudio, faImage, faPaintBrush } from '@fortawesome/free-solid-svg-icons'; +import { faAsterisk, faFileAudio, faImage, faPaintBrush, faBrain } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable, runInAction, trace } from 'mobx'; import { observer } from "mobx-react"; @@ -24,6 +24,8 @@ import FaceRectangles from './FaceRectangles'; import { FieldView, FieldViewProps } from './FieldView'; import "./ImageBox.scss"; import React = require("react"); +import { SearchUtil } from '../../util/SearchUtil'; +import { ClientRecommender } from '../../ClientRecommender'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { documentSchema } from '../../../new_fields/documentSchemas'; import { Id } from '../../../new_fields/FieldSymbols'; @@ -32,7 +34,7 @@ var path = require('path'); const { Howl } = require('howler'); -library.add(faImage, faEye as any, faPaintBrush); +library.add(faImage, faEye as any, faPaintBrush, faBrain); library.add(faFileAudio, faAsterisk); @@ -147,6 +149,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum let modes: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : []; modes.push({ description: "Generate Tags", event: this.generateMetadata, icon: "tag" }); modes.push({ description: "Find Faces", event: this.extractFaces, icon: "camera" }); + //modes.push({ description: "Recommend", event: this.extractText, icon: "brain" }); !existingAnalyze && ContextMenu.Instance.addItem({ description: "Analyzers...", subitems: modes, icon: "hand-point-right" }); ContextMenu.Instance.addItem({ description: "Image Funcs...", subitems: funcs, icon: "asterisk" }); |
