├── requirements.txt ├── .npmrc ├── client ├── src │ ├── public │ │ ├── corgi.jpg │ │ └── favicon.ico │ ├── components │ │ ├── App.css │ │ ├── pages │ │ │ ├── Game.css │ │ │ ├── NotFound.js │ │ │ ├── Chatbook.css │ │ │ ├── Profile.css │ │ │ ├── Feed.js │ │ │ ├── Profile.js │ │ │ ├── Game.js │ │ │ ├── LLM.js │ │ │ └── Chatbook.js │ │ ├── modules │ │ │ ├── Chat.css │ │ │ ├── Document.css │ │ │ ├── SingleUser.css │ │ │ ├── CatHappiness.css │ │ │ ├── CatHappiness.js │ │ │ ├── SingleMessage.css │ │ │ ├── SingleMessage.js │ │ │ ├── Card.css │ │ │ ├── SingleComment.js │ │ │ ├── NavBar.css │ │ │ ├── SingleStory.js │ │ │ ├── SingleUser.js │ │ │ ├── NewPostInput.css │ │ │ ├── ChatList.js │ │ │ ├── Document.js │ │ │ ├── CommentsBlock.js │ │ │ ├── Chat.js │ │ │ ├── Card.js │ │ │ ├── NavBar.js │ │ │ ├── Corpus.js │ │ │ └── NewPostInput.js │ │ └── App.js │ ├── index.js │ ├── input.js │ ├── client-socket.js │ ├── utilities.css │ ├── utilities.js │ └── canvasManager.js └── dist │ ├── player-icons │ ├── blue.png │ ├── red.png │ ├── green.png │ ├── orange.png │ ├── purple.png │ ├── silver.png │ └── yellow.png │ └── index.html ├── .gitignore ├── .prettierrc ├── server ├── models │ ├── document.js │ ├── user.js │ ├── story.js │ ├── comment.js │ └── message.js ├── auth.js ├── validator.js ├── server-socket.js ├── server.js ├── rag.js ├── api.js └── game-logic.js ├── .babelrc ├── README.md ├── LICENSE ├── package.json └── webpack.config.js /requirements.txt: -------------------------------------------------------------------------------- 1 | chromadb -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /client/src/public/corgi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weblab-workshops/catbook-react/HEAD/client/src/public/corgi.jpg -------------------------------------------------------------------------------- /client/src/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weblab-workshops/catbook-react/HEAD/client/src/public/favicon.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | client/dist/bundle.js 2 | node_modules/ 3 | venv/ 4 | .venv/ 5 | .DS_Store 6 | .env 7 | chroma.log 8 | chroma_data/ 9 | -------------------------------------------------------------------------------- /client/dist/player-icons/blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weblab-workshops/catbook-react/HEAD/client/dist/player-icons/blue.png -------------------------------------------------------------------------------- /client/dist/player-icons/red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weblab-workshops/catbook-react/HEAD/client/dist/player-icons/red.png -------------------------------------------------------------------------------- /client/dist/player-icons/green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weblab-workshops/catbook-react/HEAD/client/dist/player-icons/green.png -------------------------------------------------------------------------------- /client/dist/player-icons/orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weblab-workshops/catbook-react/HEAD/client/dist/player-icons/orange.png -------------------------------------------------------------------------------- /client/dist/player-icons/purple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weblab-workshops/catbook-react/HEAD/client/dist/player-icons/purple.png -------------------------------------------------------------------------------- /client/dist/player-icons/silver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weblab-workshops/catbook-react/HEAD/client/dist/player-icons/silver.png -------------------------------------------------------------------------------- /client/dist/player-icons/yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weblab-workshops/catbook-react/HEAD/client/dist/player-icons/yellow.png -------------------------------------------------------------------------------- /client/src/components/App.css: -------------------------------------------------------------------------------- 1 | .App-container { 2 | max-width: 960px; 3 | padding: var(--m) var(--m); 4 | margin: auto; 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "trailingComma": "es5", 8 | "arrowParens": "always" 9 | } -------------------------------------------------------------------------------- /client/src/components/pages/Game.css: -------------------------------------------------------------------------------- 1 | .Game-winner { 2 | position: absolute; 3 | top: 50%; 4 | left: 50%; 5 | transform: translate(-50%, -50%); 6 | padding: 40px; 7 | background-color: #333333; 8 | color: white; 9 | } 10 | -------------------------------------------------------------------------------- /server/models/document.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const DocumentSchema = new mongoose.Schema({ 4 | content: String, 5 | }); 6 | 7 | // compile model from schema 8 | module.exports = mongoose.model("document", DocumentSchema); 9 | -------------------------------------------------------------------------------- /client/src/components/modules/Chat.css: -------------------------------------------------------------------------------- 1 | .Chat-container { 2 | height: 100%; 3 | } 4 | 5 | .Chat-newContainer { 6 | width: 100%; 7 | } 8 | 9 | .Chat-historyContainer { 10 | flex-grow: 1; 11 | overflow-y: auto; 12 | overflow-x: hidden; 13 | } 14 | -------------------------------------------------------------------------------- /client/src/components/modules/Document.css: -------------------------------------------------------------------------------- 1 | .Document-save-button { 2 | border-radius: 0; 3 | } 4 | 5 | .NewDocument { 6 | margin-top: 2rem; 7 | } 8 | 9 | .CorpusContainer { 10 | height: 40vh; 11 | overflow-y: scroll; 12 | scroll-behavior: smooth; 13 | } -------------------------------------------------------------------------------- /server/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const UserSchema = new mongoose.Schema({ 4 | name: String, 5 | googleid: String, 6 | }); 7 | 8 | // compile model from schema 9 | module.exports = mongoose.model("user", UserSchema); 10 | -------------------------------------------------------------------------------- /client/src/components/pages/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const NotFound = () => { 4 | return ( 5 |
6 |

404 Not Found

7 |

The page you requested couldn't be found.

8 |
9 | ); 10 | }; 11 | 12 | export default NotFound; 13 | -------------------------------------------------------------------------------- /client/src/components/pages/Chatbook.css: -------------------------------------------------------------------------------- 1 | .Chatbook-container { 2 | height: calc(100vh - 120px); 3 | width: 100% 4 | } 5 | 6 | .Chatbook-userList { 7 | flex: 0 0 220px; 8 | margin-right: var(--l); 9 | } 10 | 11 | .Chatbook-chatContainer { 12 | flex-grow: 1; 13 | overflow-x: hidden; 14 | } 15 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./components/App.js"; 4 | 5 | // renders React Component "Root" into the DOM element with ID "root" 6 | ReactDOM.render(, document.getElementById("root")); 7 | 8 | // allows for live updating 9 | module.hot.accept(); 10 | -------------------------------------------------------------------------------- /server/models/story.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | //define a story schema for the database 4 | const StorySchema = new mongoose.Schema({ 5 | creator_id: String, 6 | creator_name: String, 7 | content: String, 8 | }); 9 | 10 | // compile model from schema 11 | module.exports = mongoose.model("story", StorySchema); 12 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "targets": { "browsers": ["last 2 versions"] }, 5 | "corejs": "3", 6 | "useBuiltIns": "usage" 7 | }], 8 | "@babel/react", 9 | { 10 | "plugins" : ["@babel/plugin-transform-class-properties"] 11 | } 12 | 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /client/src/components/modules/SingleUser.css: -------------------------------------------------------------------------------- 1 | .SingleUser-container { 2 | background: var(--white); 3 | border-radius: var(--xs); 4 | padding: var(--m) var(--l); 5 | margin-bottom: var(--s); 6 | } 7 | 8 | .SingleUser-container:hover { 9 | background: var(--medgrey); 10 | } 11 | 12 | .SingleUser-container--active { 13 | background: var(--grey); 14 | } 15 | -------------------------------------------------------------------------------- /client/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Gamebook 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /client/src/components/modules/CatHappiness.css: -------------------------------------------------------------------------------- 1 | .CatHappiness-container { 2 | border: var(--darkgrey) 1px solid; 3 | border-radius: var(--xs); 4 | margin-top: var(--m); 5 | } 6 | 7 | .CatHappiness-container:first-of-type { 8 | margin-top: var(--l); 9 | } 10 | 11 | .CatHappiness-story { 12 | padding: var(--m) var(--l); 13 | } 14 | 15 | .CatHappiness-storyContent { 16 | font-size: 40px; 17 | margin: var(--s) 0 0; 18 | } 19 | -------------------------------------------------------------------------------- /client/src/input.js: -------------------------------------------------------------------------------- 1 | import { move } from "./client-socket"; 2 | 3 | /** Callback function that calls correct movement from key */ 4 | export const handleInput = (e) => { 5 | if (e.key === "ArrowUp") { 6 | move("up"); 7 | } else if (e.key === "ArrowDown") { 8 | move("down"); 9 | } else if (e.key === "ArrowLeft") { 10 | move("left"); 11 | } else if (e.key === "ArrowRight") { 12 | move("right"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /server/models/comment.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | //define a comment schema for the database 4 | const CommentSchema = new mongoose.Schema({ 5 | creator_id: String, 6 | creator_name: String, 7 | parent: String, // links to the _id of a parent story (_id is an autogenerated field by Mongoose). 8 | content: String, 9 | }); 10 | 11 | // compile model from schema 12 | module.exports = mongoose.model("comment", CommentSchema); 13 | -------------------------------------------------------------------------------- /client/src/client-socket.js: -------------------------------------------------------------------------------- 1 | import socketIOClient from "socket.io-client"; 2 | import { post } from "./utilities"; 3 | const endpoint = window.location.hostname + ":" + window.location.port; 4 | export const socket = socketIOClient(endpoint); 5 | socket.on("connect", () => { 6 | post("/api/initsocket", { socketid: socket.id }); 7 | }); 8 | 9 | /** send a message to the server with the move you made in game */ 10 | export const move = (dir) => { 11 | socket.emit("move", dir); 12 | }; 13 | -------------------------------------------------------------------------------- /server/models/message.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | //define a message schema for the database 4 | const MessageSchema = new mongoose.Schema({ 5 | sender: { 6 | _id: String, 7 | name: String, 8 | }, 9 | recipient: { 10 | _id: String, 11 | name: String, 12 | }, 13 | timestamp: { type: Date, default: Date.now }, 14 | content: String, 15 | }); 16 | 17 | // compile model from schema 18 | module.exports = mongoose.model("message", MessageSchema); 19 | -------------------------------------------------------------------------------- /client/src/components/modules/CatHappiness.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./CatHappiness.css"; 3 | 4 | /** 5 | * Component that renders cat happiness 6 | * 7 | * Proptypes 8 | * @param {int} catHappiness is how happy your cat is 9 | */ 10 | const CatHappiness = (props) => { 11 | return ( 12 |
13 |
14 |

{props.catHappiness}

15 |
16 |
17 | ); 18 | }; 19 | 20 | export default CatHappiness; 21 | -------------------------------------------------------------------------------- /client/src/components/modules/SingleMessage.css: -------------------------------------------------------------------------------- 1 | .SingleMessage-container { 2 | margin: 0 0 var(--xs); 3 | max-width: 100%; 4 | } 5 | 6 | .SingleMessage-sender { 7 | width: 106px; 8 | margin-right: var(--xs); 9 | flex-shrink: 0; 10 | } 11 | 12 | .SingleMessage-content { 13 | padding: var(--s) var(--m); 14 | background: var(--medgrey); 15 | border-radius: var(--l); 16 | flex-shrink: 1; 17 | overflow-wrap: break-word; 18 | overflow-wrap: anywhere; 19 | } 20 | 21 | .SingleMessage-mine .SingleMessage-content { 22 | background: var(--primary--dim); 23 | color: var(--white); 24 | } 25 | -------------------------------------------------------------------------------- /client/src/components/modules/SingleMessage.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | 3 | import "./SingleMessage.css"; 4 | 5 | /** 6 | * Renders a single chat message 7 | * 8 | * Proptypes 9 | * @param {MessageObject} message 10 | */ 11 | const SingleMessage = (props) => { 12 | return ( 13 |
14 | {props.message.sender.name + ":"} 15 | {props.message.content} 16 |
17 | ); 18 | } 19 | 20 | export default SingleMessage; 21 | -------------------------------------------------------------------------------- /client/src/components/modules/Card.css: -------------------------------------------------------------------------------- 1 | .Card-container { 2 | border: var(--darkgrey) 1px solid; 3 | border-radius: var(--xs); 4 | margin-top: var(--m); 5 | } 6 | 7 | .Card-container:first-of-type { 8 | margin-top: var(--l); 9 | } 10 | 11 | .Card-story, 12 | .Card-commentSection { 13 | padding: var(--m) var(--l); 14 | } 15 | 16 | .Card-storyUser { 17 | font-size: 20px; 18 | font-weight: 600; 19 | } 20 | 21 | .Card-storyContent { 22 | font-size: 40px; 23 | margin: var(--s) 0 0; 24 | } 25 | 26 | .Card-commentSection { 27 | background-color: var(--grey); 28 | border-top: 1px solid var(--darkgrey); 29 | } 30 | 31 | .Card-commentBody { 32 | margin: 0 0 var(--s); 33 | } 34 | -------------------------------------------------------------------------------- /client/src/components/modules/SingleComment.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "@reach/router"; 3 | 4 | /** 5 | * Component to render a single comment 6 | * 7 | * Proptypes 8 | * @param {string} _id of comment 9 | * @param {string} creator_name 10 | * @param {string} creator_id 11 | * @param {string} content of the comment 12 | */ 13 | const SingleComment = (props) => { 14 | return ( 15 |
16 | 17 | {props.creator_name} 18 | 19 | {" | " + props.content} 20 |
21 | ); 22 | }; 23 | 24 | export default SingleComment; 25 | -------------------------------------------------------------------------------- /client/src/components/modules/NavBar.css: -------------------------------------------------------------------------------- 1 | .NavBar-container { 2 | padding: var(--s) var(--m); 3 | background-color: var(--primary); 4 | } 5 | 6 | .NavBar-title { 7 | color: var(--white); 8 | font-size: 20px; 9 | } 10 | 11 | .NavBar-title-red { 12 | color: #ffa4a4; 13 | font-size: 20px; 14 | } 15 | 16 | .NavBar-linkContainer { 17 | margin-left: var(--l); 18 | } 19 | 20 | .NavBar-link, 21 | .NavBar-link:visited { 22 | color: var(--white); 23 | opacity: 0.6; 24 | text-decoration: none; 25 | } 26 | 27 | .NavBar-link:hover { 28 | opacity: 0.8; 29 | } 30 | 31 | .NavBar-link.NavBar-login { 32 | opacity: 1; 33 | } 34 | 35 | .NavBar-link + .NavBar-link { 36 | margin-left: var(--m); 37 | } 38 | -------------------------------------------------------------------------------- /client/src/components/modules/SingleStory.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "@reach/router"; 3 | 4 | /** 5 | * Story is a component that renders creator and content of a story 6 | * 7 | * Proptypes 8 | * @param {string} _id of the story 9 | * @param {string} creator_name 10 | * @param {string} creator_id 11 | * @param {string} content of the story 12 | */ 13 | const SingleStory = (props) => { 14 | return ( 15 |
16 | 17 | {props.creator_name} 18 | 19 |

{props.content}

20 |
21 | ); 22 | }; 23 | 24 | export default SingleStory; 25 | -------------------------------------------------------------------------------- /client/src/components/modules/SingleUser.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | 3 | import "./SingleUser.css"; 4 | 5 | /** 6 | * Component to render an online user 7 | * 8 | * Proptypes 9 | * @param {(UserObject) => ()} setActiveUser function that takes in user, 10 | * sets it to active 11 | * @param {UserObject} user 12 | * @param {boolean} active 13 | */ 14 | const SingleUser = (props) => { 15 | return ( 16 |
{ 21 | props.setActiveUser(props.user); 22 | }} 23 | > 24 | {props.user.name} 25 |
26 | ); 27 | } 28 | 29 | export default SingleUser; 30 | -------------------------------------------------------------------------------- /client/src/components/modules/NewPostInput.css: -------------------------------------------------------------------------------- 1 | .NewPostInput-input, 2 | .NewPostInput-button { 3 | font-family: "Open Sans"; 4 | font-size: 14px; 5 | padding: var(--s) var(--m); 6 | } 7 | 8 | .NewPostInput-input { 9 | flex-grow: 1; 10 | border: var(--darkgrey) 1px solid; 11 | border-radius: var(--xs) 0 0 var(--xs); 12 | transition: box-shadow 0.1s; 13 | } 14 | 15 | .NewPostInput-input:focus-within { 16 | border-color: var(--primary--dim); 17 | box-shadow: 0 0 0 var(--xs) rgba(0, 123, 255, 0.25); 18 | } 19 | 20 | .NewPostInput-button { 21 | color: var(--primary); 22 | border: var(--primary) 1px solid; 23 | border-radius: 0 var(--xs) var(--xs) 0; 24 | background: none; 25 | transition: color 0.1s, background 0.1s; 26 | } 27 | 28 | .NewPostInput-button:hover { 29 | color: var(--white); 30 | background-color: var(--primary); 31 | } 32 | -------------------------------------------------------------------------------- /client/src/components/modules/ChatList.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import SingleUser from "./SingleUser.js"; 3 | 4 | import "./SingleUser.css"; 5 | 6 | /** 7 | * List of users that are online to chat with and all chat 8 | * 9 | * Proptypes 10 | * @param {UserObject[]} users to display 11 | * @param {UserObject} active user in chat 12 | * @param {string} userId id of current logged in user 13 | * @param {(UserObject) => ()} setActiveUser function that takes in user, sets it to active 14 | */ 15 | const ChatList = (props) => { 16 | return ( 17 | <> 18 |

Open Chats

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 |
22 |
23 | {props.comments.map((comment) => ( 24 | 31 | ))} 32 | {props.userId && ( 33 | 34 | )} 35 |
36 |
37 | ); 38 | }; 39 | 40 | export default CommentsBlock; 41 | -------------------------------------------------------------------------------- /client/src/components/pages/Profile.css: -------------------------------------------------------------------------------- 1 | .Profile-avatarContainer { 2 | padding: 0 35%; 3 | } 4 | 5 | .Profile-avatar { 6 | /* make it responsive */ 7 | max-width: 100%; 8 | width: 100%; 9 | height: auto; 10 | display: block; 11 | /* div height to be the same as width*/ 12 | padding-top: 100%; 13 | 14 | /* make it a circle */ 15 | border-radius: 50%; 16 | 17 | /* Centering on image`s center*/ 18 | background-position-y: center; 19 | background-position-x: center; 20 | background-repeat: no-repeat; 21 | 22 | /* it makes the clue thing, takes smaller dimension to fill div */ 23 | background-size: cover; 24 | 25 | /* it is optional, for making this div centered in parent*/ 26 | margin: 0 auto; 27 | top: 0; 28 | left: 0; 29 | right: 0; 30 | bottom: 0; 31 | background-image: url("../../public/corgi.jpg"); 32 | } 33 | 34 | .Profile-name { 35 | font-size: 40px; 36 | font-weight: 300; 37 | } 38 | 39 | .Profile-line { 40 | border-color: var(--grey); 41 | } 42 | 43 | .Profile-subContainer { 44 | flex-grow: 1; 45 | flex-basis: 0; 46 | } 47 | 48 | .Profile-subContainer + .Profile-subContainer { 49 | margin-left: var(--m); 50 | } 51 | 52 | .Profile-subTitle { 53 | font-weight: 300; 54 | font-size: 24px; 55 | margin: var(--m) var(--s); 56 | } 57 | -------------------------------------------------------------------------------- /client/src/components/modules/Chat.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import SingleMessage from "./SingleMessage.js"; 3 | import { NewMessage } from "./NewPostInput.js"; 4 | 5 | import "./Chat.css"; 6 | 7 | /** 8 | * @typedef UserObject 9 | * @property {string} _id 10 | * @property {string} name 11 | */ 12 | /** 13 | * @typedef MessageObject 14 | * @property {UserObject} sender 15 | * @property {string} content 16 | */ 17 | /** 18 | * @typedef ChatData 19 | * @property {MessageObject[]} messages 20 | * @property {UserObject} recipient 21 | */ 22 | 23 | /** 24 | * Renders main chat window including previous messages, 25 | * who is being chatted with, and the new message input. 26 | * 27 | * Proptypes 28 | * @param {ChatData} data 29 | */ 30 | const Chat = (props) => { 31 | return ( 32 |
33 |

Chatting with {props.data.recipient.name}

34 |
35 | {props.data.messages.map((m, i) => ( 36 | 37 | ))} 38 |
39 |
40 | 41 |
42 |
43 | ); 44 | } 45 | 46 | export default Chat; 47 | -------------------------------------------------------------------------------- /client/src/components/modules/Card.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import SingleStory from "./SingleStory.js"; 3 | import CommentsBlock from "./CommentsBlock.js"; 4 | import { get } from "../../utilities"; 5 | 6 | import "./Card.css"; 7 | 8 | /** 9 | * Card is a component for displaying content like stories 10 | * 11 | * Proptypes 12 | * @param {string} _id of the story 13 | * @param {string} creator_name 14 | * @param {string} creator_id 15 | * @param {string} content of the story 16 | */ 17 | const Card = (props) => { 18 | const [comments, setComments] = useState([]); 19 | 20 | useEffect(() => { 21 | get("/api/comment", { parent: props._id }).then((comments) => { 22 | setComments(comments); 23 | }); 24 | }, []); 25 | 26 | // this gets called when the user pushes "Submit", so their 27 | // post gets added to the screen right away 28 | const addNewComment = (commentObj) => { 29 | setComments(comments.concat([commentObj])); 30 | }; 31 | 32 | return ( 33 |
34 | 40 | 47 |
48 | ); 49 | }; 50 | 51 | export default Card; 52 | -------------------------------------------------------------------------------- /client/src/components/pages/Feed.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import Card from "../modules/Card.js"; 3 | import { NewStory } from "../modules/NewPostInput.js"; 4 | 5 | import { get } from "../../utilities"; 6 | 7 | const Feed = (props) => { 8 | const [stories, setStories] = useState([]); 9 | 10 | // called when the "Feed" component "mounts", i.e. 11 | // when it shows up on screen 12 | useEffect(() => { 13 | document.title = "News Feed"; 14 | get("/api/stories").then((storyObjs) => { 15 | let reversedStoryObjs = storyObjs.reverse(); 16 | setStories(reversedStoryObjs); 17 | }); 18 | }, []); 19 | 20 | // this gets called when the user pushes "Submit", so their 21 | // post gets added to the screen right away 22 | const addNewStory = (storyObj) => { 23 | setStories([storyObj].concat(stories)); 24 | }; 25 | 26 | let storiesList = null; 27 | const hasStories = stories.length !== 0; 28 | if (hasStories) { 29 | storiesList = stories.map((storyObj) => ( 30 | 38 | )); 39 | } else { 40 | storiesList =
No stories!
; 41 | } 42 | return ( 43 | <> 44 | {props.userId && } 45 | {storiesList} 46 | 47 | ); 48 | }; 49 | 50 | export default Feed; 51 | -------------------------------------------------------------------------------- /client/src/utilities.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Welcome to your ✨𝔲𝔱𝔦𝔩𝔦𝔱𝔶 𝔰𝔱𝔶𝔩𝔢𝔰✨! 3 | * This file includes utility classes that are super simple 4 | * and can be used to add general styles; variable definitions 5 | * for colors; and styles to html, body, and other high level 6 | * DOMs. 7 | * 8 | * All utility classes start with a `u-` and all do 9 | * one basic CSS thing (for example, making the font-weight 10 | * 600 for bolding) or are super generic. 11 | * 12 | * This is 𝙉𝙊𝙏 the place to define classes for components or 13 | * do rigorous styling. You shoud not need to change this file 14 | * much after initial creation. 15 | */ 16 | 17 | @import url("https://fonts.googleapis.com/css?family=Open+Sans:300,600"); 18 | 19 | :root { 20 | --primary: #396dff; 21 | --primary--dim: #6987db; 22 | --darkgrey: #d4d4d4; 23 | --medgrey: #e0e0e0; 24 | --grey: #f7f7f7; 25 | --white: #fff; 26 | 27 | --xs: 4px; 28 | --s: 8px; 29 | --m: 16px; 30 | --l: 24px; 31 | } 32 | 33 | body { 34 | margin: 0; 35 | padding: 0; 36 | font-family: "Open Sans", sans-serif; 37 | font-weight: 300; 38 | } 39 | 40 | form { 41 | flex-grow: 1; 42 | } 43 | 44 | .u-flex { 45 | display: flex; 46 | } 47 | 48 | .u-flexColumn { 49 | display: flex; 50 | flex-direction: column; 51 | } 52 | 53 | .u-flex-justifyCenter { 54 | justify-content: center; 55 | } 56 | 57 | .u-flex-alignCenter { 58 | align-items: center; 59 | } 60 | 61 | .u-inlineBlock { 62 | display: inline-block; 63 | } 64 | 65 | .u-bold { 66 | font-weight: 600; 67 | } 68 | 69 | .u-textCenter { 70 | text-align: center; 71 | } 72 | 73 | .u-relative { 74 | position: relative; 75 | } 76 | 77 | .u-pointer { 78 | cursor: pointer; 79 | } 80 | 81 | .u-link { 82 | color: var(--primary); 83 | text-decoration: none; 84 | cursor: pointer; 85 | } 86 | 87 | .u-link:hover { 88 | color: var(--primary--dim); 89 | } 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "catbook-react", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server/server.js", 6 | "scripts": { 7 | "hotloader": "webpack serve --config ./webpack.config.js --mode development --port 5050", 8 | "start": "nodemon", 9 | "build": "webpack" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/mit6148-workshops/catbook-react.git" 14 | }, 15 | "author": "", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/mit6148-workshops/catbook-react/issues" 19 | }, 20 | "homepage": "https://github.com/mit6148-workshops/catbook-react#readme", 21 | "engines": { 22 | "node": ">=18.x" 23 | }, 24 | "dependencies": { 25 | "@reach/router": "^1.3.4", 26 | "chromadb": "^1.7.3", 27 | "connect-ensure-login": "^0.1.1", 28 | "core-js": "^3.8.1", 29 | "dotenv": "^16.3.1", 30 | "cors": "^2.8.5", 31 | "express": "^4.17.1", 32 | "express-session": "^1.17.1", 33 | "google-auth-library": "^6.1.3", 34 | "mongoose": "^5.11.9", 35 | "nodemon": "^2.0.6", 36 | "openai": "^4.24.3", 37 | "react": "^16.14.0", 38 | "react-dom": "^16.14.0", 39 | "react-google-login": "^5.2.1", 40 | "react-router-dom": "^5.2.0", 41 | "socket.io": "^2.3.0", 42 | "socket.io-client": "^2.3.1", 43 | "url-loader": "^4.1.1" 44 | }, 45 | "devDependencies": { 46 | "@babel/cli": "^7.23.4", 47 | "@babel/core": "^7.23.6", 48 | "@babel/preset-env": "^7.23.6", 49 | "@babel/preset-react": "^7.23.3", 50 | "babel-loader": "^8.2.2", 51 | "css-loader": "^5.0.1", 52 | "file-loader": "^6.2.0", 53 | "html-webpack-plugin": "^5.5.0", 54 | "react-hot-loader": "^4.13.0", 55 | "style-loader": "^2.0.0", 56 | "webpack": "^5.75.0", 57 | "webpack-cli": "^4.10.0", 58 | "webpack-dev-server": "^4.11.1" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /client/src/components/pages/Profile.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import CatHappiness from "../modules/CatHappiness.js"; 3 | import { get } from "../../utilities"; 4 | 5 | import "../../utilities.css"; 6 | import "./Profile.css"; 7 | 8 | const Profile = (props) => { 9 | const [catHappiness, setCatHappiness] = useState(0); 10 | const [user, setUser] = useState(); 11 | 12 | useEffect(() => { 13 | document.title = "Profile Page"; 14 | get(`/api/user`, { userid: props.userId }).then((userObj) => setUser(userObj)); 15 | }, []); 16 | 17 | const incrementCatHappiness = () => { 18 | setCatHappiness(catHappiness + 1); 19 | }; 20 | 21 | if (!user) { 22 | return
Loading!
; 23 | } 24 | return ( 25 | <> 26 |
{ 29 | incrementCatHappiness(); 30 | }} 31 | > 32 |
33 |
34 |

{user.name}

35 |
36 |
37 |
38 |

About Me

39 |
40 | I am really allergic to cats i don't know why i have a catbook 41 |
42 |
43 |
44 |

Cat Happiness

45 | 46 |
47 |
48 |

My Favorite Type of Cat

49 |
corgi
50 |
51 |
52 | 53 | ); 54 | }; 55 | 56 | export default Profile; 57 | -------------------------------------------------------------------------------- /client/src/components/modules/NavBar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "@reach/router"; 3 | import GoogleLogin, { GoogleLogout } from "react-google-login"; 4 | 5 | import "./NavBar.css"; 6 | 7 | // This identifies your web application to Google's authentication service 8 | const GOOGLE_CLIENT_ID = "395785444978-7b9v7l0ap2h3308528vu1ddnt3rqftjc.apps.googleusercontent.com"; 9 | 10 | /** 11 | * The navigation bar at the top of all pages. Takes no props. 12 | */ 13 | const NavBar = (props) => { 14 | return ( 15 | 57 | ); 58 | }; 59 | 60 | export default NavBar; 61 | -------------------------------------------------------------------------------- /client/src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import NavBar from "./modules/NavBar.js"; 3 | import { Router } from "@reach/router"; 4 | import Feed from "./pages/Feed.js"; 5 | import NotFound from "./pages/NotFound.js"; 6 | import Profile from "./pages/Profile.js"; 7 | import Chatbook from "./pages/Chatbook.js"; 8 | import Game from "./pages/Game.js"; 9 | import LLM from "./pages/LLM.js"; 10 | 11 | import { socket } from "../client-socket.js"; 12 | 13 | import { get, post } from "../utilities"; 14 | 15 | // to use styles, import the necessary CSS files 16 | import "../utilities.css"; 17 | import "./App.css"; 18 | 19 | /** 20 | * Define the "App" component as a function. 21 | */ 22 | const App = () => { 23 | const [userId, setUserId] = useState(null); 24 | 25 | useEffect(() => { 26 | get("/api/whoami").then((user) => { 27 | if (user._id) { 28 | // they are registed in the database, and currently logged in. 29 | setUserId(user._id); 30 | } 31 | }); 32 | }, []); 33 | 34 | const handleLogin = (res) => { 35 | const userToken = res.tokenObj.id_token; 36 | post("/api/login", { token: userToken }).then((user) => { 37 | setUserId(user._id); 38 | post("/api/initsocket", { socketid: socket.id }); 39 | }); 40 | }; 41 | 42 | const handleLogout = () => { 43 | console.log("Logged out successfully!"); 44 | setUserId(null); 45 | post("/api/logout"); 46 | }; 47 | 48 | // required method: whatever is returned defines what 49 | // shows up on screen 50 | return ( 51 | // <> is like a
, but won't show 52 | // up in the DOM tree 53 | <> 54 | 55 |
56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
65 | 66 | ); 67 | }; 68 | 69 | export default App; 70 | -------------------------------------------------------------------------------- /client/src/components/pages/Game.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react"; 2 | import { socket } from "../../client-socket.js"; 3 | import { get, post } from "../../utilities"; 4 | import { drawCanvas } from "../../canvasManager"; 5 | import { handleInput } from "../../input"; 6 | 7 | import "../../utilities.css"; 8 | import "./Game.css"; 9 | 10 | const Game = (props) => { 11 | const [winnerModal, setWinnerModal] = useState(null); 12 | 13 | // add event listener on mount 14 | useEffect(() => { 15 | window.addEventListener("keydown", handleInput); 16 | 17 | // remove event listener on unmount 18 | return () => { 19 | window.removeEventListener("keydown", handleInput); 20 | post("/api/despawn", { userid: props.userId }); 21 | }; 22 | }, []); 23 | 24 | // update game periodically 25 | useEffect(() => { 26 | socket.on("update", (update) => { 27 | processUpdate(update); 28 | }); 29 | }, []); 30 | 31 | const processUpdate = (update) => { 32 | // set winnerModal if update has defined winner 33 | if (update.winner) { 34 | setWinnerModal( 35 |
the winner is {update.winner} yay cool cool
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
Log in before chatting with the LLM
; 40 | } 41 | if (!runnable) { 42 | return ( 43 | <> 44 |
error detected
45 |
this is most likely due to one of two reasons:
46 |
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 |
60 | <> 61 | {props.loading ? ( 62 |
Loading...
63 | ) : ( 64 | <> 65 | {props.corpus.length === 0 &&
Empty!
} 66 | {props.corpus.map((doc) => ( 67 | 74 | ))} 75 | 76 | )} 77 | 78 |
79 |
80 | 81 | {alertContent &&
{alertContent}
} 82 |
83 | 84 | ); 85 | }; 86 | 87 | export default Corpus; 88 | -------------------------------------------------------------------------------- /server/server-socket.js: -------------------------------------------------------------------------------- 1 | const gameLogic = require("./game-logic"); 2 | 3 | let io; 4 | 5 | const userToSocketMap = {}; // maps user ID to socket object 6 | const socketToUserMap = {}; // maps socket ID to user object 7 | 8 | const getAllConnectedUsers = () => Object.values(socketToUserMap); 9 | const getSocketFromUserID = (userid) => userToSocketMap[userid]; 10 | const getUserFromSocketID = (socketid) => socketToUserMap[socketid]; 11 | const getSocketFromSocketID = (socketid) => io.sockets.connected[socketid]; 12 | 13 | /** Send game state to client */ 14 | const sendGameState = () => { 15 | io.emit("update", gameLogic.gameState); 16 | }; 17 | 18 | /** Start running game: game loop emits game states to all clients at 60 frames per second */ 19 | const startRunningGame = () => { 20 | let winResetTimer = 0; 21 | setInterval(() => { 22 | gameLogic.updateGameState(); 23 | sendGameState(); 24 | 25 | // Reset game 5 seconds after someone wins. 26 | if (gameLogic.gameState.winner != null) { 27 | winResetTimer += 1; 28 | } 29 | if (winResetTimer > 60 * 5) { 30 | winResetTimer = 0; 31 | gameLogic.resetWinner(); 32 | } 33 | }, 1000 / 60); // 60 frames per second 34 | }; 35 | 36 | startRunningGame(); 37 | 38 | const addUserToGame = (user) => { 39 | gameLogic.spawnPlayer(user._id); 40 | }; 41 | 42 | const removeUserFromGame = (user) => { 43 | gameLogic.removePlayer(user._id); 44 | }; 45 | 46 | const addUser = (user, socket) => { 47 | const oldSocket = userToSocketMap[user._id]; 48 | if (oldSocket && oldSocket.id !== socket.id) { 49 | // there was an old tab open for this user, force it to disconnect 50 | oldSocket.disconnect(); 51 | delete socketToUserMap[oldSocket.id]; 52 | } 53 | 54 | userToSocketMap[user._id] = socket; 55 | socketToUserMap[socket.id] = user; 56 | io.emit("activeUsers", { activeUsers: getAllConnectedUsers() }); 57 | }; 58 | 59 | const removeUser = (user, socket) => { 60 | if (user) { 61 | delete userToSocketMap[user._id]; 62 | removeUserFromGame(user); // Remove user from game if they disconnect 63 | } 64 | delete socketToUserMap[socket.id]; 65 | io.emit("activeUsers", { activeUsers: getAllConnectedUsers() }); 66 | }; 67 | 68 | module.exports = { 69 | init: (http) => { 70 | io = require("socket.io")(http); 71 | 72 | io.on("connection", (socket) => { 73 | console.log(`socket has connected ${socket.id}`); 74 | socket.on("disconnect", (reason) => { 75 | const user = getUserFromSocketID(socket.id); 76 | removeUser(user, socket); 77 | }); 78 | socket.on("move", (dir) => { 79 | // Listen for moves from client and move player accordingly 80 | const user = getUserFromSocketID(socket.id); 81 | if (user) gameLogic.movePlayer(user._id, dir); 82 | }); 83 | }); 84 | }, 85 | 86 | addUser: addUser, 87 | removeUser: removeUser, 88 | 89 | getSocketFromUserID: getSocketFromUserID, 90 | getUserFromSocketID: getUserFromSocketID, 91 | getSocketFromSocketID: getSocketFromSocketID, 92 | getAllConnectedUsers: getAllConnectedUsers, 93 | addUserToGame: addUserToGame, 94 | removeUserFromGame: removeUserFromGame, 95 | getIo: () => io, 96 | }; 97 | -------------------------------------------------------------------------------- /client/src/components/modules/NewPostInput.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | import "./NewPostInput.css"; 4 | import { post } from "../../utilities"; 5 | 6 | /** 7 | * New Post is a parent component for all input components 8 | * 9 | * Proptypes 10 | * @param {string} defaultText is the placeholder text 11 | * @param {string} storyId optional prop, used for comments 12 | * @param {({storyId, value}) => void} onSubmit: (function) triggered when this post is submitted, takes {storyId, value} as parameters 13 | */ 14 | const NewPostInput = (props) => { 15 | const [value, setValue] = useState(""); 16 | 17 | // called whenever the user types in the new post input box 18 | const handleChange = (event) => { 19 | setValue(event.target.value); 20 | }; 21 | 22 | // called when the user hits "Submit" for a new post 23 | const handleSubmit = (event) => { 24 | event.preventDefault(); 25 | props.onSubmit && props.onSubmit(value); 26 | setValue(""); 27 | }; 28 | 29 | return ( 30 |
31 | 38 | 46 |
47 | ); 48 | }; 49 | 50 | /** 51 | * New Comment is a New Post component for comments 52 | * 53 | * Proptypes 54 | * @param {string} defaultText is the placeholder text 55 | * @param {string} storyId to add comment to 56 | */ 57 | const NewComment = (props) => { 58 | const addComment = (value) => { 59 | const body = { parent: props.storyId, content: value }; 60 | post("/api/comment", body).then((comment) => { 61 | // display this comment on the screen 62 | props.addNewComment(comment); 63 | }); 64 | }; 65 | 66 | return ; 67 | }; 68 | 69 | /** 70 | * New Story is a New Post component for comments 71 | * 72 | * Proptypes 73 | * @param {string} defaultText is the placeholder text 74 | */ 75 | const NewStory = (props) => { 76 | const addStory = (value) => { 77 | const body = { content: value }; 78 | post("/api/story", body).then((story) => { 79 | // display this story on the screen 80 | props.addNewStory(story); 81 | }); 82 | }; 83 | 84 | return ; 85 | }; 86 | 87 | /** 88 | * New Message is a New Message component for messages 89 | * 90 | * Proptypes 91 | * @param {UserObject} recipient is the intended recipient 92 | */ 93 | const NewMessage = (props) => { 94 | const sendMessage = (value) => { 95 | const body = { recipient: props.recipient, content: value }; 96 | post("/api/message", body); 97 | }; 98 | 99 | return ; 100 | }; 101 | 102 | export { NewComment, NewStory, NewMessage, NewPostInput }; 103 | -------------------------------------------------------------------------------- /client/src/components/pages/Chatbook.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import ChatList from "../modules/ChatList.js"; 3 | import Chat from "../modules/Chat.js"; 4 | import { socket } from "../../client-socket.js"; 5 | import { get } from "../../utilities"; 6 | 7 | import "./Chatbook.css"; 8 | 9 | const ALL_CHAT = { 10 | _id: "ALL_CHAT", 11 | name: "ALL CHAT", 12 | }; 13 | 14 | /** 15 | * Page component to display when at the "/chat" route 16 | * 17 | * Proptypes 18 | * @param {string} userId id of current logged in user 19 | */ 20 | const Chatbook = (props) => { 21 | /** 22 | * @typedef UserObject 23 | * @property {string} _id 24 | * @property {string} name 25 | */ 26 | /** 27 | * @typedef MessageObject 28 | * @property {UserObject} sender 29 | * @property {string} content 30 | */ 31 | /** 32 | * @typedef ChatData 33 | * @property {MessageObject[]} messages 34 | * @property {UserObject} recipient 35 | */ 36 | 37 | const [activeUsers, setActiveUsers] = useState([]); 38 | 39 | const [activeChat, setActiveChat] = useState({ 40 | recipient: ALL_CHAT, 41 | messages: [], 42 | }); 43 | 44 | const loadMessageHistory = (recipient) => { 45 | get("/api/chat", { recipient_id: recipient._id }).then((messages) => { 46 | setActiveChat({ 47 | recipient: recipient, 48 | messages: messages, 49 | }); 50 | }); 51 | }; 52 | 53 | useEffect(() => { 54 | document.title = "Chatbook"; 55 | }, []); 56 | 57 | useEffect(() => { 58 | loadMessageHistory(activeChat.recipient); 59 | }, [activeChat.recipient._id]); 60 | 61 | useEffect(() => { 62 | get("/api/activeUsers").then((data) => { 63 | // If user is logged in, we load their chats. If they are not logged in, 64 | // there's nothing to load. (Also prevents data races with socket event) 65 | if (props.userId) { 66 | setActiveUsers([ALL_CHAT].concat(data.activeUsers)); 67 | }; 68 | }); 69 | }, []); 70 | 71 | useEffect(() => { 72 | const addMessages = (data) => { 73 | if ( 74 | (data.recipient._id === activeChat.recipient._id && 75 | data.sender._id === props.userId) || 76 | (data.sender._id === activeChat.recipient._id && 77 | data.recipient._id === props.userId) || 78 | (data.recipient._id === "ALL_CHAT" && activeChat.recipient._id === "ALL_CHAT") 79 | ) { 80 | setActiveChat(prevActiveChat => ({ 81 | recipient: prevActiveChat.recipient, 82 | messages: prevActiveChat.messages.concat(data), 83 | })); 84 | } 85 | }; 86 | socket.on("message", addMessages); 87 | return () => { 88 | socket.off("message", addMessages); 89 | }; 90 | }, [activeChat.recipient._id, props.userId]); 91 | 92 | useEffect(() => { 93 | const callback = (data) => { 94 | setActiveUsers([ALL_CHAT].concat(data.activeUsers)); 95 | }; 96 | socket.on("activeUsers", callback); 97 | return () => { 98 | socket.off("activeUsers", callback); 99 | }; 100 | }, []); 101 | 102 | const setActiveUser = (user) => { 103 | if (user._id !== activeChat.recipient._id) { 104 | setActiveChat({ 105 | recipient: user, 106 | messages: [], 107 | }); 108 | } 109 | }; 110 | 111 | if (!props.userId) { 112 | return
Log in before using Chatbook
; 113 | } 114 | return ( 115 | <> 116 |
117 |
118 | 124 |
125 |
126 | 127 |
128 |
129 | 130 | ); 131 | } 132 | 133 | export default Chatbook; 134 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | server.js -- The core of your server 4 | |-------------------------------------------------------------------------- 5 | | 6 | | This file defines how your server starts up. Think of it as the main() of your server. 7 | | At a high level, this file does the following things: 8 | | - Connect to the database 9 | | - Sets up server middleware (i.e. addons that enable things like json parsing, user login) 10 | | - Hooks up all the backend routes specified in api.js 11 | | - Fowards frontend routes that should be handled by the React router 12 | | - Sets up error handling in case something goes wrong when handling a request 13 | | - Actually starts the webserver 14 | */ 15 | 16 | // validator runs some basic checks to make sure you've set everything up correctly 17 | // this is a tool provided by staff, so you don't need to worry about it 18 | const validator = require("./validator"); 19 | validator.checkSetup(); 20 | 21 | //import libraries needed for the webserver to work! 22 | const http = require("http"); 23 | const bodyParser = require("body-parser"); // allow node to automatically parse POST body requests as JSON 24 | const express = require("express"); // backend framework for our node server. 25 | const session = require("express-session"); // library that stores info about each connected user 26 | const mongoose = require("mongoose"); // library to connect to MongoDB 27 | const path = require("path"); // provide utilities for working with file and directory paths 28 | const cors = require("cors"); // enable cross origin headers 29 | 30 | const api = require("./api"); 31 | const auth = require("./auth"); 32 | require("dotenv").config(); 33 | 34 | // socket stuff 35 | const socketManager = require("./server-socket"); 36 | 37 | // Server configuration below 38 | // TODO change connection URL after setting up your own database 39 | // THIS IS HIDDEN IN A DOTENV FILE 40 | const mongoConnectionURL = process.env.mongoURL; 41 | // TODO change database name to the name you chose 42 | const databaseName = "catbook"; 43 | 44 | // connect to mongodb 45 | mongoose 46 | .connect(mongoConnectionURL, { 47 | useNewUrlParser: true, 48 | useUnifiedTopology: true, 49 | dbName: databaseName, 50 | }) 51 | .then(() => console.log("Connected to MongoDB")) 52 | .catch((err) => console.log(`Error connecting to MongoDB: ${err}`)); 53 | 54 | // create a new express server 55 | const app = express(); 56 | app.use(validator.checkRoutes); 57 | 58 | // Enable cross origin requests 59 | app.use(cors()); 60 | 61 | // set up bodyParser, which allows us to process POST requests 62 | app.use(bodyParser.urlencoded({ extended: false })); 63 | app.use(bodyParser.json()); 64 | 65 | // set up a session, which will persist login data across requests 66 | app.use( 67 | session({ 68 | secret: process.env.SESSION_SECRET, 69 | resave: false, 70 | saveUninitialized: false, 71 | }) 72 | ); 73 | 74 | // this checks if the user is logged in, and populates "req.user" 75 | app.use(auth.populateCurrentUser); 76 | 77 | // connect user-defined routes 78 | app.use("/api", api); 79 | 80 | // load the compiled react files, which will serve /index.html and /bundle.js 81 | const reactPath = path.resolve(__dirname, "..", "client", "dist"); 82 | app.use(express.static(reactPath)); 83 | 84 | // for all other routes, render index.html and let react router handle it 85 | app.get("*", (req, res) => { 86 | res.sendFile(path.join(reactPath, "index.html")); 87 | }); 88 | 89 | // any server errors cause this function to run 90 | app.use((err, req, res, next) => { 91 | const status = err.status || 500; 92 | if (status === 500) { 93 | // 500 means Internal Server Error 94 | console.log("The server errored when processing a request!"); 95 | console.log(err); 96 | } 97 | 98 | res.status(status); 99 | res.send({ 100 | status: status, 101 | message: err.message, 102 | }); 103 | }); 104 | 105 | // hardcode port to 3000 for now 106 | const port = process.env.PORT || 3000; 107 | const server = http.Server(app); 108 | socketManager.init(server); 109 | 110 | server.listen(port, () => { 111 | console.log(`Server running on port: ${port}`); 112 | }); 113 | -------------------------------------------------------------------------------- /server/rag.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const Document = require("./models/document"); 3 | 4 | const ANYSCALE_API_KEY = process.env.ANYSCALE_API_KEY; 5 | const CHROMADB_URI = process.env.CHROMADB_URI || "http://localhost:8000"; 6 | 7 | // some information about this model: https://ai.meta.com/llama/ 8 | const MODEL = "meta-llama/Llama-2-13b-chat-hf"; 9 | 10 | // another common choice of embedding model is text-embedding-ada-002. 11 | // we use gte-large because this is the only embedding model anyscale has access to 12 | const EMBEDDING_MODEL = "thenlper/gte-large"; 13 | 14 | // anyscale uses openAI under the hood! but anyscale gives us $10 free credits 15 | const { OpenAI } = require("openai"); 16 | const anyscale = new OpenAI({ 17 | baseURL: "https://api.endpoints.anyscale.com/v1", 18 | apiKey: ANYSCALE_API_KEY, 19 | }); 20 | 21 | // check whether the api key is valid. 22 | // this is only called on server start, so it does not waste too many resources (and will present expensive server crashes when api keys expire) 23 | let hasapikey = false; 24 | const validateAPIKey = async () => { 25 | try { 26 | await anyscale.chat.completions.create({ 27 | model: "meta-llama/Llama-2-7b-chat-hf", 28 | messages: [{ role: "system", content: "" }], 29 | }); 30 | hasapikey = true; 31 | return hasapikey; 32 | } catch { 33 | console.log("validate api key failed"); 34 | return hasapikey; 35 | } 36 | }; 37 | 38 | const isRunnable = () => hasapikey && collection !== null; 39 | 40 | // embedding helper function 41 | const generateEmbedding = async (document) => { 42 | const embedding = await anyscale.embeddings.create({ 43 | model: EMBEDDING_MODEL, 44 | input: document, 45 | }); 46 | return embedding.data[0].embedding; 47 | }; 48 | 49 | // chat completion helper function 50 | const chatCompletion = async (query, context) => { 51 | const prompt = { 52 | model: MODEL, 53 | messages: [ 54 | { 55 | role: "system", 56 | content: 57 | "Your role is to answer questions for a user. You are given the following context to help you answer questions: \n" + 58 | `${context}. \n` + 59 | "Please do not mention that you were given any context in your response.", 60 | }, 61 | { role: "user", content: `${query}` }, 62 | ], 63 | // temperature controls the variance in the llms responses 64 | // higher temperature = more variance 65 | temperature: 0.7, 66 | }; 67 | const completion = await anyscale.chat.completions.create(prompt); 68 | return completion.choices[0].message.content; 69 | }; 70 | 71 | // initialize vector database 72 | const COLLECTION_NAME = "catbook-collection"; 73 | const { ChromaClient } = require("chromadb"); 74 | const client = new ChromaClient({ 75 | path: CHROMADB_URI, 76 | }); 77 | 78 | let collection = null; 79 | 80 | // sync main and vector dbs 81 | const syncDBs = async () => { 82 | // retrieve all documents 83 | const allDocs = await collection.get(); 84 | // delete all documents 85 | await collection.delete({ 86 | ids: allDocs.ids, 87 | }); 88 | // retrieve corpus from main db 89 | const allMongoDocs = await Document.find({}); 90 | if (allMongoDocs.length === 0) { 91 | // avoid errors associated with passing empty lists to chroma 92 | console.log("number of documents", await collection.count()); 93 | return; 94 | } 95 | const allMongoDocIds = allMongoDocs.map((mongoDoc) => mongoDoc._id.toString()); 96 | const allMongoDocContent = allMongoDocs.map((mongoDoc) => mongoDoc.content); 97 | let allMongoDocEmbeddings = allMongoDocs.map((mongoDoc) => generateEmbedding(mongoDoc.content)); 98 | allMongoDocEmbeddings = await Promise.all(allMongoDocEmbeddings); // ensure embeddings finish generating 99 | // add corpus to vector db 100 | await collection.add({ 101 | ids: allMongoDocIds, 102 | embeddings: allMongoDocEmbeddings, 103 | documents: allMongoDocContent, 104 | }); 105 | console.log("number of documents", await collection.count()); 106 | }; 107 | 108 | const initCollection = async () => { 109 | await validateAPIKey(); 110 | if (!hasapikey) return; 111 | try { 112 | collection = await client.getOrCreateCollection({ 113 | name: COLLECTION_NAME, 114 | }); 115 | // initialize collection embeddings with corpus 116 | // in production, this function should not run that often, so it is OK to resync the two dbs here 117 | await syncDBs(); 118 | console.log("finished initializing chroma collection"); 119 | } catch (error) { 120 | console.log("chromadb not running"); 121 | } 122 | }; 123 | 124 | // This is an async function => we don't know that the collection is 125 | // initialized before someone else runs functions that depend on the 126 | // collection, so we could get null pointer errors when collection = null 127 | // before initCollection() has finished. That's probably okay, but if we 128 | // see errors, it's worth keeping in mind. 129 | initCollection(); 130 | 131 | // retrieving context helper function 132 | const NUM_DOCUMENTS = 2; 133 | const retrieveContext = async (query, k) => { 134 | const queryEmbedding = await generateEmbedding(query); 135 | const results = await collection.query({ 136 | queryEmbeddings: [queryEmbedding], 137 | nResults: k, 138 | }); 139 | return results.documents; 140 | }; 141 | 142 | // RAG 143 | const retrievalAugmentedGeneration = async (query) => { 144 | const context = await retrieveContext(query, NUM_DOCUMENTS); 145 | const llmResponse = await chatCompletion(query, context); 146 | return llmResponse; 147 | }; 148 | 149 | // add a document to collection 150 | const addDocument = async (document) => { 151 | const embedding = await generateEmbedding(document.content); 152 | await collection.add({ 153 | ids: [document._id.toString()], 154 | embeddings: [embedding], 155 | documents: [document.content], 156 | }); 157 | }; 158 | 159 | // update a document in collection 160 | const updateDocument = async (document) => { 161 | await collection.delete({ ids: [document._id.toString()] }); 162 | await addDocument(document); 163 | }; 164 | 165 | // delete a document in collection 166 | const deleteDocument = async (id) => { 167 | await collection.delete({ 168 | ids: [id.toString()], 169 | }); 170 | }; 171 | 172 | module.exports = { 173 | isRunnable: isRunnable, 174 | addDocument: addDocument, 175 | updateDocument: updateDocument, 176 | deleteDocument: deleteDocument, 177 | retrievalAugmentedGeneration: retrievalAugmentedGeneration, 178 | }; 179 | -------------------------------------------------------------------------------- /server/api.js: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | api.js -- server routes 4 | |-------------------------------------------------------------------------- 5 | | 6 | | This file defines the routes for your server. 7 | | 8 | */ 9 | 10 | const express = require("express"); 11 | 12 | // import models so we can interact with the database 13 | const Story = require("./models/story"); 14 | const Comment = require("./models/comment"); 15 | const User = require("./models/user"); 16 | const Message = require("./models/message"); 17 | const Document = require("./models/document"); 18 | 19 | // import authentication library 20 | const auth = require("./auth"); 21 | 22 | // api endpoints: all these paths will be prefixed with "/api/" 23 | const router = express.Router(); 24 | 25 | const socketManager = require("./server-socket"); 26 | const ragManager = require("./rag"); 27 | 28 | // health check API route: if this doesn't return 200, the server is down :( 29 | router.get("/health", (_req, res) => { 30 | res.status(200); 31 | res.send({}); 32 | }); 33 | 34 | router.get("/stories", (req, res) => { 35 | // empty selector means get all documents 36 | Story.find({}).then((stories) => res.send(stories)); 37 | }); 38 | 39 | router.post("/story", auth.ensureLoggedIn, (req, res) => { 40 | const newStory = new Story({ 41 | creator_id: req.user._id, 42 | creator_name: req.user.name, 43 | content: req.body.content, 44 | }); 45 | 46 | newStory.save().then((story) => res.send(story)); 47 | }); 48 | 49 | router.get("/comment", (req, res) => { 50 | Comment.find({ parent: req.query.parent }).then((comments) => { 51 | res.send(comments); 52 | }); 53 | }); 54 | 55 | router.post("/comment", auth.ensureLoggedIn, (req, res) => { 56 | const newComment = new Comment({ 57 | creator_id: req.user._id, 58 | creator_name: req.user.name, 59 | parent: req.body.parent, 60 | content: req.body.content, 61 | }); 62 | 63 | newComment.save().then((comment) => res.send(comment)); 64 | }); 65 | 66 | router.post("/login", auth.login); 67 | router.post("/logout", auth.logout); 68 | router.get("/whoami", (req, res) => { 69 | if (!req.user) { 70 | // not logged in 71 | return res.send({}); 72 | } 73 | 74 | res.send(req.user); 75 | }); 76 | 77 | router.get("/user", (req, res) => { 78 | User.findById(req.query.userid).then((user) => { 79 | res.send(user); 80 | }); 81 | }); 82 | 83 | router.post("/initsocket", (req, res) => { 84 | // do nothing if user not logged in 85 | if (req.user) 86 | socketManager.addUser(req.user, socketManager.getSocketFromSocketID(req.body.socketid)); 87 | res.send({}); 88 | }); 89 | 90 | router.get("/chat", (req, res) => { 91 | let query; 92 | if (req.query.recipient_id === "ALL_CHAT") { 93 | // get any message sent by anybody to ALL_CHAT 94 | query = { "recipient._id": "ALL_CHAT" }; 95 | } else { 96 | // get messages that are from me->you OR you->me 97 | query = { 98 | $or: [ 99 | { "sender._id": req.user._id, "recipient._id": req.query.recipient_id }, 100 | { "sender._id": req.query.recipient_id, "recipient._id": req.user._id }, 101 | ], 102 | }; 103 | } 104 | 105 | Message.find(query).then((messages) => res.send(messages)); 106 | }); 107 | 108 | router.post("/message", auth.ensureLoggedIn, (req, res) => { 109 | console.log(`Received a chat message from ${req.user.name}: ${req.body.content}`); 110 | 111 | // insert this message into the database 112 | const message = new Message({ 113 | recipient: req.body.recipient, 114 | sender: { 115 | _id: req.user._id, 116 | name: req.user.name, 117 | }, 118 | content: req.body.content, 119 | }); 120 | message.save(); 121 | 122 | if (req.body.recipient._id == "ALL_CHAT") { 123 | socketManager.getIo().emit("message", message); 124 | } else { 125 | socketManager.getSocketFromUserID(req.user._id).emit("message", message); 126 | if (req.user._id !== req.body.recipient._id) { 127 | socketManager.getSocketFromUserID(req.body.recipient._id).emit("message", message); 128 | } 129 | } 130 | }); 131 | 132 | router.get("/activeUsers", (req, res) => { 133 | res.send({ activeUsers: socketManager.getAllConnectedUsers() }); 134 | }); 135 | 136 | router.post("/spawn", (req, res) => { 137 | if (req.user) { 138 | socketManager.addUserToGame(req.user); 139 | } 140 | res.send({}); 141 | }); 142 | 143 | router.post("/despawn", (req, res) => { 144 | if (req.user) { 145 | socketManager.removeUserFromGame(req.user); 146 | } 147 | res.send({}); 148 | }); 149 | 150 | router.get("/isrunnable", (req, res) => { 151 | res.send({ isrunnable: ragManager.isRunnable() }); 152 | }); 153 | 154 | router.post("/document", (req, res) => { 155 | const newDocument = new Document({ 156 | content: req.body.content, 157 | }); 158 | 159 | const addDocument = async (document) => { 160 | try { 161 | await document.save(); 162 | await ragManager.addDocument(document); 163 | res.send(document); 164 | } catch (error) { 165 | console.log("error:", error); 166 | res.status(500); 167 | res.send({}); 168 | } 169 | }; 170 | 171 | addDocument(newDocument); 172 | }); 173 | 174 | router.get("/document", (req, res) => { 175 | Document.find({}).then((documents) => res.send(documents)); 176 | }); 177 | 178 | router.post("/updateDocument", (req, res) => { 179 | const updateDocument = async (id) => { 180 | const document = await Document.findById(id); 181 | if (!document) res.send({}); 182 | try { 183 | document.content = req.body.content; 184 | await document.save(); 185 | await ragManager.updateDocument(document); 186 | res.send({}); 187 | } catch (error) { 188 | console.log("error:", error); 189 | res.status(500); 190 | res.send({}); 191 | } 192 | }; 193 | updateDocument(req.body._id); 194 | }); 195 | 196 | router.post("/deleteDocument", (req, res) => { 197 | const deleteDocument = async (id) => { 198 | const document = await Document.findById(id); 199 | if (!document) res.send({}); 200 | try { 201 | await ragManager.deleteDocument(id); 202 | await document.remove(); 203 | res.send({}); 204 | } catch { 205 | // if deleting from the vector db failed (e.g., it doesn't exist) 206 | await document.remove(); 207 | res.send({}); 208 | } 209 | }; 210 | deleteDocument(req.body._id); 211 | }); 212 | 213 | router.post("/query", (req, res) => { 214 | const makeQuery = async () => { 215 | try { 216 | const queryresponse = await ragManager.retrievalAugmentedGeneration(req.body.query); 217 | res.send({ queryresponse }); 218 | } catch (error) { 219 | console.log("error:", error); 220 | res.status(500); 221 | res.send({}); 222 | } 223 | }; 224 | makeQuery(); 225 | }); 226 | 227 | // anything else falls to this "not found" case 228 | router.all("*", (req, res) => { 229 | console.log(`API route not found: ${req.method} ${req.url}`); 230 | res.status(404).send({ msg: "API route not found" }); 231 | }); 232 | 233 | module.exports = router; 234 | -------------------------------------------------------------------------------- /server/game-logic.js: -------------------------------------------------------------------------------- 1 | /** constants */ 2 | const MAP_LENGTH = 500; 3 | const INITIAL_RADIUS = 20; 4 | const MAX_PLAYER_SIZE = 200; 5 | const FOOD_SIZE = 2; 6 | const EDIBLE_RANGE_RATIO = 0.9; 7 | const EDIBLE_SIZE_RATIO = 0.9; 8 | const colors = ["red", "blue", "green", "yellow", "purple", "orange", "silver"]; // colors to use for players 9 | 10 | /** Utils! */ 11 | 12 | /** Helper to generate a random integer */ 13 | const getRandomInt = (min, max) => { 14 | min = Math.ceil(min); 15 | max = Math.floor(max); 16 | return Math.floor(Math.random() * (max - min) + min); // The maximum is exclusive and the minimum is inclusive 17 | }; 18 | 19 | /** Helper to generate a random position on the map */ 20 | const getRandomPosition = () => { 21 | return { 22 | x: getRandomInt(0, MAP_LENGTH), 23 | y: getRandomInt(0, MAP_LENGTH), 24 | }; 25 | }; 26 | 27 | let playersEaten = []; // A list of ids of any players that have just been eaten! 28 | 29 | /** Helper to compute when player 1 tries to eat player 2 */ 30 | const playerAttemptEatPlayer = (pid1, pid2) => { 31 | const player1Position = gameState.players[pid1].position; 32 | const player2Position = gameState.players[pid2].position; 33 | const x1 = player1Position.x; 34 | const y1 = player1Position.y; 35 | const x2 = player2Position.x; 36 | const y2 = player2Position.y; 37 | const dist = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); 38 | if (dist < gameState.players[pid1].radius * EDIBLE_RANGE_RATIO) { 39 | // player 2 is within player 1's eat range 40 | if (gameState.players[pid1].radius * EDIBLE_SIZE_RATIO > gameState.players[pid2].radius) { 41 | // player 1 is big enough to eat player 2 42 | gameState.players[pid1].radius += gameState.players[pid2].radius; 43 | playersEaten.push(pid2); 44 | } 45 | } 46 | }; 47 | 48 | /** Attempts all pairwise eating between players */ 49 | const computePlayersEatPlayers = () => { 50 | if (Object.keys(gameState.players).length >= 2) { 51 | Object.keys(gameState.players).forEach((pid1) => { 52 | Object.keys(gameState.players).forEach((pid2) => { 53 | playerAttemptEatPlayer(pid1, pid2); 54 | }); 55 | }); 56 | } 57 | // Remove players who have been eaten 58 | playersEaten.forEach((playerid) => { 59 | removePlayer(playerid); 60 | }); 61 | playersEaten = []; // Reset players that have just been eaten 62 | }; 63 | 64 | /** Helper to check a player eating a piece of food */ 65 | const playerAttemptEatFood = (pid1, f) => { 66 | const player1Position = gameState.players[pid1].position; 67 | const foodPosition = f.position; 68 | const x1 = player1Position.x; 69 | const y1 = player1Position.y; 70 | const x2 = foodPosition.x; 71 | const y2 = foodPosition.y; 72 | const dist = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); 73 | if (dist < gameState.players[pid1].radius - FOOD_SIZE) { 74 | // food is within player 1's eat range 75 | if (gameState.players[pid1].radius > FOOD_SIZE) { 76 | // player 1 is big enough to eat food 77 | gameState.players[pid1].radius += FOOD_SIZE; 78 | removeFood(f); 79 | } 80 | } 81 | }; 82 | 83 | /** Attempts all pairwise eating between each player and all foods */ 84 | const computePlayersEatFoods = () => { 85 | Object.keys(gameState.players).forEach((pid1) => { 86 | gameState.food.forEach((f) => { 87 | playerAttemptEatFood(pid1, f); 88 | }); 89 | }); 90 | }; 91 | 92 | /** Game state */ 93 | const gameState = { 94 | winner: null, 95 | players: {}, 96 | food: [], 97 | }; 98 | 99 | /** Game logic */ 100 | 101 | /** Adds a player to the game state, initialized with a random location */ 102 | const spawnPlayer = (id) => { 103 | gameState.players[id] = { 104 | position: getRandomPosition(), 105 | radius: INITIAL_RADIUS, 106 | color: colors[Math.floor(Math.random() * colors.length)], 107 | }; 108 | }; 109 | 110 | /** Adds a food to the game state, initialized with a random location */ 111 | const spawnFood = () => { 112 | gameState.food.push({ 113 | position: getRandomPosition(), 114 | radius: FOOD_SIZE, 115 | color: colors[Math.floor(Math.random() * colors.length)], 116 | }); 117 | }; 118 | 119 | /** Moves a player based off the sent data from the "move" socket msg */ 120 | const movePlayer = (id, dir) => { 121 | // Unbounded moves 122 | // if (dir === "up") { 123 | // gameState.players[id].position.y += 10; 124 | // } else if (dir === "down") { 125 | // gameState.players[id].position.y -= 10; 126 | // } else if (dir === "left") { 127 | // gameState.players[id].position.x -= 10; 128 | // } else if (dir === "right") { 129 | // gameState.players[id].position.x += 10; 130 | // } 131 | 132 | // If player doesn't exist, don't move anything 133 | if (gameState.players[id] == undefined) { 134 | return; 135 | } 136 | 137 | // Initialize a desired position to move to 138 | const desiredPosition = { 139 | x: gameState.players[id].position.x, 140 | y: gameState.players[id].position.y, 141 | }; 142 | 143 | // Calculate desired position 144 | if (dir === "up") { 145 | desiredPosition.y += 10; 146 | } else if (dir === "down") { 147 | desiredPosition.y -= 10; 148 | } else if (dir === "left") { 149 | desiredPosition.x -= 10; 150 | } else if (dir === "right") { 151 | desiredPosition.x += 10; 152 | } 153 | 154 | // Keep player in bounds 155 | if (desiredPosition.x > MAP_LENGTH) { 156 | desiredPosition.x = MAP_LENGTH; 157 | } 158 | if (desiredPosition.x < 0) { 159 | desiredPosition.x = 0; 160 | } 161 | if (desiredPosition.y > MAP_LENGTH) { 162 | desiredPosition.y = MAP_LENGTH; 163 | } 164 | if (desiredPosition.y < 0) { 165 | desiredPosition.y = 0; 166 | } 167 | 168 | // Move player 169 | gameState.players[id].position = desiredPosition; 170 | }; 171 | 172 | /** Spawn a food if there are less than 10 foods */ 173 | const checkEnoughFoods = () => { 174 | if (gameState.food.length < 10) { 175 | spawnFood(); 176 | } 177 | }; 178 | 179 | /** Check win condition */ 180 | const checkWin = () => { 181 | const winners = Object.keys(gameState.players).filter((key) => { 182 | // check if player is sufficiently large 183 | const player = gameState.players[key]; 184 | if (player.radius > MAX_PLAYER_SIZE) { 185 | return true; 186 | } 187 | }); 188 | 189 | // WARNING: race condition here; if players' radii become >200 at the same time, game will keep going 190 | if (winners.length === 1) { 191 | gameState.winner = winners[0]; 192 | Object.keys(gameState.players).forEach((key) => { 193 | // remove all players from the game (effectively resetting the game) 194 | removePlayer(key); 195 | }); 196 | } 197 | }; 198 | 199 | /** Update the game state. This function is called once per server tick. */ 200 | const updateGameState = () => { 201 | checkWin(); 202 | computePlayersEatPlayers(); 203 | computePlayersEatFoods(); 204 | checkEnoughFoods(); 205 | }; 206 | 207 | /** Remove a player from the game state if they disconnect or if they get eaten */ 208 | const removePlayer = (id) => { 209 | if (gameState.players[id] != undefined) { 210 | delete gameState.players[id]; 211 | } 212 | }; 213 | 214 | /** Remove a food from the game state if it gets eaten, given reference to food object */ 215 | const removeFood = (f) => { 216 | let ix = gameState.food.indexOf(f); 217 | if (ix !== -1) { 218 | gameState.food.splice(ix, 1); 219 | } 220 | }; 221 | 222 | const resetWinner = () => { 223 | gameState.winner = null; 224 | }; 225 | 226 | module.exports = { 227 | gameState, 228 | spawnPlayer, 229 | movePlayer, 230 | removePlayer, 231 | updateGameState, 232 | resetWinner, 233 | }; 234 | --------------------------------------------------------------------------------