19 | {props.users.map((user, i) => (
20 |
26 | ))}
27 | >
28 | );
29 | };
30 |
31 | export default ChatList;
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # catbook-react
2 |
3 | ## start up
4 |
5 | run `npm start` in one terminal and `npm run hotloader` in another
6 |
7 | To run the LLM parts of the app:
8 | 1. Make a virtual environment: `python3 -m venv .venv`. This virtual environment should be in Python 3.10 or earlier; Python 3.11+ won't work.
9 | 2. Activate the virtual environment (venv): `. .venv/bin/activate`
10 | 3. Install the dependencies from requirements.txt in the venv: `pip install -r requirements.txt`
11 | 4. Run the local ChromaDB instance: `chroma run`
12 | 5. Set the environment variable `ANYSCALE_API_KEY`. Using a .env file is probably the simplest way to do this.
13 |
14 | visit `http://localhost:5050`
15 |
16 | ## don't touch
17 |
18 | the following files students do not need to edit. feel free to read them if you would like.
19 |
20 | ```
21 | client/dist/index.html
22 | client/src/index.js
23 | client/src/utilities.js
24 | client/src/client-socket.js
25 | server/validator.js
26 | server/server-socket.js
27 | .babelrc
28 | .npmrc
29 | .prettierrc
30 | package-lock.json
31 | webpack.config.js
32 | ```
33 |
--------------------------------------------------------------------------------
/client/src/components/modules/Document.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import "./Document.css";
3 |
4 | const Document = (props) => {
5 | const [value, setValue] = useState(props.content);
6 |
7 | const handleChange = (event) => {
8 | setValue(event.target.value);
9 | };
10 |
11 | return (
12 | <>
13 |
14 |
15 |
23 |
31 |
32 | >
33 | );
34 | };
35 |
36 | export default Document;
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) MIT 6.9620 Web Lab: A Programming Class and Competition
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/client/src/components/modules/CommentsBlock.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import SingleComment from "./SingleComment.js";
3 | import { NewComment } from "./NewPostInput.js";
4 |
5 | /**
6 | * @typedef ContentObject
7 | * @property {string} _id of story/comment
8 | * @property {string} creator_name
9 | * @property {string} content of the story/comment
10 | */
11 |
12 | /**
13 | * Component that holds all the comments for a story
14 | *
15 | * Proptypes
16 | * @param {ContentObject[]} comments
17 | * @param {ContentObject} story
18 | */
19 | const CommentsBlock = (props) => {
20 | return (
21 |
36 | );
37 | } else {
38 | setWinnerModal(null);
39 | }
40 | drawCanvas(update);
41 | };
42 |
43 | // set a spawn button if the player is not in the game
44 | let spawnButton = null;
45 | if (props.userId) {
46 | spawnButton = (
47 |
48 |
55 |
56 | );
57 | }
58 |
59 | // display text if the player is not logged in
60 | let loginModal = null;
61 | if (!props.userId) {
62 | loginModal =
Please Login First!
;
63 | }
64 |
65 | return (
66 | <>
67 |
68 | {/* important: canvas needs id to be referenced by canvasManager */}
69 |
70 | {loginModal}
71 | {winnerModal}
72 | {spawnButton}
73 |
74 | >
75 | );
76 | };
77 |
78 | export default Game;
79 |
--------------------------------------------------------------------------------
/server/auth.js:
--------------------------------------------------------------------------------
1 | const { OAuth2Client } = require("google-auth-library");
2 | const User = require("./models/user");
3 | const socketManager = require("./server-socket");
4 |
5 | // create a new OAuth client used to verify google sign-in
6 | const CLIENT_ID = "395785444978-7b9v7l0ap2h3308528vu1ddnt3rqftjc.apps.googleusercontent.com";
7 | const client = new OAuth2Client(CLIENT_ID);
8 |
9 | // accepts a login token from the frontend, and verifies that it's legit
10 | function verify(token) {
11 | return client
12 | .verifyIdToken({
13 | idToken: token,
14 | audience: CLIENT_ID,
15 | })
16 | .then((ticket) => ticket.getPayload());
17 | }
18 |
19 | // gets user from DB, or makes a new account if it doesn't exist yet
20 | function getOrCreateUser(user) {
21 | // the "sub" field means "subject", which is a unique identifier for each user
22 | return User.findOne({ googleid: user.sub }).then((existingUser) => {
23 | if (existingUser) return existingUser;
24 |
25 | const newUser = new User({
26 | name: user.name,
27 | googleid: user.sub,
28 | });
29 |
30 | return newUser.save();
31 | });
32 | }
33 |
34 | function login(req, res) {
35 | verify(req.body.token)
36 | .then((user) => getOrCreateUser(user))
37 | .then((user) => {
38 | // persist user in the session
39 | req.session.user = user;
40 | res.send(user);
41 | })
42 | .catch((err) => {
43 | console.log(`Failed to log in: ${err}`);
44 | res.status(401).send({ err });
45 | });
46 | }
47 |
48 | function logout(req, res) {
49 | const userSocket = socketManager.getSocketFromUserID(req.user._id);
50 | if (userSocket) {
51 | // delete user's socket if they logged out
52 | socketManager.removeUser(req.user, userSocket);
53 | }
54 |
55 | req.session.user = null;
56 | res.send({});
57 | }
58 |
59 | function populateCurrentUser(req, res, next) {
60 | // simply populate "req.user" for convenience
61 | req.user = req.session.user;
62 | next();
63 | }
64 |
65 | function ensureLoggedIn(req, res, next) {
66 | if (!req.user) {
67 | return res.status(401).send({ err: "not logged in" });
68 | }
69 |
70 | next();
71 | }
72 |
73 | module.exports = {
74 | login,
75 | logout,
76 | populateCurrentUser,
77 | ensureLoggedIn,
78 | };
79 |
--------------------------------------------------------------------------------
/server/validator.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const net = require("net");
3 |
4 | /**
5 | * Provides some basic checks to make sure you've
6 | * correctly set up your repository.
7 | *
8 | * You normally shouldn't need to modify this file.
9 | *
10 | * Curent checks:
11 | * - node_modules exists
12 | * - makes sure 'npx webpack' was called if required
13 | * - warns if visiting port 3000 while running hot reloader
14 | */
15 |
16 | class NodeSetupError extends Error {}
17 | let routeChecked = false;
18 |
19 | // poke port 5050 to see if 'npm run hotloader' was possibly called
20 | function checkHotLoader() {
21 | return new Promise((resolve, reject) => {
22 | var server = net.createServer();
23 |
24 | server.once("error", (err) => {
25 | resolve(err.code === "EADDRINUSE");
26 | });
27 |
28 | server.once("listening", () => server.close());
29 | server.once("close", () => resolve(false));
30 | server.listen(5050);
31 | });
32 | }
33 |
34 | module.exports = {
35 | checkSetup: () => {
36 | if (!fs.existsSync("./node_modules/")) {
37 | throw new NodeSetupError(
38 | "node_modules not found! This probably means you forgot to run 'npm install'"
39 | );
40 | }
41 | },
42 |
43 | checkRoutes: (req, res, next) => {
44 | if (!routeChecked && req.url === "/") {
45 | // if the server receives a request on /, we must be on port 3000 not 5050
46 | if (!fs.existsSync("./client/dist/bundle.js")) {
47 | throw new NodeSetupError(
48 | "Couldn't find bundle.js! If you want to run the hot reloader, make sure 'npm run hotloader'\n" +
49 | "is running and then go to http://localhost:5050 instead of port 3000.\n" +
50 | "If you're not using the hot reloader, make sure to run 'npx webpack' before visiting this page"
51 | );
52 | }
53 |
54 | checkHotLoader().then((active) => {
55 | if (active) {
56 | console.log(
57 | "Warning: It looks like 'npm run hotloader' may be running. Are you sure you don't want\n" +
58 | "to use the hot reloader? To use it, visit http://localhost:5050 and not port 3000"
59 | );
60 | }
61 | });
62 |
63 | routeChecked = true; // only runs once to avoid spam/overhead
64 | }
65 | next();
66 | },
67 | };
68 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /*
2 | |--------------------------------------------------------------------------
3 | | webpack.config.js -- Configuration for Webpack
4 | |--------------------------------------------------------------------------
5 | |
6 | | Webpack turns all the clientside HTML, CSS, Javascript into one bundle.js file.
7 | | This is done for performance reasons, as well as for compatability reasons.
8 | |
9 | | You do not have to worry about this file, except for proxy section below.
10 | | All proxies does is route traffic from the hotloader to the backend.
11 | | You must define explicity all routes here, as we do for the /api/* routes.
12 | |
13 | | The rest of this file tell webpack which types of files to bundle (in the rules).
14 | | In addition, it also uses babel to transpile your javascript into code all browsers can use.
15 | | see https://babeljs.io/docs/en/ if this interests you!
16 | |
17 | */
18 |
19 | const path = require("path");
20 | const entryFile = path.resolve(__dirname, "client", "src", "index.js");
21 | const outputDir = path.resolve(__dirname, "client", "dist");
22 |
23 | const webpack = require("webpack");
24 |
25 | module.exports = {
26 | entry: [entryFile],
27 | output: {
28 | path: outputDir,
29 | publicPath: "/",
30 | filename: "bundle.js",
31 | },
32 | devtool: "inline-source-map",
33 | module: {
34 | rules: [
35 | {
36 | test: /\.(js|jsx)$/,
37 | loader: "babel-loader",
38 | exclude: /node_modules/,
39 | },
40 | {
41 | test: /\.(scss|css)$/,
42 | use: [
43 | {
44 | loader: "style-loader",
45 | },
46 | {
47 | loader: "css-loader",
48 | },
49 | ],
50 | },
51 | {
52 | test: /\.(png|svg|jpg|gif)$/,
53 | use: [
54 | {
55 | loader: "url-loader",
56 | },
57 | ],
58 | },
59 | ],
60 | },
61 | resolve: {
62 | extensions: ["*", ".js", ".jsx"],
63 | },
64 | plugins: [new webpack.HotModuleReplacementPlugin()],
65 | devServer: {
66 | historyApiFallback: true,
67 | static: "./client/dist",
68 | hot: true,
69 | proxy: {
70 | "/api": "http://localhost:3000",
71 | "/socket.io/*": {
72 | target: "http://localhost:3000",
73 | ws: true,
74 | },
75 | },
76 | },
77 | };
78 |
--------------------------------------------------------------------------------
/client/src/components/pages/LLM.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import Corpus from "../modules/Corpus";
3 | import { NewPostInput } from "../modules/NewPostInput";
4 | import { get, post } from "../../utilities";
5 |
6 | const LLM = (props) => {
7 | const [loading, setLoading] = useState(false);
8 | const [corpus, setCorpus] = useState([]);
9 | const [response, setResponse] = useState("");
10 | const [runnable, setRunnable] = useState(false);
11 |
12 | useEffect(() => {
13 | get("/api/isrunnable").then((res) => {
14 | if (res.isrunnable) {
15 | setRunnable(true);
16 | get("/api/document").then((corpus) => {
17 | setCorpus(corpus);
18 | });
19 | }
20 | setLoading(false);
21 | });
22 | }, []);
23 |
24 | const makeQuery = (q) => {
25 | setResponse("querying the model...");
26 | post("/api/query", { query: q })
27 | .then((res) => {
28 | setResponse(res.queryresponse);
29 | })
30 | .catch(() => {
31 | setResponse("error during query. check your server logs!");
32 | setTimeout(() => {
33 | setResponse("");
34 | }, 2000);
35 | });
36 | };
37 |
38 | if (!props.userId) {
39 | return
47 | 1. a valid api key is not configured. add a valid key to a .env in root to begin chatting
48 | with the LLM!
49 |
50 |
51 | 2. your chroma db server is not running. run `chroma run` in a separate terminal to start
52 | up the db (follow setup guide to make sure this is set up correctly)
53 |
54 | >
55 | );
56 | }
57 | return (
58 | <>
59 |
60 |
Corpus
61 |
62 |
63 |
64 |
Query the LLM
65 |
66 |
{response}
67 |
68 | >
69 | );
70 | };
71 |
72 | export default LLM;
73 |
--------------------------------------------------------------------------------
/client/src/utilities.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Utility functions to make API requests.
3 | * By importing this file, you can use the provided get and post functions.
4 | * You shouldn't need to modify this file, but if you want to learn more
5 | * about how these functions work, google search "Fetch API"
6 | *
7 | * These functions return promises, which means you should use ".then" on them.
8 | * e.g. get('/api/foo', { bar: 0 }).then(res => console.log(res))
9 | */
10 |
11 | // ex: formatParams({ some_key: "some_value", a: "b"}) => "some_key=some_value&a=b"
12 | function formatParams(params) {
13 | // iterate of all the keys of params as an array,
14 | // map it to a new array of URL string encoded key,value pairs
15 | // join all the url params using an ampersand (&).
16 | return Object.keys(params)
17 | .map((key) => key + "=" + encodeURIComponent(params[key]))
18 | .join("&");
19 | }
20 |
21 | // convert a fetch result to a JSON object with error handling for fetch and json errors
22 | function convertToJSON(res) {
23 | if (!res.ok) {
24 | throw `API request failed with response status ${res.status} and text: ${res.statusText}`;
25 | }
26 |
27 | return res
28 | .clone() // clone so that the original is still readable for debugging
29 | .json() // start converting to JSON object
30 | .catch((error) => {
31 | // throw an error containing the text that couldn't be converted to JSON
32 | return res.text().then((text) => {
33 | throw `API request's result could not be converted to a JSON object: \n${text}`;
34 | });
35 | });
36 | }
37 |
38 | // Helper code to make a get request. Default parameter of empty JSON Object for params.
39 | // Returns a Promise to a JSON Object.
40 | export function get(endpoint, params = {}) {
41 | const fullPath = endpoint + "?" + formatParams(params);
42 | return fetch(fullPath)
43 | .then(convertToJSON)
44 | .catch((error) => {
45 | // give a useful error message
46 | throw `GET request to ${fullPath} failed with error:\n${error}`;
47 | });
48 | }
49 |
50 | // Helper code to make a post request. Default parameter of empty JSON Object for params.
51 | // Returns a Promise to a JSON Object.
52 | export function post(endpoint, params = {}) {
53 | return fetch(endpoint, {
54 | method: "post",
55 | headers: { "Content-type": "application/json" },
56 | body: JSON.stringify(params),
57 | })
58 | .then(convertToJSON) // convert result to JSON object
59 | .catch((error) => {
60 | // give a useful error message
61 | throw `POST request to ${endpoint} failed with error:\n${error}`;
62 | });
63 | }
64 |
--------------------------------------------------------------------------------
/client/src/canvasManager.js:
--------------------------------------------------------------------------------
1 | let canvas;
2 |
3 | /** utils */
4 |
5 | // load sprites!
6 | let sprites = {
7 | red: null,
8 | blue: null,
9 | green: null,
10 | yellow: null,
11 | purple: null,
12 | orange: null,
13 | silver: null,
14 | };
15 | Object.keys(sprites).forEach((key) => {
16 | sprites[key] = new Image(400, 400);
17 | sprites[key].src = `../player-icons/${key}.png`; // Load sprites from dist folder
18 | });
19 |
20 | // converts a coordinate in a normal X Y plane to canvas coordinates
21 | const convertCoord = (x, y) => {
22 | if (!canvas) return;
23 | return {
24 | drawX: x,
25 | drawY: canvas.height - y,
26 | };
27 | };
28 |
29 | // fills a circle at a given x, y canvas coord with radius and color
30 | const fillCircle = (context, x, y, radius, color) => {
31 | context.beginPath();
32 | context.arc(x, y, radius, 0, 2 * Math.PI, false);
33 | context.fillStyle = color;
34 | context.fill();
35 | };
36 |
37 | // draws a sprite instead of a colored circle
38 | const drawSprite = (context, x, y, radius, color) => {
39 | context.save();
40 | // Saves current context so we can restore to here once we are done drawing
41 | context.beginPath();
42 | context.arc(x, y, radius, 0, 2 * Math.PI, false);
43 | context.closePath();
44 | context.clip(); // Sets circular clipping region for sprite image
45 | context.drawImage(sprites[color], x - radius, y - radius, radius * 2, radius * 2);
46 | context.restore();
47 | // Restores context to last save (before clipping was applied), so we can draw normally again
48 | };
49 |
50 | /** drawing functions */
51 |
52 | const drawPlayer = (context, x, y, radius, color) => {
53 | const { drawX, drawY } = convertCoord(x, y);
54 | drawSprite(context, drawX, drawY, radius, color);
55 | };
56 |
57 | const drawCircle = (context, x, y, radius, color) => {
58 | const { drawX, drawY } = convertCoord(x, y);
59 | fillCircle(context, drawX, drawY, radius, color);
60 | };
61 |
62 | /** main draw */
63 | export const drawCanvas = (drawState) => {
64 | // use id of canvas element in HTML DOM to get reference to canvas object
65 | canvas = document.getElementById("game-canvas");
66 | if (!canvas) return;
67 | const context = canvas.getContext("2d");
68 |
69 | // clear the canvas to black
70 | context.fillStyle = "black";
71 | context.fillRect(0, 0, canvas.width, canvas.height);
72 |
73 | // draw all the players
74 | Object.values(drawState.players).forEach((p) => {
75 | drawPlayer(context, p.position.x, p.position.y, p.radius, p.color);
76 | });
77 |
78 | // draw all the foods
79 | Object.values(drawState.food).forEach((f) => {
80 | drawCircle(context, f.position.x, f.position.y, f.radius, f.color);
81 | });
82 | };
83 |
--------------------------------------------------------------------------------
/client/src/components/modules/Corpus.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from "react";
2 | import Document from "./Document";
3 | import { NewPostInput } from "./NewPostInput";
4 | import "./Document.css";
5 |
6 | import { get, post } from "../../utilities";
7 |
8 | const Corpus = (props) => {
9 | const [alertContent, setAlertContent] = useState("");
10 | const corpusRef = useRef(null);
11 |
12 | const alert = (message, ms) => {
13 | setAlertContent(message);
14 | setTimeout(() => {
15 | setAlertContent("");
16 | }, ms);
17 | };
18 |
19 | const handleNewDocument = (content) => {
20 | setAlertContent("generating document...");
21 | post("/api/document", { content: content })
22 | .then((newDoc) => {
23 | props.setCorpus(props.corpus.concat([newDoc]));
24 | if (corpusRef.current) {
25 | corpusRef.current.scrollTop = corpusRef.current.scrollHeight;
26 | }
27 | alert("document successfully generated!", 2000);
28 | })
29 | .catch(() => {
30 | alert("error adding document. check server logs!", 2000);
31 | });
32 | };
33 |
34 | const handleUpdateDocument = (id, content) => {
35 | setAlertContent("updating document...");
36 | post("/api/updateDocument", { _id: id, content: content })
37 | .then(() => {
38 | alert("document successfully updated!", 2000);
39 | })
40 | .catch(() => {
41 | alert("error updating document. check server logs!", 2000);
42 | });
43 | };
44 |
45 | const handleDeleteDocument = (id) => {
46 | setAlertContent("deleting document...");
47 | post("/api/deleteDocument", { _id: id })
48 | .then(() => {
49 | props.setCorpus(props.corpus.filter((doc) => doc._id !== id));
50 | alert("document successfully deleted!", 2000);
51 | })
52 | .catch(() => {
53 | alert("error deleting document. check server logs!", 2000);
54 | });
55 | };
56 |
57 | return (
58 | <>
59 |