diff options
Diffstat (limited to 'src/client')
-rw-r--r-- | src/client/ClientRecommender.scss | 12 | ||||
-rw-r--r-- | src/client/ClientRecommender.tsx | 329 | ||||
-rw-r--r-- | src/client/cognitive_services/CognitiveServices.ts | 89 | ||||
-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 | 12 | ||||
-rw-r--r-- | src/client/views/Recommendations.scss | 68 | ||||
-rw-r--r-- | src/client/views/Recommendations.tsx | 194 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSchemaCells.tsx | 13 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 9 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentContentsView.tsx | 8 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 91 | ||||
-rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 8 |
16 files changed, 867 insertions, 7 deletions
diff --git a/src/client/ClientRecommender.scss b/src/client/ClientRecommender.scss new file mode 100644 index 000000000..49163cdc8 --- /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..14af0a69b --- /dev/null +++ b/src/client/ClientRecommender.tsx @@ -0,0 +1,329 @@ +import { Doc } from "../new_fields/Doc"; +import { StrCast, Cast } from "../new_fields/Types"; +import { List } from "../new_fields/List"; +import { CognitiveServices } 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 { ToPlainText, RichTextField } from "../new_fields/RichTextField"; + +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; +} + +@observer +export class ClientRecommender extends React.Component<RecommenderProps> { + + static Instance: ClientRecommender; + private mainDoc?: RecommenderDocument; + private docVectors: Set<RecommenderDocument> = new Set(); + private highKP: string[] = []; + + @observable private corr_matrix = [[0, 0], [0, 0]]; + + 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() { + ClientRecommender.Instance.docVectors.forEach((doc: RecommenderDocument) => { + if (ClientRecommender.Instance.mainDoc) { + const distance = ClientRecommender.Instance.distance(ClientRecommender.Instance.mainDoc.vectorDoc, doc.vectorDoc, "euclidian"); + doc.score = distance; + } + } + ); + let doclist = Array.from(ClientRecommender.Instance.docVectors); + doclist.sort((a: RecommenderDocument, b: RecommenderDocument) => a.score - b.score); + return doclist; + } + + /*** + * Computes the mean of a set of vectors + */ + + public mean(paragraph: Set<number[]>, dataDoc: Doc, mainDoc: boolean) { + const n = 200; + 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); + const internalDoc: RecommenderDocument = { actualDoc: dataDoc, vectorDoc: meanVector, score: 0 }; + if (mainDoc) ClientRecommender.Instance.mainDoc = internalDoc; + ClientRecommender.Instance.addToDocSet(internalDoc); + } + return meanVector; + } + + private addToDocSet(internalDoc: RecommenderDocument) { + if (ClientRecommender.Instance.docVectors) { + ClientRecommender.Instance.docVectors.add(internalDoc); + } + } + + /*** + * Uses Cognitive Services to extract keywords from a document + */ + + public async extractText(dataDoc: Doc, extDoc: Doc, internal: boolean = true, mainDoc: boolean = false) { + let fielddata = Cast(dataDoc.data, RichTextField); + let data: string; + fielddata ? data = fielddata[ToPlainText]() : data = ""; + let converter = async (results: any, data: string) => { + let keyterms = new List<string>(); // raw keywords + let keyterms_counted = new List<string>(); // keywords, where each keyword is repeated as + let highKP: string[] = [""]; // most frequent + let high = 0; + results.documents.forEach((doc: any) => { + let keyPhrases = doc.keyPhrases; + keyPhrases.map((kp: string) => { + const frequency = this.countFrequencies(kp, data); + if (frequency > high) { + high = frequency; + highKP = [kp]; + } + else if (frequency === high) { + highKP.push(kp); + } + let words = kp.split(" "); // separates phrase into words + words = this.removeStopWords(words); // removes stop words if they appear in phrases + words.forEach((word) => { + keyterms.push(word); + for (let i = 0; i < frequency; i++) { + keyterms_counted.push(word); + } + }); + }); + }); + this.highKP = highKP; + console.log(highKP); + const kts_counted = new List<string>(); + keyterms_counted.forEach(kt => kts_counted.push(kt.toLowerCase())); + const values = await this.sendRequest(highKP); + return { keyterms: keyterms, keyterms_counted: kts_counted, values }; + }; + return CognitiveServices.Text.Appliers.analyzer(dataDoc, extDoc, ["key words"], data, converter, mainDoc, internal); + } + + private countFrequencies(keyphrase: string, paragraph: string) { + let data = paragraph.split(" "); + let kp_array = keyphrase.split(" "); + let num_keywords = kp_array.length; + let par_length = data.length; + let frequency = 0; + // console.log("Paragraph: ", data); + // console.log("Keyphrases:", kp_array); + 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; + } + + private removeStopWords(word_array: string[]) { + //console.log(sw.removeStopwords(word_array)); + return sw.removeStopwords(word_array); + } + + private async sendRequest(keywords: string[]) { + let query = ""; + keywords.forEach((kp: string) => query += " " + kp); + return new Promise<any>(resolve => { + this.arxivrequest(query).then(resolve); + }); + } + + /** + * Request to the arXiv server for ML articles. + */ + + arxivrequest = async (query: string) => { + let xhttp = new XMLHttpRequest(); + let serveraddress = "http://export.arxiv.org/api"; + let endpoint = serveraddress + "/query?search_query=all:" + query + "&start=0&max_results=5"; + 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 <= 5) { + 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 <= 5) { + 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); + } + + processArxivResult = (result: any) => { + var xmlDoc = result as XMLDocument; + let text = xmlDoc.getElementsByTagName("title")[0].childNodes[0].nodeValue; + console.log(text); + } + + /*** + * Creates distance matrix for all Documents analyzed + */ + + @action + public createDistanceMatrix(documents: Set<RecommenderDocument> = ClientRecommender.Instance.docVectors) { + const documents_list = Array.from(documents); + const n = documents_list.length; + var matrix = new Array<number>(n).fill(0).map(() => new Array<number>(n).fill(0)); + for (let i = 0; i < n; i++) { + var doc1 = documents_list[i]; + for (let j = 0; j < n; j++) { + var doc2 = documents_list[j]; + matrix[i][j] = ClientRecommender.Instance.distance(doc1.vectorDoc, doc2.vectorDoc, "euclidian"); + } + } + ClientRecommender.Instance.corr_matrix = matrix; + return matrix; + } + + @computed + private get generateRows() { + const n = ClientRecommender.Instance.corr_matrix.length; + let rows: JSX.Element[] = []; + for (let i = 0; i < n; i++) { + let children: JSX.Element[] = []; + for (let j = 0; j < n; j++) { + //let cell = React.createElement("td", ClientRecommender.Instance.corr_matrix[i][j]); + let cell = <td>{ClientRecommender.Instance.corr_matrix[i][j].toFixed(4)}</td>; + children.push(cell); + } + //let row = React.createElement("tr", { children: children, key: i }); + let row = <tr>{children}</tr>; + rows.push(row); + } + return rows; + } + + render() { + return (<div className="wrapper"> + <h3 >{ClientRecommender.Instance.props.title ? ClientRecommender.Instance.props.title : "hello"}</h3> + {/* <table className="space" > + <tbody> + <tr key="1"> + <td key="1">{ClientRecommender.Instance.corr_matrix[0][0].toFixed(4)}</td> + <td key="2">{ClientRecommender.Instance.corr_matrix[0][1].toFixed(4)}</td> + </tr> + <tr key="2"> + <td key="1">{ClientRecommender.Instance.corr_matrix[1][0].toFixed(4)}</td> + <td key="2">{ClientRecommender.Instance.corr_matrix[1][1].toFixed(4)}</td> + </tr> + </tbody> + </table> */} + <table className="space"> + <tbody> + {ClientRecommender.Instance.generateRows} + </tbody> + </table> + </div>); + } + +}
\ No newline at end of file diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts index 08fcb4883..7c660c347 100644 --- a/src/client/cognitive_services/CognitiveServices.ts +++ b/src/client/cognitive_services/CognitiveServices.ts @@ -6,12 +6,16 @@ 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"; 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, keyterms_counted: Field, values: any }>; export type Tag = { name: string, confidence: number }; export type Rectangle = { top: number, left: number, width: number, height: number }; @@ -19,7 +23,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 +224,86 @@ 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, data: string) { + 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.length > 0) { + console.log("successful vectorization!"); + var vectorValues = new Set<number[]>(); + wordvecs.forEach((wordvec: any) => { + //console.log(wordvec.word); + vectorValues.add(wordvec as number[]); + }); + ClientRecommender.Instance.mean(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, mainDoc: boolean = false, internal: boolean = true) => { + let results = await ExecuteQuery(Service.Text, Manager, data); + console.log(results); + let { keyterms, values, keyterms_counted } = await converter(results, data); + //target[keys[0]] = Docs.Get.DocumentHierarchyFromJson(results, "Key Word Analysis"); + target[keys[0]] = keyterms; + console.log("analyzed!"); + if (internal) { + await vectorize(keyterms_counted, dataDoc, mainDoc, data); + } else { + return values; + } + }; + + // 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 e5d5885cd..de56878b8 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -19,6 +19,7 @@ export enum DocumentType { YOUTUBE = "youtube", DRAGBOX = "dragbox", PRES = "presentation", + RECOMMENDATION = "recommendation", LINKFOLLOW = "linkfollow", PRESELEMENT = "preselement" }
\ No newline at end of file diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 98f56af01..09486edd1 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -44,6 +44,9 @@ import { PresBox } from "../views/nodes/PresBox"; import { ComputedField } from "../../new_fields/ScriptField"; import { ProxyField } from "../../new_fields/Proxy"; import { DocumentType } from "./DocumentTypes"; +import { RecommendationsBox } from "../views/Recommendations"; +//import { PresBox } from "../views/nodes/PresBox"; +//import { PresField } from "../../new_fields/PresField"; import { LinkFollowBox } from "../views/linking/LinkFollowBox"; import { PresElementBox } from "../views/presentationview/PresElementBox"; var requestImageSize = require('../util/request-image-size'); @@ -173,6 +176,10 @@ export namespace Docs { layout: { view: DragBox }, options: { width: 40, height: 40 }, }], + [DocumentType.RECOMMENDATION, { + layout: { view: RecommendationsBox }, + options: { width: 200, height: 200 }, + }], [DocumentType.LINKFOLLOW, { layout: { view: LinkFollowBox } }], @@ -474,6 +481,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 6706dcb89..d65ec3f40 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -117,4 +117,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 ebf833dbe..ab6cee763 100644 --- a/src/client/util/TooltipTextMenu.scss +++ b/src/client/util/TooltipTextMenu.scss @@ -351,5 +351,5 @@ .dragger{ color: #eee; - margin-left: 5px; + margin: 5px; }
\ No newline at end of file diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index c519991a5..fd8c958a4 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 "./Recommendations"; import SharingManager from "../util/SharingManager"; const modifiers = ["control", "meta", "shift", "alt"]; @@ -73,6 +74,7 @@ export default class KeyManager { main.toggleColorPicker(true); 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 3b0457dff..17de708a2 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -38,6 +38,11 @@ import { DocumentView } from './nodes/DocumentView'; 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 './Recommendations'; +import PresModeMenu from './presentationview/PresentationModeMenu'; +import { PresBox } from './nodes/PresBox'; import { OverlayView } from './OverlayView'; @observer @@ -628,6 +633,12 @@ export class MainView extends React.Component { </div >; } + // clusterDocuments = () => { + // DocumentManager.Instance.DocumentViews(); + // } + + + @action @@ -681,6 +692,7 @@ export class MainView extends React.Component { {this.mainContent} <PreviewCursor /> <ContextMenu /> + {/* <RecommendationsBox /> */} {this.nodesMenu()} {this.miscButtons} <PDFMenu /> diff --git a/src/client/views/Recommendations.scss b/src/client/views/Recommendations.scss new file mode 100644 index 000000000..dd8a105f6 --- /dev/null +++ b/src/client/views/Recommendations.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/Recommendations.tsx b/src/client/views/Recommendations.tsx new file mode 100644 index 000000000..b7b1d84d0 --- /dev/null +++ b/src/client/views/Recommendations.tsx @@ -0,0 +1,194 @@ +import { observer } from "mobx-react"; +import React = require("react"); +import { observable, action } from "mobx"; +import Measure from "react-measure"; +import "./Recommendations.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); } + + static Instance: RecommendationsBox; + // @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 private _documents: { preview: Doc, score: number }[] = []; + private previewDocs: Doc[] = []; + + constructor(props: FieldViewProps) { + super(props); + RecommendationsBox.Instance = this; + } + + 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 bounds = DocListCast(renderDoc.data).reduce((bounds, doc) => { + var [sptX, sptY] = [NumCast(doc.x), NumCast(doc.y)]; + let [bptX, bptY] = [sptX + doc[WidthSym](), sptY + doc[HeightSym]()]; + return { + x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y), + r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b) + }; + }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE }); + } + 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> + {/* onPointerDown={action(() => { + this._useIcons = !this._useIcons; + this._displayDim = this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE); + })} + onPointerEnter={action(() => this._displayDim = this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE))} + onPointerLeave={action(() => this._displayDim = 50)} > */} + <DocumentView + fitToBox={StrCast(doc.type).indexOf(DocumentType.COL) !== -1} + Document={newRenderDoc} + addDocument={returnFalse} + removeDocument={returnFalse} + ScreenToLocalTransform={Transform.Identity} + addDocTab={returnFalse} + renderDepth={1} + PanelWidth={returnXDimension} + PanelHeight={returnYDimension} + focus={emptyFunction} + backgroundColor={returnEmptyString} + // selectOnLoad={false} + pinToPres={emptyFunction} + parentActive={returnFalse} + whenActiveChanged={returnFalse} + bringToFront={emptyFunction} + zoomToScale={emptyFunction} + getScale={returnOne} + ContainingCollectionView={undefined} + ContentScaling={scale} + /> + </div>; + // const data = renderDoc.data; + // if (data instanceof ObjectField) newRenderDoc.data = ObjectField.MakeCopy(data); + // newRenderDoc.preview = true; + // this.previewDocs.push(newRenderDoc); + 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; + // } + + render() { + // 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 ( + // <Measure offset onResize={action((r: any) => { this._width = r.offset.width; this._height = r.offset.height; })}> + // {({ measureRef }) => ( + <div className="rec-scroll"> + <p>Recommendations for "{title}"</p> + {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, true, undefined, undefined, undefined, this.props.Document.sourceDocContext as Doc)}> + <FontAwesomeIcon className="documentdecorations-icon" icon={"bullseye"} size="sm" /> + </div> + <div style={{ marginRight: 50 }} onClick={() => DocUtils.MakeLink(this.props.Document.sourceDoc as Doc, doc, undefined, "User Selected Link", "Generated from Recommender", undefined)}> + <FontAwesomeIcon className="documentdecorations-icon" icon={"link"} size="sm" /> + </div> + </div> + ); + })} + + </div> + // ); + // } + + // </Measure> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index 179e44266..322fd837a 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -27,6 +27,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; import { KeyCodes } from "../../northstar/utils/KeyCodes"; import { undoBatch } from "../../util/UndoManager"; +import { List } from "lodash"; library.add(faExpand); @@ -86,10 +87,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 bfd3e6481..3d01d954e 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -38,6 +38,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 { SearchBox } from "../../SearchBox"; +import { RouteStore } from "../../../../server/RouteStore"; +import { string, number, elementType } from "prop-types"; +import { DocServer } from "../../../DocServer"; +import { FormattedTextBox } from "../../nodes/FormattedTextBox"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); @@ -754,6 +762,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 75dd27f46..e5eb8dbce 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -30,6 +30,14 @@ import { PresElementBox } from "../presentationview/PresElementBox"; import { VideoBox } from "./VideoBox"; import { WebBox } from "./WebBox"; import React = require("react"); +import { FieldViewProps } from "./FieldView"; +import { Without, OmitKeys } from "../../../Utils"; +import { Cast, StrCast, NumCast } from "../../../new_fields/Types"; +import { List } from "../../../new_fields/List"; +import { Doc } from "../../../new_fields/Doc"; +import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox"; +import { RecommendationsBox } from "../../views/Recommendations"; +import { ScriptField } from "../../../new_fields/ScriptField"; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? type BindingProps = Without<FieldViewProps, 'fieldKey'>; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index e50008fdf..0064b98c3 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -36,12 +36,19 @@ import { DocumentContentsView } from "./DocumentContentsView"; import "./DocumentView.scss"; import { FormattedTextBox } from './FormattedTextBox'; import React = require("react"); +import requestPromise = require('request-promise'); +import { RecommendationsBox } from '../Recommendations'; +import { SearchUtil } from '../../util/SearchUtil'; +import { ClientRecommender } from '../../ClientRecommender'; import { DocumentType } from '../../documents/DocumentTypes'; +import { SchemaHeaderField } from '../../../new_fields/SchemaHeaderField'; +const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { ImageField } from '../../../new_fields/URLField'; import SharingManager from '../../util/SharingManager'; import { Scripting } from '../../util/Scripting'; +library.add(fa.faBrain); library.add(fa.faTrash); library.add(fa.faShare); library.add(fa.faDownload); @@ -511,6 +518,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" }); @@ -557,6 +580,72 @@ 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 mainDoc: boolean = false; + const dataDoc = Doc.GetDataDoc(doc); + if (doc.type === DocumentType.TEXT) { + if (dataDoc === Doc.GetDataDoc(this.props.Document)) { + mainDoc = true; + } + if (!documents.includes(dataDoc)) { + documents.push(dataDoc); + const extdoc = doc.data_ext as Doc; + return ClientRecommender.Instance.extractText(doc, extdoc ? extdoc : doc, true, mainDoc); + } + } + })); + const doclist = ClientRecommender.Instance.computeSimilarities(); + 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.Instance.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.Instance.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 @@ -564,7 +653,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu return Document(this.props.Document.layout instanceof Doc ? this.props.Document.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. getLayoutPropStr = (prop: string) => StrCast(this.setsLayoutProp(prop) ? this.props.Document[prop] : this.layoutDoc[prop]); diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index a198a0764..f36b9895f 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 } from 'mobx'; import { observer } from "mobx-react"; @@ -27,12 +27,15 @@ 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 { DocumentType } from '../../documents/Documents'; var requestImageSize = require('../../util/request-image-size'); 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); @@ -207,6 +210,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD 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" }); |