aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/pdf/GPTPopup
diff options
context:
space:
mode:
authorSophie Zhang <sophie_zhang@brown.edu>2023-04-13 01:13:33 -0400
committerSophie Zhang <sophie_zhang@brown.edu>2023-04-13 01:13:33 -0400
commitdb582e135fceb6162d0c9cf00e2580fb1349fddb (patch)
treea072ab129241e5ed06fb09d582d5339be3edb889 /src/client/views/pdf/GPTPopup
parenta0ae93e3b14069c0de419fc5dcade84d460a0b30 (diff)
added text edits
Diffstat (limited to 'src/client/views/pdf/GPTPopup')
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.scss132
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.tsx186
2 files changed, 318 insertions, 0 deletions
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.scss b/src/client/views/pdf/GPTPopup/GPTPopup.scss
new file mode 100644
index 000000000..50fbe5211
--- /dev/null
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.scss
@@ -0,0 +1,132 @@
+$textgrey: #707070;
+$lighttextgrey: #a3a3a3;
+$greyborder: #d3d3d3;
+$lightgrey: #ececec;
+$button: #5b97ff;
+$highlightedText: #82e0ff;
+
+.summary-box {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ background-color: #ffffff;
+ box-shadow: 0 2px 5px #7474748d;
+ color: $textgrey;
+ position: fixed;
+ bottom: 5px;
+ right: 5px;
+ width: 250px;
+ min-height: 200px;
+ border-radius: 15px;
+ padding: 15px;
+ padding-bottom: 0;
+ z-index: 999;
+
+ .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;
+ }
+
+ .highlighted-text {
+ background-color: $highlightedText;
+ }
+}
+
+// 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/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
new file mode 100644
index 000000000..91bc0a7ff
--- /dev/null
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -0,0 +1,186 @@
+import React = require('react');
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import ReactLoading from 'react-loading';
+import Typist from 'react-typist';
+import { Doc } from '../../../../fields/Doc';
+import { Docs } from '../../../documents/Documents';
+import './GPTPopup.scss';
+
+export enum GPTPopupMode {
+ SUMMARY,
+ EDIT,
+}
+
+interface GPTPopupProps {
+ visible: boolean;
+ text: string;
+ loading: boolean;
+ mode: GPTPopupMode;
+ callSummaryApi: (e: React.PointerEvent) => Promise<void>;
+ callEditApi: (e: React.PointerEvent) => Promise<void>;
+ replaceText: (replacement: string) => void;
+ highlightRange?: number[];
+}
+
+@observer
+export class GPTPopup extends React.Component<GPTPopupProps> {
+ static Instance: GPTPopup;
+
+ @observable
+ private done: boolean = false;
+ @observable
+ private sidebarId: string = '';
+
+ @action
+ public setDone = (done: boolean) => {
+ this.done = 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.loading) {
+ this.setDone(false);
+ }
+ };
+
+ summaryBox = () => (
+ <>
+ <div>
+ {this.heading('SUMMARY')}
+ <div className="content-wrapper">
+ {!this.props.loading &&
+ (!this.done ? (
+ <Typist
+ key={this.props.text}
+ avgTypingDelay={15}
+ cursor={{ hideWhenDone: true }}
+ onTypingDone={() => {
+ setTimeout(() => {
+ this.setDone(true);
+ }, 500);
+ }}>
+ {this.props.text}
+ </Typist>
+ ) : (
+ this.props.text
+ ))}
+ </div>
+ </div>
+ {!this.props.loading && (
+ <div className="btns-wrapper">
+ {this.done ? (
+ <>
+ <button className="icon-btn" onPointerDown={e => this.props.callSummaryApi(e)}>
+ <FontAwesomeIcon icon="redo-alt" size="lg" />
+ </button>
+ <button
+ className="text-btn"
+ onClick={e => {
+ this.transferToText();
+ }}>
+ Transfer to Text
+ </button>
+ </>
+ ) : (
+ <div className="summarizing">
+ <span>Summarizing</span>
+ <ReactLoading type="bubbles" color="#bcbcbc" width={20} height={20} />
+ <button
+ className="btn-secondary"
+ onClick={e => {
+ this.setDone(true);
+ }}>
+ Stop Animation
+ </button>
+ </div>
+ )}
+ </div>
+ )}
+ </>
+ );
+
+ editBox = () => {
+ const hr = this.props.highlightRange;
+ return (
+ hr && (
+ <>
+ <div>
+ {this.heading('TEXT EDIT SUGGESTIONS')}
+ <div className="content-wrapper">
+ <div>
+ {this.props.text.slice(0, hr[0])} <span className="highlighted-text">{this.props.text.slice(hr[0], hr[1])}</span> {this.props.text.slice(hr[1])}
+ </div>
+ </div>
+ </div>
+ {!this.props.loading && (
+ <div className="btns-wrapper">
+ <>
+ <button className="icon-btn" onPointerDown={e => this.props.callEditApi(e)}>
+ <FontAwesomeIcon icon="redo-alt" size="lg" />
+ </button>
+ <button
+ className="text-btn"
+ onClick={e => {
+ this.props.replaceText(this.props.text);
+ }}>
+ Replace Text
+ </button>
+ </>
+ </div>
+ )}
+ {this.aiWarning()}
+ </>
+ )
+ );
+ };
+
+ aiWarning = () =>
+ this.done ? (
+ <div className="ai-warning">
+ <FontAwesomeIcon icon="exclamation-circle" size="sm" style={{ paddingRight: '5px' }} />
+ AI generated responses can contain inaccurate or misleading content.
+ </div>
+ ) : (
+ <></>
+ );
+
+ heading = (headingText: string) => (
+ <div className="summary-heading">
+ <label className="summary-text">{headingText}</label>
+ {this.props.loading && <ReactLoading type="spin" color="#bcbcbc" width={14} height={14} />}
+ </div>
+ );
+
+ render() {
+ return (
+ <div className="summary-box" style={{ display: this.props.visible ? 'flex' : 'none' }}>
+ {this.props.mode === GPTPopupMode.SUMMARY ? this.summaryBox() : this.editBox()}
+ </div>
+ );
+ }
+}