diff options
Diffstat (limited to 'src/client/views/pdf')
-rw-r--r-- | src/client/views/pdf/AnchorMenu.tsx | 103 | ||||
-rw-r--r-- | src/client/views/pdf/GPTPopup/GPTPopup.scss | 66 | ||||
-rw-r--r-- | src/client/views/pdf/GPTPopup/GPTPopup.tsx | 261 |
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> ); } |