import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, IconButton, Type } from 'browndash-components'; import { action, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { CgClose, 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 { DocUtils } from '../../../documents/DocUtils'; import { Docs } from '../../../documents/Documents'; import { SettingsManager } from '../../../util/SettingsManager'; import { SnappingManager } from '../../../util/SnappingManager'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { DocumentView } from '../../nodes/DocumentView'; import { AnchorMenu } from '../AnchorMenu'; import './GPTPopup.scss'; export enum GPTPopupMode { SUMMARY, EDIT, IMAGE, FLASHCARD, DATA, CARD, SORT, QUIZ, } export enum GPTQuizType { CURRENT = 0, CHOOSE = 1, MULTIPLE = 2, } interface GPTPopupProps {} @observer export class GPTPopup extends ObservableReactComponent { // eslint-disable-next-line no-use-before-define static Instance: GPTPopup; private messagesEndRef: React.RefObject; @observable private chatMode: boolean = false; private correlatedColumns: string[] = []; @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 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; }; @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; } @observable sortRespText: string = ''; @action setSortRespText(resp: string) { this.sortRespText = resp; } @observable chatSortPrompt: string = ''; sortPromptChanged = action((e: React.ChangeEvent) => { this.chatSortPrompt = e.target.value; }); @observable quizAnswer: string = ''; quizAnswerChanged = action((e: React.ChangeEvent) => { 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. ']; /** * When the cards are in quiz mode in the card view, allows gpt to determine whether the user's answer was correct * @returns */ 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(); } }; /** * 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 */ 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) | null = null; /** * Callback function that causes the card view to update the childpair string list * @param callback */ @action public setRegenerateCallback(callback: () => Promise) { 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; } /** * Generates a response to the user's question depending on the type of their question */ 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); } this.setLoading(false); this.setSortDone(true); }; /** * Generates a Dalle image and uploads it to the server. */ generateImage = async () => { if (this.imgDesc === '') return undefined; 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; }; /** * 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); }; /** * Transfers the summarization text to a sidebar annotation text document. */ private transferToText = () => { const newDoc = Docs.Create.TextDocument(this.text.trim(), { _width: 200, _height: 50, _layout_fitWidth: true, _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, { link_relationship: 'GPT Summary', }); } }; /** * 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) => { 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) => { 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' }); } }, 50); }; componentDidUpdate = () => { if (this.loading) { this.setDone(false); } }; @observable quizMode: GPTQuizType = GPTQuizType.CURRENT; @action setQuizMode(g: GPTQuizType) { this.quizMode = g; } cardMenu = () => (
); handleKeyPress = async (e: React.KeyboardEvent, isSort: boolean) => { 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(); } }; cardActual = (opt: GPTPopupMode) => { const isSort = opt === GPTPopupMode.SORT; return (
{this.conversationArray.map((message, index) => (
{message}
))} {(!this.cardsDoneLoading || this.loading) &&
...
}
{ this.handleKeyPress(e, isSort); }} type="text" placeholder={`${isSort ? 'Have ChatGPT sort, tag, define, or filter your cards for you!' : 'Define the selected card!'}`} />
); }; sortBox = () => (
{this.heading(this.mode === GPTPopupMode.SORT ? 'SORTING' : 'QUIZ')} <> {!this.cardsDoneLoading ? (
{this.loading ? Loading... : Reading Cards...}
) : this.mode === GPTPopupMode.CARD ? ( this.cardMenu() ) : ( this.cardActual(this.mode) ) // Call the functions to render JSX }
); imageBox = () => (
{this.heading('GENERATED IMAGE')}
{this.imgUrls.map((rawSrc, i) => (
dalle generation
))}
{!this.loading && } color={StrCast(Doc.UserDoc().userVariantColor)} />}
); summaryBox = () => ( <>
{this.heading('SUMMARY')}
{!this.loading && (!this.done ? ( { setTimeout(() => { this.setDone(true); }, 500); }, ]} /> ) : ( this.text ))}
{!this.loading && (
{this.done ? ( <> } color={StrCast(SettingsManager.userVariantColor)} />
)}
)} ); dataAnalysisBox = () => ( <>
{this.heading('ANALYSIS')}
{!this.loading && (!this.done ? ( { setTimeout(() => { this.setDone(true); }, 500); }, ]} /> ) : ( this.text ))}
{!this.loading && (
{this.done ? ( this.chatMode ? ( { 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%' }} /> ) : ( <>
)} )} ); aiWarning = () => this.done ? (
AI generated responses can contain inaccurate or misleading content.
) : null; heading = (headingText: string) => (
{this.loading ? ( ) : ( <> {(this.mode === GPTPopupMode.SORT || this.mode === GPTPopupMode.QUIZ) && ( } onClick={() => (this.mode = GPTPopupMode.CARD)} style={{ right: '50px', position: 'absolute' }} /> )} } onClick={() => { this.setVisible(false); }} /> )}
); 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 (
{content}
); } }