aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/pdf/GPTPopup
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2025-03-16 19:02:56 -0400
committerbobzel <zzzman@gmail.com>2025-03-16 19:02:56 -0400
commitdf708c90d8356934d2e3d9123129c761d328c1fe (patch)
tree98b0588710ac8ca00c303960da0851614aacf597 /src/client/views/pdf/GPTPopup
parent7d9fae09e8906e5636f6ea695ad560797b08d023 (diff)
parentf4042257be7359734a0dd35cedbf03fe4aa14cf1 (diff)
Merge branch 'DocCreatorMenu-work' 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.scss111
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.tsx989
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>
);
}