diff options
author | Sophie Zhang <sophie_zhang@brown.edu> | 2023-02-27 16:56:02 -0500 |
---|---|---|
committer | Sophie Zhang <sophie_zhang@brown.edu> | 2023-02-27 16:56:02 -0500 |
commit | ed94cc8fb516455dbeea223d0fab25e4fb1c1d78 (patch) | |
tree | 662cee07a6a64fe1b688fc336c46f10574881b44 | |
parent | d2be7d2bfec6978238811cc50ad3effd2a19d15d (diff) |
feat: added summarization functionality to WebBox and cleaned up code
-rw-r--r-- | src/client/apis/gpt/Summarization.ts | 20 | ||||
-rw-r--r-- | src/client/views/MainView.tsx | 1 | ||||
-rw-r--r-- | src/client/views/nodes/PDFBox.tsx | 1 | ||||
-rw-r--r-- | src/client/views/nodes/WebBox.tsx | 6 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 1 | ||||
-rw-r--r-- | src/client/views/pdf/AnchorMenu.tsx | 39 | ||||
-rw-r--r-- | src/client/views/pdf/GPTPopup.scss | 91 | ||||
-rw-r--r-- | src/client/views/pdf/GPTPopup.tsx | 115 | ||||
-rw-r--r-- | src/client/views/pdf/PDFViewer.tsx | 19 |
9 files changed, 243 insertions, 50 deletions
diff --git a/src/client/apis/gpt/Summarization.ts b/src/client/apis/gpt/Summarization.ts index 931e0e48f..c0dcc0267 100644 --- a/src/client/apis/gpt/Summarization.ts +++ b/src/client/apis/gpt/Summarization.ts @@ -1,22 +1,32 @@ import { Configuration, OpenAIApi } from 'openai'; +/** + * Summarizes the inputted text with OpenAI. + * + * @param text Text to summarize + * @returns Summary of text + */ const gptSummarize = async (text: string) => { text += '.'; + const model = 'text-curie-001'; // Most advanced: text-davinci-003 + const maxTokens = 200; + const temp = 0.8; + try { const configuration = new Configuration({ apiKey: process.env.OPENAI_KEY, }); const openai = new OpenAIApi(configuration); const response = await openai.createCompletion({ - model: 'text-curie-001', - max_tokens: 256, - temperature: 0.7, - prompt: `Summarize this text in one sentence: ${text}`, + model: model, + max_tokens: maxTokens, + temperature: temp, + prompt: `Summarize this text: ${text}`, }); return response.data.choices[0].text; } catch (err) { console.log(err); - return ''; + return 'Error connecting with API.'; } }; 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 |