aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/pdf
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/pdf')
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.scss92
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.tsx375
2 files changed, 396 insertions, 71 deletions
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.scss b/src/client/views/pdf/GPTPopup/GPTPopup.scss
index 6d8793f82..0247dc10c 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.scss
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.scss
@@ -7,10 +7,13 @@ $highlightedText: #82e0ff;
.summary-box {
position: fixed;
- bottom: 10px;
- right: 10px;
+ top: 115px;
+ left: 75px;
width: 250px;
+ height: 200px;
min-height: 200px;
+ min-width: 180px;
+
border-radius: 16px;
padding: 16px;
padding-bottom: 0;
@@ -21,6 +24,18 @@ $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;
+ }
.summary-heading {
display: flex;
@@ -51,25 +66,76 @@ $highlightedText: #82e0ff;
.content-wrapper {
padding-top: 10px;
min-height: 50px;
- max-height: 150px;
+ // max-height: 150px;
overflow-y: auto;
+ height: 100%
}
.btns-wrapper-gpt {
- height: 50px;
+ height: 100%;
display: flex;
justify-content: center;
align-items: center;
- transform: translateY(30px);
+ flex-direction: column;
+
+ .inputWrapper{
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 60px;
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ background-color: white;
+
+ }
.searchBox-input{
- transform: translateY(-15px);
- height: 50px;
+ height: 40px;
border-radius: 10px;
+ position: absolute;
+ bottom: 10px;
border-color: #5b97ff;
+ width: 90%
}
+ .chat-wrapper {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ max-height: calc(100vh - 80px);
+ overflow-y: auto;
+ padding-bottom: 60px;
+ }
+
+ .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;
+ }
+
+
.summarizing {
@@ -78,16 +144,14 @@ $highlightedText: #82e0ff;
}
- }
- button {
- font-size: 9px;
- padding: 10px;
- color: #ffffff;
- background-color: $button;
- border-radius: 5px;
+
+
+
}
+
+
.text-btn {
&:hover {
background-color: $button;
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index a37e73e27..d5f5f620c 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -3,7 +3,7 @@ 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 } from 'react-icons/cg';
+import { CgClose, CgCornerUpLeft } from 'react-icons/cg';
import ReactLoading from 'react-loading';
import { TypeAnimation } from 'react-type-animation';
import { ClientUtils } from '../../../../ClientUtils';
@@ -11,13 +11,14 @@ import { Doc } from '../../../../fields/Doc';
import { NumCast, StrCast } from '../../../../fields/Types';
import { Networking } from '../../../Network';
import { GPTCallType, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT';
-import { Docs } from '../../../documents/Documents';
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';
-import { SettingsManager } from '../../../util/SettingsManager';
-import { SnappingManager } from '../../../util/SnappingManager';
export enum GPTPopupMode {
SUMMARY,
@@ -25,7 +26,15 @@ export enum GPTPopupMode {
IMAGE,
FLASHCARD,
DATA,
+ CARD,
SORT,
+ QUIZ,
+}
+
+export enum GPTQuizType {
+ CURRENT = 0,
+ CHOOSE = 1,
+ MULTIPLE = 2,
}
interface GPTPopupProps {}
@@ -34,6 +43,8 @@ interface GPTPopupProps {}
export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
// eslint-disable-next-line no-use-before-define
static Instance: GPTPopup;
+ private messagesEndRef: React.RefObject<HTMLDivElement>;
+
@observable private chatMode: boolean = false;
private correlatedColumns: string[] = [];
@@ -63,7 +74,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
};
@observable
public dataJson: string = '';
- public dataChatPrompt: string | null = null;
+ public dataChatPrompt: string | undefined = undefined;
@action
public setDataJson = (text: string) => {
if (text === '') this.dataChatPrompt = '';
@@ -140,7 +151,8 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
this.sortDesc = t;
};
- @observable onSortComplete?: (sortResult: string) => void;
+ @observable onSortComplete?: (sortResult: string, questionType: string, tag?: string) => void;
+ @observable onQuizRandom?: () => void;
@observable cardsDoneLoading = false;
@action setCardsDoneLoading(done: boolean) {
@@ -148,22 +160,157 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
this.cardsDoneLoading = done;
}
+ @observable sortRespText: string = '';
+
+ @action setSortRespText(resp: string) {
+ this.sortRespText = resp;
+ }
+
+ @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. '];
+
+ /**
+ * 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<void>) | null = null;
+
+ /**
+ * Callback function that causes the card view to update the childpair string list
+ * @param callback
+ */
+ @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;
+ }
+
/**
- * Sorts cards in the CollectionCardDeckView
+ * Generates a response to the user's question depending on the type of their question
*/
- generateSort = async () => {
+ 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);
+ // 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 :(');
+ 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) {
@@ -305,67 +452,143 @@ export class GPTPopup extends ObservableReactComponent<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 = () => (
+ <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)}
+ 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 with ChatGPT!"
+ text="Quiz Cards!"
+ onClick={() => {
+ this.conversationArray = ['Define the selected card!'];
+ this.setMode(GPTPopupMode.QUIZ);
+ if (this.onQuizRandom) {
+ this.onQuizRandom();
+ }
+ }}
+ color={StrCast(Doc.UserDoc().userVariantColor)}
+ type={Type.TERT}
+ style={{
+ width: '100%',
+ textAlign: 'center',
+ color: '#ffffff',
+ fontSize: '16px',
+ height: '40%',
+ }}
+ />
+ </div>
+ );
+
+ 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 (
+ <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' }} />
+ </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>
+ );
+ };
+
sortBox = () => (
- <>
- <div>
- {this.heading('SORTING')}
- {this.loading ? (
+ <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} />
- <span>Loading...</span>
+ {this.loading ? <span>Loading...</span> : <span>Reading Cards...</span>}
</div>
</div>
+ ) : this.mode === GPTPopupMode.CARD ? (
+ this.cardMenu()
) : (
- <>
- {!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>
- </>
+ this.cardActual(this.mode)
+ ) // Call the functions to render JSX
+ }
+ </>
+ </div>
);
+
imageBox = () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
{this.heading('GENERATED IMAGE')}
@@ -511,14 +734,52 @@ 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(SettingsManager.userVariantColor)} tooltip="close" icon={<CgClose size="16px" />} onClick={() => this.setVisible(false)} />}
+ {this.loading ? (
+ <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);
+ }}
+ />
+ </>
+ )}
</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' }}>
- {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}
+ {content}
+ <div className="resize-handle" />
</div>
);
}