aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSophie Zhang <sophie_zhang@brown.edu>2023-03-01 23:33:01 -0500
committerSophie Zhang <sophie_zhang@brown.edu>2023-03-01 23:33:01 -0500
commitf189ce6f25b91fcd402b7e81ba8ed378e39e6142 (patch)
tree181a0903c6adff0975216dc63e175be2656f9486
parent08e15b05cd014f99726826c9db407e738040cdbb (diff)
Added text completion
-rw-r--r--src/client/apis/gpt/Summarization.ts45
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.scss4
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx129
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx26
-rw-r--r--src/client/views/pdf/PDFViewer.tsx1
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');