aboutsummaryrefslogtreecommitdiff
path: root/src/client/views
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views')
-rw-r--r--src/client/views/ViewBoxInterface.ts1
-rw-r--r--src/client/views/collections/CollectionCardDeckView.tsx157
-rw-r--r--src/client/views/collections/CollectionSubView.tsx1
-rw-r--r--src/client/views/collections/CollectionView.tsx4
-rw-r--r--src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts38
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx4
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.tsx231
7 files changed, 191 insertions, 245 deletions
diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts
index a66a20cf6..30da8c616 100644
--- a/src/client/views/ViewBoxInterface.ts
+++ b/src/client/views/ViewBoxInterface.ts
@@ -22,6 +22,7 @@ export abstract class ViewBoxInterface<P> extends ObservableReactComponent<React
return ''; //
}
promoteCollection?: () => void; // moves contents of collection to parent
+ hasChildDocs?: () => Doc[];
updateIcon?: (usePanelDimensions?: boolean) => Promise<void>; // updates the icon representation of the document
getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
restoreView?: (viewSpec: Doc) => boolean; // DEPRECATED: do not use, it will go away. see PresBox.restoreTargetDocView
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index e00aa65d7..d7f4251f3 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -1,22 +1,22 @@
-import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import * as CSS from 'csstype';
+import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { computedFn } from 'mobx-utils';
import * as React from 'react';
-import * as CSS from 'csstype';
-import { ClientUtils, imageUrlToBase64, returnFalse, returnNever, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
+import { ClientUtils, returnFalse, returnNever, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
import { emptyFunction } from '../../../Utils';
import { Doc, DocListCast, Opt } from '../../../fields/Doc';
-import { Animation, DocData } from '../../../fields/DocSymbols';
+import { Animation } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
-import { URLField } from '../../../fields/URLField';
-import { gptImageLabel, GPTTypeStyle } from '../../apis/gpt/GPT';
+import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { DocumentType } from '../../documents/DocumentTypes';
import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { dropActionType } from '../../util/DropActionTypes';
+import { SettingsManager } from '../../util/SettingsManager';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
import { undoable, UndoManager } from '../../util/UndoManager';
@@ -25,11 +25,8 @@ import { StyleProp } from '../StyleProp';
import { TagItem } from '../TagsView';
import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
import { FocusViewOptions } from '../nodes/FocusViewOptions';
-import { GPTPopup } from '../pdf/GPTPopup/GPTPopup';
import './CollectionCardDeckView.scss';
-import { CollectionSubView, docSortings, SubCollectionViewProps } from './CollectionSubView';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { SettingsManager } from '../../util/SettingsManager';
+import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
/**
* New view type specifically for studying more dynamically. Allows you to reorder docs however you see fit, easily
@@ -42,7 +39,6 @@ import { SettingsManager } from '../../util/SettingsManager';
export class CollectionCardView extends CollectionSubView() {
private _dropDisposer?: DragManager.DragDropDisposer;
private _disposers: { [key: string]: IReactionDisposer } = {};
- private _textToDoc = new Map<string, Doc>();
private _oldWheel: HTMLElement | null = null;
private _dropped = false; // set when a card doc has just moved and the drop method has been called - prevents the pointerUp method from hiding doc decorations (which needs to be done when clicking on a card to animate it to front/center)
private _setCurDocScript = () => ScriptField.MakeScript('scriptContext.layoutDoc._card_curDoc=this', { scriptContext: 'any' })!;
@@ -74,22 +70,7 @@ export class CollectionCardView extends CollectionSubView() {
return Math.ceil(this.cardDeckWidth / this.cardWidth);
}
- /**
- * update's gpt's doc-text list and initialize callbacks to respond to GPT output
- */
- updateDocumentDescriptions = () =>
- this.childPairStringList().then(docDescriptions => {
- GPTPopup.Instance.setDocumentDescriptions(docDescriptions.join());
- GPTPopup.Instance.onGptResponse = this.processGptResponse;
- GPTPopup.Instance.onQuizRandom = this.randomlyChooseDoc;
- });
-
componentDidMount() {
- this._disposers.chatVis = reaction(
- () => GPTPopup.Instance.Visible,
- vis => !vis && this.onGptHide()
- );
- GPTPopup.Instance.setRetrieveDocDescriptionsCallback(this.Document, this.updateDocumentDescriptions);
this._props.setContentViewBox?.(this);
// if card deck moves, then the child doc views are hidden so their screen to local transforms will return empty rectangles
// when inquired from the dom (below in childScreenToLocal). When the doc is actually rendered, we need to act like the
@@ -110,10 +91,7 @@ export class CollectionCardView extends CollectionSubView() {
);
}
- onGptHide = () => Doc.setDocFilter(this.Document, 'tags', '#chat', 'remove');
componentWillUnmount() {
- GPTPopup.Instance.onGptResponse = undefined;
- GPTPopup.Instance.onQuizRandom = undefined;
Object.keys(this._disposers).forEach(key => this._disposers[key]?.());
this._dropDisposer?.();
}
@@ -128,7 +106,7 @@ export class CollectionCardView extends CollectionSubView() {
* Circle arc size, in radians, to layout cards
*/
@computed get archAngle() {
- return NumCast(this.layoutDoc.card_arch, 90) * (Math.PI / 180) * (this.childCards.length < this._maxRowCount ? this.childCards.length / this._maxRowCount : 1);
+ return NumCast(this.layoutDoc.card_arch, 90) * (Math.PI / 180) * (this.childDocsNoInk.length < this._maxRowCount ? this.childDocsNoInk.length / this._maxRowCount : 1);
}
/**
* Spacing card rows as a percent of Doc size. 100 means rows spread out to fill 100% of the Doc vertically. Default is 60%
@@ -140,7 +118,7 @@ export class CollectionCardView extends CollectionSubView() {
/**
* The child documents to be rendered-- everything other than ink/link docs (which are marks as being svg's)
*/
- @computed get childCards() {
+ @computed get childDocsNoInk() {
return this.childLayoutPairs.filter(pair => !pair.layout.layout_isSvg);
}
@@ -148,7 +126,7 @@ export class CollectionCardView extends CollectionSubView() {
* how much to scale down the contents of the view so that everything will fit
*/
@computed get fitContentScale() {
- const length = Math.min(this.childCards.length, this._maxRowCount);
+ const length = Math.min(this.childDocsNoInk.length, this._maxRowCount);
return (this.childPanelWidth() * length) / this._props.PanelWidth();
}
@@ -164,17 +142,12 @@ export class CollectionCardView extends CollectionSubView() {
return this._props.PanelWidth() - 2 * this.xMargin;
}
- /**
- * When in quiz mode, randomly selects a document
- */
- randomlyChooseDoc = () => (this.layoutDoc._card_curDoc = this.childDocs[Math.floor(Math.random() * this.childDocs.length)]);
-
setHoveredNodeIndex = action((index: number) => {
if (!SnappingManager.IsDragging) this._hoveredNodeIndex = index;
});
isSelected = (doc: Doc) => this._docRefs.get(doc)?.IsSelected;
- childPanelWidth = () => NumCast(this.layoutDoc.childPanelWidth, Math.max(150, this._props.PanelWidth() / (this.childCards.length > this._maxRowCount ? this._maxRowCount : this.childCards.length) / this.nativeScaling));
+ childPanelWidth = () => NumCast(this.layoutDoc.childPanelWidth, Math.max(150, this._props.PanelWidth() / (this.childDocsNoInk.length > this._maxRowCount ? this._maxRowCount : this.childDocsNoInk.length) / this.nativeScaling));
childPanelHeight = () => this._props.PanelHeight() * this.fitContentScale;
onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this.isAnyChildContentActive();
@@ -318,10 +291,10 @@ export class CollectionCardView extends CollectionSubView() {
* @returns number of cards in row that contains index
*/
cardsInRowThatIncludesCardIndex = (index: number) => {
- if (this.childCards.length < this._maxRowCount) {
- return this.childCards.length;
+ if (this.childDocsNoInk.length < this._maxRowCount) {
+ return this.childDocsNoInk.length;
}
- const totalCards = this.childCards.length;
+ const totalCards = this.childDocsNoInk.length;
if (index < totalCards - (totalCards % this._maxRowCount)) {
return this._maxRowCount;
}
@@ -385,102 +358,6 @@ export class CollectionCardView extends CollectionSubView() {
: this.translateY(index);
};
- /**
- * A list of the text content of all the child docs. RTF documents will have just their text and pdf documents will have the first 50 words.
- * Image documents are converted to bse64 and gpt generates a description for them. all other documents use their title. This string is
- * inputted into the gpt prompt to sort everything together
- * @returns
- */
- childPairStringList = () => {
- const docToText = (doc: Doc) => {
- switch (doc.type) {
- case DocumentType.PDF: return StrCast(doc.text).split(/\s+/).slice(0, 50).join(' '); // first 50 words of pdf text
- case DocumentType.IMG: return this.getImageDesc(doc);
- case DocumentType.RTF: return StrCast(RTFCast(doc.text).Text);
- default: return StrCast(doc.title);
- } // prettier-ignore
- };
- const docTextPromises = this.childCards
- .map(pair => pair.layout)
- .map(async doc => {
- const docText = (await docToText(doc)) ?? '';
- doc.gptInputText = docText;
- this._textToDoc.set(docText.replace(/\n/g, ' ').trim(), doc);
- return `======${docText.replace(/\n/g, ' ').trim()}======`;
- });
- return Promise.all<string>(docTextPromises);
- };
-
- /**
- * Calls the gpt API to generate descriptions for the images in the view
- * @param image
- * @returns
- */
- getImageDesc = async (image: Doc) => {
- if (StrCast(image.description)) return StrCast(image.description); // Return existing description
- const { href } = (image.data as URLField).url;
- const hrefParts = href.split('.');
- const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
- try {
- const hrefBase64 = await imageUrlToBase64(hrefComplete);
- const response = await gptImageLabel(hrefBase64, 'Give three to five labels to describe this image.');
- image[DocData].description = response.trim();
- return response; // Return the response from gptImageLabel
- } catch (error) {
- console.log(error);
- }
- return '';
- };
-
- /**
- * 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
- */
- processGptResponse = (gptOutput: string, questionType: GPTTypeStyle, tag?: string) =>
- undoable(() => {
- // Split the string into individual list items
- const listItems = gptOutput.split('======').filter(item => item.trim() !== '');
-
- if (questionType === GPTTypeStyle.Filter) {
- this.childDocs.forEach(d => {
- TagItem.removeTagFromDoc(d, '#chat');
- });
- }
-
- if (questionType === GPTTypeStyle.SortDocs) {
- this.Document[this._props.fieldKey + '_sort'] = docSortings.Chat;
- }
-
- listItems.forEach((item, index) => {
- const normalizedItem = item.trim();
- // find the corresponding Doc in the textToDoc map
- const doc = this._textToDoc.get(normalizedItem);
- if (doc) {
- switch (questionType) {
- case GPTTypeStyle.SortDocs:
- doc.chatIndex = index;
- break;
- case GPTTypeStyle.AssignTags:
- if (tag) {
- const hashTag = tag.startsWith('#') ? tag : '#' + tag[0].toLowerCase() + tag.slice(1);
- const filterTag = Doc.MyFilterHotKeys.map(key => StrCast(key.toolType)).find(key => key.includes(tag)) ?? hashTag;
- TagItem.addTagToDoc(doc, filterTag);
- }
- break;
- case GPTTypeStyle.Filter:
- TagItem.addTagToDoc(doc, '#chat');
- Doc.setDocFilter(this.Document, 'tags', '#chat', 'check');
- break;
- }
- } else {
- console.warn(`No matching document found for item: ${normalizedItem}`);
- }
- });
- }, '')();
-
childScreenToLocal = computedFn((doc: Doc, index: number, isSelected: boolean) => () => {
// need to explicitly trigger an invalidation since we're reading everything from the Dom
this._forceChildXf;
@@ -620,7 +497,7 @@ export class CollectionCardView extends CollectionSubView() {
curDoc = () => DocCast(this.layoutDoc._card_curDoc);
render() {
- const fitContentScale = this.childCards.length === 0 ? 1 : this.fitContentScale;
+ const fitContentScale = this.childDocsNoInk.length === 0 ? 1 : this.fitContentScale;
return (
<div
className="collectionCardView-outer"
@@ -652,7 +529,7 @@ export class CollectionCardView extends CollectionSubView() {
<div
className="collectionCardView-flashcardUI"
style={{
- pointerEvents: this.childCards.length === 0 ? undefined : 'none',
+ pointerEvents: this.childDocsNoInk.length === 0 ? undefined : 'none',
height: `${100 / this.nativeScaling / fitContentScale}%`,
width: `${100 / this.nativeScaling / fitContentScale}%`,
transform: `scale(${this.nativeScaling * fitContentScale})`,
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 5e99bec39..954f9aa4c 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -121,6 +121,7 @@ export function CollectionSubView<X>() {
return this.dataDoc[this._props.fieldKey]; // this used to be 'layoutDoc', but then template fields will get ignored since the template is not a proto of the layout. hopefully nothing depending on the previous code.
}
+ hasChildDocs = () => this.childLayoutPairs.map(pair => pair.layout);
@computed get childLayoutPairs(): { layout: Doc; data: Doc }[] {
const { Document, TemplateDataDocument } = this._props;
const validPairs = this.childDocs
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 6f0833a22..a4900e9d7 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -89,6 +89,8 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
TraceMobx();
if (type === undefined) return null;
switch (type) {
+ default:
+ case CollectionViewType.Freeform: return <CollectionFreeFormView key="collview" {...props} />;
case CollectionViewType.Schema: return <CollectionSchemaView key="collview" {...props} />;
case CollectionViewType.Calendar: return <CalendarBox key="collview" {...props} />;
case CollectionViewType.Docking: return <CollectionDockingView key="collview" {...props} />;
@@ -105,8 +107,6 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
case CollectionViewType.Time: return <CollectionTimeView key="collview" {...props} />;
case CollectionViewType.Grid: return <CollectionGridView key="collview" {...props} />;
case CollectionViewType.Card: return <CollectionCardView key="collview" {...props} />;
- case CollectionViewType.Freeform:
- default: return <CollectionFreeFormView key="collview" {...props} />;
}
};
diff --git a/src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts b/src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts
index 6dc36b0d1..284879a4a 100644
--- a/src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts
+++ b/src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts
@@ -263,79 +263,79 @@ const standardOptions = ['title', 'backgroundColor'];
* Description of document options and data field for each type.
*/
const documentTypesInfo: { [key in supportedDocTypes]: { options: string[]; dataDescription: string } } = {
- [supportedDocTypes.comparison]: {
+ comparison: {
options: [...standardOptions, 'fontColor', 'text_align'],
dataDescription: 'an array of two documents of any kind that can be compared.',
},
- [supportedDocTypes.deck]: {
+ deck: {
options: [...standardOptions, 'fontColor', 'text_align'],
dataDescription: 'an array of flashcard docs',
},
- [supportedDocTypes.flashcard]: {
+ flashcard: {
options: [...standardOptions, 'fontColor', 'text_align'],
dataDescription: 'an array of two strings. the first string contains a question, and the second string contains an answer',
},
- [supportedDocTypes.text]: {
+ text: {
options: [...standardOptions, 'fontColor', 'text_align'],
dataDescription: 'The text content of the document.',
},
- [supportedDocTypes.web]: {
+ web: {
options: [],
dataDescription: 'A URL to a webpage. Example: https://en.wikipedia.org/wiki/Brown_University',
},
- [supportedDocTypes.html]: {
+ html: {
options: [],
dataDescription: 'The HTML-formatted text content of the document.',
},
- [supportedDocTypes.equation]: {
+ equation: {
options: [...standardOptions, 'fontColor'],
dataDescription: 'The equation content represented as a MathML string.',
},
- [supportedDocTypes.functionplot]: {
+ functionplot: {
options: [...standardOptions, 'function_definition'],
dataDescription: 'The function definition(s) for plotting. Provide as a string or array of function definitions.',
},
- [supportedDocTypes.dataviz]: {
+ dataviz: {
options: [...standardOptions, 'chartType'],
dataDescription: 'A string of comma-separated values representing the CSV data.',
},
- [supportedDocTypes.notetaking]: {
+ notetaking: {
options: standardOptions,
dataDescription: 'An array of related text documents with small amounts of text.',
},
- [supportedDocTypes.rtf]: {
+ rtf: {
options: standardOptions,
dataDescription: 'The rich text content in RTF format.',
},
- [supportedDocTypes.image]: {
+ image: {
options: standardOptions,
dataDescription: `A url string that must end with '.png', '.jpeg', '.gif', or '.jpg'`,
},
- [supportedDocTypes.pdf]: {
+ pdf: {
options: standardOptions,
dataDescription: 'the pdf content as a PDF file url.',
},
- [supportedDocTypes.audio]: {
+ audio: {
options: standardOptions,
dataDescription: 'The audio content as a file url.',
},
- [supportedDocTypes.video]: {
+ video: {
options: standardOptions,
dataDescription: 'The video content as a file url.',
},
- [supportedDocTypes.message]: {
+ message: {
options: standardOptions,
dataDescription: 'The message content of the document.',
},
- [supportedDocTypes.diagram]: {
+ diagram: {
options: standardOptions,
dataDescription: 'diagram content as a text string in Mermaid format.',
},
- [supportedDocTypes.script]: {
+ script: {
options: standardOptions,
dataDescription: 'The compilable JavaScript code. Use this for creating scripts.',
},
- [supportedDocTypes.collection]: {
+ collection: {
options: [...standardOptions, 'type_collection'],
dataDescription: 'A collection of Docs represented as an array.',
},
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 2e14fb1d9..6960247e9 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1063,9 +1063,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
generateImage = async () => {
GPTPopup.Instance?.setTextAnchor(this.getAnchor(false));
- GPTPopup.Instance?.setImgTargetDoc(this.Document);
- GPTPopup.Instance.addToCollection = this._props.addDocument;
- GPTPopup.Instance.generateImage((this.dataDoc.text as RichTextField)?.Text);
+ GPTPopup.Instance.generateImage((this.dataDoc.text as RichTextField)?.Text, this.Document, this._props.addDocument);
};
breakupDictation = () => {
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index 0b1ee78e3..f09d786d0 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -1,6 +1,6 @@
import { Button, IconButton, Toggle, ToggleType, Type } from '@dash/components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-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';
@@ -10,12 +10,15 @@ import { ClientUtils } from '../../../../ClientUtils';
import { Doc } from '../../../../fields/Doc';
import { NumCast, StrCast } from '../../../../fields/Types';
import { Networking } from '../../../Network';
-import { GPTCallType, GPTTypeStyle, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT';
+import { DescriptionSeperator, DocSeperator, GPTCallType, GPTTypeStyle, 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 { ObservableReactComponent } from '../../ObservableReactComponent';
+import { TagItem } from '../../TagsView';
+import { docSortings } from '../../collections/CollectionSubView';
import { DocumentView } from '../../nodes/DocumentView';
import { AnchorMenu } from '../AnchorMenu';
import './GPTPopup.scss';
@@ -29,61 +32,71 @@ export enum GPTPopupMode {
QUIZ_RESPONSE, // user definitions or explanations to be evaluated by GPT
}
-export enum GPTQuizType {
- CURRENT = 0,
- CHOOSE = 1,
- MULTIPLE = 2,
-}
-
@observer
export class GPTPopup extends ObservableReactComponent<object> {
// eslint-disable-next-line no-use-before-define
static Instance: GPTPopup;
- private _retrieveDocDescriptions: (() => Promise<void>) | 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: 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();
+ }
+ public addDoc: ((doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean) | undefined;
+ public createFilteredDoc: (axes?: string[]) => boolean = () => false;
public setSidebarFieldKey = (id: string) => (this._sidebarFieldKey = id);
- public setDocumentDescriptions = (t: string) => (this._documentDescriptions = t);
public setImgTargetDoc = (anchor: Doc) => (this._imgTargetDoc = anchor);
public setTextAnchor = (anchor: Doc) => (this._textAnchor = anchor);
- public onGptResponse?: (sortResult: string, questionType: GPTTypeStyle, tag?: string) => void;
- public onQuizRandom?: () => void;
public setDataJson = (text: string) => {
if (text === '') this._dataChatPrompt = '';
this._dataJson = text;
};
- constructor(props: object) {
- super(props);
- makeObservable(this);
- GPTPopup.Instance = this;
- this._messagesEndRef = React.createRef();
+ componentDidUpdate() {
+ this._gptProcessing && this.setStopAnimatingResponse(false);
+ }
+ componentDidMount(): void {
+ reaction(
+ () => DocumentView.Selected().lastElement(),
+ selDoc => {
+ const hasChildDocs = selDoc?.ComponentView?.hasChildDocs;
+ if (hasChildDocs) {
+ this._textToDocMap.clear();
+ this.setCollectionContext(selDoc.Document);
+ this.onGptResponse = (sortResult: string, questionType: GPTTypeStyle, tag?: string) => this.processGptResponse(selDoc, this._textToDocMap, sortResult, questionType, tag);
+ this.onQuizRandom = () => this.randomlyChooseDoc(selDoc.Document, hasChildDocs());
+ this._documentDescriptions = Promise.all(hasChildDocs().map(doc =>
+ Doc.getDescription(doc).then(text => this._textToDocMap.set(text, doc) && `${DescriptionSeperator}${text}${DescriptionSeperator}`)
+ )).then(docDescriptions => docDescriptions.join()); // prettier-ignore
+ }
+ },
+ { fireImmediately: true }
+ );
}
-
- componentDidUpdate = () => this._gptProcessing && this.setStopAnimatingResponse(false);
@observable private _conversationArray: string[] = ['Hi! In this pop up, you can ask ChatGPT questions about your documents and filter / sort them. '];
@observable private _chatEnabled: boolean = false;
@action private setChatEnabled = (start: boolean) => (this._chatEnabled = start);
- @observable public Visible: boolean = false;
- @action public setVisible = (vis: boolean) => (this.Visible = vis);
@observable private _gptProcessing: boolean = false;
- @action public setGptProcessing = (loading: boolean) => (this._gptProcessing = loading);
+ @action private setGptProcessing = (loading: boolean) => (this._gptProcessing = loading);
@observable private _responseText: string = '';
- @action public setResponseText = (text: string) => (this._responseText = text);
+ @action private setResponseText = (text: string) => (this._responseText = text);
@observable private _imgUrls: string[][] = [];
- @action public setImgUrls = (imgs: string[][]) => (this._imgUrls = imgs);
- @observable private _mode: GPTPopupMode = GPTPopupMode.SUMMARY;
- @action public setMode = (mode: GPTPopupMode) => (this._mode = mode);
+ @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 = '';
@@ -91,23 +104,68 @@ export class GPTPopup extends ObservableReactComponent<object> {
@observable private _quizAnswer: string = '';
@action setQuizAnswer = (e: React.ChangeEvent<HTMLInputElement>) => (this._quizAnswer = e.target.value);
@observable private _stopAnimatingResponse: boolean = false;
- @action public setStopAnimatingResponse = (done: boolean) => (this._stopAnimatingResponse = done);
+ @action private setStopAnimatingResponse = (done: boolean) => (this._stopAnimatingResponse = done);
+
+ @observable private _mode: GPTPopupMode = GPTPopupMode.SUMMARY;
+ @action public setMode = (mode: GPTPopupMode) => (this._mode = mode);
+ @observable public Visible: boolean = false;
+ @action public setVisible = (vis: boolean) => (this.Visible = vis);
+
+ onQuizRandom?: () => void;
+ onGptResponse?: (sortResult: string, questionType: GPTTypeStyle, tag?: string) => void;
+ questionTypeNumberToStyle = (questionType: string) => +questionType.split(' ')[0][0];
/**
- * sets callback needed to retrieve an updated description of the collection's documents
- * @param collectionDoc - the collection doc context for the GPT prompts
- * @param callback - function to retrieve document descriptions
+ * 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
*/
- public setRetrieveDocDescriptionsCallback(collectionDoc: Doc | undefined, callback: null | (() => Promise<void>)) {
- this.setCollectionContext(collectionDoc);
- this._retrieveDocDescriptions = callback;
- }
+ processGptResponse = (docView: DocumentView, textToDocMap: Map<string, Doc>, gptOutput: string, questionType: GPTTypeStyle, tag?: string) =>
+ undoable(() => {
+ // Split the string into individual list items
+ const listItems = gptOutput.split('======').filter(item => item.trim() !== '');
- 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;
- public questionTypeNumberToStyle = (questionType: string) => +questionType.split(' ')[0][0];
+ if (questionType === GPTTypeStyle.Filter) {
+ docView.ComponentView?.hasChildDocs?.().forEach(d => TagItem.removeTagFromDoc(d, '#chat'));
+ }
+
+ if (questionType === GPTTypeStyle.SortDocs) {
+ docView.Document[docView.ComponentView?.fieldKey + '_sort'] = docSortings.Chat;
+ }
+ listItems.forEach((item, index) => {
+ const normalizedItem = item.replace(/\n/g, ' ').trim();
+ // find the corresponding Doc in the textToDoc map
+ const doc = textToDocMap.get(normalizedItem);
+ if (doc) {
+ switch (questionType) {
+ case GPTTypeStyle.SortDocs:
+ doc.chatIndex = index;
+ break;
+ case GPTTypeStyle.AssignTags:
+ if (tag) {
+ const hashTag = tag.startsWith('#') ? tag : '#' + tag[0].toLowerCase() + tag.slice(1);
+ const filterTag = Doc.MyFilterHotKeys.map(key => StrCast(key.toolType)).find(key => key.includes(tag)) ?? hashTag;
+ TagItem.addTagToDoc(doc, filterTag);
+ }
+ break;
+ case GPTTypeStyle.Filter:
+ TagItem.addTagToDoc(doc, '#chat');
+ Doc.setDocFilter(docView.Document, 'tags', '#chat', 'check');
+ break;
+ }
+ } else {
+ console.warn(`No matching document found for item: ${normalizedItem}`);
+ }
+ });
+ }, '')();
+
+ /**
+ * When in quiz mode, randomly selects a document
+ */
+ randomlyChooseDoc = (doc: Doc, childDocs: Doc[]) => DocumentView.getDocumentView(childDocs[Math.floor(Math.random() * childDocs.length)])?.select(false);
/**
* Generates a rubric for evaluating the user's description of the document's text
* @param doc the doc the user is providing info about
@@ -116,9 +174,11 @@ export class GPTPopup extends ObservableReactComponent<object> {
generateRubric = (doc: Doc) =>
StrCast(doc.gptRubric)
? Promise.resolve(StrCast(doc.gptRubric))
- : gptAPICall(StrCast(doc.gptInputText), GPTCallType.RUBRIC)
- .then(res => (doc.gptRubric = res))
- .catch(err => console.error('GPT call failed', err));
+ : Doc.getDescription(doc).then(desc =>
+ gptAPICall(desc, GPTCallType.RUBRIC)
+ .then(res => (doc.gptRubric = res))
+ .catch(err => console.error('GPT call failed', err))
+ );
/**
* When the cards are in quiz mode in the card view, allows gpt to determine whether the user's answer was correct
@@ -128,17 +188,18 @@ export class GPTPopup extends ObservableReactComponent<object> {
*/
generateQuizAnswerAnalysis = (doc: Doc, quizAnswer: string) =>
this.generateRubric(doc).then(() =>
- gptAPICall(
- `Question: ${StrCast(doc.gptInputText)};
- UserAnswer: ${quizAnswer};
- Rubric: ${StrCast(doc.gptRubric)}`,
- GPTCallType.QUIZ
- ).then(res => {
- this._conversationArray.push(res || 'GPT provided no answer');
- this.onQuizRandom?.();
- })
- .catch(err => console.error('GPT call failed', err))
- ) // prettier-ignore
+ Doc.getDescription(doc).then(desc =>
+ gptAPICall(
+ `Question: ${desc};
+ UserAnswer: ${quizAnswer};
+ Rubric: ${StrCast(doc.gptRubric)}`,
+ GPTCallType.QUIZ
+ ).then(res => {
+ this._conversationArray.push(res || 'GPT provided no answer');
+ this.onQuizRandom?.();
+ })
+ .catch(err => console.error('GPT call failed', err))
+ )) // prettier-ignore
/**
* Generates a response to the user's question about the docs in the collection.
@@ -146,32 +207,33 @@ export class GPTPopup extends ObservableReactComponent<object> {
* @param userPrompt the user's input that chat will respond to
*/
generateUserPromptResponse = (userPrompt: string) =>
- (this._retrieveDocDescriptions ?? Promise.resolve)().then(() =>
- gptAPICall(userPrompt, GPTCallType.TYPE).then(questionType =>
- (() => {
- switch (this.questionTypeNumberToStyle(questionType)) {
- case GPTTypeStyle.AssignTags:
- case GPTTypeStyle.Filter: return gptAPICall(this._documentDescriptions, GPTCallType.SUBSET, userPrompt);
- case GPTTypeStyle.SortDocs: return gptAPICall(this._documentDescriptions, GPTCallType.SORT, userPrompt);
- default: return gptAPICall(StrCast(DocumentView.SelectedDocs().lastElement()?.gptInputText), GPTCallType.INFO, userPrompt);
- } // prettier-ignore
- })().then(
- action(res => {
- // Trigger the callback with the result
- this.onGptResponse?.(res || 'Something went wrong :(', this.questionTypeNumberToStyle(questionType), questionType.split(' ').slice(1).join(' '));
- this._conversationArray.push(
- ![GPTTypeStyle.GeneralInfo, GPTTypeStyle.DocInfo].includes(this.questionTypeNumberToStyle(questionType))?
- // Extract explanation surrounded by ------ at the top or both at the top and bottom
- (res.match(/------\s*([\s\S]*?)\s*(?:------|$)/) ?? [])[1]?.trim() ?? 'No explanation found' : res);
- })
- ).catch(err => console.log(err))
+ gptAPICall(userPrompt, GPTCallType.TYPE).then(questionType =>
+ (async () => {
+ switch (this.questionTypeNumberToStyle(questionType)) {
+ case GPTTypeStyle.AssignTags:
+ case GPTTypeStyle.Filter: return this._documentDescriptions?.then(descs => gptAPICall(descs, GPTCallType.SUBSET, userPrompt)) ?? "";
+ case GPTTypeStyle.SortDocs: return this._documentDescriptions?.then(descs => gptAPICall(descs, GPTCallType.SORT, userPrompt)) ?? "";
+ default: return Doc.getDescription(DocumentView.SelectedDocs().lastElement()).then(desc => gptAPICall(desc, GPTCallType.INFO, userPrompt));
+ } // prettier-ignore
+ })().then(
+ action(res => {
+ // Trigger the callback with the result
+ this.onGptResponse?.(res || 'Something went wrong :(', this.questionTypeNumberToStyle(questionType), questionType.split(' ').slice(1).join(' '));
+ this._conversationArray.push(
+ [GPTTypeStyle.GeneralInfo, GPTTypeStyle.DocInfo].includes(this.questionTypeNumberToStyle(questionType)) ? 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))
- ); // prettier-ignore
+ ).catch(err => console.log(err)); // prettier-ignore
/**
* Generates a Dalle image and uploads it to the server.
*/
- generateImage = (imgDesc: string) => {
+ generateImage = (imgDesc: string, imgTarget: Doc, addToCollection?: (doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) => {
+ this._imgTargetDoc = imgTarget;
+ this.addDoc = addToCollection;
this.setImgUrls([]);
this.setMode(GPTPopupMode.IMAGE);
this.setVisible(true);
@@ -237,7 +299,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
_layout_fitWidth: true,
_layout_autoHeight: true,
});
- this.addDoc(newDoc, this._sidebarFieldKey);
+ this.addDoc?.(newDoc, this._sidebarFieldKey);
const anchor = AnchorMenu.Instance?.GetAnchor(undefined, false);
if (anchor) {
DocUtils.MakeLink(newDoc, anchor, {
@@ -270,7 +332,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
newDoc.overlayY = NumCast(textAnchor.y) + NumCast(textAnchor._height);
Doc.AddToMyOverlay(newDoc);
} else {
- this.addToCollection?.(newDoc);
+ this.addDoc?.(newDoc);
}
// Create link between prompt and image
DocUtils.MakeLink(textAnchor, newDoc, { link_relationship: 'Image Prompt' });
@@ -282,8 +344,8 @@ export class GPTPopup extends ObservableReactComponent<object> {
gptMenu = () => (
<div className="btns-wrapper-gpt">
<Button
- tooltip="Have ChatGPT sort, tag, define, or filter your documents for you!"
- text="Modify/Sort 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}
@@ -297,8 +359,8 @@ export class GPTPopup extends ObservableReactComponent<object> {
}}
/>
<Button
- tooltip="Test your knowledge with ChatGPT!"
- text="Quiz Cards!"
+ tooltip="Test your knowledge by verifying answers with ChatGPT"
+ text="Take Quiz"
onClick={() => {
this._conversationArray = ['Define the selected card!'];
this.setMode(GPTPopupMode.QUIZ_RESPONSE);
@@ -386,7 +448,14 @@ export class GPTPopup extends ObservableReactComponent<object> {
</div>
))}
</div>
- {this._gptProcessing ? null : <IconButton tooltip="Generate Again" onClick={() => this.generateImage(this._imageDescription)} 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>
);