diff options
Diffstat (limited to 'src/client/views/pdf')
-rw-r--r-- | src/client/views/pdf/AnchorMenu.tsx | 75 | ||||
-rw-r--r-- | src/client/views/pdf/GPTPopup/GPTPopup.scss | 64 | ||||
-rw-r--r-- | src/client/views/pdf/GPTPopup/GPTPopup.tsx | 193 | ||||
-rw-r--r-- | src/client/views/pdf/PDFViewer.tsx | 4 |
4 files changed, 229 insertions, 107 deletions
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index e35e011e2..7404650d6 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,23 +130,22 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { * @param e pointer down event */ gptSummarize = async (e: React.PointerEvent) => { + 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); + GPTPopup.Instance.setLoading(false); }; /** @@ -185,9 +160,9 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { 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); + GPTPopup.Instance.setVisible(true); this.setGPTMode(GPTPopupMode.EDIT); - this.setLoading(true); + GPTPopup.Instance.setLoading(true); try { let res = await gptAPICall(selectedText, GPTCallType.EDIT); @@ -197,16 +172,20 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { const resultText = fullText.slice(0, sel.from - 1) + res + fullText.slice(sel.to - 1); if (res) { - this.setGPTPopupText(resultText); + GPTPopup.Instance.setText(resultText); this.setHighlightRange([sel.from - 1, sel.from - 1 + res.length]); } else { - this.setGPTPopupText('Something went wrong.'); + GPTPopup.Instance.setText('Something went wrong.'); } } catch (err) { console.error(err); } - this.setLoading(false); + GPTPopup.Instance.setLoading(false); + }; + + gptImage = async () => { + console.log(this.GetAnchor(undefined, false)); }; /** @@ -325,17 +304,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 +312,9 @@ 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)} - /> - )} + {/* Removed text editing for now, not quite ready */} + {/* {this.canEdit() && <IconButton tooltip={'AI edit suggestions'} onPointerDown={this.gptEdit} icon={<FontAwesomeIcon icon="pencil-alt" />} color={StrCast(Doc.UserDoc().userColor)} />} */} + {<IconButton tooltip={'Generate DALL-E Image'} onPointerDown={this.gptImage} icon={<FontAwesomeIcon icon="image" />} 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..478b7d4ba 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,57 @@ $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 { + position: relative; + + img { + 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..fc6fc1af8 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx +++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx @@ -1,82 +1,192 @@ 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'; 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 imageUrls: string[] = []; + @action + public setImgUrls = (imgs: string[]) => { + this.imageUrls = 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; /** * 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); + if (this.targetAnchor) { + DocUtils.MakeLink(newDoc, this.targetAnchor, { + 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.imageUrls.map(rawSrc => ( + <div className="img-wrapper"> + <div className="img-container"> + <img key={rawSrc} src={this.getPreviewUrl(rawSrc)} width={150} height={150} alt="dalle generation" /> + </div> + <div className="btn-container"> + <Button text="Save Image" onClick={() => this.transferToImage(rawSrc)} color={StrCast(Doc.UserDoc().userColor)} type={Type.TERT} /> + </div> + </div> + ))} + </div> + </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 +194,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> @@ -125,7 +228,7 @@ export class GPTPopup extends React.Component<GPTPopupProps> { ); editBox = () => { - const hr = this.props.highlightRange; + const hr = this.highlightRange; return ( <> <div> @@ -133,22 +236,22 @@ export class GPTPopup extends React.Component<GPTPopupProps> { <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])} + {this.text.slice(0, hr[0])} <span className="highlighted-text">{this.text.slice(hr[0], hr[1])}</span> {this.text.slice(hr[1])} </div> )} </div> </div> - {hr && !this.props.loading && ( + {hr && !this.loading && ( <> <div className="btns-wrapper"> <> - <button className="icon-btn" onPointerDown={e => this.props.callEditApi(e)}> + <button className="icon-btn" onPointerDown={this.callEditApi}> <FontAwesomeIcon icon="redo-alt" size="lg" /> </button> <button className="text-btn" onClick={e => { - this.props.replaceText(this.props.text); + this.replaceText(this.text); }}> Replace Text </button> @@ -174,14 +277,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().userColor)} 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() : this.editBox()} </div> ); } diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 1319a236d..c9fee4813 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -424,6 +424,10 @@ export class PDFViewer extends React.Component<IViewerProps> { // Changing which document to add the annotation to (the currently selected PDF) GPTPopup.Instance.setSidebarId('data_sidebar'); + const anchor = this._getAnchor(undefined, false); + if (anchor) { + GPTPopup.Instance.setTargetAnchor(anchor); + } GPTPopup.Instance.addDoc = this.props.sidebarAddDoc; }; |