aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/pdf/AnchorMenu.tsx
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/AnchorMenu.tsx
parenta0ae93e3b14069c0de419fc5dcade84d460a0b30 (diff)
added text edits
Diffstat (limited to 'src/client/views/pdf/AnchorMenu.tsx')
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx147
1 files changed, 127 insertions, 20 deletions
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" />