@plasius/chatbot 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/CODE_OF_CONDUCT.md +79 -0
  3. package/CONTRIBUTORS.md +27 -0
  4. package/LICENSE +21 -0
  5. package/README.md +43 -0
  6. package/SECURITY.md +17 -0
  7. package/dist/chatbot.d.ts +9 -0
  8. package/dist/chatbot.d.ts.map +1 -0
  9. package/dist/chatbot.js +180 -0
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +1 -0
  13. package/dist/inputbox.d.ts +7 -0
  14. package/dist/inputbox.d.ts.map +1 -0
  15. package/dist/inputbox.js +4 -0
  16. package/dist/renderpriority.d.ts +8 -0
  17. package/dist/renderpriority.d.ts.map +1 -0
  18. package/dist/renderpriority.js +8 -0
  19. package/dist/styles/chatbot.module.css +106 -0
  20. package/dist-cjs/chatbot.d.ts +9 -0
  21. package/dist-cjs/chatbot.d.ts.map +1 -0
  22. package/dist-cjs/chatbot.js +219 -0
  23. package/dist-cjs/index.d.ts +2 -0
  24. package/dist-cjs/index.d.ts.map +1 -0
  25. package/dist-cjs/index.js +8 -0
  26. package/dist-cjs/inputbox.d.ts +7 -0
  27. package/dist-cjs/inputbox.d.ts.map +1 -0
  28. package/dist-cjs/inputbox.js +7 -0
  29. package/dist-cjs/renderpriority.d.ts +8 -0
  30. package/dist-cjs/renderpriority.d.ts.map +1 -0
  31. package/dist-cjs/renderpriority.js +10 -0
  32. package/dist-cjs/styles/chatbot.module.css +106 -0
  33. package/docs/adrs/adr-0001-chatbot-package-scope.md +21 -0
  34. package/docs/adrs/adr-0002-public-repo-governance.md +24 -0
  35. package/docs/adrs/adr-template.md +35 -0
  36. package/legal/CLA-REGISTRY.csv +1 -0
  37. package/legal/CLA.md +22 -0
  38. package/legal/CORPORATE_CLA.md +57 -0
  39. package/legal/INDIVIDUAL_CLA.md +91 -0
  40. package/package.json +116 -0
  41. package/src/chatbot.tsx +306 -0
  42. package/src/global.d.ts +9 -0
  43. package/src/index.ts +1 -0
  44. package/src/inputbox.tsx +11 -0
  45. package/src/renderpriority.ts +8 -0
  46. package/src/styles/chatbot.module.css +106 -0
  47. package/src/types/emoji-picker-react-esm.d.ts +3 -0
package/package.json ADDED
@@ -0,0 +1,116 @@
1
+ {
2
+ "name": "@plasius/chatbot",
3
+ "version": "1.0.0",
4
+ "description": "OpenAI Chatbot",
5
+ "main": "./dist-cjs/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "private": false,
8
+ "type": "module",
9
+ "scripts": {
10
+ "build": "tsc --build --listEmittedFiles && rsync -av --include '*/' --include '*.module.css' --exclude '*' src/ dist/ || true && npm run build:cjs",
11
+ "test": "vitest run",
12
+ "test:watch": "vitest",
13
+ "test:coverage": "vitest run --coverage",
14
+ "test:coverage:watch": "vitest --coverage",
15
+ "clean": "rimraf dist-cjs dist tsconfig.tsbuildinfo",
16
+ "reset:clean": "rm -rf node_modules package-lock.json && npm run clean",
17
+ "audit:ts": "tsc --noEmit --pretty",
18
+ "audit:eslint": "eslint \"{src,apps,packages}/**/*.{ts,tsx}\" --max-warnings=0 --ext .ts,.tsx",
19
+ "audit:deps": "depcheck --skip-missing=true",
20
+ "audit:npm": "npm audit --audit-level=moderate || true",
21
+ "audit:test": "vitest run --coverage",
22
+ "audit:all": "npm-run-all -l audit:ts audit:eslint audit:deps audit:npm audit:test",
23
+ "build:cjs": "tsc -p tsconfig.json --module commonjs --moduleResolution node --outDir dist-cjs --tsBuildInfoFile dist-cjs/tsconfig.tsbuildinfo --listEmittedFiles && rsync -av --include '*/' --include '*.module.css' --exclude '*' src/ dist-cjs/ || true",
24
+ "lint": "eslint .",
25
+ "prepare": "npm run build"
26
+ },
27
+ "author": "Plasius LTD <development@plasius.co.uk>",
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "@plasius/entity-manager": "^1.0.4",
31
+ "@plasius/error": "^1.0.0",
32
+ "@plasius/profile": "^1.0.0",
33
+ "@plasius/schema": "^1.0.0",
34
+ "emoji-picker-react": "^4.12.2",
35
+ "react": "19.1.0",
36
+ "react-dom": "19.1.0",
37
+ "react-router-dom": "^7.6.2",
38
+ "react-icons": "^5.5.0"
39
+ },
40
+ "peerDependencies": {
41
+ "openai": "^5.19.1",
42
+ "react": "^19.1.0"
43
+ },
44
+ "devDependencies": {
45
+ "@testing-library/react": "^16.3.0",
46
+ "@types/react": "^19.1.8",
47
+ "@types/uuid": "^10.0.0",
48
+ "@typescript-eslint/eslint-plugin": "^8.38.0",
49
+ "@typescript-eslint/parser": "^8.38.0",
50
+ "@vitest/coverage-v8": "^3.2.4",
51
+ "ajv": "^6.12.6",
52
+ "depcheck": "^1.4.7",
53
+ "eslint": "^9.33.0",
54
+ "npm-run-all": "^4.1.5",
55
+ "openai": "^5.19.1",
56
+ "react": "19.1.0",
57
+ "react-dom": "19.1.0",
58
+ "tsx": "^4.20.3",
59
+ "typescript": "^5.8.3",
60
+ "vitest": "^3.2.4",
61
+ "zod": "^4.1.5"
62
+ },
63
+ "overrides": {
64
+ "react": "19.1.0",
65
+ "react-dom": "19.1.0"
66
+ },
67
+ "sideEffects": [
68
+ "*.css"
69
+ ],
70
+ "exports": {
71
+ ".": {
72
+ "types": "./dist/index.d.ts",
73
+ "import": "./dist/index.js",
74
+ "require": "./dist-cjs/index.js"
75
+ },
76
+ "./package.json": "./package.json"
77
+ },
78
+ "module": "./dist/index.js",
79
+ "files": [
80
+ "dist",
81
+ "dist-cjs",
82
+ "src",
83
+ "README.md",
84
+ "CHANGELOG.md",
85
+ "LICENSE",
86
+ "SECURITY.md",
87
+ "CODE_OF_CONDUCT.md",
88
+ "CONTRIBUTORS.md",
89
+ "docs",
90
+ "legal"
91
+ ],
92
+ "repository": {
93
+ "type": "git",
94
+ "url": "git+https://github.com/Plasius-LTD/chatbot.git"
95
+ },
96
+ "bugs": {
97
+ "url": "https://github.com/Plasius-LTD/chatbot/issues"
98
+ },
99
+ "homepage": "https://github.com/Plasius-LTD/chatbot#readme",
100
+ "publishConfig": {
101
+ "access": "public"
102
+ },
103
+ "funding": [
104
+ {
105
+ "type": "patreon",
106
+ "url": "https://www.patreon.com/c/plasiusltd/membership"
107
+ },
108
+ {
109
+ "type": "github",
110
+ "url": "https://github.com/sponsors/Plasius-LTD"
111
+ }
112
+ ],
113
+ "engines": {
114
+ "node": ">=22.12"
115
+ }
116
+ }
@@ -0,0 +1,306 @@
1
+ import React, { lazy, Suspense, useState, useEffect } from "react";
2
+ import type { EmojiClickData } from "emoji-picker-react";
3
+
4
+ const EmojiPicker = lazy(() =>
5
+ import("emoji-picker-react/dist/emoji-picker-react.esm.js").then(
6
+ (module) => ({
7
+ default: module.EmojiPicker, // <--- force the correct component export
8
+ })
9
+ )
10
+ );
11
+
12
+ import { FaPaperPlane, FaSmile } from "react-icons/fa";
13
+
14
+ import OpenAI from "openai";
15
+
16
+ import styles from "./styles/chatbot.module.css"; // Import a CSS file for styling
17
+
18
+ interface ChatBotProps {
19
+ openaiOrgID: string;
20
+ openaiProjectKey: string;
21
+ openaiAPIKey: string;
22
+ }
23
+
24
+ export default function ChatBot(
25
+ props: React.PropsWithChildren<ChatBotProps>
26
+ ): React.ReactElement {
27
+ const [messages, setMessages] = useState<
28
+ OpenAI.Chat.Completions.ChatCompletionMessageParam[]
29
+ >([]);
30
+ const [input, setInput] = useState<string>("");
31
+ const [showEmojiPicker, setShowEmojiPicker] = useState<boolean>(false);
32
+
33
+ const openai = new OpenAI({
34
+ apiKey: props.openaiAPIKey,
35
+ project: props.openaiProjectKey,
36
+ organization: props.openaiOrgID,
37
+ dangerouslyAllowBrowser: true,
38
+ });
39
+
40
+ const chat = async (
41
+ msgs: OpenAI.Chat.Completions.ChatCompletionMessageParam[],
42
+ callback: (arg: OpenAI.Chat.Completions.ChatCompletionMessageParam) => void
43
+ ): Promise<void> => {
44
+ try {
45
+ const value = await openai.chat.completions.create({
46
+ model: "gpt-o1",
47
+ messages: msgs,
48
+ });
49
+ value.choices.forEach((choice: OpenAI.ChatCompletion.Choice) => {
50
+ callback({ content: choice.message.content ?? "", role: "system" });
51
+ });
52
+ } catch (err) {
53
+ console.error("chat() failed", err);
54
+ }
55
+ };
56
+
57
+ const objects = window.location.origin + "/api/objects/list";
58
+ const decorations = window.location.origin + "/api/decorations/list";
59
+ const locations = window.location.origin + "/api/locations/list";
60
+ const surfaces = window.location.origin + "/api/surfaces/list";
61
+
62
+ useEffect(() => {
63
+ void chat(
64
+ [
65
+ {
66
+ role: "system",
67
+ content: `You are a game designer, you are responsible for helping build the world and game mechanics, adjusting the game to be more fun for the player playing,
68
+ using your knowledge of gameplay mechanics and world building you are going to help assign objects to the map.
69
+
70
+ You can find the list of objects from the following url: ${objects}
71
+ You can find the list of decorations from the following url: ${decorations}
72
+ You can find the list of surfaces from the following url: ${surfaces}
73
+
74
+ Each location is a hexagon with a radius of 10 meters and 10m tall (allowing for locations to be on top of each other!), and a q and r coordinate system.
75
+ The q coordinate is the horizontal axis, and the r coordinate is the vertical axis. The center of the hexagon is at (0, 0),
76
+ and the corners are at (5, 8.66), (10, 0), (5, -8.66), (-5, -8.66), (-10, 0), and (-5, 8.66).
77
+ Adjacent hexagons are at (q + 1, r), (q - 1, r), (q, r + 1), (q, r - 1), (q + 1, r - 1), and (q - 1, r + 1) and you should try and
78
+ coordinate over the hexagons to make sure the objects are placed in a way that makes sense.
79
+
80
+ Try and align surfaces, decorations, and objects to a 1m size hexagon when placing items so they align to each other in the world,
81
+ but avoid overlapping the objects with each other, unless they are meant to overlap (like a chair under a table, or a tree in a bush).
82
+ Surfaces should not overlap with each other, and should be placed in a way that makes sense for the location,
83
+ such as a road should be continuous and have purpose, to or from somewhere,
84
+ use the locations map to identify good roads, forests, mountains, lakes and oceans locations.
85
+
86
+ for each prompt the user gives you, will relate to a specific location in the game world, you should take in the location,
87
+ some basic information about the users expectations for the location and return a json object with the following fields:
88
+ {
89
+ "location": {
90
+ "r": "number", // 10m hexagon radius
91
+ "q": "number", // 10m hexagon radius
92
+ "elevation": "number",
93
+ "name": "string",
94
+ "description": "string",
95
+ "type": "string",
96
+
97
+ "surfaces": [{
98
+ "location": {
99
+ "q": "number", // 1m hexagon radius
100
+ "r": "number", // 1m hexagon radius
101
+ "elevation": "number"
102
+ },
103
+ "name": "string",
104
+ "type": "string",
105
+ "description": "string",
106
+ "url": "string",
107
+ "image": "string",
108
+ "rotation": "number",
109
+ "color": "string"
110
+ }],
111
+ "decorations": [
112
+ {
113
+ "name": "string",
114
+ "type": "string",
115
+ "description": "string",
116
+ "url": "string",
117
+ "image": "string",
118
+ "rotation": "number",
119
+ "scale": "number",
120
+ "color": "string",
121
+ "location": {
122
+ "x": "number",
123
+ "y": "number",
124
+ "z": "number"
125
+ }
126
+ }
127
+ ],
128
+ "objects": [
129
+ {
130
+ "name": "string",
131
+ "type": "string",
132
+ "description": "string",
133
+ "url": "string",
134
+ "image": "string",
135
+ "rotation": "number",
136
+ "scale": "number",
137
+ "color": "string",
138
+ "location": {
139
+ "x": "number",
140
+ "y": "number",
141
+ "z": "number"
142
+ }
143
+ }
144
+ ]
145
+ }
146
+ }
147
+
148
+ You can find the list of populated locations from the following url: ${locations} for reference and to allow you to be more creative in your assignments.
149
+ If your current location is in the list, then take the current objects and decorations into account when placing the new objects, and remove or replace the old ones.`,
150
+ },
151
+ ],
152
+ (arg: OpenAI.Chat.Completions.ChatCompletionMessageParam) => {
153
+ setMessages((prev) => [...prev, arg]);
154
+ }
155
+ );
156
+ }, []);
157
+
158
+ const handleSend = async (): Promise<void> => {
159
+ if (input.trim()) {
160
+ setMessages((prev) => [...prev, { content: input, role: "user" }]);
161
+ setInput("");
162
+ setShowEmojiPicker(false);
163
+
164
+ try {
165
+ const value = await openai.chat.completions.create({
166
+ model: "gpt-4o-mini",
167
+ messages: [{ content: input, role: "user" }],
168
+ });
169
+ value.choices.forEach((choice: OpenAI.ChatCompletion.Choice) => {
170
+ setMessages((prev) => [
171
+ ...prev,
172
+ { content: choice.message.content ?? "", role: "system" },
173
+ ]);
174
+ });
175
+ } catch (err) {
176
+ console.error("handleSend() failed", err);
177
+ }
178
+ }
179
+ };
180
+
181
+ const handleEmojiClick = (emojiData: EmojiClickData): void => {
182
+ setInput((prev) => prev + (emojiData.emoji ?? ""));
183
+ };
184
+
185
+ const contentToString = (
186
+ content: OpenAI.Chat.Completions.ChatCompletionMessageParam["content"]
187
+ ): string => {
188
+ if (typeof content === "string" || content == null) return content ?? "";
189
+ if (Array.isArray(content)) {
190
+ return content
191
+ .map((part: unknown) => {
192
+ if (typeof part === "string") return part;
193
+ if (
194
+ typeof part === "object" &&
195
+ part !== null &&
196
+ Object.prototype.hasOwnProperty.call(part, "text")
197
+ ) {
198
+ const text = (part as Record<string, unknown>).text;
199
+ return typeof text === "string" ? text : "";
200
+ }
201
+ return "";
202
+ })
203
+ .join("");
204
+ }
205
+ return "";
206
+ };
207
+
208
+ return (
209
+ <div className={styles.chatbotcontainer}>
210
+ <div className={styles.messagesbox}>
211
+ {messages.map((msg, index) => (
212
+ <div key={index} className={styles.message + ` ${styles[msg.role]}`}>
213
+ <div className={styles.bubble}>{contentToString(msg.content)}</div>
214
+ </div>
215
+ ))}
216
+ </div>
217
+ <div className={styles.inputbox}>
218
+ <input
219
+ type="text"
220
+ value={input}
221
+ onChange={(e) => setInput(e.target.value)}
222
+ onKeyUp={async (e) => {
223
+ if (e.key === "Enter" && e.shiftKey === false) {
224
+ await handleSend();
225
+ e.stopPropagation();
226
+ }
227
+ }}
228
+ placeholder="Type a message..."
229
+ />
230
+ <FaSmile
231
+ onClick={() => setShowEmojiPicker(!showEmojiPicker)}
232
+ className={styles.emojiicon}
233
+ />
234
+ {showEmojiPicker && (
235
+ <Suspense fallback={<div>Loading emoji picker...</div>}>
236
+ <EmojiPicker onEmojiClick={handleEmojiClick} />
237
+ </Suspense>
238
+ )}
239
+ <FaPaperPlane onClick={handleSend} className={styles.sendicon} />
240
+ </div>
241
+ </div>
242
+ );
243
+ }
244
+
245
+ /*
246
+
247
+ // Chatbot.tsx
248
+ import React, { useState } from 'react';
249
+ import { FaPaperPlane, FaSmile } from 'react-icons/fa';
250
+ import Picker, { IEmojiData } from 'emoji-picker-react';
251
+
252
+ interface Message {
253
+ text: string;
254
+ user: 'me' | 'bot';
255
+ }
256
+
257
+ const Chatbot: React.FC = () => {
258
+ const [messages, setMessages] = useState<Message[]>([]);
259
+ const [input, setInput] = useState<string>('');
260
+ const [showEmojiPicker, setShowEmojiPicker] = useState<boolean>(false);
261
+
262
+ const handleSend = () => {
263
+ if (input.trim()) {
264
+ setMessages([...messages, { text: input, user: 'me' }]);
265
+ setInput('');
266
+ setShowEmojiPicker(false);
267
+
268
+ // Here you would also call the OpenAI API and handle the response
269
+ // Example:
270
+ // fetchOpenAIResponse(input).then(response => {
271
+ // setMessages([...messages, { text: input, user: 'me' }, { text: response, user: 'bot' }]);
272
+ // });
273
+ }
274
+ };
275
+
276
+ const handleEmojiClick = (event: React.MouseEvent<Element, MouseEvent>, emojiObject: IEmojiData) => {
277
+ setInput(input + emojiObject.emoji);
278
+ };
279
+
280
+ return (
281
+ <div className="chatbot-container">
282
+ <div className="messages-box">
283
+ {messages.map((msg, index) => (
284
+ <div key={index} className={`message ${msg.user}`}>
285
+ {msg.text}
286
+ </div>
287
+ ))}
288
+ </div>
289
+ <div className="input-box">
290
+ <input
291
+ type="text"
292
+ value={input}
293
+ onChange={(e) => setInput(e.target.value)}
294
+ placeholder="Type a message..."
295
+ />
296
+ <FaSmile onClick={() => setShowEmojiPicker(!showEmojiPicker)} className="emoji-icon" />
297
+ {showEmojiPicker && <Picker onEmojiClick={handleEmojiClick} />}
298
+ <FaPaperPlane onClick={handleSend} className="send-icon" />
299
+ </div>
300
+ </div>
301
+ );
302
+ };
303
+
304
+ export default Chatbot;
305
+
306
+ */
@@ -0,0 +1,9 @@
1
+ // global.d.ts
2
+ declare module "*.module.css" {
3
+ const classes: { [key: string]: string };
4
+ export default classes;
5
+ }
6
+ declare module "*.module.scss" {
7
+ const classes: { [key: string]: string };
8
+ export default classes;
9
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { default as ChatBot } from "./chatbot.js";
@@ -0,0 +1,11 @@
1
+ import React from "react";
2
+
3
+ interface InputBoxProps {
4
+ children: React.ReactNode;
5
+ }
6
+
7
+ export function InputBox(
8
+ props: InputBoxProps
9
+ ): React.ReactElement<InputBoxProps> {
10
+ return <div>{props.children}</div>;
11
+ }
@@ -0,0 +1,8 @@
1
+ enum RenderPriority {
2
+ Three = 0,
3
+ UI = 1,
4
+ Animation = 2,
5
+ Particle = 3,
6
+ }
7
+
8
+ export { RenderPriority };
@@ -0,0 +1,106 @@
1
+ /* src/components/Chatbot.css */
2
+ .chatbotcontainer {
3
+ display: flex;
4
+ flex-direction: column;
5
+ height: 100vh;
6
+ width: 100%;
7
+ max-width: 600px;
8
+ margin: 0 auto;
9
+ border: 1px solid #ddd;
10
+ border-radius: 8px;
11
+ background: #f9f9f9;
12
+ }
13
+
14
+ .messagesbox {
15
+ flex: 1;
16
+ padding: 10px;
17
+ overflow-y: auto;
18
+ background: #fff;
19
+ }
20
+
21
+ .message {
22
+ display: flex;
23
+ margin-bottom: 10px;
24
+ }
25
+
26
+ .message.user {
27
+ justify-content: flex-end;
28
+ }
29
+
30
+ .message.system {
31
+ justify-content: flex-start;
32
+ }
33
+
34
+ .bubble {
35
+ max-width: 60%;
36
+ padding: 10px 15px;
37
+ border-radius: 15px;
38
+ font-size: 14px;
39
+ line-height: 1.5;
40
+ position: relative;
41
+ word-wrap: break-word;
42
+ }
43
+
44
+ .message.user .bubble {
45
+ background: #007bff;
46
+ color: #fff;
47
+ }
48
+
49
+ .message.system .bubble {
50
+ background: #e9ecef;
51
+ color: #333;
52
+ }
53
+
54
+ .bubble::after {
55
+ content: '';
56
+ position: absolute;
57
+ width: 0;
58
+ height: 0;
59
+ border-style: solid;
60
+ }
61
+
62
+ .message.user .bubble::after {
63
+ right: -10px;
64
+ top: 50%;
65
+ border-width: 10px 0 10px 10px;
66
+ border-color: transparent transparent transparent #007bff;
67
+ transform: translateY(-50%);
68
+ }
69
+
70
+ .message.system .bubble::after {
71
+ left: -10px;
72
+ top: 50%;
73
+ border-width: 10px 10px 10px 0;
74
+ border-color: transparent #e9ecef transparent transparent;
75
+ transform: translateY(-50%);
76
+ }
77
+
78
+ .inputbox {
79
+ display: flex;
80
+ align-items: center;
81
+ padding: 10px;
82
+ border-top: 1px solid #ddd;
83
+ background: #fff;
84
+ }
85
+
86
+ .inputbox input {
87
+ flex: 1;
88
+ padding: 10px;
89
+ border: 1px solid #ddd;
90
+ border-radius: 20px;
91
+ font-size: 14px;
92
+ }
93
+
94
+ .emojiicon, .sendicon {
95
+ margin-left: 10px;
96
+ cursor: pointer;
97
+ color: whitesmoke;
98
+ }
99
+
100
+ .emojiicon {
101
+ font-size: 20px;
102
+ }
103
+
104
+ .sendicon {
105
+ font-size: 20px;
106
+ }
@@ -0,0 +1,3 @@
1
+ declare module "emoji-picker-react/dist/emoji-picker-react.esm.js" {
2
+ export const EmojiPicker: React.ComponentType<any>;
3
+ }