aboutsummaryrefslogtreecommitdiff
path: root/src/client/views
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views')
-rw-r--r--src/client/views/MainView.tsx1
-rw-r--r--src/client/views/nodes/PDFBox.tsx1
-rw-r--r--src/client/views/nodes/WebBox.tsx6
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx1
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx39
-rw-r--r--src/client/views/pdf/GPTPopup.scss91
-rw-r--r--src/client/views/pdf/GPTPopup.tsx115
-rw-r--r--src/client/views/pdf/PDFViewer.tsx19
8 files changed, 228 insertions, 45 deletions
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index da64e7c12..d7603cf5a 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -245,6 +245,7 @@ export class MainView extends React.Component {
library.add(
...[
+ fa.faExclamationCircle,
fa.faEdit,
fa.faTrash,
fa.faTrashAlt,
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index b88ac113e..40c04c08f 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -485,6 +485,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}}>
<PDFViewer
{...this.props}
+ sidebarAddDoc={this.sidebarAddDocument}
rootDoc={this.rootDoc}
layoutDoc={this.layoutDoc}
dataDoc={this.dataDoc}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 996028ec8..43b3b0536 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -37,6 +37,7 @@ import './WebBox.scss';
import React = require('react');
import { DragManager } from '../../util/DragManager';
import { gptSummarize } 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');
@@ -358,8 +359,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
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;
}
}
};
@@ -780,6 +785,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.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 683ccb9c4..b9327db0d 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -888,7 +888,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
-
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 6bcfbe4c2..8f9261614 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -45,7 +45,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@observable public Highlighting: boolean = false;
@observable public Status: 'marquee' | 'annotation' | '' = '';
- // GPT additions (flow 2)
+ // GPT additions
@observable private summarizedText: string = '';
@observable private loadingSummary: boolean = false;
@observable private showGPTPopup: boolean = false;
@@ -63,7 +63,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
};
private selectedText: string = '';
- setSelectedText = (txt: string) => {
+ public setSelectedText = (txt: string) => {
this.selectedText = txt;
};
@@ -71,9 +71,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
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;
@@ -123,14 +120,27 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
);
}
- getGPTSummary = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, returnFalse, returnFalse, e => this.OnSummary?.(e));
+ /**
+ * 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
+ */
invokeGPT = async (e: React.PointerEvent) => {
this.setGPTPopupVis(true);
this.setLoading(true);
const res = await gptSummarize(this.selectedText);
+ // const res = await this.mockSummarize();
if (res) {
this.setSummarizedText(res);
} else {
@@ -243,12 +253,15 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
<FontAwesomeIcon icon="comment-alt" size="lg" />
</button>
</Tooltip>
- <Tooltip key="gpt" title={<div className="dash-tooltip">Summarize with AI</div>}>
- <button className="antimodeMenu-button annotate" onPointerDown={this.invokeGPT} style={{ cursor: 'grab' }}>
- <FontAwesomeIcon icon="comment-dots" size="lg" />
- </button>
- </Tooltip>
- <GPTPopup key="gptpopup" visible={this.showGPTPopup} text={this.summarizedText} loadingSummary={this.loadingSummary} />
+ {/* GPT Summarize icon only shows up when text is highlighted, not on marquee selection*/}
+ {AnchorMenu.Instance.StartCropDrag === unimplementedFunction && (
+ <Tooltip key="gpt" title={<div className="dash-tooltip">Summarize with AI</div>}>
+ <button className="antimodeMenu-button annotate" onPointerDown={this.invokeGPT} 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.invokeGPT} />
{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
index 9605cfd07..7b7d2e3f7 100644
--- a/src/client/views/pdf/GPTPopup.scss
+++ b/src/client/views/pdf/GPTPopup.scss
@@ -1,33 +1,104 @@
$textgrey: #707070;
-$bordergrey: #d3d3d3;
+$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: 200px;
- height: 200px;
+ width: 250px;
border-radius: 15px;
- padding: 20px;
- overflow: auto;
+ padding: 15px;
+ padding-bottom: 0px;
.summary-heading {
display: flex;
align-items: center;
- border-bottom: 1px solid $bordergrey;
- margin-bottom: 10px;
+ border-bottom: 1px solid $greyborder;
padding-bottom: 5px;
.summary-text {
font-size: 12px;
font-weight: 500;
- letter-spacing: 1px;
- margin: 0;
- padding-right: 10px;
}
}
+
+ 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
diff --git a/src/client/views/pdf/GPTPopup.tsx b/src/client/views/pdf/GPTPopup.tsx
index 76f3c8187..ec4fa58dc 100644
--- a/src/client/views/pdf/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup.tsx
@@ -1,30 +1,129 @@
+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 ? 'block' : 'none' }}>
+ <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} /> : <></>}
+ {this.props.loadingSummary && <ReactLoading type="spin" color="#bcbcbc" width={14} height={14} />}
</div>
- {!this.props.loadingSummary ? (
- <Typist key={this.props.text} avgTypingDelay={20} cursor={{ hideWhenDone: true }}>
- {this.props.text}
- </Typist>
- ) : (
- <></>
+ <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 324f31f23..408ba07d1 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -25,6 +25,7 @@ import { Annotation } from './Annotation';
import './PDFViewer.scss';
import React = require('react');
import { gptSummarize } from '../../apis/gpt/Summarization';
+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;
@@ -41,6 +42,7 @@ 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, summarize?: boolean) => void);
@@ -83,19 +85,6 @@ 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;
@@ -435,6 +424,10 @@ export class PDFViewer extends React.Component<IViewerProps> {
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