@plasius/chatbot 1.0.0 → 1.0.1

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.
package/CHANGELOG.md CHANGED
@@ -20,6 +20,23 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
20
20
  - **Security**
21
21
  - (placeholder)
22
22
 
23
+ ## [1.0.1] - 2026-02-21
24
+
25
+ - **Added**
26
+ - Add chatbot API client helpers (`getChatbotUsage`, `sendChatbotMessage`) and typed `ChatbotClientError`.
27
+ - Add signed-in gating UX and demo usage-limit messaging in the `ChatBot` component.
28
+ - Add usage/auth/client tests for chatbot API integration behavior.
29
+
30
+ - **Changed**
31
+ - Refactor `ChatBot` to call backend `/ai/chatbot` instead of browser-side OpenAI keys.
32
+ - Simplify package peer requirements by removing direct `openai` dependency from `@plasius/chatbot`.
33
+
34
+ - **Fixed**
35
+ - (placeholder)
36
+
37
+ - **Security**
38
+ - (placeholder)
39
+
23
40
  ## [1.0.0] - 2026-02-12
24
41
 
25
42
  - **Added**
@@ -48,7 +65,7 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
48
65
 
49
66
  ---
50
67
 
51
- [Unreleased]: https://github.com/Plasius-LTD/chatbot/compare/v1.0.0...HEAD
68
+ [Unreleased]: https://github.com/Plasius-LTD/chatbot/compare/v1.0.1...HEAD
52
69
 
53
70
  ## [1.0.0] - 2026-02-11
54
71
 
@@ -64,3 +81,4 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
64
81
  - **Security**
65
82
  - (placeholder)
66
83
  [1.0.0]: https://github.com/Plasius-LTD/chatbot/releases/tag/v1.0.0
84
+ [1.0.1]: https://github.com/Plasius-LTD/chatbot/releases/tag/v1.0.1
package/dist/chatbot.d.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  import React from "react";
2
- interface ChatBotProps {
3
- openaiOrgID: string;
4
- openaiProjectKey: string;
5
- openaiAPIKey: string;
2
+ import { type ChatMessage, type ChatbotClientOptions, type ChatbotUsage } from "./client.js";
3
+ export interface ChatBotProps extends ChatbotClientOptions {
4
+ initialMessages?: ChatMessage[];
5
+ systemPrompt?: string;
6
+ placeholder?: string;
7
+ title?: string;
8
+ onUsageChange?: (usage: ChatbotUsage) => void;
9
+ onAuthRequired?: () => void;
6
10
  }
7
11
  export default function ChatBot(props: React.PropsWithChildren<ChatBotProps>): React.ReactElement;
8
- export {};
9
12
  //# sourceMappingURL=chatbot.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"chatbot.d.ts","sourceRoot":"","sources":["../src/chatbot.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8C,MAAM,OAAO,CAAC;AAiBnE,UAAU,YAAY;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,CAAC,OAAO,UAAU,OAAO,CAC7B,KAAK,EAAE,KAAK,CAAC,iBAAiB,CAAC,YAAY,CAAC,GAC3C,KAAK,CAAC,YAAY,CAyNpB"}
1
+ {"version":3,"file":"chatbot.d.ts","sourceRoot":"","sources":["../src/chatbot.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAoE,MAAM,OAAO,CAAC;AAIzF,OAAO,EAIL,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,YAAY,EAClB,MAAM,aAAa,CAAC;AAUrB,MAAM,WAAW,YAAa,SAAQ,oBAAoB;IACxD,eAAe,CAAC,EAAE,WAAW,EAAE,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IAC9C,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAoBD,MAAM,CAAC,OAAO,UAAU,OAAO,CAC7B,KAAK,EAAE,KAAK,CAAC,iBAAiB,CAAC,YAAY,CAAC,GAC3C,KAAK,CAAC,YAAY,CA2NpB"}
package/dist/chatbot.js CHANGED
@@ -1,180 +1,160 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { lazy, Suspense, useState, useEffect } from "react";
2
+ import { lazy, Suspense, useCallback, useEffect, useMemo, useState } from "react";
3
+ import { FaPaperPlane, FaSmile } from "react-icons/fa";
4
+ import styles from "./styles/chatbot.module.css";
5
+ import { ChatbotClientError, getChatbotUsage, sendChatbotMessage, } from "./client.js";
3
6
  const EmojiPicker = lazy(() => import("emoji-picker-react/dist/emoji-picker-react.esm.js").then((module) => ({
4
- default: module.EmojiPicker, // <--- force the correct component export
7
+ default: module.EmojiPicker,
5
8
  })));
6
- import { FaPaperPlane, FaSmile } from "react-icons/fa";
7
- import OpenAI from "openai";
8
- import styles from "./styles/chatbot.module.css"; // Import a CSS file for styling
9
+ const DEFAULT_TITLE = "Plasius Chatbot";
10
+ const DEFAULT_PLACEHOLDER = "Ask Plasius something...";
11
+ const DEFAULT_SYSTEM_PROMPT = "You are the Plasius assistant. Keep responses concise, practical, and factual.";
12
+ function statusMessage(state, usage) {
13
+ if (state === "loading")
14
+ return "Checking access...";
15
+ if (state === "signed_out")
16
+ return "Sign in to use chatbot.";
17
+ if (state === "limit_reached") {
18
+ if (usage) {
19
+ return `Demo limit reached (${usage.used}/${usage.limit} messages).`;
20
+ }
21
+ return "Demo limit reached.";
22
+ }
23
+ if (state === "error")
24
+ return "Chatbot is currently unavailable.";
25
+ return "";
26
+ }
9
27
  export default function ChatBot(props) {
10
- const [messages, setMessages] = useState([]);
28
+ const [messages, setMessages] = useState(props.initialMessages ?? []);
11
29
  const [input, setInput] = useState("");
12
30
  const [showEmojiPicker, setShowEmojiPicker] = useState(false);
13
- const openai = new OpenAI({
14
- apiKey: props.openaiAPIKey,
15
- project: props.openaiProjectKey,
16
- organization: props.openaiOrgID,
17
- dangerouslyAllowBrowser: true,
18
- });
19
- const chat = async (msgs, callback) => {
20
- try {
21
- const value = await openai.chat.completions.create({
22
- model: "gpt-o1",
23
- messages: msgs,
24
- });
25
- value.choices.forEach((choice) => {
26
- callback({ content: choice.message.content ?? "", role: "system" });
27
- });
28
- }
29
- catch (err) {
30
- console.error("chat() failed", err);
31
- }
32
- };
33
- const objects = window.location.origin + "/api/objects/list";
34
- const decorations = window.location.origin + "/api/decorations/list";
35
- const locations = window.location.origin + "/api/locations/list";
36
- const surfaces = window.location.origin + "/api/surfaces/list";
31
+ const [isSending, setIsSending] = useState(false);
32
+ const [state, setState] = useState("loading");
33
+ const [usage, setUsage] = useState(null);
34
+ const [errorMessage, setErrorMessage] = useState(null);
35
+ const clientOptions = useMemo(() => ({
36
+ endpoint: props.endpoint,
37
+ credentials: props.credentials,
38
+ headers: props.headers,
39
+ fetchFn: props.fetchFn,
40
+ csrfCookieName: props.csrfCookieName,
41
+ csrfHeaderName: props.csrfHeaderName,
42
+ bootstrapCsrf: props.bootstrapCsrf,
43
+ }), [
44
+ props.endpoint,
45
+ props.credentials,
46
+ props.headers,
47
+ props.fetchFn,
48
+ props.csrfCookieName,
49
+ props.csrfHeaderName,
50
+ props.bootstrapCsrf,
51
+ ]);
52
+ const applyUsage = useCallback((nextUsage) => {
53
+ setUsage(nextUsage);
54
+ props.onUsageChange?.(nextUsage);
55
+ setState(nextUsage.exhausted ? "limit_reached" : "ready");
56
+ }, [props.onUsageChange]);
37
57
  useEffect(() => {
38
- void chat([
39
- {
40
- role: "system",
41
- 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,
42
- using your knowledge of gameplay mechanics and world building you are going to help assign objects to the map.
43
-
44
- You can find the list of objects from the following url: ${objects}
45
- You can find the list of decorations from the following url: ${decorations}
46
- You can find the list of surfaces from the following url: ${surfaces}
47
-
48
- 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.
49
- The q coordinate is the horizontal axis, and the r coordinate is the vertical axis. The center of the hexagon is at (0, 0),
50
- and the corners are at (5, 8.66), (10, 0), (5, -8.66), (-5, -8.66), (-10, 0), and (-5, 8.66).
51
- 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
52
- coordinate over the hexagons to make sure the objects are placed in a way that makes sense.
53
-
54
- Try and align surfaces, decorations, and objects to a 1m size hexagon when placing items so they align to each other in the world,
55
- 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).
56
- Surfaces should not overlap with each other, and should be placed in a way that makes sense for the location,
57
- such as a road should be continuous and have purpose, to or from somewhere,
58
- use the locations map to identify good roads, forests, mountains, lakes and oceans locations.
59
-
60
- for each prompt the user gives you, will relate to a specific location in the game world, you should take in the location,
61
- some basic information about the users expectations for the location and return a json object with the following fields:
62
- {
63
- "location": {
64
- "r": "number", // 10m hexagon radius
65
- "q": "number", // 10m hexagon radius
66
- "elevation": "number",
67
- "name": "string",
68
- "description": "string",
69
- "type": "string",
70
-
71
- "surfaces": [{
72
- "location": {
73
- "q": "number", // 1m hexagon radius
74
- "r": "number", // 1m hexagon radius
75
- "elevation": "number"
76
- },
77
- "name": "string",
78
- "type": "string",
79
- "description": "string",
80
- "url": "string",
81
- "image": "string",
82
- "rotation": "number",
83
- "color": "string"
84
- }],
85
- "decorations": [
86
- {
87
- "name": "string",
88
- "type": "string",
89
- "description": "string",
90
- "url": "string",
91
- "image": "string",
92
- "rotation": "number",
93
- "scale": "number",
94
- "color": "string",
95
- "location": {
96
- "x": "number",
97
- "y": "number",
98
- "z": "number"
99
- }
100
- }
101
- ],
102
- "objects": [
103
- {
104
- "name": "string",
105
- "type": "string",
106
- "description": "string",
107
- "url": "string",
108
- "image": "string",
109
- "rotation": "number",
110
- "scale": "number",
111
- "color": "string",
112
- "location": {
113
- "x": "number",
114
- "y": "number",
115
- "z": "number"
116
- }
117
- }
118
- ]
119
- }
120
- }
121
-
122
- 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.
123
- 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.`,
124
- },
125
- ], (arg) => {
126
- setMessages((prev) => [...prev, arg]);
127
- });
128
- }, []);
129
- const handleSend = async () => {
130
- if (input.trim()) {
131
- setMessages((prev) => [...prev, { content: input, role: "user" }]);
132
- setInput("");
133
- setShowEmojiPicker(false);
58
+ let active = true;
59
+ const loadUsage = async () => {
60
+ setState("loading");
61
+ setErrorMessage(null);
134
62
  try {
135
- const value = await openai.chat.completions.create({
136
- model: "gpt-4o-mini",
137
- messages: [{ content: input, role: "user" }],
138
- });
139
- value.choices.forEach((choice) => {
140
- setMessages((prev) => [
141
- ...prev,
142
- { content: choice.message.content ?? "", role: "system" },
143
- ]);
144
- });
63
+ const result = await getChatbotUsage(clientOptions);
64
+ if (!active)
65
+ return;
66
+ applyUsage(result.usage);
145
67
  }
146
- catch (err) {
147
- console.error("handleSend() failed", err);
68
+ catch (error) {
69
+ if (!active)
70
+ return;
71
+ if (error instanceof ChatbotClientError && error.status === 401) {
72
+ setState("signed_out");
73
+ setErrorMessage("Sign in to start chatting.");
74
+ props.onAuthRequired?.();
75
+ return;
76
+ }
77
+ setState("error");
78
+ setErrorMessage(error instanceof Error ? error.message : "Failed to load chatbot.");
148
79
  }
149
- }
150
- };
80
+ };
81
+ void loadUsage();
82
+ return () => {
83
+ active = false;
84
+ };
85
+ }, [applyUsage, clientOptions, props.onAuthRequired]);
86
+ const sendDisabled = isSending ||
87
+ state === "loading" ||
88
+ state === "signed_out" ||
89
+ state === "limit_reached" ||
90
+ !input.trim();
151
91
  const handleEmojiClick = (emojiData) => {
152
92
  setInput((prev) => prev + (emojiData.emoji ?? ""));
153
93
  };
154
- const contentToString = (content) => {
155
- if (typeof content === "string" || content == null)
156
- return content ?? "";
157
- if (Array.isArray(content)) {
158
- return content
159
- .map((part) => {
160
- if (typeof part === "string")
161
- return part;
162
- if (typeof part === "object" &&
163
- part !== null &&
164
- Object.prototype.hasOwnProperty.call(part, "text")) {
165
- const text = part.text;
166
- return typeof text === "string" ? text : "";
94
+ const handleSend = useCallback(async () => {
95
+ const message = input.trim();
96
+ if (!message || sendDisabled)
97
+ return;
98
+ const userMessage = { role: "user", content: message };
99
+ const nextHistory = [...messages, userMessage].slice(-20);
100
+ setMessages((prev) => [...prev, userMessage]);
101
+ setInput("");
102
+ setShowEmojiPicker(false);
103
+ setIsSending(true);
104
+ setErrorMessage(null);
105
+ try {
106
+ const response = await sendChatbotMessage({
107
+ message,
108
+ history: nextHistory,
109
+ systemPrompt: props.systemPrompt ?? DEFAULT_SYSTEM_PROMPT,
110
+ }, clientOptions);
111
+ setMessages((prev) => [
112
+ ...prev,
113
+ { role: "assistant", content: response.reply },
114
+ ]);
115
+ applyUsage(response.usage);
116
+ }
117
+ catch (error) {
118
+ if (error instanceof ChatbotClientError) {
119
+ if (error.status === 401) {
120
+ setState("signed_out");
121
+ setErrorMessage("You must be signed in to use chatbot.");
122
+ props.onAuthRequired?.();
123
+ return;
124
+ }
125
+ if (error.status === 429) {
126
+ if (error.usage) {
127
+ applyUsage(error.usage);
128
+ }
129
+ else {
130
+ setState("limit_reached");
131
+ }
132
+ setErrorMessage("You reached the 10 message demo limit.");
133
+ return;
167
134
  }
168
- return "";
169
- })
170
- .join("");
135
+ setState("error");
136
+ setErrorMessage(error.message);
137
+ return;
138
+ }
139
+ setState("error");
140
+ setErrorMessage(error instanceof Error ? error.message : "Message failed.");
171
141
  }
172
- return "";
173
- };
174
- return (_jsxs("div", { className: styles.chatbotcontainer, children: [_jsx("div", { className: styles.messagesbox, children: messages.map((msg, index) => (_jsx("div", { className: styles.message + ` ${styles[msg.role]}`, children: _jsx("div", { className: styles.bubble, children: contentToString(msg.content) }) }, index))) }), _jsxs("div", { className: styles.inputbox, children: [_jsx("input", { type: "text", value: input, onChange: (e) => setInput(e.target.value), onKeyUp: async (e) => {
175
- if (e.key === "Enter" && e.shiftKey === false) {
142
+ finally {
143
+ setIsSending(false);
144
+ }
145
+ }, [
146
+ applyUsage,
147
+ clientOptions,
148
+ input,
149
+ messages,
150
+ props.onAuthRequired,
151
+ props.systemPrompt,
152
+ sendDisabled,
153
+ ]);
154
+ return (_jsxs("div", { className: styles.chatbotcontainer, children: [_jsxs("div", { className: styles.header, children: [_jsx("div", { className: styles.title, children: props.title ?? DEFAULT_TITLE }), _jsx("div", { className: styles.usage, children: usage ? `${usage.used}/${usage.limit} used` : "No usage data" })] }), (state !== "ready" || errorMessage) && (_jsx("div", { className: styles.notice, children: errorMessage ?? statusMessage(state, usage) })), _jsx("div", { className: styles.messagesbox, children: messages.map((msg, index) => (_jsx("div", { className: styles.message, children: _jsx("div", { className: styles[msg.role], children: _jsx("div", { className: styles.bubble, children: msg.content }) }) }, `${msg.role}-${index}`))) }), _jsxs("div", { className: styles.inputbox, children: [_jsx("input", { type: "text", value: input, disabled: sendDisabled, onChange: (event) => setInput(event.target.value), onKeyUp: async (event) => {
155
+ if (event.key === "Enter" && !event.shiftKey) {
176
156
  await handleSend();
177
- e.stopPropagation();
157
+ event.stopPropagation();
178
158
  }
179
- }, placeholder: "Type a message..." }), _jsx(FaSmile, { onClick: () => setShowEmojiPicker(!showEmojiPicker), className: styles.emojiicon }), showEmojiPicker && (_jsx(Suspense, { fallback: _jsx("div", { children: "Loading emoji picker..." }), children: _jsx(EmojiPicker, { onEmojiClick: handleEmojiClick }) })), _jsx(FaPaperPlane, { onClick: handleSend, className: styles.sendicon })] })] }));
159
+ }, placeholder: props.placeholder ?? DEFAULT_PLACEHOLDER }), _jsx("button", { type: "button", className: styles.iconButton, onClick: () => setShowEmojiPicker((current) => !current), disabled: state === "signed_out" || state === "limit_reached", "aria-label": "Open emoji picker", children: _jsx(FaSmile, { className: styles.emojiicon }) }), showEmojiPicker && (_jsx("div", { className: styles.emojiPicker, children: _jsx(Suspense, { fallback: _jsx("div", { children: "Loading emoji picker..." }), children: _jsx(EmojiPicker, { onEmojiClick: handleEmojiClick }) }) })), _jsx("button", { type: "button", className: styles.iconButton, onClick: () => void handleSend(), disabled: sendDisabled, "aria-label": "Send message", children: _jsx(FaPaperPlane, { className: styles.sendicon }) })] })] }));
180
160
  }
@@ -0,0 +1,41 @@
1
+ export type ChatRole = "system" | "user" | "assistant";
2
+ export interface ChatMessage {
3
+ role: ChatRole;
4
+ content: string;
5
+ }
6
+ export interface ChatbotUsage {
7
+ limit: number;
8
+ used: number;
9
+ remaining: number;
10
+ exhausted: boolean;
11
+ }
12
+ export interface ChatbotReply {
13
+ reply: string;
14
+ model: string;
15
+ usage: ChatbotUsage;
16
+ }
17
+ export interface ChatbotUsageResponse {
18
+ usage: ChatbotUsage;
19
+ }
20
+ export interface ChatbotClientOptions {
21
+ endpoint?: string;
22
+ credentials?: RequestCredentials;
23
+ headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
24
+ fetchFn?: typeof fetch;
25
+ csrfCookieName?: string;
26
+ csrfHeaderName?: string;
27
+ bootstrapCsrf?: boolean;
28
+ }
29
+ export declare class ChatbotClientError extends Error {
30
+ status: number;
31
+ code?: string;
32
+ usage?: ChatbotUsage;
33
+ constructor(status: number, message: string, code?: string, usage?: ChatbotUsage);
34
+ }
35
+ export declare function getChatbotUsage(options?: ChatbotClientOptions): Promise<ChatbotUsageResponse>;
36
+ export declare function sendChatbotMessage(payload: {
37
+ message: string;
38
+ history?: ChatMessage[];
39
+ systemPrompt?: string;
40
+ }, options?: ChatbotClientOptions): Promise<ChatbotReply>;
41
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;AAEvD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,YAAY,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,YAAY,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,OAAO,CAAC,EAAE,WAAW,GAAG,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;IACnE,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAQD,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,YAAY,CAAC;gBAET,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,YAAY;CAOjF;AAiHD,wBAAsB,eAAe,CACnC,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,oBAAoB,CAAC,CA4B/B;AAED,wBAAsB,kBAAkB,CACtC,OAAO,EAAE;IACP,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,EACD,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,YAAY,CAAC,CAiDvB"}
package/dist/client.js ADDED
@@ -0,0 +1,168 @@
1
+ export class ChatbotClientError extends Error {
2
+ constructor(status, message, code, usage) {
3
+ super(message);
4
+ this.name = "ChatbotClientError";
5
+ this.status = status;
6
+ this.code = code;
7
+ this.usage = usage;
8
+ }
9
+ }
10
+ const DEFAULT_ENDPOINT = "/ai/chatbot";
11
+ const DEFAULT_CSRF_COOKIE_NAME = "csrf-token";
12
+ const DEFAULT_CSRF_HEADER_NAME = "x-csrf-token";
13
+ function resolveFetch(fetchFn) {
14
+ const resolved = fetchFn ?? (typeof fetch !== "undefined" ? fetch : undefined);
15
+ if (!resolved) {
16
+ throw new Error("No fetch implementation is available.");
17
+ }
18
+ return resolved;
19
+ }
20
+ async function resolveHeaders(headers) {
21
+ if (!headers)
22
+ return undefined;
23
+ if (typeof headers === "function") {
24
+ return await headers();
25
+ }
26
+ return headers;
27
+ }
28
+ function readCookie(name) {
29
+ if (typeof document === "undefined" || typeof document.cookie !== "string") {
30
+ return undefined;
31
+ }
32
+ const entry = document.cookie
33
+ .split(";")
34
+ .map((part) => part.trim())
35
+ .find((value) => value.startsWith(`${name}=`));
36
+ if (!entry)
37
+ return undefined;
38
+ const [, rawValue = ""] = entry.split("=");
39
+ try {
40
+ return decodeURIComponent(rawValue);
41
+ }
42
+ catch {
43
+ return rawValue;
44
+ }
45
+ }
46
+ function normalizeUsage(value) {
47
+ if (!value || typeof value !== "object")
48
+ return undefined;
49
+ const usage = value;
50
+ if (typeof usage.limit !== "number" ||
51
+ typeof usage.used !== "number" ||
52
+ typeof usage.remaining !== "number" ||
53
+ typeof usage.exhausted !== "boolean") {
54
+ return undefined;
55
+ }
56
+ return {
57
+ limit: usage.limit,
58
+ used: usage.used,
59
+ remaining: usage.remaining,
60
+ exhausted: usage.exhausted,
61
+ };
62
+ }
63
+ async function parseBody(response) {
64
+ const contentType = response.headers.get("content-type") ?? "";
65
+ if (contentType.includes("application/json")) {
66
+ return await response.json();
67
+ }
68
+ const text = await response.text();
69
+ if (!text)
70
+ return undefined;
71
+ try {
72
+ return JSON.parse(text);
73
+ }
74
+ catch {
75
+ return text;
76
+ }
77
+ }
78
+ function normalizeError(status, body) {
79
+ const payload = body && typeof body === "object" ? body : undefined;
80
+ const fallbackMessage = status === 401
81
+ ? "Sign in to use chatbot."
82
+ : status === 429
83
+ ? "Chatbot usage limit reached."
84
+ : "Chatbot request failed.";
85
+ const message = payload?.message ?? fallbackMessage;
86
+ return new ChatbotClientError(status, message, payload?.error, payload?.usage);
87
+ }
88
+ async function ensureCsrfToken(fetcher, endpoint, options, baseHeaders) {
89
+ const cookieName = options.csrfCookieName ?? DEFAULT_CSRF_COOKIE_NAME;
90
+ const existing = readCookie(cookieName);
91
+ if (existing || options.bootstrapCsrf === false) {
92
+ return existing;
93
+ }
94
+ await fetcher(endpoint, {
95
+ method: "GET",
96
+ credentials: options.credentials ?? "include",
97
+ headers: baseHeaders,
98
+ });
99
+ return readCookie(cookieName);
100
+ }
101
+ export async function getChatbotUsage(options = {}) {
102
+ const fetcher = resolveFetch(options.fetchFn);
103
+ const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
104
+ const customHeaders = await resolveHeaders(options.headers);
105
+ const response = await fetcher(endpoint, {
106
+ method: "GET",
107
+ credentials: options.credentials ?? "include",
108
+ headers: {
109
+ Accept: "application/json",
110
+ ...(customHeaders ?? {}),
111
+ },
112
+ });
113
+ const body = await parseBody(response);
114
+ if (!response.ok) {
115
+ throw normalizeError(response.status, body);
116
+ }
117
+ if (!body || typeof body !== "object") {
118
+ throw new Error("Invalid chatbot usage response.");
119
+ }
120
+ const usage = normalizeUsage(body.usage);
121
+ if (!usage) {
122
+ throw new Error("Invalid chatbot usage response.");
123
+ }
124
+ return { usage };
125
+ }
126
+ export async function sendChatbotMessage(payload, options = {}) {
127
+ const fetcher = resolveFetch(options.fetchFn);
128
+ const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
129
+ const customHeaders = await resolveHeaders(options.headers);
130
+ const baseHeaders = {
131
+ Accept: "application/json",
132
+ "Content-Type": "application/json",
133
+ ...(customHeaders ?? {}),
134
+ };
135
+ const csrfToken = await ensureCsrfToken(fetcher, endpoint, options, baseHeaders);
136
+ const csrfHeader = options.csrfHeaderName ?? DEFAULT_CSRF_HEADER_NAME;
137
+ const requestHeaders = csrfToken
138
+ ? {
139
+ ...baseHeaders,
140
+ [csrfHeader]: csrfToken,
141
+ }
142
+ : baseHeaders;
143
+ const response = await fetcher(endpoint, {
144
+ method: "POST",
145
+ credentials: options.credentials ?? "include",
146
+ headers: requestHeaders,
147
+ body: JSON.stringify({
148
+ message: payload.message,
149
+ history: payload.history ?? [],
150
+ systemPrompt: payload.systemPrompt,
151
+ }),
152
+ });
153
+ const body = await parseBody(response);
154
+ if (!response.ok) {
155
+ throw normalizeError(response.status, body);
156
+ }
157
+ if (!body || typeof body !== "object") {
158
+ throw new Error("Invalid chatbot response.");
159
+ }
160
+ const content = body;
161
+ const reply = content.reply;
162
+ const model = content.model;
163
+ const usage = normalizeUsage(content.usage);
164
+ if (typeof reply !== "string" || typeof model !== "string" || !usage) {
165
+ throw new Error("Invalid chatbot response.");
166
+ }
167
+ return { reply, model, usage };
168
+ }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export { default as ChatBot } from "./chatbot.js";
2
+ export * from "./client.js";
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,cAAc,CAAC;AAClD,cAAc,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
1
  export { default as ChatBot } from "./chatbot.js";
2
+ export * from "./client.js";