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/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 |
10 files changed, 162 insertions, 89 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 56a1e4fcc..da1d352f2 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; @@ -142,7 +143,6 @@ left: 0; height: 100%; overflow: hidden; - transition: 200ms; .beforeBox-cont { height: 100%; @@ -156,7 +156,6 @@ width: 3px; display: inline-block; background: white; - transition: 200ms; .slide-handle { position: absolute; @@ -231,13 +230,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/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 6619b765b..86da64e5e 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -796,6 +796,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 9d06f81ed..c07a113d3 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -24,13 +24,11 @@ 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'; @@ -39,7 +37,7 @@ import SpeechRecognition, { useSpeechRecognition } from 'react-speech-recognitio // 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'; |