diff options
author | Sophie Zhang <sophie_zhang@brown.edu> | 2023-04-13 01:13:33 -0400 |
---|---|---|
committer | Sophie Zhang <sophie_zhang@brown.edu> | 2023-04-13 01:13:33 -0400 |
commit | db582e135fceb6162d0c9cf00e2580fb1349fddb (patch) | |
tree | a072ab129241e5ed06fb09d582d5339be3edb889 /src | |
parent | a0ae93e3b14069c0de419fc5dcade84d460a0b30 (diff) |
added text edits
Diffstat (limited to 'src')
-rw-r--r-- | src/client/apis/gpt/GPT.ts | 87 | ||||
-rw-r--r-- | src/client/apis/gpt/Summarization.ts | 48 | ||||
-rw-r--r-- | src/client/views/nodes/WebBox.tsx | 2 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 31 | ||||
-rw-r--r-- | src/client/views/pdf/AnchorMenu.tsx | 147 | ||||
-rw-r--r-- | src/client/views/pdf/GPTPopup.tsx | 131 | ||||
-rw-r--r-- | src/client/views/pdf/GPTPopup/GPTPopup.scss (renamed from src/client/views/pdf/GPTPopup.scss) | 15 | ||||
-rw-r--r-- | src/client/views/pdf/GPTPopup/GPTPopup.tsx | 186 | ||||
-rw-r--r-- | src/client/views/pdf/PDFViewer.tsx | 2 |
9 files changed, 427 insertions, 222 deletions
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts new file mode 100644 index 000000000..4b3960902 --- /dev/null +++ b/src/client/apis/gpt/GPT.ts @@ -0,0 +1,87 @@ +import { Configuration, OpenAIApi } from 'openai'; + +enum GPTCallType { + SUMMARY = 'summary', + COMPLETION = 'completion', + EDIT = 'edit', +} + +type GPTCallOpts = { + model: string; + maxTokens: number; + temp: number; + prompt: string; +}; + +const callTypeMap: { [type: string]: GPTCallOpts } = { + summary: { model: 'text-davinci-003', maxTokens: 256, temp: 0.5, prompt: 'Summarize this text briefly: ' }, + edit: { model: 'text-davinci-003', maxTokens: 256, temp: 0.5, prompt: 'Reword this: ' }, + completion: { model: 'text-davinci-003', maxTokens: 256, 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}`, + }); + console.log(response.data.choices[0]); + return response.data.choices[0].text; + } catch (err) { + console.log(err); + return 'Error connecting with API.'; + } +}; + +const gptImageCall = async (prompt: string) => { + try { + const configuration = new Configuration({ + apiKey: process.env.OPENAI_KEY, + }); + const openai = new OpenAIApi(configuration); + const response = await openai.createImage({ + prompt: prompt, + n: 1, + size: '1024x1024', + }); + return response.data.data[0].url; + } catch (err) { + console.error(err); + return; + } +}; + +// const gptEditCall = async (selectedText: string, fullText: 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.1, +// prompt: `Replace the phrase ${selectedText} inside of ${fullText}.`, +// }); +// return response.data.choices[0].text.trim(); +// } catch (err) { +// console.log(err); +// return 'Error connecting with API.'; +// } +// }; + +export { gptAPICall, gptImageCall, GPTCallType }; diff --git a/src/client/apis/gpt/Summarization.ts b/src/client/apis/gpt/Summarization.ts deleted file mode 100644 index 0d9b5dfcd..000000000 --- a/src/client/apis/gpt/Summarization.ts +++ /dev/null @@ -1,48 +0,0 @@ -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: 256, 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/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index e5ef85b5a..abde5a9ea 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -36,7 +36,7 @@ import { PinProps, PresBox } from './trails'; import './WebBox.scss'; import React = require('react'); import { DragManager } from '../../util/DragManager'; -import { GPTPopup } from '../pdf/GPTPopup'; +import { GPTPopup } from '../pdf/GPTPopup/GPTPopup'; const { CreateImage } = require('./WebBoxRenderer'); const _global = (window /* browser */ || global) /* node */ as any; const htmlToText = require('html-to-text'); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 4ad1f73b0..3eae47f49 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -23,7 +23,7 @@ import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } fro import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils'; import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils'; -import { gptAPICall, GPTCallType } from '../../../apis/gpt/Summarization'; +import { gptAPICall, GPTCallType, gptImageCall } from '../../../apis/gpt/GPT'; import { DocServer } from '../../../DocServer'; import { Docs, DocUtils } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; @@ -891,26 +891,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps 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); + let image_url = await gptImageCall((this.dataDoc.text as RichTextField)?.Text); 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, - }) + 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); @@ -919,7 +909,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this.props.addDocument?.(newDoc); } // Create link between prompt and image - DocUtils.MakeLink({doc: this.rootDoc}, {doc: newDoc}, "Image Prompt"); + DocUtils.MakeLink({ doc: this.rootDoc }, { doc: newDoc }, 'Image Prompt'); } } catch (err) { console.log(err); @@ -1189,6 +1179,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props); this.autoLink(); } + // Accessing editor and text doc for gpt assisted text edits + if (this._editorView && selected) { + AnchorMenu.Instance?.setEditorView(this._editorView); + AnchorMenu.Instance?.setTextDoc(this.dataDoc); + } }), { fireImmediately: true } ); diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index 1b30e1f68..b66f294f4 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -10,10 +10,11 @@ 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 { gptAPICall, GPTCallType } from '../../apis/gpt/GPT'; +import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup'; import { LightboxView } from '../LightboxView'; +import { EditorView } from 'prosemirror-view'; +import './AnchorMenu.scss'; @observer export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { @@ -46,27 +47,55 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { @observable public Status: 'marquee' | 'annotation' | '' = ''; // GPT additions - @observable private summarizedText: string = ''; + @observable private GPTpopupText: string = ''; @observable private loadingSummary: boolean = false; @observable private showGPTPopup: boolean = false; + @observable private GPTMode: GPTPopupMode = GPTPopupMode.SUMMARY; + @observable private selectedText: string = ''; + @observable private editorView?: EditorView; + @observable private textDoc?: Doc; + @observable private highlightRange: number[] | undefined; + private selectionRange: number[] | undefined; + @action setGPTPopupVis = (vis: boolean) => { this.showGPTPopup = vis; }; @action - setSummarizedText = (txt: string) => { - this.summarizedText = txt; + setGPTMode = (mode: GPTPopupMode) => { + this.GPTMode = mode; + }; + + @action + setGPTPopupText = (txt: string) => { + this.GPTpopupText = txt; }; + @action setLoading = (loading: boolean) => { this.loadingSummary = loading; }; - private selectedText: string = ''; + @action + setHighlightRange(r: number[] | undefined) { + this.highlightRange = r; + } + + @action public setSelectedText = (txt: string) => { this.selectedText = txt; }; + @action + public setEditorView = (editor: EditorView) => { + this.editorView = editor; + }; + + @action + public setTextDoc = (textDoc: Doc) => { + this.textDoc = textDoc; + }; + public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search public OnCrop: (e: PointerEvent) => void = unimplementedFunction; @@ -104,7 +133,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { if (!opacity) { this._showLinkPopup = false; this.setGPTPopupVis(false); - this.setSummarizedText(''); + this.setGPTPopupText(''); } }, { fireImmediately: true } @@ -114,20 +143,20 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { selected => { this._showLinkPopup = false; this.setGPTPopupVis(false); - this.setSummarizedText(''); + this.setGPTPopupText(''); AnchorMenu.Instance.fadeOut(true); } ); } /** - * Returns a mock summary simulating an API call. + * Returns a mock api response. * @returns A Promise that resolves into a string */ - mockSummarize = async (): Promise<string> => { + mockGPTCall = async (): Promise<string> => { return new Promise((resolve, reject) => { setTimeout(() => { - resolve('Mock summary. This is a summary of the highlighted text.'); + resolve('test'); }, 1000); }); }; @@ -137,19 +166,68 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { * @param e pointer down event */ gptSummarize = async (e: React.PointerEvent) => { + this.setHighlightRange(undefined); + this.setGPTPopupVis(true); + this.setGPTMode(GPTPopupMode.SUMMARY); + this.setLoading(true); + + try { + const res = await gptAPICall(this.selectedText, GPTCallType.SUMMARY); + if (res) { + this.setGPTPopupText(res); + } else { + this.setGPTPopupText('Something went wrong.'); + } + } catch (err) { + console.error(err); + } + + this.setLoading(false); + }; + + /** + * Makes a GPT call to edit selected text. + * @returns nothing + */ + gptEdit = async () => { + if (!this.editorView) return; + this.setHighlightRange(undefined); + const state = this.editorView.state; + const sel = state.selection; + const fullText = state.doc.textBetween(0, this.editorView.state.doc.content.size, ' \n'); + const selectedText = state.doc.textBetween(sel.from, sel.to); + this.setGPTPopupVis(true); + this.setGPTMode(GPTPopupMode.EDIT); 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.'); + + try { + let res = await gptAPICall(selectedText, GPTCallType.EDIT); + // let res = await this.mockGPTCall(); + res = res.trim(); + const resultText = fullText.slice(0, sel.from - 1) + res + fullText.slice(sel.to); + + if (res) { + this.setGPTPopupText(resultText); + this.setHighlightRange([sel.from - 1, sel.from - 1 + res.length]); + } else { + this.setGPTPopupText('Something went wrong.'); + } + } catch (err) { + console.error(err); } this.setLoading(false); }; + /** + * Replaces text suggestions from GPT. + */ + replaceText = (replacement: string) => { + if (!this.editorView || !this.textDoc) return; + this.textDoc.text = replacement; + }; + pointerDown = (e: React.PointerEvent) => { setupMoveUpEvents( this, @@ -250,7 +328,19 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { canSummarize = (): boolean => { const docs = SelectionManager.Docs(); if (docs.length > 0) { - return docs[0].type === 'pdf' || docs[0].type === 'web'; + return docs.some(doc => doc.type === 'pdf' || doc.type === 'web'); + } + return false; + }; + + /** + * Returns whether the selected text can be edited. + * @returns Whether the GPT icon for summarization should appear + */ + canEdit = (): boolean => { + const docs = SelectionManager.Docs(); + if (docs.length > 0) { + return docs.some(doc => doc.type === 'rtf'); } return false; }; @@ -273,7 +363,17 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { </button> </Tooltip> )} - <GPTPopup key="gptpopup" visible={this.showGPTPopup} text={this.summarizedText} loadingSummary={this.loadingSummary} callApi={this.gptSummarize} /> + <GPTPopup + key="gptpopup" + visible={this.showGPTPopup} + text={this.GPTpopupText} + highlightRange={this.highlightRange} + loading={this.loadingSummary} + callSummaryApi={this.gptSummarize} + callEditApi={this.gptEdit} + replaceText={this.replaceText} + mode={this.GPTMode} + /> {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' }}> @@ -281,6 +381,13 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { </button> </Tooltip> )} + {this.canEdit() && ( + <Tooltip key="gpttextedit" title={<div className="dash-tooltip">Edit text with AI</div>}> + <button className="antimodeMenu-button annotate" onPointerDown={this.gptEdit} style={{ cursor: 'grab' }}> + <FontAwesomeIcon icon="pencil-alt" size="lg" /> + </button> + </Tooltip> + )} <Tooltip key="link" title={<div className="dash-tooltip">Find document to link to selected text</div>}> <button className="antimodeMenu-button link" onPointerDown={this.toggleLinkPopup}> <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(1.5)' }} icon={'search'} size="lg" /> diff --git a/src/client/views/pdf/GPTPopup.tsx b/src/client/views/pdf/GPTPopup.tsx deleted file mode 100644 index ec4fa58dc..000000000 --- a/src/client/views/pdf/GPTPopup.tsx +++ /dev/null @@ -1,131 +0,0 @@ -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/GPTPopup.scss b/src/client/views/pdf/GPTPopup/GPTPopup.scss index 7b7d2e3f7..50fbe5211 100644 --- a/src/client/views/pdf/GPTPopup.scss +++ b/src/client/views/pdf/GPTPopup/GPTPopup.scss @@ -3,19 +3,24 @@ $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: absolute; - bottom: 40px; + position: fixed; + bottom: 5px; + right: 5px; width: 250px; + min-height: 200px; border-radius: 15px; padding: 15px; - padding-bottom: 0px; + padding-bottom: 0; + z-index: 999; .summary-heading { display: flex; @@ -99,6 +104,10 @@ $button: #5b97ff; color: $lighttextgrey; border-top: 1px solid $greyborder; } + + .highlighted-text { + background-color: $highlightedText; + } } // Typist CSS 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> + ); + } +} diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 3f891789a..d17d0e13c 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -24,7 +24,7 @@ import { AnchorMenu } from './AnchorMenu'; import { Annotation } from './Annotation'; import './PDFViewer.scss'; import React = require('react'); -import { GPTPopup } from './GPTPopup'; +import { GPTPopup } from './GPTPopup/GPTPopup'; const PDFJSViewer = require('pdfjs-dist/web/pdf_viewer'); const pdfjsLib = require('pdfjs-dist'); const _global = (window /* browser */ || global) /* node */ as any; |