aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/views/nodes/MapBox/MapAnchorMenu.scss54
-rw-r--r--src/client/views/nodes/MapBox/MapAnchorMenu.tsx393
2 files changed, 447 insertions, 0 deletions
diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.scss b/src/client/views/nodes/MapBox/MapAnchorMenu.scss
new file mode 100644
index 000000000..6990bdcf1
--- /dev/null
+++ b/src/client/views/nodes/MapBox/MapAnchorMenu.scss
@@ -0,0 +1,54 @@
+.anchorMenu-addTag {
+ display: grid;
+ width: 200px;
+ padding: 5px;
+ grid-template-columns: 90px 20px 90px;
+}
+.anchorMenu-highlighter {
+ padding-right: 5px;
+ .antimodeMenu-button {
+ padding: 0;
+ padding: 0;
+ padding-right: 0px;
+ padding-left: 0px;
+ width: 5px;
+ }
+}
+.anchor-color-preview-button {
+ width: 25px !important;
+ .anchor-color-preview {
+ display: flex;
+ flex-direction: column;
+ padding-right: 3px;
+ width: unset !important;
+ .color-preview {
+ width: 60%;
+ top: 80%;
+ height: 4px;
+ position: relative;
+ top: unset;
+ width: 15px;
+ margin-top: 5px;
+ display: block;
+ }
+ }
+}
+
+.color-wrapper {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+
+ button.color-button {
+ width: 20px;
+ height: 20px;
+ border-radius: 15px !important;
+ margin: 3px;
+ border: 2px solid transparent !important;
+ padding: 3px;
+
+ &.active {
+ border: 2px solid white;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx
new file mode 100644
index 000000000..798905bcd
--- /dev/null
+++ b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx
@@ -0,0 +1,393 @@
+import React = require('react');
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+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 { gptAPICall, GPTCallType } from '../../apis/gpt/GPT';
+import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup';
+import { EditorView } from 'prosemirror-view';
+import './MapAnchorMenu.scss';
+import { ColorPicker, Group, IconButton, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components';
+import { StrCast } from '../../../fields/Types';
+import { DocumentType } from '../../documents/DocumentTypes';
+
+@observer
+export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
+ static Instance: MapAnchorMenu;
+
+ private _disposer: IReactionDisposer | undefined;
+ private _disposer2: IReactionDisposer | undefined;
+ private _commentCont = React.createRef<HTMLButtonElement>();
+
+ @observable private highlightColor: string = 'rgba(245, 230, 95, 0.616)';
+
+ @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<Doc> = () => 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<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => Opt<Doc> = (color: string, isTargetToggler: boolean) => undefined;
+ public GetAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, 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);
+
+ MapAnchorMenu.Instance = this;
+ MapAnchorMenu.Instance._canFade = false;
+ }
+
+ componentWillUnmount() {
+ this._disposer?.();
+ this._disposer2?.();
+ }
+
+ componentDidMount() {
+ this._disposer2 = reaction(
+ () => this._opacity,
+ opacity => {
+ if (!opacity) {
+ this.setGPTPopupVis(false);
+ this.setGPTPopupText('');
+ }
+ },
+ { fireImmediately: true }
+ );
+ this._disposer = reaction(
+ () => SelectionManager.Views().slice(),
+ selected => {
+ this.setGPTPopupVis(false);
+ this.setGPTPopupText('');
+ MapAnchorMenu.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);
+ MapAnchorMenu.Instance.fadeOut(true);
+ };
+
+ @computed get highlighter() {
+ return (
+ <Group>
+ <IconButton
+ icon={<FontAwesomeIcon icon="highlighter" style={{ transition: 'transform 0.1s', transform: 'rotate(-45deg)' }} />}
+ tooltip={'Click to Highlight'}
+ onClick={this.highlightClicked}
+ colorPicker={this.highlightColor}
+ color={StrCast(Doc.UserDoc().userColor)}
+ />
+ <ColorPicker selectedColor={this.highlightColor} setFinalColor={this.changeHighlightColor} setSelectedColor={this.changeHighlightColor} size={Size.XSMALL} />
+ </Group>
+ );
+ }
+
+ @action changeHighlightColor = (color: string) => {
+ 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: '',
+ };
+ 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 === DocumentType.PDF || doc.type === DocumentType.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}
+ <IconButton
+ tooltip="Drag to Place Annotation" //
+ onPointerDown={this.pointerDown}
+ icon={<FontAwesomeIcon icon="comment-alt" />}
+ color={StrCast(Doc.UserDoc().userColor)}
+ />
+ {/* GPT Summarize icon only shows up when text is highlighted, not on marquee selection*/}
+ {MapAnchorMenu.Instance.StartCropDrag === unimplementedFunction && this.canSummarize() && (
+ <IconButton
+ tooltip="Summarize with AI" //
+ onPointerDown={this.gptSummarize}
+ icon={<FontAwesomeIcon icon="comment-dots" size="lg" />}
+ color={StrCast(Doc.UserDoc().userColor)}
+ />
+ )}
+ <GPTPopup
+ key="gptpopup"
+ visible={this.showGPTPopup}
+ text={this.GPTpopupText}
+ highlightRange={this.highlightRange}
+ loading={this.loadingGPT}
+ callSummaryApi={this.gptSummarize}
+ callEditApi={this.gptEdit}
+ replaceText={this.replaceText}
+ mode={this.GPTMode}
+ />
+ {MapAnchorMenu.Instance.OnAudio === unimplementedFunction ? null : (
+ <IconButton
+ tooltip="Click to Record Annotation" //
+ onPointerDown={this.audioDown}
+ icon={<FontAwesomeIcon icon="microphone" />}
+ color={StrCast(Doc.UserDoc().userColor)}
+ />
+ )}
+ {this.canEdit() && (
+ <IconButton
+ tooltip="AI edit suggestions" //
+ onPointerDown={this.gptEdit}
+ icon={<FontAwesomeIcon icon="pencil-alt" />}
+ color={StrCast(Doc.UserDoc().userColor)}
+ />
+ )}
+ <Popup
+ tooltip="Find document to link to selected text" //
+ type={Type.PRIM}
+ icon={<FontAwesomeIcon icon={'search'} />}
+ popup={<LinkPopup key="popup" linkCreateAnchor={this.onMakeAnchor} />}
+ color={StrCast(Doc.UserDoc().userColor)}
+ />
+ {MapAnchorMenu.Instance.StartCropDrag === unimplementedFunction ? null : (
+ <IconButton
+ tooltip="Click/Drag to create cropped image" //
+ onPointerDown={this.cropDown}
+ icon={<FontAwesomeIcon icon="image" />}
+ color={StrCast(Doc.UserDoc().userColor)}
+ />
+ )}
+ </>
+ ) : (
+ <>
+ {this.Delete !== returnFalse && (
+ <IconButton
+ tooltip="Remove Link Anchor" //
+ onPointerDown={this.Delete}
+ icon={<FontAwesomeIcon icon="trash-alt" />}
+ color={StrCast(Doc.UserDoc().userColor)}
+ />
+ )}
+ {this.PinToPres !== returnFalse && (
+ <IconButton
+ tooltip="Pin to Presentation" //
+ onPointerDown={this.PinToPres}
+ icon={<FontAwesomeIcon icon="map-pin" />}
+ color={StrCast(Doc.UserDoc().userColor)}
+ />
+ )}
+ {this.ShowTargetTrail !== returnFalse && (
+ <IconButton
+ tooltip="Show Linked Trail" //
+ onPointerDown={this.ShowTargetTrail}
+ icon={<FontAwesomeIcon icon="taxi" />}
+ color={StrCast(Doc.UserDoc().userColor)}
+ />
+ )}
+ {this.IsTargetToggler !== returnFalse && (
+ <Toggle
+ tooltip={'Make target visibility toggle on click'}
+ type={Type.PRIM}
+ toggleType={ToggleType.BUTTON}
+ toggleStatus={this.IsTargetToggler()}
+ onClick={this.MakeTargetToggle}
+ icon={<FontAwesomeIcon icon="thumbtack" />}
+ color={StrCast(Doc.UserDoc().userColor)}
+ />
+ )}
+ </>
+ );
+
+ return this.getElement(buttons);
+ }
+}