aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/apis/gpt/Summarization.ts48
-rw-r--r--src/client/views/MainView.tsx3
-rw-r--r--src/client/views/MarqueeAnnotator.tsx2
-rw-r--r--src/client/views/collections/CollectionView.tsx2
-rw-r--r--src/client/views/nodes/ImageBox.tsx2
-rw-r--r--src/client/views/nodes/PDFBox.tsx1
-rw-r--r--src/client/views/nodes/WebBox.tsx23
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.scss4
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx79
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx86
-rw-r--r--src/client/views/pdf/GPTPopup.scss123
-rw-r--r--src/client/views/pdf/GPTPopup.tsx131
-rw-r--r--src/client/views/pdf/PDFViewer.tsx12
13 files changed, 511 insertions, 5 deletions
diff --git a/src/client/apis/gpt/Summarization.ts b/src/client/apis/gpt/Summarization.ts
new file mode 100644
index 000000000..b65736237
--- /dev/null
+++ b/src/client/apis/gpt/Summarization.ts
@@ -0,0 +1,48 @@
+import { Configuration, OpenAIApi } from 'openai';
+
+enum GPTCallType {
+ SUMMARY = 'summary',
+ COMPLETION = 'completion',
+}
+
+type GPTCallOpts = {
+ model: string;
+ maxTokens: number;
+ temp: number;
+ prompt: string;
+};
+
+const callTypeMap: { [type: string]: GPTCallOpts } = {
+ summary: { model: 'text-davinci-003', maxTokens: 100, temp: 0.5, prompt: 'Summarize this text: ' },
+ completion: { model: 'text-davinci-003', maxTokens: 100, temp: 0.5, prompt: '' },
+};
+
+/**
+ * Calls the OpenAI API.
+ *
+ * @param inputText Text to process
+ * @returns AI Output
+ */
+const gptAPICall = async (inputText: string, callType: GPTCallType) => {
+ if (callType === GPTCallType.SUMMARY) inputText += '.';
+ const opts: GPTCallOpts = callTypeMap[callType];
+ try {
+ const configuration = new Configuration({
+ apiKey: process.env.OPENAI_KEY,
+ });
+ const openai = new OpenAIApi(configuration);
+ const response = await openai.createCompletion({
+ model: opts.model,
+ max_tokens: opts.maxTokens,
+ temperature: opts.temp,
+ prompt: `${opts.prompt}${inputText}`,
+ });
+ return response.data.choices[0].text;
+ } catch (err) {
+ console.log(err);
+ return 'Error connecting with API.';
+ }
+};
+
+
+export { gptAPICall, GPTCallType};
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index e1ba5943d..5f2bef5c8 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -244,6 +244,7 @@ export class MainView extends React.Component {
library.add(
...[
+ fa.faExclamationCircle,
fa.faEdit,
fa.faTrash,
fa.faTrashAlt,
@@ -274,6 +275,7 @@ export class MainView extends React.Component {
fa.faHandPointRight,
fa.faCompass,
fa.faSnowflake,
+ fa.faStar,
fa.faMicrophone,
fa.faKeyboard,
fa.faQuestion,
@@ -321,6 +323,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 30867a774..c02435518 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -199,7 +199,7 @@ 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);
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 316f1e4e9..065ce861a 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/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index c6d4cb694..60f799463 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -344,7 +344,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@computed get nativeSize() {
TraceMobx();
const nativeWidth = NumCast(this.dataDoc[this.fieldKey + '-nativeWidth'], NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth'], 500));
- const nativeHeight = NumCast(this.dataDoc[this.fieldKey + '-nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '-nativeHeight'], 1));
+ const nativeHeight = NumCast(this.dataDoc[this.fieldKey + '-nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '-nativeHeight'], 500));
const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + '-nativeOrientation'], 1);
return { nativeWidth, nativeHeight, nativeOrientation };
}
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 5f207cc0d..7e0b5c4d3 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -492,6 +492,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}}>
<PDFViewer
{...this.props}
+ sidebarAddDoc={this.sidebarAddDocument}
rootDoc={this.rootDoc}
addDocTab={this.sidebarAddDocTab}
layoutDoc={this.layoutDoc}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index b1acef44a..5f9420a54 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -14,7 +14,6 @@ import { TraceMobx } from '../../../fields/util';
import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentManager } from '../../util/DocumentManager';
-import { DragManager } from '../../util/DragManager';
import { ScriptingGlobals } from '../../util/ScriptingGlobals';
import { SnappingManager } from '../../util/SnappingManager';
import { undoBatch, UndoManager } from '../../util/UndoManager';
@@ -36,6 +35,9 @@ import { LinkDocPreview } from './LinkDocPreview';
import { PinProps, PresBox } from './trails';
import './WebBox.scss';
import React = require('react');
+import { DragManager } from '../../util/DragManager';
+import { gptAPICall, GPTCallType } from '../../apis/gpt/Summarization';
+import { GPTPopup } from '../pdf/GPTPopup';
const { CreateImage } = require('./WebBoxRenderer');
const _global = (window /* browser */ || global) /* node */ as any;
const htmlToText = require('html-to-text');
@@ -58,6 +60,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
private _searchRef = React.createRef<HTMLInputElement>();
private _searchString = '';
+ // GPT Additions
+ private _summaryText: string = '';
+ setSummaryText = async () => {
+ try {
+ const summary = await gptAPICall(this.selectionText(), GPTCallType.SUMMARY);
+ this._summaryText = `Summary: ${summary}`;
+ } catch (err) {
+ console.log(err);
+ this._summaryText = 'Failed to fetch summary.';
+ }
+ };
+ summaryText = () => this._summaryText;
+
private get _getAnchor() {
return AnchorMenu.Instance?.GetAnchor;
}
@@ -359,8 +374,13 @@ 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();
+ AnchorMenu.Instance.setSelectedText(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);
+ // Changing which document to add the annotation to (the currently selected WebBox)
+ GPTPopup.Instance.setSidebarId(`${this.props.fieldKey}-${this._urlHash}-sidebar`);
+ GPTPopup.Instance.addDoc = this.sidebarAddDocument;
}
}
};
@@ -778,6 +798,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => {
+ console.log(annotationKey);
(doc instanceof Doc ? [doc] : doc).forEach(doc => (doc.webUrl = this._url));
return this.addDocument(doc, annotationKey);
};
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index cbe0a465d..fd7fbb333 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -149,6 +149,10 @@ audiotag:hover {
}
}
+.gpt-typing-wrapper {
+ padding: 10px;
+}
+
// .menuicon {
// display: inline-block;
// border-right: 1px solid rgba(0, 0, 0, 0.2);
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index d857b150c..60be9775c 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -63,6 +63,9 @@ import { schema } from './schema_rts';
import { SummaryView } from './SummaryView';
import applyDevTools = require('prosemirror-dev-tools');
import React = require('react');
+import { Configuration, OpenAIApi } from 'openai';
+import { Networking } from '../../../Network';
+import { gptAPICall, GPTCallType } from '../../../apis/gpt/Summarization';
import { PinProps, PresBox } from '../trails';
const translateGoogleApi = require('translate-google-api');
@@ -170,6 +173,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
};
}
+ // State for GPT
+ @observable
+ private gptRes: string = '';
+
public static PasteOnLoad: ClipboardEvent | undefined;
public static SelectOnLoad = '';
public static DontSelectInitialText = false; // whether initial text should be selected or not
@@ -844,12 +851,84 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const options = cm.findByDescription('Options...');
const optionItems = options && 'subitems' in options ? options.subitems : [];
+ optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' });
+ optionItems.push({ description: `Ask GPT-3`, event: () => this.askGPT(), icon: 'lightbulb' });
optionItems.push({ description: !this.Document._singleLine ? 'Make Single Line' : 'Make Multi Line', event: () => (this.layoutDoc._singleLine = !this.layoutDoc._singleLine), icon: !this.Document._singleLine ? 'grip-lines' : 'bars' });
optionItems.push({ description: `${this.Document._autoHeight ? 'Lock' : 'Auto'} Height`, event: () => (this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight), icon: this.Document._autoHeight ? 'lock' : 'unlock' });
!options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
this._downX = this._downY = Number.NaN;
};
+ mockGPT = async (): Promise<string> => {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ resolve('Mock GPT Call.');
+ }, 2000);
+ });
+ };
+
+ animateRes = (resIndex: number) => {
+ if (resIndex < this.gptRes.length) {
+ this.dataDoc.text = (this.dataDoc.text as RichTextField)?.Text + this.gptRes[resIndex];
+ setTimeout(() => {
+ this.animateRes(resIndex + 1);
+ }, 20);
+ }
+ };
+
+ askGPT = action(async () => {
+ try {
+ let res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION);
+ // let res = await this.mockGPT();
+ if (res) {
+ this.gptRes = res;
+ this.animateRes(0);
+ }
+ } catch (err) {
+ console.log(err);
+ this.dataDoc.text = (this.dataDoc.text as RichTextField)?.Text + 'Something went wrong';
+ }
+ });
+
+ generateImage = async () => {
+ console.log('Generate image from text: ', (this.dataDoc.text as RichTextField)?.Text);
+ try {
+ const configuration = new Configuration({
+ apiKey: process.env.OPENAI_KEY,
+ });
+ const openai = new OpenAIApi(configuration);
+ const response = await openai.createImage({
+ prompt: (this.dataDoc.text as RichTextField)?.Text,
+ n: 1,
+ size: '1024x1024',
+ });
+ let image_url = response.data.data[0].url;
+ console.log(image_url);
+ if (image_url) {
+ const [{ accessPaths }] = await Networking.PostToServer('/uploadRemoteImage', { sources: [image_url] });
+ const source = Utils.prepend(accessPaths.agnostic.client);
+ const newDoc = Docs.Create.ImageDocument(source, {
+ x: NumCast(this.rootDoc.x) + NumCast(this.layoutDoc._width) + 10,
+ y: NumCast(this.rootDoc.y),
+ _height: 200,
+ _width: 200,
+ })
+ if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.rootDoc)) {
+ newDoc.overlayX = this.rootDoc.x;
+ newDoc.overlayY = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height);
+ Doc.AddDocToList(Doc.MyOverlayDocs, undefined, newDoc);
+ } else {
+ this.props.addDocument?.(newDoc);
+ }
+ // Create link between prompt and image
+ DocUtils.MakeLink({doc: this.rootDoc}, {doc: newDoc}, "Image Prompt");
+ }
+ } catch (err) {
+ console.log(err);
+ return '';
+ }
+ };
+
breakupDictation = () => {
if (this._editorView && this._recording) {
this.stopDictation(true);
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index fc8f1da49..1b30e1f68 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 { gptAPICall, GPTCallType } from '../../apis/gpt/Summarization';
+import { GPTPopup } from './GPTPopup';
import './AnchorMenu.scss';
import { LightboxView } from '../LightboxView';
@@ -43,6 +45,28 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@observable public Status: 'marquee' | 'annotation' | '' = '';
+ // GPT additions
+ @observable private summarizedText: string = '';
+ @observable private loadingSummary: boolean = false;
+ @observable private showGPTPopup: boolean = false;
+ @action
+ setGPTPopupVis = (vis: boolean) => {
+ this.showGPTPopup = vis;
+ };
+ @action
+ setSummarizedText = (txt: string) => {
+ this.summarizedText = txt;
+ };
+ @action
+ setLoading = (loading: boolean) => {
+ this.loadingSummary = loading;
+ };
+
+ private selectedText: string = '';
+ public setSelectedText = (txt: string) => {
+ this.selectedText = txt;
+ };
+
public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search
public OnCrop: (e: PointerEvent) => void = unimplementedFunction;
@@ -76,18 +100,56 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
componentDidMount() {
this._disposer2 = reaction(
() => this._opacity,
- opacity => !opacity && (this._showLinkPopup = false),
+ opacity => {
+ if (!opacity) {
+ this._showLinkPopup = false;
+ this.setGPTPopupVis(false);
+ this.setSummarizedText('');
+ }
+ },
{ fireImmediately: true }
);
this._disposer = reaction(
() => SelectionManager.Views(),
selected => {
this._showLinkPopup = false;
+ this.setGPTPopupVis(false);
+ this.setSummarizedText('');
AnchorMenu.Instance.fadeOut(true);
}
);
}
+ /**
+ * Returns a mock summary simulating an API call.
+ * @returns A Promise that resolves into a string
+ */
+ mockSummarize = async (): Promise<string> => {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ resolve('Mock summary. This is a summary of the highlighted text.');
+ }, 1000);
+ });
+ };
+
+ /**
+ * Invokes the API with the selected text and stores it in the summarized text.
+ * @param e pointer down event
+ */
+ gptSummarize = async (e: React.PointerEvent) => {
+ this.setGPTPopupVis(true);
+ this.setLoading(true);
+ const res = await gptAPICall(this.selectedText, GPTCallType.SUMMARY);
+ // const res = await this.mockSummarize();
+ if (res) {
+ this.setSummarizedText(res);
+ } else {
+ this.setSummarizedText('Something went wrong.');
+ }
+
+ this.setLoading(false);
+ };
+
pointerDown = (e: React.PointerEvent) => {
setupMoveUpEvents(
this,
@@ -180,6 +242,19 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
this.highlightColor = Utils.colorString(col);
};
+ /**
+ * Returns whether the selected text can be summarized. The goal is to have
+ * all selected text available to summarize but its only supported for pdf and web ATM.
+ * @returns Whether the GPT icon for summarization should appear
+ */
+ canSummarize = (): boolean => {
+ const docs = SelectionManager.Docs();
+ if (docs.length > 0) {
+ return docs[0].type === 'pdf' || docs[0].type === 'web';
+ }
+ return false;
+ };
+
render() {
const buttons =
this.Status === 'marquee' ? (
@@ -190,6 +265,15 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
<FontAwesomeIcon icon="comment-alt" size="lg" />
</button>
</Tooltip>
+ {/* GPT Summarize icon only shows up when text is highlighted, not on marquee selection*/}
+ {AnchorMenu.Instance.StartCropDrag === unimplementedFunction && this.canSummarize() && (
+ <Tooltip key="gpt" title={<div className="dash-tooltip">Summarize with AI</div>}>
+ <button className="antimodeMenu-button annotate" onPointerDown={this.gptSummarize} style={{ cursor: 'grab' }}>
+ <FontAwesomeIcon icon="comment-dots" size="lg" />
+ </button>
+ </Tooltip>
+ )}
+ <GPTPopup key="gptpopup" visible={this.showGPTPopup} text={this.summarizedText} loadingSummary={this.loadingSummary} callApi={this.gptSummarize} />
{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..7b7d2e3f7
--- /dev/null
+++ b/src/client/views/pdf/GPTPopup.scss
@@ -0,0 +1,123 @@
+$textgrey: #707070;
+$lighttextgrey: #a3a3a3;
+$greyborder: #d3d3d3;
+$lightgrey: #ececec;
+$button: #5b97ff;
+
+.summary-box {
+ display: flex;
+ flex-direction: column;
+ background-color: #ffffff;
+ box-shadow: 0 2px 5px #7474748d;
+ color: $textgrey;
+ position: absolute;
+ bottom: 40px;
+ width: 250px;
+ border-radius: 15px;
+ padding: 15px;
+ padding-bottom: 0px;
+
+ .summary-heading {
+ display: flex;
+ align-items: center;
+ border-bottom: 1px solid $greyborder;
+ padding-bottom: 5px;
+
+ .summary-text {
+ font-size: 12px;
+ font-weight: 500;
+ }
+ }
+
+ label {
+ color: $textgrey;
+ font-size: 12px;
+ font-weight: 400;
+ letter-spacing: 1px;
+ margin: 0;
+ padding-right: 5px;
+ }
+
+ a {
+ cursor: pointer;
+ }
+
+ .content-wrapper {
+ padding-top: 10px;
+ min-height: 50px;
+ max-height: 150px;
+ overflow-y: auto;
+ }
+
+ .btns-wrapper {
+ height: 50px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .summarizing {
+ display: flex;
+ align-items: center;
+ }
+ }
+
+ button {
+ font-size: 9px;
+ padding: 10px;
+ color: #ffffff;
+ background-color: $button;
+ border-radius: 5px;
+ }
+
+ .text-btn {
+ &:hover {
+ background-color: $button;
+ }
+ }
+
+ .btn-secondary {
+ font-size: 8px;
+ padding: 10px 5px;
+ background-color: $lightgrey;
+ color: $textgrey;
+ &:hover {
+ background-color: $lightgrey;
+ }
+ }
+
+ .icon-btn {
+ background-color: #ffffff;
+ padding: 10px;
+ border-radius: 50%;
+ color: $button;
+ border: 1px solid $button;
+ }
+
+ .ai-warning {
+ padding: 10px 0;
+ font-size: 10px;
+ color: $lighttextgrey;
+ border-top: 1px solid $greyborder;
+ }
+}
+
+// Typist CSS
+.Typist .Cursor {
+ display: inline-block;
+}
+.Typist .Cursor--blinking {
+ opacity: 1;
+ animation: blink 1s linear infinite;
+}
+
+@keyframes blink {
+ 0% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
diff --git a/src/client/views/pdf/GPTPopup.tsx b/src/client/views/pdf/GPTPopup.tsx
new file mode 100644
index 000000000..ec4fa58dc
--- /dev/null
+++ b/src/client/views/pdf/GPTPopup.tsx
@@ -0,0 +1,131 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import React = require('react');
+import ReactLoading from 'react-loading';
+import Typist from 'react-typist';
+import { Doc } from '../../../fields/Doc';
+import { Docs } from '../../documents/Documents';
+import './GPTPopup.scss';
+
+interface GPTPopupProps {
+ visible: boolean;
+ text: string;
+ loadingSummary: boolean;
+ callApi: (e: React.PointerEvent) => Promise<void>;
+}
+
+@observer
+export class GPTPopup extends React.Component<GPTPopupProps> {
+ static Instance: GPTPopup;
+
+ @observable
+ private summaryDone: boolean = false;
+ @observable
+ private sidebarId: string = '';
+ @action
+ public setSummaryDone = (done: boolean) => {
+ this.summaryDone = done;
+ };
+ @action
+ public setSidebarId = (id: string) => {
+ this.sidebarId = id;
+ };
+
+ public addDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean = () => false;
+
+ /**
+ * Transfers the summarization text to a sidebar annotation text document.
+ */
+ private transferToText = () => {
+ const newDoc = Docs.Create.TextDocument(this.props.text.trim(), {
+ _width: 200,
+ _height: 50,
+ _fitWidth: true,
+ _autoHeight: true,
+ });
+ this.addDoc(newDoc, this.sidebarId);
+ };
+
+ constructor(props: GPTPopupProps) {
+ super(props);
+ GPTPopup.Instance = this;
+ }
+
+ componentDidUpdate = () => {
+ if (this.props.loadingSummary) {
+ this.setSummaryDone(false);
+ }
+ };
+
+ render() {
+ return (
+ <div className="summary-box" style={{ display: this.props.visible ? 'flex' : 'none' }}>
+ <div className="summary-heading">
+ <label className="summary-text">SUMMARY</label>
+ {this.props.loadingSummary && <ReactLoading type="spin" color="#bcbcbc" width={14} height={14} />}
+ </div>
+ <div className="content-wrapper">
+ {!this.props.loadingSummary &&
+ (!this.summaryDone ? (
+ <Typist
+ key={this.props.text}
+ avgTypingDelay={15}
+ cursor={{ hideWhenDone: true }}
+ onTypingDone={action(() => {
+ setTimeout(
+ action(() => {
+ this.summaryDone = true;
+ }),
+ 500
+ );
+ })}>
+ {this.props.text}
+ </Typist>
+ ) : (
+ this.props.text
+ ))}
+ </div>
+ {!this.props.loadingSummary && (
+ <div className="btns-wrapper">
+ {this.summaryDone ? (
+ <>
+ <button className="icon-btn" onPointerDown={e => this.props.callApi(e)}>
+ <FontAwesomeIcon icon="redo-alt" size="lg" />
+ </button>
+ <button
+ className="text-btn"
+ onClick={e => {
+ this.transferToText();
+ }}>
+ Transfer to Text
+ </button>
+ </>
+ ) : (
+ <div className="summarizing">
+ <label>Summarizing</label>
+ <ReactLoading type="bubbles" color="#bcbcbc" width={20} height={20} />
+ <button
+ className="btn-secondary"
+ onClick={e => {
+ this.setSummaryDone(true);
+ }}>
+ Stop Animation
+ </button>
+ </div>
+ )}
+ </div>
+ )}
+ {this.summaryDone && (
+ <div className="ai-warning">
+ <FontAwesomeIcon icon="exclamation-circle" size="sm" style={{ paddingRight: '5px' }} />
+ AI generated responses can contain inaccurate or misleading content.{' '}
+ <a target="_blank" href="https://www.nytimes.com/2023/02/08/technology/ai-chatbots-disinformation.html">
+ Learn More
+ </a>
+ </div>
+ )}
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 86d14c22e..3f891789a 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 { GPTPopup } from './GPTPopup';
const PDFJSViewer = require('pdfjs-dist/web/pdf_viewer');
const pdfjsLib = require('pdfjs-dist');
const _global = (window /* browser */ || global) /* node */ as any;
@@ -40,9 +41,10 @@ interface IViewerProps extends FieldViewProps {
fieldKey: string;
pdf: Pdfjs.PDFDocumentProxy;
url: string;
+ sidebarAddDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean;
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;
}
@@ -415,10 +417,18 @@ 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);
}
+
+ // Changing which document to add the annotation to (the currently selected PDF)
+ GPTPopup.Instance.setSidebarId('data-sidebar');
+ GPTPopup.Instance.addDoc = this.props.sidebarAddDoc;
};
@action