diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/util/request-image-size.ts | 1 | ||||
-rw-r--r-- | src/client/views/global/globalScripts.ts | 18 | ||||
-rw-r--r-- | src/client/views/nodes/ChatBox/MessageComponent.scss | 10 | ||||
-rw-r--r-- | src/client/views/nodes/ChatBox/MessageComponent.tsx | 110 | ||||
-rw-r--r-- | src/client/views/nodes/ComparisonBox.scss | 6 | ||||
-rw-r--r-- | src/client/views/nodes/ComparisonBox.tsx | 172 | ||||
-rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 5 | ||||
-rw-r--r-- | src/client/views/nodes/PDFBox.tsx | 8 | ||||
-rw-r--r-- | src/client/views/pdf/PDFViewer.tsx | 4 | ||||
-rw-r--r-- | src/server/# Server Architecture.md | 87 | ||||
-rw-r--r-- | src/server/ApiManagers/AssistantManager.ts | 2 |
11 files changed, 233 insertions, 190 deletions
diff --git a/src/client/util/request-image-size.ts b/src/client/util/request-image-size.ts index 0f98a2710..48cb6e3a5 100644 --- a/src/client/util/request-image-size.ts +++ b/src/client/util/request-image-size.ts @@ -54,6 +54,7 @@ module.exports = function requestImageSize(options: any) { } } catch (err) { /* empty */ + console.log("Error: ", err) } }); diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index 7730ed385..a985986d6 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -450,15 +450,19 @@ function setActiveTool(tool: InkTool | Gestures, keepPrim: boolean, checkResult? GestureOverlay.Instance.InkShape = tool as Gestures; } } else if (tool) { - if ([InkTool.StrokeEraser, InkTool.RadiusEraser, InkTool.SegmentEraser].includes(tool as any)) { - Doc.UserDoc().activeEraserTool = tool; - } - // pen or eraser - if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape && !keepPrim) { + if (Doc.UserDoc().ActiveTool === tool) { Doc.ActiveTool = InkTool.None; } else { - Doc.ActiveTool = tool as any; - GestureOverlay.Instance.InkShape = undefined; + if ([InkTool.StrokeEraser, InkTool.RadiusEraser, InkTool.SegmentEraser].includes(tool as any)) { + Doc.UserDoc().activeEraserTool = tool; + } + // pen or eraser + if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape && !keepPrim) { + Doc.ActiveTool = InkTool.None; + } else { + Doc.ActiveTool = tool as any; + GestureOverlay.Instance.InkShape = undefined; + } } } else { Doc.ActiveTool = InkTool.None; diff --git a/src/client/views/nodes/ChatBox/MessageComponent.scss b/src/client/views/nodes/ChatBox/MessageComponent.scss new file mode 100644 index 000000000..6fcc0e5e7 --- /dev/null +++ b/src/client/views/nodes/ChatBox/MessageComponent.scss @@ -0,0 +1,10 @@ +MessageComponent-citation { + color: lightblue; + vertical-align: super; + font-size: smaller; +} +MessageComponent-file_path { + color: lightblue; + vertical-align: baseline; + font-size: inherit; +} diff --git a/src/client/views/nodes/ChatBox/MessageComponent.tsx b/src/client/views/nodes/ChatBox/MessageComponent.tsx index fced0b4d5..f27a18891 100644 --- a/src/client/views/nodes/ChatBox/MessageComponent.tsx +++ b/src/client/views/nodes/ChatBox/MessageComponent.tsx @@ -1,11 +1,25 @@ +/* eslint-disable jsx-a11y/control-has-associated-label */ /* eslint-disable react/require-default-props */ -import React from 'react'; -import { observer } from 'mobx-react'; import { MathJax, MathJaxContext } from 'better-react-mathjax'; +import { observer } from 'mobx-react'; +import React from 'react'; +import * as Tb from 'react-icons/tb'; import ReactMarkdown from 'react-markdown'; -import { TbCircle0Filled, TbCircle1Filled, TbCircle2Filled, TbCircle3Filled, TbCircle4Filled, TbCircle5Filled, TbCircle6Filled, TbCircle7Filled, TbCircle8Filled, TbCircle9Filled } from 'react-icons/tb'; +import './MessageComponent.scss'; import { AssistantMessage } from './types'; +const TbCircles = [ + Tb.TbCircleNumber0Filled, + Tb.TbCircleNumber1Filled, + Tb.TbCircleNumber2Filled, + Tb.TbCircleNumber3Filled, + Tb.TbCircleNumber4Filled, + Tb.TbCircleNumber5Filled, + Tb.TbCircleNumber6Filled, + Tb.TbCircleNumber7Filled, + Tb.TbCircleNumber8Filled, + Tb.TbCircleNumber9Filled, +]; interface MessageComponentProps { message: AssistantMessage; toggleToolLogs: (index: number) => void; @@ -17,89 +31,41 @@ interface MessageComponentProps { isCurrent?: boolean; } -const MessageComponent: React.FC<MessageComponentProps> = function ({ message, toggleToolLogs, expandedLogIndex, goToLinkedDoc, index, showModal, setCurrentFile, isCurrent = false }) { - // const messageClass = `${message.role} ${isCurrent ? 'current-message' : ''}`; - - const LinkRenderer = ({ href, children }: { href: string; children: React.ReactNode }) => { - // console.log(href + " " + children) - const regex = /([a-zA-Z0-9_.!-]+)~~~(citation|file_path)/; - const matches = href.match(regex); - // console.log(href) - // console.log(matches) - const url = matches ? matches[1] : href; - const linkType = matches ? matches[2] : null; - if (linkType === 'citation') { - switch (children) { - case '0': - children = <TbCircle0Filled />; - break; - case '1': - children = <TbCircle1Filled />; - break; - case '2': - children = <TbCircle2Filled />; - break; - case '3': - children = <TbCircle3Filled />; - break; - case '4': - children = <TbCircle4Filled />; - break; - case '5': - children = <TbCircle5Filled />; - break; - case '6': - children = <TbCircle6Filled />; - break; - case '7': - children = <TbCircle7Filled />; - break; - case '8': - children = <TbCircle8Filled />; - break; - case '9': - children = <TbCircle9Filled />; - break; - default: - break; - } - } - // console.log(linkType) - const style = { - color: 'lightblue', - verticalAlign: linkType === 'citation' ? 'super' : 'baseline', - fontSize: linkType === 'citation' ? 'smaller' : 'inherit', - }; - - return ( - <a +const LinkRendererWrapper = (goToLinkedDoc: (url: string) => void, showModal: () => void, setCurrentFile: (file: { url: string }) => void) => + function LinkRenderer({ href, children }: { href?: string; children?: React.ReactNode }) { + const Children = TbCircles[Number(children)]; // pascal case variable needed to convert IconType to JSX.Element tag + const [, aurl, linkType] = href?.match(/([a-zA-Z0-9_.!-]+)~~~(citation|file_path)/) ?? [undefined, href, null]; + const renderType = (content: JSX.Element | null, click: (url: string) => void):JSX.Element => ( + // eslint-disable-next-line jsx-a11y/anchor-is-valid + <a className={`MessageComponent-${linkType}`} href="#" onClick={e => { e.preventDefault(); - if (linkType === 'citation') { - goToLinkedDoc(url); - } else if (linkType === 'file_path') { - showModal(); - setCurrentFile({ url }); - } - }} - style={style}> - {children} - </a> - ); + aurl && click(aurl); + }}> + {content} + </a> + ); // prettier-ignore + switch (linkType) { + case 'citation': return renderType(<Children />, (url: string) => goToLinkedDoc(url)); + case 'file_path': return renderType(null, (url: string) => { showModal(); setCurrentFile({ url }); }); + default: return null; + } // prettier-ignore }; +const MessageComponent: React.FC<MessageComponentProps> = function ({ message, toggleToolLogs, expandedLogIndex, goToLinkedDoc, index, showModal, setCurrentFile, isCurrent = false }) { + // const messageClass = `${message.role} ${isCurrent ? 'current-message' : ''}`; return ( <div className={`message ${message.role}`}> <MathJaxContext> <MathJax dynamic hideUntilTypeset="every"> - <ReactMarkdown components={{ a: LinkRenderer }}>{message.text ? message.text : ''}</ReactMarkdown> + <ReactMarkdown components={{ a: LinkRendererWrapper(goToLinkedDoc, showModal, setCurrentFile) }}>{message.text}</ReactMarkdown> </MathJax> </MathJaxContext> {message.image && <img src={message.image} alt="" />} <div className="message-footer"> {message.tool_logs && ( - <button className="toggle-logs-button" onClick={() => toggleToolLogs(index)}> + <button type="button" className="toggle-logs-button" onClick={() => toggleToolLogs(index)}> {expandedLogIndex === index ? 'Hide Code Interpreter Logs' : 'Show Code Interpreter Logs'} </button> )} diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss index f7389e39b..7848add24 100644 --- a/src/client/views/nodes/ComparisonBox.scss +++ b/src/client/views/nodes/ComparisonBox.scss @@ -10,6 +10,7 @@ pointer-events: none; display: flex; p { + // bcz: what is this styling for? if text in the comparison box is colored, then this causes it to render with a black outline color: rgb(0, 0, 0); -webkit-text-stroke-color: black; -webkit-text-stroke-width: 0.2px; @@ -57,7 +58,6 @@ left: 0; height: 100%; overflow: hidden; - transition: 200ms; .beforeBox-cont { height: 100%; @@ -71,7 +71,6 @@ width: 3px; display: inline-block; background: white; - transition: 200ms; .slide-handle { position: absolute; @@ -146,13 +145,14 @@ } } -.explain { +.comparisonBox-explain { position: absolute; top: 10px; left: 10px; z-index: 200; // padding: 5px; background: #dfdfdf; + pointer-events: none; } .comparisonBox-interactive { diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 3d33ff862..3e1c415d6 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -25,52 +25,39 @@ import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import ReactLoading from 'react-loading'; -import { ContextMenu } from '../ContextMenu'; -import { ContextMenuProps } from '../ContextMenuItem'; -import { tickStep } from 'd3'; -import { CollectionCarouselView } from '../collections/CollectionCarouselView'; +enum RevealOp { + Hover = 'hover', + Flip = 'flip', +} +enum UsePath { + Alternate = 'alternate', +} @observer export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ComparisonBox, fieldKey); } private _disposers: (DragManager.DragDropDisposer | undefined)[] = [undefined, undefined]; + private _closeRef = React.createRef<HTMLDivElement>(); + @observable _inputValue = ''; + @observable _outputValue = ''; + @observable _loading = false; + @observable _errorMessage = ''; + @observable _outputMessage = ''; + @observable _animating = ''; + @observable private _isEmpty = false; + @observable _yRelativeToTop: boolean = true; + constructor(props: FieldViewProps) { super(props); makeObservable(this); } - - @observable private _inputValue = ''; - @observable private _outputValue = ''; - @observable private _loading = false; - @observable private _isEmpty = false; - @observable _yRelativeToTop: boolean = true; - @action handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { this._inputValue = e.target.value; console.log(this._inputValue); }; - @observable _animating = ''; - - @computed get clipWidth() { - return NumCast(this.layoutDoc[this.clipWidthKey], 50); - } - get clipWidthKey() { - return '_' + this._props.fieldKey + '_clipWidth'; - } - - @computed get clipHeight() { - return NumCast(this.layoutDoc[this.clipHeightKey], 200); - } - get revealOp() { - return this.layoutDoc[`_${this._props.fieldKey}_revealOp`]; - } - get clipHeightKey() { - return '_' + this._props.fieldKey + '_clipHeight'; - } - componentDidMount() { this._props.setContentViewBox?.(this); reaction( @@ -78,6 +65,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() selected => !selected && (this.childActive = false) // what it should update to ); } + protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string, disposerId: number) => { this._disposers[disposerId]?.(); if (ele) { @@ -85,7 +73,20 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() } }; - private internalDrop = undoable((e: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => { + @computed get useAlternate() { return this.layoutDoc[`_${this.fieldKey}_usePath`] === UsePath.Alternate; } // prettier-ignore + @computed get revealOp() { return this.layoutDoc[`_${this.fieldKey}_revealOp`] as Opt<RevealOp>; } // prettier-ignore + @computed get clipWidth() { return NumCast(this.layoutDoc[`_${this.fieldKey}_clipWidth`], 50); } // prettier-ignore + set useAlternate(alt: boolean) { this.layoutDoc[`_${this.fieldKey}_usePath`] = alt ? UsePath.Alternate : undefined; } // prettier-ignore + set revealOp(op: Opt<RevealOp>){ this.layoutDoc[`_${this.fieldKey}_revealOp`] = op; } // prettier-ignore + set clipWidth(width: number) { this.layoutDoc[`_${this.fieldKey}_clipWidth`] = width; } // prettier-ignore + + animateClipWidth = action((clipWidth: number, duration = 200 /* ms */) => { + this._animating = `all ${duration}ms`; // turn on clip animation transition, then turn it off at end of animation + setTimeout(action(() => { this._animating = ''; }), duration); // prettier-ignore + this.clipWidth = clipWidth; + }); + + internalDrop = undoable((e: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => { if (dropEvent.complete.docDragData) { const { droppedDocuments } = dropEvent.complete.docDragData; const added = dropEvent.complete.docDragData.moveDocument?.(droppedDocuments, this.Document, (doc: Doc | Doc[]) => this.addDoc(toList(doc).lastElement(), fieldKey)); @@ -98,51 +99,34 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() return undefined; }, 'internal drop'); - private registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => { + registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => { if (e.button !== 2) { setupMoveUpEvents( this, e, this.onPointerMove, emptyFunction, - action((moveEv, doubleTap) => { + action((clickEv, doubleTap) => { if (doubleTap) { this.childActive = true; if (!this.dataDoc[this.fieldKey + '_1'] && !this.dataDoc[this.fieldKey]) this.dataDoc[this.fieldKey + '_1'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc); if (!this.dataDoc[this.fieldKey + '_2'] && !this.dataDoc[this.fieldKey + '_alternate']) this.dataDoc[this.fieldKey + '_2'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc); + // DocumentView.addViewRenderedCb(DocCast(this.dataDoc[this.fieldKey + '_1']), dv => { + // dv?.select(false); + // }); } }), - false, + true, undefined, - action(() => { - if (this.childActive) return; - this._animating = 'all 200ms'; - // on click, animate slider movement to the targetWidth - this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this._props.PanelWidth(); - // this.layoutDoc[this.clipHeightKey] = (targetWidth * 100) / this._props.PanelHeight(); - - setTimeout( - action(() => { - this._animating = ''; - }), - 200 - ); - }) + () => !this._isAnyChildContentActive && this.animateClipWidth((targetWidth * 100) / this._props.PanelWidth()) ); } }; - // private onClick(e: React.PointerEvent<HTMLDivElement>) { - // setupMoveUpEvents( - // this, e, this.onPointerMOve, emptyFunction(), () => {this._isAnyChildContentActive = true;}, emptyFunction(), emptyFunction() - // ) - // } - - @action - private onPointerMove = ({ movementX }: PointerEvent) => { + onPointerMove = ({ movementX }: PointerEvent) => { const width = movementX * this.ScreenToLocalBoxXf().Scale + (this.clipWidth / 100) * this._props.PanelWidth(); - if (width && width > 5 && width < this._props.PanelWidth()) { - this.layoutDoc[this.clipWidthKey] = (width * 100) / this._props.PanelWidth(); + if (width > 5 && width < this._props.PanelWidth()) { + this.clipWidth = (width * 100) / this._props.PanelWidth(); } return false; }; @@ -206,8 +190,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() ); }; docStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string): any => { - if (property === StyleProp.PointerEvents) return 'none'; - return this._props.styleProvider?.(doc, props, property); + switch (property) { + case StyleProp.PointerEvents: return 'none'; + default: return this._props.styleProvider?.(doc, props, property); + } // prettier-ignore }; moveDoc1 = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => toList(docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_1'), true); moveDoc2 = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => toList(docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_2'), true); @@ -246,8 +232,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() return layoutTemplateString; }; - _closeRef = React.createRef<HTMLDivElement>(); - createFlashcardPile(collectionArr: Doc[], gpt: boolean) { const newCol = Docs.Create.CarouselDocument(collectionArr, { _width: NumCast(this.layoutDoc['_' + this._props.fieldKey + '_width'], 250) + 50, @@ -287,28 +271,26 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() * Flips a flashcard to the alternate side for the user to view. */ flipFlashcard = () => { - const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; - this.layoutDoc[`_${this._props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : undefined; + this.useAlternate = !this.useAlternate; }; /** * Changes the view option to hover for a flashcard. */ - hoverFlip = (side: string | undefined) => { - if (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] === 'hover') this.layoutDoc[`_${this._props.fieldKey}_usePath`] = side; + hoverFlip = (alternate: boolean) => { + if (this.revealOp === RevealOp.Hover) this.useAlternate = alternate; }; /** * Creates the button used to flip the flashcards. */ @computed get overlayAlternateIcon() { - const usepath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; return ( <Tooltip title={<div className="dash-tooltip">flip</div>}> <div className="formattedTextBox-alternateButton" onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => { - if (!this.layoutDoc[`_${this._props.fieldKey}_revealOp`] || this.layoutDoc[`_${this._props.fieldKey}_revealOp`] === 'flip') { + if (!this.revealOp || this.revealOp === RevealOp.Flip) { this.flipFlashcard(); // console.log('Print Front of cards: ' + (RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text ?? '')); @@ -317,9 +299,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() }) } style={{ - background: this.revealOp === 'hover' ? 'gray' : usepath === 'alternate' ? 'white' : 'black', - color: this.revealOp === 'hover' ? 'black' : usepath === 'alternate' ? 'black' : 'white', - display: 'inline-block', + background: this.useAlternate ? 'white' : 'black', + color: this.useAlternate ? 'black' : 'white', }}> <div key="alternate" className="formattedTextBox-flip"> <FontAwesomeIcon icon="turn-up" size="1x" /> @@ -333,13 +314,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() return ( <div> <Tooltip - title={ - this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? ( - <div className="dash-tooltip">Flip to front side to use GPT</div> - ) : ( - <div className="dash-tooltip">Ask GPT to create an answer on the back side of the flashcard based on your question on the front</div> - ) - }> + title={this.useAlternate ? <div className="dash-tooltip">Flip to front side to use GPT</div> : <div className="dash-tooltip">Ask GPT to create an answer on the back side of the flashcard based on your question on the front</div>}> <div style={{ position: 'absolute', bottom: '3px', right: '50px', cursor: 'pointer' }} onPointerDown={e => (!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? this.askGPT(GPTCallType.CHATCARD) : null)}> <FontAwesomeIcon icon="lightbulb" size="xl" /> </div> @@ -360,7 +335,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() )} <Tooltip title={<div className="dash-tooltip">Hover to reveal</div>}> <div style={{ position: 'absolute', bottom: '3px', right: '25px', cursor: 'pointer' }} onClick={e => this.handleHover()}> - <FontAwesomeIcon color={this.revealOp === 'hover' ? 'blue' : 'black'} icon="hand-point-up" size="xl" /> + <FontAwesomeIcon color={this.revealOp === RevealOp.Hover ? 'blue' : 'black'} icon="hand-point-up" size="xl" /> </div> </Tooltip> {/* <Tooltip title={<div className="dash-tooltip">Remove this side of the flashcard</div>}> @@ -381,25 +356,24 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() @action handleRenderGPTClick = () => { // Call the GPT model and get the output - this.layoutDoc[`_${this._props.fieldKey}_usePath`] = 'alternate'; + this.useAlternate = true; this._outputValue = ''; if (this._inputValue) this.askGPT(GPTCallType.QUIZ); }; @action handleHover = () => { - if (this.revealOp === 'hover') { - this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'flip'; + if (this.revealOp === RevealOp.Hover) { + this.revealOp = RevealOp.Flip; this.Document.forceActive = false; } else { - this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'hover'; + this.revealOp = RevealOp.Hover; this.Document.forceActive = true; } - //this.revealOp === 'hover' ? (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'flip') : (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'hover'); }; @action handleRenderClick = () => { // Call the GPT model and get the output - this.layoutDoc[`_${this._props.fieldKey}_usePath`] = undefined; + this.useAlternate = false; }; animateRes = (resIndex: number, newText: string, callType: GPTCallType) => { @@ -537,7 +511,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() ); if (this.Document._layout_isFlashcard) { - const side = this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? 1 : 0; + const side = this.useAlternate ? 1 : 0; // add text box to each side when comparison box is first created // (!this.dataDoc[this.fieldKey + '_0'] && this.dataDoc[this._props.fieldKey + '_0'] !== 'empty') @@ -580,11 +554,12 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() <p style={{ display: text === '' ? 'flex' : 'none', color: 'white', marginLeft: '10px' }}>Return to all flashcards and add text to both sides. </p> <div className="input-box"> <textarea - value={this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? this._outputValue : this._inputValue} + value={this.useAlternate ? this._outputValue : this._inputValue} onChange={this.handleInputChange} onScroll={e => e.stopPropagation()} - placeholder={!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? 'Enter a response for GPT to evaluate.' : ''} - readOnly={this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate'}></textarea> + placeholder={!this.useAlternate ? 'Enter a response for GPT to evaluate.' : ''} + readOnly={this.useAlternate} + /> {this._loading ? ( <div className="loading-spinner" style={{ position: 'absolute' }}> @@ -592,12 +567,12 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() </div> ) : null} </div> - <div className="submit-button" style={{ overflow: 'hidden', display: this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? 'none' : 'flex' }}> + <div className="submit-button" style={{ overflow: 'hidden', display: this.useAlternate ? 'none' : 'flex' }}> <button type="button" onClick={this.handleRenderGPTClick} style={{ borderRadius: '2px', marginBottom: '3px' }}> Submit </button> </div> - <div className="submit-button" style={{ overflow: 'hidden', marginBottom: '2px', display: this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? 'flex' : 'none' }}> + <div className="submit-button" style={{ overflow: 'hidden', marginBottom: '2px', display: this.useAlternate ? 'flex' : 'none' }}> <button type="button" onClick={this.handleRenderClick} style={{ borderRadius: '2px' }}> Redo the Question </button> @@ -613,15 +588,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} /* change className to easily disable/enable pointer events in CSS */ // onContextMenu={this.specificMenu} style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden' }} - onMouseEnter={() => { - this.hoverFlip('alternate'); - }} - onMouseLeave={() => { - this.hoverFlip(undefined); - }} - // onPointerUp={() => (this._isAnyChildContentActive = true)} - > - {!this.layoutDoc[`_${this._props.fieldKey}_usePath`] && StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text) === '' && !this.childActive ? <p className="explain">Enter text in the flashcard. </p> : null} + onMouseEnter={() => this.hoverFlip(true)} + onMouseLeave={() => this.hoverFlip(false)}> + {!this.useAlternate && StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text) === '' && !this.childActive ? <p className="comparisonBox-explain">Enter text in the flashcard. </p> : null} {displayBox(`${this.fieldKey}_${side === 0 ? 1 : 0}`, side, this._props.PanelWidth() - 3)} {this._loading ? ( <div className="loading-spinner" style={{ position: 'absolute' }}> @@ -645,9 +614,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() className="slide-bar" style={{ left: `calc(${this.clipWidth + '%'} - 0.5px)`, + transition: this._animating, cursor: this.clipWidth < 5 ? 'e-resize' : this.clipWidth / 100 > (this._props.PanelWidth() - 5) / this._props.PanelWidth() ? 'w-resize' : undefined, }} - onPointerDown={e => !this.childActive && this.registerSliding(e, this._props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */ + onPointerDown={e => this.registerSliding(e, this._props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */ > <div className="slide-handle" /> </div> diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index ff938df78..8068407bb 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -674,6 +674,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; focus = (anchor: Doc, options: FocusViewOptions) => (anchor.type === DocumentType.CONFIG ? undefined : this._ffref.current?.focus(anchor, options)); + renderedPixelDimensions = async () => { + const { nativeWidth: width, nativeHeight: height } = await Networking.PostToServer('/inspectImage', { source: this.paths[0] }); + return { width, height }; + }; + savedAnnotations = () => this._savedAnnotations; render() { TraceMobx(); diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 7a89b143b..8db68ddfe 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -655,9 +655,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { ); else { if (!PDFBox.pdfpromise.get(href)) PDFBox.pdfpromise.set(href, Pdfjs.getDocument(href).promise); - PDFBox.pdfpromise.get(href)?.then((pdf: any) => { - PDFBox.pdfcache.set(href, (this._pdf = pdf)); - }); + PDFBox.pdfpromise.get(href)?.then( + action((pdf: any) => { + PDFBox.pdfcache.set(href, (this._pdf = pdf)); + }) + ); } } return pdfView ?? this.renderTitleBox; diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index befbee48b..38d14014f 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -24,20 +24,18 @@ import { FieldViewProps } from '../nodes/FieldView'; import { FocusViewOptions } from '../nodes/FocusViewOptions'; import { LinkInfo } from '../nodes/LinkDocPreview'; import { PDFBox } from '../nodes/PDFBox'; -import { ComparisonBox } from '../nodes/ComparisonBox'; import { ObservableReactComponent } from '../ObservableReactComponent'; import { StyleProp } from '../StyleProp'; import { AnchorMenu } from './AnchorMenu'; import { Annotation } from './Annotation'; import { GPTPopup } from './GPTPopup/GPTPopup'; -import { Docs } from '../../documents/Documents'; import './PDFViewer.scss'; import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT'; import ReactLoading from 'react-loading'; // pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`; // The workerSrc property shall be specified. -Pdfjs.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@4.2.67/build/pdf.worker.mjs'; +Pdfjs.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@4.3.136/build/pdf.worker.mjs'; interface IViewerProps extends FieldViewProps { pdfBox: PDFBox; diff --git a/src/server/# Server Architecture.md b/src/server/# Server Architecture.md new file mode 100644 index 000000000..27d5a1a1a --- /dev/null +++ b/src/server/# Server Architecture.md @@ -0,0 +1,87 @@ +# Server Architecture + +## index.ts + +This file launches the server at port `1050`. After executing functions that need to occur before the server starts and [configuring the server](#server_initializationts), it registers all of the server's routes. Some of these it does directly, like `"/home"` which serves the user the appropriate HTML file, and others it does indirectly by instantiating the API manager responsible for registering it. Finally, it sets the web socket and the server listening on their respective ports. + +Note that if the server is running in release mode, this is where the `DashSessionAgent` is launched. This is essentially a separate process that monitors the server and can perform operations like backing up the database, sending a zipped copy of the database to a given email address and monitoring / restarting the server process in the event of an uncaught exception. + +## server_initialization.ts + +Here, we build the server object from the ground up, customizing it with `middleware`. You can think of middleware as plugins that perform useful transformations or inject helpful functionality when the server is serving its routes. This mainly includes things like authenticating a user for security, establishing sessions so that a user doesn't have to log in every time they launch Dash, invoking utilities that parse the object of requests from their stringified form in which they were sent over the network, etc. + +Here we also register the authentication routes. These don't need the [RouteManager](#RouteManagerts) security abstraction since a user won't be logged in when trying to access `"/login"` (i.e. they don't need to be protected). Note that the actual functionality for these routes is contained in [this file](#AuthenticationManagerts). + +## websocket.ts + +This file launches the web socket at port `4321`. While the server is sufficient for unidirectional requests, sometimes bidirectional communication between clients and the server is required. One such example in Dash is the updating of a field: since multiple clients might care about its value, we'd like to be able to broadcast to each client the change in real time. This is where the [web socket](https://blog.teamtreehouse.com/an-introduction-to-websockets) comes in. + +Thus, this file primarily sets up those bidirectional routes, or events. When the client socket emits an event, it will trigger the handler (invoke the code) registered to that particular event on the server. + +## ApiManager.ts + +This is a very lightweight class that just provides a template for a logical partition that encompasses a subsection of the server's functionality. For example, I might want to have all the routes that have to do with sending and receiving user information to be written in and registered by a `UserManager`. Or, maybe I want to have everything pertaining to uploading media content to the server to exist in a silo defined by `UploadManager`. + +For context, before this system, every route was defined in [index.ts](#indexts), which had become a rather long mess of a file. So, while both the `UserManager` and `UploadManager` register their routes in the same, single server, they logically separate code into searchable, well-defined files. + +## Passport.ts + +If we just used the `Express` server out of the box, we'd have no way of controlling who accesses what routes. `PassportJS` is a node module (actually, also an example of [middleware](#server_initializationts)). Take a look at [official documentation](http://www.passportjs.org/docs/) for a more in-depth explanation. Practically, it adds a user property to the http request object, and as you can see in [RouteManager.ts](#RouteManagerts) we only execute the route if the user object exists, indicating that a user is indeed logged in. + +## RouteManager.ts + +With the Express server, it's very easy to add a route. You just call `server.get("/myroute", () => console.log("do something here in the handler"));` to set up a get request, for example. However, this doesn't take advantage of [PassportJS](#Passportts) adding authentication information to the request object. Instead, the server will serve this route blindly, no matter who tries to access it, and this is a problem. Therefore, we'd like every single route to first check if a user is logged in before we carry it out, if that matters to us (and in most cases, it does). It would be a pretty inefficient to write that check in the handler for any and all new routes. So, we wrote an abstraction to add some convenience to the above security implementation. There's a wrapper that exists around those server route registration calls that basically takes in the same information, but registers as the handler not the core developer-written handler, but instead a function that inspects the request object's user property. If the handler needs to be secure and there is a user, or there is no user and the handler doesn't need to be secure, it invokes it. Otherwise, it redirects the caller to `"/login"`. + +Another benefit of handling things this way is that we can enforce the format of routes, and that's the second part of the handler. It records any malformed or duplicate route registrations, and won't let you start Dash until the formatting or conflicts are resolved. + +## DashUserModel.ts + +This file effectively specifies the model for a Dash user. The `userSchema` is the meat and potatoes here, and is only mirrored by the `DashUserModel` type. It's the schema, providing easy updating of the underlying database contents, that's used to instantiate the actual mongoose model, and it also handles the middleware conversion of the initially plain text password to the hashed, stored result. The mongoose model, used primarily in [Passport.ts](#passportts) and [AuthenticationManager.ts](#AuthenticationManagerts), has helpful functions for finding a user matching a particular query in the database without having to directly interact with the `users` MongoDB collection. + +## AuthenticationManager.ts + +This is where all the log in, sign up and password recovery logic lives, invoked when the relevant pages whose layouts are defined by [.pug](#pug) templates submit forms to the relevant routes on the server. These requests are what actually contain, for example, the username and password that the user submitted, either in signing up or logging in, and thus these are what are parsed by [PassportJS](#Passportts) for authentication (during log in), or written to the database via manipulations of the [DashUserModel](#DashUserModelts) (during sign up). + +## ActionUtilities.ts + +This file contains a bunch of potentially useful but, as of this writing, largely unused utility functions for server side operations, like simple convenience wrappers around reading and writing text files, automatically bookending the execution of a function with logs to the server, data conversion functions and even dispatching an email programmatically (sent from a dummy gmail account `brownptcdash@gmail.com`. + +## .pug + +This [file format](https://pugjs.org/api/getting-started.html) is actually pretty interesting - it's a substantially streamlined version of HTML that lets you define webpage layouts (that are maybe not quite as sophisticated or interactive as those backed by React components) very quickly. All of the individual .pug files extend `layout.pug`, which ensures that each one contains the appropriate, consistent header information as well as a common style sheet. Importantly, these pages are rendered by invoking `render` on an Express.response object, with the format `res.render("path/to/target.pug", { var1: "define variables referenced in target.pug", var2: "from TypeScript code" }`. Take a look at the `"/activity"` route in `UserManager.ts` and `user_activity.pug` for an example of defining and referencing variables. + +# Externals + +## Environment Variables + +### Why? + +Using an external [API](https://www.freecodecamp.org/news/what-is-an-api-in-english-please-b880a3214a82/) can be a fantastic and interesting way to add depth and maturity to Dash. For example, as of this writing we already use some features of Google and Microsoft Cognitive Services APIs for either integrating their proprietary data formats with our own, or providing mature machine learning driven analysis tools that we simply don't have the time or resources to develop in house. But often times, since these services either are paid or at least require us to connect them to a single billing account, they require that we create and use an [API key](https://rapidapi.com/blog/api-glossary/api-key/) to identify our interactions with these APIs. + +These keys are considered sensitive information, since anyone with the key can impersonate you and even rack up huge charges on your billing account. We don't necessarily even want these to go onto GitHub, our secure private repo, so we definitely don't want to use these hard coded values in our TypeScript code: when it's transpiled and sent to the browser, anyone can open up the developer inspector, search for 'API_KEY' and probably instantly find it embedded in and steal it from your transpiled source code. And yet, we need some way to easily interact with these values! + +### What? + +This is where environment variables step up to the plate: we can define a file called `.env` in the root of our project directory (and add it to the `.gitignore` to be extra safe!). This file is a key-value mapping with a specific format (_no spaces or quoted strings within an entry, with a single entry on each line_): + +`key1=value1` +`key2=value2` +`...` +`GOOGLE_MAPS_API_KEY=319440934820394` + +When the _server_ process starts, it's backed by a file system and can (and always will if a `.env` file is present) read in every key-value entry in `.env` and store them at `process.env.key1`, `process.env.key2` and `process.env.GOOGLE_MAPS_API_KEY`. At runtime, these keys will evaluate, in code, to the stringified version whatever is put on the right hand side of the equals sign in the `.env` file entry. So if you need to refer to a piece of sensitive information from the server, it's trivial. Your work is done. + +### Complications and Solutions + +But this is of no help if we, as we often do, need to reference sensitive authorization keys, etc. from the client. Since it is not backed by a NodeJS process (after it's transpiled and sent to the browser, it's then executed by the browser) and, since it's running on the _client_ machine which cannot read from the _server_ machine, it might seem like we'd be out of luck. For a while, I thought we were. + +But, you can add a [plugin to Webpack](https://webpack.js.org/plugins/define-plugin/) (see the entry for this) that will read in the environment variables from the server and embed them securely to be accessed in the client side, still from `process.env.GOOGLE_MAPS_API_KEY`, for example. Note that since our dependencies inject lots of (to us) unneeded environment variables into our server process, we can choose to only transfer Dash-specific environment variables to the client by (arbitrarily) prepending them with `_CLIENT_` in `.env`, and then, in `webpack.config.js`, choosing to only copy over entries with that prefix. For example, say that I wanted to be able to reference `key1` and `GOOGLE_MAPS_API_KEY` on the client side, and only needed to use `key2` on the server. My .env would look like this: + +`_CLIENT_key1=value1` +`key2=value2` +`...` +`_CLIENT_GOOGLE_MAPS_API_KEY=319440934820394` + +Note that if you have an entry that you want to use `key1` on both the client and server sides, since the Webpack processing removes the `_CLIENT_` only for the client access, you would call `process.env._CLIENT_key1` on the server, and `process.env.key1` on the client. + +And that's pretty much all there is to it! Search for `process.env` in `CognitiveServices.ts` and `CollectionMapView.tsx` for examples of retrieving these variables on the client side. diff --git a/src/server/ApiManagers/AssistantManager.ts b/src/server/ApiManagers/AssistantManager.ts index 82e48167a..b42314e41 100644 --- a/src/server/ApiManagers/AssistantManager.ts +++ b/src/server/ApiManagers/AssistantManager.ts @@ -4,7 +4,7 @@ import OpenAI from 'openai'; import * as path from 'path'; import { promisify } from 'util'; import * as uuid from 'uuid'; -import { filesDirectory, publicDirectory } from '..'; +import { filesDirectory, publicDirectory } from '../SocketData'; import { Method } from '../RouteManager'; import ApiManager, { Registration } from './ApiManager'; |