aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/pdf
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/pdf')
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx103
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.scss66
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.tsx261
3 files changed, 252 insertions, 178 deletions
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 8e53a87f6..b0924888a 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -49,9 +49,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@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;
@@ -60,25 +57,11 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
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;
}
@@ -131,19 +114,12 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
componentDidMount() {
this._disposer2 = reaction(
() => this._opacity,
- opacity => {
- if (!opacity) {
- this.setGPTPopupVis(false);
- this.setGPTPopupText('');
- }
- },
+ opacity => {},
{ fireImmediately: true }
);
this._disposer = reaction(
() => SelectionManager.Views().slice(),
selected => {
- this.setGPTPopupVis(false);
- this.setGPTPopupText('');
AnchorMenu.Instance.fadeOut(true);
}
);
@@ -154,67 +130,23 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
* @param e pointer down event
*/
gptSummarize = async (e: React.PointerEvent) => {
+ // move this logic to gptpopup, need to implement generate again
+ GPTPopup.Instance.setVisible(true);
this.setHighlightRange(undefined);
- this.setGPTPopupVis(true);
- this.setGPTMode(GPTPopupMode.SUMMARY);
- this.setLoading(true);
+ GPTPopup.Instance.setMode(GPTPopupMode.SUMMARY);
+ GPTPopup.Instance.setLoading(true);
try {
const res = await gptAPICall(this.selectedText, GPTCallType.SUMMARY);
if (res) {
- this.setGPTPopupText(res);
+ GPTPopup.Instance.setText(res);
} else {
- this.setGPTPopupText('Something went wrong.');
+ GPTPopup.Instance.setText('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;
+ GPTPopup.Instance.setLoading(false);
};
pointerDown = (e: React.PointerEvent) => {
@@ -325,17 +257,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
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}
- />
{AnchorMenu.Instance.OnAudio === unimplementedFunction ? null : (
<IconButton
tooltip="Click to Record Annotation" //
@@ -344,14 +265,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
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}
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.scss b/src/client/views/pdf/GPTPopup/GPTPopup.scss
index 44413ede7..5d966395c 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.scss
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.scss
@@ -6,12 +6,6 @@ $button: #5b97ff;
$highlightedText: #82e0ff;
.summary-box {
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- background-color: #ffffff;
- box-shadow: 0 2px 5px #7474748d;
- color: $textgrey;
position: fixed;
bottom: 10px;
right: 10px;
@@ -21,9 +15,16 @@ $highlightedText: #82e0ff;
padding: 15px;
padding-bottom: 0;
z-index: 999;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ background-color: #ffffff;
+ box-shadow: 0 2px 5px #7474748d;
+ color: $textgrey;
.summary-heading {
display: flex;
+ justify-content: space-between;
align-items: center;
border-bottom: 1px solid $greyborder;
padding-bottom: 5px;
@@ -110,6 +111,59 @@ $highlightedText: #82e0ff;
}
}
+.image-content-wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 8px;
+ padding-bottom: 16px;
+
+ .img-wrapper {
+ position: relative;
+ cursor: pointer;
+
+ .img-container {
+ pointer-events: none;
+ position: relative;
+
+ img {
+ pointer-events: all;
+ position: relative;
+ }
+ }
+
+ .img-container::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ }
+
+ .btn-container {
+ position: absolute;
+ right: 8px;
+ bottom: 8px;
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ }
+
+ &:hover {
+ .img-container::after {
+ opacity: 1;
+ }
+
+ .btn-container {
+ opacity: 1;
+ }
+ }
+ }
+}
+
// Typist CSS
.Typist .Cursor {
display: inline-block;
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index 8bd060d4f..034470c6e 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -1,82 +1,233 @@
import React = require('react');
+import './GPTPopup.scss';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, observable } from 'mobx';
import { observer } from 'mobx-react';
import ReactLoading from 'react-loading';
import Typist from 'react-typist';
import { Doc } from '../../../../fields/Doc';
-import { Docs } from '../../../documents/Documents';
-import './GPTPopup.scss';
+import { DocUtils, Docs } from '../../../documents/Documents';
+import { Button, IconButton, Type } from 'browndash-components';
+import { NumCast, StrCast } from '../../../../fields/Types';
+import { CgClose } from 'react-icons/cg';
+import { AnchorMenu } from '../AnchorMenu';
+import { gptImageCall } from '../../../apis/gpt/GPT';
+import { Networking } from '../../../Network';
+import { Utils } from '../../../../Utils';
export enum GPTPopupMode {
SUMMARY,
EDIT,
+ IMAGE,
}
-interface GPTPopupProps {
- visible: boolean;
- text: string;
- loading: boolean;
- mode: GPTPopupMode;
- callSummaryApi: (e: React.PointerEvent) => Promise<void>;
- callEditApi: (e: React.PointerEvent) => Promise<void>;
- replaceText: (replacement: string) => void;
- highlightRange?: number[];
-}
+interface GPTPopupProps {}
@observer
export class GPTPopup extends React.Component<GPTPopupProps> {
static Instance: GPTPopup;
@observable
- private done: boolean = false;
+ public visible: boolean = false;
+ @action
+ public setVisible = (vis: boolean) => {
+ this.visible = vis;
+ };
@observable
- private sidebarId: string = '';
+ public loading: boolean = false;
+ @action
+ public setLoading = (loading: boolean) => {
+ this.loading = loading;
+ };
+ @observable
+ public text: string = '';
+ @action
+ public setText = (text: string) => {
+ this.text = text;
+ };
+
+ @observable
+ public imgDesc: string = '';
+ @action
+ public setImgDesc = (text: string) => {
+ this.imgDesc = text;
+ };
+ @observable
+ public imgUrls: string[][] = [];
+ @action
+ public setImgUrls = (imgs: string[][]) => {
+ this.imgUrls = imgs;
+ };
+
+ @observable
+ public mode: GPTPopupMode = GPTPopupMode.SUMMARY;
+ @action
+ public setMode = (mode: GPTPopupMode) => {
+ this.mode = mode;
+ };
+
+ @observable
+ public highlightRange: number[] = [];
+ @action callSummaryApi = () => {};
+ @action callEditApi = () => {};
+ @action replaceText = (replacement: string) => {};
+
+ @observable
+ private done: boolean = false;
@action
public setDone = (done: boolean) => {
this.done = done;
};
+
+ // change what can be a ref into a ref
+ @observable
+ private sidebarId: string = '';
@action
public setSidebarId = (id: string) => {
this.sidebarId = id;
};
+ // pdfs and webpages
+ @observable
+ private targetAnchor: Doc | undefined;
+ @action
+ public setTargetAnchor = (anchor: Doc) => {
+ this.targetAnchor = anchor;
+ };
+
+ @observable
+ private imgTargetDoc: Doc | undefined;
+ @action
+ public setImgTargetDoc = (anchor: Doc) => {
+ this.imgTargetDoc = anchor;
+ };
+
+ @observable
+ private textAnchor: Doc | undefined;
+ @action
+ public setTextAnchor = (anchor: Doc) => {
+ this.textAnchor = anchor;
+ };
+
public addDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean = () => false;
+ public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
+
+ /**
+ * Generates a Dalle image and uploads it to the server.
+ */
+ generateImage = async () => {
+ if (this.imgDesc === '') return;
+ this.setImgUrls([]);
+ this.setMode(GPTPopupMode.IMAGE);
+ this.setVisible(true);
+ this.setLoading(true);
+
+ try {
+ let image_urls = await gptImageCall(this.imgDesc);
+ if (image_urls && image_urls[0]) {
+ const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [image_urls[0]] });
+ const source = Utils.prepend(result.accessPaths.agnostic.client);
+ this.setImgUrls([[image_urls[0], source]]);
+ }
+ } catch (err) {
+ console.log(err);
+ return '';
+ }
+ GPTPopup.Instance.setLoading(false);
+ };
/**
* Transfers the summarization text to a sidebar annotation text document.
*/
private transferToText = () => {
- const newDoc = Docs.Create.TextDocument(this.props.text.trim(), {
+ const newDoc = Docs.Create.TextDocument(this.text.trim(), {
_width: 200,
_height: 50,
_layout_fitWidth: true,
_layout_autoHeight: true,
});
this.addDoc(newDoc, this.sidebarId);
+ const anchor = AnchorMenu.Instance?.GetAnchor(undefined, false);
+ if (anchor) {
+ DocUtils.MakeLink(newDoc, anchor, {
+ link_relationship: 'GPT Summary',
+ });
+ }
};
+ /**
+ * Transfers the image urls to actual image docs
+ */
+ private transferToImage = (source: string) => {
+ const textAnchor = this.imgTargetDoc;
+ if (!textAnchor) return;
+ const newDoc = Docs.Create.ImageDocument(source, {
+ x: NumCast(textAnchor.x) + NumCast(textAnchor._width) + 10,
+ y: NumCast(textAnchor.y),
+ _height: 200,
+ _width: 200,
+ data_nativeWidth: 1024,
+ data_nativeHeight: 1024,
+ });
+ if (Doc.IsInMyOverlay(textAnchor)) {
+ newDoc.overlayX = textAnchor.x;
+ newDoc.overlayY = NumCast(textAnchor.y) + NumCast(textAnchor._height);
+ Doc.AddToMyOverlay(newDoc);
+ } else {
+ this.addToCollection?.(newDoc);
+ }
+ // Create link between prompt and image
+ DocUtils.MakeLink(textAnchor, newDoc, { link_relationship: 'Image Prompt' });
+ };
+
+ private getPreviewUrl = (source: string) => source.split('.').join('_m.');
+
constructor(props: GPTPopupProps) {
super(props);
GPTPopup.Instance = this;
}
componentDidUpdate = () => {
- if (this.props.loading) {
+ if (this.loading) {
this.setDone(false);
}
};
+ imageBox = () => {
+ return (
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
+ {this.heading('GENERATED IMAGE')}
+ <div className="image-content-wrapper">
+ {this.imgUrls.map(rawSrc => (
+ <div className="img-wrapper">
+ <div className="img-container">
+ <img key={rawSrc[0]} src={rawSrc[0]} width={150} height={150} alt="dalle generation" />
+ </div>
+ <div className="btn-container">
+ <Button text="Save Image" onClick={() => this.transferToImage(rawSrc[1])} color={StrCast(Doc.UserDoc().userColor)} type={Type.TERT} />
+ </div>
+ </div>
+ ))}
+ </div>
+ {!this.loading && (
+ <>
+ <IconButton tooltip="Generate Again" onClick={this.generateImage} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} />
+ </>
+ )}
+ </div>
+ );
+ };
+
summaryBox = () => (
<>
<div>
{this.heading('SUMMARY')}
<div className="content-wrapper">
- {!this.props.loading &&
+ {!this.loading &&
(!this.done ? (
<Typist
- key={this.props.text}
+ key={this.text}
avgTypingDelay={15}
cursor={{ hideWhenDone: true }}
onTypingDone={() => {
@@ -84,39 +235,32 @@ export class GPTPopup extends React.Component<GPTPopupProps> {
this.setDone(true);
}, 500);
}}>
- {this.props.text}
+ {this.text}
</Typist>
) : (
- this.props.text
+ this.text
))}
</div>
</div>
- {!this.props.loading && (
+ {!this.loading && (
<div className="btns-wrapper">
{this.done ? (
<>
- <button className="icon-btn" onPointerDown={e => this.props.callSummaryApi(e)}>
- <FontAwesomeIcon icon="redo-alt" size="lg" />
- </button>
- <button
- className="text-btn"
- onClick={e => {
- this.transferToText();
- }}>
- Transfer to Text
- </button>
+ <IconButton tooltip="Generate Again" onClick={this.callSummaryApi} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} />
+ <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} />
</>
) : (
<div className="summarizing">
<span>Summarizing</span>
<ReactLoading type="bubbles" color="#bcbcbc" width={20} height={20} />
- <button
- className="btn-secondary"
- onClick={e => {
+ <Button
+ text="Stop Animation"
+ onClick={() => {
this.setDone(true);
- }}>
- Stop Animation
- </button>
+ }}
+ color={StrCast(Doc.UserDoc().userVariantColor)}
+ type={Type.TERT}
+ />
</div>
)}
</div>
@@ -124,43 +268,6 @@ export class GPTPopup extends React.Component<GPTPopupProps> {
</>
);
- editBox = () => {
- const hr = this.props.highlightRange;
- return (
- <>
- <div>
- {this.heading('TEXT EDIT SUGGESTIONS')}
- <div className="content-wrapper">
- {hr && (
- <div>
- {this.props.text.slice(0, hr[0])} <span className="highlighted-text">{this.props.text.slice(hr[0], hr[1])}</span> {this.props.text.slice(hr[1])}
- </div>
- )}
- </div>
- </div>
- {hr && !this.props.loading && (
- <>
- <div className="btns-wrapper">
- <>
- <button className="icon-btn" onPointerDown={e => this.props.callEditApi(e)}>
- <FontAwesomeIcon icon="redo-alt" size="lg" />
- </button>
- <button
- className="text-btn"
- onClick={e => {
- this.props.replaceText(this.props.text);
- }}>
- Replace Text
- </button>
- </>
- </div>
- {this.aiWarning()}
- </>
- )}
- </>
- );
- };
-
aiWarning = () =>
this.done ? (
<div className="ai-warning">
@@ -174,14 +281,14 @@ export class GPTPopup extends React.Component<GPTPopupProps> {
heading = (headingText: string) => (
<div className="summary-heading">
<label className="summary-text">{headingText}</label>
- {this.props.loading && <ReactLoading type="spin" color="#bcbcbc" width={14} height={14} />}
+ {this.loading ? <ReactLoading type="spin" color="#bcbcbc" width={14} height={14} /> : <IconButton color={StrCast(Doc.UserDoc().userVariantColor)} tooltip="close" icon={<CgClose size="16px" />} onClick={() => this.setVisible(false)} />}
</div>
);
render() {
return (
- <div className="summary-box" style={{ display: this.props.visible ? 'flex' : 'none' }}>
- {this.props.mode === GPTPopupMode.SUMMARY ? this.summaryBox() : this.editBox()}
+ <div className="summary-box" style={{ display: this.visible ? 'flex' : 'none' }}>
+ {this.mode === GPTPopupMode.SUMMARY ? this.summaryBox() : this.mode === GPTPopupMode.IMAGE ? this.imageBox() : <></>}
</div>
);
}