├── .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 | 
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 |
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 |
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 |
25 |
26 |
27 | )
28 | }
29 |
30 | function JamsocketLogo() {
31 | return (
32 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------