diff options
-rw-r--r-- | src/client/apis/gpt/Summarization.ts | 45 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.scss | 4 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 129 | ||||
-rw-r--r-- | src/client/views/pdf/AnchorMenu.tsx | 26 | ||||
-rw-r--r-- | src/client/views/pdf/PDFViewer.tsx | 1 |
5 files changed, 154 insertions, 51 deletions
diff --git a/src/client/apis/gpt/Summarization.ts b/src/client/apis/gpt/Summarization.ts index ba98ad591..b65736237 100644 --- a/src/client/apis/gpt/Summarization.ts +++ b/src/client/apis/gpt/Summarization.ts @@ -1,27 +1,41 @@ 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: '' }, +}; + /** - * Summarizes the inputted text with OpenAI. - * - * @param text Text to summarize - * @returns Summary of text + * Calls the OpenAI API. + * + * @param inputText Text to process + * @returns AI Output */ -const gptSummarize = async (text: string) => { - text += '.'; - const model = 'text-curie-001'; // Most advanced: text-davinci-003 - const maxTokens = 200; - const temp = 0.5; - +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: model, - max_tokens: maxTokens, - temperature: temp, - prompt: `Summarize this text: ${text}`, + model: opts.model, + max_tokens: opts.maxTokens, + temperature: opts.temp, + prompt: `${opts.prompt}${inputText}`, }); return response.data.choices[0].text; } catch (err) { @@ -30,4 +44,5 @@ const gptSummarize = async (text: string) => { } }; -export { gptSummarize }; + +export { gptAPICall, GPTCallType}; 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 b9327db0d..35c845deb 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -64,12 +64,21 @@ import { SummaryView } from './SummaryView'; import applyDevTools = require('prosemirror-dev-tools'); import React = require('react'); import { Configuration, OpenAIApi } from 'openai'; +import { gptAPICall, GPTCallType } from '../../../apis/gpt/Summarization'; +import ReactLoading from 'react-loading'; +import Typist from 'react-typist'; const translateGoogleApi = require('translate-google-api'); export const GoogleRef = 'googleDocId'; type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void; +enum GPTStatus { + LOADING, + TYPING, + NONE, +} + @observer export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { public static LayoutString(fieldStr: string) { @@ -172,6 +181,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; } + // State for GPT Typing animation + @observable + private gptStatus: GPTStatus = GPTStatus.NONE; + @observable + private gptPrompt: string = ''; + @observable + private gptRes: string = ''; + + @action + private setGPTStatus = (status: GPTStatus) => { + this.gptStatus = status; + }; + public static PasteOnLoad: ClipboardEvent | undefined; public static SelectOnLoad = ''; public static DontSelectInitialText = false; // whether initial text should be selected or not @@ -840,14 +862,48 @@ 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: 'expand-arrows-alt' }); optionItems.push({ description: `${this.Document._autoHeight ? 'Lock' : 'Auto'} Height`, event: () => (this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight), icon: 'plus' }); !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); + }); + }; + + askGPT = action(async () => { + try { + this.gptPrompt = (this.dataDoc.text as RichTextField)?.Text; + this.setGPTStatus(GPTStatus.LOADING); + let res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION); + // let res = await this.mockGPT(); + if (res) { + this.gptRes = res; + this.setGPTStatus(GPTStatus.TYPING); + } else { + this.setGPTStatus(GPTStatus.NONE); + } + } catch (err) { + console.log(err); + this.dataDoc.text = (this.dataDoc.text as RichTextField)?.Text + 'Something went wrong'; + this.setGPTStatus(GPTStatus.NONE); + } + }); + + setGPTText = action(() => { + this.dataDoc.text = (this.dataDoc.text as RichTextField)?.Text + this.gptRes; + this.gptRes = ''; + this.setGPTStatus(GPTStatus.NONE); + }); + generateImage = async () => { - console.log("Generate image from text: ", (this.dataDoc.text as RichTextField)?.Text); + console.log('Generate image from text: ', (this.dataDoc.text as RichTextField)?.Text); try { const configuration = new Configuration({ apiKey: process.env.OPENAI_KEY, @@ -856,17 +912,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const response = await openai.createImage({ prompt: (this.dataDoc.text as RichTextField)?.Text, n: 1, - size: "1024x1024", + size: '1024x1024', }); let image_url = response.data.data[0].url; console.log(image_url); if (image_url) { const newDoc = Docs.Create.ImageDocument(image_url, { - x: NumCast(this.rootDoc.x) + NumCast(this.layoutDoc._width) + 10, + x: NumCast(this.rootDoc.x) + NumCast(this.layoutDoc._width) + 10, y: NumCast(this.rootDoc.y), _height: 200, - _width: 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); @@ -878,15 +934,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps newDoc.data = image_url; newDoc._height = 200; newDoc._width = 200; - DocUtils.MakeLink({doc: this.rootDoc}, {doc: newDoc}, "Dall-E"); - }, 500) + DocUtils.MakeLink({ doc: this.rootDoc }, { doc: newDoc }, 'Dall-E'); + }, 500); } } catch (err) { console.log(err); return ''; } - - } + }; breakupDictation = () => { if (this._editorView && this._recording) { @@ -1942,29 +1997,45 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps onPointerUp={this.onPointerUp} onPointerDown={this.onPointerDown} onDoubleClick={this.onDoubleClick}> - <div - className={`formattedTextBox-outer${selected ? '-selected' : ''}`} - ref={this._scrollRef} - style={{ - width: this.props.dontSelectOnLoad ? '100%' : `calc(100% - ${this.sidebarWidthPercent})`, - pointerEvents: !active && !SnappingManager.GetIsDragging() ? 'none' : undefined, - overflow: this.layoutDoc._singleLine ? 'hidden' : this.layoutDoc._autoHeight ? 'visible' : undefined, - }} - onScroll={this.onScroll} - onDrop={this.ondrop}> + {this.gptStatus === GPTStatus.NONE || this.gptStatus === GPTStatus.LOADING ? ( <div - className={minimal ? 'formattedTextBox-minimal' : `formattedTextBox-inner${rounded}${selPaddingClass}`} - ref={this.createDropTarget} + className={`formattedTextBox-outer${selected ? '-selected' : ''}`} + ref={this._scrollRef} style={{ - padding: StrCast(this.layoutDoc._textBoxPadding), - paddingLeft: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`), - paddingRight: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`), - paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`), - paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`), - pointerEvents: !active && !SnappingManager.GetIsDragging() ? (this.layoutDoc.isLinkButton ? 'none' : undefined) : undefined, + width: this.props.dontSelectOnLoad ? '100%' : `calc(100% - ${this.sidebarWidthPercent})`, + pointerEvents: !active && !SnappingManager.GetIsDragging() ? 'none' : undefined, + overflow: this.layoutDoc._singleLine ? 'hidden' : this.layoutDoc._autoHeight ? 'visible' : undefined, }} - /> - </div> + onScroll={this.onScroll} + onDrop={this.ondrop}> + <div + className={minimal ? 'formattedTextBox-minimal' : `formattedTextBox-inner${rounded}${selPaddingClass}`} + ref={this.createDropTarget} + style={{ + padding: StrCast(this.layoutDoc._textBoxPadding), + paddingLeft: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`), + paddingRight: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`), + paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`), + paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`), + pointerEvents: !active && !SnappingManager.GetIsDragging() ? (this.layoutDoc.isLinkButton ? 'none' : undefined) : undefined, + }} + /> + </div> + ) : ( + <div className="gpt-typing-wrapper"> + <div>{this.gptPrompt}</div> + <br /> + <Typist + key={this.gptRes} + avgTypingDelay={15} + cursor={{ hideWhenDone: true }} + onTypingDone={() => { + this.setGPTText(); + }}> + <div>{this.gptRes}</div> + </Typist> + </div> + )} {this.noSidebar || this.props.dontSelectOnLoad || !this.SidebarShown || this.sidebarWidthPercent === '0%' ? null : this.sidebarCollection} {this.noSidebar || this.Document._noSidebar || this.props.dontSelectOnLoad || this.Document._singleLine ? null : this.sidebarHandle} {this.audioHandle} diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index 8f9261614..04904b3b1 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -10,7 +10,7 @@ import { SelectionManager } from '../../util/SelectionManager'; import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu'; import { LinkPopup } from '../linking/LinkPopup'; import { ButtonDropdown } from '../nodes/formattedText/RichTextMenu'; -import { gptSummarize } from '../../apis/gpt/Summarization'; +import { gptAPICall, GPTCallType } from '../../apis/gpt/Summarization'; import { GPTPopup } from './GPTPopup'; import './AnchorMenu.scss'; @@ -136,16 +136,17 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { * Invokes the API with the selected text and stores it in the summarized text. * @param e pointer down event */ - invokeGPT = async (e: React.PointerEvent) => { + gptSummarize = async (e: React.PointerEvent) => { this.setGPTPopupVis(true); this.setLoading(true); - const res = await gptSummarize(this.selectedText); + 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); }; @@ -243,6 +244,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' ? ( @@ -254,14 +268,14 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { </button> </Tooltip> {/* GPT Summarize icon only shows up when text is highlighted, not on marquee selection*/} - {AnchorMenu.Instance.StartCropDrag === unimplementedFunction && ( + {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.invokeGPT} style={{ cursor: 'grab' }}> + <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.invokeGPT} /> + <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/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 6e268c561..88854debe 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -24,7 +24,6 @@ import { AnchorMenu } from './AnchorMenu'; 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'); |