@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 +19 -1
- package/dist/chatbot.d.ts +8 -5
- package/dist/chatbot.d.ts.map +1 -1
- package/dist/chatbot.js +142 -162
- package/dist/client.d.ts +41 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +168 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/styles/chatbot.module.css +94 -61
- package/dist-cjs/chatbot.d.ts +8 -5
- package/dist-cjs/chatbot.d.ts.map +1 -1
- package/dist-cjs/chatbot.js +141 -161
- package/dist-cjs/client.d.ts +41 -0
- package/dist-cjs/client.d.ts.map +1 -0
- package/dist-cjs/client.js +174 -0
- package/dist-cjs/index.d.ts +1 -0
- package/dist-cjs/index.d.ts.map +1 -1
- package/dist-cjs/index.js +15 -0
- package/dist-cjs/styles/chatbot.module.css +94 -61
- package/docs/adrs/index.md +4 -0
- package/package.json +1 -3
- package/src/chatbot.tsx +219 -255
- package/src/client.ts +254 -0
- package/src/index.ts +1 -0
- package/src/styles/chatbot.module.css +94 -61
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.
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
package/dist/chatbot.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chatbot.d.ts","sourceRoot":"","sources":["../src/chatbot.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
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,
|
|
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,
|
|
7
|
+
default: module.EmojiPicker,
|
|
5
8
|
})));
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
136
|
-
|
|
137
|
-
|
|
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 (
|
|
147
|
-
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
157
|
+
event.stopPropagation();
|
|
178
158
|
}
|
|
179
|
-
}, placeholder:
|
|
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
|
}
|
package/dist/client.d.ts
ADDED
|
@@ -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
package/dist/index.d.ts.map
CHANGED
|
@@ -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