diff options
Diffstat (limited to 'src/client/views/pdf')
| -rw-r--r-- | src/client/views/pdf/AnchorMenu.tsx | 86 | ||||
| -rw-r--r-- | src/client/views/pdf/GPTPopup/GPTPopup.scss | 43 | ||||
| -rw-r--r-- | src/client/views/pdf/GPTPopup/GPTPopup.tsx | 146 | ||||
| -rw-r--r-- | src/client/views/pdf/PDFViewer.tsx | 5 |
4 files changed, 248 insertions, 32 deletions
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index a837969aa..2f6824466 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -7,13 +7,14 @@ import { ColorResult } from 'react-color'; import { ClientUtils, returnFalse, setupMoveUpEvents } from '../../../ClientUtils'; import { emptyFunction, unimplementedFunction } from '../../../Utils'; import { Doc, Opt } from '../../../fields/Doc'; -import { DocumentType } from '../../documents/DocumentTypes'; +import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT'; +import { Docs } from '../../documents/Documents'; import { SettingsManager } from '../../util/SettingsManager'; import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu'; import { LinkPopup } from '../linking/LinkPopup'; -import './AnchorMenu.scss'; -import { GPTPopup } from './GPTPopup/GPTPopup'; import { DocumentView } from '../nodes/DocumentView'; +import './AnchorMenu.scss'; +import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup'; @observer export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { @@ -36,10 +37,10 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { @observable public Status: 'marquee' | 'annotation' | '' = ''; // GPT additions - @observable private selectedText: string = ''; + @observable private _selectedText: string = ''; @action public setSelectedText = (txt: string) => { - this.selectedText = txt; + this._selectedText = txt.trim(); }; public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search @@ -59,6 +60,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { public get Active() { return this._left > 0; } + public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined; componentWillUnmount() { this._disposer?.(); @@ -76,8 +78,62 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { * @param e pointer down event */ gptSummarize = async () => { - GPTPopup.Instance?.setSelectedText(this.selectedText); - GPTPopup.Instance.generateSummary(); + GPTPopup.Instance.setVisible(true); + GPTPopup.Instance.setMode(GPTPopupMode.SUMMARY); + GPTPopup.Instance.setLoading(true); + + try { + const res = await gptAPICall(this._selectedText, GPTCallType.SUMMARY); + GPTPopup.Instance.setText(res || 'Something went wrong.'); + } catch (err) { + console.error(err); + } + GPTPopup.Instance.setLoading(false); + }; + // gptSummarize = async () => { + // GPTPopup.Instance?.setSelectedText(this._selectedText); + // GPTPopup.Instance.generateSummary(); + // }; + + /** + * Invokes the API with the selected text and stores it in the selected text. + * @param e pointer down event + */ + gptFlashcards = async () => { + const queryText = this._selectedText; + try { + const res = await gptAPICall(queryText, GPTCallType.FLASHCARD); + console.log(res); + GPTPopup.Instance.setText(res || 'Something went wrong.'); + this.transferToFlashcard(res || 'Something went wrong'); + } catch (err) { + console.error(err); + } + GPTPopup.Instance.setLoading(false); + }; + + /* + * Transfers the flashcard text generated by GPT on flashcards and creates a collection out them. + */ + transferToFlashcard = (text: string) => { + // put each question generated by GPT on the front of the flashcard + const senArr = text.split('Question'); + const collectionArr: Doc[] = []; + for (let i = 1; i < senArr.length; i++) { + console.log('Arr ' + i + ': ' + senArr[i]); + const newDoc = Docs.Create.ComparisonDocument(senArr[i], { _layout_isFlashcard: true, _width: 300, _height: 300 }); + newDoc.text = senArr[i]; + collectionArr.push(newDoc); + } + // create a new carousel collection of these flashcards + const newCol = Docs.Create.CarouselDocument(collectionArr, { + _width: 250, + _height: 200, + _layout_fitWidth: false, + _layout_autoHeight: true, + }); + + this.addToCollection?.(newCol); }; pointerDown = (e: React.PointerEvent) => { @@ -140,13 +196,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { this.highlightColor = ClientUtils.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 = () => DocumentView.SelectedDocs().some(doc => [DocumentType.PDF, DocumentType.WEB].includes(doc.type as any)); - render() { const buttons = this.Status === 'marquee' ? ( @@ -161,7 +210,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { /> </div> {/* GPT Summarize icon only shows up when text is highlighted, not on marquee selection */} - {AnchorMenu.Instance.StartCropDrag === unimplementedFunction && this.canSummarize() && ( + {this._selectedText && ( <IconButton tooltip="Summarize with AI" // onPointerDown={this.gptSummarize} @@ -169,6 +218,13 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { color={SettingsManager.userColor} /> )} + {/* Adds a create flashcards option to the anchor menu, which calls the gptFlashcard method. */} + <IconButton + tooltip="Create flashcards" // + onPointerDown={this.gptFlashcards} + icon={<FontAwesomeIcon icon="id-card" size="lg" />} + color={SettingsManager.userColor} + /> {AnchorMenu.Instance.OnAudio === unimplementedFunction ? null : ( <IconButton tooltip="Click to Record Annotation" // diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.scss b/src/client/views/pdf/GPTPopup/GPTPopup.scss index 5d966395c..6d8793f82 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.scss +++ b/src/client/views/pdf/GPTPopup/GPTPopup.scss @@ -11,8 +11,8 @@ $highlightedText: #82e0ff; right: 10px; width: 250px; min-height: 200px; - border-radius: 15px; - padding: 15px; + border-radius: 16px; + padding: 16px; padding-bottom: 0; z-index: 999; display: flex; @@ -55,16 +55,29 @@ $highlightedText: #82e0ff; overflow-y: auto; } - .btns-wrapper { + .btns-wrapper-gpt { height: 50px; display: flex; - justify-content: space-between; + justify-content: center; align-items: center; + transform: translateY(30px); + + + .searchBox-input{ + transform: translateY(-15px); + height: 50px; + border-radius: 10px; + border-color: #5b97ff; + } + + .summarizing { display: flex; align-items: center; } + + } button { @@ -111,6 +124,28 @@ $highlightedText: #82e0ff; } } +.loading-spinner { + display: flex; + justify-content: center; + align-items: center; + height: 100px; + font-size: 20px; + font-weight: bold; + color: #666; +} + + + + + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + + + .image-content-wrapper { display: flex; flex-direction: column; diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx index c1bfdf176..cb5aad32d 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx +++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx @@ -17,12 +17,16 @@ import { DocUtils } from '../../../documents/DocUtils'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { AnchorMenu } from '../AnchorMenu'; import './GPTPopup.scss'; +import { SettingsManager } from '../../../util/SettingsManager'; +import { SnappingManager } from '../../../util/SnappingManager'; export enum GPTPopupMode { SUMMARY, EDIT, IMAGE, + FLASHCARD, DATA, + SORT, } interface GPTPopupProps {} @@ -32,6 +36,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { // eslint-disable-next-line no-use-before-define static Instance: GPTPopup; @observable private chatMode: boolean = false; + private correlatedColumns: string[] = []; @observable public visible: boolean = false; @@ -99,6 +104,14 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { this.chatMode = false; }; + @observable + private sortDone: boolean = false; // this is so redundant but the og done variable was causing weird unknown problems and im just a girl + + @action + public setSortDone = (done: boolean) => { + this.sortDone = done; + }; + // change what can be a ref into a ref @observable private sidebarId: string = ''; @@ -121,10 +134,48 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { this.textAnchor = anchor; }; + @observable + public sortDesc: string = ''; + + @action public setSortDesc = (t: string) => { + this.sortDesc = t; + }; + + @observable onSortComplete?: (sortResult: string) => void; + @observable cardsDoneLoading = false; + + @action setCardsDoneLoading(done: boolean) { + console.log(done + 'HI HIHI'); + this.cardsDoneLoading = done; + } + public addDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean = () => false; + public createFilteredDoc: (axes?: any) => boolean = () => false; public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined; /** + * Sorts cards in the CollectionCardDeckView + */ + generateSort = async () => { + this.setLoading(true); + this.setSortDone(false); + + try { + const res = await gptAPICall(this.sortDesc, GPTCallType.SORT); + // Trigger the callback with the result + if (this.onSortComplete) { + this.onSortComplete(res || 'Something went wrong :('); + console.log(res); + } + } catch (err) { + console.error(err); + } + + this.setLoading(false); + this.setSortDone(true); + }; + + /** * Generates a Dalle image and uploads it to the server. */ generateImage = async () => { @@ -136,12 +187,9 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { try { const imageUrls = await gptImageCall(this.imgDesc); - console.log('Image urls: ', imageUrls); if (imageUrls && imageUrls[0]) { const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [imageUrls[0]] }); - console.log('Upload result: ', result); const source = ClientUtils.prepend(result.accessPaths.agnostic.client); - console.log('Upload source: ', source); this.setImgUrls([[imageUrls[0], source]]); } } catch (err) { @@ -151,6 +199,10 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { return undefined; }; + /** + * Completes an API call to generate a summary of + * this.selectedText in the popup. + */ generateSummary = async () => { GPTPopup.Instance.setVisible(true); GPTPopup.Instance.setMode(GPTPopupMode.SUMMARY); @@ -165,12 +217,21 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { GPTPopup.Instance.setLoading(false); }; + /** + * Completes an API call to generate an analysis of + * this.dataJson in the popup. + */ generateDataAnalysis = async () => { GPTPopup.Instance.setVisible(true); GPTPopup.Instance.setLoading(true); try { const res = await gptAPICall(this.dataJson, GPTCallType.DATA, this.dataChatPrompt); - GPTPopup.Instance.setText(res || 'Something went wrong.'); + const json = JSON.parse(res! as string); + const keys = Object.keys(json); + this.correlatedColumns = []; + this.correlatedColumns.push(json[keys[0]]); + this.correlatedColumns.push(json[keys[1]]); + GPTPopup.Instance.setText(json[keys[2]] || 'Something went wrong.'); } catch (err) { console.error(err); } @@ -188,6 +249,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { _layout_autoHeight: true, }); this.addDoc(newDoc, this.sidebarId); + // newDoc.data = 'Hello world'; const anchor = AnchorMenu.Instance?.GetAnchor(undefined, false); if (anchor) { DocUtils.MakeLink(newDoc, anchor, { @@ -197,6 +259,13 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { }; /** + * Creates a histogram to show the correlation relationship that was found + */ + private createVisualization = () => { + this.createFilteredDoc(this.correlatedColumns); + }; + + /** * Transfers the image urls to actual image docs */ private transferToImage = (source: string) => { @@ -245,6 +314,59 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { } }; + sortBox = () => ( + <> + <div> + {this.heading('SORTING')} + {this.loading ? ( + <div className="content-wrapper"> + <div className="loading-spinner"> + <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} /> + <span>Loading...</span> + </div> + </div> + ) : ( + <> + {!this.cardsDoneLoading ? ( + <div className="content-wrapper"> + <div className="loading-spinner"> + <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} /> + <span>Reading Cards...</span> + </div> + </div> + ) : ( + !this.sortDone && ( + <div className="btns-wrapper-gpt"> + <Button + tooltip="Have ChatGPT sort your cards for you!" + text="Sort!" + onClick={this.generateSort} + color={StrCast(Doc.UserDoc().userVariantColor)} + type={Type.TERT} + style={{ + width: '90%', // Almost as wide as the container + textAlign: 'center', + color: '#ffffff', // White text + fontSize: '16px', // Adjust font size as needed + }} + /> + </div> + ) + )} + + {this.sortDone && ( + <div> + <div className="content-wrapper"> + <p>{this.text === 'Something went wrong :(' ? 'Something went wrong :(' : 'Sorting done! Feel free to move things around / regenerate :) !'}</p> + <IconButton tooltip="Generate Again" onClick={() => this.setSortDone(false)} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} /> + </div> + </div> + )} + </> + )} + </div> + </> + ); imageBox = () => ( <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> {this.heading('GENERATED IMAGE')} @@ -291,8 +413,8 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { <div className="btns-wrapper"> {this.done ? ( <> - <IconButton tooltip="Generate Again" onClick={this.generateSummary} 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} /> + <IconButton tooltip="Generate Again" onClick={this.generateSummary /* this.callSummaryApi */} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(SettingsManager.userVariantColor)} /> + <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(SettingsManager.userVariantColor)} type={Type.TERT} /> </> ) : ( <div className="summarizing"> @@ -303,7 +425,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { onClick={() => { this.setDone(true); }} - color={StrCast(Doc.UserDoc().userVariantColor)} + color={StrCast(SettingsManager.userVariantColor)} type={Type.TERT} /> </div> @@ -356,8 +478,8 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { /> ) : ( <> - <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} /> - <Button tooltip="Chat with AI" text="Chat with AI" onClick={this.chatWithAI} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} /> + <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(SnappingManager.userVariantColor)} type={Type.TERT} /> + <Button tooltip="Chat with AI" text="Chat with AI" onClick={this.chatWithAI} color={StrCast(SnappingManager.userVariantColor)} type={Type.TERT} /> </> ) ) : ( @@ -369,7 +491,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { onClick={() => { this.setDone(true); }} - color={StrCast(Doc.UserDoc().userVariantColor)} + color={StrCast(SnappingManager.userVariantColor)} type={Type.TERT} /> </div> @@ -390,14 +512,14 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { heading = (headingText: string) => ( <div className="summary-heading"> <label className="summary-text">{headingText}</label> - {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)} />} + {this.loading ? <ReactLoading type="spin" color="#bcbcbc" width={14} height={14} /> : <IconButton color={StrCast(SettingsManager.userVariantColor)} tooltip="close" icon={<CgClose size="16px" />} onClick={() => this.setVisible(false)} />} </div> ); render() { return ( <div className="summary-box" style={{ display: this.visible ? 'flex' : 'none' }}> - {this.mode === GPTPopupMode.SUMMARY ? this.summaryBox() : this.mode === GPTPopupMode.DATA ? this.dataAnalysisBox() : this.mode === GPTPopupMode.IMAGE ? this.imageBox() : null} + {this.mode === GPTPopupMode.SUMMARY ? this.summaryBox() : this.mode === GPTPopupMode.DATA ? this.dataAnalysisBox() : this.mode === GPTPopupMode.IMAGE ? this.imageBox() : this.mode === GPTPopupMode.SORT ? this.sortBox() : null} </div> ); } diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 2327ee0d8..6c1617c38 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -24,11 +24,13 @@ import { FieldViewProps } from '../nodes/FieldView'; import { FocusViewOptions } from '../nodes/FocusViewOptions'; import { LinkInfo } from '../nodes/LinkDocPreview'; import { PDFBox } from '../nodes/PDFBox'; +import { ComparisonBox } from '../nodes/ComparisonBox'; import { ObservableReactComponent } from '../ObservableReactComponent'; import { StyleProp } from '../StyleProp'; import { AnchorMenu } from './AnchorMenu'; import { Annotation } from './Annotation'; import { GPTPopup } from './GPTPopup/GPTPopup'; +import { Docs } from '../../documents/Documents'; import './PDFViewer.scss'; // pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`; @@ -430,9 +432,10 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { AnchorMenu.Instance.jumpTo(e.clientX, e.clientY); } - // Changing which document to add the annotation to (the currently selected PDF) GPTPopup.Instance.setSidebarId('data_sidebar'); GPTPopup.Instance.addDoc = this._props.sidebarAddDoc; + // allows for creating collection + AnchorMenu.Instance.addToCollection = this._props.DocumentView?.()._props.addDocument; }; @action |
