import React = require('react'); import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { action, computed, IReactionDisposer, observable, ObservableMap, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { ColorState } from 'react-color'; import { Doc, Opt } from '../../../fields/Doc'; import { returnFalse, setupMoveUpEvents, unimplementedFunction, Utils } from '../../../Utils'; 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/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 { static Instance: AnchorMenu; private _disposer: IReactionDisposer | undefined; private _disposer2: IReactionDisposer | undefined; private _commentCont = React.createRef(); private _palette = [ 'rgba(208, 2, 27, 0.8)', 'rgba(238, 0, 0, 0.8)', 'rgba(245, 166, 35, 0.8)', 'rgba(248, 231, 28, 0.8)', 'rgba(245, 230, 95, 0.616)', 'rgba(139, 87, 42, 0.8)', 'rgba(126, 211, 33, 0.8)', 'rgba(65, 117, 5, 0.8)', 'rgba(144, 19, 254, 0.8)', 'rgba(238, 169, 184, 0.8)', 'rgba(224, 187, 228, 0.8)', 'rgba(225, 223, 211, 0.8)', 'rgba(255, 255, 255, 0.8)', 'rgba(155, 155, 155, 0.8)', 'rgba(0, 0, 0, 0.8)', ]; @observable private highlightColor: string = 'rgba(245, 230, 95, 0.616)'; @observable private _showLinkPopup: boolean = false; @observable public Status: 'marquee' | 'annotation' | '' = ''; // GPT additions @observable private GPTpopupText: string = ''; @observable private loadingGPT: 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 setGPTMode = (mode: GPTPopupMode) => { this.GPTMode = mode; }; @action setGPTPopupText = (txt: string) => { this.GPTpopupText = txt; }; @action setLoading = (loading: boolean) => { this.loadingGPT = loading; }; @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 = () => undefined; // Method to get anchor from text search public OnCrop: (e: PointerEvent) => void = unimplementedFunction; public OnClick: (e: PointerEvent) => void = unimplementedFunction; public OnAudio: (e: PointerEvent) => void = unimplementedFunction; public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction; public StartCropDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction; public Highlight: (color: string, isTargetToggler: boolean, savedAnnotations?: ObservableMap, addAsAnnotation?: boolean) => Opt = (color: string, isTargetToggler: boolean) => undefined; public GetAnchor: (savedAnnotations: Opt>, addAsAnnotation: boolean) => Opt = (savedAnnotations: Opt>, addAsAnnotation: boolean) => undefined; public Delete: () => void = unimplementedFunction; public PinToPres: () => void = unimplementedFunction; public MakeTargetToggle: () => void = unimplementedFunction; public ShowTargetTrail: () => void = unimplementedFunction; public IsTargetToggler: () => boolean = returnFalse; public get Active() { return this._left > 0; } constructor(props: Readonly<{}>) { super(props); AnchorMenu.Instance = this; AnchorMenu.Instance._canFade = false; } componentWillUnmount() { this._disposer?.(); this._disposer2?.(); } componentDidMount() { this._disposer2 = reaction( () => this._opacity, opacity => { if (!opacity) { this._showLinkPopup = false; this.setGPTPopupVis(false); this.setGPTPopupText(''); } }, { fireImmediately: true } ); this._disposer = reaction( () => SelectionManager.Views().slice(), selected => { this._showLinkPopup = false; this.setGPTPopupVis(false); this.setGPTPopupText(''); AnchorMenu.Instance.fadeOut(true); } ); } /** * Invokes the API with the selected text and stores it in the summarized text. * @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); try { let res = await gptAPICall(selectedText, GPTCallType.EDIT); // let res = await this.mockGPTCall(); if (!res) return; res = res.trim(); const resultText = fullText.slice(0, sel.from - 1) + res + fullText.slice(sel.to - 1); 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, e, (e: PointerEvent) => { this.StartDrag(e, this._commentCont.current!); return true; }, returnFalse, e => this.OnClick?.(e) ); }; audioDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, returnFalse, returnFalse, e => this.OnAudio?.(e)); }; cropDown = (e: React.PointerEvent) => { setupMoveUpEvents( this, e, (e: PointerEvent) => { this.StartCropDrag(e, this._commentCont.current!); return true; }, returnFalse, e => this.OnCrop?.(e) ); }; @action highlightClicked = (e: React.MouseEvent) => { this.Highlight(this.highlightColor, false, undefined, true); AnchorMenu.Instance.fadeOut(true); }; @action toggleLinkPopup = (e: React.MouseEvent) => { //ignore the potential null type error because this method cannot be called unless the user selects text and clicks the link button //change popup visibility field to visible this._showLinkPopup = !this._showLinkPopup; }; @computed get highlighter() { const button = ( ); const dropdownContent = (

Change highlighter color:

{this._palette.map(color => { if (color) { return this.highlightColor === color ? ( ) : ( ); } })}
); return ( {'Click to Highlight'}}>
); } @action changeHighlightColor = (color: string, e: React.PointerEvent) => { const col: ColorState = { hex: color, hsl: { a: 0, h: 0, s: 0, l: 0, source: '' }, hsv: { a: 0, h: 0, s: 0, v: 0, source: '' }, rgb: { a: 0, r: 0, b: 0, g: 0, source: '' }, oldHue: 0, source: '', }; e.preventDefault(); e.stopPropagation(); 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.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; }; render() { const buttons = this.Status === 'marquee' ? ( <> {this.highlighter} Drag to Place Annotation}> {/* GPT Summarize icon only shows up when text is highlighted, not on marquee selection*/} {AnchorMenu.Instance.StartCropDrag === unimplementedFunction && this.canSummarize() && ( Summarize with AI}> )} {AnchorMenu.Instance.OnAudio === unimplementedFunction ? null : ( Click to Record Annotation}> )} {this.canEdit() && ( AI edit suggestions}> )} Find document to link to selected text}> , {AnchorMenu.Instance.StartCropDrag === unimplementedFunction ? null : ( Click/Drag to create cropped image}> )} ) : ( <> Remove Link Anchor}> Pin to Presentation}> Show Linked Trail}> make target visibility toggle on click}> ); return this.getElement(buttons); } }