aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package-lock.json73
-rw-r--r--package.json1
-rw-r--r--src/client/apis/gpt/Summarization.ts35
-rw-r--r--src/client/views/MainView.tsx1
-rw-r--r--src/client/views/MarqueeAnnotator.tsx19
-rw-r--r--src/client/views/collections/CollectionView.tsx2
-rw-r--r--src/client/views/nodes/WebBox.tsx19
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx43
-rw-r--r--src/client/views/pdf/GPTPopup.scss9
-rw-r--r--src/client/views/pdf/GPTPopup.tsx19
-rw-r--r--src/client/views/pdf/PDFViewer.tsx22
11 files changed, 199 insertions, 44 deletions
diff --git a/package-lock.json b/package-lock.json
index 547a6e1d7..6ea3936cd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5303,16 +5303,6 @@
"integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=",
"dev": true
},
- "d": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
- "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
- "dev": true,
- "requires": {
- "es5-ext": "^0.10.50",
- "type": "^1.0.1"
- }
- },
"d3-array": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
@@ -6480,28 +6470,6 @@
"is-symbol": "^1.0.2"
}
},
- "es5-ext": {
- "version": "0.10.62",
- "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz",
- "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==",
- "dev": true,
- "requires": {
- "es6-iterator": "^2.0.3",
- "es6-symbol": "^3.1.3",
- "next-tick": "^1.1.0"
- }
- },
- "es6-iterator": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
- "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
- "dev": true,
- "requires": {
- "d": "1",
- "es5-ext": "^0.10.35",
- "es6-symbol": "^3.1.1"
- }
- },
"es6-promise": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz",
@@ -6513,7 +6481,6 @@
"integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
"dev": true,
"requires": {
- "d": "^1.0.1",
"ext": "^1.1.2"
}
},
@@ -16724,6 +16691,40 @@
"mimic-fn": "^2.1.0"
}
},
+ "openai": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/openai/-/openai-3.1.0.tgz",
+ "integrity": "sha512-v5kKFH5o+8ld+t0arudj833Mgm3GcgBnbyN9946bj6u7bvel4Yg6YFz2A4HLIYDzmMjIo0s6vSG9x73kOwvdCg==",
+ "requires": {
+ "axios": "^0.26.0",
+ "form-data": "^4.0.0"
+ },
+ "dependencies": {
+ "axios": {
+ "version": "0.26.1",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
+ "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
+ "requires": {
+ "follow-redirects": "^1.14.8"
+ }
+ },
+ "follow-redirects": {
+ "version": "1.15.2",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
+ "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
+ },
+ "form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ }
+ }
+ }
+ },
"opentype.js": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-1.3.4.tgz",
@@ -21392,12 +21393,6 @@
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
},
- "type": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
- "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==",
- "dev": true
- },
"type-check": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
diff --git a/package.json b/package.json
index 712354355..480657d5f 100644
--- a/package.json
+++ b/package.json
@@ -238,6 +238,7 @@
"nodemon": "^1.19.4",
"normalize.css": "^8.0.1",
"npm": "^6.14.18",
+ "openai": "^3.1.0",
"p-limit": "^2.2.0",
"passport": "^0.4.0",
"passport-google-oauth20": "^2.0.0",
diff --git a/src/client/apis/gpt/Summarization.ts b/src/client/apis/gpt/Summarization.ts
new file mode 100644
index 000000000..3706c7a5b
--- /dev/null
+++ b/src/client/apis/gpt/Summarization.ts
@@ -0,0 +1,35 @@
+import { Configuration, OpenAIApi } from 'openai';
+
+const gptSummarize = async (text: string) => {
+ try {
+ const configuration = new Configuration({
+ apiKey: process.env.OPENAI_KEY,
+ });
+ const openai = new OpenAIApi(configuration);
+ const response = await openai.createCompletion({
+ model: 'text-davinci-003',
+ max_tokens: 256,
+ temperature: 0.7,
+ prompt: `Summarize this text in one sentence: ${text}`,
+ });
+ return response.data.choices[0].text;
+ } catch (err) {
+ console.log(err);
+ return '';
+ }
+};
+
+// Summarizing with the MeaningCloud API
+const fetchSummary = async (text: string, numSentences?: number) => {
+ const key = '0b41c071f838e573847f477e8f69e9d9';
+ const queryURL = '';
+ const sentences = numSentences ? numSentences : 3;
+ const URL = `https://api.meaningcloud.com/summarization-1.0?key=${key}&txt=${text}&sentences=${sentences}`;
+
+ const res = await fetch(URL);
+ const data = await res.json();
+ console.log(data.summary);
+ return data.summary;
+};
+
+export { fetchSummary, gptSummarize };
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 895ed9bda..68f1bd72b 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -321,6 +321,7 @@ export class MainView extends React.Component {
fa.faClone,
fa.faCloudUploadAlt,
fa.faCommentAlt,
+ fa.faCommentDots,
fa.faCompressArrowsAlt,
fa.faCut,
fa.faEllipsisV,
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index 5ab91dd70..9e9f24393 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -28,6 +28,8 @@ export interface MarqueeAnnotatorProps {
docView: DocumentView;
savedAnnotations: () => ObservableMap<number, HTMLDivElement[]>;
selectionText: () => string;
+ summaryText: () => string;
+ setSummaryText: () => Promise<void>;
annotationLayer: HTMLDivElement;
addDocument: (doc: Doc) => boolean;
getPageFromScroll?: (top: number) => number;
@@ -50,6 +52,10 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
AnchorMenu.Instance.OnCrop = (e: PointerEvent) => this.props.anchorMenuCrop?.(this.highlight('', true), true);
AnchorMenu.Instance.OnClick = (e: PointerEvent) => this.props.anchorMenuClick?.()?.(this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true));
+ AnchorMenu.Instance.OnSummary = async (e: PointerEvent) => {
+ await this.props.setSummaryText();
+ this.props.anchorMenuClick?.()?.(this.highlight('', true, undefined, undefined, true));
+ };
AnchorMenu.Instance.OnAudio = unimplementedFunction;
AnchorMenu.Instance.Highlight = this.highlight;
AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations, true);
@@ -141,7 +147,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
@undoBatch
@action
- makeAnnotationDocument = (color: string, isLinkButton?: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>): Opt<Doc> => {
+ makeAnnotationDocument = (color: string, isLinkButton?: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, summarize?: boolean): Opt<Doc> => {
const savedAnnoMap = savedAnnotations?.values() && Array.from(savedAnnotations?.values()).length ? savedAnnotations : this.props.savedAnnotations();
if (savedAnnoMap.size === 0) return undefined;
const savedAnnos = Array.from(savedAnnoMap.values())[0];
@@ -159,7 +165,12 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
return marqueeAnno;
}
- const textRegionAnno = Docs.Create.HTMLAnchorDocument([], { annotationOn: this.props.rootDoc, text: this.props.selectionText(), backgroundColor: 'transparent', title: 'Selection on ' + this.props.rootDoc.title });
+ const textRegionAnno = Docs.Create.HTMLAnchorDocument([], {
+ annotationOn: this.props.rootDoc,
+ text: summarize ? this.props.summaryText() : this.props.selectionText(),
+ backgroundColor: 'transparent',
+ title: 'Selection on ' + this.props.rootDoc.title,
+ });
let minX = Number.MAX_VALUE;
let maxX = -Number.MAX_VALUE;
let minY = Number.MAX_VALUE;
@@ -194,10 +205,10 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
return textRegionAnno;
};
@action
- highlight = (color: string, isLinkButton: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => {
+ highlight = (color: string, isLinkButton: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean, summarize?: boolean) => {
// creates annotation documents for current highlights
const effectiveAcl = GetEffectiveAcl(this.props.rootDoc[DataSym]);
- const annotationDoc = [AclAugment, AclSelfEdit, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color, isLinkButton, savedAnnotations);
+ const annotationDoc = [AclAugment, AclSelfEdit, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color, isLinkButton, savedAnnotations, summarize);
addAsAnnotation && !savedAnnotations && annotationDoc && this.props.addDocument(annotationDoc);
return (annotationDoc as Doc) ?? undefined;
};
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index a28b1ca19..55f46906e 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -67,6 +67,8 @@ interface CollectionViewProps_ extends FieldViewProps {
AddToMap?: (treeViewDoc: Doc, index: number[]) => Doc[];
RemFromMap?: (treeViewDoc: Doc, index: number[]) => Doc[];
hierarchyIndex?: number[]; // hierarchical index of a document up to the rendering root (primarily used for tree views)
+ // for animation trail in-between selection
+ isTrailBox?: boolean;
}
export interface CollectionViewProps extends React.PropsWithChildren<CollectionViewProps_> {}
@observer
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index d0d638e98..996028ec8 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -36,6 +36,7 @@ import { VideoBox } from './VideoBox';
import './WebBox.scss';
import React = require('react');
import { DragManager } from '../../util/DragManager';
+import { gptSummarize } from '../../apis/gpt/Summarization';
const { CreateImage } = require('./WebBoxRenderer');
const _global = (window /* browser */ || global) /* node */ as any;
const htmlToText = require('html-to-text');
@@ -58,6 +59,21 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
private _searchRef = React.createRef<HTMLInputElement>();
private _searchString = '';
+ // GPT Additions
+ private _summaryText: string = '';
+ private _selectionText: string = '';
+ setSummaryText = async () => {
+ try {
+ const summary = await gptSummarize(this.selectionText());
+ this._summaryText = `Summary: ${summary}`;
+ } catch (err) {
+ console.log(err);
+ this._summaryText = 'Failed to fetch summary.';
+ }
+ };
+ summaryText = () => this._summaryText;
+ selectionText = () => this._selectionText;
+
private get _getAnchor() {
return AnchorMenu.Instance?.GetAnchor;
}
@@ -341,6 +357,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const scale = (this.props.NativeDimScaling?.() || 1) * mainContBounds.scale;
const sel = this._iframe.contentWindow.getSelection();
if (sel) {
+ this._selectionText = sel.toString();
this._textAnnotationCreator = () => this.createTextAnnotation(sel, !sel.isCollapsed ? sel.getRangeAt(0) : undefined);
AnchorMenu.Instance.jumpTo(e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale);
}
@@ -1017,6 +1034,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
finishMarquee={this.finishMarquee}
savedAnnotations={this.savedAnnotationsCreator}
selectionText={returnEmptyString}
+ summaryText={this.summaryText}
+ setSummaryText={this.setSummaryText}
annotationLayer={this._annotationLayer.current}
mainCont={this._mainCont.current}
/>
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index c53cc608c..63c8f9145 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -10,6 +10,8 @@ import { SelectionManager } from '../../util/SelectionManager';
import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu';
import { LinkPopup } from '../linking/LinkPopup';
import { ButtonDropdown } from '../nodes/formattedText/RichTextMenu';
+import { gptSummarize } from '../../apis/gpt/Summarization';
+import { GPTPopup } from './GPTPopup';
import './AnchorMenu.scss';
@observer
@@ -43,10 +45,30 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@observable public Highlighting: boolean = false;
@observable public Status: 'marquee' | 'annotation' | '' = '';
+ // GPT additions (flow 2)
+ @observable private summarizedText: string = '';
+ @observable private showGPTPopup: boolean = false;
+ @action
+ setGPTPopupVis = (vis: boolean) => {
+ this.showGPTPopup = vis;
+ };
+ @action
+ setSummarizedText = (txt: string) => {
+ this.summarizedText = txt;
+ };
+
+ private selectedText: string = '';
+ setSelectedText = (txt: string) => {
+ this.selectedText = txt;
+ };
+
public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search
public OnCrop: (e: PointerEvent) => void = unimplementedFunction;
public OnClick: (e: PointerEvent) => void = unimplementedFunction;
+ public OnSummary: (e: PointerEvent) => Promise<void> = () => {
+ return new Promise(() => {});
+ };
public OnAudio: (e: PointerEvent) => void = unimplementedFunction;
public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
public StartCropDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
@@ -83,11 +105,26 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
() => SelectionManager.Views(),
selected => {
this._showLinkPopup = false;
+ this.setGPTPopupVis(false);
AnchorMenu.Instance.fadeOut(true);
}
);
}
+ getGPTSummary = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, returnFalse, returnFalse, e => this.OnSummary?.(e));
+ };
+
+ invokeGPT = async (e: React.PointerEvent) => {
+ this.setGPTPopupVis(true);
+ const res = await gptSummarize(this.selectedText);
+ if (res) {
+ this.setSummarizedText(res);
+ } else {
+ this.setSummarizedText('Something went wrong.');
+ }
+ };
+
pointerDown = (e: React.PointerEvent) => {
setupMoveUpEvents(
this,
@@ -192,6 +229,12 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
<FontAwesomeIcon icon="comment-alt" size="lg" />
</button>
</Tooltip>
+ <Tooltip key="gpt" title={<div className="dash-tooltip">Summarize with GPT-3</div>}>
+ <button className="antimodeMenu-button annotate" onPointerDown={this.getGPTSummary} style={{ cursor: 'grab' }}>
+ <FontAwesomeIcon icon="comment-dots" size="lg" />
+ </button>
+ </Tooltip>
+ <GPTPopup key="gptpopup" visible={this.showGPTPopup} text={this.summarizedText} />
{AnchorMenu.Instance.OnAudio === unimplementedFunction ? null : (
<Tooltip key="annoaudiotate" title={<div className="dash-tooltip">Click to Record Annotation</div>}>
<button className="antimodeMenu-button annotate" onPointerDown={this.audioDown} style={{ cursor: 'grab' }}>
diff --git a/src/client/views/pdf/GPTPopup.scss b/src/client/views/pdf/GPTPopup.scss
new file mode 100644
index 000000000..6f2e39b7e
--- /dev/null
+++ b/src/client/views/pdf/GPTPopup.scss
@@ -0,0 +1,9 @@
+.summary-box {
+ background-color: #ffffff;
+ position: absolute;
+ top: 0;
+ width: 200px;
+ height: 200px;
+ padding: 20px;
+ overflow: auto;
+}
diff --git a/src/client/views/pdf/GPTPopup.tsx b/src/client/views/pdf/GPTPopup.tsx
new file mode 100644
index 000000000..110351126
--- /dev/null
+++ b/src/client/views/pdf/GPTPopup.tsx
@@ -0,0 +1,19 @@
+import { observer } from 'mobx-react';
+import React = require('react');
+import './GPTPopup.scss';
+
+interface GPTPopupProps {
+ visible: boolean;
+ text: string;
+}
+
+@observer
+export class GPTPopup extends React.Component<GPTPopupProps> {
+ render() {
+ return (
+ <div className="summary-box" style={{ display: this.props.visible ? 'block' : 'none' }}>
+ {`Summary: ${this.props.text}`}
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index b0b7816b8..324f31f23 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -24,6 +24,7 @@ import { AnchorMenu } from './AnchorMenu';
import { Annotation } from './Annotation';
import './PDFViewer.scss';
import React = require('react');
+import { gptSummarize } from '../../apis/gpt/Summarization';
const PDFJSViewer = require('pdfjs-dist/web/pdf_viewer');
const pdfjsLib = require('pdfjs-dist');
const _global = (window /* browser */ || global) /* node */ as any;
@@ -42,7 +43,7 @@ interface IViewerProps extends FieldViewProps {
url: string;
loaded?: (nw: number, nh: number, np: number) => void;
setPdfViewer: (view: PDFViewer) => void;
- anchorMenuClick?: () => undefined | ((anchor: Doc) => void);
+ anchorMenuClick?: () => undefined | ((anchor: Doc, summarize?: boolean) => void);
crop: (region: Doc | undefined, addCrop?: boolean) => Doc | undefined;
}
@@ -82,6 +83,19 @@ export class PDFViewer extends React.Component<IViewerProps> {
return AnchorMenu.Instance?.GetAnchor;
}
+ // Fields for using GPT to summarize selected text
+ private _summaryText: string = '';
+ setSummaryText = async () => {
+ try {
+ const summary = await gptSummarize(this.selectionText());
+ this._summaryText = `Summary: ${summary}`;
+ } catch (err) {
+ console.log(err);
+ this._summaryText = 'Failed to fetch summary.';
+ }
+ };
+ summaryText = () => this._summaryText;
+
selectionText = () => this._selectionText;
selectionContent = () => this._selectionContent;
@@ -413,6 +427,10 @@ export class PDFViewer extends React.Component<IViewerProps> {
document.removeEventListener('pointerup', this.onSelectEnd);
const sel = window.getSelection();
+ if (sel) {
+ AnchorMenu.Instance.setSelectedText(sel.toString());
+ }
+
if (sel?.type === 'Range') {
this.createTextAnnotation(sel, sel.getRangeAt(0));
AnchorMenu.Instance.jumpTo(e.clientX, e.clientY);
@@ -596,6 +614,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
finishMarquee={this.finishMarquee}
savedAnnotations={this.savedAnnotations}
selectionText={this.selectionText}
+ setSummaryText={this.setSummaryText}
+ summaryText={this.summaryText}
annotationLayer={this._annotationLayer.current}
mainCont={this._mainCont.current}
anchorMenuCrop={this._textSelecting ? undefined : this.crop}