aboutsummaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
Diffstat (limited to 'src/client')
-rw-r--r--src/client/ClientRecommender.scss12
-rw-r--r--src/client/ClientRecommender.tsx329
-rw-r--r--src/client/cognitive_services/CognitiveServices.ts89
-rw-r--r--src/client/documents/DocumentTypes.ts1
-rw-r--r--src/client/documents/Documents.ts11
-rw-r--r--src/client/util/SearchUtil.ts25
-rw-r--r--src/client/util/TooltipTextMenu.scss2
-rw-r--r--src/client/views/GlobalKeyHandler.ts2
-rw-r--r--src/client/views/MainView.tsx12
-rw-r--r--src/client/views/Recommendations.scss68
-rw-r--r--src/client/views/Recommendations.tsx194
-rw-r--r--src/client/views/collections/CollectionSchemaCells.tsx13
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx9
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx8
-rw-r--r--src/client/views/nodes/DocumentView.tsx91
-rw-r--r--src/client/views/nodes/ImageBox.tsx8
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" });