diff options
Diffstat (limited to 'src/client/views')
| -rw-r--r-- | src/client/views/ViewBoxInterface.ts | 1 | ||||
| -rw-r--r-- | src/client/views/collections/CollectionCardDeckView.tsx | 157 | ||||
| -rw-r--r-- | src/client/views/collections/CollectionSubView.tsx | 1 | ||||
| -rw-r--r-- | src/client/views/collections/CollectionView.tsx | 4 | ||||
| -rw-r--r-- | src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts | 38 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 4 | ||||
| -rw-r--r-- | src/client/views/pdf/GPTPopup/GPTPopup.tsx | 231 |
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> ); |
