├── .github
└── workflows
│ └── release.yml
├── .gitignore
├── .placeholder
├── README.md
├── index.js
└── package.json
├── .vscode
├── cspell.json
├── launch.json
└── settings.json
├── LICENSE
├── README.md
├── VERSION
├── demo
├── backend
│ ├── express
│ │ ├── .gitignore
│ │ ├── package.json
│ │ ├── src
│ │ │ └── index.ts
│ │ ├── tsconfig.json
│ │ └── yarn.lock
│ ├── next
│ │ ├── .eslintrc.json
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── next.config.js
│ │ ├── package.json
│ │ ├── postcss.config.js
│ │ ├── public
│ │ │ └── .gitkeep
│ │ ├── src
│ │ │ ├── app
│ │ │ │ ├── favicon.ico
│ │ │ │ ├── globals.css
│ │ │ │ ├── layout.tsx
│ │ │ │ └── page.tsx
│ │ │ └── pages
│ │ │ │ └── api
│ │ │ │ └── beak
│ │ │ │ └── [...path].ts
│ │ ├── tailwind.config.ts
│ │ ├── tsconfig.json
│ │ └── yarn.lock
│ └── remix
│ │ ├── .eslintrc.cjs
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── app
│ │ ├── entry.client.tsx
│ │ ├── entry.server.tsx
│ │ ├── root.tsx
│ │ ├── routes
│ │ │ ├── _index.tsx
│ │ │ └── beak.$.tsx
│ │ └── styles
│ │ │ └── global.css
│ │ ├── package.json
│ │ ├── public
│ │ └── favicon.ico
│ │ ├── remix.config.js
│ │ ├── remix.env.d.ts
│ │ ├── tsconfig.json
│ │ └── yarn.lock
└── frontend
│ └── presentation
│ ├── .eslintrc.cjs
│ ├── .gitignore
│ ├── README.md
│ ├── index.html
│ ├── package.json
│ ├── public
│ └── .gitkeep
│ ├── src
│ ├── App.css
│ ├── App.tsx
│ ├── index.css
│ ├── main.tsx
│ └── vite-env.d.ts
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ ├── vite.config.ts
│ └── yarn.lock
├── docs
├── deployment
│ ├── express.md
│ ├── next.md
│ └── remix.md
└── img
│ └── screenshot.png
├── package.json
├── packages
├── core
│ ├── package.json
│ ├── src
│ │ ├── beak.ts
│ │ ├── index.ts
│ │ ├── openai.ts
│ │ └── types.ts
│ ├── tsconfig.cjs.json
│ └── tsconfig.esm.json
├── css
│ ├── Assistant.css
│ ├── Button.css
│ ├── Header.css
│ ├── Input.css
│ ├── Messages.css
│ ├── Window.css
│ └── index.css
├── express
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ └── tsconfig.json
├── next
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ └── tsconfig.json
├── openai
│ ├── package.json
│ ├── src
│ │ ├── chat.ts
│ │ ├── index.ts
│ │ ├── openai.ts
│ │ └── types.ts
│ ├── test
│ │ ├── chat.test.ts
│ │ ├── openai.test.ts
│ │ └── utils.ts
│ ├── tsconfig.cjs.json
│ └── tsconfig.esm.json
├── react
│ ├── package.json
│ ├── src
│ │ ├── Assistant.tsx
│ │ ├── Beak.tsx
│ │ ├── Button.tsx
│ │ ├── Header.tsx
│ │ ├── Icons.tsx
│ │ ├── Input.tsx
│ │ ├── Messages.tsx
│ │ ├── Textarea.tsx
│ │ ├── Window.tsx
│ │ ├── hooks.ts
│ │ ├── index.ts
│ │ └── props.ts
│ ├── tsconfig.json
│ └── vite.config.ts
├── remix
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ └── tsconfig.json
└── server
│ ├── package.json
│ ├── src
│ └── index.ts
│ └── tsconfig.json
├── scripts
└── sync-versions.ts
├── tsconfig.json
└── yarn.lock
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release Workflow
2 |
3 | on:
4 | push:
5 | branches:
6 | - release
7 |
8 | jobs:
9 | release:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | - name: Set up Node.js
14 | uses: actions/setup-node@v2
15 | with:
16 | node-version: "20"
17 | registry-url: "https://registry.npmjs.org"
18 | - name: Install dependencies
19 | run: |
20 | yarn install
21 | - name: Build
22 | run: |
23 | yarn build
24 | - name: Publish (openai)
25 | run: |
26 | yarn workspace @beakjs/openai publish --access public
27 | env:
28 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
29 | - name: Publish (core)
30 | run: |
31 | yarn workspace @beakjs/core publish --access public
32 | env:
33 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
34 | - name: Publish (react)
35 | run: |
36 | yarn workspace @beakjs/react publish --access public
37 | env:
38 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
39 | - name: Publish (server)
40 | run: |
41 | yarn workspace @beakjs/server publish --access public
42 | env:
43 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
44 | - name: Publish (next)
45 | run: |
46 | yarn workspace @beakjs/next publish --access public
47 | env:
48 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
49 | - name: Publish (next)
50 | run: |
51 | yarn workspace @beakjs/remix publish --access public
52 | env:
53 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
54 | - name: Publish (express)
55 | run: |
56 | yarn workspace @beakjs/express publish --access public
57 | env:
58 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
59 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | /dist
3 |
4 | # OS files
5 | .DS_Store
6 | Thumbs.db
7 | .cache
8 |
9 | TODO.md
10 | TODO.later.md
11 |
12 | packages/*/dist
--------------------------------------------------------------------------------
/.placeholder/README.md:
--------------------------------------------------------------------------------
1 | # Placeholder Package
2 |
3 | This package is a placeholder.
4 |
5 | Please use [@beakjs/react](https://www.npmjs.com/package/@beakjs/react) instead.
6 |
--------------------------------------------------------------------------------
/.placeholder/index.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mme/beakjs/c837e84da19082ae6ed977381548efb74930d678/.placeholder/index.js
--------------------------------------------------------------------------------
/.placeholder/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "beakjs",
3 | "version": "0.0.4",
4 | "description": "This is a placeholder package.",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mme/beakjs.git"
12 | },
13 | "keywords": [],
14 | "author": "",
15 | "license": "ISC"
16 | }
17 |
--------------------------------------------------------------------------------
/.vscode/cspell.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2",
3 | "language": "en",
4 | "words": [
5 | "langchain",
6 | "openai",
7 | "beakjs",
8 | "tailwindcss",
9 | "headlessui",
10 | "Markus",
11 | "Ecker",
12 | "autosize",
13 | "jsonrepair",
14 | "chatcmpl",
15 | "uuidv4",
16 | "Hola",
17 | "Mundo",
18 | "OpenAIAPIKey",
19 | "nextjs",
20 | "typecheck",
21 | "isbot"
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "chrome",
9 | "request": "launch",
10 | "name": "Debug Demo",
11 | "url": "http://localhost:5173",
12 | "webRoot": "${workspaceFolder}/demo/presentation"
13 | }
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.exclude": {
3 | "**/.cache": true,
4 | "**/node_modules": true,
5 | ".placeholder": true,
6 | "packages/*/dist": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2023 Markus Ecker
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🐦 Beak.js [](https://twitter.com/mme_xyz) [](https://www.npmjs.com/package/@beakjs/react)
2 |
3 | Beak.js contains everything you need to create custom AI-powered assistants for your React app.
4 |
5 | **Key Features:**
6 |
7 | - **Built-in UI** - Comes with a beautiful, fully customizable chat window.
8 | - **Easy to Use** - Integrates with your existing React app in just a few lines of code.
9 | - **Open Source** - Beak.js is open source and free to use.
10 |
11 |
12 |
13 | ## Getting Started
14 |
15 | ### Installation
16 |
17 | First up, add Beak.js to your project:
18 |
19 | ```bash
20 | npm install @beakjs/react --save
21 |
22 | # or with yarn
23 | yarn add @beakjs/react
24 | ```
25 |
26 | ### Setup
27 |
28 | Next, wrap your app in the `Beak` component and add the assistant window:
29 |
30 | ```jsx
31 | import { Beak } from "@beakjs/react";
32 |
33 | const App = () => (
34 |
38 |
39 |
40 |
41 | );
42 | ```
43 |
44 | Now, you've got a chat window ready in the bottom right corner of your website. Give it a try!
45 |
46 | **Note:** Don't expose your API key in public-facing apps - this is for development only. See [Deployment](#deployment) for information on how to securely deploy your app without compromising your API key.
47 |
48 | ### Making Beak.js work with your app
49 |
50 | You can let the assistant carry out tasks in your app by setting up functions with `useBeakFunction`:
51 |
52 | ```jsx
53 | import { useBeakFunction } from "@beakjs/react";
54 |
55 | const MyApp = () => {
56 | const [message, setMessage] = useState("Hello World!");
57 |
58 | useBeakFunction({
59 | name: "updateMessage",
60 | description: "This function updates the app's display message.",
61 | parameters: {
62 | message: {
63 | description: "A short message to display on screen.",
64 | },
65 | },
66 | handler: ({ message }) => {
67 | setMessage(message);
68 |
69 | return `Updated the message to: "${message}"`;
70 | },
71 | });
72 |
73 | return
{message}
;
74 | };
75 | ```
76 |
77 | Note that functions become available to the assistant as soon as their respective component is mounted. This is a powerful concept, ensuring that the assistant will be able to call functions relevant to the current context of your app.
78 |
79 | ### Let the Assistant Know What's Happening On Screen
80 |
81 | You can easily let the assistant know what it currently on screen by using `useBeakInfo`:
82 |
83 | ```jsx
84 | import { useBeakInfo } from "@beakjs/react";
85 |
86 | const MyApp = () => {
87 | const [message, setMessage] = useState("Hello World!");
88 |
89 | useBeakInfo("current message", message);
90 |
91 | // ...
92 | };
93 | ```
94 |
95 | By using `useBeakFunction` together with `useBeakInfo`, your assistant can see what's happening on the screen and take action within your app depending on the current context.
96 |
97 | ## Deployment
98 |
99 | To keep your API key safe, we use a server side handler that forwards your assistant's requests to OpenAI. Furthermore, this handler can be used to add authentication and rate limiting to your assistant.
100 |
101 | Currently, the following frameworks are supported:
102 |
103 | - [Next.js](/docs/deployment/next.md)
104 | - [Remix](/docs/deployment/remix.md)
105 | - [Express](/docs/deployment/express.md)
106 |
107 | Read more about secure deployment by clicking the links above.
108 |
109 | ## Run the Demo
110 |
111 | To run the demo, build the project and start the demo app:
112 |
113 | ```bash
114 | git clone git@github.com:mme/beakjs.git && cd beakjs
115 | yarn && yarn build
116 | cd demo/frontend/presentation
117 | echo "VITE_OPENAI_API_KEY=sk-your-openai-key" > .env
118 | yarn && yarn dev
119 | ```
120 |
121 | Then go to http://localhost:5173/ to see the demo.
122 |
123 | ## Issues
124 |
125 | Feel free to submit issues and enhancement requests.
126 |
127 | ## License
128 |
129 | MIT
130 |
131 | Copyright (c) 2023, Markus Ecker
132 |
--------------------------------------------------------------------------------
/VERSION:
--------------------------------------------------------------------------------
1 | 0.0.6
--------------------------------------------------------------------------------
/demo/backend/express/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
3 | dist
--------------------------------------------------------------------------------
/demo/backend/express/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo-backend-express",
3 | "version": "0.0.6",
4 | "main": "dist/index.js",
5 | "repository": "https://github.com/mme/beakjs.git",
6 | "type": "module",
7 | "scripts": {
8 | "build": "tsc",
9 | "clean": "rm -rf node_modules && rm -rf .next && rm -rf dist",
10 | "start": "node dist/index.js"
11 | },
12 | "author": "Markus Ecker",
13 | "license": "MIT",
14 | "private": true,
15 | "dependencies": {
16 | "@beakjs/express": "0.0.6",
17 | "dotenv": "^16.3.1",
18 | "express": "^4.18.2"
19 | },
20 | "devDependencies": {
21 | "@types/express": "^4.17.21",
22 | "typescript": "^5.0.4"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/demo/backend/express/src/index.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { beakHandler } from "@beakjs/express";
3 | import dotenv from "dotenv";
4 | dotenv.config();
5 |
6 | const app = express();
7 | const port = 3000;
8 |
9 | app.use(express.json());
10 | app.use("/beak", beakHandler());
11 |
12 | app.listen(port, () => {
13 | console.log(`Server running on http://localhost:${port}`);
14 | });
15 |
--------------------------------------------------------------------------------
/demo/backend/express/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2019",
4 | "module": "ESNext",
5 | "declaration": true,
6 | "strict": true,
7 | "esModuleInterop": true,
8 | "skipLibCheck": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "outDir": "dist",
11 | "lib": ["es2019"],
12 | "allowJs": true,
13 | "allowSyntheticDefaultImports": true,
14 | "moduleResolution": "node",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "noEmit": false,
18 | "types": ["node"]
19 | },
20 | "include": ["src"],
21 | "exclude": ["node_modules", "dist"]
22 | }
23 |
--------------------------------------------------------------------------------
/demo/backend/express/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@types/body-parser@*":
6 | version "1.19.5"
7 | resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4"
8 | integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==
9 | dependencies:
10 | "@types/connect" "*"
11 | "@types/node" "*"
12 |
13 | "@types/connect@*":
14 | version "3.4.38"
15 | resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858"
16 | integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==
17 | dependencies:
18 | "@types/node" "*"
19 |
20 | "@types/express-serve-static-core@^4.17.33":
21 | version "4.17.41"
22 | resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz#5077defa630c2e8d28aa9ffc2c01c157c305bef6"
23 | integrity sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==
24 | dependencies:
25 | "@types/node" "*"
26 | "@types/qs" "*"
27 | "@types/range-parser" "*"
28 | "@types/send" "*"
29 |
30 | "@types/express@^4.17.21":
31 | version "4.17.21"
32 | resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d"
33 | integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==
34 | dependencies:
35 | "@types/body-parser" "*"
36 | "@types/express-serve-static-core" "^4.17.33"
37 | "@types/qs" "*"
38 | "@types/serve-static" "*"
39 |
40 | "@types/http-errors@*":
41 | version "2.0.4"
42 | resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f"
43 | integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==
44 |
45 | "@types/mime@*":
46 | version "3.0.4"
47 | resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.4.tgz#2198ac274de6017b44d941e00261d5bc6a0e0a45"
48 | integrity sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==
49 |
50 | "@types/mime@^1":
51 | version "1.3.5"
52 | resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690"
53 | integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==
54 |
55 | "@types/node@*":
56 | version "20.9.2"
57 | resolved "https://registry.yarnpkg.com/@types/node/-/node-20.9.2.tgz#002815c8e87fe0c9369121c78b52e800fadc0ac6"
58 | integrity sha512-WHZXKFCEyIUJzAwh3NyyTHYSR35SevJ6mZ1nWwJafKtiQbqRTIKSRcw3Ma3acqgsent3RRDqeVwpHntMk+9irg==
59 | dependencies:
60 | undici-types "~5.26.4"
61 |
62 | "@types/qs@*":
63 | version "6.9.10"
64 | resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.10.tgz#0af26845b5067e1c9a622658a51f60a3934d51e8"
65 | integrity sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==
66 |
67 | "@types/range-parser@*":
68 | version "1.2.7"
69 | resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
70 | integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
71 |
72 | "@types/send@*":
73 | version "0.17.4"
74 | resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a"
75 | integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==
76 | dependencies:
77 | "@types/mime" "^1"
78 | "@types/node" "*"
79 |
80 | "@types/serve-static@*":
81 | version "1.15.5"
82 | resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.5.tgz#15e67500ec40789a1e8c9defc2d32a896f05b033"
83 | integrity sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==
84 | dependencies:
85 | "@types/http-errors" "*"
86 | "@types/mime" "*"
87 | "@types/node" "*"
88 |
89 | accepts@~1.3.8:
90 | version "1.3.8"
91 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
92 | integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
93 | dependencies:
94 | mime-types "~2.1.34"
95 | negotiator "0.6.3"
96 |
97 | array-flatten@1.1.1:
98 | version "1.1.1"
99 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
100 | integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==
101 |
102 | body-parser@1.20.1:
103 | version "1.20.1"
104 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668"
105 | integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==
106 | dependencies:
107 | bytes "3.1.2"
108 | content-type "~1.0.4"
109 | debug "2.6.9"
110 | depd "2.0.0"
111 | destroy "1.2.0"
112 | http-errors "2.0.0"
113 | iconv-lite "0.4.24"
114 | on-finished "2.4.1"
115 | qs "6.11.0"
116 | raw-body "2.5.1"
117 | type-is "~1.6.18"
118 | unpipe "1.0.0"
119 |
120 | bytes@3.1.2:
121 | version "3.1.2"
122 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
123 | integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
124 |
125 | call-bind@^1.0.0:
126 | version "1.0.5"
127 | resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.5.tgz#6fa2b7845ce0ea49bf4d8b9ef64727a2c2e2e513"
128 | integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==
129 | dependencies:
130 | function-bind "^1.1.2"
131 | get-intrinsic "^1.2.1"
132 | set-function-length "^1.1.1"
133 |
134 | content-disposition@0.5.4:
135 | version "0.5.4"
136 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
137 | integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
138 | dependencies:
139 | safe-buffer "5.2.1"
140 |
141 | content-type@~1.0.4:
142 | version "1.0.5"
143 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
144 | integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
145 |
146 | cookie-signature@1.0.6:
147 | version "1.0.6"
148 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
149 | integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
150 |
151 | cookie@0.5.0:
152 | version "0.5.0"
153 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b"
154 | integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
155 |
156 | debug@2.6.9:
157 | version "2.6.9"
158 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
159 | integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
160 | dependencies:
161 | ms "2.0.0"
162 |
163 | define-data-property@^1.1.1:
164 | version "1.1.1"
165 | resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3"
166 | integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==
167 | dependencies:
168 | get-intrinsic "^1.2.1"
169 | gopd "^1.0.1"
170 | has-property-descriptors "^1.0.0"
171 |
172 | depd@2.0.0:
173 | version "2.0.0"
174 | resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
175 | integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
176 |
177 | destroy@1.2.0:
178 | version "1.2.0"
179 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
180 | integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
181 |
182 | ee-first@1.1.1:
183 | version "1.1.1"
184 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
185 | integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
186 |
187 | encodeurl@~1.0.2:
188 | version "1.0.2"
189 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
190 | integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
191 |
192 | escape-html@~1.0.3:
193 | version "1.0.3"
194 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
195 | integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
196 |
197 | etag@~1.8.1:
198 | version "1.8.1"
199 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
200 | integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
201 |
202 | express@^4.18.2:
203 | version "4.18.2"
204 | resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59"
205 | integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==
206 | dependencies:
207 | accepts "~1.3.8"
208 | array-flatten "1.1.1"
209 | body-parser "1.20.1"
210 | content-disposition "0.5.4"
211 | content-type "~1.0.4"
212 | cookie "0.5.0"
213 | cookie-signature "1.0.6"
214 | debug "2.6.9"
215 | depd "2.0.0"
216 | encodeurl "~1.0.2"
217 | escape-html "~1.0.3"
218 | etag "~1.8.1"
219 | finalhandler "1.2.0"
220 | fresh "0.5.2"
221 | http-errors "2.0.0"
222 | merge-descriptors "1.0.1"
223 | methods "~1.1.2"
224 | on-finished "2.4.1"
225 | parseurl "~1.3.3"
226 | path-to-regexp "0.1.7"
227 | proxy-addr "~2.0.7"
228 | qs "6.11.0"
229 | range-parser "~1.2.1"
230 | safe-buffer "5.2.1"
231 | send "0.18.0"
232 | serve-static "1.15.0"
233 | setprototypeof "1.2.0"
234 | statuses "2.0.1"
235 | type-is "~1.6.18"
236 | utils-merge "1.0.1"
237 | vary "~1.1.2"
238 |
239 | finalhandler@1.2.0:
240 | version "1.2.0"
241 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32"
242 | integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==
243 | dependencies:
244 | debug "2.6.9"
245 | encodeurl "~1.0.2"
246 | escape-html "~1.0.3"
247 | on-finished "2.4.1"
248 | parseurl "~1.3.3"
249 | statuses "2.0.1"
250 | unpipe "~1.0.0"
251 |
252 | forwarded@0.2.0:
253 | version "0.2.0"
254 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
255 | integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
256 |
257 | fresh@0.5.2:
258 | version "0.5.2"
259 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
260 | integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
261 |
262 | function-bind@^1.1.2:
263 | version "1.1.2"
264 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
265 | integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
266 |
267 | get-intrinsic@^1.0.2, get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2:
268 | version "1.2.2"
269 | resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz#281b7622971123e1ef4b3c90fd7539306da93f3b"
270 | integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==
271 | dependencies:
272 | function-bind "^1.1.2"
273 | has-proto "^1.0.1"
274 | has-symbols "^1.0.3"
275 | hasown "^2.0.0"
276 |
277 | gopd@^1.0.1:
278 | version "1.0.1"
279 | resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
280 | integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==
281 | dependencies:
282 | get-intrinsic "^1.1.3"
283 |
284 | has-property-descriptors@^1.0.0:
285 | version "1.0.1"
286 | resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz#52ba30b6c5ec87fd89fa574bc1c39125c6f65340"
287 | integrity sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==
288 | dependencies:
289 | get-intrinsic "^1.2.2"
290 |
291 | has-proto@^1.0.1:
292 | version "1.0.1"
293 | resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0"
294 | integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==
295 |
296 | has-symbols@^1.0.3:
297 | version "1.0.3"
298 | resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
299 | integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
300 |
301 | hasown@^2.0.0:
302 | version "2.0.0"
303 | resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c"
304 | integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==
305 | dependencies:
306 | function-bind "^1.1.2"
307 |
308 | http-errors@2.0.0:
309 | version "2.0.0"
310 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
311 | integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
312 | dependencies:
313 | depd "2.0.0"
314 | inherits "2.0.4"
315 | setprototypeof "1.2.0"
316 | statuses "2.0.1"
317 | toidentifier "1.0.1"
318 |
319 | iconv-lite@0.4.24:
320 | version "0.4.24"
321 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
322 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
323 | dependencies:
324 | safer-buffer ">= 2.1.2 < 3"
325 |
326 | inherits@2.0.4:
327 | version "2.0.4"
328 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
329 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
330 |
331 | ipaddr.js@1.9.1:
332 | version "1.9.1"
333 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
334 | integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
335 |
336 | media-typer@0.3.0:
337 | version "0.3.0"
338 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
339 | integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
340 |
341 | merge-descriptors@1.0.1:
342 | version "1.0.1"
343 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
344 | integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==
345 |
346 | methods@~1.1.2:
347 | version "1.1.2"
348 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
349 | integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
350 |
351 | mime-db@1.52.0:
352 | version "1.52.0"
353 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
354 | integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
355 |
356 | mime-types@~2.1.24, mime-types@~2.1.34:
357 | version "2.1.35"
358 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
359 | integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
360 | dependencies:
361 | mime-db "1.52.0"
362 |
363 | mime@1.6.0:
364 | version "1.6.0"
365 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
366 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
367 |
368 | ms@2.0.0:
369 | version "2.0.0"
370 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
371 | integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
372 |
373 | ms@2.1.3:
374 | version "2.1.3"
375 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
376 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
377 |
378 | negotiator@0.6.3:
379 | version "0.6.3"
380 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
381 | integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
382 |
383 | object-inspect@^1.9.0:
384 | version "1.13.1"
385 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2"
386 | integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==
387 |
388 | on-finished@2.4.1:
389 | version "2.4.1"
390 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
391 | integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
392 | dependencies:
393 | ee-first "1.1.1"
394 |
395 | parseurl@~1.3.3:
396 | version "1.3.3"
397 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
398 | integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
399 |
400 | path-to-regexp@0.1.7:
401 | version "0.1.7"
402 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
403 | integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==
404 |
405 | proxy-addr@~2.0.7:
406 | version "2.0.7"
407 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
408 | integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==
409 | dependencies:
410 | forwarded "0.2.0"
411 | ipaddr.js "1.9.1"
412 |
413 | qs@6.11.0:
414 | version "6.11.0"
415 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
416 | integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==
417 | dependencies:
418 | side-channel "^1.0.4"
419 |
420 | range-parser@~1.2.1:
421 | version "1.2.1"
422 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
423 | integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
424 |
425 | raw-body@2.5.1:
426 | version "2.5.1"
427 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857"
428 | integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==
429 | dependencies:
430 | bytes "3.1.2"
431 | http-errors "2.0.0"
432 | iconv-lite "0.4.24"
433 | unpipe "1.0.0"
434 |
435 | safe-buffer@5.2.1:
436 | version "5.2.1"
437 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
438 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
439 |
440 | "safer-buffer@>= 2.1.2 < 3":
441 | version "2.1.2"
442 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
443 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
444 |
445 | send@0.18.0:
446 | version "0.18.0"
447 | resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"
448 | integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==
449 | dependencies:
450 | debug "2.6.9"
451 | depd "2.0.0"
452 | destroy "1.2.0"
453 | encodeurl "~1.0.2"
454 | escape-html "~1.0.3"
455 | etag "~1.8.1"
456 | fresh "0.5.2"
457 | http-errors "2.0.0"
458 | mime "1.6.0"
459 | ms "2.1.3"
460 | on-finished "2.4.1"
461 | range-parser "~1.2.1"
462 | statuses "2.0.1"
463 |
464 | serve-static@1.15.0:
465 | version "1.15.0"
466 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540"
467 | integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==
468 | dependencies:
469 | encodeurl "~1.0.2"
470 | escape-html "~1.0.3"
471 | parseurl "~1.3.3"
472 | send "0.18.0"
473 |
474 | set-function-length@^1.1.1:
475 | version "1.1.1"
476 | resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.1.1.tgz#4bc39fafb0307224a33e106a7d35ca1218d659ed"
477 | integrity sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==
478 | dependencies:
479 | define-data-property "^1.1.1"
480 | get-intrinsic "^1.2.1"
481 | gopd "^1.0.1"
482 | has-property-descriptors "^1.0.0"
483 |
484 | setprototypeof@1.2.0:
485 | version "1.2.0"
486 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
487 | integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
488 |
489 | side-channel@^1.0.4:
490 | version "1.0.4"
491 | resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
492 | integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
493 | dependencies:
494 | call-bind "^1.0.0"
495 | get-intrinsic "^1.0.2"
496 | object-inspect "^1.9.0"
497 |
498 | statuses@2.0.1:
499 | version "2.0.1"
500 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
501 | integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
502 |
503 | toidentifier@1.0.1:
504 | version "1.0.1"
505 | resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
506 | integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
507 |
508 | type-is@~1.6.18:
509 | version "1.6.18"
510 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
511 | integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
512 | dependencies:
513 | media-typer "0.3.0"
514 | mime-types "~2.1.24"
515 |
516 | typescript@^5.3.2:
517 | version "5.3.2"
518 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.2.tgz#00d1c7c1c46928c5845c1ee8d0cc2791031d4c43"
519 | integrity sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==
520 |
521 | undici-types@~5.26.4:
522 | version "5.26.5"
523 | resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
524 | integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
525 |
526 | unpipe@1.0.0, unpipe@~1.0.0:
527 | version "1.0.0"
528 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
529 | integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
530 |
531 | utils-merge@1.0.1:
532 | version "1.0.1"
533 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
534 | integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
535 |
536 | vary@~1.1.2:
537 | version "1.1.2"
538 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
539 | integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
540 |
--------------------------------------------------------------------------------
/demo/backend/next/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/demo/backend/next/.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 | .yarn/install-state.gz
8 |
9 | .env
10 |
11 | # testing
12 | /coverage
13 |
14 | # next.js
15 | /.next/
16 | /out/
17 |
18 | # production
19 | /build
20 |
21 | # misc
22 | .DS_Store
23 | *.pem
24 |
25 | # debug
26 | npm-debug.log*
27 | yarn-debug.log*
28 | yarn-error.log*
29 |
30 | # local env files
31 | .env*.local
32 |
33 | # vercel
34 | .vercel
35 |
36 | # typescript
37 | *.tsbuildinfo
38 | next-env.d.ts
39 |
--------------------------------------------------------------------------------
/demo/backend/next/README.md:
--------------------------------------------------------------------------------
1 | # Next.js Demo
2 |
3 | To run this demo:
4 |
5 | ```bash
6 | yarn install &&
7 | cat "OPENAI_API_KEY=" > .env &&
8 | yarn dev
9 | ```
10 |
11 | Then go to [http://localhost:3000](http://localhost:3000) to see the result.
12 |
--------------------------------------------------------------------------------
/demo/backend/next/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {}
3 |
4 | module.exports = nextConfig
5 |
--------------------------------------------------------------------------------
/demo/backend/next/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo-backend-next",
3 | "version": "0.0.6",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "clean": "rm -rf node_modules && rm -rf .next && rm -rf dist"
11 | },
12 | "dependencies": {
13 | "react": "^18.2.0",
14 | "react-dom": "^18.2.0",
15 | "next": "14.0.3",
16 | "@beakjs/react": "0.0.6",
17 | "@beakjs/next": "0.0.6"
18 | },
19 | "devDependencies": {
20 | "typescript": "^5.0.4",
21 | "@types/node": "^20.1.0",
22 | "@types/react": "^18.2.15",
23 | "@types/react-dom": "^18.2.7",
24 | "autoprefixer": "^10.0.1",
25 | "postcss": "^8",
26 | "tailwindcss": "^3.3.0",
27 | "eslint": "^8",
28 | "eslint-config-next": "14.0.3"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/demo/backend/next/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/demo/backend/next/public/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mme/beakjs/c837e84da19082ae6ed977381548efb74930d678/demo/backend/next/public/.gitkeep
--------------------------------------------------------------------------------
/demo/backend/next/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mme/beakjs/c837e84da19082ae6ed977381548efb74930d678/demo/backend/next/src/app/favicon.ico
--------------------------------------------------------------------------------
/demo/backend/next/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html,
6 | body,
7 | #root {
8 | height: 100%;
9 | margin: 0;
10 | padding: 0;
11 | }
12 |
13 | #root {
14 | display: flex;
15 | }
16 |
17 | .slide {
18 | display: flex;
19 | inset: 0;
20 | display: flex;
21 | flex: 1;
22 | inset: 0;
23 | font-family: sans-serif;
24 | justify-content: center;
25 | align-items: center;
26 | font-size: 2rem;
27 | padding: 5rem;
28 | background-repeat: no-repeat;
29 | background-size: cover;
30 | text-shadow: -1px 0 white, 0 1px white, 1px 0 white, 0 -1px white;
31 | }
32 |
33 | @media (min-width: 640px) {
34 | .slide {
35 | font-size: 5rem;
36 | padding: 10rem;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/demo/backend/next/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import "./globals.css";
4 |
5 | const inter = Inter({ subsets: ["latin"] });
6 |
7 | export const metadata: Metadata = {
8 | title: "Create Next App",
9 | description: "Generated by create next app",
10 | };
11 |
12 | export default function RootLayout({
13 | children,
14 | }: {
15 | children: React.ReactNode;
16 | }) {
17 | return (
18 |
19 |
20 | {children}
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/demo/backend/next/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import {
5 | Beak,
6 | AssistantWindow,
7 | useBeakInfo,
8 | useBeakFunction,
9 | } from "@beakjs/react";
10 |
11 | import { DebugLogger } from "@beakjs/core";
12 |
13 | const App = () => {
14 | const debugLogger = new DebugLogger(["chat-api"]);
15 | return (
16 |
26 |
27 |
28 |
29 | );
30 | };
31 |
32 | const Presentation = () => {
33 | const [state, setState] = useState({
34 | message: "Hello World!",
35 | backgroundImage: "none",
36 | });
37 |
38 | useBeakInfo("current slide", state);
39 |
40 | useBeakFunction({
41 | name: "presentSlide",
42 | description: "Present a slide in the presentation you are giving.",
43 | feedback: "auto",
44 | parameters: {
45 | message: {
46 | description:
47 | "A short message to display in the presentation slide, max 40 words.",
48 | },
49 | backgroundImage: {
50 | description:
51 | "What to display in the background of the slide (i.e. 'dog' or 'house'), or 'none' for a white background",
52 | },
53 | },
54 | handler: ({ message, backgroundImage }) => {
55 | setState({
56 | message: message,
57 | backgroundImage: backgroundImage,
58 | });
59 |
60 | return `Presented slide with message "${message}" and background image "${backgroundImage}"`;
61 | },
62 | });
63 |
64 | return ;
65 | };
66 |
67 | type SlideProps = {
68 | message: string;
69 | backgroundImage: string;
70 | };
71 |
72 | const Slide = ({ message, backgroundImage }: SlideProps) => {
73 | if (backgroundImage !== "none") {
74 | backgroundImage =
75 | 'url("https://source.unsplash.com/featured/?' +
76 | encodeURIComponent(backgroundImage) +
77 | '")';
78 | }
79 | return (
80 |
86 | {message}
87 |
88 | );
89 | };
90 |
91 | export default App;
92 |
--------------------------------------------------------------------------------
/demo/backend/next/src/pages/api/beak/[...path].ts:
--------------------------------------------------------------------------------
1 | import { beakHandler } from "@beakjs/next";
2 |
3 | const handler = beakHandler();
4 |
5 | export default handler;
6 |
--------------------------------------------------------------------------------
/demo/backend/next/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss'
2 |
3 | const config: Config = {
4 | content: [
5 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
6 | './src/components/**/*.{js,ts,jsx,tsx,mdx}',
7 | './src/app/**/*.{js,ts,jsx,tsx,mdx}',
8 | ],
9 | theme: {
10 | extend: {
11 | backgroundImage: {
12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
13 | 'gradient-conic':
14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
15 | },
16 | },
17 | },
18 | plugins: [],
19 | }
20 | export default config
21 |
--------------------------------------------------------------------------------
/demo/backend/next/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/demo/backend/remix/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('eslint').Linter.Config} */
2 | module.exports = {
3 | extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"],
4 | };
5 |
--------------------------------------------------------------------------------
/demo/backend/remix/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | /.cache
4 | /build
5 | /public/build
6 | .env
7 |
--------------------------------------------------------------------------------
/demo/backend/remix/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to Remix!
2 |
3 | - [Remix Docs](https://remix.run/docs)
4 |
5 | ## Development
6 |
7 | From your terminal:
8 |
9 | ```sh
10 | npm run dev
11 | ```
12 |
13 | This starts your app in development mode, rebuilding assets on file changes.
14 |
15 | ## Deployment
16 |
17 | First, build your app for production:
18 |
19 | ```sh
20 | npm run build
21 | ```
22 |
23 | Then run the app in production mode:
24 |
25 | ```sh
26 | npm start
27 | ```
28 |
29 | Now you'll need to pick a host to deploy it to.
30 |
31 | ### DIY
32 |
33 | If you're familiar with deploying node applications, the built-in Remix app server is production-ready.
34 |
35 | Make sure to deploy the output of `remix build`
36 |
37 | - `build/`
38 | - `public/build/`
39 |
--------------------------------------------------------------------------------
/demo/backend/remix/app/entry.client.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * By default, Remix will handle hydrating your app on the client for you.
3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
4 | * For more information, see https://remix.run/file-conventions/entry.client
5 | */
6 |
7 | import { RemixBrowser } from "@remix-run/react";
8 | import { startTransition, StrictMode } from "react";
9 | import { hydrateRoot } from "react-dom/client";
10 |
11 | startTransition(() => {
12 | hydrateRoot(
13 | document,
14 |
15 |
16 |
17 | );
18 | });
19 |
--------------------------------------------------------------------------------
/demo/backend/remix/app/entry.server.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * By default, Remix will handle generating the HTTP Response for you.
3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
4 | * For more information, see https://remix.run/file-conventions/entry.server
5 | */
6 |
7 | import { PassThrough } from "node:stream";
8 |
9 | import type { AppLoadContext, EntryContext } from "@remix-run/node";
10 | import { createReadableStreamFromReadable } from "@remix-run/node";
11 | import { RemixServer } from "@remix-run/react";
12 | import isbot from "isbot";
13 | import { renderToPipeableStream } from "react-dom/server";
14 |
15 | const ABORT_DELAY = 5_000;
16 |
17 | export default function handleRequest(
18 | request: Request,
19 | responseStatusCode: number,
20 | responseHeaders: Headers,
21 | remixContext: EntryContext,
22 | loadContext: AppLoadContext
23 | ) {
24 | return isbot(request.headers.get("user-agent"))
25 | ? handleBotRequest(
26 | request,
27 | responseStatusCode,
28 | responseHeaders,
29 | remixContext
30 | )
31 | : handleBrowserRequest(
32 | request,
33 | responseStatusCode,
34 | responseHeaders,
35 | remixContext
36 | );
37 | }
38 |
39 | function handleBotRequest(
40 | request: Request,
41 | responseStatusCode: number,
42 | responseHeaders: Headers,
43 | remixContext: EntryContext
44 | ) {
45 | return new Promise((resolve, reject) => {
46 | let shellRendered = false;
47 | const { pipe, abort } = renderToPipeableStream(
48 | ,
53 | {
54 | onAllReady() {
55 | shellRendered = true;
56 | const body = new PassThrough();
57 | const stream = createReadableStreamFromReadable(body);
58 |
59 | responseHeaders.set("Content-Type", "text/html");
60 |
61 | resolve(
62 | new Response(stream, {
63 | headers: responseHeaders,
64 | status: responseStatusCode,
65 | })
66 | );
67 |
68 | pipe(body);
69 | },
70 | onShellError(error: unknown) {
71 | reject(error);
72 | },
73 | onError(error: unknown) {
74 | responseStatusCode = 500;
75 | // Log streaming rendering errors from inside the shell. Don't log
76 | // errors encountered during initial shell rendering since they'll
77 | // reject and get logged in handleDocumentRequest.
78 | if (shellRendered) {
79 | console.error(error);
80 | }
81 | },
82 | }
83 | );
84 |
85 | setTimeout(abort, ABORT_DELAY);
86 | });
87 | }
88 |
89 | function handleBrowserRequest(
90 | request: Request,
91 | responseStatusCode: number,
92 | responseHeaders: Headers,
93 | remixContext: EntryContext
94 | ) {
95 | return new Promise((resolve, reject) => {
96 | let shellRendered = false;
97 | const { pipe, abort } = renderToPipeableStream(
98 | ,
103 | {
104 | onShellReady() {
105 | shellRendered = true;
106 | const body = new PassThrough();
107 | const stream = createReadableStreamFromReadable(body);
108 |
109 | responseHeaders.set("Content-Type", "text/html");
110 |
111 | resolve(
112 | new Response(stream, {
113 | headers: responseHeaders,
114 | status: responseStatusCode,
115 | })
116 | );
117 |
118 | pipe(body);
119 | },
120 | onShellError(error: unknown) {
121 | reject(error);
122 | },
123 | onError(error: unknown) {
124 | responseStatusCode = 500;
125 | // Log streaming rendering errors from inside the shell. Don't log
126 | // errors encountered during initial shell rendering since they'll
127 | // reject and get logged in handleDocumentRequest.
128 | if (shellRendered) {
129 | console.error(error);
130 | }
131 | },
132 | }
133 | );
134 |
135 | setTimeout(abort, ABORT_DELAY);
136 | });
137 | }
138 |
--------------------------------------------------------------------------------
/demo/backend/remix/app/root.tsx:
--------------------------------------------------------------------------------
1 | import { cssBundleHref } from "@remix-run/css-bundle";
2 | import type { LinksFunction } from "@remix-run/node";
3 | import {
4 | Links,
5 | LiveReload,
6 | Meta,
7 | Outlet,
8 | Scripts,
9 | ScrollRestoration,
10 | } from "@remix-run/react";
11 |
12 | export const links: LinksFunction = () => [
13 | ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
14 | ];
15 |
16 | export default function App() {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/demo/backend/remix/app/routes/_index.tsx:
--------------------------------------------------------------------------------
1 | import type { MetaFunction } from "@remix-run/node";
2 | import { useState } from "react";
3 | import {
4 | Beak,
5 | AssistantWindow,
6 | useBeakInfo,
7 | useBeakFunction,
8 | } from "@beakjs/react";
9 |
10 | import "../styles/global.css";
11 |
12 | export const meta: MetaFunction = () => {
13 | return [
14 | { title: "New Remix App" },
15 | { name: "description", content: "Welcome to Remix!" },
16 | ];
17 | };
18 |
19 | const App = () => {
20 | return (
21 |
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | const Presentation = () => {
37 | const [state, setState] = useState({
38 | message: "Hello World!",
39 | backgroundImage: "none",
40 | });
41 |
42 | useBeakInfo("current slide", state);
43 |
44 | useBeakFunction({
45 | name: "presentSlide",
46 | description: "Present a slide in the presentation you are giving.",
47 | feedback: "auto",
48 | parameters: {
49 | message: {
50 | description:
51 | "A short message to display in the presentation slide, max 40 words.",
52 | },
53 | backgroundImage: {
54 | description:
55 | "What to display in the background of the slide (i.e. 'dog' or 'house'), or 'none' for a white background",
56 | },
57 | },
58 | handler: ({ message, backgroundImage }) => {
59 | setState({
60 | message: message,
61 | backgroundImage: backgroundImage,
62 | });
63 |
64 | return `Presented slide with message "${message}" and background image "${backgroundImage}"`;
65 | },
66 | });
67 |
68 | return ;
69 | };
70 |
71 | type SlideProps = {
72 | message: string;
73 | backgroundImage: string;
74 | };
75 |
76 | export const Slide = ({ message, backgroundImage }: SlideProps) => {
77 | if (backgroundImage !== "none") {
78 | backgroundImage =
79 | 'url("https://source.unsplash.com/featured/?' +
80 | encodeURIComponent(backgroundImage) +
81 | '")';
82 | }
83 | return (
84 |
90 | {message}
91 |
92 | );
93 | };
94 |
95 | export default App;
96 |
--------------------------------------------------------------------------------
/demo/backend/remix/app/routes/beak.$.tsx:
--------------------------------------------------------------------------------
1 | import { beakHandler } from "@beakjs/remix";
2 |
3 | export const action = beakHandler();
4 |
--------------------------------------------------------------------------------
/demo/backend/remix/app/styles/global.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | height: 100%;
4 | margin: 0;
5 | padding: 0;
6 | }
7 |
8 | body {
9 | display: flex;
10 | }
11 |
12 | .slide {
13 | display: flex;
14 | inset: 0;
15 | display: flex;
16 | flex: 1;
17 | inset: 0;
18 | font-family: sans-serif;
19 | justify-content: center;
20 | align-items: center;
21 | font-size: 2rem;
22 | padding: 5rem;
23 | background-repeat: no-repeat;
24 | background-size: cover;
25 | text-shadow: -1px 0 white, 0 1px white, 1px 0 white, 0 -1px white;
26 | }
27 |
28 | @media (min-width: 640px) {
29 | .slide {
30 | font-size: 5rem;
31 | padding: 10rem;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/demo/backend/remix/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo-backend-remix",
3 | "version": "0.0.6",
4 | "private": true,
5 | "sideEffects": false,
6 | "type": "module",
7 | "scripts": {
8 | "build": "remix build",
9 | "dev": "remix dev --manual",
10 | "start": "remix-serve ./build/index.js",
11 | "typecheck": "tsc",
12 | "clean": "rm -rf build node_modules public/build",
13 | "routes": "remix routes"
14 | },
15 | "dependencies": {
16 | "@remix-run/css-bundle": "^2.3.0",
17 | "@remix-run/node": "^2.3.0",
18 | "@remix-run/react": "^2.3.0",
19 | "@remix-run/serve": "^2.3.0",
20 | "isbot": "^3.6.8",
21 | "react": "^18.2.0",
22 | "react-dom": "^18.2.0",
23 | "@beakjs/remix": "0.0.6",
24 | "@beakjs/react": "0.0.6"
25 | },
26 | "devDependencies": {
27 | "@remix-run/dev": "^2.3.0",
28 | "@remix-run/eslint-config": "^2.3.0",
29 | "@types/react": "^18.2.20",
30 | "@types/react-dom": "^18.2.7",
31 | "eslint": "^8.38.0",
32 | "typescript": "^5.1.6"
33 | },
34 | "engines": {
35 | "node": ">=18.0.0"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/demo/backend/remix/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mme/beakjs/c837e84da19082ae6ed977381548efb74930d678/demo/backend/remix/public/favicon.ico
--------------------------------------------------------------------------------
/demo/backend/remix/remix.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('@remix-run/dev').AppConfig} */
2 | export default {
3 | ignoredRouteFiles: ["**/.*"],
4 | // appDirectory: "app",
5 | // assetsBuildDirectory: "public/build",
6 | // publicPath: "/build/",
7 | // serverBuildPath: "build/index.js",
8 | };
9 |
--------------------------------------------------------------------------------
/demo/backend/remix/remix.env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/demo/backend/remix/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
3 | "compilerOptions": {
4 | "lib": ["DOM", "DOM.Iterable", "ES2022"],
5 | "isolatedModules": true,
6 | "esModuleInterop": true,
7 | "jsx": "react-jsx",
8 | "moduleResolution": "Bundler",
9 | "resolveJsonModule": true,
10 | "target": "ES2022",
11 | "strict": true,
12 | "allowJs": true,
13 | "forceConsistentCasingInFileNames": true,
14 | "baseUrl": ".",
15 | "paths": {
16 | "~/*": ["./app/*"]
17 | },
18 |
19 | // Remix takes care of building everything in `remix build`.
20 | "noEmit": true
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/demo/frontend/presentation/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:@typescript-eslint/recommended',
7 | 'plugin:react-hooks/recommended',
8 | ],
9 | ignorePatterns: ['dist', '.eslintrc.cjs'],
10 | parser: '@typescript-eslint/parser',
11 | plugins: ['react-refresh'],
12 | rules: {
13 | 'react-refresh/only-export-components': [
14 | 'warn',
15 | { allowConstantExport: true },
16 | ],
17 | },
18 | }
19 |
--------------------------------------------------------------------------------
/demo/frontend/presentation/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | .env
--------------------------------------------------------------------------------
/demo/frontend/presentation/README.md:
--------------------------------------------------------------------------------
1 | # React + TypeScript + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
10 | ## Expanding the ESLint configuration
11 |
12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
13 |
14 | - Configure the top-level `parserOptions` property like this:
15 |
16 | ```js
17 | parserOptions: {
18 | ecmaVersion: 'latest',
19 | sourceType: 'module',
20 | project: ['./tsconfig.json', './tsconfig.node.json'],
21 | tsconfigRootDir: __dirname,
22 | },
23 | ```
24 |
25 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
26 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
27 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
28 |
--------------------------------------------------------------------------------
/demo/frontend/presentation/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 | Vite + React + TS
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/demo/frontend/presentation/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo-frontend-presentation",
3 | "private": true,
4 | "version": "0.0.6",
5 | "type": "module",
6 | "homepage": "https://mme.github.io/beakjs",
7 | "scripts": {
8 | "dev": "vite",
9 | "build": "tsc && vite build",
10 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
11 | "preview": "vite preview",
12 | "clean": "rm -rf dist",
13 | "deploy": "gh-pages -d dist"
14 | },
15 | "dependencies": {
16 | "react": "^18.2.0",
17 | "react-dom": "^18.2.0"
18 | },
19 | "devDependencies": {
20 | "@types/react": "^18.2.15",
21 | "@types/react-dom": "^18.2.7",
22 | "@typescript-eslint/eslint-plugin": "^6.0.0",
23 | "@typescript-eslint/parser": "^6.0.0",
24 | "@vitejs/plugin-react": "^4.0.3",
25 | "eslint": "^8.45.0",
26 | "eslint-plugin-react-hooks": "^4.6.0",
27 | "eslint-plugin-react-refresh": "^0.4.3",
28 | "gh-pages": "^6.1.0",
29 | "typescript": "^5.0.4",
30 | "vite": "^4.3.5"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/demo/frontend/presentation/public/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mme/beakjs/c837e84da19082ae6ed977381548efb74930d678/demo/frontend/presentation/public/.gitkeep
--------------------------------------------------------------------------------
/demo/frontend/presentation/src/App.css:
--------------------------------------------------------------------------------
1 | .slide {
2 | display: flex;
3 | flex: 1;
4 | inset: 0;
5 | font-family: sans-serif;
6 | justify-content: center;
7 | align-items: center;
8 | font-size: 2rem;
9 | padding: 5rem;
10 | background-repeat: no-repeat;
11 | background-size: cover;
12 | text-shadow: -1px 0 white, 0 1px white, 1px 0 white, 0 -1px white;
13 | margin: 0;
14 | width: 100%;
15 | height: 100%;
16 | box-sizing: border-box;
17 | }
18 |
19 | @media (min-width: 640px) {
20 | .slide {
21 | font-size: 5rem;
22 | padding: 10rem;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/demo/frontend/presentation/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import {
3 | Beak,
4 | AssistantWindow,
5 | useBeakInfo,
6 | useBeakFunction,
7 | } from "@beakjs/react";
8 |
9 | import { DebugLogger } from "@beakjs/core";
10 | import "./App.css";
11 |
12 | const App = () => {
13 | const [openAIApiKey, setOpenAIApiKey] = useState(
14 | import.meta.env.VITE_OPENAI_API_KEY
15 | );
16 |
17 | const debugLogger = new DebugLogger(["chat-api"]);
18 |
19 | const handleSubmit = (event: React.FormEvent) => {
20 | event.preventDefault();
21 |
22 | const form = event.currentTarget;
23 | const apiKeyInputElement = form.elements.namedItem(
24 | "apiKey"
25 | ) as HTMLInputElement;
26 |
27 | if (apiKeyInputElement) {
28 | const apiKey = apiKeyInputElement.value;
29 | setOpenAIApiKey(apiKey);
30 | }
31 | };
32 |
33 | return (
34 |
35 | {openAIApiKey ? (
36 |
46 |
47 |
48 |
49 | ) : (
50 |
51 | )}
52 |
53 | );
54 | };
55 |
56 | const Presentation = () => {
57 | const [state, setState] = useState({
58 | message: "Hello World!",
59 | backgroundImage: "none",
60 | });
61 |
62 | useBeakInfo("current slide", state);
63 |
64 | useBeakFunction({
65 | name: "presentSlide",
66 | description: "Present a slide in the presentation you are giving.",
67 | feedback: "auto",
68 | parameters: {
69 | message: {
70 | description:
71 | "A short message to display in the presentation slide, max 30 words.",
72 | },
73 | backgroundImage: {
74 | description:
75 | "What to display in the background of the slide (i.e. 'dog' or 'house'), or 'none' for a white background",
76 | },
77 | },
78 | handler: ({ message, backgroundImage }) => {
79 | setState({
80 | message: message,
81 | backgroundImage: backgroundImage,
82 | });
83 |
84 | return `Presented slide with message "${message}" and background image "${backgroundImage}"`;
85 | },
86 | });
87 |
88 | return ;
89 | };
90 |
91 | type SlideProps = {
92 | message: string;
93 | backgroundImage: string;
94 | };
95 |
96 | export const Slide = ({ message, backgroundImage }: SlideProps) => {
97 | if (backgroundImage !== "none") {
98 | backgroundImage =
99 | 'url("https://source.unsplash.com/featured/?' +
100 | encodeURIComponent(backgroundImage) +
101 | '")';
102 | }
103 | return (
104 |
110 | {message}
111 |
112 | );
113 | };
114 |
115 | const ApiKeyForm = ({ handleSubmit }: { handleSubmit: any }) => {
116 | return (
117 |
126 |
172 |
173 | );
174 | };
175 |
176 | export default App;
177 |
--------------------------------------------------------------------------------
/demo/frontend/presentation/src/index.css:
--------------------------------------------------------------------------------
1 | html,
2 | body,
3 | #root {
4 | height: 100%;
5 | margin: 0;
6 | padding: 0;
7 | }
8 |
9 | #root {
10 | display: flex;
11 | }
12 |
--------------------------------------------------------------------------------
/demo/frontend/presentation/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import App from "./App.tsx";
4 |
5 | import "./index.css";
6 |
7 | ReactDOM.createRoot(document.getElementById("root")!).render(
8 |
9 |
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/demo/frontend/presentation/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/demo/frontend/presentation/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/demo/frontend/presentation/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/demo/frontend/presentation/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react";
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | base: "/beakjs/",
8 | });
9 |
--------------------------------------------------------------------------------
/docs/deployment/express.md:
--------------------------------------------------------------------------------
1 | # Setting up Express
2 |
3 | ## Credentials
4 |
5 | In the root of your project, create a `.env` file and add your OpenAI API key:
6 |
7 | ```bash
8 | OPENAI_API_KEY=sk-...
9 | ```
10 |
11 | Then install the `dotenv` package if you haven't already:
12 |
13 | ```bash
14 | npm install dotenv --save
15 | # or
16 | yarn add dotenv
17 | ```
18 |
19 | ## Install Beak.js for Express
20 |
21 | ```bash
22 | npm install @beak/express --save
23 | # or
24 | yarn add @beak/express
25 | ```
26 |
27 | ## Add the middleware
28 |
29 | For example:
30 |
31 | ```typescript
32 | import express from "express";
33 | import { beakHandler } from "@beakjs/express";
34 | import dotenv from "dotenv";
35 | dotenv.config();
36 |
37 | const app = express();
38 | const port = 3000;
39 |
40 | app.use("/beak", beakHandler());
41 |
42 | app.listen(port, () => {
43 | console.log(`Server running on http://localhost:${port}`);
44 | });
45 | ```
46 |
47 | ## Configure Beak to use the Express backend
48 |
49 | ```typescript
50 | const App = () => {
51 | return ... your app code goes here;
52 | };
53 | ```
54 |
--------------------------------------------------------------------------------
/docs/deployment/next.md:
--------------------------------------------------------------------------------
1 | # Setting up Next.js
2 |
3 | ## Credentials
4 |
5 | In the root of your project, create a `.env` file and add your OpenAI API key:
6 |
7 | ```bash
8 | OPENAI_API_KEY=sk-...
9 | ```
10 |
11 | ## Install Beak.js for Next.js
12 |
13 | ```bash
14 | npm install @beak/next --save
15 | # or
16 | yarn add @beak/next
17 | ```
18 |
19 | ## Add the Beak.js handler
20 |
21 | Create a new wildcard API route in `pages/api/beak/[...path].ts`:
22 |
23 | ```typescript
24 | import { beakHandler } from "@beakjs/next";
25 |
26 | const handler = beakHandler();
27 |
28 | export default handler;
29 | ```
30 |
31 | ## Configure Beak to use the Next.js backend
32 |
33 | ```typescript
34 | const App = () => {
35 | return ... your app code goes here;
36 | };
37 | ```
38 |
--------------------------------------------------------------------------------
/docs/deployment/remix.md:
--------------------------------------------------------------------------------
1 | # Setting up Remix
2 |
3 | ## Credentials
4 |
5 | In the root of your project, create a `.env` file and add your OpenAI API key:
6 |
7 | ```bash
8 | OPENAI_API_KEY=sk-...
9 | ```
10 |
11 | ## Install Beak.js for Remix
12 |
13 | ```bash
14 | npm install @beak/remix --save
15 | # or
16 | yarn add @beak/remix
17 | ```
18 |
19 | ## Add the Beak.js handler
20 |
21 | Create a new wildcard API route in `app/routes/beak.$.tsx`:
22 |
23 | ```typescript
24 | import { beakHandler } from "@beakjs/remix";
25 | export const action = beakHandler();
26 | ```
27 |
28 | ## Configure Beak to use the Remix backend
29 |
30 | ```typescript
31 | const App = () => {
32 | return ... your app code goes here;
33 | };
34 | ```
35 |
--------------------------------------------------------------------------------
/docs/img/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mme/beakjs/c837e84da19082ae6ed977381548efb74930d678/docs/img/screenshot.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "beakjs",
3 | "version": "0.0.6",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "dependencies": {
7 | "eventemitter3": "^5.0.1",
8 | "jsonrepair": "^3.2.4",
9 | "react-textarea-autosize": "^8.4.1",
10 | "uuid": "^9.0.1",
11 | "react": "^18.2.0",
12 | "react-dom": "^18.2.0"
13 | },
14 | "devDependencies": {
15 | "@types/jest": "^29.5.1",
16 | "@types/node": "^20.1.0",
17 | "@types/react": "^18.2.15",
18 | "@types/react-dom": "^18.2.7",
19 | "@types/uuid": "^9.0.6",
20 | "jest": "^29.5.0",
21 | "ts-jest": "^29.1.0",
22 | "ts-node": "^10.9.1",
23 | "typescript": "^5.0.4",
24 | "vite": "^4.3.5",
25 | "vite-tsconfig-paths": "^4.2.0"
26 | },
27 | "scripts": {
28 | "dev": "vite",
29 | "build": "yarn workspaces run build",
30 | "prepublishOnly": "yarn build",
31 | "serve": "vite preview",
32 | "test": "jest",
33 | "sync-versions": "ts-node scripts/sync-versions.ts",
34 | "clean": "rm -rf dist && rm -rf node_modules && yarn workspaces run clean"
35 | },
36 | "jest": {
37 | "testTimeout": 60000,
38 | "transform": {
39 | "^.+\\.(ts|tsx)$": "ts-jest"
40 | },
41 | "testEnvironment": "node",
42 | "moduleFileExtensions": [
43 | "ts",
44 | "tsx",
45 | "js"
46 | ],
47 | "testPathIgnorePatterns": [
48 | "/node_modules/"
49 | ]
50 | },
51 | "files": [
52 | "dist/**/*"
53 | ],
54 | "private": true,
55 | "workspaces": [
56 | "packages/openai",
57 | "packages/core",
58 | "packages/react",
59 | "packages/server",
60 | "packages/next",
61 | "packages/remix",
62 | "packages/express",
63 | "demo/frontend/presentation",
64 | "demo/backend/next",
65 | "demo/backend/express",
66 | "demo/backend/remix"
67 | ]
68 | }
69 |
--------------------------------------------------------------------------------
/packages/core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Markus Ecker",
3 | "license": "MIT",
4 | "name": "@beakjs/core",
5 | "version": "0.0.6",
6 | "description": "BeakJS core library",
7 | "main": "dist/cjs/index.js",
8 | "module": "dist/esm/index.js",
9 | "types": "dist/cjs/index.d.ts",
10 | "scripts": {
11 | "build": "tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json",
12 | "prepublishOnly": "cp ../../README.md .",
13 | "clean": "rm -rf dist && rm -rf node_modules",
14 | "test": "jest"
15 | },
16 | "files": [
17 | "dist/**/*",
18 | "README.md"
19 | ],
20 | "dependencies": {
21 | "@beakjs/openai": "0.0.6",
22 | "eventemitter3": "^5.0.1",
23 | "uuid": "^9.0.1"
24 | },
25 | "devDependencies": {
26 | "@types/uuid": "^9.0.6",
27 | "typescript": "^5.0.4",
28 | "@types/jest": "^29.5.1",
29 | "jest": "^29.5.0",
30 | "ts-jest": "^29.1.0"
31 | },
32 | "peerDependencies": {},
33 | "repository": {
34 | "type": "git",
35 | "url": "https://github.com/mme/beakjs.git"
36 | },
37 | "jest": {
38 | "testTimeout": 60000,
39 | "transform": {
40 | "^.+\\.(ts|tsx)$": "ts-jest"
41 | },
42 | "testEnvironment": "node",
43 | "moduleFileExtensions": [
44 | "ts",
45 | "tsx",
46 | "js"
47 | ],
48 | "testPathIgnorePatterns": [
49 | "/node_modules/"
50 | ]
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/packages/core/src/beak.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from "eventemitter3";
2 | import { OpenAI, OpenAIModel, CustomModel } from "@beakjs/openai";
3 | import {
4 | LLMAdapter,
5 | Message,
6 | FunctionDefinition,
7 | FunctionCall,
8 | QueryChatCompletionParams,
9 | DebugLogger,
10 | } from "./types";
11 | import { v4 as uuidv4 } from "uuid";
12 | import { OpenAIAdapter } from "./openai";
13 |
14 | const DEFAULT_INSTRUCTIONS =
15 | "Assistant is running inside a web application. Assistant never returns JSON " +
16 | "as a text reply, always uses the correct format for function calls.";
17 |
18 | const FORMATTING_INSTRUCTIONS =
19 | "Never provide instructions for executing function calls to the user, instead " +
20 | "use the function call interface.";
21 |
22 | export interface BeakConfiguration {
23 | openAIApiKey?: string;
24 | baseUrl?: string;
25 | openAIModel?: OpenAIModel | CustomModel;
26 | maxFeedback?: number;
27 | instructions?: string;
28 | temperature?: number;
29 | maxTokens?: number;
30 | debugLogger?: DebugLogger;
31 | formattingInstructions?: string;
32 | }
33 |
34 | interface BeakEvents {
35 | change: (messages: Message) => void;
36 | partial: (name: string, args: any) => void;
37 | function: (functionCall: FunctionCall) => void;
38 | content: (content: string) => void;
39 | error: (error: Error) => void;
40 | end: () => void;
41 | }
42 |
43 | interface BeakInfo {
44 | description: string;
45 | data: any;
46 | }
47 |
48 | export class BeakCore extends EventEmitter {
49 | public _messages: Message[] = [];
50 |
51 | private instructionsMessage: Message;
52 | private formattingInstructionsMessage?: Message;
53 | public configuration: BeakConfiguration;
54 | public functions: Record = {};
55 | public info: Record = {};
56 | private debug: DebugLogger;
57 |
58 | constructor(configuration: BeakConfiguration) {
59 | super();
60 | this.configuration = configuration;
61 |
62 | let client = this.newAdapter();
63 | this.instructionsMessage = new Message({
64 | role: "system",
65 | content: this.configuration.instructions || DEFAULT_INSTRUCTIONS,
66 | status: "success",
67 | });
68 | this.instructionsMessage.calculateNumTokens(client);
69 |
70 | if (this.configuration.formattingInstructions !== "") {
71 | this.formattingInstructionsMessage = new Message({
72 | role: "system",
73 | content:
74 | this.configuration.formattingInstructions || FORMATTING_INSTRUCTIONS,
75 | status: "success",
76 | });
77 | this.formattingInstructionsMessage.calculateNumTokens(client);
78 | }
79 |
80 | this.debug = configuration.debugLogger || new DebugLogger();
81 | }
82 |
83 | public addFunction(fun: FunctionDefinition) {
84 | this.functions[fun.name] = fun;
85 | }
86 |
87 | public removeFunction(fun: FunctionDefinition | string) {
88 | if (typeof fun === "string") {
89 | delete this.functions[fun];
90 | } else {
91 | delete this.functions[fun.name];
92 | }
93 | }
94 |
95 | public addInfo(data: any, description: string = "data"): string {
96 | const id = uuidv4();
97 | this.info[id] = { description, data };
98 | return id;
99 | }
100 |
101 | public removeInfo(id: string) {
102 | delete this.info[id];
103 | }
104 |
105 | private infoMessage(): Message | undefined {
106 | const info = Object.values(this.info);
107 | if (info.length == 0) {
108 | return undefined;
109 | }
110 | const infoStrings = info.map(
111 | (c) => c.description + ": " + JSON.stringify(c.data)
112 | );
113 | infoStrings.sort();
114 | const ctxString = infoStrings.join("\n");
115 | return new Message({
116 | role: "system",
117 | content:
118 | "Partial snapshot of the application's current state:\n\n" + ctxString,
119 | });
120 | }
121 |
122 | public get messages(): Message[] {
123 | return this._messages
124 | .filter((message) => message.role != "system")
125 | .map((message) => message.copy());
126 | }
127 |
128 | public async runChatCompletion(content: string): Promise {
129 | this.debug.log(
130 | "beak-complete",
131 | "Running runChatCompletion with content:",
132 | content
133 | );
134 |
135 | const userMessage = new Message({
136 | role: "user",
137 | content: content,
138 | status: "success",
139 | });
140 | userMessage.calculateNumTokens(this.newAdapter());
141 |
142 | this._messages.push(userMessage);
143 | this.emit("change", userMessage.copy());
144 |
145 | const maxIterations = (this.configuration.maxFeedback || 2) + 1;
146 | this.debug.log("beak-complete", "Max iterations:", maxIterations);
147 |
148 | let functionCall: "auto" | "none" = "auto";
149 |
150 | for (let i = 0; i < maxIterations; i++) {
151 | this.debug.log("beak-complete", "Iteration:", i);
152 |
153 | let message = new Message({ role: "assistant", status: "pending" });
154 | this._messages.push(message);
155 | this.emit("change", message.copy());
156 |
157 | const client = this.newAdapter();
158 |
159 | const contextMessage = this.infoMessage();
160 | let newMessages: Message[] = [];
161 | try {
162 | newMessages = await this.runChatCompletionAsync(client, {
163 | maxTokens: this.configuration.maxTokens,
164 | messages: [
165 | this.instructionsMessage,
166 | ...(this.formattingInstructionsMessage !== undefined
167 | ? [this.formattingInstructionsMessage]
168 | : []),
169 | ...(contextMessage !== undefined ? [contextMessage] : []),
170 | // we leave out the last message, because it is the one we are currently working on
171 | ...this._messages.slice(0, this._messages.length - 1),
172 | ],
173 | functions: Object.values(this.functions),
174 | functionCall: functionCall,
175 | temperature: this.configuration.temperature,
176 | });
177 |
178 | this.debug.log(
179 | "beak-complete",
180 | "runChatCompletionAsync returned message:",
181 | message
182 | );
183 | } catch (error) {
184 | console.error(error);
185 | return;
186 | }
187 |
188 | let hasFeedback = false;
189 |
190 | // first, filter out any empty messages
191 | for (const message of newMessages) {
192 | if (!message.functionCall && !message.content) {
193 | this.debug.log(
194 | "beak-complete",
195 | "No content, removing message:",
196 | message
197 | );
198 | this._messages = this._messages.filter((m) => m.id !== message.id);
199 | this.emit("change", this._messages.slice(-1)[0].copy());
200 | }
201 | }
202 |
203 | for (const message of newMessages) {
204 | message.calculateNumTokens(client);
205 |
206 | // handle text message
207 | if (!message.functionCall) {
208 | message.status = "success";
209 | this.debug.log(
210 | "beak-complete",
211 | "No function call, finalizing message:",
212 | message
213 | );
214 | this.emit("change", message.copy());
215 |
216 | continue; // next message
217 | }
218 |
219 | // handle function call
220 | const currentCall = message.functionCall;
221 |
222 | // handle function call not found
223 | if (!(currentCall.name in this.functions)) {
224 | message.status = "error";
225 | message.calculateNumTokens(client);
226 | this.emit("change", message.copy());
227 |
228 | // Insert a new message for the error message
229 | const errorMessage = new Message({
230 | role: "function",
231 | name: currentCall.name,
232 | status: "error",
233 | content: `Error: Function ${currentCall.name} not found.`,
234 | });
235 | errorMessage.calculateNumTokens(client);
236 |
237 | this._messages.push(errorMessage);
238 | this.emit("change", errorMessage.copy());
239 |
240 | continue; // next message
241 | }
242 |
243 | const fun = this.functions[currentCall.name];
244 |
245 | this.debug.log(
246 | "beak-complete",
247 | "Calling function:",
248 | currentCall.name,
249 | "with arguments:",
250 | currentCall.arguments
251 | );
252 |
253 | try {
254 | const result = await fun.handler(currentCall.arguments);
255 |
256 | message.status = "success";
257 |
258 | const resultString =
259 | typeof result === "string" ? result : JSON.stringify(result);
260 | const resultMessage = new Message({
261 | role: "function",
262 | name: currentCall.name,
263 | status: "success",
264 | content: resultString,
265 | result: result,
266 | });
267 | resultMessage.calculateNumTokens(client);
268 | this._messages.push(resultMessage);
269 |
270 | this.emit("change", resultMessage.copy());
271 |
272 | // in case the function wants feedback, signal that the result should be fed back to the LLM
273 | if (fun.feedback !== "none") {
274 | hasFeedback = true;
275 | }
276 |
277 | // if the function only wants text feedback, set function call to none
278 | if (fun.feedback === "text") {
279 | functionCall = "none";
280 | }
281 |
282 | continue; // next message
283 | } catch (error) {
284 | this.debug.log("beak-complete", "Error calling function:", error);
285 |
286 | message.status = "error";
287 |
288 | const errorMessage = new Message({
289 | role: "function",
290 | name: currentCall.name,
291 | status: "error",
292 | content: `Error: ${error}`,
293 | });
294 | errorMessage.calculateNumTokens(client);
295 | this._messages.push(errorMessage);
296 |
297 | this.emit("change", errorMessage.copy());
298 |
299 | // feed back the error message to the llm
300 | if (fun.feedback !== "none") {
301 | hasFeedback = true;
302 | }
303 |
304 | continue; // next message
305 | }
306 | }
307 |
308 | if (!hasFeedback) {
309 | break;
310 | }
311 | }
312 |
313 | this.debug.log("beak-complete", "Done running runChatCompletion");
314 |
315 | return;
316 | }
317 |
318 | private async runChatCompletionAsync(
319 | client: LLMAdapter,
320 | params: QueryChatCompletionParams
321 | ) {
322 | return new Promise((resolve, reject) => {
323 | const newMessages: Message[] = this._messages.slice(-1);
324 |
325 | const cleanup = () => {
326 | client.off("partial");
327 | client.off("function");
328 | client.off("content");
329 | client.off("error");
330 | client.off("end");
331 | };
332 | client.on("partial", (name, args) => {
333 | let [message] = newMessages.slice(-1);
334 |
335 | if (message.content) {
336 | // finalize this text message
337 | message.status = "success";
338 | message.calculateNumTokens(client);
339 |
340 | // create a new message for the function call
341 | message = new Message({ role: "assistant" });
342 | newMessages.push(message);
343 | this._messages.push(message);
344 | }
345 |
346 | message.status = "partial";
347 | this.emit("partial", name, args);
348 | this.emit("change", message.copy());
349 | });
350 |
351 | client.on("function", (functionCall: FunctionCall) => {
352 | const [message] = newMessages.slice(-1);
353 | message.functionCall = functionCall;
354 |
355 | this.emit("function", functionCall);
356 | this.emit("change", message.copy());
357 | });
358 |
359 | client.on("content", (content: string) => {
360 | let [message] = newMessages.slice(-1);
361 |
362 | if (message.functionCall) {
363 | // we leave the function call message unfinished and create a new one
364 | // for the text message
365 | message = new Message({ role: "assistant" });
366 | newMessages.push(message);
367 | this._messages.push(message);
368 | }
369 |
370 | message.content = message.content ? message.content + content : content;
371 |
372 | this.emit("content", content);
373 | this.emit("change", message.copy());
374 | });
375 |
376 | client.on("error", (error: Error) => {
377 | const [message] = newMessages.slice(-1);
378 | message.status = "error";
379 | message.content = "Error: " + error;
380 |
381 | this.emit("error", error);
382 | this.emit("change", message.copy());
383 | cleanup();
384 | reject(error);
385 | });
386 |
387 | client.on("end", () => {
388 | const [message] = newMessages.slice(-1);
389 |
390 | this.emit("end");
391 | cleanup();
392 | resolve(newMessages);
393 | });
394 |
395 | client.queryChatCompletion(params);
396 | });
397 | }
398 |
399 | private newAdapter(): LLMAdapter {
400 | return new OpenAIAdapter(
401 | new OpenAI({
402 | apiKey: this.configuration.openAIApiKey,
403 | baseUrl: this.configuration.baseUrl,
404 | model: this.configuration.openAIModel,
405 | debugLogger: this.configuration.debugLogger,
406 | })
407 | );
408 | }
409 | }
410 |
--------------------------------------------------------------------------------
/packages/core/src/index.ts:
--------------------------------------------------------------------------------
1 | export { BeakCore } from "./beak";
2 | export type { FunctionDefinition } from "./types";
3 | export { Message, DebugLogger } from "./types";
4 | export type { OpenAIModel, CustomModel } from "@beakjs/openai";
5 |
--------------------------------------------------------------------------------
/packages/core/src/openai.ts:
--------------------------------------------------------------------------------
1 | import {
2 | OpenAI,
3 | FetchChatCompletionParams,
4 | OpenAIFunction,
5 | OpenAIMessage,
6 | } from "@beakjs/openai";
7 | import {
8 | FunctionCall,
9 | FunctionDefinition,
10 | LLMAdapter,
11 | LLMEvent,
12 | Message,
13 | QueryChatCompletionParams,
14 | } from "./types";
15 |
16 | export class OpenAIAdapter implements LLMAdapter {
17 | constructor(private openai: OpenAI) {}
18 |
19 | countTokens(message: Message): number {
20 | return this.openai.countTokens(message);
21 | }
22 |
23 | async queryChatCompletion(params: QueryChatCompletionParams): Promise {
24 | const openAIParams: FetchChatCompletionParams = {
25 | messages: params.messages.map(messageToOpenAI),
26 | functions: functionsToOpenAIFormat(params.functions),
27 | maxTokens: params.maxTokens,
28 | temperature: params.temperature,
29 | };
30 | return await this.openai.queryChatCompletion(openAIParams);
31 | }
32 |
33 | on(event: LLMEvent, listener: (...args: any[]) => void): this {
34 | this.openai.on(event, listener);
35 | return this;
36 | }
37 |
38 | off(event: LLMEvent, listener?: (...args: any[]) => void): this {
39 | this.openai.off(event, listener);
40 | return this;
41 | }
42 | }
43 |
44 | function messageToOpenAI(message: Message): OpenAIMessage {
45 | const content = message.content || "";
46 | if (message.role === "system") {
47 | return { role: message.role, content };
48 | } else if (message.role === "function") {
49 | return {
50 | role: message.role,
51 | content,
52 | name: message.name,
53 | };
54 | } else {
55 | let functionCall = functionCallToOpenAI(message.functionCall);
56 |
57 | return {
58 | role: message.role,
59 | content,
60 | ...(functionCall !== undefined && { function_call: functionCall }),
61 | };
62 | }
63 | }
64 |
65 | function functionsToOpenAIFormat(
66 | functions?: FunctionDefinition[]
67 | ): OpenAIFunction[] | undefined {
68 | if (functions === undefined) {
69 | return undefined;
70 | }
71 | return functions.map((fun) => {
72 | const args = fun.parameters;
73 | let openAiProperties: { [key: string]: any } = {};
74 | let required: string[] = [];
75 |
76 | if (args) {
77 | for (const [name, arg] of Object.entries(args)) {
78 | const description = arg.description;
79 | if (typeof arg.type === "string" || arg.type === undefined) {
80 | const type = arg.type || "string";
81 | openAiProperties[name] = {
82 | type: arg.type,
83 | ...(description ? { description } : {}),
84 | };
85 | } else if (Array.isArray(arg.type)) {
86 | openAiProperties[name] = {
87 | type: "enum",
88 | enum: arg.type,
89 | ...(description ? { description } : {}),
90 | };
91 | }
92 |
93 | if (arg.optional !== true) {
94 | required.push(name);
95 | }
96 | }
97 | }
98 |
99 | return {
100 | name: fun.name,
101 | description: fun.description,
102 | parameters: {
103 | type: "object",
104 | properties: openAiProperties,
105 | ...(required.length ? { required } : {}),
106 | },
107 | };
108 | });
109 | }
110 |
111 | function functionCallToOpenAI(functionCall?: FunctionCall): any {
112 | if (functionCall === undefined) {
113 | return undefined;
114 | }
115 | return {
116 | name: functionCall.name,
117 | arguments: JSON.stringify(functionCall.arguments),
118 | };
119 | }
120 |
--------------------------------------------------------------------------------
/packages/core/src/types.ts:
--------------------------------------------------------------------------------
1 | import { v4 as uuidv4 } from "uuid";
2 |
3 | export type Role = "system" | "user" | "assistant" | "function";
4 |
5 | export type MessageStatus = "pending" | "partial" | "success" | "error";
6 |
7 | export type MessageOptions = {
8 | role?: Role;
9 | content?: string;
10 | status?: MessageStatus;
11 | functionCall?: FunctionCall;
12 | name?: string;
13 | result?: any;
14 | };
15 |
16 | /**
17 | * A message in BeakJs.
18 | */
19 | export class Message {
20 | /**
21 | * The unique identifier for the message.
22 | * @type {string}
23 | */
24 | public id: string = uuidv4();
25 |
26 | /**
27 | * The role of the message. Can be "system", "user", "assistant" or "function".
28 | * @type {Role}
29 | */
30 | public role: Role = "user";
31 |
32 | /**
33 | * Optional content of the message.
34 | * @type {string}
35 | */
36 | public content?: string;
37 |
38 | /**
39 | * The status of the message. Can be "pending", "partial", "success" or "error".
40 | * @type {MessageStatus}
41 | */
42 | public status: MessageStatus = "pending";
43 |
44 | /**
45 | * An optional function call returned by the assistant.
46 | * @type {FunctionCall}
47 | */
48 | public functionCall?: FunctionCall;
49 |
50 | /**
51 | * The name of the function called. Only available if the role is "function".
52 | * @type {string}
53 | */
54 | public name?: string;
55 |
56 | /**
57 | * The result of the function call as object. Only available if the role is "function".
58 | *
59 | * Note that a string representation of the call will also be available in the content field.
60 | * @type {any}
61 | */
62 | public result?: any;
63 |
64 | /**
65 | * The number of tokens in the message.
66 | * @type {number}
67 | */
68 | public numTokens: number = 0;
69 |
70 | /**
71 | * The date the message was created.
72 | * @type {Date}
73 | */
74 | public createdAt: Date = new Date();
75 |
76 | constructor(options?: MessageOptions) {
77 | options ||= {};
78 | Object.assign(this, options);
79 | }
80 |
81 | calculateNumTokens(llm: LLMAdapter) {
82 | this.numTokens = llm.countTokens(this);
83 | }
84 |
85 | copy(): Message {
86 | return Object.assign(new Message({}), this);
87 | }
88 | }
89 |
90 | export type FunctionHandler = (args: { [key: string]: any }) => any;
91 |
92 | export type Feedback = "none" | "auto" | "text";
93 |
94 | export interface FunctionDefinition {
95 | name: string;
96 | description?: string;
97 | parameters?: { [key: string]: FunctionParameter };
98 | feedback?: Feedback;
99 | handler: FunctionHandler;
100 | }
101 |
102 | export interface FunctionParameter {
103 | description?: string;
104 | type?: "string" | "number" | string[];
105 | optional?: boolean;
106 | }
107 |
108 | export interface FunctionCall {
109 | name: string;
110 | arguments: { [key: string]: any };
111 | }
112 |
113 | export type LLMEvent = "content" | "function" | "partial" | "error" | "end";
114 |
115 | export abstract class LLMAdapter {
116 | abstract countTokens(message: Message): number;
117 |
118 | abstract queryChatCompletion(
119 | params: QueryChatCompletionParams
120 | ): Promise;
121 |
122 | abstract on(event: LLMEvent, listener: (...args: any[]) => void): this;
123 |
124 | abstract off(event: LLMEvent, listener?: (...args: any[]) => void): this;
125 | }
126 |
127 | export interface QueryChatCompletionParams {
128 | messages: Message[];
129 | functions?: FunctionDefinition[];
130 | functionCall?: "none" | "auto";
131 | maxTokens?: number;
132 | temperature?: number;
133 | }
134 |
135 | type DebugEvent = "chat-internal" | "chat-api" | "beak-complete";
136 |
137 | export class DebugLogger {
138 | private debugEvents: DebugEvent[] = [];
139 |
140 | constructor(debugEvents?: DebugEvent[]) {
141 | this.debugEvents = debugEvents || [];
142 | }
143 |
144 | log(debugEvent: DebugEvent, ...args: any[]) {
145 | if (this.debugEvents.includes(debugEvent)) {
146 | console.log(`[${debugEvent}]`, ...args);
147 | }
148 | }
149 | table(debugEvent: DebugEvent, message: string, ...args: any[]) {
150 | if (this.debugEvents.includes(debugEvent)) {
151 | console.log(`[${debugEvent}] - ${message}:`);
152 | console.table(...args);
153 | }
154 | }
155 | warn(debugEvent: DebugEvent, ...args: any[]) {
156 | if (this.debugEvents.includes(debugEvent)) {
157 | console.warn(`[${debugEvent}]`, ...args);
158 | }
159 | }
160 | error(debugEvent: DebugEvent, ...args: any[]) {
161 | if (this.debugEvents.includes(debugEvent)) {
162 | console.error(`[${debugEvent}]`, ...args);
163 | }
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/packages/core/tsconfig.cjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES6",
4 | "module": "CommonJS",
5 | "declaration": true,
6 | "strict": true,
7 | "esModuleInterop": true,
8 | "skipLibCheck": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "outDir": "dist/cjs",
11 | "lib": ["esnext", "DOM"],
12 | "allowJs": true,
13 | "allowSyntheticDefaultImports": true,
14 | "moduleResolution": "node",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "noEmit": false
18 | },
19 | "include": ["src"]
20 | }
21 |
--------------------------------------------------------------------------------
/packages/core/tsconfig.esm.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES6",
4 | "module": "ESNext",
5 | "declaration": true,
6 | "strict": true,
7 | "esModuleInterop": true,
8 | "skipLibCheck": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "outDir": "dist/esm",
11 | "lib": ["esnext", "DOM"],
12 | "allowJs": true,
13 | "allowSyntheticDefaultImports": true,
14 | "moduleResolution": "node",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "noEmit": false
18 | },
19 | "include": ["src"]
20 | }
21 |
--------------------------------------------------------------------------------
/packages/css/Assistant.css:
--------------------------------------------------------------------------------
1 | .beakAssistantWindow {
2 | position: fixed;
3 | bottom: 1rem;
4 | right: 1rem;
5 | z-index: 30;
6 | line-height: 1.5;
7 | -webkit-text-size-adjust: 100%;
8 | -moz-tab-size: 4;
9 | -o-tab-size: 4;
10 | tab-size: 4;
11 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
12 | "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
13 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
14 | font-feature-settings: normal;
15 | font-variation-settings: normal;
16 | touch-action: manipulation;
17 | }
18 |
19 | .beakAssistantWindow svg {
20 | display: inline-block;
21 | vertical-align: middle;
22 | }
23 |
--------------------------------------------------------------------------------
/packages/css/Button.css:
--------------------------------------------------------------------------------
1 | .beakButton {
2 | width: 3.5rem;
3 | height: 3.5rem;
4 | display: flex;
5 | align-items: center;
6 | justify-content: center;
7 | border-radius: 50%;
8 | border: 1px solid rgba(255, 255, 255, 0.2);
9 | outline: none;
10 | position: relative;
11 | transform: scale(1);
12 | transition: transform 200ms;
13 | background-color: var(--beak-button-background-color);
14 | color: var(--beak-button-icon-color);
15 | cursor: pointer;
16 | }
17 |
18 | .beakButton:hover {
19 | transform: scale(1.1);
20 | }
21 |
22 | .beakButton:active {
23 | transform: scale(0.75);
24 | }
25 |
26 | .beakButtonIcon {
27 | transition: opacity 100ms, transform 300ms;
28 | position: absolute;
29 | top: 50%;
30 | left: 50%;
31 | transform: translate(-50%, -50%);
32 | }
33 |
34 | /* State when the chat is open */
35 | .beakButton.open .beakButtonIconOpen {
36 | transform: translate(-50%, -50%) scale(0) rotate(90deg);
37 | opacity: 0;
38 | }
39 |
40 | .beakButton.open .beakButtonIconClose {
41 | transform: translate(-50%, -50%) scale(1) rotate(0deg);
42 | opacity: 1;
43 | }
44 |
45 | /* State when the chat is closed */
46 | .beakButton:not(.open) .beakButtonIconOpen {
47 | transform: translate(-50%, -50%) scale(1) rotate(0deg);
48 | opacity: 1;
49 | }
50 |
51 | .beakButton:not(.open) .beakButtonIconClose {
52 | transform: translate(-50%, -50%) scale(0) rotate(-90deg);
53 | opacity: 0;
54 | }
55 |
--------------------------------------------------------------------------------
/packages/css/Header.css:
--------------------------------------------------------------------------------
1 | .beakHeader {
2 | height: 56px;
3 | font-weight: 500;
4 | display: flex;
5 | justify-content: center;
6 | align-items: center;
7 | position: relative;
8 | background-color: var(--beak-header-background-color);
9 | color: var(--beak-header-title-color);
10 | border-top-left-radius: 0;
11 | border-top-right-radius: 0;
12 | border-bottom: 1px solid var(--beak-header-separator-color);
13 | }
14 |
15 | @media (min-width: 640px) {
16 | .beakHeader {
17 | padding-left: 24px;
18 | padding-right: 24px;
19 | border-top-left-radius: 8px;
20 | border-top-right-radius: 8px;
21 | }
22 | }
23 |
24 | .beakHeader > button {
25 | border: 0;
26 | padding: 0px;
27 | position: absolute;
28 | top: 50%;
29 | right: 16px;
30 | transform: translateY(-50%);
31 | outline: none;
32 | color: var(--beak-header-close-button-color);
33 | background-color: transparent;
34 | cursor: pointer;
35 | }
36 |
37 | .beakHeader > button:focus {
38 | outline: none;
39 | }
40 |
--------------------------------------------------------------------------------
/packages/css/Input.css:
--------------------------------------------------------------------------------
1 | .beakInput {
2 | border-top: 1px solid var(--beak-input-separator-color);
3 | padding-left: 2rem;
4 | padding-right: 2.5rem;
5 | padding-top: 1rem;
6 | padding-bottom: 1rem;
7 | display: flex;
8 | align-items: center;
9 | cursor: text;
10 | position: relative;
11 | border-bottom-left-radius: 0.75rem;
12 | border-bottom-right-radius: 0.75rem;
13 | background-color: var(--beak-input-background-color);
14 | }
15 |
16 | .beakInput > button {
17 | position: absolute;
18 | right: 0.5rem;
19 | padding: 0.25rem;
20 | cursor: pointer;
21 | transition-property: transform;
22 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
23 | transition-duration: 200ms;
24 | transform: scale(1);
25 | color: rgba(0, 0, 0, 0.25);
26 | -webkit-appearance: button;
27 | appearance: button;
28 | background-color: transparent;
29 | background-image: none;
30 | text-transform: none;
31 | font-family: inherit;
32 | font-size: 100%;
33 | font-weight: inherit;
34 | line-height: inherit;
35 | border: 0;
36 | margin: 0;
37 | text-indent: 0px;
38 | text-shadow: none;
39 | display: inline-block;
40 | text-align: center;
41 | }
42 |
43 | .beakInput > button:not([disabled]) {
44 | color: var(--beak-input-send-button-color);
45 | }
46 |
47 | .beakInput > button:not([disabled]):hover {
48 | transform: scale(1.1);
49 | }
50 |
51 | .beakInput > button[disabled] {
52 | color: var(--beak-input-send-button-disabled-color);
53 | }
54 |
55 | .beakInput > textarea {
56 | width: 100%;
57 | outline: 2px solid transparent;
58 | outline-offset: 2px;
59 | resize: none;
60 | white-space: pre-wrap;
61 | overflow-wrap: break-word;
62 | -webkit-font-smoothing: antialiased;
63 | -moz-osx-font-smoothing: grayscale;
64 | cursor: text;
65 | font-size: 0.875rem;
66 | line-height: 1.25rem;
67 | margin: 0;
68 | padding: 0;
69 | font-family: inherit;
70 | font-weight: inherit;
71 | color: var(--beak-input-color);
72 | border: 0px;
73 | background-color: var(--beak-input-background-color);
74 | }
75 |
76 | .beakInput > textarea::placeholder {
77 | color: var(--beak-input-placeholder-color);
78 | opacity: 1;
79 | }
80 |
--------------------------------------------------------------------------------
/packages/css/Messages.css:
--------------------------------------------------------------------------------
1 | .beakMessages {
2 | overflow-y: scroll;
3 | flex: 1;
4 | padding: 1rem 2rem;
5 | display: flex;
6 | flex-direction: column;
7 | background-color: var(--beak-messages-background-color);
8 | }
9 |
10 | .beakMessages::-webkit-scrollbar {
11 | width: 9px;
12 | }
13 |
14 | .beakMessages::-webkit-scrollbar-thumb {
15 | background-color: var(--beak-scrollbar-color);
16 | border-radius: 10rem;
17 | border: 2px solid var(--beak-messages-background-color);
18 | }
19 |
20 | .beakMessages::-webkit-scrollbar-track-piece:start {
21 | background: transparent;
22 | }
23 |
24 | .beakMessages::-webkit-scrollbar-track-piece:end {
25 | background: transparent;
26 | }
27 |
28 | .beakMessage {
29 | border-radius: 0.5rem;
30 | padding: 1rem;
31 | font-size: 0.875rem;
32 | line-height: 1.25rem;
33 | overflow-wrap: break-word;
34 | max-width: 80%;
35 | margin-bottom: 0.5rem;
36 | }
37 |
38 | .beakMessage.beakUserMessage {
39 | background: var(--beak-message-user-background-color);
40 | color: var(--beak-message-user-color);
41 | margin-left: auto;
42 | }
43 |
44 | .beakMessage.beakAssistantMessage {
45 | background: var(--beak-message-assistant-background-color);
46 | color: var(--beak-message-assistant-color);
47 | margin-right: auto;
48 | }
49 |
50 | .beakMessage.beakUserMessage + .beakMessage.beakAssistantMessage {
51 | margin-top: 1.5rem;
52 | }
53 |
54 | .beakMessage.beakAssistantMessage + .beakMessage.beakUserMessage {
55 | margin-top: 1.5rem;
56 | }
57 |
--------------------------------------------------------------------------------
/packages/css/Window.css:
--------------------------------------------------------------------------------
1 | .beakWindow {
2 | position: fixed;
3 | inset: 0px;
4 | transform-origin: bottom;
5 | border-color: rgb(229 231 235);
6 | background-color: rgb(255 255 255);
7 | border-radius: 0.75rem;
8 | box-shadow: rgba(0, 0, 0, 0.16) 0px 5px 40px;
9 | flex-direction: column;
10 | transition: opacity 100ms ease-out, transform 200ms ease-out;
11 | opacity: 0;
12 | transform: scale(0.95) translateY(20px);
13 | display: flex;
14 | pointer-events: none;
15 | }
16 |
17 | .beakWindow.open {
18 | opacity: 1;
19 | transform: scale(1) translateY(0);
20 | pointer-events: auto;
21 | }
22 |
23 | @media (min-width: 640px) {
24 | .beakWindow {
25 | transform-origin: bottom right;
26 | bottom: 5rem;
27 | right: 1rem;
28 | top: auto;
29 | left: auto;
30 | border-width: 1px;
31 | margin-bottom: 1rem;
32 | width: 24rem;
33 | height: 600px;
34 | min-height: 200px;
35 | max-height: calc(100% - 6rem);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/packages/css/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --beak-primary-color: rgb(59 130 246);
3 | --beak-contrast-color: rgb(255 255 255);
4 | --beak-secondary-color: rgb(243 244 246);
5 | --beak-secondary-contrast-color: rgb(0 0 0);
6 | --beak-background-color: rgb(255 255 255);
7 | --beak-muted-color: rgb(106 106 106);
8 | --beak-separator-color: rgba(0, 0, 0, 0.08);
9 | --beak-scrollbar-color: rgba(0, 0, 0, 0.2);
10 |
11 | --beak-button-background-color: var(--beak-primary-color);
12 |
13 | /* The color of the icon in the open/close button */
14 | --beak-button-icon-color: var(--beak-contrast-color);
15 |
16 | /* The background color of the header of the chat window */
17 | --beak-header-background-color: var(--beak-primary-color);
18 |
19 | /* The color of the title in the header of the chat window */
20 | --beak-header-title-color: var(--beak-contrast-color);
21 |
22 | /* The color of the close button in the header of the chat window */
23 | --beak-header-close-button-color: var(--beak-contrast-color);
24 |
25 | /* The color of the separator between the header and the messages area */
26 | --beak-header-separator-color: var(--beak-separator-color);
27 |
28 | /* The background color of the input area */
29 | --beak-input-background-color: var(--beak-background-color);
30 |
31 | /* The color of the send button */
32 | --beak-input-send-button-color: var(--beak-primary-color);
33 |
34 | /* The color of the send button when disabled */
35 | --beak-input-send-button-disabled-color: var(--beak-muted-color);
36 |
37 | /* The color of the placeholder text in the input area */
38 | --beak-input-placeholder-color: var(--beak-muted-color);
39 |
40 | /* The color of the text in the input area */
41 | --beak-input-color: var(--beak-secondary-contrast-color);
42 |
43 | /* The color of the separator between the input area and the messages area */
44 | --beak-input-separator-color: var(--beak-separator-color);
45 |
46 | /* The background color of the messages area */
47 | --beak-messages-background-color: var(--beak-background-color);
48 |
49 | /* The background color of a user message */
50 | --beak-message-user-background-color: var(--beak-primary-color);
51 | --beak-message-user-color: var(--beak-contrast-color);
52 |
53 | /* The background color of a assistant message */
54 | --beak-message-assistant-background-color: var(--beak-secondary-color);
55 | --beak-message-assistant-color: var(--beak-secondary-contrast-color);
56 |
57 | /*
58 | * Dark Mode
59 | */
60 |
61 | --beak-dark-primary-color: rgb(59 130 246);
62 | --beak-dark-contrast-color: rgb(255 255 255);
63 | --beak-dark-secondary-color: rgb(37, 36, 42);
64 | --beak-dark-secondary-contrast-color: rgb(255 255 255);
65 | --beak-dark-background-color: rgb(0 0 0);
66 | --beak-dark-muted-color: rgb(106 106 106);
67 | --beak-dark-separator-color: rgba(255, 255, 255, 0.15);
68 | --beak-dark-scrollbar-color: rgba(255, 255, 255, 0.3);
69 | --beak-dark-button-background-color: var(--beak-dark-primary-color);
70 | --beak-dark-button-icon-color: var(--beak-dark-contrast-color);
71 | --beak-dark-header-background-color: var(--beak-dark-primary-color);
72 | --beak-dark-header-title-color: var(--beak-dark-contrast-color);
73 | --beak-dark-header-close-button-color: var(--beak-dark-contrast-color);
74 | --beak-dark-header-separator-color: var(--beak-dark-separator-color);
75 | --beak-dark-input-background-color: var(--beak-dark-background-color);
76 | --beak-dark-input-send-button-color: var(--beak-dark-primary-color);
77 | --beak-dark-input-send-button-disabled-color: var(--beak-dark-muted-color);
78 | --beak-dark-input-placeholder-color: var(--beak-dark-muted-color);
79 | --beak-dark-input-color: var(--beak-dark-secondary-contrast-color);
80 | --beak-dark-input-separator-color: var(--beak-dark-separator-color);
81 | --beak-dark-messages-background-color: var(--beak-dark-background-color);
82 | --beak-dark-message-user-background-color: var(--beak-dark-primary-color);
83 | --beak-dark-message-user-color: var(--beak-dark-contrast-color);
84 | --beak-dark-message-assistant-background-color: var(
85 | --beak-dark-secondary-color
86 | );
87 | --beak-dark-message-assistant-color: var(
88 | --beak-dark-secondary-contrast-color
89 | );
90 | }
91 |
92 | .beakColorSchemeDark,
93 | .beakColorSchemeDark * {
94 | color-scheme: light dark;
95 | --beak-primary-color: var(--beak-dark-primary-color);
96 | --beak-contrast-color: var(--beak-dark-contrast-color);
97 | --beak-secondary-color: var(--beak-dark-secondary-color);
98 | --beak-secondary-contrast-color: var(--beak-dark-secondary-contrast-color);
99 | --beak-background-color: var(--beak-dark-background-color);
100 | --beak-muted-color: var(--beak-dark-muted-color);
101 | --beak-separator-color: var(--beak-dark-separator-color);
102 | --beak-scrollbar-color: var(--beak-dark-scrollbar-color);
103 | --beak-button-background-color: var(--beak-dark-button-background-color);
104 | --beak-button-icon-color: var(--beak-dark-button-icon-color);
105 | --beak-header-background-color: var(--beak-dark-header-background-color);
106 | --beak-header-title-color: var(--beak-dark-header-title-color);
107 | --beak-header-close-button-color: var(--beak-dark-header-close-button-color);
108 | --beak-header-separator-color: var(--beak-dark-header-separator-color);
109 | --beak-input-background-color: var(--beak-dark-input-background-color);
110 | --beak-input-send-button-color: var(--beak-dark-input-send-button-color);
111 | --beak-input-send-button-disabled-color: var(
112 | --beak-dark-input-send-button-disabled-color
113 | );
114 | --beak-input-placeholder-color: var(--beak-dark-input-placeholder-color);
115 | --beak-input-color: var(--beak-dark-input-color);
116 | --beak-input-separator-color: var(--beak-dark-input-separator-color);
117 | --beak-messages-background-color: var(--beak-dark-messages-background-color);
118 | --beak-message-user-background-color: var(
119 | --beak-dark-message-user-background-color
120 | );
121 | --beak-message-user-color: var(--beak-dark-message-user-color);
122 | --beak-message-assistant-background-color: var(
123 | --beak-dark-message-assistant-background-color
124 | );
125 | --beak-message-assistant-color: var(--beak-dark-message-assistant-color);
126 | }
127 |
128 | @media (prefers-color-scheme: dark) {
129 | .beakColorSchemeAuto,
130 | .beakColorSchemeAuto * {
131 | color-scheme: light dark;
132 | --beak-primary-color: var(--beak-dark-primary-color);
133 | --beak-contrast-color: var(--beak-dark-contrast-color);
134 | --beak-secondary-color: var(--beak-dark-secondary-color);
135 | --beak-secondary-contrast-color: var(--beak-dark-secondary-contrast-color);
136 | --beak-background-color: var(--beak-dark-background-color);
137 | --beak-muted-color: var(--beak-dark-muted-color);
138 | --beak-separator-color: var(--beak-dark-separator-color);
139 | --beak-scrollbar-color: var(--beak-dark-scrollbar-color);
140 | --beak-button-background-color: var(--beak-dark-button-background-color);
141 | --beak-button-icon-color: var(--beak-dark-button-icon-color);
142 | --beak-header-background-color: var(--beak-dark-header-background-color);
143 | --beak-header-title-color: var(--beak-dark-header-title-color);
144 | --beak-header-close-button-color: var(
145 | --beak-dark-header-close-button-color
146 | );
147 | --beak-header-separator-color: var(--beak-dark-header-separator-color);
148 | --beak-input-background-color: var(--beak-dark-input-background-color);
149 | --beak-input-send-button-color: var(--beak-dark-input-send-button-color);
150 | --beak-input-send-button-disabled-color: var(
151 | --beak-dark-input-send-button-disabled-color
152 | );
153 | --beak-input-placeholder-color: var(--beak-dark-input-placeholder-color);
154 | --beak-input-color: var(--beak-dark-input-color);
155 | --beak-input-separator-color: var(--beak-dark-input-separator-color);
156 | --beak-messages-background-color: var(
157 | --beak-dark-messages-background-color
158 | );
159 | --beak-message-user-background-color: var(
160 | --beak-dark-message-user-background-color
161 | );
162 | --beak-message-user-color: var(--beak-dark-message-user-color);
163 | --beak-message-assistant-background-color: var(
164 | --beak-dark-message-assistant-background-color
165 | );
166 | --beak-message-assistant-color: var(--beak-dark-message-assistant-color);
167 | }
168 | }
169 |
170 | .beakActivityDot1 {
171 | animation: beakActivityDotsAnimation 1.05s infinite;
172 | }
173 | .beakActivityDot2 {
174 | animation-delay: 0.1s;
175 | }
176 | .beakActivityDot3 {
177 | animation-delay: 0.2s;
178 | }
179 | @keyframes beakActivityDotsAnimation {
180 | 0%,
181 | 57.14% {
182 | animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
183 | transform: translate(0);
184 | }
185 | 28.57% {
186 | animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
187 | transform: translateY(-6px);
188 | }
189 | 100% {
190 | transform: translate(0);
191 | }
192 | }
193 |
194 | @keyframes beakSpinAnimation {
195 | to {
196 | transform: rotate(360deg);
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/packages/express/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@beakjs/express",
3 | "version": "0.0.6",
4 | "description": "Beak.js proxy for Express",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "scripts": {
8 | "build": "tsc",
9 | "prepublishOnly": "cp ../../README.md .",
10 | "clean": "rm -rf dist && rm -rf node_modules"
11 | },
12 | "files": [
13 | "dist/**/*",
14 | "README.md"
15 | ],
16 | "dependencies": {
17 | "@beakjs/server": "0.0.6"
18 | },
19 | "peerDependencies": {
20 | "express": "^4.18.2"
21 | },
22 | "devDependencies": {
23 | "express": "^4.18.2",
24 | "@types/express": "^4.17.21"
25 | },
26 | "repository": {
27 | "type": "git",
28 | "url": "https://github.com/mme/beakjs.git"
29 | },
30 | "author": "Markus Ecker",
31 | "license": "MIT"
32 | }
33 |
--------------------------------------------------------------------------------
/packages/express/src/index.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 | import {
3 | BeakServer,
4 | BeakServerProps,
5 | HttpAdapter,
6 | FetchChatCompletionParams,
7 | } from "@beakjs/server";
8 |
9 | export function beakHandler(beakProps?: BeakServerProps) {
10 | const beakServer = new BeakServer(beakProps);
11 |
12 | return async function handler(req: Request, res: Response) {
13 | if (req.path.endsWith("/v1/chat/completions") && req.method === "POST") {
14 | const adapter = createHttpAdapter(res);
15 | try {
16 | const params = await parseBody(req);
17 | await beakServer.handleRequest(req, params, adapter);
18 | } catch (error: any) {
19 | console.error(error);
20 | res.status(500).send("Internal Server Error.");
21 | }
22 | } else {
23 | res.status(404).send("Not found");
24 | }
25 | };
26 | }
27 |
28 | function createHttpAdapter(res: Response): HttpAdapter {
29 | return {
30 | onData(data: any) {
31 | res.write(`data: ${JSON.stringify(data)}\n`);
32 | },
33 | onEnd() {
34 | res.end("[DONE]\n");
35 | },
36 | onError(error: any) {
37 | console.error(error);
38 | res.status(500).send(`Error: ${error.message || "An error occurred"}`);
39 | },
40 | };
41 | }
42 |
43 | async function parseBody(req: Request): Promise {
44 | if (req.body) {
45 | return req.body as T;
46 | }
47 | if (!req.readable) {
48 | throw new Error("No body found.");
49 | }
50 | const rawData = await new Promise((resolve, reject) => {
51 | let data = "";
52 | req.on("data", (chunk) => (data += chunk));
53 | req.on("end", () => resolve(data));
54 | req.on("error", (err) => reject(err));
55 | });
56 |
57 | return JSON.parse(rawData) as T;
58 | }
59 |
--------------------------------------------------------------------------------
/packages/express/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "module": "commonjs",
5 | "declaration": true,
6 | "strict": true,
7 | "esModuleInterop": true,
8 | "skipLibCheck": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "outDir": "dist",
11 | "lib": ["es2019", "dom"],
12 | "allowJs": true,
13 | "allowSyntheticDefaultImports": true,
14 | "moduleResolution": "node",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "noEmit": false,
18 | "types": ["node"]
19 | },
20 | "include": ["src"],
21 | "exclude": ["node_modules", "dist"]
22 | }
23 |
--------------------------------------------------------------------------------
/packages/next/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@beakjs/next",
3 | "version": "0.0.6",
4 | "description": "Beak.js proxy for Next.js",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "scripts": {
8 | "build": "tsc",
9 | "prepublishOnly": "cp ../../README.md .",
10 | "clean": "rm -rf dist && rm -rf node_modules"
11 | },
12 | "files": [
13 | "dist/**/*",
14 | "README.md"
15 | ],
16 | "dependencies": {
17 | "@beakjs/server": "0.0.6"
18 | },
19 | "peerDependencies": {
20 | "next": "^14.0.3"
21 | },
22 | "devDependencies": {
23 | "next": "^14.0.3"
24 | },
25 | "repository": {
26 | "type": "git",
27 | "url": "https://github.com/mme/beakjs.git"
28 | },
29 | "author": "Markus Ecker",
30 | "license": "MIT"
31 | }
32 |
--------------------------------------------------------------------------------
/packages/next/src/index.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 | import {
3 | BeakServer,
4 | BeakServerProps,
5 | HttpAdapter,
6 | FetchChatCompletionParams,
7 | } from "@beakjs/server";
8 |
9 | export function beakHandler(beakProps?: BeakServerProps) {
10 | const beakServer = new BeakServer(beakProps);
11 |
12 | async function handler(req: NextApiRequest, res: NextApiResponse) {
13 | if (req.url?.endsWith("/v1/chat/completions") && req.method === "POST") {
14 | const adapter = createHttpAdapter(res);
15 |
16 | try {
17 | const params = req.body as FetchChatCompletionParams;
18 | await beakServer.handleRequest(req, params, adapter);
19 | } catch (error) {
20 | console.error(error);
21 | res.status(500).send("Internal Server Error.");
22 | }
23 | } else {
24 | res.status(404).send("Not found");
25 | }
26 | }
27 |
28 | return handler;
29 | }
30 |
31 | function createHttpAdapter(res: NextApiResponse): HttpAdapter {
32 | return {
33 | onData(data: any) {
34 | res.write(`data: ${JSON.stringify(data)}\n`);
35 | },
36 | onEnd() {
37 | res.end("[DONE]\n");
38 | },
39 | onError(error: any) {
40 | console.error(error);
41 | res.status(500).send(`Error: ${error.message || "An error occurred"}`);
42 | },
43 | };
44 | }
45 |
--------------------------------------------------------------------------------
/packages/next/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "module": "commonjs",
5 | "declaration": true,
6 | "strict": true,
7 | "esModuleInterop": true,
8 | "skipLibCheck": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "outDir": "dist",
11 | "lib": ["es2019", "dom"],
12 | "allowJs": true,
13 | "allowSyntheticDefaultImports": true,
14 | "moduleResolution": "node",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "noEmit": false,
18 | "types": ["node"]
19 | },
20 | "include": ["src"],
21 | "exclude": ["node_modules", "dist"]
22 | }
23 |
--------------------------------------------------------------------------------
/packages/openai/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@beakjs/openai",
3 | "version": "0.0.6",
4 | "author": "Markus Ecker",
5 | "license": "MIT",
6 | "description": "Beak.js OpenAI library",
7 | "main": "dist/cjs/index.js",
8 | "module": "dist/esm/index.js",
9 | "types": "dist/cjs/index.d.ts",
10 | "scripts": {
11 | "build": "tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json",
12 | "prepublishOnly": "cp ../../README.md .",
13 | "clean": "rm -rf dist && rm -rf node_modules",
14 | "test": "jest"
15 | },
16 | "files": [
17 | "dist/**/*",
18 | "README.md"
19 | ],
20 | "dependencies": {
21 | "eventemitter3": "^5.0.1",
22 | "jsonrepair": "^3.2.4"
23 | },
24 | "devDependencies": {
25 | "typescript": "^5.0.4",
26 | "@types/jest": "^29.5.1",
27 | "jest": "^29.5.0",
28 | "ts-jest": "^29.1.0"
29 | },
30 | "peerDependencies": {},
31 | "repository": {
32 | "type": "git",
33 | "url": "https://github.com/mme/beakjs.git"
34 | },
35 | "jest": {
36 | "testTimeout": 60000,
37 | "transform": {
38 | "^.+\\.(ts|tsx)$": "ts-jest"
39 | },
40 | "testEnvironment": "node",
41 | "moduleFileExtensions": [
42 | "ts",
43 | "tsx",
44 | "js"
45 | ],
46 | "testPathIgnorePatterns": [
47 | "/node_modules/"
48 | ]
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/packages/openai/src/chat.ts:
--------------------------------------------------------------------------------
1 | import EventEmitter from "eventemitter3";
2 | import {
3 | OpenAIMessage,
4 | OpenAIFunction,
5 | DebugLogger,
6 | NoopDebugLogger,
7 | DEFAULT_MODEL,
8 | } from "./types";
9 |
10 | interface ChatCompletionConfiguration {
11 | apiKey?: string;
12 | baseUrl?: string;
13 | debugLogger?: DebugLogger;
14 | }
15 |
16 | interface ChatCompletionEvents {
17 | end: void;
18 | data: any;
19 | error: any;
20 | }
21 |
22 | export interface FetchChatCompletionParams {
23 | model?: string;
24 | messages: OpenAIMessage[];
25 | functions?: OpenAIFunction[];
26 | functionCall?: "none" | "auto";
27 | temperature?: number;
28 | maxTokens?: number;
29 | }
30 |
31 | const DEFAULT_BASE_URL = "https://api.openai.com";
32 | const COMPLETIONS_PATH = "/v1/chat/completions";
33 |
34 | export class ChatCompletion extends EventEmitter {
35 | private apiKey?: string;
36 | private buffer = new Uint8Array();
37 | private bodyReader: ReadableStreamDefaultReader | null = null;
38 | private debug: DebugLogger;
39 | private url: string;
40 |
41 | constructor({ apiKey, baseUrl, debugLogger }: ChatCompletionConfiguration) {
42 | super();
43 | this.apiKey = apiKey;
44 | this.debug = debugLogger || NoopDebugLogger;
45 | this.url =
46 | (baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "") + COMPLETIONS_PATH;
47 |
48 | if (!apiKey && !baseUrl) {
49 | console.warn("No API key or base URL provided.");
50 | }
51 | }
52 |
53 | private async cleanup() {
54 | this.debug.log("chat-internal", "Cleaning up...");
55 |
56 | if (this.bodyReader) {
57 | try {
58 | await this.bodyReader.cancel();
59 | } catch (error) {
60 | console.warn("Failed to cancel body reader:", error);
61 | }
62 | }
63 | this.bodyReader = null;
64 | this.buffer = new Uint8Array();
65 | }
66 |
67 | public async fetchChatCompletion({
68 | model,
69 | messages,
70 | functions,
71 | functionCall,
72 | temperature,
73 | }: FetchChatCompletionParams): Promise {
74 | await this.cleanup();
75 |
76 | functionCall ||= "auto";
77 | temperature ||= 0.5;
78 | functions ||= [];
79 | model ||= DEFAULT_MODEL;
80 |
81 | if (functions.length == 0) {
82 | functionCall = undefined;
83 | }
84 |
85 | try {
86 | this.debug.log("chat-api", "Fetching chat completion...");
87 | this.debug.table("chat-api", "Params", {
88 | model,
89 | functionCall,
90 | temperature,
91 | });
92 | this.debug.table("chat-api", "Functions", functions);
93 | this.debug.table("chat-api", "Messages", messages);
94 |
95 | const response = await fetch(this.url, {
96 | method: "POST",
97 | headers: {
98 | "Content-Type": "application/json",
99 | ...(this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}),
100 | },
101 | body: JSON.stringify({
102 | model,
103 | messages,
104 | stream: true,
105 | ...(functions.length ? { functions } : {}),
106 | ...(temperature ? { temperature } : {}),
107 | ...(functionCall && functions.length
108 | ? { function_call: functionCall }
109 | : {}),
110 | }),
111 | });
112 |
113 | this.debug.table("chat-api", "Response", {
114 | status: response.status,
115 | statusText: response.statusText,
116 | });
117 |
118 | if (!response.ok) {
119 | try {
120 | const errorText = await response.text();
121 | await this.cleanup();
122 | const msg = `Error ${response.status}: ${errorText}`;
123 | this.debug.error("chat-api", msg);
124 | this.emit("error", new Error(msg));
125 | } catch (_error) {
126 | await this.cleanup();
127 | const msg = `Error ${response.status}: ${response.statusText}`;
128 | this.debug.error("chat-api", msg);
129 | this.emit("error", new Error(msg));
130 | }
131 | return;
132 | }
133 |
134 | if (response.body == null) {
135 | await this.cleanup();
136 | const msg = "Response body is null";
137 | this.debug.error("chat-api", msg);
138 | this.emit("error", new Error(msg));
139 | return;
140 | }
141 |
142 | this.bodyReader = response.body.getReader();
143 |
144 | await this.streamBody();
145 | } catch (error) {
146 | await this.cleanup();
147 | this.debug.error("chat-api", error);
148 | this.emit("error", error);
149 | return;
150 | }
151 | }
152 |
153 | private async streamBody() {
154 | while (true) {
155 | try {
156 | const { done, value } = await this.bodyReader!.read();
157 |
158 | if (done) {
159 | await this.cleanup();
160 | this.emit("end");
161 | return;
162 | }
163 |
164 | const shouldContinue = await this.processData(value);
165 |
166 | if (!shouldContinue) {
167 | return;
168 | }
169 | } catch (error) {
170 | await this.cleanup();
171 | this.emit("error", error);
172 | return;
173 | }
174 | }
175 | }
176 |
177 | private async processData(data: Uint8Array): Promise {
178 | // Append new data to the temp buffer
179 | const newBuffer = new Uint8Array(this.buffer.length + data.length);
180 | newBuffer.set(this.buffer);
181 | newBuffer.set(data, this.buffer.length);
182 | this.buffer = newBuffer;
183 |
184 | const dataString = new TextDecoder("utf-8").decode(this.buffer);
185 | this.debug.log("chat-internal", "Received data chunk:", dataString);
186 |
187 | let lines = dataString.split("\n").filter((line) => line.trim() !== "");
188 |
189 | // If the last line isn't complete, keep it in the buffer for next time
190 | if (!dataString.endsWith("\n")) {
191 | const lastLine = lines.pop() || "";
192 | const remainingBytes = new TextEncoder().encode(lastLine);
193 | this.buffer = new Uint8Array(remainingBytes);
194 | } else {
195 | this.buffer = new Uint8Array();
196 | }
197 |
198 | for (const line of lines) {
199 | const cleanedLine = line.replace(/^data: /, "");
200 |
201 | if (cleanedLine === "[DONE]") {
202 | this.debug.log("chat-internal", "Received DONE signal.");
203 | await this.cleanup();
204 | this.emit("end");
205 | return false;
206 | }
207 |
208 | let json;
209 | try {
210 | json = JSON.parse(cleanedLine);
211 | } catch (error) {
212 | console.error("Failed to parse JSON:", error);
213 | continue;
214 | }
215 | this.debug.log("chat-internal", "Parsed JSON from line:", json);
216 |
217 | this.emit("data", json);
218 | }
219 | return true;
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/packages/openai/src/index.ts:
--------------------------------------------------------------------------------
1 | export { OpenAI } from "./openai";
2 | export { ChatCompletion } from "./chat";
3 | export type {
4 | OpenAIModel,
5 | OpenAIMessage,
6 | OpenAIFunction,
7 | CustomModel,
8 | } from "./types";
9 | export type { FetchChatCompletionParams } from "./chat";
10 |
--------------------------------------------------------------------------------
/packages/openai/src/openai.ts:
--------------------------------------------------------------------------------
1 | import EventEmitter from "eventemitter3";
2 | import { jsonrepair } from "jsonrepair";
3 | import {
4 | OpenAIChatCompletionChunk,
5 | OpenAIModel,
6 | OpenAIFunction,
7 | OpenAIMessage,
8 | DebugLogger,
9 | NoopDebugLogger,
10 | DEFAULT_MODEL,
11 | CustomModel,
12 | } from "./types";
13 | import { ChatCompletion, FetchChatCompletionParams } from "./chat";
14 |
15 | interface OpenAIConfiguration {
16 | apiKey?: string;
17 | baseUrl?: string;
18 | model?: OpenAIModel | CustomModel;
19 | debugLogger?: DebugLogger;
20 | }
21 |
22 | interface OpenAIEvents {
23 | content: string;
24 | partial: [string, string];
25 | error: any;
26 | function: {
27 | name: string;
28 | arguments: any;
29 | };
30 | end: void;
31 | }
32 |
33 | export class OpenAI extends EventEmitter {
34 | private apiKey?: string;
35 | private baseUrl?: string;
36 | private model: OpenAIModel | CustomModel;
37 | private debug: DebugLogger;
38 |
39 | private completionClient: ChatCompletion | null = null;
40 | private mode: "function" | "message" | null = null;
41 | private functionCallName: string = "";
42 | private functionCallArguments: string = "";
43 |
44 | constructor(params: OpenAIConfiguration) {
45 | super();
46 | this.apiKey = params.apiKey;
47 | this.model = params.model || DEFAULT_MODEL;
48 | this.debug = params.debugLogger || NoopDebugLogger;
49 | this.baseUrl = params.baseUrl;
50 | }
51 |
52 | public async queryChatCompletion(params: FetchChatCompletionParams) {
53 | params = { ...params };
54 | if (!(this.model instanceof CustomModel)) {
55 | params.maxTokens ||= maxTokensForModel(this.model);
56 | } else if (!params.maxTokens) {
57 | throw new Error("maxTokens must be specified for custom models.");
58 | }
59 | params.functions ||= [];
60 | params.model =
61 | this.model instanceof CustomModel ? this.model.name : this.model;
62 | params.messages = this.buildPrompt(params);
63 | return await this.runPrompt(params);
64 | }
65 |
66 | private buildPrompt(params: FetchChatCompletionParams): OpenAIMessage[] {
67 | let maxTokens = params.maxTokens!;
68 | const messages = params.messages!;
69 | const functions = params.functions!;
70 | const functionsNumTokens = countFunctionsTokens(functions);
71 | if (functionsNumTokens > maxTokens) {
72 | throw new Error(
73 | `Too many tokens in function calls: ${functionsNumTokens} > ${maxTokens}`
74 | );
75 | }
76 | maxTokens -= functionsNumTokens;
77 |
78 | for (const message of messages) {
79 | if (message.role === "system") {
80 | const numTokens = message.numTokens || this.countTokens(message);
81 | maxTokens -= numTokens;
82 |
83 | if (maxTokens < 0) {
84 | throw new Error("Not enough tokens for system message.");
85 | }
86 | }
87 | }
88 |
89 | const result: OpenAIMessage[] = [];
90 | let cutoff: boolean = false;
91 |
92 | const reversedMessages = [...messages].reverse();
93 | for (const message of reversedMessages) {
94 | if (message.role === "system") {
95 | result.unshift(message);
96 | continue;
97 | } else if (cutoff) {
98 | continue;
99 | }
100 | let numTokens = message.numTokens || this.countTokens(message);
101 | if (maxTokens < numTokens) {
102 | cutoff = true;
103 | continue;
104 | }
105 | result.unshift(message);
106 | maxTokens -= numTokens;
107 | }
108 |
109 | return result;
110 | }
111 |
112 | private async runPrompt(params: FetchChatCompletionParams): Promise {
113 | this.completionClient = new ChatCompletion({
114 | apiKey: this.apiKey,
115 | baseUrl: this.baseUrl,
116 | debugLogger: this.debug,
117 | });
118 |
119 | this.completionClient.on("data", this.onData);
120 | this.completionClient.on("error", this.onError);
121 | this.completionClient.on("end", this.onEnd);
122 |
123 | await this.completionClient.fetchChatCompletion(params);
124 | }
125 |
126 | private onData = (data: OpenAIChatCompletionChunk) => {
127 | // In case we are in a function call but the next message is not a function call, flush it.
128 | if (this.mode === "function" && !data.choices[0].delta.function_call) {
129 | const success = this.tryFlushFunctionCall();
130 | if (!success) {
131 | return;
132 | }
133 | }
134 |
135 | this.mode = data.choices[0].delta.function_call ? "function" : "message";
136 |
137 | if (this.mode === "message") {
138 | // if we get a message, emit the content and return;
139 |
140 | if (data.choices[0].delta.content) {
141 | this.emit("content", data.choices[0].delta.content);
142 | }
143 |
144 | return;
145 | } else if (this.mode === "function") {
146 | // if we get a function call, we buffer the name and arguments, then emit a partial event.
147 |
148 | if (data.choices[0].delta.function_call!.name) {
149 | this.functionCallName = data.choices[0].delta.function_call!.name!;
150 | }
151 | if (data.choices[0].delta.function_call!.arguments) {
152 | this.functionCallArguments +=
153 | data.choices[0].delta.function_call!.arguments!;
154 | }
155 | this.emit("partial", this.functionCallName, this.functionCallArguments);
156 |
157 | return;
158 | }
159 | };
160 |
161 | private onError = (error: any) => {
162 | this.emit("error", error);
163 | this.cleanup();
164 | };
165 |
166 | private onEnd = () => {
167 | if (this.mode === "function") {
168 | const success = this.tryFlushFunctionCall();
169 | if (!success) {
170 | return;
171 | }
172 | }
173 | this.emit("end");
174 | this.cleanup();
175 | };
176 |
177 | private tryFlushFunctionCall(): boolean {
178 | let args: any = null;
179 | try {
180 | args = JSON.parse(fixJson(this.functionCallArguments));
181 | } catch (error) {
182 | this.emit("error", error);
183 | this.cleanup();
184 | return false;
185 | }
186 | this.emit("function", {
187 | name: this.functionCallName,
188 | arguments: args,
189 | });
190 | this.mode = null;
191 | this.functionCallName = "";
192 | this.functionCallArguments = "";
193 | return true;
194 | }
195 |
196 | private cleanup() {
197 | if (this.completionClient) {
198 | this.completionClient.off("data", this.onData);
199 | this.completionClient.off("error", this.onError);
200 | this.completionClient.off("end", this.onEnd);
201 | }
202 | this.completionClient = null;
203 | this.mode = null;
204 | this.functionCallName = "";
205 | this.functionCallArguments = "";
206 | }
207 |
208 | public countTokens(message: OpenAIMessage): number {
209 | if (message.content) {
210 | return estimateTokens(message.content);
211 | } else if (message.function_call) {
212 | return estimateTokens(JSON.stringify(message.function_call));
213 | }
214 | return 0;
215 | }
216 | }
217 |
218 | const maxTokensByModel: { [key in OpenAIModel]: number } = {
219 | "gpt-3.5-turbo": 4097,
220 | "gpt-3.5-turbo-16k": 16385,
221 | "gpt-4": 8192,
222 | "gpt-4-32k": 32768,
223 | "gpt-3.5-turbo-0301": 4097,
224 | "gpt-4-0314": 8192,
225 | "gpt-4-32k-0314": 32768,
226 | "gpt-3.5-turbo-0613": 4097,
227 | "gpt-4-0613": 8192,
228 | "gpt-4-32k-0613": 32768,
229 | "gpt-3.5-turbo-16k-0613": 16385,
230 | };
231 |
232 | function estimateTokens(text: string): number {
233 | return text.length / 3;
234 | }
235 |
236 | function maxTokensForModel(model: OpenAIModel): number {
237 | return maxTokensByModel[model];
238 | }
239 |
240 | function fixJson(json: string): string {
241 | if (json === "") {
242 | json = "{}";
243 | }
244 | json = json.trim();
245 | if (!json.startsWith("{")) {
246 | json = "{" + json;
247 | }
248 | if (!json.endsWith("}")) {
249 | json = json + "}";
250 | }
251 | return jsonrepair(json);
252 | }
253 |
254 | function countFunctionsTokens(functions: OpenAIFunction[]): number {
255 | if (functions.length === 0) {
256 | return 0;
257 | }
258 | const json = JSON.stringify(functions);
259 | return estimateTokens(json);
260 | }
261 |
--------------------------------------------------------------------------------
/packages/openai/src/types.ts:
--------------------------------------------------------------------------------
1 | export type OpenAIRole = "system" | "user" | "assistant" | "function";
2 |
3 | export type OpenAIModel =
4 | | "gpt-3.5-turbo"
5 | | "gpt-3.5-turbo-16k"
6 | | "gpt-4"
7 | | "gpt-4-32k"
8 | | "gpt-3.5-turbo-0301"
9 | | "gpt-4-0314"
10 | | "gpt-4-32k-0314"
11 | | "gpt-3.5-turbo-0613"
12 | | "gpt-4-0613"
13 | | "gpt-4-32k-0613"
14 | | "gpt-3.5-turbo-16k-0613";
15 |
16 | export class CustomModel {
17 | constructor(public name: string) {}
18 | }
19 |
20 | export interface OpenAIChatCompletionChunk {
21 | choices: {
22 | delta: {
23 | role: OpenAIRole;
24 | content?: string | null;
25 | function_call?: {
26 | name?: string;
27 | arguments?: string;
28 | };
29 | };
30 | }[];
31 | }
32 |
33 | export interface OpenAIFunction {
34 | name: string;
35 | parameters: Record;
36 | description?: string;
37 | }
38 |
39 | export interface OpenAIMessage {
40 | content?: string;
41 | role: OpenAIRole;
42 | numTokens?: number;
43 | name?: string;
44 | function_call?: any;
45 | }
46 |
47 | type DebugEvent = "chat-internal" | "chat-api" | "beak-complete";
48 |
49 | export interface DebugLogger {
50 | log(debugEvent: DebugEvent, ...args: any[]): void;
51 | table(debugEvent: DebugEvent, message: string, ...args: any[]): void;
52 | warn(debugEvent: DebugEvent, ...args: any[]): void;
53 | error(debugEvent: DebugEvent, ...args: any[]): void;
54 | }
55 |
56 | export const NoopDebugLogger: DebugLogger = {
57 | log() {},
58 | table() {},
59 | warn() {},
60 | error() {},
61 | };
62 |
63 | export const DEFAULT_MODEL = "gpt-4";
64 |
--------------------------------------------------------------------------------
/packages/openai/test/chat.test.ts:
--------------------------------------------------------------------------------
1 | import { mockContentResponses, mockFunctionResponse } from "./utils";
2 | import { ChatCompletion } from "../src/chat";
3 | import { OpenAIFunction, OpenAIMessage } from "../src/types";
4 |
5 | global.fetch = jest.fn();
6 |
7 | describe("ChatCompletion", () => {
8 | it("should stream chat completion events correctly", async () => {
9 | const model = "gpt-3.5-turbo";
10 |
11 | const mockStream = new ReadableStream({
12 | start(controller) {
13 | for (let data of mockContentResponses("Hello world!", model)) {
14 | controller.enqueue(new TextEncoder().encode(data));
15 | }
16 | controller.close();
17 | },
18 | });
19 | (fetch as jest.Mock).mockResolvedValue({
20 | ok: true,
21 | status: 200,
22 | statusText: "OK",
23 | body: mockStream,
24 | });
25 |
26 | const apiKey = "sk-xyz";
27 | const chatCompletion = new ChatCompletion({ apiKey });
28 |
29 | const onDataMock = jest.fn();
30 | const onErrorMock = jest.fn();
31 | const onEndMock = jest.fn();
32 | chatCompletion.on("data", onDataMock);
33 | chatCompletion.on("error", onErrorMock);
34 | chatCompletion.on("end", onEndMock);
35 |
36 | const messages: OpenAIMessage[] = [
37 | {
38 | role: "user",
39 | content: "Hello!",
40 | },
41 | ];
42 | await chatCompletion.fetchChatCompletion({
43 | model,
44 | messages,
45 | });
46 |
47 | expect(onDataMock).toHaveBeenCalledTimes(5);
48 | expect(onErrorMock).not.toHaveBeenCalled();
49 | expect(onEndMock).toHaveBeenCalledTimes(1);
50 |
51 | const extractedData = onDataMock.mock.calls.map((call) => {
52 | const arg = call[0];
53 | const choice = arg.choices[0];
54 | return {
55 | delta: choice.delta,
56 | finish_reason: choice.finish_reason,
57 | };
58 | });
59 |
60 | expect(extractedData[0].delta).toEqual({ content: "" });
61 | expect(extractedData[0].finish_reason).toBe(null);
62 | expect(extractedData[1].delta).toEqual({ content: "Hell" });
63 | expect(extractedData[1].finish_reason).toBe(null);
64 | expect(extractedData[2].delta).toEqual({ content: "o wo" });
65 | expect(extractedData[2].finish_reason).toBe(null);
66 | expect(extractedData[3].delta).toEqual({ content: "rld!" });
67 | expect(extractedData[3].finish_reason).toBe(null);
68 | expect(extractedData[4].delta).toEqual({});
69 | expect(extractedData[4].finish_reason).toBe("stop");
70 | });
71 |
72 | it("should stream chat completion function events correctly", async () => {
73 | const model = "gpt-3.5-turbo";
74 |
75 | const mockStream = new ReadableStream({
76 | start(controller) {
77 | for (let data of mockFunctionResponse(
78 | "sayHi",
79 | { name: "Markus" },
80 | model
81 | )) {
82 | controller.enqueue(new TextEncoder().encode(data));
83 | }
84 | controller.close();
85 | },
86 | });
87 |
88 | (fetch as any).mockResolvedValue({
89 | ok: true,
90 | status: 200,
91 | statusText: "OK",
92 | body: mockStream,
93 | });
94 |
95 | const apiKey = "sk-xyz";
96 | const chatCompletion = new ChatCompletion({ apiKey });
97 |
98 | const onDataMock = jest.fn();
99 | const onErrorMock = jest.fn();
100 | const onEndMock = jest.fn();
101 | chatCompletion.on("data", onDataMock);
102 | chatCompletion.on("error", onErrorMock);
103 | chatCompletion.on("end", onEndMock);
104 |
105 | const messages: OpenAIMessage[] = [
106 | {
107 | role: "user",
108 | content: "Hello!",
109 | },
110 | ];
111 | const functions: OpenAIFunction[] = [
112 | {
113 | name: "sayHi",
114 | parameters: {
115 | type: "object",
116 | properties: {
117 | name: {
118 | type: "string",
119 | },
120 | },
121 | },
122 | description: "Say hi to someone.",
123 | },
124 | ];
125 | await chatCompletion.fetchChatCompletion({
126 | model,
127 | messages,
128 | functions,
129 | });
130 |
131 | expect(onDataMock).toHaveBeenCalledTimes(7);
132 | expect(onErrorMock).not.toHaveBeenCalled();
133 | expect(onEndMock).toHaveBeenCalledTimes(1);
134 |
135 | const extractedData = onDataMock.mock.calls.map((call) => {
136 | const arg = call[0];
137 | const choice = arg.choices[0];
138 | return {
139 | delta: choice.delta,
140 | finish_reason: choice.finish_reason,
141 | };
142 | });
143 |
144 | expect(extractedData[0].delta).toEqual({
145 | role: "assistant",
146 | content: null,
147 | function_call: {
148 | name: "sayHi",
149 | arguments: "",
150 | },
151 | });
152 | expect(extractedData[0].finish_reason).toBe(null);
153 | expect(extractedData[1].delta).toEqual({
154 | function_call: {
155 | arguments: '{"na',
156 | },
157 | });
158 | expect(extractedData[1].finish_reason).toBe(null);
159 | expect(extractedData[2].delta).toEqual({
160 | function_call: {
161 | arguments: 'me":',
162 | },
163 | });
164 | expect(extractedData[2].finish_reason).toBe(null);
165 | expect(extractedData[3].delta).toEqual({
166 | function_call: {
167 | arguments: '"Mar',
168 | },
169 | });
170 | expect(extractedData[3].finish_reason).toBe(null);
171 | expect(extractedData[4].delta).toEqual({
172 | function_call: {
173 | arguments: 'kus"',
174 | },
175 | });
176 | expect(extractedData[4].finish_reason).toBe(null);
177 | expect(extractedData[5].delta).toEqual({
178 | function_call: {
179 | arguments: "}",
180 | },
181 | });
182 | expect(extractedData[5].finish_reason).toBe(null);
183 | expect(extractedData[6].delta).toEqual({});
184 | expect(extractedData[6].finish_reason).toBe("function_call");
185 | });
186 |
187 | it("should handle HTTP error events correctly", async () => {
188 | (fetch as any).mockRejectedValueOnce({
189 | response: {
190 | ok: false,
191 | status: 500,
192 | statusText: "Internal Server Error",
193 | },
194 | });
195 |
196 | const apiKey = "sk-xyz";
197 | const chatCompletion = new ChatCompletion({ apiKey });
198 |
199 | const onDataMock = jest.fn();
200 | const onErrorMock = jest.fn();
201 | const onEndMock = jest.fn();
202 | chatCompletion.on("data", onDataMock);
203 | chatCompletion.on("error", onErrorMock);
204 | chatCompletion.on("end", onEndMock);
205 |
206 | const model = "gpt-3.5-turbo";
207 | const messages: OpenAIMessage[] = [
208 | {
209 | role: "user",
210 | content: "Hello!",
211 | },
212 | ];
213 |
214 | await chatCompletion.fetchChatCompletion({
215 | model,
216 | messages,
217 | });
218 |
219 | expect(onDataMock).not.toHaveBeenCalled();
220 | expect(onErrorMock).toHaveBeenCalledTimes(1);
221 | expect(onEndMock).not.toHaveBeenCalled();
222 | });
223 | });
224 |
--------------------------------------------------------------------------------
/packages/openai/test/openai.test.ts:
--------------------------------------------------------------------------------
1 | import { mockContentResponses, mockFunctionResponse } from "./utils";
2 | import { OpenAI, FetchChatCompletionParams } from "../src";
3 | import { OpenAIMessage } from "../src/types";
4 |
5 | global.fetch = jest.fn();
6 |
7 | describe("OpenAI", () => {
8 | it("should handle stream content events correctly", async () => {
9 | const model = "gpt-3.5-turbo";
10 |
11 | const mockStream = new ReadableStream({
12 | start(controller) {
13 | for (let data of mockContentResponses("Hello world!", model)) {
14 | controller.enqueue(new TextEncoder().encode(data));
15 | }
16 | controller.close();
17 | },
18 | });
19 |
20 | (fetch as jest.Mock).mockResolvedValue({
21 | ok: true,
22 | status: 200,
23 | statusText: "OK",
24 | body: mockStream,
25 | });
26 |
27 | const apiKey = "sk-xyz";
28 | const openai = new OpenAI({ apiKey, model });
29 |
30 | const onContentMock = jest.fn();
31 | const onFunctionMock = jest.fn();
32 | const onErrorMock = jest.fn();
33 | const onEndMock = jest.fn();
34 |
35 | openai.on("content", onContentMock);
36 | openai.on("function", onFunctionMock);
37 | openai.on("error", onErrorMock);
38 | openai.on("end", onEndMock);
39 |
40 | await openai.queryChatCompletion({
41 | messages: [{ role: "user", content: "Hello!" }],
42 | });
43 |
44 | expect(onContentMock).toHaveBeenCalledTimes(3);
45 | expect(onFunctionMock).not.toHaveBeenCalled();
46 | expect(onErrorMock).not.toHaveBeenCalled();
47 | expect(onEndMock).toHaveBeenCalledTimes(1);
48 |
49 | const extractedData = onContentMock.mock.calls.map((call) => {
50 | return call[0];
51 | });
52 |
53 | expect(extractedData[0]).toEqual("Hell");
54 | expect(extractedData[1]).toEqual("o wo");
55 | expect(extractedData[2]).toEqual("rld!");
56 | });
57 |
58 | it("should handle function events correctly", async () => {
59 | const model = "gpt-3.5-turbo";
60 |
61 | // Set up the ReadableStream for the fetch mock
62 | const mockStream = new ReadableStream({
63 | start(controller) {
64 | for (let data of mockFunctionResponse(
65 | "sayHello",
66 | { name: "world" },
67 | model
68 | )) {
69 | controller.enqueue(new TextEncoder().encode(data));
70 | }
71 | controller.close();
72 | },
73 | });
74 |
75 | // Mock the fetch function
76 | (fetch as jest.Mock).mockResolvedValue({
77 | ok: true,
78 | status: 200,
79 | statusText: "OK",
80 | body: mockStream,
81 | });
82 |
83 | const apiKey = "sk-xyz";
84 | const openai = new OpenAI({ apiKey, model });
85 |
86 | const onContentMock = jest.fn();
87 | const onFunctionMock = jest.fn();
88 | const onErrorMock = jest.fn();
89 | const onEndMock = jest.fn();
90 |
91 | openai.on("content", onContentMock);
92 | openai.on("function", onFunctionMock);
93 | openai.on("error", onErrorMock);
94 | openai.on("end", onEndMock);
95 |
96 | await openai.queryChatCompletion({
97 | messages: [{ role: "user", content: "Say hello to the world!" }],
98 | functions: [
99 | {
100 | name: "sayHello",
101 | description: "Say hello to someone",
102 | parameters: {
103 | name: {
104 | type: "string",
105 | description: "The name of the person to say hello to",
106 | },
107 | },
108 | },
109 | ],
110 | });
111 |
112 | expect(onContentMock).not.toHaveBeenCalled();
113 | expect(onFunctionMock).toHaveBeenCalledTimes(1);
114 | expect(onErrorMock).not.toHaveBeenCalled();
115 | expect(onEndMock).toHaveBeenCalledTimes(1);
116 |
117 | const extractedData = onFunctionMock.mock.calls[0][0];
118 |
119 | expect(extractedData).toEqual({
120 | name: "sayHello",
121 | arguments: {
122 | name: "world",
123 | },
124 | });
125 | });
126 |
127 | it("should handle partial function events correctly", async () => {
128 | const model = "gpt-3.5-turbo";
129 |
130 | // Set up the ReadableStream for the fetch mock
131 | const mockStream = new ReadableStream({
132 | start(controller) {
133 | for (let data of mockFunctionResponse(
134 | "sayHello",
135 | { name: "world" },
136 | model
137 | )) {
138 | controller.enqueue(new TextEncoder().encode(data));
139 | }
140 | controller.close();
141 | },
142 | });
143 |
144 | // Mock the fetch function
145 | (fetch as jest.Mock).mockResolvedValue({
146 | ok: true,
147 | status: 200,
148 | statusText: "OK",
149 | body: mockStream,
150 | });
151 |
152 | const apiKey = "sk-xyz";
153 |
154 | const openai = new OpenAI({ apiKey, model });
155 |
156 | const onContentMock = jest.fn();
157 | const onFunctionMock = jest.fn();
158 | const onPartialMock = jest.fn();
159 | const onErrorMock = jest.fn();
160 | const onEndMock = jest.fn();
161 |
162 | openai.on("content", onContentMock);
163 | openai.on("function", onFunctionMock);
164 | openai.on("partial", onPartialMock);
165 | openai.on("error", onErrorMock);
166 | openai.on("end", onEndMock);
167 |
168 | const content = "Hello!";
169 | await openai.queryChatCompletion({
170 | messages: [{ role: "user", content }],
171 | functions: [
172 | {
173 | name: "sayHello",
174 | description: "Say hello to someone",
175 | parameters: {
176 | name: {
177 | type: "string",
178 | description: "The name of the person to say hello to",
179 | },
180 | },
181 | },
182 | ],
183 | });
184 |
185 | expect(onContentMock).not.toHaveBeenCalled();
186 | expect(onFunctionMock).toHaveBeenCalledTimes(1);
187 | expect(onErrorMock).not.toHaveBeenCalled();
188 | expect(onEndMock).toHaveBeenCalledTimes(1);
189 |
190 | const extractedData = onFunctionMock.mock.calls[0][0];
191 |
192 | expect(extractedData).toEqual({
193 | name: "sayHello",
194 | arguments: {
195 | name: "world",
196 | },
197 | });
198 |
199 | const extractedPartialData = onPartialMock.mock.calls.map((call) => {
200 | return {
201 | name: call[0],
202 | args: call[1],
203 | };
204 | });
205 |
206 | expect(extractedPartialData[0]).toEqual({ name: "sayHello", args: "" });
207 | expect(extractedPartialData[1]).toEqual({ name: "sayHello", args: '{"na' });
208 | expect(extractedPartialData[2]).toEqual({
209 | name: "sayHello",
210 | args: '{"name":',
211 | });
212 | expect(extractedPartialData[3]).toEqual({
213 | name: "sayHello",
214 | args: '{"name":"wor',
215 | });
216 | expect(extractedPartialData[4]).toEqual({
217 | name: "sayHello",
218 | args: '{"name":"world"}',
219 | });
220 | });
221 |
222 | it("should handle HTTP error events correctly", async () => {
223 | (fetch as any).mockRejectedValueOnce({
224 | response: {
225 | ok: false,
226 | status: 500,
227 | statusText: "Internal Server Error",
228 | },
229 | });
230 |
231 | const apiKey = "sk-xyz";
232 | const model = "gpt-3.5-turbo";
233 |
234 | const openai = new OpenAI({ apiKey, model });
235 |
236 | const onContentMock = jest.fn();
237 | const onFunctionMock = jest.fn();
238 | const onErrorMock = jest.fn();
239 | const onEndMock = jest.fn();
240 |
241 | openai.on("content", onContentMock);
242 | openai.on("function", onFunctionMock);
243 | openai.on("error", onErrorMock);
244 | openai.on("end", onEndMock);
245 |
246 | await openai.queryChatCompletion({
247 | messages: [{ role: "user", content: "Hello!" }],
248 | });
249 |
250 | expect(onContentMock).not.toHaveBeenCalled();
251 | expect(onFunctionMock).not.toHaveBeenCalled();
252 | expect(onErrorMock).toHaveBeenCalledTimes(1);
253 | expect(onEndMock).not.toHaveBeenCalled();
254 | });
255 |
256 | it("does not remove messages when enough tokens are available", async () => {
257 | let openai = new OpenAI({ apiKey: "sk-xyz", model: "gpt-3.5-turbo" });
258 | const messages: OpenAIMessage[] = [
259 | { role: "user", content: "Hello world!" },
260 | ];
261 | let params: FetchChatCompletionParams = {
262 | messages: messages,
263 | maxTokens: 5,
264 | functions: [],
265 | };
266 | let prompt = (openai as any).buildPrompt(params);
267 | expect(prompt).toEqual(messages);
268 | });
269 |
270 | it("does not remove messages when enough tokens are available", async () => {
271 | let openai = new OpenAI({ apiKey: "sk-xyz", model: "gpt-3.5-turbo" });
272 | const messages: OpenAIMessage[] = [
273 | { role: "user", content: "Hello world!" },
274 | { role: "user", content: "Hallo welt!" },
275 | ];
276 | let params: FetchChatCompletionParams = {
277 | messages: messages,
278 | maxTokens: 10,
279 | functions: [],
280 | };
281 | let prompt = (openai as any).buildPrompt(params);
282 |
283 | expect(prompt).toEqual(messages);
284 | });
285 |
286 | it("does remove messages when too few tokens are available", async () => {
287 | let openai = new OpenAI({ apiKey: "sk-xyz", model: "gpt-3.5-turbo" });
288 | const messages: OpenAIMessage[] = [
289 | { role: "user", content: "Hello world!" },
290 | { role: "user", content: "Hallo welt!" },
291 | ];
292 | let params: FetchChatCompletionParams = {
293 | messages: messages,
294 | maxTokens: 5,
295 | functions: [],
296 | };
297 | let prompt = (openai as any).buildPrompt(params);
298 | expect(prompt).toEqual([
299 | {
300 | content: "Hallo welt!",
301 | role: "user",
302 | },
303 | ]);
304 | });
305 |
306 | it("does not remove system messages", async () => {
307 | let openai = new OpenAI({ apiKey: "sk-xyz", model: "gpt-3.5-turbo" });
308 | const messages: OpenAIMessage[] = [
309 | { role: "system", content: "Hola mundo!" },
310 | { role: "user", content: "Hello world!" },
311 | { role: "user", content: "Hallo welt!" },
312 | ];
313 | let params: FetchChatCompletionParams = {
314 | messages: messages,
315 | maxTokens: 10,
316 | functions: [],
317 | };
318 | let prompt = (openai as any).buildPrompt(params);
319 | expect(prompt).toEqual([
320 | {
321 | content: "Hola mundo!",
322 | role: "system",
323 | },
324 | {
325 | content: "Hallo welt!",
326 | role: "user",
327 | },
328 | ]);
329 | });
330 | });
331 |
--------------------------------------------------------------------------------
/packages/openai/test/utils.ts:
--------------------------------------------------------------------------------
1 | export function mockContentResponses(
2 | sentence: string,
3 | model: string
4 | ): string[] {
5 | const responses: any[] = [];
6 | const chunks: string[] = [];
7 | let start = 0;
8 | const chunkSize = 4;
9 |
10 | while (start < sentence.length) {
11 | chunks.push(sentence.slice(start, start + chunkSize));
12 | start += chunkSize;
13 | }
14 |
15 | const id = `chatcmpl-aaaaaaaaaaaaaaaaaaaaaa`;
16 | const timestamp = Math.floor(Date.now() / 1000);
17 |
18 | responses.push(createJsonString(id, timestamp, model, ""));
19 |
20 | for (const chunk of chunks) {
21 | responses.push(createJsonString(id, timestamp, model, chunk));
22 | }
23 |
24 | responses.push(createJsonString(id, timestamp, model, null, "stop"));
25 | responses.push("data: [DONE]\n");
26 |
27 | return responses;
28 | }
29 |
30 | function createJsonString(
31 | id: string,
32 | timestamp: number,
33 | model: string,
34 | content: string | null,
35 | finishReason: string | null = null
36 | ): string {
37 | const jsonString = {
38 | id: id,
39 | object: "chat.completion.chunk",
40 | created: timestamp,
41 | model: model,
42 | choices: [
43 | {
44 | index: 0,
45 | delta: content !== null ? { content: content } : {},
46 | finish_reason: finishReason,
47 | },
48 | ],
49 | };
50 |
51 | return `data: ${JSON.stringify(jsonString)}\n`;
52 | }
53 |
54 | export function mockFunctionResponse(
55 | functionName: string,
56 | functionArgs: any,
57 | model: string
58 | ): string[] {
59 | const serializedArgs = JSON.stringify(functionArgs);
60 | const chunks: any[] = [];
61 | let start = 0;
62 | const chunkSize = 4;
63 |
64 | while (start < serializedArgs.length) {
65 | chunks.push(serializedArgs.slice(start, start + chunkSize));
66 | start += chunkSize;
67 | }
68 |
69 | const id = "chatcmpl-aaaaaaaaaaaaaaaaaaaaaa";
70 | const timestamp = Math.floor(Date.now() / 1000);
71 |
72 | let responses: string[] = [];
73 |
74 | responses.push(
75 | createFunctionJsonString(id, timestamp, model, functionName, null)
76 | );
77 |
78 | for (const chunk of chunks) {
79 | responses.push(createFunctionJsonString(id, timestamp, model, null, chunk));
80 | }
81 |
82 | responses.push(
83 | createFunctionJsonString(id, timestamp, model, null, null, "function_call")
84 | );
85 |
86 | responses.push("data: [DONE]\n");
87 |
88 | return responses;
89 | }
90 |
91 | function createFunctionJsonString(
92 | id: string,
93 | timestamp: number,
94 | model: string,
95 | name: string | null,
96 | content: string | null,
97 | finishReason: string | null = null
98 | ): string {
99 | let delta = {};
100 | if (content !== null) {
101 | delta = {
102 | function_call: {
103 | arguments: content,
104 | },
105 | };
106 | } else if (name !== null) {
107 | delta = {
108 | role: "assistant",
109 | content: null,
110 | function_call: {
111 | name: name,
112 | arguments: "",
113 | },
114 | };
115 | }
116 | const jsonString = {
117 | id: id,
118 | object: "chat.completion.chunk",
119 | created: timestamp,
120 | model: model,
121 | choices: [
122 | {
123 | index: 0,
124 | delta: delta,
125 | finish_reason: finishReason,
126 | },
127 | ],
128 | };
129 |
130 | return `data: ${JSON.stringify(jsonString)}\n`;
131 | }
132 |
--------------------------------------------------------------------------------
/packages/openai/tsconfig.cjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES6",
4 | "module": "CommonJS",
5 | "declaration": true,
6 | "strict": true,
7 | "esModuleInterop": true,
8 | "skipLibCheck": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "outDir": "dist/cjs",
11 | "lib": ["DOM"],
12 | "allowJs": true,
13 | "allowSyntheticDefaultImports": true,
14 | "moduleResolution": "node",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "noEmit": false
18 | },
19 | "include": ["src"]
20 | }
21 |
--------------------------------------------------------------------------------
/packages/openai/tsconfig.esm.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES6",
4 | "module": "ESNext",
5 | "declaration": true,
6 | "strict": true,
7 | "esModuleInterop": true,
8 | "skipLibCheck": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "outDir": "dist/esm",
11 | "lib": ["DOM"],
12 | "allowJs": true,
13 | "allowSyntheticDefaultImports": true,
14 | "moduleResolution": "node",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "noEmit": false
18 | },
19 | "include": ["src"]
20 | }
21 |
--------------------------------------------------------------------------------
/packages/react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@beakjs/react",
3 | "version": "0.0.6",
4 | "description": "React library for Beak.js",
5 | "main": "dist/beakjs.umd.js",
6 | "module": "dist/beakjs.es.mjs",
7 | "types": "dist/index.d.ts",
8 | "exports": {
9 | "import": "./dist/beakjs.es.mjs",
10 | "require": "./dist/beakjs.umd.js",
11 | "types": "./dist/index.d.ts"
12 | },
13 | "scripts": {
14 | "build": "vite build",
15 | "prepublishOnly": "cp ../../README.md .",
16 | "clean": "rm -rf dist && rm -rf node_modules"
17 | },
18 | "files": [
19 | "dist/**/*",
20 | "README.md"
21 | ],
22 | "dependencies": {
23 | "@beakjs/core": "0.0.6"
24 | },
25 | "devDependencies": {
26 | "@types/react": "^18.2.15",
27 | "@types/react-dom": "^18.2.7",
28 | "react": "^18.2.0",
29 | "react-dom": "^18.2.0",
30 | "rollup-plugin-visualizer": "^5.9.2",
31 | "typescript": "^5.0.4",
32 | "vite": "^4.3.5",
33 | "vite-plugin-css-injected-by-js": "^3.3.0",
34 | "vite-plugin-dts": "^3.6.3",
35 | "vite-tsconfig-paths": "^4.2.0"
36 | },
37 | "peerDependencies": {
38 | "react": "^18.2.0",
39 | "react-dom": "^18.2.0"
40 | },
41 | "repository": {
42 | "type": "git",
43 | "url": "https://github.com/mme/beakjs.git"
44 | },
45 | "author": "Markus Ecker",
46 | "license": "MIT"
47 | }
48 |
--------------------------------------------------------------------------------
/packages/react/src/Assistant.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useMemo } from "react";
2 | import {
3 | BeakColorScheme,
4 | BeakContext,
5 | BeakIcons,
6 | useBeakContext,
7 | } from "./Beak";
8 | import { Message } from "@beakjs/core";
9 | import {
10 | ButtonProps,
11 | HeaderProps,
12 | WindowProps,
13 | MessagesProps,
14 | InputProps,
15 | } from "./props";
16 | import { Window as DefaultWindow } from "./Window";
17 | import { Button as DefaultButton } from "./Button";
18 | import { Header as DefaultHeader } from "./Header";
19 | import { Messages as DefaultMessages } from "./Messages";
20 | import { Input as DefaultInput } from "./Input";
21 | import "../../css/Assistant.css";
22 |
23 | interface AssistantWindowProps {
24 | defaultOpen?: boolean;
25 | clickOutsideToClose?: boolean;
26 | hitEscapeToClose?: boolean;
27 | hotkey?: string;
28 | icons?: BeakIcons;
29 | colorScheme?: BeakColorScheme;
30 | Window?: React.ComponentType;
31 | Button?: React.ComponentType;
32 | Header?: React.ComponentType;
33 | Messages?: React.ComponentType;
34 | Input?: React.ComponentType;
35 | }
36 |
37 | export const AssistantWindow: React.FC = ({
38 | defaultOpen = false,
39 | clickOutsideToClose = true,
40 | hitEscapeToClose = true,
41 | hotkey = "K",
42 | icons,
43 | colorScheme,
44 | Window = DefaultWindow,
45 | Button = DefaultButton,
46 | Header = DefaultHeader,
47 | Messages = DefaultMessages,
48 | Input = DefaultInput,
49 | }) => {
50 | const context = useBeakContext();
51 | const beak = context.beak;
52 |
53 | const [open, setOpen] = React.useState(defaultOpen);
54 | const [messages, setMessages] = React.useState(
55 | initialMessages(context.labels.initial)
56 | );
57 |
58 | const inProgress = messages.some((message) => message.status === "pending");
59 |
60 | const onChange = useCallback(() => {
61 | setMessages([...initialMessages(context.labels.initial), ...beak.messages]);
62 | }, [beak]);
63 |
64 | useEffect(() => {
65 | beak.on("change", onChange);
66 | return () => {
67 | beak.off("change", onChange);
68 | };
69 | }, [onChange]);
70 |
71 | const sendMessage = async (message: string) => {
72 | await beak.runChatCompletion(message);
73 | };
74 |
75 | const ctx = useMemo(() => {
76 | return {
77 | ...context,
78 | icons: {
79 | ...context.icons,
80 | ...icons,
81 | },
82 | colorScheme: colorScheme || context.colorScheme,
83 | };
84 | }, [context, icons, colorScheme]);
85 |
86 | const colorSchemeClass =
87 | "beakColorScheme" +
88 | ctx.colorScheme[0].toUpperCase() +
89 | ctx.colorScheme.slice(1);
90 |
91 | return (
92 |
93 |
94 |
95 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | );
109 | };
110 |
111 | function initialMessages(initial?: string | string[]): Message[] {
112 | let initialArray: string[] = [];
113 | if (initial) {
114 | if (Array.isArray(initial)) {
115 | initialArray.push(...initial);
116 | } else {
117 | initialArray.push(initial);
118 | }
119 | }
120 |
121 | return initialArray.map(
122 | (message) =>
123 | new Message({
124 | role: "assistant",
125 | content: message,
126 | status: "success",
127 | })
128 | );
129 | }
130 |
--------------------------------------------------------------------------------
/packages/react/src/Beak.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from "react";
2 | import { BeakCore, OpenAIModel, CustomModel, DebugLogger } from "@beakjs/core";
3 | import * as DefaultIcons from "./Icons";
4 |
5 | const DEFAULT_DEBUG_LOGGER = new DebugLogger([]);
6 |
7 | export type BeakColorScheme = "auto" | "light" | "dark";
8 |
9 | export interface BeakIcons {
10 | openIcon?: React.ReactNode;
11 | closeIcon?: React.ReactNode;
12 | headerCloseIcon?: React.ReactNode;
13 | sendIcon?: React.ReactNode;
14 | activityIcon?: React.ReactNode;
15 | spinnerIcon?: React.ReactNode;
16 | }
17 |
18 | interface BeakLabels {
19 | initial?: string | string[];
20 | title?: string;
21 | placeholder?: string;
22 | thinking?: string;
23 | done?: string;
24 | error?: string;
25 | }
26 |
27 | interface BeakContext {
28 | beak: BeakCore;
29 | labels: Required;
30 | icons: Required;
31 | colorScheme: BeakColorScheme;
32 | debugLogger: DebugLogger;
33 | }
34 |
35 | export const BeakContext = React.createContext(
36 | undefined
37 | );
38 |
39 | export function useBeakContext(): BeakContext {
40 | const context = React.useContext(BeakContext);
41 | if (context === undefined) {
42 | throw new Error(
43 | "Context not found. Did you forget to wrap your app in a component?"
44 | );
45 | }
46 | return context;
47 | }
48 |
49 | interface BeakProps {
50 | __unsafeOpenAIApiKey__?: string;
51 | baseUrl?: string;
52 | openAIModel?: OpenAIModel | CustomModel;
53 | temperature?: number;
54 | instructions?: string;
55 | maxFeedback?: number;
56 | labels?: BeakLabels;
57 | debugLogger?: DebugLogger;
58 | children?: React.ReactNode;
59 | }
60 |
61 | export const Beak: React.FC = ({
62 | __unsafeOpenAIApiKey__,
63 | baseUrl,
64 | openAIModel,
65 | temperature,
66 | instructions,
67 | maxFeedback,
68 | labels,
69 | debugLogger,
70 | children,
71 | }) => {
72 | const beak = useMemo(
73 | () =>
74 | new BeakCore({
75 | openAIApiKey: __unsafeOpenAIApiKey__,
76 | baseUrl: baseUrl,
77 | openAIModel: openAIModel,
78 | maxFeedback: maxFeedback,
79 | instructions: instructions,
80 | temperature: temperature,
81 | debugLogger: debugLogger,
82 | }),
83 | [
84 | __unsafeOpenAIApiKey__,
85 | baseUrl,
86 | openAIModel,
87 | maxFeedback,
88 | instructions,
89 | temperature,
90 | debugLogger,
91 | ]
92 | );
93 | const context = useMemo(
94 | () => ({
95 | beak: beak,
96 | labels: {
97 | ...{
98 | initial: "",
99 | title: "Assistant",
100 | placeholder: "Type a message...",
101 | thinking: "Thinking...",
102 | done: "✅ Done",
103 | error: "❌ An error occurred. Please try again.",
104 | },
105 | ...labels,
106 | },
107 |
108 | debugLogger: debugLogger || DEFAULT_DEBUG_LOGGER,
109 | colorScheme: "auto" as BeakColorScheme,
110 | icons: {
111 | openIcon: DefaultIcons.OpenIcon,
112 | closeIcon: DefaultIcons.CloseIcon,
113 | headerCloseIcon: DefaultIcons.HeaderCloseIcon,
114 | sendIcon: DefaultIcons.SendIcon,
115 | activityIcon: DefaultIcons.ActivityIcon,
116 | spinnerIcon: DefaultIcons.SpinnerIcon,
117 | },
118 | }),
119 | [labels, debugLogger]
120 | );
121 | return (
122 | {children}
123 | );
124 | };
125 |
--------------------------------------------------------------------------------
/packages/react/src/Button.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ButtonProps } from "./props";
3 | import { useBeakContext } from "./Beak";
4 | import "../../css/Button.css";
5 |
6 | export const Button: React.FC = ({ open, setOpen }) => {
7 | const context = useBeakContext();
8 | // To ensure that the mouse handler fires even when the button is scaled down
9 | // we wrap the button in a div and attach the handler to the div
10 | return (
11 | setOpen(!open)}>
12 |
23 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/packages/react/src/Header.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { HeaderProps } from "./props";
3 | import "../../css/Header.css";
4 | import { useBeakContext } from "./Beak";
5 |
6 | export const Header: React.FC = ({ setOpen }) => {
7 | const context = useBeakContext();
8 |
9 | return (
10 |
11 |
{context.labels.title}
12 |
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/packages/react/src/Icons.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export const OpenIcon = (
4 |
19 | );
20 |
21 | export const CloseIcon = (
22 |
37 | );
38 |
39 | export const HeaderCloseIcon = (
40 |
55 | );
56 |
57 | export const SendIcon = (
58 |
73 | );
74 |
75 | export const SpinnerIcon = (
76 |
101 | );
102 |
103 | export const ActivityIcon = (
104 |
130 | );
131 |
--------------------------------------------------------------------------------
/packages/react/src/Input.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState } from "react";
2 | import { InputProps } from "./props";
3 | import { useBeakContext } from "./Beak";
4 | import "../../css/Input.css";
5 | import AutoResizingTextarea from "./Textarea";
6 |
7 | export const Input: React.FC = ({ inProgress, onSend }) => {
8 | const context = useBeakContext();
9 | const textareaRef = useRef(null);
10 |
11 | const handleDivClick = (event: React.MouseEvent) => {
12 | // Check if the clicked element is not the textarea itself
13 | if (event.target !== event.currentTarget) return;
14 |
15 | textareaRef.current?.focus();
16 | };
17 |
18 | const [text, setText] = useState("");
19 | const send = () => {
20 | if (inProgress) return;
21 | onSend(text);
22 | setText("");
23 |
24 | textareaRef.current?.focus();
25 | };
26 |
27 | const icon = inProgress ? context.icons.activityIcon : context.icons.sendIcon;
28 | const disabled = inProgress || text.length === 0;
29 |
30 | return (
31 |
32 |
35 |
setText(event.target.value)}
42 | onKeyDown={(event) => {
43 | if (event.key === "Enter" && !event.shiftKey) {
44 | event.preventDefault();
45 | send();
46 | }
47 | }}
48 | />
49 |
50 | );
51 | };
52 |
--------------------------------------------------------------------------------
/packages/react/src/Messages.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { MessagesProps } from "./props";
3 | import { useBeakContext } from "./Beak";
4 | import "../../css/Messages.css";
5 |
6 | export const Messages: React.FC = ({ messages }) => {
7 | const context = useBeakContext();
8 | const messagesEndRef = React.useRef(null);
9 |
10 | const scrollToBottom = () => {
11 | if (messagesEndRef.current) {
12 | messagesEndRef.current.scrollIntoView({
13 | behavior: "auto",
14 | });
15 | }
16 | };
17 |
18 | useEffect(() => {
19 | scrollToBottom();
20 | }, [messages]);
21 |
22 | return (
23 |
24 | {messages.map((message, index) => {
25 | if (message.role === "user") {
26 | return (
27 |
28 | {message.content}
29 |
30 | );
31 | } else if (message.role == "assistant" && message.status !== "error") {
32 | if (message.status === "pending" && !message.content) {
33 | return (
34 |
35 | {context.icons.spinnerIcon}
36 |
37 | );
38 | } else if (message.status === "partial") {
39 | return (
40 |
41 | {context.labels.thinking} {context.icons.spinnerIcon}
42 |
43 | );
44 | } else if (message.content) {
45 | return (
46 |
47 | {message.content}
48 |
49 | );
50 | }
51 | } else if (
52 | message.role === "function" &&
53 | message.status === "success"
54 | ) {
55 | return (
56 |
57 | {context.labels.done}
58 |
59 | );
60 | } else if (message.status === "error") {
61 | return (
62 |
63 | {context.labels.error}
64 |
65 | );
66 | }
67 | })}
68 |
69 |
70 | );
71 | };
72 |
--------------------------------------------------------------------------------
/packages/react/src/Textarea.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | useState,
3 | useRef,
4 | useEffect,
5 | forwardRef,
6 | useImperativeHandle,
7 | } from "react";
8 |
9 | interface AutoResizingTextareaProps {
10 | maxRows?: number;
11 | placeholder?: string;
12 | value: string;
13 | onChange: (event: React.ChangeEvent) => void;
14 | onKeyDown?: (event: React.KeyboardEvent) => void;
15 | autoFocus?: boolean;
16 | }
17 |
18 | const AutoResizingTextarea = forwardRef<
19 | HTMLTextAreaElement,
20 | AutoResizingTextareaProps
21 | >(
22 | (
23 | { maxRows = 1, placeholder, value, onChange, onKeyDown, autoFocus },
24 | ref
25 | ) => {
26 | const internalTextareaRef = useRef(null);
27 | const [maxHeight, setMaxHeight] = useState(0);
28 |
29 | useImperativeHandle(
30 | ref,
31 | () => internalTextareaRef.current as HTMLTextAreaElement
32 | );
33 |
34 | useEffect(() => {
35 | const calculateMaxHeight = () => {
36 | const textarea = internalTextareaRef.current;
37 | if (textarea) {
38 | textarea.style.height = "auto";
39 | const singleRowHeight = textarea.scrollHeight;
40 | setMaxHeight(singleRowHeight * maxRows);
41 | if (autoFocus) {
42 | textarea.focus();
43 | }
44 | }
45 | };
46 |
47 | calculateMaxHeight();
48 | }, [maxRows]);
49 |
50 | useEffect(() => {
51 | const textarea = internalTextareaRef.current;
52 | if (textarea) {
53 | textarea.style.height = "auto";
54 | textarea.style.height = `${Math.min(
55 | textarea.scrollHeight,
56 | maxHeight
57 | )}px`;
58 | }
59 | }, [value, maxHeight]);
60 |
61 | return (
62 |
75 | );
76 | }
77 | );
78 |
79 | export default AutoResizingTextarea;
80 |
--------------------------------------------------------------------------------
/packages/react/src/Window.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect } from "react";
2 | import { WindowProps } from "./props";
3 | import "../../css/Window.css";
4 |
5 | export const Window = ({
6 | open,
7 | setOpen,
8 | children,
9 | clickOutsideToClose,
10 | hotkey,
11 | hitEscapeToClose,
12 | }: WindowProps) => {
13 | const windowRef = React.useRef(null);
14 |
15 | const handleClickOutside = useCallback(
16 | (event: MouseEvent) => {
17 | if (!clickOutsideToClose) {
18 | return;
19 | }
20 |
21 | const parentElement = windowRef.current?.parentElement;
22 |
23 | if (
24 | open &&
25 | parentElement &&
26 | !parentElement.contains(event.target as any)
27 | ) {
28 | setOpen(false);
29 | }
30 | },
31 | [clickOutsideToClose, open, setOpen]
32 | );
33 |
34 | const handleKeyDown = useCallback(
35 | (event: KeyboardEvent) => {
36 | const target = event.target as HTMLElement;
37 | const isInput =
38 | target.tagName === "INPUT" ||
39 | target.tagName === "SELECT" ||
40 | target.tagName === "TEXTAREA" ||
41 | target.isContentEditable;
42 |
43 | const isDescendantOfWrapper = windowRef.current?.contains(target);
44 |
45 | if (
46 | open &&
47 | event.key === "Escape" &&
48 | (!isInput || isDescendantOfWrapper) &&
49 | hitEscapeToClose
50 | ) {
51 | setOpen(false);
52 | } else if (
53 | event.key === hotkey &&
54 | ((isMacOS() && event.metaKey) || (!isMacOS() && event.ctrlKey)) &&
55 | (!isInput || isDescendantOfWrapper)
56 | ) {
57 | setOpen(!open);
58 | }
59 | },
60 | [hitEscapeToClose, hotkey, open, setOpen]
61 | );
62 |
63 | const adjustForMobile = useCallback(() => {
64 | const beakWindow = windowRef.current;
65 | const vv = window.visualViewport;
66 | if (!beakWindow || !vv) {
67 | return;
68 | }
69 |
70 | if (window.innerWidth < 640 && open) {
71 | beakWindow.style.height = `${vv.height}px`;
72 | beakWindow.style.left = `${vv.offsetLeft}px`;
73 | beakWindow.style.top = `${vv.offsetTop}px`;
74 |
75 | document.body.style.position = "fixed";
76 | document.body.style.width = "100%";
77 | document.body.style.height = `${window.innerHeight}px`;
78 | document.body.style.overflow = "hidden";
79 | document.body.style.touchAction = "none";
80 |
81 | // Prevent scrolling on iOS
82 | document.body.addEventListener("touchmove", preventScroll, {
83 | passive: false,
84 | });
85 | } else {
86 | beakWindow.style.height = "";
87 | beakWindow.style.left = "";
88 | beakWindow.style.top = "";
89 | document.body.style.position = "";
90 | document.body.style.height = "";
91 | document.body.style.width = "";
92 | document.body.style.overflow = "";
93 | document.body.style.top = "";
94 | document.body.style.touchAction = "";
95 |
96 | document.body.removeEventListener("touchmove", preventScroll);
97 | }
98 | }, [open]);
99 |
100 | useEffect(() => {
101 | document.addEventListener("mousedown", handleClickOutside);
102 | document.addEventListener("keydown", handleKeyDown);
103 | if (window.visualViewport) {
104 | window.visualViewport.addEventListener("resize", adjustForMobile);
105 | adjustForMobile();
106 | }
107 |
108 | return () => {
109 | document.removeEventListener("mousedown", handleClickOutside);
110 | document.removeEventListener("keydown", handleKeyDown);
111 | if (window.visualViewport) {
112 | window.visualViewport.removeEventListener("resize", adjustForMobile);
113 | }
114 | };
115 | }, [adjustForMobile, handleClickOutside, handleKeyDown]);
116 |
117 | return (
118 |
119 | {open && children}
120 |
121 | );
122 | };
123 |
124 | const preventScroll = (event: TouchEvent): void => {
125 | let targetElement = event.target as Element;
126 |
127 | // Function to check if the target has the parent with a given class
128 | const hasParentWithClass = (element: Element, className: string): boolean => {
129 | while (element && element !== document.body) {
130 | if (element.classList.contains(className)) {
131 | return true;
132 | }
133 | element = element.parentElement!;
134 | }
135 | return false;
136 | };
137 |
138 | // Check if the target of the touch event is inside an element with the 'beakMessages' class
139 | if (!hasParentWithClass(targetElement, "beakMessages")) {
140 | event.preventDefault();
141 | }
142 | };
143 |
144 | function isMacOS() {
145 | return /Mac|iMac|Macintosh/i.test(navigator.userAgent);
146 | }
147 |
--------------------------------------------------------------------------------
/packages/react/src/hooks.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { FunctionDefinition } from "@beakjs/core";
3 | import { useBeakContext } from "./Beak";
4 |
5 | export function useBeakFunction(definition: FunctionDefinition) {
6 | const context = useBeakContext();
7 | const beak = context.beak;
8 | if (beak == null) {
9 | throw new Error(
10 | "Beak not found in context. Did you forget to wrap your app in a component?"
11 | );
12 | }
13 | useEffect(() => {
14 | beak.addFunction(definition);
15 |
16 | return () => {
17 | beak.removeFunction(definition);
18 | };
19 | }, [definition]);
20 | }
21 |
22 | export function useBeakInfo(data: any): void;
23 | export function useBeakInfo(description: string, data: any): void;
24 | export function useBeakInfo(descriptionOrData: any, data?: any): void {
25 | const context = useBeakContext();
26 |
27 | if (!context) {
28 | throw new Error("Context is not defined.");
29 | }
30 |
31 | const beak = context.beak;
32 |
33 | if (beak == null) {
34 | throw new Error(
35 | "Beak not found in context. Did you forget to wrap your app in a component?"
36 | );
37 | }
38 |
39 | const actualDescription = data ? descriptionOrData : "data";
40 | const actualData = data ?? descriptionOrData;
41 |
42 | useEffect(() => {
43 | const id = beak.addInfo(actualData, actualDescription);
44 |
45 | return () => {
46 | beak.removeInfo(id);
47 | };
48 | }, [beak, actualDescription, actualData]);
49 | }
50 |
--------------------------------------------------------------------------------
/packages/react/src/index.ts:
--------------------------------------------------------------------------------
1 | import "../../css/index.css";
2 | export { AssistantWindow } from "./Assistant";
3 | export { useBeakFunction, useBeakInfo } from "./hooks";
4 | export { Beak } from "./Beak";
5 |
--------------------------------------------------------------------------------
/packages/react/src/props.ts:
--------------------------------------------------------------------------------
1 | import { Message } from "@beakjs/core";
2 |
3 | export interface ButtonProps {
4 | open: boolean;
5 | setOpen: (open: boolean) => void;
6 | }
7 |
8 | export interface WindowProps {
9 | open: boolean;
10 | setOpen: (open: boolean) => void;
11 | clickOutsideToClose: boolean;
12 | hitEscapeToClose: boolean;
13 | hotkey: string;
14 | children?: React.ReactNode;
15 | }
16 |
17 | export interface HeaderProps {
18 | open: boolean;
19 | setOpen: (open: boolean) => void;
20 | }
21 |
22 | export interface MessagesProps {
23 | messages: Message[];
24 | }
25 |
26 | export interface InputProps {
27 | inProgress: boolean;
28 | onSend: (text: string) => void;
29 | }
30 |
--------------------------------------------------------------------------------
/packages/react/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "strict": true,
5 | "esModuleInterop": true,
6 | "skipLibCheck": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "outDir": "dist",
9 | "lib": ["dom", "dom.iterable", "esnext"],
10 | "allowJs": true,
11 | "allowSyntheticDefaultImports": true,
12 | "module": "ESNext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": false,
17 | "jsx": "react"
18 | },
19 | "include": ["src"]
20 | }
21 |
--------------------------------------------------------------------------------
/packages/react/vite.config.ts:
--------------------------------------------------------------------------------
1 | // vite.config.ts
2 | import { defineConfig } from "vite";
3 | import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js";
4 | import dts from "vite-plugin-dts";
5 |
6 | export default defineConfig({
7 | plugins: [cssInjectedByJsPlugin(), dts()],
8 | build: {
9 | lib: {
10 | entry: "src/index.ts",
11 | name: "BeakJS",
12 | fileName: (format) =>
13 | `beakjs.${format}.${format === "es" ? "mjs" : "js"}`,
14 | },
15 | rollupOptions: {
16 | external: ["react", "react-dom"],
17 | output: {
18 | globals: {
19 | react: "React",
20 | "react-dom": "ReactDOM",
21 | },
22 | },
23 | },
24 | },
25 | ssr: {
26 | target: "node",
27 | },
28 | });
29 |
--------------------------------------------------------------------------------
/packages/remix/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@beakjs/remix",
3 | "version": "0.0.6",
4 | "description": "Beak.js proxy for Remix",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "scripts": {
8 | "build": "tsc",
9 | "prepublishOnly": "cp ../../README.md .",
10 | "clean": "rm -rf dist && rm -rf node_modules"
11 | },
12 | "files": [
13 | "dist/**/*",
14 | "README.md"
15 | ],
16 | "dependencies": {
17 | "@beakjs/server": "0.0.6"
18 | },
19 | "peerDependencies": {
20 | "@remix-run/node": "^2.3.0"
21 | },
22 | "devDependencies": {
23 | "@remix-run/node": "^2.3.0"
24 | },
25 | "repository": {
26 | "type": "git",
27 | "url": "https://github.com/mme/beakjs.git"
28 | },
29 | "author": "Markus Ecker",
30 | "license": "MIT"
31 | }
32 |
--------------------------------------------------------------------------------
/packages/remix/src/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BeakServer,
3 | BeakServerProps,
4 | HttpAdapter,
5 | FetchChatCompletionParams,
6 | } from "@beakjs/server";
7 | import { ActionFunction, DataFunctionArgs } from "@remix-run/node";
8 |
9 | export function beakHandler(
10 | beakProps?: BeakServerProps
11 | ): ActionFunction {
12 | const beakServer = new BeakServer(beakProps);
13 |
14 | return async ({ request }: DataFunctionArgs) => {
15 | const url = new URL(request.url);
16 |
17 | if (
18 | url.pathname.endsWith("/v1/chat/completions") &&
19 | request.method === "POST"
20 | ) {
21 | // Set up a ReadableStream to stream the response
22 | const stream = new ReadableStream({
23 | async start(controller) {
24 | try {
25 | const params: FetchChatCompletionParams = await request.json();
26 | const adapter = createStreamingHttpAdapter(controller);
27 | await beakServer.handleRequest(request, params, adapter);
28 | } catch (error) {
29 | console.error(error);
30 | controller.error("Internal Server Error");
31 | }
32 | },
33 | });
34 |
35 | return new Response(stream, {
36 | headers: { "Content-Type": "text/plain" },
37 | });
38 | } else {
39 | return new Response("Not found", { status: 404 });
40 | }
41 | };
42 | }
43 |
44 | function createStreamingHttpAdapter(
45 | controller: ReadableStreamDefaultController
46 | ): HttpAdapter {
47 | return {
48 | onData(data: any) {
49 | controller.enqueue(`data: ${JSON.stringify(data)}\n`);
50 | },
51 | onEnd() {
52 | controller.close();
53 | },
54 | onError(error: any) {
55 | console.error(error);
56 | controller.error(`Error: ${error.message || "An error occurred"}`);
57 | },
58 | };
59 | }
60 |
--------------------------------------------------------------------------------
/packages/remix/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "module": "commonjs",
5 | "declaration": true,
6 | "strict": true,
7 | "esModuleInterop": true,
8 | "skipLibCheck": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "outDir": "dist",
11 | "lib": ["es2019", "dom"],
12 | "allowJs": true,
13 | "allowSyntheticDefaultImports": true,
14 | "moduleResolution": "node",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "noEmit": false,
18 | "types": ["node"]
19 | },
20 | "include": ["src"],
21 | "exclude": ["node_modules", "dist"]
22 | }
23 |
--------------------------------------------------------------------------------
/packages/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@beakjs/server",
3 | "version": "0.0.6",
4 | "description": "Beak.js OpenAI Proxy Server",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "scripts": {
8 | "build": "tsc",
9 | "prepublishOnly": "cp ../../README.md .",
10 | "clean": "rm -rf dist && rm -rf node_modules"
11 | },
12 | "files": [
13 | "dist/**/*",
14 | "README.md"
15 | ],
16 | "dependencies": {
17 | "@beakjs/openai": "0.0.6",
18 | "bottleneck": "^2.19.5",
19 | "redis": "^4.6.10"
20 | },
21 | "repository": {
22 | "type": "git",
23 | "url": "https://github.com/mme/beakjs.git"
24 | },
25 | "author": "Markus Ecker",
26 | "license": "MIT"
27 | }
28 |
--------------------------------------------------------------------------------
/packages/server/src/index.ts:
--------------------------------------------------------------------------------
1 | import Bottleneck from "bottleneck";
2 | import { ChatCompletion, FetchChatCompletionParams } from "@beakjs/openai";
3 | export type { FetchChatCompletionParams } from "@beakjs/openai";
4 |
5 | export interface BeakServerProps {
6 | openAIApiKey: string;
7 | rateLimiterOptions?: RateLimiter;
8 | rateLimiterKey?: (req: T) => string | undefined;
9 | }
10 |
11 | interface RateLimiter {
12 | requestsPerSecond?: number;
13 | maxConcurrent?: number;
14 | requestPerSecondByClient?: number;
15 | maxConcurrentByClient?: number;
16 | redis?: RedisConfiguration;
17 | }
18 |
19 | interface RedisConfiguration {
20 | host: string;
21 | port?: number;
22 | }
23 |
24 | export interface HttpAdapter {
25 | onEnd: () => void;
26 | onData: (data: any) => void;
27 | onError: (error: any) => void;
28 | }
29 |
30 | export class BeakServer {
31 | private openAIApiKey: string;
32 | private rateLimiter: Bottleneck;
33 | private rateLimiterGroup: Bottleneck.Group;
34 | private rateLimiterKey?: (req: T) => string | undefined;
35 |
36 | constructor(props?: BeakServerProps) {
37 | if (props?.openAIApiKey) {
38 | this.openAIApiKey = props.openAIApiKey;
39 | } else if (process.env.OPENAI_API_KEY) {
40 | this.openAIApiKey = process.env.OPENAI_API_KEY;
41 | } else {
42 | throw new Error(
43 | "OpenAI API key is not defined. Either set OPENAI_API_KEY environment " +
44 | "variable or pass it as a configuration parameter."
45 | );
46 | }
47 |
48 | const rateLimiterOptions = props?.rateLimiterOptions || {};
49 | rateLimiterOptions.requestsPerSecond ||= 10;
50 | rateLimiterOptions.maxConcurrent ||= 2;
51 | rateLimiterOptions.requestPerSecondByClient ||= 0.5;
52 | rateLimiterOptions.maxConcurrentByClient ||= 1;
53 |
54 | const datastore = rateLimiterOptions.redis ? "redis" : "local";
55 | let clientOptions: any = {};
56 | if (rateLimiterOptions.redis) {
57 | clientOptions["clientOptions"] = {
58 | host: rateLimiterOptions.redis.host,
59 | port: rateLimiterOptions.redis.port || 6379,
60 | };
61 | }
62 |
63 | this.rateLimiter = new Bottleneck({
64 | maxConcurrent: rateLimiterOptions.maxConcurrent,
65 | minTime: Math.round(1000 / rateLimiterOptions.requestsPerSecond),
66 | datastore,
67 | ...clientOptions,
68 | });
69 |
70 | this.rateLimiterGroup = new Bottleneck.Group({
71 | maxConcurrent: rateLimiterOptions.maxConcurrentByClient,
72 | minTime: Math.round(1000 / rateLimiterOptions.requestPerSecondByClient),
73 | datastore,
74 | ...clientOptions,
75 | });
76 | }
77 |
78 | private async handleRequestImplementation(
79 | params: FetchChatCompletionParams,
80 | adapter: HttpAdapter
81 | ) {
82 | const chat = new ChatCompletion({ apiKey: this.openAIApiKey });
83 | chat.on("data", (data) => adapter.onData(data));
84 | chat.on("error", (error) => adapter.onError(error));
85 | chat.on("end", () => adapter.onEnd());
86 | await chat.fetchChatCompletion(params);
87 | }
88 |
89 | async handleRequest(
90 | req: T,
91 | params: FetchChatCompletionParams,
92 | adapter: HttpAdapter
93 | ) {
94 | const rateLimiterKey = this.rateLimiterKey
95 | ? this.rateLimiterKey(req)
96 | : undefined;
97 | if (rateLimiterKey) {
98 | await this.rateLimiter.schedule(async () => {});
99 | await this.rateLimiterGroup.key(rateLimiterKey).schedule(async () => {
100 | await this.handleRequestImplementation(params, adapter);
101 | });
102 | } else {
103 | await this.rateLimiter.schedule(async () => {
104 | await this.handleRequestImplementation(params, adapter);
105 | });
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/packages/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "module": "commonjs",
5 | "declaration": true,
6 | "strict": true,
7 | "esModuleInterop": true,
8 | "skipLibCheck": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "outDir": "dist",
11 | "lib": ["es2019"],
12 | "allowJs": true,
13 | "allowSyntheticDefaultImports": true,
14 | "moduleResolution": "node",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "noEmit": false,
18 | "types": ["node"]
19 | },
20 | "include": ["src"],
21 | "exclude": ["node_modules", "dist"]
22 | }
23 |
--------------------------------------------------------------------------------
/scripts/sync-versions.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "fs";
2 |
3 | // Define the paths for the VERSION file and the package.json files
4 | const versionFilePath = "./VERSION";
5 | const rootPackageJsonPath = "./package.json";
6 | const otherPackageJsonPaths = [
7 | rootPackageJsonPath,
8 | "./packages/openai/package.json",
9 | "./packages/server/package.json",
10 | "./packages/express/package.json",
11 | "./packages/next/package.json",
12 | "./packages/remix/package.json",
13 | "./packages/core/package.json",
14 | "./packages/react/package.json",
15 | "./demo/frontend/presentation/package.json",
16 | "./demo/backend/next/package.json",
17 | "./demo/backend/express/package.json",
18 | "./demo/backend/remix/package.json",
19 | ];
20 |
21 | // This function reads the VERSION file and returns the version string
22 | function readVersionFile(filePath: string): string {
23 | return fs.readFileSync(filePath, "utf8").trim();
24 | }
25 |
26 | // This function reads a package.json file and returns its content as a JSON object
27 | function readPackageJson(filePath: string): any {
28 | return JSON.parse(fs.readFileSync(filePath, "utf8"));
29 | }
30 |
31 | // This function writes the updated JSON content back to the package.json file
32 | function writePackageJson(filePath: string, content: any): void {
33 | fs.writeFileSync(filePath, JSON.stringify(content, null, 2) + "\n");
34 | }
35 |
36 | // This function updates the version and dependencies in targetPackageJson using the versions from rootPackageJson
37 | function updatePackageData(
38 | rootPackageJson: any,
39 | targetPackageJson: any,
40 | newVersion: string
41 | ): boolean {
42 | let hasChanged = false;
43 |
44 | // Update version
45 | if (newVersion && targetPackageJson.version !== newVersion) {
46 | targetPackageJson.version = newVersion;
47 | hasChanged = true;
48 | }
49 |
50 | // Update dependencies if they exist in rootPackageJson
51 | const typesOfDependencies = [
52 | "dependencies",
53 | "devDependencies",
54 | "peerDependencies",
55 | ];
56 | typesOfDependencies.forEach((depType) => {
57 | if (rootPackageJson[depType] && targetPackageJson[depType]) {
58 | Object.keys(targetPackageJson[depType]).forEach((dep) => {
59 | if (dep.startsWith("@beakjs/")) {
60 | targetPackageJson[depType][dep] = newVersion;
61 | hasChanged = true;
62 | } else if (rootPackageJson[depType][dep]) {
63 | targetPackageJson[depType][dep] = rootPackageJson[depType][dep];
64 | hasChanged = true;
65 | }
66 | });
67 | }
68 | });
69 |
70 | return hasChanged;
71 | }
72 |
73 | // Main function to update version and dependencies versions in all package.json files
74 | function updateVersionsAndDependencies(): void {
75 | const newVersion = readVersionFile(versionFilePath);
76 | const rootPackageJson = readPackageJson(rootPackageJsonPath);
77 |
78 | otherPackageJsonPaths.forEach((packageJsonPath) => {
79 | try {
80 | const targetPackageJson = readPackageJson(packageJsonPath);
81 | const hasChanged = updatePackageData(
82 | rootPackageJson,
83 | targetPackageJson,
84 | newVersion
85 | );
86 |
87 | if (hasChanged) {
88 | writePackageJson(packageJsonPath, targetPackageJson);
89 | console.log(`Updated version and dependencies in ${packageJsonPath}`);
90 | } else {
91 | console.log(`No changes made to ${packageJsonPath}`);
92 | }
93 | } catch (error) {
94 | console.error(`Error updating ${packageJsonPath}: ${error}`);
95 | }
96 | });
97 | }
98 |
99 | updateVersionsAndDependencies();
100 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "strict": true,
5 | "esModuleInterop": true,
6 | "skipLibCheck": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "outDir": "dist",
9 | "lib": ["dom", "dom.iterable", "esnext"],
10 | "allowJs": true,
11 | "allowSyntheticDefaultImports": true,
12 | "module": "CommonJS",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "jsx": "react",
17 | "baseUrl": ".",
18 | "paths": {
19 | "@beakjs/core": ["packages/core"]
20 | }
21 | },
22 | "include": ["packages"],
23 | "exclude": ["packages/test"]
24 | }
25 |
--------------------------------------------------------------------------------