diff options
| author | bobzel <zzzman@gmail.com> | 2024-05-19 01:01:02 -0400 |
|---|---|---|
| committer | bobzel <zzzman@gmail.com> | 2024-05-19 01:01:02 -0400 |
| commit | 6e72f969029c22fe797397a6437836a0482260b6 (patch) | |
| tree | e8ccde75702e557b2226c9069263e1bc3bd21a4b /src/client/views/pdf/GPTPopup | |
| parent | 5ff0bef5d3c4825aa7210a26c98aae3b24f4a835 (diff) | |
| parent | 13dc6de0e0099f699ad0d2bb54401e6a0aa25018 (diff) | |
Merge branch 'restoringEslint' into alyssa-starter
Diffstat (limited to 'src/client/views/pdf/GPTPopup')
| -rw-r--r-- | src/client/views/pdf/GPTPopup/GPTPopup.scss | 43 | ||||
| -rw-r--r-- | src/client/views/pdf/GPTPopup/GPTPopup.tsx | 341 |
2 files changed, 313 insertions, 71 deletions
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 0b741c85e..cb5aad32d 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx +++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx @@ -1,3 +1,4 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, IconButton, Type } from 'browndash-components'; import { action, makeObservable, observable } from 'mobx'; @@ -6,30 +7,36 @@ import * as React from 'react'; import { CgClose } from 'react-icons/cg'; import ReactLoading from 'react-loading'; import { TypeAnimation } from 'react-type-animation'; -import { Utils } from '../../../../Utils'; +import { ClientUtils } from '../../../../ClientUtils'; import { Doc } from '../../../../fields/Doc'; import { NumCast, StrCast } from '../../../../fields/Types'; import { Networking } from '../../../Network'; -import { gptImageCall } from '../../../apis/gpt/GPT'; -import { DocUtils, Docs } from '../../../documents/Documents'; +import { GPTCallType, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT'; +import { Docs } from '../../../documents/Documents'; +import { DocUtils } from '../../../documents/DocUtils'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { AnchorMenu } from '../AnchorMenu'; import './GPTPopup.scss'; -import { ComparisonBox } from '../../nodes/ComparisonBox'; -import { DashColor, emptyFunction, lightOrDark, returnFalse } from '../../../../Utils'; +import { SettingsManager } from '../../../util/SettingsManager'; +import { SnappingManager } from '../../../util/SnappingManager'; export enum GPTPopupMode { SUMMARY, EDIT, IMAGE, FLASHCARD, + DATA, + SORT, } interface GPTPopupProps {} @observer 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; @@ -49,6 +56,20 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { public setText = (text: string) => { this.text = text; }; + @observable + public selectedText: string = ''; + @action + public setSelectedText = (text: string) => { + this.selectedText = text; + }; + @observable + public dataJson: string = ''; + public dataChatPrompt: string | null = null; + @action + public setDataJson = (text: string) => { + if (text === '') this.dataChatPrompt = ''; + this.dataJson = text; + }; @observable public imgDesc: string = ''; @@ -74,14 +95,21 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { @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; + 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 @@ -106,31 +134,108 @@ 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 () => { - if (this.imgDesc === '') return; + if (this.imgDesc === '') return undefined; 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]]); + const imageUrls = await gptImageCall(this.imgDesc); + if (imageUrls && imageUrls[0]) { + const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [imageUrls[0]] }); + const source = ClientUtils.prepend(result.accessPaths.agnostic.client); + this.setImgUrls([[imageUrls[0], source]]); } } catch (err) { - console.log(err); - return ''; + console.error(err); } this.setLoading(false); + 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); + 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); + }; + + /** + * 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); + 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); + } + GPTPopup.Instance.setLoading(false); }; /** @@ -144,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, { @@ -153,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) => { @@ -177,6 +290,16 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { DocUtils.MakeLink(textAnchor, newDoc, { link_relationship: 'Image Prompt' }); }; + /** + * Creates a chatbox for analyzing data so that users can ask specific questions. + */ + private chatWithAI = () => { + this.chatMode = true; + }; + dataPromptChanged = action((e: React.ChangeEvent<HTMLInputElement>) => { + this.dataChatPrompt = e.target.value; + }); + private getPreviewUrl = (source: string) => source.split('.').join('_m.'); constructor(props: GPTPopupProps) { @@ -191,55 +314,77 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { } }; - 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> + 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.loading && ( + </div> + ) : ( <> - <IconButton tooltip="Generate Again" onClick={this.generateImage} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} /> - </> - )} - </div> - ); - }; - - data = () => { - 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" /> + {!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> - <div className="btn-container"> - <Button text="Save Image" onClick={() => this.transferToImage(rawSrc[1])} color={StrCast(Doc.UserDoc().userColor)} type={Type.TERT} /> + ) : ( + !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> - ))} - </div> - {!this.loading && ( - <> - <IconButton tooltip="Generate Again" onClick={this.generateImage} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} /> + )} </> )} </div> - ); - }; + </> + ); + imageBox = () => ( + <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 = () => ( <> @@ -258,7 +403,6 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { }, 500); }, ]} - //cursor={{ hideWhenDone: true }} /> ) : ( this.text @@ -269,9 +413,8 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { <div className="btns-wrapper"> {this.done ? ( <> - <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} /> - {/* <Button tooltip="Transfer to flashcard" text="flashcard" onClick={this.transferToFlashcard} 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"> @@ -282,7 +425,73 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { onClick={() => { this.setDone(true); }} - color={StrCast(Doc.UserDoc().userVariantColor)} + color={StrCast(SettingsManager.userVariantColor)} + type={Type.TERT} + /> + </div> + )} + </div> + )} + </> + ); + + dataAnalysisBox = () => ( + <> + <div> + {this.heading('ANALYSIS')} + <div className="content-wrapper"> + {!this.loading && + (!this.done ? ( + <TypeAnimation + speed={50} + sequence={[ + this.text, + () => { + setTimeout(() => { + this.setDone(true); + }, 500); + }, + ]} + /> + ) : ( + this.text + ))} + </div> + </div> + {!this.loading && ( + <div className="btns-wrapper"> + {this.done ? ( + this.chatMode ? ( + <input + defaultValue="" + autoComplete="off" + onChange={this.dataPromptChanged} + onKeyDown={e => { + e.key === 'Enter' ? this.generateDataAnalysis() : null; + e.stopPropagation(); + }} + type="text" + placeholder="Ask GPT a question about the data..." + id="search-input" + className="searchBox-input" + style={{ width: '100%' }} + /> + ) : ( + <> + <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} /> + </> + ) + ) : ( + <div className="summarizing"> + <span>Summarizing</span> + <ReactLoading type="bubbles" color="#bcbcbc" width={20} height={20} /> + <Button + text="Stop Animation" + onClick={() => { + this.setDone(true); + }} + color={StrCast(SnappingManager.userVariantColor)} type={Type.TERT} /> </div> @@ -298,21 +507,19 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { <FontAwesomeIcon icon="exclamation-circle" size="sm" style={{ paddingRight: '5px' }} /> AI generated responses can contain inaccurate or misleading content. </div> - ) : ( - <></> - ); + ) : null; 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.IMAGE ? this.imageBox() : this.mode == GPTPopupMode.FLASHCARD ? this.summaryBox() : <></>} + {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> ); } |
