diff options
Diffstat (limited to 'src/client/views/pdf')
| -rw-r--r-- | src/client/views/pdf/AnchorMenu.tsx | 77 | ||||
| -rw-r--r-- | src/client/views/pdf/Annotation.tsx | 7 | ||||
| -rw-r--r-- | src/client/views/pdf/GPTPopup/GPTPopup.scss | 20 | ||||
| -rw-r--r-- | src/client/views/pdf/GPTPopup/GPTPopup.tsx | 200 | ||||
| -rw-r--r-- | src/client/views/pdf/PDFViewer.scss | 4 | ||||
| -rw-r--r-- | src/client/views/pdf/PDFViewer.tsx | 84 |
6 files changed, 203 insertions, 189 deletions
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index 9aa8fe649..eb6516403 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -1,24 +1,19 @@ import { ColorPicker, Group, IconButton, Popup, Size, Toggle, ToggleType, Type } from '@dash/components'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; +import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { ColorResult } from 'react-color'; -import ReactLoading from 'react-loading'; import { ClientUtils, returnFalse, setupMoveUpEvents } from '../../../ClientUtils'; import { emptyFunction, unimplementedFunction } from '../../../Utils'; import { Doc, Opt } from '../../../fields/Doc'; -import { DocData } from '../../../fields/DocSymbols'; import { SettingsManager } from '../../util/SettingsManager'; -import { undoBatch } from '../../util/UndoManager'; import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu'; import { LinkPopup } from '../linking/LinkPopup'; -import { ComparisonBox } from '../nodes/ComparisonBox'; import { DocumentView } from '../nodes/DocumentView'; -import { DrawingOptions, SmartDrawHandler } from '../smartdraw/SmartDrawHandler'; +import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; import './AnchorMenu.scss'; import { GPTPopup } from './GPTPopup/GPTPopup'; -import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; @observer export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { @@ -73,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; @@ -98,57 +92,11 @@ 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; - }) - ); - }; - - /** - * Creates a GPT drawing based on selected text. - */ - gptDraw = (e: React.PointerEvent) => { - try { - SmartDrawHandler.Instance.AddDrawing = this.createDrawingAnnotation; - runInAction(() => (this._isLoading = true)); - SmartDrawHandler.Instance.drawWithGPT({ X: e.clientX, Y: e.clientY }, this._selectedText, 5, 100, true)?.then( - action(() => { - this._isLoading = false; - }) - ); - } catch (err) { - console.error(err); - } + gptAskAboutSelection = () => { + GPTPopup.Instance.askAIAboutSelection(this._selectedText); + AnchorMenu.Instance.fadeOut(true); }; - /** - * Defines how a GPT drawing should be added to the current document. - */ - @undoBatch - createDrawingAnnotation = action((drawing: Doc, opts: DrawingOptions, gptRes: string) => { - this.AddDrawingAnnotation(drawing); - const docData = drawing[DocData]; - docData.title = opts.text?.match(/^(.*?)~~~.*$/)?.[1] || opts.text; - docData.ai_drawing_input = opts.text; - docData.ai_drawing_complexity = opts.complexity; - docData.ai_drawing_colored = opts.autoColor; - docData.ai_drawing_size = opts.size; - docData.ai_drawing_data = gptRes; - docData.ai = 'gpt'; - }); - pointerDown = (e: React.PointerEvent) => { setupMoveUpEvents( this, @@ -225,23 +173,14 @@ 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. */} - <IconButton tooltip="Create flashcards" onPointerDown={this.gptFlashcards} icon={<FontAwesomeIcon icon="layer-group" size="lg" />} color={SettingsManager.userColor} /> - <IconButton tooltip="Create labels" onPointerDown={this.makeLabels} icon={<FontAwesomeIcon icon="tag" size="lg" />} color={SettingsManager.userColor} /> - {this._selectedText && ( - <IconButton - tooltip="Create drawing" - onPointerDown={e => this.gptDraw(e)} - icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <FontAwesomeIcon icon="paintbrush" 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 : ( <IconButton diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index 1891cfd4c..e8a5235c9 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -50,7 +50,7 @@ interface IAnnotationProps extends FieldViewProps { } @observer export class Annotation extends ObservableReactComponent<IAnnotationProps> { - constructor(props: any) { + constructor(props: IAnnotationProps) { super(props); makeObservable(this); } @@ -58,7 +58,7 @@ export class Annotation extends ObservableReactComponent<IAnnotationProps> { @computed get linkHighlighted() { const found = LinkManager.Instance.getAllDirectLinks(this._props.annoDoc).find(link => { const a1 = Doc.getOppositeAnchor(link, this._props.annoDoc); - return a1 && Doc.GetBrushStatus(DocCast(a1.annotationOn, a1)); + return a1 && Doc.GetBrushStatus(DocCast(a1.annotationOn, a1)!); }); return found; } @@ -112,6 +112,7 @@ export class Annotation extends ObservableReactComponent<IAnnotationProps> { outline = () => (this.linkHighlighted ? 'solid 1px lightBlue' : undefined); background = () => (this._props.annoDoc[Highlight] ? 'orange' : StrCast(this._props.annoDoc.backgroundColor)); render() { + // eslint-disable-next-line @typescript-eslint/no-unused-vars const forceRenderHack = [this.background(), this.outline(), this.opacity()]; // forces a re-render when these change -- because RegionAnnotation doesn't do this internally.. return ( <div style={{ display: this._props.annoDoc.textCopied && !Doc.GetBrushHighlightStatus(this._props.annoDoc) ? 'none' : undefined }}> @@ -121,7 +122,7 @@ export class Annotation extends ObservableReactComponent<IAnnotationProps> { .map(([x, y, width, height]) => ( <div key={'' + x + y + width + height} - style={{ pointerEvents: this._props.pointerEvents?.() as any }} + style={{ pointerEvents: this._props.pointerEvents?.() as Property.PointerEvents }} onPointerDown={this.onPointerDown} onContextMenu={this.onContextMenu} onPointerEnter={() => { diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.scss b/src/client/views/pdf/GPTPopup/GPTPopup.scss index c8903e09f..bb43291ee 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.scss +++ b/src/client/views/pdf/GPTPopup/GPTPopup.scss @@ -9,18 +9,16 @@ $headingHeight: 32px; .gptPopup-summary-box { position: fixed; + padding-left: 10px; + padding-right: 10px; top: 115px; left: 75px; width: 100%; height: 100%; top: 0; left: 0; - pointer-events: none; border-top: solid gray 20px; border-radius: 16px; - padding: 16px; - padding-bottom: 0; - padding-top: 0px; z-index: 999; display: flex; flex-direction: column; @@ -29,11 +27,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 { @@ -50,7 +50,6 @@ $headingHeight: 32px; } label { - color: $textgrey; font-size: 12px; font-weight: 400; letter-spacing: 1px; @@ -65,7 +64,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 { @@ -73,7 +74,6 @@ $headingHeight: 32px; justify-content: center; align-items: center; height: $inputHeight; - background-color: white; width: 100%; pointer-events: all; diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx index 4dc45e6a0..c45d8e052 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; @@ -209,7 +210,7 @@ export class GPTPopup extends ObservableReactComponent<object> { generateFireflyImage = (imgDesc: string) => { const selView = DocumentView.Selected().lastElement(); const selDoc = selView?.Document; - if (selDoc && (selView._props.renderDepth > 1 || selDoc[Doc.LayoutFieldKey(selDoc)] instanceof ImageField)) { + if (selDoc && (selView._props.renderDepth > 1 || selDoc[Doc.LayoutDataKey(selDoc)] instanceof ImageField)) { const oldPrompt = StrCast(selDoc.ai_firefly_prompt, StrCast(selDoc.title)); const newPrompt = oldPrompt ? `${oldPrompt} ~~~ ${imgDesc}` : imgDesc; return DrawingFillHandler.drawingToImage(selDoc, 100, newPrompt, selDoc) @@ -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 @@ -378,7 +411,8 @@ export class GPTPopup extends ObservableReactComponent<object> { tooltip="Ask Firefly to create images" text="Ask Firefly" onClick={() => this.setMode(GPTPopupMode.FIREFLY)} - color={StrCast(Doc.UserDoc().userVariantColor)} + color={SettingsManager.userColor} + background={SettingsManager.userVariantColor} type={Type.TERT} style={{ width: '100%', @@ -393,7 +427,8 @@ export class GPTPopup extends ObservableReactComponent<object> { 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)} + color={SettingsManager.userColor} + background={SettingsManager.userVariantColor} type={Type.TERT} style={{ width: '100%', @@ -412,7 +447,8 @@ export class GPTPopup extends ObservableReactComponent<object> { this.setMode(GPTPopupMode.QUIZ_RESPONSE); this.onQuizRandom?.(); }} - color={StrCast(Doc.UserDoc().userVariantColor)} + color={SettingsManager.userColor} + background={SettingsManager.userVariantColor} type={Type.TERT} style={{ width: '100%', @@ -484,6 +520,7 @@ export class GPTPopup extends ObservableReactComponent<object> { onChange={e => onChange(e.target.value)} onKeyDown={e => this.handleKeyPress(e, this._mode)} type="text" + style={{ color: SnappingManager.userColor }} placeholder={placeholder} /> <Button // @@ -491,7 +528,8 @@ export class GPTPopup extends ObservableReactComponent<object> { type={Type.TERT} icon={<AiOutlineSend />} iconPlacement="right" - color={SnappingManager.userVariantColor} + color={SettingsManager.userColor} + background={SettingsManager.userVariantColor} onClick={() => this.callGpt(this._mode)} /> <DictationButton ref={r => (this._askDictation = r)} setInput={onChange} /> @@ -518,7 +556,7 @@ export class GPTPopup extends ObservableReactComponent<object> { </div> </div> <div key={rawSrc[0] + i + 'btn'} className="btn-container"> - <Button text="Save Image" onClick={() => this.transferToImage(rawSrc[1])} color={StrCast(Doc.UserDoc().userColor)} type={Type.TERT} /> + <Button text="Save Image" onClick={() => this.transferToImage(rawSrc[1])} color={SettingsManager.userColor} background={SettingsManager.userVariantColor} type={Type.TERT} /> </div> </> ))} @@ -528,7 +566,8 @@ export class GPTPopup extends ObservableReactComponent<object> { 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)} + color={SettingsManager.userColor} + background={SettingsManager.userVariantColor} /> )} </div> @@ -536,37 +575,86 @@ 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={SettingsManager.userColor} + background={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={SettingsManager.userColor} + background={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={SettingsManager.userColor} + background={SettingsManager.userVariantColor} + type={Type.TERT} + /> + <Button + tooltip="Create Flashcards" // + text="Create Flashcards" + onClick={this.createFlashcards} + color={SettingsManager.userColor} + background={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} /> + <Button text="Stop Animation" onClick={() => this.setStopAnimatingResponse(true)} color={SettingsManager.userColor} background={SettingsManager.userVariantColor} type={Type.TERT} /> </div> )} </div> @@ -611,19 +699,19 @@ export class GPTPopup extends ObservableReactComponent<object> { placeholder="Ask GPT a question about the data..." id="search-input" className="searchBox-input" - style={{ width: '100%' }} + style={{ width: '100%', color: SnappingManager.userColor }} /> ) : ( <> - <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(SnappingManager.userVariantColor)} type={Type.TERT} /> - <Button tooltip="Chat with AI" text="Chat with AI" onClick={() => this.setChatEnabled(true)} color={StrCast(SnappingManager.userVariantColor)} type={Type.TERT} /> + <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={SettingsManager.userColor} background={SettingsManager.userVariantColor} type={Type.TERT} /> + <Button tooltip="Chat with AI" text="Chat with AI" onClick={() => this.setChatEnabled(true)} color={SettingsManager.userColor} background={SettingsManager.userVariantColor} type={Type.TERT} /> </> ) ) : ( <div className="summarizing"> <span>Summarizing</span> <ReactLoading type="bubbles" color="#bcbcbc" width={20} height={20} /> - <Button text="Stop Animation" onClick={() => this.setStopAnimatingResponse(true)} color={StrCast(SnappingManager.userVariantColor)} type={Type.TERT} /> + <Button text="Stop Animation" onClick={() => this.setStopAnimatingResponse(true)} color={SettingsManager.userColor} background={SettingsManager.userVariantColor} type={Type.TERT} /> </div> )} </div> @@ -640,7 +728,7 @@ export class GPTPopup extends ObservableReactComponent<object> { ); heading = (headingText: string) => ( - <div className="summary-heading"> + <div className="summary-heading" style={{ color: SnappingManager.userBackgroundColor }}> <label className="summary-text">{headingText}</label> {this._gptProcessing ? ( <ReactLoading type="spin" color="#bcbcbc" width={14} height={14} /> @@ -656,7 +744,7 @@ export class GPTPopup extends ObservableReactComponent<object> { onClick={() => this._collectionContext && Doc.setDocFilter(this._collectionContext, 'tags', GPTPopup.ChatTag, 'remove')} /> {[GPTPopupMode.USER_PROMPT, GPTPopupMode.QUIZ_RESPONSE, GPTPopupMode.FIREFLY].includes(this._mode) && ( - <IconButton color={StrCast(SettingsManager.userVariantColor)} tooltip="back" icon={<CgCornerUpLeft size="16px" />} onClick={() => (this._mode = GPTPopupMode.GPT_MENU)} /> + <IconButton color={SettingsManager.userVariantColor} background={SettingsManager.userColor} tooltip="back" icon={<CgCornerUpLeft size="16px" />} onClick={() => (this._mode = GPTPopupMode.GPT_MENU)} /> )} </> )} @@ -665,7 +753,7 @@ export class GPTPopup extends ObservableReactComponent<object> { render() { return ( - <div className="gptPopup-summary-box" style={{ display: SnappingManager.ChatVisible ? 'flex' : 'none', overflow: 'auto' }}> + <div className="gptPopup-summary-box" style={{ background: SnappingManager.userColor, color: SnappingManager.userBackgroundColor, display: SnappingManager.ChatVisible ? 'flex' : 'none', overflow: 'auto' }}> {(() => { //prettier-ignore switch (this._mode) { diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss index 030251762..1aab2b853 100644 --- a/src/client/views/pdf/PDFViewer.scss +++ b/src/client/views/pdf/PDFViewer.scss @@ -29,7 +29,11 @@ .textLayer { opacity: unset; mix-blend-mode: multiply; // bcz: makes text fuzzy! + // all these below fix issues with PDFjs transform: scale(var(--devicePixelRatio)); + --total-scale-factor: var(--scale-factor); // these 3 are used by PDFjs but not defined. why??? + --scale-round-x: 1px; + --scale-round-y: 1px; } [data-main-rotation='90'] { transform: scale(var(--devicePixelRatio)) rotate(90deg) translateY(-100%); diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 167421a4a..fc2567fbc 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -1,9 +1,10 @@ import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as Pdfjs from 'pdfjs-dist'; +import { GlobalWorkerOptions } from 'pdfjs-dist/build/pdf.mjs'; import * as PDFJSViewer from 'pdfjs-dist/web/pdf_viewer.mjs'; -import 'pdfjs-dist/webpack.mjs'; // sets the PDF workerSrc import * as React from 'react'; +import ReactLoading from 'react-loading'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, returnAll, returnFalse, returnNone, returnZero, smoothScroll } from '../../../ClientUtils'; import { CreateLinkToActiveAudio, Doc, DocListCast, Opt } from '../../../fields/Doc'; import { DocData, Height } from '../../../fields/DocSymbols'; @@ -11,9 +12,10 @@ import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, numberRange } from '../../../Utils'; +import { emptyFunction, numberRange, unimplementedFunction } from '../../../Utils'; import { DocUtils } from '../../documents/DocUtils'; import { SnappingManager } from '../../util/SnappingManager'; +import { Transform } from '../../util/Transform'; import { MarqueeOptionsMenu } from '../collections/collectionFreeForm'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { MarqueeAnnotator } from '../MarqueeAnnotator'; @@ -28,13 +30,13 @@ 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'; +import { DocumentViewProps } from '../nodes/DocumentContentsView'; +if (window?.Worker) GlobalWorkerOptions.workerSrc = 'files/node_modules/pdfjs-dist/build/pdf.worker.min.mjs'; // npm start/etc use copyfiles to copy the worker from the pdfjs-dist package to the public folder +export * from 'pdfjs-dist/build/pdf.mjs'; interface IViewerProps extends FieldViewProps { pdfBox: PDFBox; - Document: Doc; + Doc: Doc; dataDoc: Doc; layoutDoc: Doc; fieldKey: string; @@ -53,7 +55,7 @@ interface IViewerProps extends FieldViewProps { */ @observer export class PDFViewer extends ObservableReactComponent<IViewerProps> { - static _annotationStyle = addStyleSheet(); + static _annotationStyle = addStyleSheet().sheet; constructor(props: IViewerProps) { super(props); @@ -112,8 +114,8 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { () => this._props.layoutDoc._layout_autoHeight, layoutAutoHeight => { if (layoutAutoHeight) { - this._props.layoutDoc._nativeHeight = NumCast(this._props.Document[this._props.fieldKey + '_nativeHeight']); - this._props.setHeight?.(NumCast(this._props.Document[this._props.fieldKey + '_nativeHeight']) * (this._props.NativeDimScaling?.() || 1)); + this._props.layoutDoc._nativeHeight = NumCast(this._props.Doc[this._props.fieldKey + '_nativeHeight']); + this._props.setHeight?.(NumCast(this._props.Doc[this._props.fieldKey + '_nativeHeight']) * (this._props.NativeDimScaling?.() || 1)); } } ); @@ -124,7 +126,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { { fireImmediately: true } ); this._disposers.curPage = reaction( - () => Cast(this._props.Document._layout_curPage, 'number', null), + () => Cast(this._props.Doc._layout_curPage, 'number', null), page => page !== undefined && page !== this._pdfViewer?.currentPageNumber && this.gotoPage(page), { fireImmediately: true } ); @@ -132,7 +134,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { componentWillUnmount = () => { Object.values(this._disposers).forEach(disposer => disposer?.()); - document.removeEventListener('copy', this.copy); + document.removeEventListener('copy', this.copy, true); }; copy = (e: ClipboardEvent) => { @@ -144,6 +146,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { e.clipboardData.setData('dash/pdfAnchor', anchor[DocData][Id]); } e.preventDefault(); + e.stopPropagation(); } }; @@ -180,7 +183,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { scrollFocus = (doc: Doc, scrollTop: number, options: FocusViewOptions) => { const mainCont = this._mainCont.current; let focusSpeed: Opt<number>; - if (doc !== this._props.Document && mainCont) { + if (doc !== this._props.Doc && mainCont) { const windowHeight = this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1); const scrollTo = ClientUtils.scrollIntoView(scrollTop, doc[Height](), NumCast(this._props.layoutDoc._layout_scrollTop), windowHeight, windowHeight * 0.1, this._scrollHeight); if (scrollTo !== undefined && scrollTo !== this._props.layoutDoc._layout_scrollTop) { @@ -215,11 +218,11 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { { fireImmediately: true } ); this._disposers.scroll = reaction( - () => Math.abs(NumCast(this._props.Document._layout_scrollTop)), + () => Math.abs(NumCast(this._props.Doc._layout_scrollTop)), pos => { if (!this._ignoreScroll) { this._showWaiting && this.setupPdfJsViewer(); - const viewTrans = quickScroll?.loc ?? StrCast(this._props.Document._viewTransition); + const viewTrans = quickScroll?.loc ?? StrCast(this._props.Doc._viewTransition); const durationMiliStr = viewTrans.match(/([0-9]*)ms/); const durationSecStr = viewTrans.match(/([0-9.]*)s/); const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0; @@ -260,16 +263,11 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { } return; } - document.removeEventListener('copy', this.copy); - document.addEventListener('copy', this.copy); + document.removeEventListener('copy', this.copy, true); + document.addEventListener('copy', this.copy, true); const eventBus = new PDFJSViewer.EventBus(); eventBus._on('pagesinit', this.pagesinit); - eventBus._on( - 'pagerendered', - action(() => { - this._showWaiting = false; - }) - ); + eventBus._on('pagerendered',action(() => (this._showWaiting = false))); // prettier-ignore const pdfLinkService = new PDFJSViewer.PDFLinkService({ eventBus }); const pdfFindController = new PDFJSViewer.PDFFindController({ linkService: pdfLinkService, eventBus }); this._pdfViewer = new PDFJSViewer.PDFViewer({ @@ -370,9 +368,9 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { // if alt+left click, drag and annotate this._downX = e.clientX; this._downY = e.clientY; - if ((this._props.Document._freeform_scale || 1) !== 1) return; + if ((this._props.Doc._freeform_scale || 1) !== 1) return; if ((e.button !== 0 || e.altKey) && this._props.isContentActive()) { - this._setPreviewCursor?.(e.clientX, e.clientY, true, false, this._props.Document); + this._setPreviewCursor?.(e.clientX, e.clientY, true, false, this._props.Doc); } if (!e.altKey && e.button === 0 && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) { this._props.select(false); @@ -392,25 +390,9 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { } }; - /** - * Create a flashcard pile based on the selected text of a pdf. - */ - gptPDFFlashcards = async () => { - const queryText = this._selectionText; - 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); - } - this._loading = false; - }; - @action finishMarquee = (/* x?: number, y?: number */) => { + AnchorMenu.Instance.makeLabels = unimplementedFunction; this._getAnchor = AnchorMenu.Instance?.GetAnchor; this.isAnnotating = false; this._marqueeref.current?.onTerminateSelection(); @@ -429,7 +411,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') { @@ -441,14 +423,14 @@ 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; }; addDrawingAnnotation = (drawing: Doc) => { - // drawing[DocData].x = this._props.pdfBox.ScreenToLocalBoxXf().TranslateX + // drawing.x = this._props.pdfBox.ScreenToLocalBoxXf().TranslateX // const scaleX = this._mainCont.current.offsetWidth / boundingRect.width; - drawing.y = NumCast(drawing.y) + NumCast(this._props.Document.layout_scrollTop); + drawing.y = NumCast(drawing.y) + NumCast(this._props.Doc.layout_scrollTop); this._props.addDocument?.(drawing); }; @@ -493,7 +475,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { onClick = (e: React.MouseEvent) => { this._scrollStopper?.(); if (this._setPreviewCursor && e.button === 0 && Math.abs(e.clientX - this._downX) < ClientUtils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < ClientUtils.DRAG_THRESHOLD) { - this._setPreviewCursor(e.clientX, e.clientY, false, false, this._props.Document); + this._setPreviewCursor(e.clientX, e.clientY, false, false, this._props.Doc); } // e.stopPropagation(); // bcz: not sure why this was here. We need to allow the DocumentView to get clicks to process doubleClicks }; @@ -521,7 +503,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { @computed get annotationLayer() { const inlineAnnos = this.inlineTextAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).filter(anno => !anno.hidden); return ( - <div className="pdfViewerDash-annotationLayer" style={{ height: Doc.NativeHeight(this._props.Document), transform: `scale(${NumCast(this._props.layoutDoc._freeform_scale, 1)})` }} ref={this._annotationLayer}> + <div className="pdfViewerDash-annotationLayer" style={{ height: Doc.NativeHeight(this._props.Doc), transform: `scale(${NumCast(this._props.layoutDoc._freeform_scale, 1)})` }} ref={this._annotationLayer}> {inlineAnnos.map(anno => ( <Annotation {...this._props} fieldKey={this._props.fieldKey + '_annotations'} pointerEvents={this.pointerEvents} containerDataDoc={this._props.dataDoc} annoDoc={anno} key={`${anno[Id]}-annotation`} /> ))} @@ -536,7 +518,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { panelHeight = () => this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1); transparentFilter = () => [...this._props.childFilters(), ClientUtils.TransparentBackgroundFilter]; opaqueFilter = () => [...this._props.childFilters(), ClientUtils.noDragDocsFilter, ...(SnappingManager.CanEmbed && this._props.isContentActive() ? [] : [ClientUtils.OpaqueBackgroundFilter])]; - childStyleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string) => { + childStyleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps & DocumentViewProps>, property: string) => { if (doc instanceof Doc && property === StyleProp.PointerEvents) { if (this.inlineTextAnnotations.includes(doc) || this._props.isContentActive() === false) return 'none'; const isInk = doc.layout_isSvg && !props?.LayoutTemplateString; @@ -612,7 +594,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { onClick={this.onClick} style={{ overflowX: NumCast(this._props.layoutDoc._freeform_scale, 1) !== 1 ? 'scroll' : undefined, - height: !this._props.Document._layout_fitWidth && window.screen.width > 600 ? Doc.NativeHeight(this._props.Document) : `100%`, + height: !this._props.Doc._layout_fitWidth && window.screen.width > 600 ? Doc.NativeHeight(this._props.Doc) : `100%`, }}> {this.pdfViewerDiv} {this.annotationLayer} @@ -621,12 +603,12 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { {!this._mainCont.current || !this._annotationLayer.current || !this.props.pdfBox.DocumentView ? null : ( <MarqueeAnnotator ref={this._marqueeref} - Document={this._props.Document} + Document={this._props.Doc} getPageFromScroll={this.getPageFromScroll} anchorMenuClick={this._props.anchorMenuClick} scrollTop={0} annotationLayerScaling={() => Pdfjs.PixelsPerInch.PDF_TO_CSS_UNITS} - annotationLayerScrollTop={NumCast(this._props.Document._layout_scrollTop)} + annotationLayerScrollTop={NumCast(this._props.Doc._layout_scrollTop)} addDocument={this.addDocumentWrapper} docView={this.props.pdfBox.DocumentView} screenTransform={this.screenToMarqueeXf} |
