diff options
author | bobzel <zzzman@gmail.com> | 2025-03-14 12:20:41 -0400 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2025-03-14 12:20:41 -0400 |
commit | 45a9f5789fa6eaacca9a39cb96cc2a8e3ebe649c (patch) | |
tree | f4d098425f93dc63b9dcb3655bd6034930670e2a | |
parent | 4433afe837412c10a278e54ee8069cad992900a0 (diff) |
simplified anchorMenu to just have an AskAI button .. then updated GPtPopup's summaryBox to have a flashcard option.
-rw-r--r-- | src/client/views/StyleProviderQuiz.tsx | 31 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 41 | ||||
-rw-r--r-- | src/client/views/nodes/trails/PresElementBox.tsx | 5 | ||||
-rw-r--r-- | src/client/views/pdf/AnchorMenu.tsx | 26 | ||||
-rw-r--r-- | src/client/views/pdf/GPTPopup/GPTPopup.scss | 16 | ||||
-rw-r--r-- | src/client/views/pdf/GPTPopup/GPTPopup.tsx | 160 | ||||
-rw-r--r-- | src/client/views/pdf/PDFViewer.tsx | 21 |
7 files changed, 152 insertions, 148 deletions
diff --git a/src/client/views/StyleProviderQuiz.tsx b/src/client/views/StyleProviderQuiz.tsx index acda38dd7..f9ee8d3db 100644 --- a/src/client/views/StyleProviderQuiz.tsx +++ b/src/client/views/StyleProviderQuiz.tsx @@ -9,7 +9,7 @@ import { DocData } from '../../fields/DocSymbols'; import { List } from '../../fields/List'; import { NumCast, StrCast } from '../../fields/Types'; import { Networking } from '../Network'; -import { GPTCallType, gptAPICall, gptImageLabel } from '../apis/gpt/GPT'; +import { GPTCallType, gptAPICall } from '../apis/gpt/GPT'; import { Docs } from '../documents/Documents'; import { ContextMenu } from './ContextMenu'; import { ContextMenuProps } from './ContextMenuItem'; @@ -110,20 +110,21 @@ export namespace styleProviderQuiz { const blob = await ImageUtility.canvasToBlob(canvas); return selectUrlToBase64(blob); } - /** - * Create flashcards from an image. - */ - async function makeFlashcardsForImage(img: ImageBox) { - img.Loading = true; - try { - const hrefBase64 = await createCanvas(img); - const response = await gptImageLabel(hrefBase64, 'Make flashcards out of this image with each question and answer labeled as "question" and "answer". Do not label each flashcard and do not include asterisks: '); - AnchorMenu.Instance.transferToFlashcard(response, NumCast(img.layoutDoc.x), NumCast(img.layoutDoc.y)); - } catch (error) { - console.log('Error', error); - } - img.Loading = false; - } + + // /** + // * Create flashcards from an image. + // */ + // async function makeFlashcardsForImage(img: ImageBox) { + // img.Loading = true; + // try { + // const hrefBase64 = await createCanvas(img); + // const response = await gptImageLabel(hrefBase64, 'Make flashcards out of this image with each question and answer labeled as "question" and "answer". Do not label each flashcard and do not include asterisks: '); + // AnchorMenu.Instance.transferToFlashcard(response, NumCast(img.layoutDoc.x), NumCast(img.layoutDoc.y)); + // } catch (error) { + // console.log('Error', error); + // } + // img.Loading = false; + // } /** * Calls the createCanvas and pushInfo methods to convert the diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index b86536f86..7b24125e7 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -13,7 +13,7 @@ import { EditorState, NodeSelection, Plugin, Selection, TextSelection, Transacti import { EditorView, NodeViewConstructor } from 'prosemirror-view'; import * as React from 'react'; import { BsMarkdownFill } from 'react-icons/bs'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, imageUrlToBase64, returnFalse, returnZero, setupMoveUpEvents, simMouseEvent, smoothScroll, StopEvent } from '../../../../ClientUtils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, returnFalse, returnZero, setupMoveUpEvents, simMouseEvent, smoothScroll, StopEvent } from '../../../../ClientUtils'; import { DateField } from '../../../../fields/DateField'; import { CreateLinkToActiveAudio, Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc'; import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, DocData, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols'; @@ -26,7 +26,7 @@ import { ComputedField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, DateCast, DocCast, FieldValue, NumCast, RTFCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util'; import { emptyFunction, numberRange, unimplementedFunction, Utils } from '../../../../Utils'; -import { gptAPICall, GPTCallType, gptImageLabel } from '../../../apis/gpt/GPT'; +import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT'; import { DocServer } from '../../../DocServer'; import { Docs } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; @@ -226,18 +226,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return anchor; }; - selectionToFlashcards = async () => { - const queryText = window.getSelection()?.toString() ?? ''; - try { - if (queryText) { - const res = await gptAPICall(queryText, GPTCallType.FLASHCARD); - AnchorMenu.Instance.transferToFlashcard(res || 'Something went wrong', NumCast(this.layoutDoc.x), NumCast(this.layoutDoc.y)); - } - } catch (err) { - console.error(err); - } - }; - @action setupAnchorMenu = () => { AnchorMenu.Instance.Status = 'marquee'; @@ -1012,31 +1000,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB !help && cm.addItem({ description: 'Help...', subitems: helpItems, icon: 'eye' }); }; - findImageTags = async () => { - const c = this.ProseRef?.getElementsByTagName('img'); - if (c) { - for (const i of c) { - // console.log(canvas.toDataURL()); - // canvas.style.zIndex = '2000000'; - // document.body.appendChild(canvas); - if (i.className !== 'ProseMirror-separator') this.getImageDesc(i.src); - } - } - }; - - getImageDesc = async (u: string) => { - try { - const hrefBase64 = await imageUrlToBase64(u); - const response = await gptImageLabel( - hrefBase64, - 'Make flashcards out of this text and image with each question and answer labeled as question and answer. Do not label each flashcard and do not include asterisks: ' + (this.dataDoc.text as RichTextField)?.Text - ); - AnchorMenu.Instance.transferToFlashcard(response || 'Something went wrong', NumCast(this.dataDoc['x']), NumCast(this.dataDoc['y'])); - } catch (error) { - console.log('Error', error); - } - }; - animateRes = (resIndex: number, newText: string) => { if (resIndex < newText.length) { const marks = this.EditorView?.state.storedMarks ?? []; diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index 957df894c..31cd1603f 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -239,10 +239,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { e.clientY, undefined, action(() => { - Array.from(classesToRestore).forEach(pair => { - // eslint-disable-next-line prefer-destructuring - pair[0].className = pair[1]; - }); + Array.from(classesToRestore).forEach(pair => (pair[0].className = pair[1])); this._dragging = false; }) ); diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index 6ae659ac1..eb6516403 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -10,7 +10,6 @@ import { Doc, Opt } from '../../../fields/Doc'; import { SettingsManager } from '../../util/SettingsManager'; import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu'; import { LinkPopup } from '../linking/LinkPopup'; -import { ComparisonBox } from '../nodes/ComparisonBox'; import { DocumentView } from '../nodes/DocumentView'; import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; import './AnchorMenu.scss'; @@ -69,7 +68,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { public MakeTargetToggle: () => void = unimplementedFunction; public ShowTargetTrail: () => void = unimplementedFunction; public IsTargetToggler: () => boolean = returnFalse; - public gptFlashcards: () => void = unimplementedFunction; public makeLabels: () => void = unimplementedFunction; public marqueeWidth = 0; public marqueeHeight = 0; @@ -94,22 +92,9 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { * Invokes the API with the selected text and stores it in the summarized text. * @param e pointer down event */ - gptSummarize = () => GPTPopup.Instance.generateSummary(this._selectedText); - - /* - * Transfers the flashcard text generated by GPT on flashcards and creates a collection out them. - */ - - transferToFlashcard = (text: string, x: number, y: number) => { - ComparisonBox.createFlashcardDeck(text, 250, 200, 'data_front', 'data_back').then( - action(newCol => { - newCol.x = x; - newCol.y = y; - newCol.zIndex = 1000; - this.addToCollection?.(newCol); - this._loading = false; - }) - ); + gptAskAboutSelection = () => { + GPTPopup.Instance.askAIAboutSelection(this._selectedText); + AnchorMenu.Instance.fadeOut(true); }; pointerDown = (e: React.PointerEvent) => { @@ -188,14 +173,13 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { {/* GPT Summarize icon only shows up when text is highlighted, not on marquee selection */} {this._selectedText && ( <IconButton - tooltip="Summarize with AI" // - onPointerDown={this.gptSummarize} + tooltip="Ask AI..." // + onPointerDown={this.gptAskAboutSelection} icon={<FontAwesomeIcon icon="comment-dots" size="lg" />} color={SettingsManager.userColor} /> )} {/* Adds a create flashcards option to the anchor menu, which calls the gptFlashcard method. */} - {this.gptFlashcards === unimplementedFunction ? null : <IconButton tooltip="Create flashcards" onPointerDown={this.gptFlashcards} icon={<FontAwesomeIcon icon="layer-group" size="lg" />} color={SettingsManager.userColor} />} {this.makeLabels === unimplementedFunction ? null : <IconButton tooltip="Create labels" onPointerDown={this.makeLabels} icon={<FontAwesomeIcon icon="tag" size="lg" />} color={SettingsManager.userColor} />} {this._selectedText && RichTextMenu.Instance?.createLinkButton()} {AnchorMenu.Instance.OnAudio === unimplementedFunction ? null : ( diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.scss b/src/client/views/pdf/GPTPopup/GPTPopup.scss index c8903e09f..18441f76e 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.scss +++ b/src/client/views/pdf/GPTPopup/GPTPopup.scss @@ -15,12 +15,8 @@ $headingHeight: 32px; 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; @@ -29,11 +25,13 @@ $headingHeight: 32px; box-shadow: 0 2px 5px #7474748d; color: $textgrey; - .gptPopup-sortBox { + .gptPopup-summaryBox-content { + padding-right: 16px; + padding-left: 16px; + position: relative; + overflow: hidden; display: flex; flex-direction: column; - height: calc(100% - $inputHeight - $headingHeight); - pointer-events: all; } .summary-heading { @@ -65,7 +63,9 @@ $headingHeight: 32px; .gptPopup-content-wrapper { padding-top: 10px; min-height: 50px; - height: calc(100% - 32px); + white-space: pre-line; + overflow: auto; + margin-bottom: 10px; } .inputWrapper { diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx index 4dc45e6a0..67213382d 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx +++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx @@ -31,6 +31,7 @@ import { OpenWhere } from '../../nodes/OpenWhere'; import { DrawingFillHandler } from '../../smartdraw/DrawingFillHandler'; import { ImageField } from '../../../../fields/URLField'; import { List } from '../../../../fields/List'; +import { ComparisonBox } from '../../nodes/ComparisonBox'; export enum GPTPopupMode { SUMMARY, // summary of seleted document text @@ -56,7 +57,7 @@ export class GPTPopup extends ObservableReactComponent<object> { 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 _aiReferenceText: 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; @@ -79,7 +80,7 @@ export class GPTPopup extends ObservableReactComponent<object> { }; componentDidUpdate() { - this._gptProcessing && this.setStopAnimatingResponse(false); + //this._gptProcessing && this.setStopAnimatingResponse(false); } componentDidMount(): void { reaction( @@ -100,14 +101,14 @@ export class GPTPopup extends ObservableReactComponent<object> { ); } + @observable private _showOriginal = true; + @observable private _responseText: string = ''; @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; @@ -286,18 +287,30 @@ export class GPTPopup extends ObservableReactComponent<object> { /** * Completes an API call to generate a summary of the specified text * - * @param text the text to summarizz + * @param text the text to summarize */ - generateSummary = (text: string) => { + private generateSummary = action((text: string) => { SnappingManager.SetChatVisible(true); - this._textToSummarize = text; - this.setMode(GPTPopupMode.SUMMARY); + this._showOriginal = false; this.setGptProcessing(true); return gptAPICall(text, GPTCallType.SUMMARY) - .then(res => this.setResponseText(res || 'Something went wrong.')) + .then(action(res => (this._responseText = res || 'Something went wrong.'))) .catch(err => console.error(err)) .finally(() => this.setGptProcessing(false)); - }; + }); + + /** + * Completes an API call to generate a summary of the specified text + * + * @param text the text to summarizz + */ + askAIAboutSelection = action((text: string) => { + SnappingManager.SetChatVisible(true); + this._aiReferenceText = text; + this._responseText = ''; + this._showOriginal = true; + this.setMode(GPTPopupMode.SUMMARY); + }); /** * Completes an API call to generate an analysis of @@ -306,14 +319,16 @@ export class GPTPopup extends ObservableReactComponent<object> { 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.'); - }) + .then( + action(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._responseText = json[keys[2]] || 'Something went wrong.'; + }) + ) .catch(err => console.error(err)) .finally(() => this.setGptProcessing(false)); }; @@ -336,6 +351,24 @@ export class GPTPopup extends ObservableReactComponent<object> { }); } }; + /** + * Create Flashcards for the selected text + */ + private createFlashcards = action( + () => + this.setGptProcessing(true) && + gptAPICall(this._aiReferenceText, GPTCallType.FLASHCARD, undefined, true) + .then(res => + ComparisonBox.createFlashcardDeck(res, 250, 200, 'data_front', 'data_back').then( + action(newCol => { + newCol.zIndex = 1000; + DocumentViewInternal.addDocTabFunc(newCol, OpenWhere.addRight); + }) + ) + ) + .catch(console.error) + .finally(action(() => (this._gptProcessing = false))) + ); /** * Creates a histogram to show the correlation relationship that was found @@ -536,35 +569,80 @@ export class GPTPopup extends ObservableReactComponent<object> { summaryBox = () => ( <> - <div style={{ height: 'calc(100% - 60px)', overflow: 'auto' }}> - {this.heading('SUMMARY')} + <div className="gptPopup-summaryBox-content"> + <div onClick={action(() => (this._showOriginal = !this._showOriginal))}>{this.heading(this._showOriginal ? 'SELECTION' : 'SUMMARY')}</div> <div className="gptPopup-content-wrapper"> - {!this._gptProcessing && - (!this._stopAnimatingResponse ? ( - <TypeAnimation - speed={50} - sequence={[ - this._responseText, - () => { - setTimeout(() => this.setStopAnimatingResponse(true), 500); - }, - ]} - /> + {!this._gptProcessing && !this._stopAnimatingResponse && this._responseText ? ( + <TypeAnimation + speed={50} + sequence={[ + this._responseText, + () => { + setTimeout(() => this.setStopAnimatingResponse(true), 500); + }, + ]} + /> + ) : this._showOriginal ? ( + this._gptProcessing ? ( + '...generating cards...' ) : ( - this._responseText - ))} + this._aiReferenceText + ) + ) : ( + this._responseText || (this._gptProcessing ? '...generating summary...' : '-no ai summary-') + )} </div> </div> - {!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._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} /> - </> + {this._gptProcessing ? null : ( + <div className="btns-wrapper" style={{ position: 'relative', width: 'calc(100% - 32px)' }}> + {this._stopAnimatingResponse || !this._responseText ? ( + <div style={{ display: 'flex' }}> + {!this._showOriginal ? ( + <> + <Button + tooltip="Show originally selected text" // + text="Selection" + onClick={action(() => (this._showOriginal = true))} + color={StrCast(SettingsManager.userVariantColor)} + type={Type.TERT} + /> + <Button + tooltip="Create a text Doc with this text and link to the text selection" // + text="Transfer To Text" + onClick={this.transferToText} + color={StrCast(SettingsManager.userVariantColor)} + type={Type.TERT} + /> + </> + ) : ( + <> + <Button + tooltip="Show AI summary of original selection text (Shift+Click to regenerate)" + text="Summary" + onClick={action(e => { + if (e.shiftKey) { + this.setStopAnimatingResponse(false); + this._aiReferenceText += ' '; + this._responseText = ''; + } + this.generateSummary(this._aiReferenceText); + })} + color={StrCast(SettingsManager.userVariantColor)} + type={Type.TERT} + /> + <Button + tooltip="Create Flashcards" // + text="Create Flashcards" + onClick={this.createFlashcards} + color={StrCast(SettingsManager.userVariantColor)} + type={Type.TERT} + /> + </> + )} + </div> ) : ( <div className="summarizing"> - <span>Summarizing</span> + <span>{this._showOriginal ? 'Creating Cards...' : 'Summarizing'}</span> <ReactLoading type="bubbles" color="#bcbcbc" width={20} height={20} /> <Button text="Stop Animation" onClick={() => this.setStopAnimatingResponse(true)} color={StrCast(SettingsManager.userVariantColor)} type={Type.TERT} /> </div> diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index a50526223..83b3dffe5 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -28,7 +28,6 @@ import { AnchorMenu } from './AnchorMenu'; import { Annotation } from './Annotation'; import { GPTPopup } from './GPTPopup/GPTPopup'; import './PDFViewer.scss'; -import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT'; import ReactLoading from 'react-loading'; import { Transform } from '../../util/Transform'; @@ -392,23 +391,6 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { } }; - /** - * Create a flashcard pile based on the selected text of a pdf. - */ - gptPDFFlashcards = async () => { - const queryText = this._selectionText; - runInAction(() => (this._loading = true)); - try { - const res = await gptAPICall(queryText, GPTCallType.FLASHCARD); - - AnchorMenu.Instance.transferToFlashcard(res || 'Something went wrong', NumCast(this._props.layoutDoc['x']), NumCast(this._props.layoutDoc['y'])); - this._selectionText = ''; - } catch (err) { - console.error(err); - } - runInAction(() => (this._loading = false)); - }; - @action finishMarquee = (/* x?: number, y?: number */) => { AnchorMenu.Instance.makeLabels = unimplementedFunction; @@ -430,7 +412,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { if (sel) { AnchorMenu.Instance.setSelectedText(sel.toString()); - AnchorMenu.Instance.setLocation(NumCast(this._props.layoutDoc['x']), NumCast(this._props.layoutDoc['y'])); + AnchorMenu.Instance.setLocation(NumCast(this._props.layoutDoc.x), NumCast(this._props.layoutDoc.y)); } if (sel?.type === 'Range') { @@ -442,7 +424,6 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { GPTPopup.Instance.addDoc = this._props.sidebarAddDoc; // allows for creating collection AnchorMenu.Instance.addToCollection = this._props.DocumentView?.()._props.addDocument; - AnchorMenu.Instance.gptFlashcards = this.gptPDFFlashcards; AnchorMenu.Instance.makeLabels = unimplementedFunction; AnchorMenu.Instance.AddDrawingAnnotation = this.addDrawingAnnotation; }; |