diff options
| author | Nathan-SR <144961007+Nathan-SR@users.noreply.github.com> | 2025-03-11 17:43:05 +0100 |
|---|---|---|
| committer | Nathan-SR <144961007+Nathan-SR@users.noreply.github.com> | 2025-03-11 17:43:05 +0100 |
| commit | fa937182bc93aa2c6faadda80ea998cdfd479b4e (patch) | |
| tree | cba8e16edcccc6fd2932173484ac444cb79abea2 /src/client/views/pdf/GPTPopup | |
| parent | cf91c46cfec6e3e36b9184764016f9c1b5c997d4 (diff) | |
| parent | 04669ffeb163688c7aefd7b5face7998252abdca (diff) | |
Merge branch 'master' of https://github.com/brown-dash/Dash-Web into DocCreatorMenu-work
Diffstat (limited to 'src/client/views/pdf/GPTPopup')
| -rw-r--r-- | src/client/views/pdf/GPTPopup/GPTPopup.scss | 111 | ||||
| -rw-r--r-- | src/client/views/pdf/GPTPopup/GPTPopup.tsx | 989 |
2 files changed, 487 insertions, 613 deletions
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.scss b/src/client/views/pdf/GPTPopup/GPTPopup.scss index 0247dc10c..c8903e09f 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.scss +++ b/src/client/views/pdf/GPTPopup/GPTPopup.scss @@ -4,19 +4,23 @@ $greyborder: #d3d3d3; $lightgrey: #ececec; $button: #5b97ff; $highlightedText: #82e0ff; +$inputHeight: 60px; +$headingHeight: 32px; -.summary-box { +.gptPopup-summary-box { position: fixed; top: 115px; left: 75px; - width: 250px; - height: 200px; - min-height: 200px; - min-width: 180px; - + width: 100%; + height: 100%; + top: 0; + left: 0; + pointer-events: none; + border-top: solid gray 20px; border-radius: 16px; padding: 16px; padding-bottom: 0; + padding-top: 0px; z-index: 999; display: flex; flex-direction: column; @@ -24,25 +28,20 @@ $highlightedText: #82e0ff; background-color: #ffffff; box-shadow: 0 2px 5px #7474748d; color: $textgrey; - resize: both; /* Allows resizing */ - overflow: auto; - - .resize-handle { - width: 10px; - height: 10px; - background: #ccc; - position: absolute; - right: 0; - bottom: 0; - cursor: se-resize; - } + + .gptPopup-sortBox { + display: flex; + flex-direction: column; + height: calc(100% - $inputHeight - $headingHeight); + pointer-events: all; + } .summary-heading { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid $greyborder; - padding-bottom: 5px; + height: $headingHeight; .summary-text { font-size: 12px; @@ -63,95 +62,77 @@ $highlightedText: #82e0ff; cursor: pointer; } - .content-wrapper { + .gptPopup-content-wrapper { padding-top: 10px; min-height: 50px; - // max-height: 150px; - overflow-y: auto; - height: 100% + height: calc(100% - 32px); } - .btns-wrapper-gpt { - height: 100%; + .inputWrapper { display: flex; justify-content: center; align-items: center; - flex-direction: column; + height: $inputHeight; + background-color: white; + width: 100%; + pointer-events: all; - .inputWrapper{ - display: flex; - justify-content: center; - align-items: center; - height: 60px; - position: absolute; - bottom: 0; - width: 100%; - background-color: white; - - - } - - .searchBox-input{ + .searchBox-input { height: 40px; border-radius: 10px; - position: absolute; - bottom: 10px; + position: relative; border-color: #5b97ff; - width: 90% + width: 90%; } + } + .btns-wrapper-gpt { + height: 100%; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; .chat-wrapper { display: flex; flex-direction: column; width: 100%; - max-height: calc(100vh - 80px); + height: 100%; overflow-y: auto; - padding-bottom: 60px; + padding-right: 5px; } - + .chat-bubbles { margin-top: 20px; display: flex; flex-direction: column; flex-grow: 1; } - + .chat-bubble { padding: 10px; margin-bottom: 10px; border-radius: 10px; max-width: 60%; } - + .user-message { background-color: #283d53; align-self: flex-end; color: whitesmoke; } - + .chat-message { background-color: #367ae7; align-self: flex-start; - color:whitesmoke; + color: whitesmoke; } - - - .summarizing { display: flex; align-items: center; } - - - - - - } - - .text-btn { &:hover { background-color: $button; @@ -198,22 +179,16 @@ $highlightedText: #82e0ff; color: #666; } - - - - @keyframes spin { to { transform: rotate(360deg); } } - - .image-content-wrapper { display: flex; flex-direction: column; - align-items: flex-start; + align-items: center; gap: 8px; padding-bottom: 16px; diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx index d5f5f620c..4dc45e6a0 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx +++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx @@ -1,401 +1,334 @@ +import { Button, IconButton, Toggle, ToggleType, Type } from '@dash/components'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button, IconButton, Type } from 'browndash-components'; -import { action, makeObservable, observable } from 'mobx'; +import { action, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { CgClose, CgCornerUpLeft } from 'react-icons/cg'; +import { AiOutlineSend } from 'react-icons/ai'; +import { CgCornerUpLeft } from 'react-icons/cg'; import ReactLoading from 'react-loading'; import { TypeAnimation } from 'react-type-animation'; import { ClientUtils } from '../../../../ClientUtils'; import { Doc } from '../../../../fields/Doc'; import { NumCast, StrCast } from '../../../../fields/Types'; import { Networking } from '../../../Network'; -import { GPTCallType, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT'; +import { DescriptionSeperator, DocSeperator, GPTCallType, GPTDocCommand, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT'; import { DocUtils } from '../../../documents/DocUtils'; import { Docs } from '../../../documents/Documents'; import { SettingsManager } from '../../../util/SettingsManager'; import { SnappingManager } from '../../../util/SnappingManager'; +import { undoable } from '../../../util/UndoManager'; +import { DictationButton } from '../../DictationButton'; import { ObservableReactComponent } from '../../ObservableReactComponent'; -import { DocumentView } from '../../nodes/DocumentView'; +import { TagItem } from '../../TagsView'; +import { ChatSortField, docSortings } from '../../collections/CollectionSubView'; +import { DocumentView, DocumentViewInternal } from '../../nodes/DocumentView'; +import { SmartDrawHandler } from '../../smartdraw/SmartDrawHandler'; import { AnchorMenu } from '../AnchorMenu'; import './GPTPopup.scss'; +import { FireflyImageDimensions } from '../../smartdraw/FireflyConstants'; +import { Upload } from '../../../../server/SharedMediaTypes'; +import { OpenWhere } from '../../nodes/OpenWhere'; +import { DrawingFillHandler } from '../../smartdraw/DrawingFillHandler'; +import { ImageField } from '../../../../fields/URLField'; +import { List } from '../../../../fields/List'; export enum GPTPopupMode { - SUMMARY, - EDIT, - IMAGE, - FLASHCARD, + SUMMARY, // summary of seleted document text + IMAGE, // generate image from image description DATA, - CARD, - SORT, - QUIZ, + GPT_MENU, // menu for choosing type of prompts user will provide + USER_PROMPT, // user prompts for sorting,filtering and asking about docs + QUIZ_RESPONSE, // user definitions or explanations to be evaluated by GPT + FIREFLY, // firefly image generation } -export enum GPTQuizType { - CURRENT = 0, - CHOOSE = 1, - MULTIPLE = 2, -} - -interface GPTPopupProps {} - @observer -export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { +export class GPTPopup extends ObservableReactComponent<object> { // eslint-disable-next-line no-use-before-define static Instance: GPTPopup; - private messagesEndRef: React.RefObject<HTMLDivElement>; - - @observable private chatMode: boolean = false; - private correlatedColumns: string[] = []; + static ChatTag = '#chat'; // tag used by GPT popup to filter docs + private _askDictation: DictationButton | null = null; + private _messagesEndRef: React.RefObject<HTMLDivElement>; + private _correlatedColumns: string[] = []; + private _dataChatPrompt: string | undefined = undefined; + private _imgTargetDoc: Doc | undefined; + private _textAnchor: Doc | undefined; + private _dataJson: string = ''; + private _documentDescriptions: Promise<string> | undefined; // a cache of the descriptions of all docs in the selected collection. makes it more efficient when asking GPT multiple questions about the collection. + private _sidebarFieldKey: string = ''; + private _textToSummarize: string = ''; + private _imageDescription: string = ''; + private _textToDocMap = new Map<string, Doc>(); // when GPT answers with a doc's content, this helps us find the Doc + private _addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined; + + constructor(props: object) { + super(props); + makeObservable(this); + GPTPopup.Instance = this; + this._messagesEndRef = React.createRef(); + } - @observable - public visible: boolean = false; - @action - public setVisible = (vis: boolean) => { - this.visible = vis; - }; - @observable - 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 selectedText: string = ''; - @action - public setSelectedText = (text: string) => { - this.selectedText = text; - }; - @observable - public dataJson: string = ''; - public dataChatPrompt: string | undefined = undefined; - @action + public addDoc: ((doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean) | undefined; + public createFilteredDoc: (axes?: string[]) => boolean = () => false; + public setSidebarFieldKey = (id: string) => (this._sidebarFieldKey = id); + public setImgTargetDoc = (anchor: Doc) => (this._imgTargetDoc = anchor); + public setTextAnchor = (anchor: Doc) => (this._textAnchor = anchor); public setDataJson = (text: string) => { - if (text === '') this.dataChatPrompt = ''; - this.dataJson = 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; + if (text === '') this._dataChatPrompt = ''; + this._dataJson = text; }; - @observable - public mode: GPTPopupMode = GPTPopupMode.SUMMARY; - @action - public setMode = (mode: GPTPopupMode) => { - this.mode = mode; - }; - - @observable - public highlightRange: number[] = []; - @action callSummaryApi = () => {}; - - @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 - @observable - private sidebarId: string = ''; - @action - public setSidebarId = (id: string) => { - this.sidebarId = id; - }; - - @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; - }; - - @observable - public sortDesc: string = ''; - - @action public setSortDesc = (t: string) => { - this.sortDesc = t; - }; - - @observable onSortComplete?: (sortResult: string, questionType: string, tag?: string) => void; - @observable onQuizRandom?: () => void; - @observable cardsDoneLoading = false; - - @action setCardsDoneLoading(done: boolean) { - console.log(done + 'HI HIHI'); - this.cardsDoneLoading = done; + componentDidUpdate() { + this._gptProcessing && this.setStopAnimatingResponse(false); } - - @observable sortRespText: string = ''; - - @action setSortRespText(resp: string) { - this.sortRespText = resp; + componentDidMount(): void { + reaction( + () => ({ selDoc: DocumentView.Selected().lastElement(), visible: SnappingManager.ChatVisible }), + ({ selDoc, visible }) => { + const hasChildDocs = visible && selDoc?.ComponentView?.hasChildDocs; + if (hasChildDocs) { + this._textToDocMap.clear(); + this.setCollectionContext(selDoc.Document); + this.onGptResponse = (sortResult: string, questionType: GPTDocCommand, args?: string) => this.processGptResponse(selDoc, this._textToDocMap, sortResult, questionType, args); + this.onQuizRandom = () => this.randomlyChooseDoc(selDoc.Document, hasChildDocs()); + this._documentDescriptions = Promise.all(hasChildDocs().map(doc => + Doc.getDescription(doc).then(text => this._textToDocMap.set(text.trim(), doc) && `${DescriptionSeperator}${text}${DescriptionSeperator}`) + )).then(docDescriptions => docDescriptions.join()); // prettier-ignore + } + }, + { fireImmediately: true } + ); } - @observable chatSortPrompt: string = ''; - - sortPromptChanged = action((e: React.ChangeEvent<HTMLInputElement>) => { - this.chatSortPrompt = e.target.value; - }); - - @observable quizAnswer: string = ''; - - quizAnswerChanged = action((e: React.ChangeEvent<HTMLInputElement>) => { - this.quizAnswer = e.target.value; - }); - - @observable conversationArray: string[] = ['Hi! In this pop up, you can ask ChatGPT questions about your documents and filter / sort them. ']; + @observable private _conversationArray: string[] = ['Hi! In this pop up, you can ask ChatGPT questions about your documents and filter / sort them. ']; + @observable private _fireflyArray: string[] = ['Hi! In this pop up, you can ask Firefly to create images. ']; + @observable private _chatEnabled: boolean = false; + @action private setChatEnabled = (start: boolean) => (this._chatEnabled = start); + @observable private _gptProcessing: boolean = false; + @action private setGptProcessing = (loading: boolean) => (this._gptProcessing = loading); + @observable private _responseText: string = ''; + @action private setResponseText = (text: string) => (this._responseText = text); + @observable private _imgUrls: string[][] = []; + @action private setImgUrls = (imgs: string[][]) => (this._imgUrls = imgs); + @observable private _collectionContext: Doc | undefined = undefined; + @action setCollectionContext = (doc: Doc | undefined) => (this._collectionContext = doc); + @observable private _userPrompt: string = ''; + @action setUserPrompt = (e: string) => (this._userPrompt = e); + @observable private _quizAnswer: string = ''; + @action setQuizAnswer = (e: string) => (this._quizAnswer = e); + @observable private _stopAnimatingResponse: boolean = false; + @action private setStopAnimatingResponse = (done: boolean) => (this._stopAnimatingResponse = done); + + @observable private _mode: GPTPopupMode = GPTPopupMode.SUMMARY; + @action public setMode = (mode: GPTPopupMode) => (this._mode = mode); + + onQuizRandom?: () => void; + onGptResponse?: (sortResult: string, questionType: GPTDocCommand, args?: string) => void; + NumberToCommandType = (questionType: string) => +questionType.split(' ')[0][0]; /** - * When the cards are in quiz mode in the card view, allows gpt to determine whether the user's answer was correct - * @returns + * Processes gpt's output depending on the type of question the user asked. Converts gpt's string output to + * usable code + * @param gptOutput + * @param questionType + * @param tag */ - generateQuiz = async () => { - this.setLoading(true); - this.setSortDone(false); - - const quizType = this.quizMode; - - const selected = DocumentView.SelectedDocs().lastElement(); - - const questionText = 'Question: ' + StrCast(selected['gptInputText']); - - if (StrCast(selected['gptRubric']) === '') { - const rubricText = 'Rubric: ' + (await this.generateRubric(StrCast(selected['gptInputText']), selected)); - } - - const rubricText = 'Rubric: ' + StrCast(selected['gptRubric']); - const queryText = questionText + ' UserAnswer: ' + this.quizAnswer + '. ' + 'Rubric' + rubricText; - - try { - const res = await gptAPICall(queryText, GPTCallType.QUIZ); - if (!res) { - console.error('GPT call failed'); - return; - } - console.log(res); - this.setQuizResp(res); - this.conversationArray.push(res); - - this.setLoading(false); - this.setSortDone(true); - } catch (err) { - console.error('GPT call failed'); - } - - if (this.onQuizRandom) { - this.onQuizRandom(); - } - }; + processGptResponse = (docView: DocumentView, textToDocMap: Map<string, Doc>, gptOutput: string, questionType: GPTDocCommand, args?: string) => + undoable(() => { + switch (questionType) { // reset collection based on question typefc + case GPTDocCommand.Sort: + docView.Document[docView.ComponentView?.fieldKey + '_sort'] = docSortings.Chat; + break; + case GPTDocCommand.Filter: + docView.ComponentView?.hasChildDocs?.().forEach(d => TagItem.removeTagFromDoc(d, GPTPopup.ChatTag)); + break; + } // prettier-ignore + + gptOutput.split('======').filter(item => item.trim() !== '') // Split output into individual document contents + .map(docContentRaw => textToDocMap.get(docContentRaw.replace(/\n/g, ' ').trim())) // the find the corresponding Doc using textToDoc map + .filter(doc => doc).map(doc => doc!) // filter out undefined values + .forEach((doc, index) => { + switch (questionType) { + case GPTDocCommand.Sort: + doc[ChatSortField] = index; + break; + case GPTDocCommand.AssignTags: + if (args) { + const hashTag = args.startsWith('#') ? args : '#' + args[0].toLowerCase() + args.slice(1); + const filterTag = Doc.MyFilterHotKeys.map(key => StrCast(key.toolType)).find(key => key.includes(args)) ?? hashTag; + TagItem.addTagToDoc(doc, filterTag); + } + break; + case GPTDocCommand.Filter: + TagItem.addTagToDoc(doc, GPTPopup.ChatTag); + Doc.setDocFilter(docView.Document, 'tags', GPTPopup.ChatTag, 'check'); + break; + } + }); // prettier-ignore + }, '')(); /** - * Generates a rubric by which to compare the user's answer to - * @param inputText user's answer - * @param doc the doc the user is providing info about - * @returns gpt's response + * When in quiz mode, randomly selects a document */ - generateRubric = async (inputText: string, doc: Doc) => { - try { - const res = await gptAPICall(inputText, GPTCallType.RUBRIC); - doc['gptRubric'] = res; - return res; - } catch (err) { - console.error('GPT call failed'); - } - }; - - @observable private regenerateCallback: (() => Promise<void>) | null = null; - + randomlyChooseDoc = (doc: Doc, childDocs: Doc[]) => DocumentView.getDocumentView(childDocs[Math.floor(Math.random() * childDocs.length)])?.select(false); /** - * Callback function that causes the card view to update the childpair string list - * @param callback + * Generates a rubric for evaluating the user's description of the document's text + * @param doc the doc the user is providing info about + * @returns gpt's response rubric */ - @action public setRegenerateCallback(callback: () => Promise<void>) { - this.regenerateCallback = callback; - } - - public addDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean = () => false; - public createFilteredDoc: (axes?: string[]) => boolean = () => false; - public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined; - - @observable quizRespText: string = ''; - - @action setQuizResp(resp: string) { - this.quizRespText = resp; - } + generateRubric = (doc: Doc) => + StrCast(doc.gptRubric) + ? Promise.resolve(StrCast(doc.gptRubric)) + : Doc.getDescription(doc).then(desc => + gptAPICall(desc, GPTCallType.MAKERUBRIC) + .then(res => (doc.gptRubric = res)) + .catch(err => console.error('GPT call failed', err)) + ); /** - * Generates a response to the user's question depending on the type of their question + * When the cards are in quiz mode in the card view, allows gpt to determine whether the user's answer was correct + * @param doc the doc the user is providing info about + * @param quizAnswer the user's answer/description for the document + * @returns */ - generateCard = async () => { - console.log(this.chatSortPrompt + 'USER PROMPT'); - this.setLoading(true); - this.setSortDone(false); - - if (this.regenerateCallback) { - await this.regenerateCallback(); - } - - try { - // const res = await gptAPICall(this.sortDesc, GPTCallType.SORT, this.chatSortPrompt); - const questionType = await gptAPICall(this.chatSortPrompt, GPTCallType.TYPE); - const questionNumber = questionType.split(' ')[0]; - console.log(questionType); - let res = ''; - - switch (questionNumber) { - case '1': - case '2': - case '4': - res = await gptAPICall(this.sortDesc, GPTCallType.SUBSET, this.chatSortPrompt); - break; - case '6': - res = await gptAPICall(this.sortDesc, GPTCallType.SORT, this.chatSortPrompt); - break; - default: - const selected = DocumentView.SelectedDocs().lastElement(); - const questionText = StrCast(selected!['gptInputText']); - - res = await gptAPICall(questionText, GPTCallType.INFO, this.chatSortPrompt); - break; - } - - // Trigger the callback with the result - if (this.onSortComplete) { - this.onSortComplete(res || 'Something went wrong :(', questionNumber, questionType.split(' ').slice(1).join(' ')); - - let explanation = res; - - if (questionType != '5' && questionType != '3') { - // Extract explanation surrounded by ------ at the top or both at the top and bottom - const explanationMatch = res.match(/------\s*([\s\S]*?)\s*(?:------|$)/) || []; - explanation = explanationMatch[1] ? explanationMatch[1].trim() : 'No explanation found'; - } - - // Set the extracted explanation to sortRespText - this.setSortRespText(explanation); - this.conversationArray.push(this.sortRespText); - this.scrollToBottom(); - - console.log(res); - } - } catch (err) { - console.error(err); + generateQuizAnswerAnalysis = (doc: Doc, quizAnswer: string) => + this.generateRubric(doc).then(() => + Doc.getDescription(doc).then(desc => + gptAPICall( + `Question: ${desc}; + UserAnswer: ${quizAnswer}; + Rubric: ${StrCast(doc.gptRubric)}`, + GPTCallType.QUIZDOC + ).then(res => { + this._conversationArray.push(res || 'GPT provided no answer'); + this.onQuizRandom?.(); + }) + .catch(err => console.error('GPT call failed', err)) + )) // prettier-ignore + + generateFireflyImage = (imgDesc: string) => { + const selView = DocumentView.Selected().lastElement(); + const selDoc = selView?.Document; + if (selDoc && (selView._props.renderDepth > 1 || selDoc[Doc.LayoutFieldKey(selDoc)] instanceof ImageField)) { + const oldPrompt = StrCast(selDoc.ai_firefly_prompt, StrCast(selDoc.title)); + const newPrompt = oldPrompt ? `${oldPrompt} ~~~ ${imgDesc}` : imgDesc; + return DrawingFillHandler.drawingToImage(selDoc, 100, newPrompt, selDoc) + .then(action(() => (this._userPrompt = ''))) + .catch(e => { + alert(e); + return undefined; + }); } - - this.setLoading(false); - this.setSortDone(true); + return SmartDrawHandler.CreateWithFirefly(imgDesc, FireflyImageDimensions.Square, 0) + .then( + action(doc => { + doc instanceof Doc && DocumentViewInternal.addDocTabFunc(doc, OpenWhere.addRight); + this._userPrompt = ''; + }) + ) + .catch(e => { + alert(e); + return undefined; + }); }; + /** + * Generates a response to the user's question about the docs in the collection. + * The type of response depends on the chat's analysis of the type of their question + * @param userPrompt the user's input that chat will respond to + */ + generateUserPromptResponse = (userPrompt: string) => + gptAPICall(userPrompt, GPTCallType.COMMANDTYPE, undefined, true).then((commandType, args = commandType.split(' ').slice(1).join(' ')) => + (async () => { + switch (this.NumberToCommandType(commandType)) { + case GPTDocCommand.AssignTags: + case GPTDocCommand.Filter: return this._documentDescriptions?.then(descs => gptAPICall(userPrompt, GPTCallType.SUBSETDOCS, descs)) ?? ""; + case GPTDocCommand.Sort: return this._documentDescriptions?.then(descs => gptAPICall(userPrompt, GPTCallType.SORTDOCS, descs)) ?? ""; + default: return Doc.getDescription(DocumentView.SelectedDocs().lastElement()).then(desc => gptAPICall(userPrompt, GPTCallType.DOCINFO, desc)); + } // prettier-ignore + })().then( + action(res => { + // Trigger the callback with the result + this.onGptResponse?.(res || 'Something went wrong :(', this.NumberToCommandType(commandType), args); + this._conversationArray.push( + this.NumberToCommandType(commandType) === GPTDocCommand.GetInfo ? res: + // Extract explanation surrounded by the DocSeperator string (defined in GPT.ts) at the top or both at the top and bottom + (res.match(new RegExp(`${DocSeperator}\\s*([\\s\\S]*?)\\s*(?:${DocSeperator}|$)`)) ?? [])[1]?.trim() ?? 'No explanation found' + ); + }) + ).catch(err => console.log(err)) + ).catch(err => console.log(err)); // prettier-ignore /** * Generates a Dalle image and uploads it to the server. */ - generateImage = async () => { - if (this.imgDesc === '') return undefined; + generateImage = (imgDesc: string, imgTarget: Doc, addToCollection?: (doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) => { + this._imgTargetDoc = imgTarget; + SnappingManager.SetChatVisible(true); + this.addDoc = addToCollection; this.setImgUrls([]); this.setMode(GPTPopupMode.IMAGE); - this.setVisible(true); - this.setLoading(true); - - try { - 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.error(err); - } - this.setLoading(false); - return undefined; + this.setGptProcessing(true); + this._imageDescription = imgDesc; + + return gptImageCall(imgDesc) + .then(imageUrls => + imageUrls?.[0] + ? Networking.PostToServer('/uploadRemoteImage', { sources: [imageUrls[0]] }).then(res => { + const source = ClientUtils.prepend((res as Upload.FileInformation[])[0].accessPaths.agnostic.client); + return this.setImgUrls([[imageUrls[0]!, source]]); + }) + : undefined + ) + .catch(err => console.error(err)) + .finally(() => this.setGptProcessing(false)); }; /** - * Completes an API call to generate a summary of - * this.selectedText in the popup. + * Completes an API call to generate a summary of the specified text + * + * @param text the text to summarizz */ - 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); + generateSummary = (text: string) => { + SnappingManager.SetChatVisible(true); + this._textToSummarize = text; + this.setMode(GPTPopupMode.SUMMARY); + this.setGptProcessing(true); + return gptAPICall(text, GPTCallType.SUMMARY) + .then(res => this.setResponseText(res || 'Something went wrong.')) + .catch(err => console.error(err)) + .finally(() => this.setGptProcessing(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); + generateDataAnalysis = () => { + this.setGptProcessing(true); + return gptAPICall(this._dataJson, GPTCallType.DATA, this._dataChatPrompt) + .then(res => { + 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]]); + this.setResponseText(json[keys[2]] || 'Something went wrong.'); + }) + .catch(err => console.error(err)) + .finally(() => this.setGptProcessing(false)); }; /** * Transfers the summarization text to a sidebar annotation text document. */ private transferToText = () => { - const newDoc = Docs.Create.TextDocument(this.text.trim(), { + const newDoc = Docs.Create.TextDocument(this._responseText.trim(), { _width: 200, _height: 50, _layout_fitWidth: true, _layout_autoHeight: true, }); - this.addDoc(newDoc, this.sidebarId); - // newDoc.data = 'Hello world'; + this.addDoc?.(newDoc, this._sidebarFieldKey); const anchor = AnchorMenu.Instance?.GetAnchor(undefined, false); if (anchor) { DocUtils.MakeLink(newDoc, anchor, { @@ -407,80 +340,44 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { /** * Creates a histogram to show the correlation relationship that was found */ - private createVisualization = () => { - this.createFilteredDoc(this.correlatedColumns); - }; + private createVisualization = () => this.createFilteredDoc(this._correlatedColumns); /** * Transfers the image urls to actual image docs */ private transferToImage = (source: string) => { - const textAnchor = this.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' }); - }; - - /** - * 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) { - super(props); - makeObservable(this); - GPTPopup.Instance = this; - this.messagesEndRef = React.createRef(); - } - - scrollToBottom = () => { - setTimeout(() => { - // Code to execute after 1 second (1000 ms) - if (this.messagesEndRef.current) { - this.messagesEndRef.current.scrollIntoView({ behavior: 'smooth', block: 'end' }); + const textAnchor = this._textAnchor ?? this._imgTargetDoc; + if (textAnchor) { + const newDoc = Docs.Create.ImageDocument(source, { + x: NumCast(textAnchor.x) + NumCast(textAnchor._width) + 10, + y: NumCast(textAnchor.y), + _height: 200, + _width: 200, + ai: 'dall-e', + tags: new List<string>(['@ai']), + 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.addDoc?.(newDoc); } - }, 50); - }; - - componentDidUpdate = () => { - if (this.loading) { - this.setDone(false); + // Create link between prompt and image + DocUtils.MakeLink(textAnchor, newDoc, { link_relationship: 'Image Prompt' }); } }; - @observable quizMode: GPTQuizType = GPTQuizType.CURRENT; - @action setQuizMode(g: GPTQuizType) { - this.quizMode = g; - } + scrollToBottom = () => setTimeout(() => this._messagesEndRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }), 50); - cardMenu = () => ( + gptMenu = () => ( <div className="btns-wrapper-gpt"> <Button - tooltip="Have ChatGPT sort, tag, define, or filter your cards for you!" - text="Modify/Sort Cards!" - onClick={() => this.setMode(GPTPopupMode.SORT)} + tooltip="Ask Firefly to create images" + text="Ask Firefly" + onClick={() => this.setMode(GPTPopupMode.FIREFLY)} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} style={{ @@ -493,163 +390,183 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { }} /> <Button - tooltip="Test your knowledge with ChatGPT!" - text="Quiz Cards!" + tooltip="Ask GPT to sort, tag, define, or filter your Docs!" + text="Ask GPT" + onClick={() => this.setMode(GPTPopupMode.USER_PROMPT)} + color={StrCast(Doc.UserDoc().userVariantColor)} + type={Type.TERT} + style={{ + width: '100%', + height: '40%', + textAlign: 'center', + color: '#ffffff', + fontSize: '16px', + marginBottom: '10px', + }} + /> + <Button + tooltip="Test your knowledge by verifying answers with ChatGPT" + text="Take Quiz" onClick={() => { - this.conversationArray = ['Define the selected card!']; - this.setMode(GPTPopupMode.QUIZ); - if (this.onQuizRandom) { - this.onQuizRandom(); - } + this._conversationArray = ['Define the selected card!']; + this.setMode(GPTPopupMode.QUIZ_RESPONSE); + this.onQuizRandom?.(); }} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} style={{ width: '100%', + height: '40%', textAlign: 'center', color: '#ffffff', fontSize: '16px', - height: '40%', }} /> </div> ); - handleKeyPress = async (e: React.KeyboardEvent, isSort: boolean) => { + callGpt = action((mode: GPTPopupMode) => { + this.setGptProcessing(true); + switch (mode) { + case GPTPopupMode.FIREFLY: + this._fireflyArray.push(this._userPrompt); + return this.generateFireflyImage(this._userPrompt).then(action(() => (this._userPrompt = ''))); + case GPTPopupMode.USER_PROMPT: + this._conversationArray.push(this._userPrompt); + return this.generateUserPromptResponse(this._userPrompt).then(action(() => (this._userPrompt = ''))); + case GPTPopupMode.QUIZ_RESPONSE: + this._conversationArray.push(this._quizAnswer); + return this.generateQuizAnswerAnalysis(DocumentView.SelectedDocs().lastElement(), this._quizAnswer).then(action(() => (this._quizAnswer = ''))); + } + }); + + @action + handleKeyPress = async (e: React.KeyboardEvent, mode: GPTPopupMode) => { + this._askDictation?.stopDictation(); if (e.key === 'Enter') { e.stopPropagation(); - if (isSort) { - this.conversationArray.push(this.chatSortPrompt); - await this.generateCard(); - this.chatSortPrompt = ''; - } else { - this.conversationArray.push(this.quizAnswer); - await this.generateQuiz(); - this.quizAnswer = ''; - } - - this.scrollToBottom(); + this.callGpt(mode)?.then(() => { + this.setGptProcessing(false); + this.scrollToBottom(); + }); } }; - cardActual = (opt: GPTPopupMode) => { - const isSort = opt === GPTPopupMode.SORT; - return ( - <div className="btns-wrapper-gpt"> - <div className="chat-wrapper"> - <div className="chat-bubbles"> - {this.conversationArray.map((message, index) => ( - <div key={index} className={`chat-bubble ${index % 2 === 1 ? 'user-message' : 'chat-message'}`}> - {message} - </div> - ))} - {(!this.cardsDoneLoading || this.loading) && <div className={`chat-bubble chat-message`}>...</div>} - </div> - - <div ref={this.messagesEndRef} style={{ height: '100px' }} /> + gptUserInput = () => ( + <div className="btns-wrapper-gpt"> + <div className="chat-wrapper"> + <div className="chat-bubbles"> + {(this._mode === GPTPopupMode.FIREFLY ? this._fireflyArray : this._conversationArray).map((message, index) => ( + <div key={index} className={`chat-bubble ${index % 2 === 1 ? 'user-message' : 'chat-message'}`}> + {message} + </div> + ))} + {this._gptProcessing && <div className="chat-bubble chat-message">...</div>} </div> - <div className="inputWrapper"> - <input - className="searchBox-input" - defaultValue="" - value={isSort ? this.chatSortPrompt : this.quizAnswer} // Controlled input - autoComplete="off" - onChange={isSort ? this.sortPromptChanged : this.quizAnswerChanged} - onKeyDown={e => { - this.handleKeyPress(e, isSort); - }} - type="text" - placeholder={`${isSort ? 'Have ChatGPT sort, tag, define, or filter your cards for you!' : 'Define the selected card!'}`} - /> - </div> + <div ref={this._messagesEndRef} style={{ height: '100px' }} /> </div> - ); - }; + </div> + ); - sortBox = () => ( - <div style={{ height: '80%' }}> - {this.heading(this.mode === GPTPopupMode.SORT ? 'SORTING' : 'QUIZ')} - <> - {!this.cardsDoneLoading ? ( - <div className="content-wrapper"> - <div className="loading-spinner"> - <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} /> - {this.loading ? <span>Loading...</span> : <span>Reading Cards...</span>} - </div> - </div> - ) : this.mode === GPTPopupMode.CARD ? ( - this.cardMenu() - ) : ( - this.cardActual(this.mode) - ) // Call the functions to render JSX - } - </> + promptBox = (heading: string, value: string, onChange: (e: string) => string, placeholder: string) => ( + <> + <div className="gptPopup-sortBox"> + {this.heading(heading)} + {this.gptUserInput()} + </div> + <div className="inputWrapper"> + <input + className="searchBox-input" + value={value} // Controlled input + autoComplete="off" + onChange={e => onChange(e.target.value)} + onKeyDown={e => this.handleKeyPress(e, this._mode)} + type="text" + placeholder={placeholder} + /> + <Button // + text="Send" + type={Type.TERT} + icon={<AiOutlineSend />} + iconPlacement="right" + color={SnappingManager.userVariantColor} + onClick={() => this.callGpt(this._mode)} + /> + <DictationButton ref={r => (this._askDictation = r)} setInput={onChange} /> + </div> + </> + ); + + menuBox = () => ( + <div className="gptPopup-sortBox"> + {this.heading('CHOOSE')} + {this.gptMenu()} </div> ); imageBox = () => ( - <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> + <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', overflow: 'auto', height: '100%', pointerEvents: 'all' }}> {this.heading('GENERATED IMAGE')} <div className="image-content-wrapper"> - {this.imgUrls.map((rawSrc, i) => ( - <div key={rawSrc[0] + i} className="img-wrapper"> - <div className="img-container"> - <img key={rawSrc[0]} src={rawSrc[0]} width={150} height={150} alt="dalle generation" /> + {this._imgUrls.map((rawSrc, i) => ( + <> + <div key={rawSrc[0] + i} className="img-wrapper"> + <div className="img-container"> + <img key={rawSrc[0]} src={rawSrc[0]} width={150} height={150} alt="dalle generation" /> + </div> </div> - <div className="btn-container"> + <div key={rawSrc[0] + i + 'btn'} 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)} />} + {this._gptProcessing ? null : ( + <IconButton + tooltip="Generate Again" + onClick={() => this._imgTargetDoc && this.generateImage(this._imageDescription, this._imgTargetDoc, this._addToCollection)} + icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} + color={StrCast(Doc.UserDoc().userVariantColor)} + /> + )} </div> ); summaryBox = () => ( <> - <div> + <div style={{ height: 'calc(100% - 60px)', overflow: 'auto' }}> {this.heading('SUMMARY')} - <div className="content-wrapper"> - {!this.loading && - (!this.done ? ( + <div className="gptPopup-content-wrapper"> + {!this._gptProcessing && + (!this._stopAnimatingResponse ? ( <TypeAnimation speed={50} sequence={[ - this.text, + this._responseText, () => { - setTimeout(() => { - this.setDone(true); - }, 500); + setTimeout(() => this.setStopAnimatingResponse(true), 500); }, ]} /> ) : ( - this.text + this._responseText ))} </div> </div> - {!this.loading && ( - <div className="btns-wrapper"> - {this.done ? ( + {!this._gptProcessing && ( + <div className="btns-wrapper" style={{ position: 'absolute', bottom: 0, width: 'calc(100% - 32px)' }}> + {this._stopAnimatingResponse ? ( <> - <IconButton tooltip="Generate Again" onClick={this.generateSummary /* this.callSummaryApi */} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(SettingsManager.userVariantColor)} /> + <IconButton tooltip="Generate Again" onClick={() => this.generateSummary(this._textToSummarize + ' ')} 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"> <span>Summarizing</span> <ReactLoading type="bubbles" color="#bcbcbc" width={20} height={20} /> - <Button - text="Stop Animation" - onClick={() => { - this.setDone(true); - }} - color={StrCast(SettingsManager.userVariantColor)} - type={Type.TERT} - /> + <Button text="Stop Animation" onClick={() => this.setStopAnimatingResponse(true)} color={StrCast(SettingsManager.userVariantColor)} type={Type.TERT} /> </div> )} </div> @@ -661,33 +578,31 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { <> <div> {this.heading('ANALYSIS')} - <div className="content-wrapper"> - {!this.loading && - (!this.done ? ( + <div className="gptPopup-content-wrapper"> + {!this._gptProcessing && + (!this._stopAnimatingResponse ? ( <TypeAnimation speed={50} sequence={[ - this.text, + this._responseText, () => { - setTimeout(() => { - this.setDone(true); - }, 500); + setTimeout(() => this.setStopAnimatingResponse(true), 500); }, ]} /> ) : ( - this.text + this._responseText ))} </div> </div> - {!this.loading && ( + {!this._gptProcessing && ( <div className="btns-wrapper"> - {this.done ? ( - this.chatMode ? ( + {this._stopAnimatingResponse ? ( + this._chatEnabled ? ( <input defaultValue="" autoComplete="off" - onChange={this.dataPromptChanged} + onChange={e => (this._dataChatPrompt = e.target.value)} onKeyDown={e => { e.key === 'Enter' ? this.generateDataAnalysis() : null; e.stopPropagation(); @@ -701,21 +616,14 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { ) : ( <> <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} /> + <Button tooltip="Chat with AI" text="Chat with AI" onClick={() => this.setChatEnabled(true)} 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} - /> + <Button text="Stop Animation" onClick={() => this.setStopAnimatingResponse(true)} color={StrCast(SnappingManager.userVariantColor)} type={Type.TERT} /> </div> )} </div> @@ -724,62 +632,53 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { ); aiWarning = () => - this.done ? ( + !this._stopAnimatingResponse ? null : ( <div className="ai-warning"> <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 ? ( + {this._gptProcessing ? ( <ReactLoading type="spin" color="#bcbcbc" width={14} height={14} /> ) : ( <> - {(this.mode === GPTPopupMode.SORT || this.mode === GPTPopupMode.QUIZ) && ( - <IconButton color={StrCast(SettingsManager.userVariantColor)} tooltip="back" icon={<CgCornerUpLeft size="16px" />} onClick={() => (this.mode = GPTPopupMode.CARD)} style={{ right: '50px', position: 'absolute' }} /> - )} - <IconButton - color={StrCast(SettingsManager.userVariantColor)} - tooltip="close" - icon={<CgClose size="16px" />} - onClick={() => { - this.setVisible(false); - }} + <Toggle + tooltip="Clear Chat filter" + toggleType={ToggleType.BUTTON} + type={Type.PRIM} + toggleStatus={Doc.hasDocFilter(this._collectionContext, 'tags', GPTPopup.ChatTag)} + text={Doc.hasDocFilter(this._collectionContext, 'tags', GPTPopup.ChatTag) ? 'filtered' : ''} + color={Doc.hasDocFilter(this._collectionContext, 'tags', GPTPopup.ChatTag) ? 'red' : 'transparent'} + onClick={() => this._collectionContext && Doc.setDocFilter(this._collectionContext, 'tags', GPTPopup.ChatTag, 'remove')} /> + {[GPTPopupMode.USER_PROMPT, GPTPopupMode.QUIZ_RESPONSE, GPTPopupMode.FIREFLY].includes(this._mode) && ( + <IconButton color={StrCast(SettingsManager.userVariantColor)} tooltip="back" icon={<CgCornerUpLeft size="16px" />} onClick={() => (this._mode = GPTPopupMode.GPT_MENU)} /> + )} </> )} </div> ); render() { - let content; - - switch (this.mode) { - case GPTPopupMode.SUMMARY: - content = this.summaryBox(); - break; - case GPTPopupMode.DATA: - content = this.dataAnalysisBox(); - break; - case GPTPopupMode.IMAGE: - content = this.imageBox(); - break; - case GPTPopupMode.SORT: - case GPTPopupMode.CARD: - case GPTPopupMode.QUIZ: - content = this.sortBox(); - break; - default: - content = null; - } - return ( - <div className="summary-box" style={{ display: this.visible ? 'flex' : 'none' }}> - {content} - <div className="resize-handle" /> + <div className="gptPopup-summary-box" style={{ display: SnappingManager.ChatVisible ? 'flex' : 'none', overflow: 'auto' }}> + {(() => { + //prettier-ignore + switch (this._mode) { + case GPTPopupMode.USER_PROMPT: return this.promptBox("ASK", this._userPrompt, this.setUserPrompt, 'Ask GPT to sort, tag, define, or filter your documents for you!'); + case GPTPopupMode.FIREFLY: return this.promptBox("CREATE", this._userPrompt, this.setUserPrompt, StrCast(DocumentView.Selected().lastElement()?.Document.ai_firefly_prompt, 'Ask Firefly to generate images')); + case GPTPopupMode.QUIZ_RESPONSE: return this.promptBox("QUIZ", this._quizAnswer, this.setQuizAnswer, 'Describe/answer the selected document!'); + case GPTPopupMode.GPT_MENU: return this.menuBox(); + case GPTPopupMode.SUMMARY: return this.summaryBox(); + case GPTPopupMode.DATA: return this.dataAnalysisBox(); + case GPTPopupMode.IMAGE: return this.imageBox(); + default: return null; + } + })()} </div> ); } |
