├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── Dockerfile.jamsocket ├── README.md ├── jamsocket.config.js ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── app │ ├── globals.css │ ├── icon.svg │ ├── layout.tsx │ └── page.tsx ├── components │ ├── Chat.tsx │ ├── Content.tsx │ ├── Header.tsx │ ├── Home.tsx │ └── Whiteboard.tsx ├── session-backend │ ├── .gitignore │ ├── index.ts │ ├── package-lock.json │ ├── package.json │ └── tsconfig.json ├── types.js └── types.ts ├── tailwind.config.js └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 100, 5 | "semi": false 6 | } 7 | -------------------------------------------------------------------------------- /Dockerfile.jamsocket: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine 2 | WORKDIR /app 3 | COPY src/session-backend/package.json src/session-backend/package-lock.json ./ 4 | RUN npm install 5 | COPY src/types.ts ../types.ts 6 | COPY src/session-backend/index.ts ./ 7 | COPY src/session-backend/tsconfig.json ./ 8 | RUN npx tsc 9 | CMD ["node", "index.js"] 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # openai-assistant-demo 2 | 3 | ![Assistant Demo Gif](https://github.com/jamsocket/openai-assistant-demo/assets/34881756/0a375c67-aa5f-406f-b31b-921896cf5dce) 4 | 5 | 6 | A shared canvas built with the OpenAI Assistant, powered by Jamsocket's session backends. 7 | 8 | ## Tools you'll need 9 | 10 | - [OpenAI API](https://platform.openai.com/docs/overview) 11 | - [Jamsocket](https://jamsocket.com/) 12 | - [Docker](https://www.docker.com/products/docker-desktop/) 13 | 14 | ## Setup 15 | 16 | 1. Clone the repo 17 | 2. `cd` into the repo and `npm install` 18 | 3. Get an [OpenAI API key](https://platform.openai.com/docs/overview) 19 | 4. Make sure Docker is running with `docker ps` 20 | 21 | ## Running the app 22 | 23 | 1. Start the dev server with `npx jamsocket dev` 24 | 2. Run the frontend with `npm run dev` 25 | 3. Navigate to `localhost:3000` 26 | 27 | If you have any questions about how to use Jamsocket or would like to talk through your particular use case, we'd love to chat! Send us an email at [hi@jamsocket.com](mailto:hi@jamsocket.com)! 28 | -------------------------------------------------------------------------------- /jamsocket.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dockerfile: './Dockerfile.jamsocket', 3 | watch: ['./src/session-backend'], 4 | } 5 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | webpack: (config, { isServer }) => { 4 | if (isServer) { 5 | config.externals.push({ 6 | encoding: 'encoding', 7 | bufferutil: 'bufferutil', 8 | 'utf-8-validate': 'utf-8-validate', 9 | 'supports-color': 'supports-color', 10 | }) 11 | } 12 | return config 13 | }, 14 | } 15 | 16 | module.exports = nextConfig 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openai-assistant-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "format": "prettier --write \"**/*.{ts,tsx,js,jsx,md}\"" 11 | }, 12 | "dependencies": { 13 | "@jamsocket/server": "0.2.1", 14 | "@jamsocket/socketio": "0.2.1", 15 | "@types/node": "20.1.4", 16 | "@types/react": "18.2.6", 17 | "@types/react-dom": "18.2.4", 18 | "autoprefixer": "10.4.14", 19 | "eslint": "8.40.0", 20 | "eslint-config-next": "13.4.3", 21 | "next": "13.4.9", 22 | "openai": "^4.18.0", 23 | "postcss": "8.4.23", 24 | "prettier": "^3.1.0", 25 | "prettier-plugin-tailwindcss": "^0.5.7", 26 | "react": "18.2.0", 27 | "react-dom": "18.2.0", 28 | "server-only": "^0.0.1", 29 | "socket.io": "^4.7.2", 30 | "tailwindcss": "3.3.2", 31 | "typescript": "5.0.4" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/app/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css' 2 | import { Inter } from 'next/font/google' 3 | 4 | const inter = Inter({ subsets: ['latin'] }) 5 | 6 | export const metadata = { 7 | title: 'Jamsocket OpenAI Assistant Demo', 8 | description: "Let's build a shared canvas with Jamsocket & OpenAI's Assistant API!", 9 | } 10 | 11 | export default function RootLayout({ children }: { children: React.ReactNode }) { 12 | return ( 13 | 14 | {children} 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import 'server-only' 2 | import HomeContainer from '../components/Home' 3 | import Jamsocket from '@jamsocket/server' 4 | 5 | const WHITEBOARD_NAME = 'openai-assistant-demo/default' 6 | const OPENAI_API_KEY = '[YOUR OPENAI_API_KEY HERE]' 7 | 8 | const jamsocket = Jamsocket.init({ dev: true }) 9 | 10 | export default async function Page() { 11 | const spawnResult = await jamsocket.spawn({ 12 | lock: WHITEBOARD_NAME, 13 | env: { OPENAI_API_KEY }, 14 | }) 15 | return 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Chat.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useEffect, useState } from 'react' 3 | import { useSend } from '@jamsocket/socketio' 4 | 5 | interface ChatProps { 6 | canAcceptMessages: boolean 7 | } 8 | export default function Chat(props: ChatProps) { 9 | const { canAcceptMessages } = props 10 | const [message, setMessage] = useState('') 11 | const [placeholder, setPlaceholder] = useState('Write a message...') 12 | const sendEvent = useSend() 13 | 14 | useEffect(() => { 15 | if (canAcceptMessages) { 16 | setPlaceholder('Write a message...') 17 | } 18 | }, [canAcceptMessages]) 19 | 20 | return ( 21 |
22 |
{ 24 | e.preventDefault() 25 | sendEvent('handle-user-prompt', message) 26 | setPlaceholder(message) 27 | setMessage('') 28 | }} 29 | > 30 | { 39 | setMessage(e.target.value) 40 | }} 41 | /> 42 |
43 |
44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /src/components/Content.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | export default function Content({ children }: { children?: React.ReactNode }) { 4 | return ( 5 |
6 | {children} 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import Chat from './Chat' 3 | 4 | export default function Header({ 5 | children, 6 | updates, 7 | }: { 8 | children?: React.ReactNode 9 | updates: string 10 | }) { 11 | return ( 12 |
13 |
14 |
15 |

16 | 17 | Whiteboard 18 |

19 | {children} 20 |
21 |
22 | 23 |
{updates}
24 |
25 |
26 |
27 | ) 28 | } 29 | 30 | function JamsocketLogo() { 31 | return ( 32 | 33 | 34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /src/components/Home.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from 'react' 4 | import Header from './Header' 5 | import Content from './Content' 6 | import { AvatarList, Spinner, Whiteboard } from './Whiteboard' 7 | import type { Shape, User } from '../types' 8 | import { SocketIOProvider, SessionBackendProvider, useReady, useEventListener, useSend } from '@jamsocket/socketio' 9 | import type { SpawnResult } from '@jamsocket/socketio' 10 | 11 | export default function HomeContainer({ spawnResult }: { spawnResult: SpawnResult }) { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | ) 19 | } 20 | 21 | function Home() { 22 | const ready = useReady() 23 | const sendEvent = useSend() 24 | 25 | const [shapes, setShapes] = useState([]) 26 | const [users, setUsers] = useState([]) 27 | const [updates, setUpdates] = useState('') 28 | 29 | useEventListener('user-entered', (id) => { 30 | const newUser = { cursorX: null, cursorY: null, id } 31 | setUsers((users) => [...users, newUser]) 32 | }) 33 | 34 | useEventListener('user-exited', (id) => { 35 | setUsers((users) => users.filter((p) => p.id !== id)) 36 | }) 37 | 38 | useEventListener('cursor-position', (user) => { 39 | setUsers((users) => users.map((p) => (p.id === user.id ? user : p))) 40 | }) 41 | 42 | useEventListener('snapshot', (shapes) => { 43 | setShapes(shapes) 44 | }) 45 | 46 | useEventListener('updates', (updates) => { 47 | setUpdates(updates) 48 | }) 49 | 50 | useEventListener('update-shape', (shape) => { 51 | setShapes((shapes) => { 52 | const shapeToUpdate = shapes.find((s) => s.id === shape.id) 53 | if (!shapeToUpdate) return [...shapes, shape] 54 | return shapes.map((s) => (s.id === shape.id ? { ...s, ...shape } : s)) 55 | }) 56 | }) 57 | 58 | return ( 59 |
60 |
61 | 62 |
63 | 64 | {ready ? ( 65 | { 69 | sendEvent('cursor-position', { x: position?.x, y: position?.y }) 70 | }} 71 | onCreateShape={(shape) => { 72 | sendEvent('create-shape', shape) 73 | setShapes([...shapes, shape]) 74 | }} 75 | onUpdateShape={(id, shape) => { 76 | sendEvent('update-shape', { id, ...shape }) 77 | setShapes((shapes) => shapes.map((s) => (s.id === id ? { ...s, ...shape } : s))) 78 | }} 79 | /> 80 | ) : ( 81 | 82 | )} 83 | 84 |
85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /src/components/Whiteboard.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState, useEffect, useCallback, useRef } from 'react' 4 | import type { Shape, User } from '../types' 5 | 6 | type WhiteboardProps = { 7 | onCreateShape: (shape: Shape) => void 8 | onUpdateShape: (id: number, shape: Partial) => void 9 | onCursorMove?: (position: { x: number; y: number } | null) => void 10 | shapes: Shape[] 11 | users?: User[] 12 | } 13 | 14 | export function Whiteboard({ 15 | onCreateShape, 16 | onUpdateShape, 17 | onCursorMove = () => {}, 18 | shapes, 19 | users = [], 20 | }: WhiteboardProps) { 21 | const [mode, setMode] = useState<'dragging' | 'creating' | null>(null) 22 | const [selectedShape, setSelectedShape] = useState(null) 23 | const [dragStart, setDragStart] = useState<[number, number] | null>(null) 24 | const [shapeStart, setShapeStart] = useState<[number, number] | null>(null) 25 | 26 | const canvasRef = useRef(null) 27 | const [ctx, setCtx] = useState(null) 28 | 29 | const renderCanvas = useCallback(() => { 30 | if (!ctx) return 31 | ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height) 32 | 33 | ctx.save() 34 | ctx.translate(ctx.canvas.width / 2, ctx.canvas.height / 2) 35 | for (const shape of shapes) { 36 | ctx.fillStyle = shape.color 37 | ctx.fillRect(shape.x, shape.y, shape.w, shape.h) 38 | } 39 | ctx.restore() 40 | }, [ctx, shapes]) 41 | 42 | const resize = useCallback(() => { 43 | if (!canvasRef.current) return 44 | const { width, height } = canvasRef.current.getBoundingClientRect() 45 | canvasRef.current.width = width 46 | canvasRef.current.height = height 47 | renderCanvas() 48 | }, [renderCanvas]) 49 | 50 | const setContext = useCallback( 51 | (canvas: HTMLCanvasElement | null) => { 52 | if (canvas === null) return 53 | canvasRef.current = canvas 54 | resize() 55 | setCtx(canvas.getContext('2d')) 56 | }, 57 | [resize], 58 | ) 59 | 60 | useEffect(() => { 61 | function onMouseUp() { 62 | setMode(null) 63 | setSelectedShape(null) 64 | setDragStart(null) 65 | } 66 | window.addEventListener('mouseup', onMouseUp) 67 | return () => { 68 | window.removeEventListener('mouseup', onMouseUp) 69 | } 70 | }, []) 71 | 72 | useEffect(() => { 73 | window.addEventListener('resize', resize) 74 | return () => { 75 | window.removeEventListener('resize', resize) 76 | } 77 | }, [resize]) 78 | 79 | useEffect(() => { 80 | renderCanvas() 81 | }) 82 | 83 | const onMouseLeave = () => { 84 | onCursorMove(null) 85 | } 86 | 87 | function getMousePosition(event: React.MouseEvent) { 88 | const { x, y, width, height } = event.currentTarget.getBoundingClientRect() 89 | return { 90 | x: event.clientX - x - width / 2, 91 | y: event.clientY - y - height / 2, 92 | } 93 | } 94 | 95 | const onMouseDown = (event: React.MouseEvent) => { 96 | const { x, y } = getMousePosition(event) 97 | const dragStart: [number, number] = [x, y] 98 | 99 | // detect if cursor is clicking a shape 100 | let n = shapes.length 101 | while (n--) { 102 | const shape = shapes[n] 103 | if (pointRectIntersect(x, y, shape.x, shape.y, shape.w, shape.h)) { 104 | setMode('dragging') 105 | setSelectedShape(shape.id) 106 | setDragStart(dragStart) 107 | setShapeStart([shape.x, shape.y]) 108 | return 109 | } 110 | } 111 | 112 | const id = Math.floor(Math.random() * 100000) 113 | const newShape = { x, y, w: 0, h: 0, color: randomColor(), id } 114 | setMode('creating') 115 | setSelectedShape(newShape.id) 116 | setDragStart(dragStart) 117 | onCreateShape(newShape) 118 | } 119 | 120 | const onMouseMove = (event: React.MouseEvent) => { 121 | const { x, y } = getMousePosition(event) 122 | onCursorMove({ x, y }) 123 | 124 | const shape = shapes.find((s) => s.id === selectedShape) 125 | if (!mode || !shape || !dragStart) return 126 | if (mode === 'dragging') { 127 | if (!shapeStart) throw new Error('Should be a shapeStart here') 128 | const dx = x - dragStart[0] 129 | const dy = y - dragStart[1] 130 | onUpdateShape(shape.id, { 131 | x: shapeStart[0] + dx, 132 | y: shapeStart[1] + dy, 133 | w: shape.w, 134 | h: shape.h, 135 | }) 136 | } else if (mode === 'creating') { 137 | onUpdateShape(shape.id, { 138 | w: Math.abs(dragStart[0] - x), 139 | h: Math.abs(dragStart[1] - y), 140 | x: Math.min(dragStart[0], x), 141 | y: Math.min(dragStart[1], y), 142 | }) 143 | } 144 | } 145 | 146 | const cursors = users 147 | .filter((u) => !!u.cursorX && !!u.cursorY && ctx) 148 | .map((u) => ({ 149 | color: getColorFromStr(u.id), 150 | x: u.cursorX! + ctx!.canvas.width / 2, 151 | y: u.cursorY! + ctx!.canvas.height / 2, 152 | })) 153 | 154 | return ( 155 |
156 | 163 | {shapes.length > 0 ? null : ( 164 |
165 |
Click and drag to draw rectangles
166 |
167 | )} 168 | 169 |
170 | ) 171 | } 172 | 173 | function Cursors({ cursors }: { cursors: { x: number; y: number; color: string }[] }) { 174 | return ( 175 |
176 | {cursors.map(({ x, y, color }, idx) => { 177 | return ( 178 | 188 | 189 | 193 | 194 | ) 195 | })} 196 |
197 | ) 198 | } 199 | 200 | function pointRectIntersect( 201 | x: number, 202 | y: number, 203 | rectX: number, 204 | rectY: number, 205 | rectW: number, 206 | rectH: number, 207 | ) { 208 | return !(x < rectX || y < rectY || x > rectX + rectW || y > rectY + rectH) 209 | } 210 | 211 | const HUE_OFFSET = (Math.random() * 360) | 0 212 | function randomColor() { 213 | const h = (Math.random() * 60 + HUE_OFFSET) % 360 | 0 214 | const s = (Math.random() * 10 + 30) | 0 215 | const l = (Math.random() * 20 + 30) | 0 216 | return `hsl(${h}, ${s}%, ${l}%)` 217 | } 218 | 219 | export function AvatarList({ users }: { users: User[] }) { 220 | return ( 221 |
222 | {users.map((u, i) => { 223 | const color = getColorFromStr(u.id) 224 | return ( 225 | 229 | 230 | 231 | 232 | 233 | ) 234 | })} 235 |
236 | ) 237 | } 238 | 239 | export function Spinner() { 240 | return ( 241 |
242 |
243 | Loading... 244 |
245 | ) 246 | } 247 | 248 | function getColorFromStr(str: string) { 249 | const hash = str.split('').reduce((acc, char) => (acc << 5) - acc + char.charCodeAt(0), 0) 250 | const hue = Math.abs(hash) % 360 251 | return `hsl(${hue}, 65%, 65%)` 252 | } 253 | -------------------------------------------------------------------------------- /src/session-backend/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | -------------------------------------------------------------------------------- /src/session-backend/index.ts: -------------------------------------------------------------------------------- 1 | import { Server, type Socket } from 'socket.io' 2 | import type { Shape } from '../types' 3 | import OpenAI from 'openai' 4 | 5 | const OPENAI_API_KEY = process.env.OPENAI_API_KEY 6 | if (!OPENAI_API_KEY) { 7 | throw new Error('OPENAI_API_KEY is not set in the environment') 8 | } 9 | const openai = new OpenAI({ apiKey: OPENAI_API_KEY }) 10 | // Create an assistant 11 | // In the tools parameter, I am supplying an array of tools with a specific JSON structure 12 | const assistant = await openai.beta.assistants.create({ 13 | instructions: 14 | 'You are a helpful AI Assistant whose job is to help your users create and edit shapes on a canvas based on their instructions. The canvas is a 2D plane with an x and y axis. The y axis goes from negative (top) to positive (bottom). The x axis goes from negative (left) to positive (right). [0, 0] is in the middle of the screen', 15 | model: 'gpt-4-1106-preview', 16 | tools: getWhiteboardTools(), 17 | }) 18 | 19 | // Create a thread for this user session 20 | const thread = await openai.beta.threads.create() 21 | 22 | // a function that starts a run and continues to poll until relevant tasks are executed or fail 23 | async function handleUserPrompt(socket: Socket, message: string): Promise { 24 | return new Promise(async (resolve, reject) => { 25 | // structure the message with context on the existing whiteboard. 26 | let messageWithContext = addMessageContext(message) 27 | 28 | // add a message to the thread 29 | await openai.beta.threads.messages.create(thread.id, { 30 | role: 'user', 31 | content: messageWithContext, 32 | }) 33 | // create a run to process the message 34 | activeRun = await openai.beta.threads.runs.create(thread.id, { 35 | assistant_id: assistant.id, 36 | }) 37 | 38 | let runResult: OpenAI.Beta.Threads.Runs.Run | undefined 39 | 40 | try { 41 | // get the run result 42 | runResult = await openai.beta.threads.runs.retrieve(thread.id, activeRun.id) 43 | 44 | const failedStatus = ['cancelled', 'failed', 'expired'] 45 | if (failedStatus.includes(runResult?.status)) { 46 | activeRun = null 47 | socket.emit('updates', `Process failed with status: ${runResult.status}`) 48 | reject(new Error(`run result failed with status: ${runResult.status}`)) 49 | } 50 | 51 | // while the run status is still in an accepted status, keep waiting for new status updates 52 | const pendingStatus = ['in_progress', 'queued', 'requires_action'] 53 | while (pendingStatus.includes(runResult?.status)) { 54 | // poll the run every second if the run is still in progress 55 | if (runResult?.status === 'in_progress' || runResult?.status === 'queued') { 56 | socket.emit('updates', `Processing your request...`) 57 | await sleep(1000) 58 | if (activeRun) { 59 | runResult = await openai.beta.threads.runs.retrieve(thread.id, activeRun.id) 60 | } 61 | continue 62 | } 63 | 64 | const toolCalls = runResult?.required_action?.submit_tool_outputs.tool_calls ?? [] 65 | const toolOutputs = toolCalls.map((call) => { 66 | const functionArgs = JSON.parse(call.function.arguments) 67 | const fn = functions[call.function.name] 68 | if (!fn) { 69 | socket.emit('updates', `Process encountered errors, restarting...`) 70 | console.error('function name did not match accepted function arguments') 71 | return { 72 | tool_call_id: call.id ?? '', 73 | output: 'error: couldnt find function', 74 | } 75 | } 76 | 77 | let fnStatus = '' 78 | // try calling the relevant function with arguments supplied by openai 79 | // if there is an error, update the output 80 | try { 81 | fn(functionArgs) 82 | fnStatus = 'Success. New shapes array: ' + JSON.stringify(shapes) 83 | } catch (err) { 84 | socket.emit('updates', `Process encountered errors, restarting...`) 85 | if (err instanceof Error) { 86 | fnStatus = err.toString() 87 | } else { 88 | fnStatus = 'unknown error occured' 89 | } 90 | } 91 | return { 92 | tool_call_id: call.id ?? '', 93 | output: `${fnStatus}`, 94 | } 95 | }) 96 | 97 | if (activeRun) { 98 | // send the tool outputs to openai 99 | runResult = await openai.beta.threads.runs.submitToolOutputs(thread.id, activeRun.id, { 100 | tool_outputs: toolOutputs, 101 | }) 102 | } 103 | } 104 | activeRun = null 105 | socket.emit('updates', '') 106 | resolve() 107 | } catch (error) { 108 | console.error('Error retrieving the run:', error) 109 | reject() 110 | } 111 | }) 112 | } 113 | 114 | // Start a socket IO server that can be used to communicate between the client and server 115 | const io = new Server(8080, { cors: { origin: '*' } }) 116 | 117 | // shapes holds the state of the whiteboard for a user session 118 | let shapes: Shape[] = [] 119 | 120 | // users array holds the state of active users 121 | const users: Set<{ id: string; socket: Socket }> = new Set() 122 | 123 | // activeRun is a variable used to check whether new messages can be processed or if an active run is already in place 124 | let activeRun: OpenAI.Beta.Threads.Runs.Run | null = null 125 | 126 | io.on('connection', async (socket: Socket) => { 127 | console.log('New user connected:', socket.id) 128 | socket.emit('snapshot', shapes) 129 | const newUser = { id: socket.id, socket } 130 | users.add(newUser) 131 | 132 | // send all existing users a 'user-entered' event for the new user 133 | socket.broadcast.emit('user-entered', newUser.id) 134 | 135 | // send the new user a 'user-entered' event for each existing user 136 | for (const user of users) { 137 | newUser.socket.emit('user-entered', user.id) 138 | } 139 | 140 | // receive a user message. this is the prompt that we'll send to the openai assistant along with some context. 141 | socket.on('handle-user-prompt', async (message) => { 142 | if (activeRun === null) { 143 | // a function that polls the run status and executes relevant tasks 144 | await handleUserPrompt(socket, message) 145 | } 146 | // send updated shapes array to the client 147 | socket.emit('snapshot', shapes) 148 | }) 149 | 150 | socket.on('create-shape', async (shape) => { 151 | shapes.push(shape) 152 | socket.broadcast.emit('snapshot', shapes) 153 | }) 154 | 155 | socket.on('update-shape', (updatedShape) => { 156 | const shape = shapes.find((s) => s.id === updatedShape.id) 157 | if (!shape) return 158 | shape.x = updatedShape.x 159 | shape.y = updatedShape.y 160 | shape.w = updatedShape.w 161 | shape.h = updatedShape.h 162 | socket.broadcast.emit('update-shape', shape) 163 | }) 164 | 165 | socket.on('cursor-position', ({ x, y }) => { 166 | socket.volatile.broadcast.emit('cursor-position', { id: socket.id, cursorX: x, cursorY: y }) 167 | }) 168 | 169 | socket.on('disconnect', () => { 170 | users.delete(newUser) 171 | socket.broadcast.emit('user-exited', newUser.id) 172 | }) 173 | }) 174 | 175 | async function sleep(ms: number): Promise { 176 | return new Promise((resolve) => { 177 | setTimeout(resolve, ms) 178 | }) 179 | } 180 | 181 | // Whiteboard functions that the Assistant can call 182 | function getWhiteboardTools(): OpenAI.Beta.Assistants.AssistantCreateParams.AssistantToolsFunction[] { 183 | return [ 184 | { 185 | type: 'function', 186 | function: { 187 | name: 'createShape', 188 | description: 'Create a rectangle in a whiteboard', 189 | parameters: { 190 | type: 'object', 191 | properties: { 192 | x: { type: 'number', description: 'x position' }, 193 | y: { type: 'number', description: 'y position' }, 194 | w: { type: 'number', description: 'width of rectangle' }, 195 | h: { type: 'number', description: 'height of rectangle' }, 196 | color: { 197 | type: 'string', 198 | description: "hsl(_, _%, _%) if a color isn't specified, just use black.", 199 | }, 200 | }, 201 | required: ['x', 'y', 'w', 'h'], 202 | }, 203 | }, 204 | }, 205 | { 206 | type: 'function', 207 | function: { 208 | name: 'editExistingShape', 209 | description: 210 | 'Updates the properties for a shape given its id. For example, if the shape array looks like this [{id: 1234, x: 0, y: 0, color: `hsl(0, 0%, 0%)`}] and my user request is to move this shape to the left, I should return {id: 1234, x: -10}', 211 | parameters: { 212 | type: 'object', 213 | properties: { 214 | id: { type: 'number', description: 'the id of the existing shape to edit' }, 215 | x: { type: 'number', description: 'x position' }, 216 | y: { type: 'number', description: 'y position' }, 217 | w: { type: 'number', description: 'width of rectangle' }, 218 | h: { type: 'number', description: 'height of rectangle' }, 219 | color: { 220 | type: 'string', 221 | description: "hsl(_, _%, _%) if a color isn't specified, just use black.", 222 | }, 223 | }, 224 | required: ['id'], 225 | }, 226 | }, 227 | }, 228 | ] 229 | } 230 | 231 | // functions we call in pollRun depending on the required action by openai 232 | const functions: Record void> = { 233 | createShape(toolOutput: any) { 234 | if ( 235 | !Number.isFinite(toolOutput?.x) || 236 | !Number.isFinite(toolOutput?.y) || 237 | !Number.isFinite(toolOutput?.w) || 238 | !Number.isFinite(toolOutput?.h) 239 | ) { 240 | throw new Error('required params were not given') 241 | } 242 | // create a new shape and add it to the shapes array 243 | const generatedShape: Shape = { 244 | x: toolOutput.x, 245 | y: toolOutput.y, 246 | w: toolOutput.w, 247 | h: toolOutput.h, 248 | color: toolOutput?.color ?? `hsl(0, 0%, 0%)`, 249 | id: Math.floor(Math.random() * 100000), 250 | } 251 | shapes.push(generatedShape) 252 | }, 253 | editExistingShape(toolOutput: any) { 254 | // find the shape to update based on id 255 | let editShape = shapes.find((shape) => shape.id === toolOutput.id) 256 | if (!editShape) { 257 | throw new Error('could not find shape') 258 | } 259 | // update the relevant parameters 260 | editShape.x = toolOutput?.x ?? editShape.x 261 | editShape.y = toolOutput?.y ?? editShape.y 262 | editShape.w = toolOutput?.w ?? editShape.w 263 | editShape.h = toolOutput?.h ?? editShape.h 264 | editShape.color = toolOutput?.color ?? editShape.color 265 | }, 266 | } 267 | 268 | function addMessageContext(message: string): string { 269 | return `\ 270 | This is the user's request: 271 | 272 | ${message}. 273 | 274 | These are the current shapes on the canvas: 275 | 276 | ${JSON.stringify(shapes, null, 2)} 277 | 278 | Remember, the y axis goes from negative (top) to positive (bottom). The x axis goes from negative (left) to positive (right). [0, 0] is in the middle of the screen. 279 | ` 280 | } 281 | -------------------------------------------------------------------------------- /src/session-backend/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openai-session-backend", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "name": "openai-session-backend", 8 | "dependencies": { 9 | "openai": "^4.18.0", 10 | "socket.io": "^4.6.1" 11 | }, 12 | "devDependencies": { 13 | "typescript": "^5.1.6" 14 | } 15 | }, 16 | "node_modules/@socket.io/component-emitter": { 17 | "version": "3.1.0", 18 | "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", 19 | "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" 20 | }, 21 | "node_modules/@types/cookie": { 22 | "version": "0.4.1", 23 | "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", 24 | "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" 25 | }, 26 | "node_modules/@types/cors": { 27 | "version": "2.8.16", 28 | "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.16.tgz", 29 | "integrity": "sha512-Trx5or1Nyg1Fq138PCuWqoApzvoSLWzZ25ORBiHMbbUT42g578lH1GT4TwYDbiUOLFuDsCkfLneT2105fsFWGg==", 30 | "dependencies": { 31 | "@types/node": "*" 32 | } 33 | }, 34 | "node_modules/@types/node": { 35 | "version": "18.18.9", 36 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.9.tgz", 37 | "integrity": "sha512-0f5klcuImLnG4Qreu9hPj/rEfFq6YRc5n2mAjSsH+ec/mJL+3voBH0+8T7o8RpFjH7ovc+TRsL/c7OYIQsPTfQ==", 38 | "dependencies": { 39 | "undici-types": "~5.26.4" 40 | } 41 | }, 42 | "node_modules/@types/node-fetch": { 43 | "version": "2.6.9", 44 | "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.9.tgz", 45 | "integrity": "sha512-bQVlnMLFJ2d35DkPNjEPmd9ueO/rh5EiaZt2bhqiSarPjZIuIV6bPQVqcrEyvNo+AfTrRGVazle1tl597w3gfA==", 46 | "dependencies": { 47 | "@types/node": "*", 48 | "form-data": "^4.0.0" 49 | } 50 | }, 51 | "node_modules/abort-controller": { 52 | "version": "3.0.0", 53 | "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", 54 | "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", 55 | "dependencies": { 56 | "event-target-shim": "^5.0.0" 57 | }, 58 | "engines": { 59 | "node": ">=6.5" 60 | } 61 | }, 62 | "node_modules/accepts": { 63 | "version": "1.3.8", 64 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 65 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 66 | "dependencies": { 67 | "mime-types": "~2.1.34", 68 | "negotiator": "0.6.3" 69 | }, 70 | "engines": { 71 | "node": ">= 0.6" 72 | } 73 | }, 74 | "node_modules/agentkeepalive": { 75 | "version": "4.5.0", 76 | "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", 77 | "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", 78 | "dependencies": { 79 | "humanize-ms": "^1.2.1" 80 | }, 81 | "engines": { 82 | "node": ">= 8.0.0" 83 | } 84 | }, 85 | "node_modules/asynckit": { 86 | "version": "0.4.0", 87 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 88 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 89 | }, 90 | "node_modules/base-64": { 91 | "version": "0.1.0", 92 | "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", 93 | "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" 94 | }, 95 | "node_modules/base64id": { 96 | "version": "2.0.0", 97 | "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", 98 | "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", 99 | "engines": { 100 | "node": "^4.5.0 || >= 5.9" 101 | } 102 | }, 103 | "node_modules/charenc": { 104 | "version": "0.0.2", 105 | "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", 106 | "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", 107 | "engines": { 108 | "node": "*" 109 | } 110 | }, 111 | "node_modules/combined-stream": { 112 | "version": "1.0.8", 113 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 114 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 115 | "dependencies": { 116 | "delayed-stream": "~1.0.0" 117 | }, 118 | "engines": { 119 | "node": ">= 0.8" 120 | } 121 | }, 122 | "node_modules/cookie": { 123 | "version": "0.4.2", 124 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", 125 | "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", 126 | "engines": { 127 | "node": ">= 0.6" 128 | } 129 | }, 130 | "node_modules/cors": { 131 | "version": "2.8.5", 132 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 133 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 134 | "dependencies": { 135 | "object-assign": "^4", 136 | "vary": "^1" 137 | }, 138 | "engines": { 139 | "node": ">= 0.10" 140 | } 141 | }, 142 | "node_modules/crypt": { 143 | "version": "0.0.2", 144 | "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", 145 | "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", 146 | "engines": { 147 | "node": "*" 148 | } 149 | }, 150 | "node_modules/debug": { 151 | "version": "4.3.4", 152 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 153 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 154 | "dependencies": { 155 | "ms": "2.1.2" 156 | }, 157 | "engines": { 158 | "node": ">=6.0" 159 | }, 160 | "peerDependenciesMeta": { 161 | "supports-color": { 162 | "optional": true 163 | } 164 | } 165 | }, 166 | "node_modules/debug/node_modules/ms": { 167 | "version": "2.1.2", 168 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 169 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 170 | }, 171 | "node_modules/delayed-stream": { 172 | "version": "1.0.0", 173 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 174 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 175 | "engines": { 176 | "node": ">=0.4.0" 177 | } 178 | }, 179 | "node_modules/digest-fetch": { 180 | "version": "1.3.0", 181 | "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.3.0.tgz", 182 | "integrity": "sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==", 183 | "dependencies": { 184 | "base-64": "^0.1.0", 185 | "md5": "^2.3.0" 186 | } 187 | }, 188 | "node_modules/engine.io": { 189 | "version": "6.5.4", 190 | "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", 191 | "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", 192 | "dependencies": { 193 | "@types/cookie": "^0.4.1", 194 | "@types/cors": "^2.8.12", 195 | "@types/node": ">=10.0.0", 196 | "accepts": "~1.3.4", 197 | "base64id": "2.0.0", 198 | "cookie": "~0.4.1", 199 | "cors": "~2.8.5", 200 | "debug": "~4.3.1", 201 | "engine.io-parser": "~5.2.1", 202 | "ws": "~8.11.0" 203 | }, 204 | "engines": { 205 | "node": ">=10.2.0" 206 | } 207 | }, 208 | "node_modules/engine.io-parser": { 209 | "version": "5.2.1", 210 | "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", 211 | "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", 212 | "engines": { 213 | "node": ">=10.0.0" 214 | } 215 | }, 216 | "node_modules/event-target-shim": { 217 | "version": "5.0.1", 218 | "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", 219 | "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", 220 | "engines": { 221 | "node": ">=6" 222 | } 223 | }, 224 | "node_modules/form-data": { 225 | "version": "4.0.0", 226 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 227 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 228 | "dependencies": { 229 | "asynckit": "^0.4.0", 230 | "combined-stream": "^1.0.8", 231 | "mime-types": "^2.1.12" 232 | }, 233 | "engines": { 234 | "node": ">= 6" 235 | } 236 | }, 237 | "node_modules/form-data-encoder": { 238 | "version": "1.7.2", 239 | "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", 240 | "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" 241 | }, 242 | "node_modules/formdata-node": { 243 | "version": "4.4.1", 244 | "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", 245 | "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", 246 | "dependencies": { 247 | "node-domexception": "1.0.0", 248 | "web-streams-polyfill": "4.0.0-beta.3" 249 | }, 250 | "engines": { 251 | "node": ">= 12.20" 252 | } 253 | }, 254 | "node_modules/formdata-node/node_modules/web-streams-polyfill": { 255 | "version": "4.0.0-beta.3", 256 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", 257 | "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", 258 | "engines": { 259 | "node": ">= 14" 260 | } 261 | }, 262 | "node_modules/humanize-ms": { 263 | "version": "1.2.1", 264 | "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", 265 | "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", 266 | "dependencies": { 267 | "ms": "^2.0.0" 268 | } 269 | }, 270 | "node_modules/is-buffer": { 271 | "version": "1.1.6", 272 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", 273 | "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" 274 | }, 275 | "node_modules/md5": { 276 | "version": "2.3.0", 277 | "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", 278 | "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", 279 | "dependencies": { 280 | "charenc": "0.0.2", 281 | "crypt": "0.0.2", 282 | "is-buffer": "~1.1.6" 283 | } 284 | }, 285 | "node_modules/mime-db": { 286 | "version": "1.52.0", 287 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 288 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 289 | "engines": { 290 | "node": ">= 0.6" 291 | } 292 | }, 293 | "node_modules/mime-types": { 294 | "version": "2.1.35", 295 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 296 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 297 | "dependencies": { 298 | "mime-db": "1.52.0" 299 | }, 300 | "engines": { 301 | "node": ">= 0.6" 302 | } 303 | }, 304 | "node_modules/ms": { 305 | "version": "2.1.3", 306 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 307 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 308 | }, 309 | "node_modules/negotiator": { 310 | "version": "0.6.3", 311 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 312 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 313 | "engines": { 314 | "node": ">= 0.6" 315 | } 316 | }, 317 | "node_modules/node-domexception": { 318 | "version": "1.0.0", 319 | "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", 320 | "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", 321 | "funding": [ 322 | { 323 | "type": "github", 324 | "url": "https://github.com/sponsors/jimmywarting" 325 | }, 326 | { 327 | "type": "github", 328 | "url": "https://paypal.me/jimmywarting" 329 | } 330 | ], 331 | "engines": { 332 | "node": ">=10.5.0" 333 | } 334 | }, 335 | "node_modules/node-fetch": { 336 | "version": "2.7.0", 337 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", 338 | "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", 339 | "dependencies": { 340 | "whatwg-url": "^5.0.0" 341 | }, 342 | "engines": { 343 | "node": "4.x || >=6.0.0" 344 | }, 345 | "peerDependencies": { 346 | "encoding": "^0.1.0" 347 | }, 348 | "peerDependenciesMeta": { 349 | "encoding": { 350 | "optional": true 351 | } 352 | } 353 | }, 354 | "node_modules/object-assign": { 355 | "version": "4.1.1", 356 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 357 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 358 | "engines": { 359 | "node": ">=0.10.0" 360 | } 361 | }, 362 | "node_modules/openai": { 363 | "version": "4.19.0", 364 | "resolved": "https://registry.npmjs.org/openai/-/openai-4.19.0.tgz", 365 | "integrity": "sha512-cJbl0noZyAaXVKBTMMq6X5BAvP1pm2rWYDBnZes99NL+Zh5/4NmlAwyuhTZEru5SqGGZIoiYKeMPXy4bm9DI0w==", 366 | "dependencies": { 367 | "@types/node": "^18.11.18", 368 | "@types/node-fetch": "^2.6.4", 369 | "abort-controller": "^3.0.0", 370 | "agentkeepalive": "^4.2.1", 371 | "digest-fetch": "^1.3.0", 372 | "form-data-encoder": "1.7.2", 373 | "formdata-node": "^4.3.2", 374 | "node-fetch": "^2.6.7", 375 | "web-streams-polyfill": "^3.2.1" 376 | }, 377 | "bin": { 378 | "openai": "bin/cli" 379 | } 380 | }, 381 | "node_modules/socket.io": { 382 | "version": "4.7.2", 383 | "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz", 384 | "integrity": "sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==", 385 | "dependencies": { 386 | "accepts": "~1.3.4", 387 | "base64id": "~2.0.0", 388 | "cors": "~2.8.5", 389 | "debug": "~4.3.2", 390 | "engine.io": "~6.5.2", 391 | "socket.io-adapter": "~2.5.2", 392 | "socket.io-parser": "~4.2.4" 393 | }, 394 | "engines": { 395 | "node": ">=10.2.0" 396 | } 397 | }, 398 | "node_modules/socket.io-adapter": { 399 | "version": "2.5.2", 400 | "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", 401 | "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", 402 | "dependencies": { 403 | "ws": "~8.11.0" 404 | } 405 | }, 406 | "node_modules/socket.io-parser": { 407 | "version": "4.2.4", 408 | "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", 409 | "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", 410 | "dependencies": { 411 | "@socket.io/component-emitter": "~3.1.0", 412 | "debug": "~4.3.1" 413 | }, 414 | "engines": { 415 | "node": ">=10.0.0" 416 | } 417 | }, 418 | "node_modules/tr46": { 419 | "version": "0.0.3", 420 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 421 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" 422 | }, 423 | "node_modules/typescript": { 424 | "version": "5.2.2", 425 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", 426 | "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", 427 | "dev": true, 428 | "bin": { 429 | "tsc": "bin/tsc", 430 | "tsserver": "bin/tsserver" 431 | }, 432 | "engines": { 433 | "node": ">=14.17" 434 | } 435 | }, 436 | "node_modules/undici-types": { 437 | "version": "5.26.5", 438 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 439 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" 440 | }, 441 | "node_modules/vary": { 442 | "version": "1.1.2", 443 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 444 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 445 | "engines": { 446 | "node": ">= 0.8" 447 | } 448 | }, 449 | "node_modules/web-streams-polyfill": { 450 | "version": "3.2.1", 451 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", 452 | "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", 453 | "engines": { 454 | "node": ">= 8" 455 | } 456 | }, 457 | "node_modules/webidl-conversions": { 458 | "version": "3.0.1", 459 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 460 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" 461 | }, 462 | "node_modules/whatwg-url": { 463 | "version": "5.0.0", 464 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 465 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 466 | "dependencies": { 467 | "tr46": "~0.0.3", 468 | "webidl-conversions": "^3.0.0" 469 | } 470 | }, 471 | "node_modules/ws": { 472 | "version": "8.11.0", 473 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", 474 | "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", 475 | "engines": { 476 | "node": ">=10.0.0" 477 | }, 478 | "peerDependencies": { 479 | "bufferutil": "^4.0.1", 480 | "utf-8-validate": "^5.0.2" 481 | }, 482 | "peerDependenciesMeta": { 483 | "bufferutil": { 484 | "optional": true 485 | }, 486 | "utf-8-validate": { 487 | "optional": true 488 | } 489 | } 490 | } 491 | }, 492 | "dependencies": { 493 | "@socket.io/component-emitter": { 494 | "version": "3.1.0", 495 | "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", 496 | "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" 497 | }, 498 | "@types/cookie": { 499 | "version": "0.4.1", 500 | "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", 501 | "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" 502 | }, 503 | "@types/cors": { 504 | "version": "2.8.16", 505 | "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.16.tgz", 506 | "integrity": "sha512-Trx5or1Nyg1Fq138PCuWqoApzvoSLWzZ25ORBiHMbbUT42g578lH1GT4TwYDbiUOLFuDsCkfLneT2105fsFWGg==", 507 | "requires": { 508 | "@types/node": "*" 509 | } 510 | }, 511 | "@types/node": { 512 | "version": "18.18.9", 513 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.9.tgz", 514 | "integrity": "sha512-0f5klcuImLnG4Qreu9hPj/rEfFq6YRc5n2mAjSsH+ec/mJL+3voBH0+8T7o8RpFjH7ovc+TRsL/c7OYIQsPTfQ==", 515 | "requires": { 516 | "undici-types": "~5.26.4" 517 | } 518 | }, 519 | "@types/node-fetch": { 520 | "version": "2.6.9", 521 | "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.9.tgz", 522 | "integrity": "sha512-bQVlnMLFJ2d35DkPNjEPmd9ueO/rh5EiaZt2bhqiSarPjZIuIV6bPQVqcrEyvNo+AfTrRGVazle1tl597w3gfA==", 523 | "requires": { 524 | "@types/node": "*", 525 | "form-data": "^4.0.0" 526 | } 527 | }, 528 | "abort-controller": { 529 | "version": "3.0.0", 530 | "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", 531 | "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", 532 | "requires": { 533 | "event-target-shim": "^5.0.0" 534 | } 535 | }, 536 | "accepts": { 537 | "version": "1.3.8", 538 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 539 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 540 | "requires": { 541 | "mime-types": "~2.1.34", 542 | "negotiator": "0.6.3" 543 | } 544 | }, 545 | "agentkeepalive": { 546 | "version": "4.5.0", 547 | "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", 548 | "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", 549 | "requires": { 550 | "humanize-ms": "^1.2.1" 551 | } 552 | }, 553 | "asynckit": { 554 | "version": "0.4.0", 555 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 556 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 557 | }, 558 | "base-64": { 559 | "version": "0.1.0", 560 | "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", 561 | "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" 562 | }, 563 | "base64id": { 564 | "version": "2.0.0", 565 | "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", 566 | "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" 567 | }, 568 | "charenc": { 569 | "version": "0.0.2", 570 | "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", 571 | "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==" 572 | }, 573 | "combined-stream": { 574 | "version": "1.0.8", 575 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 576 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 577 | "requires": { 578 | "delayed-stream": "~1.0.0" 579 | } 580 | }, 581 | "cookie": { 582 | "version": "0.4.2", 583 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", 584 | "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" 585 | }, 586 | "cors": { 587 | "version": "2.8.5", 588 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 589 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 590 | "requires": { 591 | "object-assign": "^4", 592 | "vary": "^1" 593 | } 594 | }, 595 | "crypt": { 596 | "version": "0.0.2", 597 | "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", 598 | "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==" 599 | }, 600 | "debug": { 601 | "version": "4.3.4", 602 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 603 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 604 | "requires": { 605 | "ms": "2.1.2" 606 | }, 607 | "dependencies": { 608 | "ms": { 609 | "version": "2.1.2", 610 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 611 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 612 | } 613 | } 614 | }, 615 | "delayed-stream": { 616 | "version": "1.0.0", 617 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 618 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" 619 | }, 620 | "digest-fetch": { 621 | "version": "1.3.0", 622 | "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.3.0.tgz", 623 | "integrity": "sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==", 624 | "requires": { 625 | "base-64": "^0.1.0", 626 | "md5": "^2.3.0" 627 | } 628 | }, 629 | "engine.io": { 630 | "version": "6.5.4", 631 | "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", 632 | "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", 633 | "requires": { 634 | "@types/cookie": "^0.4.1", 635 | "@types/cors": "^2.8.12", 636 | "@types/node": ">=10.0.0", 637 | "accepts": "~1.3.4", 638 | "base64id": "2.0.0", 639 | "cookie": "~0.4.1", 640 | "cors": "~2.8.5", 641 | "debug": "~4.3.1", 642 | "engine.io-parser": "~5.2.1", 643 | "ws": "~8.11.0" 644 | } 645 | }, 646 | "engine.io-parser": { 647 | "version": "5.2.1", 648 | "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", 649 | "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==" 650 | }, 651 | "event-target-shim": { 652 | "version": "5.0.1", 653 | "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", 654 | "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" 655 | }, 656 | "form-data": { 657 | "version": "4.0.0", 658 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 659 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 660 | "requires": { 661 | "asynckit": "^0.4.0", 662 | "combined-stream": "^1.0.8", 663 | "mime-types": "^2.1.12" 664 | } 665 | }, 666 | "form-data-encoder": { 667 | "version": "1.7.2", 668 | "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", 669 | "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" 670 | }, 671 | "formdata-node": { 672 | "version": "4.4.1", 673 | "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", 674 | "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", 675 | "requires": { 676 | "node-domexception": "1.0.0", 677 | "web-streams-polyfill": "4.0.0-beta.3" 678 | }, 679 | "dependencies": { 680 | "web-streams-polyfill": { 681 | "version": "4.0.0-beta.3", 682 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", 683 | "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==" 684 | } 685 | } 686 | }, 687 | "humanize-ms": { 688 | "version": "1.2.1", 689 | "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", 690 | "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", 691 | "requires": { 692 | "ms": "^2.0.0" 693 | } 694 | }, 695 | "is-buffer": { 696 | "version": "1.1.6", 697 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", 698 | "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" 699 | }, 700 | "md5": { 701 | "version": "2.3.0", 702 | "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", 703 | "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", 704 | "requires": { 705 | "charenc": "0.0.2", 706 | "crypt": "0.0.2", 707 | "is-buffer": "~1.1.6" 708 | } 709 | }, 710 | "mime-db": { 711 | "version": "1.52.0", 712 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 713 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" 714 | }, 715 | "mime-types": { 716 | "version": "2.1.35", 717 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 718 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 719 | "requires": { 720 | "mime-db": "1.52.0" 721 | } 722 | }, 723 | "ms": { 724 | "version": "2.1.3", 725 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 726 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 727 | }, 728 | "negotiator": { 729 | "version": "0.6.3", 730 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 731 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" 732 | }, 733 | "node-domexception": { 734 | "version": "1.0.0", 735 | "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", 736 | "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" 737 | }, 738 | "node-fetch": { 739 | "version": "2.7.0", 740 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", 741 | "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", 742 | "requires": { 743 | "whatwg-url": "^5.0.0" 744 | } 745 | }, 746 | "object-assign": { 747 | "version": "4.1.1", 748 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 749 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" 750 | }, 751 | "openai": { 752 | "version": "4.19.0", 753 | "resolved": "https://registry.npmjs.org/openai/-/openai-4.19.0.tgz", 754 | "integrity": "sha512-cJbl0noZyAaXVKBTMMq6X5BAvP1pm2rWYDBnZes99NL+Zh5/4NmlAwyuhTZEru5SqGGZIoiYKeMPXy4bm9DI0w==", 755 | "requires": { 756 | "@types/node": "^18.11.18", 757 | "@types/node-fetch": "^2.6.4", 758 | "abort-controller": "^3.0.0", 759 | "agentkeepalive": "^4.2.1", 760 | "digest-fetch": "^1.3.0", 761 | "form-data-encoder": "1.7.2", 762 | "formdata-node": "^4.3.2", 763 | "node-fetch": "^2.6.7", 764 | "web-streams-polyfill": "^3.2.1" 765 | } 766 | }, 767 | "socket.io": { 768 | "version": "4.7.2", 769 | "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz", 770 | "integrity": "sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==", 771 | "requires": { 772 | "accepts": "~1.3.4", 773 | "base64id": "~2.0.0", 774 | "cors": "~2.8.5", 775 | "debug": "~4.3.2", 776 | "engine.io": "~6.5.2", 777 | "socket.io-adapter": "~2.5.2", 778 | "socket.io-parser": "~4.2.4" 779 | } 780 | }, 781 | "socket.io-adapter": { 782 | "version": "2.5.2", 783 | "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", 784 | "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", 785 | "requires": { 786 | "ws": "~8.11.0" 787 | } 788 | }, 789 | "socket.io-parser": { 790 | "version": "4.2.4", 791 | "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", 792 | "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", 793 | "requires": { 794 | "@socket.io/component-emitter": "~3.1.0", 795 | "debug": "~4.3.1" 796 | } 797 | }, 798 | "tr46": { 799 | "version": "0.0.3", 800 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 801 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" 802 | }, 803 | "typescript": { 804 | "version": "5.2.2", 805 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", 806 | "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", 807 | "dev": true 808 | }, 809 | "undici-types": { 810 | "version": "5.26.5", 811 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 812 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" 813 | }, 814 | "vary": { 815 | "version": "1.1.2", 816 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 817 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" 818 | }, 819 | "web-streams-polyfill": { 820 | "version": "3.2.1", 821 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", 822 | "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==" 823 | }, 824 | "webidl-conversions": { 825 | "version": "3.0.1", 826 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 827 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" 828 | }, 829 | "whatwg-url": { 830 | "version": "5.0.0", 831 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 832 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 833 | "requires": { 834 | "tr46": "~0.0.3", 835 | "webidl-conversions": "^3.0.0" 836 | } 837 | }, 838 | "ws": { 839 | "version": "8.11.0", 840 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", 841 | "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", 842 | "requires": {} 843 | } 844 | } 845 | } 846 | -------------------------------------------------------------------------------- /src/session-backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openai-session-backend", 3 | "private": true, 4 | "type": "module", 5 | "dependencies": { 6 | "openai": "^4.18.0", 7 | "socket.io": "^4.6.1" 8 | }, 9 | "devDependencies": { 10 | "typescript": "^5.1.6" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/session-backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type Shape = { 2 | x: number 3 | y: number 4 | w: number 5 | h: number 6 | color: string 7 | id: number 8 | } 9 | 10 | export type User = { 11 | cursorX: number | null 12 | cursorY: number | null 13 | id: string 14 | } 15 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}', 5 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 6 | './src/app/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "allowImportingTsExtensions": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "nodenext", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "plugins": [ 19 | { 20 | "name": "next" 21 | } 22 | ], 23 | "paths": { 24 | "@/*": ["./src/*"] 25 | } 26 | }, 27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 28 | "exclude": ["node_modules", "src/session-backend"] 29 | } 30 | --------------------------------------------------------------------------------