chatbotlite 0.3.0 → 0.4.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.
- package/README.md +207 -223
- package/dist/client/index.cjs +292 -25
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +54 -4
- package/dist/client/index.d.ts +54 -4
- package/dist/client/index.js +292 -26
- package/dist/client/index.js.map +1 -1
- package/dist/core/index.cjs +89 -18
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +50 -5
- package/dist/core/index.d.ts +50 -5
- package/dist/core/index.js +86 -19
- package/dist/core/index.js.map +1 -1
- package/dist/index.cjs +347 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +343 -26
- package/dist/index.js.map +1 -1
- package/dist/judges-B0AAZLS9.d.ts +49 -0
- package/dist/judges-CSRIUVlF.d.cts +49 -0
- package/dist/react/index.cjs +1232 -110
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +56 -2
- package/dist/react/index.d.ts +56 -2
- package/dist/react/index.js +1232 -110
- package/dist/react/index.js.map +1 -1
- package/dist/{types-J7BXpiRU.d.cts → types-BFlAWQF4.d.cts} +16 -1
- package/dist/{types-J7BXpiRU.d.ts → types-BFlAWQF4.d.ts} +16 -1
- package/package.json +7 -3
- package/dist/types-4alyzg8O.d.cts +0 -16
- package/dist/types-4alyzg8O.d.ts +0 -16
package/dist/react/index.js
CHANGED
|
@@ -23,27 +23,16 @@ function buildSystemPrompt(knowledge) {
|
|
|
23
23
|
|
|
24
24
|
// src/core/guards.ts
|
|
25
25
|
var FORBIDDEN_PHRASES = [
|
|
26
|
-
"help is coming",
|
|
27
|
-
"someone is on the way",
|
|
28
|
-
"technician is on the way",
|
|
29
|
-
"provider is on the way",
|
|
30
|
-
"dispatching someone",
|
|
31
26
|
"i've booked",
|
|
27
|
+
// fake booking
|
|
32
28
|
"i have booked",
|
|
33
|
-
"reservation confirmed",
|
|
34
29
|
"your appointment is confirmed",
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
"i
|
|
40
|
-
|
|
41
|
-
"guaranteed delivery",
|
|
42
|
-
"guaranteed arrival",
|
|
43
|
-
"will arrive at",
|
|
44
|
-
"arriving at",
|
|
45
|
-
"i'll send",
|
|
46
|
-
"i will send"
|
|
30
|
+
// fake confirmation
|
|
31
|
+
"reservation confirmed",
|
|
32
|
+
"someone is on the way",
|
|
33
|
+
// false dispatch
|
|
34
|
+
"i guarantee"
|
|
35
|
+
// legal liability
|
|
47
36
|
];
|
|
48
37
|
function checkForbiddenPhrases(reply) {
|
|
49
38
|
const lower = reply.toLowerCase();
|
|
@@ -68,6 +57,33 @@ function stripForbidden(reply) {
|
|
|
68
57
|
return trimmed;
|
|
69
58
|
}
|
|
70
59
|
|
|
60
|
+
// src/core/judges.ts
|
|
61
|
+
async function runJudge(config, apiKey, endpointUrl, content, fetcher) {
|
|
62
|
+
const res = await fetcher(`${endpointUrl}/chat/completions`, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: {
|
|
65
|
+
Authorization: `Bearer ${apiKey}`,
|
|
66
|
+
"Content-Type": "application/json"
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify({
|
|
69
|
+
model: config.model,
|
|
70
|
+
messages: [
|
|
71
|
+
{ role: "system", content: config.prompt },
|
|
72
|
+
{ role: "user", content }
|
|
73
|
+
],
|
|
74
|
+
temperature: 0,
|
|
75
|
+
max_tokens: 10
|
|
76
|
+
})
|
|
77
|
+
});
|
|
78
|
+
if (!res.ok) {
|
|
79
|
+
return { decision: "PASS", raw: `judge HTTP ${res.status} \u2014 fail-open` };
|
|
80
|
+
}
|
|
81
|
+
const data = await res.json();
|
|
82
|
+
const raw = (data.choices?.[0]?.message?.content ?? "").trim().toUpperCase();
|
|
83
|
+
const decision = raw.startsWith("BLOCK") ? "BLOCK" : "PASS";
|
|
84
|
+
return { decision, raw };
|
|
85
|
+
}
|
|
86
|
+
|
|
71
87
|
// src/client/types.ts
|
|
72
88
|
var PROVIDER_NAMES = /* @__PURE__ */ new Set([
|
|
73
89
|
"openai",
|
|
@@ -88,22 +104,34 @@ function isKnownProvider(name) {
|
|
|
88
104
|
|
|
89
105
|
// src/client/providers.ts
|
|
90
106
|
var PROVIDER_ENDPOINTS = {
|
|
91
|
-
openai: { baseUrl: "https://api.openai.com/v1", defaultModel: "gpt-4o-mini" },
|
|
107
|
+
openai: { baseUrl: "https://api.openai.com/v1", defaultModel: "gpt-4o-mini", visionModel: "gpt-4o" },
|
|
92
108
|
deepseek: { baseUrl: "https://api.deepseek.com/v1", defaultModel: "deepseek-chat" },
|
|
93
|
-
groq: { baseUrl: "https://api.groq.com/openai/v1", defaultModel: "llama-3.3-70b-versatile" },
|
|
94
|
-
gemini: { baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai", defaultModel: "gemini-2.5-flash" },
|
|
95
|
-
anthropic: { baseUrl: "https://api.anthropic.com/v1", defaultModel: "claude-haiku-4-5" },
|
|
109
|
+
groq: { baseUrl: "https://api.groq.com/openai/v1", defaultModel: "llama-3.3-70b-versatile", visionModel: "llama-3.2-90b-vision-preview" },
|
|
110
|
+
gemini: { baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai", defaultModel: "gemini-2.5-flash", visionModel: "gemini-2.5-flash" },
|
|
111
|
+
anthropic: { baseUrl: "https://api.anthropic.com/v1", defaultModel: "claude-haiku-4-5", visionModel: "claude-haiku-4-5" },
|
|
96
112
|
cerebras: { baseUrl: "https://api.cerebras.ai/v1", defaultModel: "qwen-3-235b-a22b-instruct-2507" },
|
|
97
113
|
sambanova: { baseUrl: "https://api.sambanova.ai/v1", defaultModel: "Meta-Llama-3.3-70B-Instruct" },
|
|
98
114
|
fireworks: { baseUrl: "https://api.fireworks.ai/inference/v1", defaultModel: "accounts/fireworks/models/llama-v3p3-70b-instruct" },
|
|
99
115
|
mistral: { baseUrl: "https://api.mistral.ai/v1", defaultModel: "mistral-small-latest" },
|
|
100
|
-
openrouter: { baseUrl: "https://openrouter.ai/api/v1", defaultModel: "deepseek/deepseek-chat" },
|
|
101
|
-
moonshot: { baseUrl: "https://api.moonshot.ai/v1", defaultModel: "moonshot-v1-32k" }
|
|
116
|
+
openrouter: { baseUrl: "https://openrouter.ai/api/v1", defaultModel: "deepseek/deepseek-chat", visionModel: "openai/gpt-4o" },
|
|
117
|
+
moonshot: { baseUrl: "https://api.moonshot.ai/v1", defaultModel: "moonshot-v1-32k", visionModel: "moonshot-v1-32k-vision-preview" }
|
|
102
118
|
};
|
|
103
119
|
function isRetryableError(err) {
|
|
104
120
|
const msg = err instanceof Error ? err.message : String(err);
|
|
105
121
|
return /\b(429|rate.?limit|quota|exceed|5\d\d|timeout|ECONNRESET|fetch failed)\b/i.test(msg);
|
|
106
122
|
}
|
|
123
|
+
async function fileToDataUrl(file) {
|
|
124
|
+
const buf = new Uint8Array(await file.arrayBuffer());
|
|
125
|
+
const base64 = bufferToBase64(buf);
|
|
126
|
+
const mime = file.type || "application/octet-stream";
|
|
127
|
+
return `data:${mime};base64,${base64}`;
|
|
128
|
+
}
|
|
129
|
+
function bufferToBase64(buf) {
|
|
130
|
+
let bin = "";
|
|
131
|
+
for (let i = 0; i < buf.length; i++) bin += String.fromCharCode(buf[i]);
|
|
132
|
+
if (typeof btoa === "function") return btoa(bin);
|
|
133
|
+
return globalThis.Buffer.from(bin, "binary").toString("base64");
|
|
134
|
+
}
|
|
107
135
|
|
|
108
136
|
// src/client/chatbot.ts
|
|
109
137
|
var ChatBot = class {
|
|
@@ -112,6 +140,7 @@ var ChatBot = class {
|
|
|
112
140
|
fetcher;
|
|
113
141
|
timeoutMs;
|
|
114
142
|
cachedSystemPrompt;
|
|
143
|
+
guards;
|
|
115
144
|
constructor(init) {
|
|
116
145
|
if (!init.knowledge || typeof init.knowledge !== "string" || init.knowledge.trim().length === 0) {
|
|
117
146
|
throw new Error("chatbotlite: knowledge is required (a non-empty markdown string).");
|
|
@@ -121,8 +150,237 @@ var ChatBot = class {
|
|
|
121
150
|
this.fetcher = init.options?.fetch ?? globalThis.fetch.bind(globalThis);
|
|
122
151
|
this.timeoutMs = init.options?.timeoutMs ?? 3e4;
|
|
123
152
|
this.cachedSystemPrompt = buildSystemPrompt(init.knowledge);
|
|
153
|
+
this.guards = init.guards ?? {};
|
|
154
|
+
}
|
|
155
|
+
/** Run an LLM judge against content. Fail-open on errors. */
|
|
156
|
+
async judge(config, content) {
|
|
157
|
+
const endpoint = PROVIDER_ENDPOINTS[config.provider];
|
|
158
|
+
const key = this.keys[config.provider];
|
|
159
|
+
if (!key) {
|
|
160
|
+
return { decision: "PASS", raw: `judge provider ${config.provider} has no key \u2014 fail-open` };
|
|
161
|
+
}
|
|
162
|
+
const model = config.model ?? endpoint.defaultModel;
|
|
163
|
+
return runJudge(
|
|
164
|
+
{ provider: config.provider, model, prompt: config.prompt },
|
|
165
|
+
key,
|
|
166
|
+
endpoint.baseUrl,
|
|
167
|
+
content,
|
|
168
|
+
this.fetcher
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Stream a reply as SSE events. Returns a ReadableStream that yields tokens
|
|
173
|
+
* progressively. Designed to plug into Next.js/Hono/Express route handlers:
|
|
174
|
+
*
|
|
175
|
+
* ```ts
|
|
176
|
+
* export async function POST(req: Request) {
|
|
177
|
+
* const { message, transcript } = await req.json();
|
|
178
|
+
* const stream = await bot.replyStream(message, { history: transcript });
|
|
179
|
+
* return new Response(stream, {
|
|
180
|
+
* headers: { "Content-Type": "text/event-stream" }
|
|
181
|
+
* });
|
|
182
|
+
* }
|
|
183
|
+
* ```
|
|
184
|
+
*
|
|
185
|
+
* Events emitted (one per `data:` line, SSE format):
|
|
186
|
+
* event: token data: "<text fragment>"
|
|
187
|
+
* event: done data: {"reply":"...","usedProvider":"...","usedModel":"...","attempts":[...]}
|
|
188
|
+
* event: error data: {"message":"...","attempts":[...]}
|
|
189
|
+
*/
|
|
190
|
+
async replyStream(message, opts = {}) {
|
|
191
|
+
const systemPrompt = opts.systemPrompt ?? this.cachedSystemPrompt;
|
|
192
|
+
const messages = [
|
|
193
|
+
{ role: "system", content: systemPrompt },
|
|
194
|
+
...opts.history ?? [],
|
|
195
|
+
{ role: "user", content: message }
|
|
196
|
+
];
|
|
197
|
+
const steps = this.steps;
|
|
198
|
+
const fetcher = this.fetcher;
|
|
199
|
+
const keys = this.keys;
|
|
200
|
+
const timeoutMs = this.timeoutMs;
|
|
201
|
+
const encoder = new TextEncoder();
|
|
202
|
+
const sse = (event, data) => encoder.encode(`event: ${event}
|
|
203
|
+
data: ${data}
|
|
204
|
+
|
|
205
|
+
`);
|
|
206
|
+
return new ReadableStream({
|
|
207
|
+
async start(controller) {
|
|
208
|
+
const attempts = [];
|
|
209
|
+
let lastError;
|
|
210
|
+
let assembled = "";
|
|
211
|
+
for (const step of steps) {
|
|
212
|
+
const t0 = Date.now();
|
|
213
|
+
const endpoint = PROVIDER_ENDPOINTS[step.provider];
|
|
214
|
+
const key = keys[step.provider];
|
|
215
|
+
if (!key) {
|
|
216
|
+
attempts.push({ provider: step.provider, model: step.model, status: "error", error: "missing key", latencyMs: 0 });
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const abortCtrl = new AbortController();
|
|
220
|
+
const timer = setTimeout(() => abortCtrl.abort(), timeoutMs);
|
|
221
|
+
try {
|
|
222
|
+
const res = await fetcher(`${endpoint.baseUrl}/chat/completions`, {
|
|
223
|
+
method: "POST",
|
|
224
|
+
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
|
|
225
|
+
body: JSON.stringify({ model: step.model, messages, temperature: 0.3, max_tokens: 300, stream: true }),
|
|
226
|
+
signal: abortCtrl.signal
|
|
227
|
+
});
|
|
228
|
+
if (!res.ok) {
|
|
229
|
+
const body = await res.text();
|
|
230
|
+
throw new Error(`${res.status}: ${body.slice(0, 200)}`);
|
|
231
|
+
}
|
|
232
|
+
const reader = res.body.getReader();
|
|
233
|
+
const decoder = new TextDecoder();
|
|
234
|
+
let sseBuffer = "";
|
|
235
|
+
while (true) {
|
|
236
|
+
const { done, value } = await reader.read();
|
|
237
|
+
if (done) break;
|
|
238
|
+
sseBuffer += decoder.decode(value, { stream: true });
|
|
239
|
+
const lines = sseBuffer.split("\n");
|
|
240
|
+
sseBuffer = lines.pop() ?? "";
|
|
241
|
+
for (const line of lines) {
|
|
242
|
+
const trimmed = line.trim();
|
|
243
|
+
if (!trimmed.startsWith("data:")) continue;
|
|
244
|
+
const payload = trimmed.slice(5).trim();
|
|
245
|
+
if (payload === "[DONE]") continue;
|
|
246
|
+
try {
|
|
247
|
+
const obj = JSON.parse(payload);
|
|
248
|
+
const delta = obj.choices?.[0]?.delta?.content ?? obj.choices?.[0]?.delta?.reasoning_content ?? "";
|
|
249
|
+
if (delta) {
|
|
250
|
+
assembled += delta;
|
|
251
|
+
controller.enqueue(sse("token", JSON.stringify(delta)));
|
|
252
|
+
}
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
attempts.push({ provider: step.provider, model: step.model, status: "ok", latencyMs: Date.now() - t0 });
|
|
258
|
+
const guard = checkForbiddenPhrases(assembled);
|
|
259
|
+
const finalReply = guard.ok ? assembled : stripForbidden(assembled);
|
|
260
|
+
controller.enqueue(sse("done", JSON.stringify({
|
|
261
|
+
reply: finalReply,
|
|
262
|
+
usedProvider: step.provider,
|
|
263
|
+
usedModel: step.model,
|
|
264
|
+
guardWarnings: guard.violations,
|
|
265
|
+
attempts
|
|
266
|
+
})));
|
|
267
|
+
controller.close();
|
|
268
|
+
return;
|
|
269
|
+
} catch (err) {
|
|
270
|
+
lastError = err;
|
|
271
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
272
|
+
attempts.push({ provider: step.provider, model: step.model, status: "error", error: errMsg, latencyMs: Date.now() - t0 });
|
|
273
|
+
assembled = "";
|
|
274
|
+
if (!isRetryableError(err)) {
|
|
275
|
+
controller.enqueue(sse("error", JSON.stringify({ message: `${step.label} failed (non-retryable): ${errMsg}`, attempts })));
|
|
276
|
+
controller.close();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
} finally {
|
|
280
|
+
clearTimeout(timer);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const summary = attempts.map((a) => `${a.provider}/${a.model}:${a.error ?? "ok"}`).join(" \u2192 ");
|
|
284
|
+
controller.enqueue(sse("error", JSON.stringify({
|
|
285
|
+
message: `all chain steps failed. Trace: ${summary}. Last error: ${lastError instanceof Error ? lastError.message : String(lastError)}`,
|
|
286
|
+
attempts
|
|
287
|
+
})));
|
|
288
|
+
controller.close();
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Reply to a message with optional image attachments. Routes through vision-capable
|
|
294
|
+
* providers only — chain steps without `visionModel` are skipped (logged in attempts).
|
|
295
|
+
*
|
|
296
|
+
* Images are inlined as data: URLs. Keep total payload modest (<10MB).
|
|
297
|
+
*/
|
|
298
|
+
async replyWithMedia(message, opts = {}) {
|
|
299
|
+
const images = opts.images ?? [];
|
|
300
|
+
if (images.length === 0) {
|
|
301
|
+
return this.reply(message, opts);
|
|
302
|
+
}
|
|
303
|
+
const dataUrls = await Promise.all(images.map(fileToDataUrl));
|
|
304
|
+
const systemPrompt = opts.systemPrompt ?? this.cachedSystemPrompt;
|
|
305
|
+
const userContent = [];
|
|
306
|
+
if (message) userContent.push({ type: "text", text: message });
|
|
307
|
+
for (const url of dataUrls) userContent.push({ type: "image_url", image_url: { url } });
|
|
308
|
+
const messages = [
|
|
309
|
+
{ role: "system", content: systemPrompt },
|
|
310
|
+
...opts.history ?? [],
|
|
311
|
+
{ role: "user", content: userContent }
|
|
312
|
+
];
|
|
313
|
+
const attempts = [];
|
|
314
|
+
let lastError;
|
|
315
|
+
for (const step of this.steps) {
|
|
316
|
+
const endpoint = PROVIDER_ENDPOINTS[step.provider];
|
|
317
|
+
if (!endpoint.visionModel) {
|
|
318
|
+
attempts.push({ provider: step.provider, model: step.model, status: "error", error: "no vision support", latencyMs: 0 });
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const t0 = Date.now();
|
|
322
|
+
const visionStep = { provider: step.provider, model: endpoint.visionModel, label: `${step.provider}/${endpoint.visionModel}` };
|
|
323
|
+
try {
|
|
324
|
+
const key = this.keys[step.provider];
|
|
325
|
+
if (!key) throw new Error(`Missing API key for provider: ${step.provider}`);
|
|
326
|
+
const controller = new AbortController();
|
|
327
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
328
|
+
try {
|
|
329
|
+
const res = await this.fetcher(`${endpoint.baseUrl}/chat/completions`, {
|
|
330
|
+
method: "POST",
|
|
331
|
+
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
|
|
332
|
+
body: JSON.stringify({ model: visionStep.model, messages, temperature: 0.3, max_tokens: 400 }),
|
|
333
|
+
signal: controller.signal
|
|
334
|
+
});
|
|
335
|
+
if (!res.ok) {
|
|
336
|
+
const body = await res.text();
|
|
337
|
+
throw new Error(`${res.status}: ${body.slice(0, 200)}`);
|
|
338
|
+
}
|
|
339
|
+
const data = await res.json();
|
|
340
|
+
const reply = (data.choices?.[0]?.message?.content ?? "").trim();
|
|
341
|
+
if (!reply) throw new Error("empty vision reply");
|
|
342
|
+
attempts.push({ provider: visionStep.provider, model: visionStep.model, status: "ok", latencyMs: Date.now() - t0 });
|
|
343
|
+
const guard = checkForbiddenPhrases(reply);
|
|
344
|
+
const finalReply = guard.ok ? reply : stripForbidden(reply);
|
|
345
|
+
return {
|
|
346
|
+
reply: finalReply,
|
|
347
|
+
usedProvider: visionStep.provider,
|
|
348
|
+
usedModel: visionStep.model,
|
|
349
|
+
...data.usage ? { usage: data.usage } : {},
|
|
350
|
+
guardWarnings: guard.violations,
|
|
351
|
+
attempts
|
|
352
|
+
};
|
|
353
|
+
} finally {
|
|
354
|
+
clearTimeout(timer);
|
|
355
|
+
}
|
|
356
|
+
} catch (err) {
|
|
357
|
+
lastError = err;
|
|
358
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
359
|
+
attempts.push({ provider: visionStep.provider, model: visionStep.model, status: "error", error: errMsg, latencyMs: Date.now() - t0 });
|
|
360
|
+
if (!isRetryableError(err)) {
|
|
361
|
+
throw new Error(`chatbotlite: ${visionStep.label} vision failed (non-retryable). ${errMsg}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const summary = attempts.map((a) => `${a.provider}/${a.model}:${a.error ?? "ok"}`).join(" \u2192 ");
|
|
366
|
+
throw new Error(`chatbotlite: no vision-capable provider succeeded. Trace: ${summary}. Last error: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
|
124
367
|
}
|
|
125
368
|
async reply(message, opts = {}) {
|
|
369
|
+
let inputVerdict;
|
|
370
|
+
if (this.guards.inputJudge) {
|
|
371
|
+
inputVerdict = await this.judge(this.guards.inputJudge, message);
|
|
372
|
+
if (inputVerdict.decision === "BLOCK") {
|
|
373
|
+
return {
|
|
374
|
+
reply: "I can't process that request. Please ask in a different way.",
|
|
375
|
+
usedProvider: this.steps[0].provider,
|
|
376
|
+
usedModel: this.steps[0].model,
|
|
377
|
+
guardWarnings: [],
|
|
378
|
+
judges: { input: inputVerdict },
|
|
379
|
+
attempts: [],
|
|
380
|
+
blockedByInputJudge: true
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
}
|
|
126
384
|
const systemPrompt = opts.systemPrompt ?? this.cachedSystemPrompt;
|
|
127
385
|
const messages = [
|
|
128
386
|
{ role: "system", content: systemPrompt },
|
|
@@ -137,13 +395,21 @@ var ChatBot = class {
|
|
|
137
395
|
const result = await this.callProvider(step, messages);
|
|
138
396
|
attempts.push({ provider: step.provider, model: step.model, status: "ok", latencyMs: Date.now() - t0 });
|
|
139
397
|
const guard = checkForbiddenPhrases(result.reply);
|
|
140
|
-
|
|
398
|
+
let finalReply = guard.ok ? result.reply : stripForbidden(result.reply);
|
|
399
|
+
let outputVerdict;
|
|
400
|
+
if (this.guards.outputJudge) {
|
|
401
|
+
outputVerdict = await this.judge(this.guards.outputJudge, finalReply);
|
|
402
|
+
if (outputVerdict.decision === "BLOCK") {
|
|
403
|
+
finalReply = "Let me check with the owner and get back to you on that.";
|
|
404
|
+
}
|
|
405
|
+
}
|
|
141
406
|
return {
|
|
142
407
|
reply: finalReply,
|
|
143
408
|
usedProvider: step.provider,
|
|
144
409
|
usedModel: step.model,
|
|
145
410
|
...result.usage ? { usage: result.usage } : {},
|
|
146
411
|
guardWarnings: guard.violations,
|
|
412
|
+
...inputVerdict || outputVerdict ? { judges: { ...inputVerdict ? { input: inputVerdict } : {}, ...outputVerdict ? { output: outputVerdict } : {} } } : {},
|
|
147
413
|
attempts
|
|
148
414
|
};
|
|
149
415
|
} catch (err) {
|
|
@@ -228,6 +494,490 @@ function normalizeChainEntry(entry, keys) {
|
|
|
228
494
|
}
|
|
229
495
|
return { provider, model, label: `${provider}/${model}` };
|
|
230
496
|
}
|
|
497
|
+
|
|
498
|
+
// src/core/tools.ts
|
|
499
|
+
var MARKER_RE = /\[SKILL:(\w+)((?:\s+\w+=(?:"[^"]*"|[\w./@*+,:-]+))*)\s*\]/g;
|
|
500
|
+
var ARG_RE = /(\w+)=("([^"]*)"|([\w./@*+,:-]+))/g;
|
|
501
|
+
function coerce(value) {
|
|
502
|
+
if (value === "true") return true;
|
|
503
|
+
if (value === "false") return false;
|
|
504
|
+
if (/^-?\d+(?:\.\d+)?$/.test(value)) return Number(value);
|
|
505
|
+
return value;
|
|
506
|
+
}
|
|
507
|
+
function parseToolMarkers(text) {
|
|
508
|
+
const markers = [];
|
|
509
|
+
let m;
|
|
510
|
+
MARKER_RE.lastIndex = 0;
|
|
511
|
+
while ((m = MARKER_RE.exec(text)) !== null) {
|
|
512
|
+
const name = m[1];
|
|
513
|
+
const argsRaw = m[2] ?? "";
|
|
514
|
+
const args = {};
|
|
515
|
+
let a;
|
|
516
|
+
ARG_RE.lastIndex = 0;
|
|
517
|
+
while ((a = ARG_RE.exec(argsRaw)) !== null) {
|
|
518
|
+
const key = a[1];
|
|
519
|
+
const value = a[3] ?? a[4] ?? "";
|
|
520
|
+
args[key] = coerce(value);
|
|
521
|
+
}
|
|
522
|
+
markers.push({ name, args, raw: m[0] });
|
|
523
|
+
}
|
|
524
|
+
return markers;
|
|
525
|
+
}
|
|
526
|
+
function stripToolMarkers(text) {
|
|
527
|
+
return text.replace(MARKER_RE, "").replace(/\s+\n/g, "\n").trim();
|
|
528
|
+
}
|
|
529
|
+
var CLOUD_ICON = "M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12";
|
|
530
|
+
function UploadForReview(props) {
|
|
531
|
+
const {
|
|
532
|
+
purpose = "Files",
|
|
533
|
+
accept = "*",
|
|
534
|
+
maxMb = 10,
|
|
535
|
+
primary,
|
|
536
|
+
onPrimary,
|
|
537
|
+
border,
|
|
538
|
+
surface,
|
|
539
|
+
surfaceMuted,
|
|
540
|
+
textBody,
|
|
541
|
+
textMuted,
|
|
542
|
+
onSubmit,
|
|
543
|
+
onCancel,
|
|
544
|
+
submitting = false,
|
|
545
|
+
submitted = false
|
|
546
|
+
} = props;
|
|
547
|
+
const inputRef = useRef(null);
|
|
548
|
+
const [files, setFiles] = useState([]);
|
|
549
|
+
const [drag, setDrag] = useState(false);
|
|
550
|
+
function add(picked) {
|
|
551
|
+
const arr = Array.from(picked).filter((f) => f.size <= maxMb * 1024 * 1024);
|
|
552
|
+
setFiles((p) => [...p, ...arr]);
|
|
553
|
+
}
|
|
554
|
+
function remove(i) {
|
|
555
|
+
setFiles((p) => p.filter((_, j) => j !== i));
|
|
556
|
+
}
|
|
557
|
+
if (submitted) {
|
|
558
|
+
return /* @__PURE__ */ jsxs("div", { style: {
|
|
559
|
+
padding: "12px 16px",
|
|
560
|
+
borderRadius: 14,
|
|
561
|
+
background: surface,
|
|
562
|
+
border: `1px solid ${border}`,
|
|
563
|
+
boxShadow: "0 1px 2px rgba(15,23,42,0.04)",
|
|
564
|
+
color: textBody,
|
|
565
|
+
fontSize: 13
|
|
566
|
+
}, children: [
|
|
567
|
+
"\u2713 ",
|
|
568
|
+
files.length || 1,
|
|
569
|
+
" file",
|
|
570
|
+
files.length === 1 ? "" : "s",
|
|
571
|
+
" submitted for review."
|
|
572
|
+
] });
|
|
573
|
+
}
|
|
574
|
+
return /* @__PURE__ */ jsxs("div", { style: {
|
|
575
|
+
padding: 16,
|
|
576
|
+
borderRadius: 14,
|
|
577
|
+
background: surface,
|
|
578
|
+
border: `1px solid ${border}`,
|
|
579
|
+
boxShadow: "0 2px 8px -2px rgba(15,23,42,0.08)"
|
|
580
|
+
}, children: [
|
|
581
|
+
/* @__PURE__ */ jsxs("p", { style: { margin: 0, marginBottom: 12, fontSize: 13, fontWeight: 600, color: textBody }, children: [
|
|
582
|
+
"Upload your ",
|
|
583
|
+
purpose
|
|
584
|
+
] }),
|
|
585
|
+
/* @__PURE__ */ jsxs(
|
|
586
|
+
"div",
|
|
587
|
+
{
|
|
588
|
+
onClick: () => inputRef.current?.click(),
|
|
589
|
+
onDragOver: (e) => {
|
|
590
|
+
e.preventDefault();
|
|
591
|
+
setDrag(true);
|
|
592
|
+
},
|
|
593
|
+
onDragLeave: () => setDrag(false),
|
|
594
|
+
onDrop: (e) => {
|
|
595
|
+
e.preventDefault();
|
|
596
|
+
setDrag(false);
|
|
597
|
+
if (e.dataTransfer.files) add(e.dataTransfer.files);
|
|
598
|
+
},
|
|
599
|
+
style: {
|
|
600
|
+
border: `1.5px dashed ${drag ? primary : border}`,
|
|
601
|
+
borderRadius: 12,
|
|
602
|
+
padding: "20px 12px",
|
|
603
|
+
textAlign: "center",
|
|
604
|
+
cursor: "pointer",
|
|
605
|
+
background: drag ? `${primary}0a` : surfaceMuted,
|
|
606
|
+
transition: "border-color 120ms ease, background 120ms ease"
|
|
607
|
+
},
|
|
608
|
+
children: [
|
|
609
|
+
/* @__PURE__ */ jsx("svg", { width: "28", height: "28", viewBox: "0 0 24 24", fill: "none", stroke: primary, strokeWidth: "1.5", style: { display: "block", margin: "0 auto 6px" }, children: /* @__PURE__ */ jsx("path", { d: CLOUD_ICON, strokeLinecap: "round", strokeLinejoin: "round" }) }),
|
|
610
|
+
/* @__PURE__ */ jsxs("p", { style: { margin: 0, fontSize: 13, fontWeight: 500, color: textBody }, children: [
|
|
611
|
+
"Drop file",
|
|
612
|
+
maxMb > 0 ? "s" : "",
|
|
613
|
+
" here or click to browse"
|
|
614
|
+
] }),
|
|
615
|
+
/* @__PURE__ */ jsxs("p", { style: { margin: "4px 0 0", fontSize: 11, color: textMuted }, children: [
|
|
616
|
+
accept === "*" ? "Any file" : accept,
|
|
617
|
+
" \xB7 max ",
|
|
618
|
+
maxMb,
|
|
619
|
+
"MB"
|
|
620
|
+
] }),
|
|
621
|
+
/* @__PURE__ */ jsx(
|
|
622
|
+
"input",
|
|
623
|
+
{
|
|
624
|
+
ref: inputRef,
|
|
625
|
+
type: "file",
|
|
626
|
+
multiple: true,
|
|
627
|
+
accept,
|
|
628
|
+
style: { display: "none" },
|
|
629
|
+
onChange: (e) => {
|
|
630
|
+
if (e.target.files) add(e.target.files);
|
|
631
|
+
e.target.value = "";
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
)
|
|
635
|
+
]
|
|
636
|
+
}
|
|
637
|
+
),
|
|
638
|
+
files.length > 0 && /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, marginTop: 10 }, children: files.map((f, i) => /* @__PURE__ */ jsxs(
|
|
639
|
+
"span",
|
|
640
|
+
{
|
|
641
|
+
style: {
|
|
642
|
+
display: "inline-flex",
|
|
643
|
+
alignItems: "center",
|
|
644
|
+
gap: 6,
|
|
645
|
+
padding: "4px 8px 4px 10px",
|
|
646
|
+
borderRadius: 8,
|
|
647
|
+
background: surfaceMuted,
|
|
648
|
+
border: `1px solid ${border}`,
|
|
649
|
+
fontSize: 12,
|
|
650
|
+
color: textBody,
|
|
651
|
+
maxWidth: 220
|
|
652
|
+
},
|
|
653
|
+
children: [
|
|
654
|
+
/* @__PURE__ */ jsxs("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: [
|
|
655
|
+
"\u{1F4C4} ",
|
|
656
|
+
f.name
|
|
657
|
+
] }),
|
|
658
|
+
/* @__PURE__ */ jsx(
|
|
659
|
+
"button",
|
|
660
|
+
{
|
|
661
|
+
onClick: () => remove(i),
|
|
662
|
+
"aria-label": `Remove ${f.name}`,
|
|
663
|
+
style: { background: "transparent", border: "none", cursor: "pointer", color: textMuted, fontSize: 14, lineHeight: 1, padding: 0 },
|
|
664
|
+
children: "\xD7"
|
|
665
|
+
}
|
|
666
|
+
)
|
|
667
|
+
]
|
|
668
|
+
},
|
|
669
|
+
`${f.name}-${i}`
|
|
670
|
+
)) }),
|
|
671
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, marginTop: 12 }, children: [
|
|
672
|
+
onCancel && /* @__PURE__ */ jsx(
|
|
673
|
+
"button",
|
|
674
|
+
{
|
|
675
|
+
onClick: onCancel,
|
|
676
|
+
disabled: submitting,
|
|
677
|
+
style: {
|
|
678
|
+
padding: "8px 14px",
|
|
679
|
+
borderRadius: 10,
|
|
680
|
+
background: "transparent",
|
|
681
|
+
color: textMuted,
|
|
682
|
+
border: `1px solid ${border}`,
|
|
683
|
+
fontSize: 13,
|
|
684
|
+
cursor: submitting ? "default" : "pointer"
|
|
685
|
+
},
|
|
686
|
+
children: "Cancel"
|
|
687
|
+
}
|
|
688
|
+
),
|
|
689
|
+
/* @__PURE__ */ jsx(
|
|
690
|
+
"button",
|
|
691
|
+
{
|
|
692
|
+
onClick: () => void onSubmit(files),
|
|
693
|
+
disabled: submitting || files.length === 0,
|
|
694
|
+
style: {
|
|
695
|
+
flex: 1,
|
|
696
|
+
padding: "9px 16px",
|
|
697
|
+
borderRadius: 10,
|
|
698
|
+
background: primary,
|
|
699
|
+
color: onPrimary,
|
|
700
|
+
border: "none",
|
|
701
|
+
fontSize: 13,
|
|
702
|
+
fontWeight: 600,
|
|
703
|
+
cursor: submitting || files.length === 0 ? "default" : "pointer",
|
|
704
|
+
opacity: submitting || files.length === 0 ? 0.4 : 1
|
|
705
|
+
},
|
|
706
|
+
children: submitting ? "Submitting\u2026" : "Submit"
|
|
707
|
+
}
|
|
708
|
+
)
|
|
709
|
+
] })
|
|
710
|
+
] });
|
|
711
|
+
}
|
|
712
|
+
function ScheduleCallback(props) {
|
|
713
|
+
const {
|
|
714
|
+
durationMin = 15,
|
|
715
|
+
timezone = "UTC",
|
|
716
|
+
primary,
|
|
717
|
+
onPrimary,
|
|
718
|
+
border,
|
|
719
|
+
surface,
|
|
720
|
+
surfaceMuted,
|
|
721
|
+
textBody,
|
|
722
|
+
textMuted,
|
|
723
|
+
getAvailableSlots,
|
|
724
|
+
onConfirm,
|
|
725
|
+
submitting = false,
|
|
726
|
+
submitted = false,
|
|
727
|
+
submittedSlot
|
|
728
|
+
} = props;
|
|
729
|
+
const [slots, setSlots] = useState([]);
|
|
730
|
+
const [loading, setLoading] = useState(true);
|
|
731
|
+
const [selected, setSelected] = useState(null);
|
|
732
|
+
useEffect(() => {
|
|
733
|
+
let cancelled = false;
|
|
734
|
+
setLoading(true);
|
|
735
|
+
getAvailableSlots({ durationMin, timezone }).then((s) => {
|
|
736
|
+
if (cancelled) return;
|
|
737
|
+
setSlots(s);
|
|
738
|
+
setLoading(false);
|
|
739
|
+
}).catch(() => {
|
|
740
|
+
if (!cancelled) setLoading(false);
|
|
741
|
+
});
|
|
742
|
+
return () => {
|
|
743
|
+
cancelled = true;
|
|
744
|
+
};
|
|
745
|
+
}, [durationMin, timezone, getAvailableSlots]);
|
|
746
|
+
if (submitted && submittedSlot) {
|
|
747
|
+
return /* @__PURE__ */ jsxs("div", { style: {
|
|
748
|
+
padding: "12px 16px",
|
|
749
|
+
borderRadius: 14,
|
|
750
|
+
background: surface,
|
|
751
|
+
border: `1px solid ${border}`,
|
|
752
|
+
fontSize: 13,
|
|
753
|
+
color: textBody
|
|
754
|
+
}, children: [
|
|
755
|
+
"\u2713 Booked for ",
|
|
756
|
+
formatSlot(submittedSlot)
|
|
757
|
+
] });
|
|
758
|
+
}
|
|
759
|
+
return /* @__PURE__ */ jsxs("div", { style: {
|
|
760
|
+
padding: 16,
|
|
761
|
+
borderRadius: 14,
|
|
762
|
+
background: surface,
|
|
763
|
+
border: `1px solid ${border}`,
|
|
764
|
+
boxShadow: "0 2px 8px -2px rgba(15,23,42,0.08)"
|
|
765
|
+
}, children: [
|
|
766
|
+
/* @__PURE__ */ jsxs("p", { style: { margin: 0, marginBottom: 12, fontSize: 13, fontWeight: 600, color: textBody }, children: [
|
|
767
|
+
"Pick a ",
|
|
768
|
+
durationMin,
|
|
769
|
+
"-minute slot"
|
|
770
|
+
] }),
|
|
771
|
+
loading ? /* @__PURE__ */ jsx("p", { style: { fontSize: 12, color: textMuted }, children: "Loading availability\u2026" }) : slots.length === 0 ? /* @__PURE__ */ jsx("p", { style: { fontSize: 12, color: textMuted }, children: "No slots available \u2014 the owner will follow up." }) : /* @__PURE__ */ jsx("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 6 }, children: slots.map((slot) => {
|
|
772
|
+
const isSel = selected === slot;
|
|
773
|
+
return /* @__PURE__ */ jsx(
|
|
774
|
+
"button",
|
|
775
|
+
{
|
|
776
|
+
onClick: () => setSelected(slot),
|
|
777
|
+
style: {
|
|
778
|
+
padding: "8px 10px",
|
|
779
|
+
borderRadius: 10,
|
|
780
|
+
fontSize: 12,
|
|
781
|
+
fontWeight: isSel ? 600 : 500,
|
|
782
|
+
cursor: "pointer",
|
|
783
|
+
background: isSel ? `${primary}0d` : surfaceMuted,
|
|
784
|
+
color: textBody,
|
|
785
|
+
border: `1px solid ${isSel ? primary : border}`,
|
|
786
|
+
transition: "border-color 120ms ease, background 120ms ease"
|
|
787
|
+
},
|
|
788
|
+
children: formatSlot(slot)
|
|
789
|
+
},
|
|
790
|
+
slot
|
|
791
|
+
);
|
|
792
|
+
}) }),
|
|
793
|
+
/* @__PURE__ */ jsx(
|
|
794
|
+
"button",
|
|
795
|
+
{
|
|
796
|
+
onClick: () => {
|
|
797
|
+
if (selected) void onConfirm(selected);
|
|
798
|
+
},
|
|
799
|
+
disabled: !selected || submitting,
|
|
800
|
+
style: {
|
|
801
|
+
width: "100%",
|
|
802
|
+
marginTop: 12,
|
|
803
|
+
padding: "9px 16px",
|
|
804
|
+
borderRadius: 10,
|
|
805
|
+
background: primary,
|
|
806
|
+
color: onPrimary,
|
|
807
|
+
border: "none",
|
|
808
|
+
fontSize: 13,
|
|
809
|
+
fontWeight: 600,
|
|
810
|
+
cursor: selected && !submitting ? "pointer" : "default",
|
|
811
|
+
opacity: selected && !submitting ? 1 : 0.4
|
|
812
|
+
},
|
|
813
|
+
children: submitting ? "Booking\u2026" : "Confirm"
|
|
814
|
+
}
|
|
815
|
+
)
|
|
816
|
+
] });
|
|
817
|
+
}
|
|
818
|
+
function formatSlot(iso) {
|
|
819
|
+
try {
|
|
820
|
+
const d = new Date(iso);
|
|
821
|
+
return d.toLocaleString(void 0, {
|
|
822
|
+
weekday: "short",
|
|
823
|
+
month: "short",
|
|
824
|
+
day: "numeric",
|
|
825
|
+
hour: "numeric",
|
|
826
|
+
minute: "2-digit"
|
|
827
|
+
});
|
|
828
|
+
} catch {
|
|
829
|
+
return iso;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
function formatAmount(amountMinor, currency = "USD") {
|
|
833
|
+
try {
|
|
834
|
+
return new Intl.NumberFormat(void 0, {
|
|
835
|
+
style: "currency",
|
|
836
|
+
currency: currency.toUpperCase(),
|
|
837
|
+
minimumFractionDigits: 2
|
|
838
|
+
}).format(amountMinor / 100);
|
|
839
|
+
} catch {
|
|
840
|
+
return `${currency.toUpperCase()} ${(amountMinor / 100).toFixed(2)}`;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
function RequestPayment(props) {
|
|
844
|
+
const {
|
|
845
|
+
amount,
|
|
846
|
+
currency = "USD",
|
|
847
|
+
reason,
|
|
848
|
+
primary,
|
|
849
|
+
border,
|
|
850
|
+
surface,
|
|
851
|
+
textBody,
|
|
852
|
+
textMuted,
|
|
853
|
+
showInterac = true,
|
|
854
|
+
stripeLink,
|
|
855
|
+
onPick,
|
|
856
|
+
submitting = false,
|
|
857
|
+
submitted = false,
|
|
858
|
+
submittedMethod
|
|
859
|
+
} = props;
|
|
860
|
+
const formatted = formatAmount(amount, currency);
|
|
861
|
+
if (submitted) {
|
|
862
|
+
return /* @__PURE__ */ jsxs("div", { style: {
|
|
863
|
+
padding: "12px 16px",
|
|
864
|
+
borderRadius: 14,
|
|
865
|
+
background: surface,
|
|
866
|
+
border: `1px solid ${border}`,
|
|
867
|
+
fontSize: 13,
|
|
868
|
+
color: textBody
|
|
869
|
+
}, children: [
|
|
870
|
+
"\u2713 ",
|
|
871
|
+
submittedMethod === "interac" ? "Interac request sent \u2014 instructions to follow" : "Payment opened",
|
|
872
|
+
" \xB7 ",
|
|
873
|
+
formatted
|
|
874
|
+
] });
|
|
875
|
+
}
|
|
876
|
+
return /* @__PURE__ */ jsxs("div", { style: {
|
|
877
|
+
padding: 16,
|
|
878
|
+
borderRadius: 14,
|
|
879
|
+
background: surface,
|
|
880
|
+
border: `1px solid ${border}`,
|
|
881
|
+
boxShadow: "0 2px 8px -2px rgba(15,23,42,0.08)"
|
|
882
|
+
}, children: [
|
|
883
|
+
/* @__PURE__ */ jsxs("div", { style: { marginBottom: 12 }, children: [
|
|
884
|
+
/* @__PURE__ */ jsx("p", { style: { margin: 0, fontSize: 13, fontWeight: 600, color: textBody }, children: reason || "Payment required" }),
|
|
885
|
+
/* @__PURE__ */ jsx("p", { style: { margin: "2px 0 0", fontSize: 20, fontWeight: 700, color: textBody, letterSpacing: "-0.01em" }, children: formatted })
|
|
886
|
+
] }),
|
|
887
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
|
|
888
|
+
showInterac && /* @__PURE__ */ jsxs(
|
|
889
|
+
"button",
|
|
890
|
+
{
|
|
891
|
+
onClick: () => void onPick("interac"),
|
|
892
|
+
disabled: submitting,
|
|
893
|
+
style: {
|
|
894
|
+
display: "flex",
|
|
895
|
+
alignItems: "center",
|
|
896
|
+
gap: 12,
|
|
897
|
+
padding: 12,
|
|
898
|
+
borderRadius: 12,
|
|
899
|
+
background: surface,
|
|
900
|
+
border: `1px solid ${border}`,
|
|
901
|
+
cursor: submitting ? "default" : "pointer",
|
|
902
|
+
opacity: submitting ? 0.5 : 1,
|
|
903
|
+
textAlign: "left",
|
|
904
|
+
transition: "border-color 120ms ease"
|
|
905
|
+
},
|
|
906
|
+
onMouseEnter: (e) => {
|
|
907
|
+
e.currentTarget.style.borderColor = primary;
|
|
908
|
+
},
|
|
909
|
+
onMouseLeave: (e) => {
|
|
910
|
+
e.currentTarget.style.borderColor = border;
|
|
911
|
+
},
|
|
912
|
+
children: [
|
|
913
|
+
/* @__PURE__ */ jsx("div", { style: {
|
|
914
|
+
width: 40,
|
|
915
|
+
height: 40,
|
|
916
|
+
borderRadius: 10,
|
|
917
|
+
background: "#FFBE2E1f",
|
|
918
|
+
display: "flex",
|
|
919
|
+
alignItems: "center",
|
|
920
|
+
justifyContent: "center",
|
|
921
|
+
fontSize: 18,
|
|
922
|
+
fontWeight: 700,
|
|
923
|
+
color: "#B8860B",
|
|
924
|
+
flexShrink: 0
|
|
925
|
+
}, children: "$" }),
|
|
926
|
+
/* @__PURE__ */ jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [
|
|
927
|
+
/* @__PURE__ */ jsx("p", { style: { margin: 0, fontSize: 13, fontWeight: 600, color: textBody }, children: "Interac e-Transfer" }),
|
|
928
|
+
/* @__PURE__ */ jsx("p", { style: { margin: "2px 0 0", fontSize: 11, color: textMuted }, children: "Instant, no fees" })
|
|
929
|
+
] })
|
|
930
|
+
]
|
|
931
|
+
}
|
|
932
|
+
),
|
|
933
|
+
/* @__PURE__ */ jsxs(
|
|
934
|
+
"button",
|
|
935
|
+
{
|
|
936
|
+
onClick: () => {
|
|
937
|
+
if (stripeLink) window.open(stripeLink, "_blank", "noopener");
|
|
938
|
+
void onPick("stripe");
|
|
939
|
+
},
|
|
940
|
+
disabled: submitting,
|
|
941
|
+
style: {
|
|
942
|
+
display: "flex",
|
|
943
|
+
alignItems: "center",
|
|
944
|
+
gap: 12,
|
|
945
|
+
padding: 12,
|
|
946
|
+
borderRadius: 12,
|
|
947
|
+
background: surface,
|
|
948
|
+
border: `1px solid ${border}`,
|
|
949
|
+
cursor: submitting ? "default" : "pointer",
|
|
950
|
+
opacity: submitting ? 0.5 : 1,
|
|
951
|
+
textAlign: "left",
|
|
952
|
+
transition: "border-color 120ms ease"
|
|
953
|
+
},
|
|
954
|
+
onMouseEnter: (e) => {
|
|
955
|
+
e.currentTarget.style.borderColor = primary;
|
|
956
|
+
},
|
|
957
|
+
onMouseLeave: (e) => {
|
|
958
|
+
e.currentTarget.style.borderColor = border;
|
|
959
|
+
},
|
|
960
|
+
children: [
|
|
961
|
+
/* @__PURE__ */ jsx("div", { style: {
|
|
962
|
+
width: 40,
|
|
963
|
+
height: 40,
|
|
964
|
+
borderRadius: 10,
|
|
965
|
+
background: "#635BFF1a",
|
|
966
|
+
display: "flex",
|
|
967
|
+
alignItems: "center",
|
|
968
|
+
justifyContent: "center",
|
|
969
|
+
flexShrink: 0
|
|
970
|
+
}, children: /* @__PURE__ */ jsx("span", { style: { color: "#635BFF", fontSize: 14, fontWeight: 700 }, children: "\u{1F4B3}" }) }),
|
|
971
|
+
/* @__PURE__ */ jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [
|
|
972
|
+
/* @__PURE__ */ jsx("p", { style: { margin: 0, fontSize: 13, fontWeight: 600, color: textBody }, children: "Pay by card" }),
|
|
973
|
+
/* @__PURE__ */ jsx("p", { style: { margin: "2px 0 0", fontSize: 11, color: textMuted }, children: "Visa \xB7 Mastercard \xB7 Amex" })
|
|
974
|
+
] })
|
|
975
|
+
]
|
|
976
|
+
}
|
|
977
|
+
)
|
|
978
|
+
] })
|
|
979
|
+
] });
|
|
980
|
+
}
|
|
231
981
|
var BOLT = "\u26A1";
|
|
232
982
|
var DEFAULT_PRIMARY = "#0f172a";
|
|
233
983
|
var DEFAULT_ON_PRIMARY = "#ffffff";
|
|
@@ -244,7 +994,9 @@ var KEYFRAMES = `
|
|
|
244
994
|
@keyframes chatbotlite-slide { 0% { opacity: 0; transform: translateY(16px) scale(0.98); } 100% { opacity: 1; transform: translateY(0) scale(1); } }
|
|
245
995
|
@keyframes chatbotlite-fade-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
|
|
246
996
|
@keyframes chatbotlite-dot { 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; } 30% { transform: translateY(-4px); opacity: 1; } }
|
|
247
|
-
|
|
997
|
+
@keyframes chatbotlite-cursor { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }
|
|
998
|
+
@keyframes chatbotlite-pulse { 0%, 100% { box-shadow: 0 12px 28px -8px rgba(15,23,42,0.32), 0 4px 8px -2px rgba(15,23,42,0.12); } 50% { box-shadow: 0 14px 32px -8px rgba(15,23,42,0.36), 0 6px 12px -2px rgba(15,23,42,0.16); } }
|
|
999
|
+
.chatbotlite-launcher { transition: transform 180ms cubic-bezier(0.4, 0, 0.2, 1), box-shadow 180ms cubic-bezier(0.4, 0, 0.2, 1); animation: chatbotlite-pop 320ms cubic-bezier(0.34, 1.56, 0.64, 1), chatbotlite-pulse 3.6s ease-in-out 1.2s 2; }
|
|
248
1000
|
.chatbotlite-launcher:hover { transform: translateY(-2px) scale(1.04); }
|
|
249
1001
|
.chatbotlite-launcher:active { transform: translateY(0) scale(0.98); }
|
|
250
1002
|
.chatbotlite-close { transition: background 120ms ease; }
|
|
@@ -255,6 +1007,9 @@ var KEYFRAMES = `
|
|
|
255
1007
|
.chatbotlite-input:focus { box-shadow: 0 0 0 3px rgba(15,23,42,0.06); }
|
|
256
1008
|
.chatbotlite-msg { animation: chatbotlite-fade-in 220ms cubic-bezier(0.4, 0, 0.2, 1); }
|
|
257
1009
|
.chatbotlite-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: ${TEXT_FAINT}; margin-right: 4px; animation: chatbotlite-dot 1.2s ease-in-out infinite; }
|
|
1010
|
+
.chatbotlite-cursor { display: inline-block; width: 2px; height: 1em; background: currentColor; vertical-align: text-bottom; margin-left: 1px; animation: chatbotlite-cursor 1s steps(1) infinite; }
|
|
1011
|
+
.chatbotlite-attach-btn:hover:not(:disabled), .chatbotlite-voice-btn:hover:not(:disabled) { background: ${BORDER}; }
|
|
1012
|
+
.chatbotlite-attach-btn:active:not(:disabled), .chatbotlite-voice-btn:active:not(:disabled) { transform: scale(0.96); }
|
|
258
1013
|
.chatbotlite-dot:nth-child(2) { animation-delay: 0.15s; }
|
|
259
1014
|
.chatbotlite-dot:nth-child(3) { animation-delay: 0.3s; margin-right: 0; }
|
|
260
1015
|
.chatbotlite-brand:hover { color: ${TEXT_MUTED} !important; }
|
|
@@ -281,14 +1036,105 @@ function ChatWidget(props) {
|
|
|
281
1036
|
const resolvedGreeting = greeting ?? "Hi! How can we help?";
|
|
282
1037
|
const primary = themeOverrides?.primary ?? DEFAULT_PRIMARY;
|
|
283
1038
|
const onPrimary = themeOverrides?.onPrimary ?? DEFAULT_ON_PRIMARY;
|
|
1039
|
+
const attachCfg = props.attach;
|
|
1040
|
+
const attachEnabled = attachCfg?.enabled === true;
|
|
1041
|
+
const acceptAttr = attachCfg?.accept?.join(",");
|
|
1042
|
+
const maxSizeMb = attachCfg?.maxSizeMb ?? 10;
|
|
1043
|
+
const maxFiles = attachCfg?.maxFiles ?? 5;
|
|
1044
|
+
const voiceCfg = props.voice;
|
|
1045
|
+
const voiceEnabled = voiceCfg?.enabled === true;
|
|
1046
|
+
const voiceLang = voiceCfg?.lang ?? "en-US";
|
|
1047
|
+
const speechSupported = typeof window !== "undefined" && (Boolean(window.SpeechRecognition) || Boolean(window.webkitSpeechRecognition));
|
|
284
1048
|
const [open, setOpen] = useState(false);
|
|
285
1049
|
const [messages, setMessages] = useState([
|
|
286
1050
|
{ id: "g0", role: "assistant", content: resolvedGreeting, ts: Date.now() }
|
|
287
1051
|
]);
|
|
288
1052
|
const [input, setInput] = useState("");
|
|
289
1053
|
const [sending, setSending] = useState(false);
|
|
1054
|
+
const [files, setFiles] = useState([]);
|
|
290
1055
|
const scrollRef = useRef(null);
|
|
291
1056
|
const inputRef = useRef(null);
|
|
1057
|
+
const fileInputRef = useRef(null);
|
|
1058
|
+
const [pendingTools, setPendingTools] = useState([]);
|
|
1059
|
+
const tools = props.tools ?? {};
|
|
1060
|
+
const [voiceListening, setVoiceListening] = useState(false);
|
|
1061
|
+
const recognitionRef = useRef(null);
|
|
1062
|
+
async function continueAfterTool(toolName, result) {
|
|
1063
|
+
const ctxMsg = `[Tool ${toolName} result: ${JSON.stringify(result)}]`;
|
|
1064
|
+
setSending(true);
|
|
1065
|
+
const assistantId = `a${Date.now()}`;
|
|
1066
|
+
setMessages((prev) => [...prev, { id: assistantId, role: "assistant", content: "", ts: Date.now() }]);
|
|
1067
|
+
const appendToken = (tok) => {
|
|
1068
|
+
setMessages(
|
|
1069
|
+
(prev) => prev.map((m) => m.id === assistantId ? { ...m, content: m.content + tok } : m)
|
|
1070
|
+
);
|
|
1071
|
+
};
|
|
1072
|
+
try {
|
|
1073
|
+
const history = messages.filter((m) => m.role !== "system").map((m) => ({ role: m.role, content: m.content }));
|
|
1074
|
+
const reply = isEndpointMode ? await fetchReplyFromEndpoint(ctxMsg, history, [], appendToken) : (await bot.reply(ctxMsg, { history })).reply;
|
|
1075
|
+
const markers = parseToolMarkers(reply);
|
|
1076
|
+
const cleanReply = stripToolMarkers(reply);
|
|
1077
|
+
setMessages(
|
|
1078
|
+
(prev) => prev.map((m) => m.id === assistantId ? { ...m, content: cleanReply } : m)
|
|
1079
|
+
);
|
|
1080
|
+
if (markers.length > 0) {
|
|
1081
|
+
setPendingTools((prev) => [
|
|
1082
|
+
...prev,
|
|
1083
|
+
...markers.map((marker) => ({ messageId: assistantId, marker, status: "pending" }))
|
|
1084
|
+
]);
|
|
1085
|
+
}
|
|
1086
|
+
} catch (err) {
|
|
1087
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1088
|
+
setMessages(
|
|
1089
|
+
(prev) => prev.map(
|
|
1090
|
+
(m) => m.id === assistantId ? { ...m, content: `Sorry \u2014 something went wrong. (${errMsg})` } : m
|
|
1091
|
+
)
|
|
1092
|
+
);
|
|
1093
|
+
} finally {
|
|
1094
|
+
setSending(false);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
async function handleToolSubmit(toolName, idx, result) {
|
|
1098
|
+
setPendingTools(
|
|
1099
|
+
(prev) => prev.map((p, i) => i === idx ? { ...p, status: "submitted", result } : p)
|
|
1100
|
+
);
|
|
1101
|
+
await continueAfterTool(toolName, result);
|
|
1102
|
+
}
|
|
1103
|
+
function toggleVoice() {
|
|
1104
|
+
if (!speechSupported) return;
|
|
1105
|
+
if (voiceListening) {
|
|
1106
|
+
recognitionRef.current?.stop();
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
const Ctor = window.SpeechRecognition ?? window.webkitSpeechRecognition;
|
|
1110
|
+
if (!Ctor) return;
|
|
1111
|
+
const rec = new Ctor();
|
|
1112
|
+
rec.lang = voiceLang;
|
|
1113
|
+
rec.continuous = false;
|
|
1114
|
+
rec.interimResults = true;
|
|
1115
|
+
rec.onresult = (e) => {
|
|
1116
|
+
let transcript = "";
|
|
1117
|
+
for (let i = e.resultIndex; i < e.results.length; i++) {
|
|
1118
|
+
transcript += e.results[i][0].transcript;
|
|
1119
|
+
}
|
|
1120
|
+
setInput(transcript);
|
|
1121
|
+
};
|
|
1122
|
+
rec.onend = () => setVoiceListening(false);
|
|
1123
|
+
rec.onerror = () => setVoiceListening(false);
|
|
1124
|
+
recognitionRef.current = rec;
|
|
1125
|
+
setVoiceListening(true);
|
|
1126
|
+
rec.start();
|
|
1127
|
+
}
|
|
1128
|
+
function addFiles(picked) {
|
|
1129
|
+
const arr = Array.from(picked).filter((f) => f.size <= maxSizeMb * 1024 * 1024);
|
|
1130
|
+
setFiles((prev) => {
|
|
1131
|
+
const combined = [...prev, ...arr];
|
|
1132
|
+
return combined.slice(0, maxFiles);
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
function removeFile(idx) {
|
|
1136
|
+
setFiles((prev) => prev.filter((_, i) => i !== idx));
|
|
1137
|
+
}
|
|
292
1138
|
useEffect(() => {
|
|
293
1139
|
ensureStyles();
|
|
294
1140
|
}, []);
|
|
@@ -309,13 +1155,69 @@ function ChatWidget(props) {
|
|
|
309
1155
|
}
|
|
310
1156
|
return void 0;
|
|
311
1157
|
}, [open]);
|
|
312
|
-
async function fetchReplyFromEndpoint(text, history) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
1158
|
+
async function fetchReplyFromEndpoint(text, history, attachedFiles, onToken) {
|
|
1159
|
+
let body;
|
|
1160
|
+
const headers = { Accept: "text/event-stream, application/json" };
|
|
1161
|
+
if (attachedFiles.length > 0) {
|
|
1162
|
+
const form = new FormData();
|
|
1163
|
+
form.append("message", text);
|
|
1164
|
+
form.append("transcript", JSON.stringify(history));
|
|
1165
|
+
for (const f of attachedFiles) form.append("attachments", f, f.name);
|
|
1166
|
+
body = form;
|
|
1167
|
+
} else {
|
|
1168
|
+
headers["Content-Type"] = "application/json";
|
|
1169
|
+
body = JSON.stringify({ message: text, transcript: history });
|
|
1170
|
+
}
|
|
1171
|
+
const res = await fetch(props.endpoint, { method: "POST", headers, body });
|
|
318
1172
|
if (!res.ok) throw new Error(`Endpoint ${res.status}: ${await res.text().catch(() => "")}`);
|
|
1173
|
+
const contentType = res.headers.get("Content-Type") ?? "";
|
|
1174
|
+
if (contentType.includes("text/event-stream") && res.body) {
|
|
1175
|
+
const reader = res.body.getReader();
|
|
1176
|
+
const decoder = new TextDecoder();
|
|
1177
|
+
let buffer = "";
|
|
1178
|
+
let assembled = "";
|
|
1179
|
+
let lastError = null;
|
|
1180
|
+
while (true) {
|
|
1181
|
+
const { done, value } = await reader.read();
|
|
1182
|
+
if (done) break;
|
|
1183
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1184
|
+
const events = buffer.split("\n\n");
|
|
1185
|
+
buffer = events.pop() ?? "";
|
|
1186
|
+
for (const evt of events) {
|
|
1187
|
+
const lines = evt.split("\n");
|
|
1188
|
+
let evtName = "message";
|
|
1189
|
+
let data2 = "";
|
|
1190
|
+
for (const line of lines) {
|
|
1191
|
+
if (line.startsWith("event:")) evtName = line.slice(6).trim();
|
|
1192
|
+
else if (line.startsWith("data:")) data2 = line.slice(5).trim();
|
|
1193
|
+
}
|
|
1194
|
+
if (!data2) continue;
|
|
1195
|
+
if (evtName === "token") {
|
|
1196
|
+
try {
|
|
1197
|
+
const tok = JSON.parse(data2);
|
|
1198
|
+
assembled += tok;
|
|
1199
|
+
onToken(tok);
|
|
1200
|
+
} catch {
|
|
1201
|
+
}
|
|
1202
|
+
} else if (evtName === "done") {
|
|
1203
|
+
try {
|
|
1204
|
+
const obj = JSON.parse(data2);
|
|
1205
|
+
if (obj.reply) return obj.reply;
|
|
1206
|
+
} catch {
|
|
1207
|
+
}
|
|
1208
|
+
} else if (evtName === "error") {
|
|
1209
|
+
try {
|
|
1210
|
+
const obj = JSON.parse(data2);
|
|
1211
|
+
lastError = obj.message ?? "stream error";
|
|
1212
|
+
} catch {
|
|
1213
|
+
lastError = "stream error";
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
if (lastError) throw new Error(lastError);
|
|
1219
|
+
return assembled;
|
|
1220
|
+
}
|
|
319
1221
|
const data = await res.json();
|
|
320
1222
|
if (data.error) throw new Error(data.error);
|
|
321
1223
|
if (!data.reply) throw new Error("Endpoint returned no reply.");
|
|
@@ -323,18 +1225,42 @@ function ChatWidget(props) {
|
|
|
323
1225
|
}
|
|
324
1226
|
async function send() {
|
|
325
1227
|
const text = input.trim();
|
|
326
|
-
|
|
1228
|
+
const attached = files;
|
|
1229
|
+
if (!text && attached.length === 0 || sending) return;
|
|
327
1230
|
setInput("");
|
|
328
|
-
|
|
1231
|
+
setFiles([]);
|
|
1232
|
+
const userContent = attached.length > 0 ? `${text}${text ? "\n" : ""}\u{1F4CE} ${attached.map((f) => f.name).join(", ")}` : text;
|
|
1233
|
+
const userMsg = { id: `u${Date.now()}`, role: "user", content: userContent, ts: Date.now() };
|
|
329
1234
|
setMessages((prev) => [...prev, userMsg]);
|
|
330
1235
|
setSending(true);
|
|
1236
|
+
const assistantId = `a${Date.now()}`;
|
|
1237
|
+
setMessages((prev) => [...prev, { id: assistantId, role: "assistant", content: "", ts: Date.now() }]);
|
|
1238
|
+
const appendToken = (tok) => {
|
|
1239
|
+
setMessages(
|
|
1240
|
+
(prev) => prev.map((m) => m.id === assistantId ? { ...m, content: m.content + tok } : m)
|
|
1241
|
+
);
|
|
1242
|
+
};
|
|
331
1243
|
try {
|
|
332
1244
|
const history = messages.filter((m) => m.role !== "system").map((m) => ({ role: m.role, content: m.content }));
|
|
333
|
-
const reply = isEndpointMode ? await fetchReplyFromEndpoint(text, history) : (await bot.reply(text, { history })).reply;
|
|
334
|
-
|
|
1245
|
+
const reply = isEndpointMode ? await fetchReplyFromEndpoint(text, history, attached, appendToken) : (await bot.reply(text, { history })).reply;
|
|
1246
|
+
const markers = parseToolMarkers(reply);
|
|
1247
|
+
const cleanReply = stripToolMarkers(reply);
|
|
1248
|
+
setMessages(
|
|
1249
|
+
(prev) => prev.map((m) => m.id === assistantId ? { ...m, content: cleanReply } : m)
|
|
1250
|
+
);
|
|
1251
|
+
if (markers.length > 0) {
|
|
1252
|
+
setPendingTools((prev) => [
|
|
1253
|
+
...prev,
|
|
1254
|
+
...markers.map((marker) => ({ messageId: assistantId, marker, status: "pending" }))
|
|
1255
|
+
]);
|
|
1256
|
+
}
|
|
335
1257
|
} catch (err) {
|
|
336
1258
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
337
|
-
setMessages(
|
|
1259
|
+
setMessages(
|
|
1260
|
+
(prev) => prev.map(
|
|
1261
|
+
(m) => m.id === assistantId ? { ...m, content: `Sorry \u2014 something went wrong. (${errMsg})` } : m
|
|
1262
|
+
)
|
|
1263
|
+
);
|
|
338
1264
|
} finally {
|
|
339
1265
|
setSending(false);
|
|
340
1266
|
}
|
|
@@ -448,29 +1374,122 @@ function ChatWidget(props) {
|
|
|
448
1374
|
background: SURFACE_MUTED
|
|
449
1375
|
},
|
|
450
1376
|
children: [
|
|
451
|
-
messages.map((m) => /* @__PURE__ */
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
1377
|
+
messages.map((m) => /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 6, alignItems: m.role === "user" ? "flex-end" : "stretch" }, children: [
|
|
1378
|
+
m.content && /* @__PURE__ */ jsxs(
|
|
1379
|
+
"div",
|
|
1380
|
+
{
|
|
1381
|
+
className: "chatbotlite-msg",
|
|
1382
|
+
style: {
|
|
1383
|
+
alignSelf: m.role === "user" ? "flex-end" : "flex-start",
|
|
1384
|
+
maxWidth: "82%",
|
|
1385
|
+
padding: "9px 13px",
|
|
1386
|
+
borderRadius: m.role === "user" ? "18px 18px 4px 18px" : "18px 18px 18px 4px",
|
|
1387
|
+
background: m.role === "user" ? primary : SURFACE,
|
|
1388
|
+
color: m.role === "user" ? onPrimary : TEXT_BODY,
|
|
1389
|
+
border: m.role === "user" ? "none" : `1px solid ${BORDER}`,
|
|
1390
|
+
fontSize: 14,
|
|
1391
|
+
lineHeight: 1.5,
|
|
1392
|
+
letterSpacing: "-0.005em",
|
|
1393
|
+
whiteSpace: "pre-wrap",
|
|
1394
|
+
wordBreak: "break-word",
|
|
1395
|
+
boxShadow: m.role === "user" ? "0 1px 2px rgba(15,23,42,0.12)" : "0 1px 2px rgba(15,23,42,0.04)"
|
|
1396
|
+
},
|
|
1397
|
+
children: [
|
|
1398
|
+
m.content,
|
|
1399
|
+
sending && m.role === "assistant" && m === messages[messages.length - 1] && /* @__PURE__ */ jsx("span", { className: "chatbotlite-cursor", "aria-hidden": "true" })
|
|
1400
|
+
]
|
|
1401
|
+
}
|
|
1402
|
+
),
|
|
1403
|
+
pendingTools.map((pt, originalIdx) => ({ pt, originalIdx })).filter(({ pt }) => pt.messageId === m.id).map(({ pt, originalIdx }) => {
|
|
1404
|
+
const toolCommonStyle = { className: "chatbotlite-msg", style: { alignSelf: "stretch" } };
|
|
1405
|
+
const palette = {
|
|
1406
|
+
primary,
|
|
1407
|
+
onPrimary,
|
|
1408
|
+
border: BORDER,
|
|
1409
|
+
surface: SURFACE,
|
|
1410
|
+
surfaceMuted: SURFACE_MUTED,
|
|
1411
|
+
textBody: TEXT_BODY,
|
|
1412
|
+
textMuted: TEXT_MUTED
|
|
1413
|
+
};
|
|
1414
|
+
if (pt.marker.name === "uploadForReview" && tools.uploadForReview) {
|
|
1415
|
+
return /* @__PURE__ */ jsx("div", { ...toolCommonStyle, children: /* @__PURE__ */ jsx(
|
|
1416
|
+
UploadForReview,
|
|
1417
|
+
{
|
|
1418
|
+
...palette,
|
|
1419
|
+
purpose: String(pt.marker.args.purpose ?? "files"),
|
|
1420
|
+
accept: String(pt.marker.args.accept ?? "*"),
|
|
1421
|
+
maxMb: Number(pt.marker.args.maxMb ?? 10),
|
|
1422
|
+
submitting: pt.status === "submitting",
|
|
1423
|
+
submitted: pt.status === "submitted",
|
|
1424
|
+
onSubmit: async (files2) => {
|
|
1425
|
+
setPendingTools(
|
|
1426
|
+
(prev) => prev.map((p, i) => i === originalIdx ? { ...p, status: "submitting" } : p)
|
|
1427
|
+
);
|
|
1428
|
+
try {
|
|
1429
|
+
const result = await tools.uploadForReview.handler({
|
|
1430
|
+
files: files2,
|
|
1431
|
+
purpose: String(pt.marker.args.purpose ?? "files")
|
|
1432
|
+
});
|
|
1433
|
+
await handleToolSubmit("uploadForReview", originalIdx, result);
|
|
1434
|
+
} catch (err) {
|
|
1435
|
+
setPendingTools(
|
|
1436
|
+
(prev) => prev.map((p, i) => i === originalIdx ? { ...p, status: "pending" } : p)
|
|
1437
|
+
);
|
|
1438
|
+
throw err;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
) }, `tool-${originalIdx}`);
|
|
1443
|
+
}
|
|
1444
|
+
if (pt.marker.name === "scheduleCallback" && tools.scheduleCallback) {
|
|
1445
|
+
return /* @__PURE__ */ jsx("div", { ...toolCommonStyle, children: /* @__PURE__ */ jsx(
|
|
1446
|
+
ScheduleCallback,
|
|
1447
|
+
{
|
|
1448
|
+
...palette,
|
|
1449
|
+
durationMin: Number(pt.marker.args.durationMin ?? 15),
|
|
1450
|
+
timezone: String(pt.marker.args.timezone ?? "UTC"),
|
|
1451
|
+
submitting: pt.status === "submitting",
|
|
1452
|
+
submitted: pt.status === "submitted",
|
|
1453
|
+
...pt.result?.confirmedAt ? { submittedSlot: String(pt.result.confirmedAt) } : {},
|
|
1454
|
+
getAvailableSlots: tools.scheduleCallback.getAvailableSlots,
|
|
1455
|
+
onConfirm: async (slot) => {
|
|
1456
|
+
setPendingTools(
|
|
1457
|
+
(prev) => prev.map((p, i) => i === originalIdx ? { ...p, status: "submitting" } : p)
|
|
1458
|
+
);
|
|
1459
|
+
const result = await tools.scheduleCallback.onConfirm({ slot });
|
|
1460
|
+
await handleToolSubmit("scheduleCallback", originalIdx, result);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
) }, `tool-${originalIdx}`);
|
|
1464
|
+
}
|
|
1465
|
+
if (pt.marker.name === "requestPayment" && tools.requestPayment) {
|
|
1466
|
+
return /* @__PURE__ */ jsx("div", { ...toolCommonStyle, children: /* @__PURE__ */ jsx(
|
|
1467
|
+
RequestPayment,
|
|
1468
|
+
{
|
|
1469
|
+
...palette,
|
|
1470
|
+
amount: Number(pt.marker.args.amount ?? 0),
|
|
1471
|
+
currency: String(pt.marker.args.currency ?? "USD"),
|
|
1472
|
+
...pt.marker.args.reason ? { reason: String(pt.marker.args.reason) } : {},
|
|
1473
|
+
showInterac: tools.requestPayment.showInterac ?? true,
|
|
1474
|
+
...tools.requestPayment.stripeLink ? { stripeLink: tools.requestPayment.stripeLink } : {},
|
|
1475
|
+
submitting: pt.status === "submitting",
|
|
1476
|
+
submitted: pt.status === "submitted",
|
|
1477
|
+
...pt.result?.method ? { submittedMethod: pt.result.method } : {},
|
|
1478
|
+
onPick: async (method) => {
|
|
1479
|
+
setPendingTools(
|
|
1480
|
+
(prev) => prev.map((p, i) => i === originalIdx ? { ...p, status: "submitting" } : p)
|
|
1481
|
+
);
|
|
1482
|
+
const amount = Number(pt.marker.args.amount ?? 0);
|
|
1483
|
+
const currency = String(pt.marker.args.currency ?? "USD");
|
|
1484
|
+
const result = await tools.requestPayment.onPick({ method, amount, currency });
|
|
1485
|
+
await handleToolSubmit("requestPayment", originalIdx, { ...result, method });
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
) }, `tool-${originalIdx}`);
|
|
1489
|
+
}
|
|
1490
|
+
return null;
|
|
1491
|
+
})
|
|
1492
|
+
] }, m.id)),
|
|
474
1493
|
sending && /* @__PURE__ */ jsxs(
|
|
475
1494
|
"div",
|
|
476
1495
|
{
|
|
@@ -495,66 +1514,169 @@ function ChatWidget(props) {
|
|
|
495
1514
|
),
|
|
496
1515
|
/* @__PURE__ */ jsxs("div", { style: {
|
|
497
1516
|
display: "flex",
|
|
1517
|
+
flexDirection: "column",
|
|
498
1518
|
padding: 12,
|
|
499
1519
|
gap: 8,
|
|
500
1520
|
background: SURFACE,
|
|
501
1521
|
borderTop: `1px solid ${BORDER}`
|
|
502
1522
|
}, children: [
|
|
503
|
-
/* @__PURE__ */ jsx(
|
|
504
|
-
"
|
|
1523
|
+
files.length > 0 && /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 6 }, children: files.map((f, i) => /* @__PURE__ */ jsxs(
|
|
1524
|
+
"span",
|
|
505
1525
|
{
|
|
506
|
-
ref: inputRef,
|
|
507
|
-
className: "chatbotlite-input",
|
|
508
|
-
type: "text",
|
|
509
|
-
value: input,
|
|
510
|
-
onChange: (e) => setInput(e.target.value),
|
|
511
|
-
onKeyDown: (e) => {
|
|
512
|
-
if (e.key === "Enter" && !e.shiftKey) {
|
|
513
|
-
e.preventDefault();
|
|
514
|
-
void send();
|
|
515
|
-
}
|
|
516
|
-
},
|
|
517
|
-
placeholder: "Type a message\u2026",
|
|
518
|
-
disabled: sending,
|
|
519
1526
|
style: {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
1527
|
+
display: "inline-flex",
|
|
1528
|
+
alignItems: "center",
|
|
1529
|
+
gap: 6,
|
|
1530
|
+
padding: "4px 8px 4px 10px",
|
|
1531
|
+
borderRadius: 8,
|
|
524
1532
|
background: SURFACE_MUTED,
|
|
525
|
-
|
|
526
|
-
|
|
1533
|
+
border: `1px solid ${BORDER}`,
|
|
1534
|
+
fontSize: 12,
|
|
527
1535
|
color: TEXT_BODY,
|
|
528
|
-
|
|
529
|
-
transition: "box-shadow 120ms ease, border-color 120ms ease"
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
),
|
|
533
|
-
/* @__PURE__ */ jsx(
|
|
534
|
-
"button",
|
|
535
|
-
{
|
|
536
|
-
className: "chatbotlite-send",
|
|
537
|
-
onClick: () => void send(),
|
|
538
|
-
disabled: sending || !input.trim(),
|
|
539
|
-
"aria-label": "Send message",
|
|
540
|
-
style: {
|
|
541
|
-
padding: "0 16px",
|
|
542
|
-
height: 40,
|
|
543
|
-
minWidth: 64,
|
|
544
|
-
borderRadius: 12,
|
|
545
|
-
background: primary,
|
|
546
|
-
color: onPrimary,
|
|
547
|
-
border: "none",
|
|
548
|
-
fontSize: 14,
|
|
549
|
-
fontWeight: 600,
|
|
550
|
-
fontFamily: FONT_STACK,
|
|
551
|
-
cursor: sending || !input.trim() ? "default" : "pointer",
|
|
552
|
-
opacity: sending || !input.trim() ? 0.4 : 1,
|
|
553
|
-
boxShadow: "0 2px 6px -1px rgba(15,23,42,0.18)"
|
|
1536
|
+
maxWidth: 200
|
|
554
1537
|
},
|
|
555
|
-
children:
|
|
556
|
-
|
|
557
|
-
|
|
1538
|
+
children: [
|
|
1539
|
+
/* @__PURE__ */ jsxs("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: [
|
|
1540
|
+
"\u{1F4CE} ",
|
|
1541
|
+
f.name
|
|
1542
|
+
] }),
|
|
1543
|
+
/* @__PURE__ */ jsx(
|
|
1544
|
+
"button",
|
|
1545
|
+
{
|
|
1546
|
+
onClick: () => removeFile(i),
|
|
1547
|
+
"aria-label": `Remove ${f.name}`,
|
|
1548
|
+
style: {
|
|
1549
|
+
background: "transparent",
|
|
1550
|
+
border: "none",
|
|
1551
|
+
cursor: "pointer",
|
|
1552
|
+
color: TEXT_MUTED,
|
|
1553
|
+
fontSize: 14,
|
|
1554
|
+
lineHeight: 1,
|
|
1555
|
+
padding: 0
|
|
1556
|
+
},
|
|
1557
|
+
children: "\xD7"
|
|
1558
|
+
}
|
|
1559
|
+
)
|
|
1560
|
+
]
|
|
1561
|
+
},
|
|
1562
|
+
`${f.name}-${i}`
|
|
1563
|
+
)) }),
|
|
1564
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8 }, children: [
|
|
1565
|
+
attachEnabled && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1566
|
+
/* @__PURE__ */ jsx(
|
|
1567
|
+
"input",
|
|
1568
|
+
{
|
|
1569
|
+
ref: fileInputRef,
|
|
1570
|
+
type: "file",
|
|
1571
|
+
multiple: true,
|
|
1572
|
+
accept: acceptAttr,
|
|
1573
|
+
style: { display: "none" },
|
|
1574
|
+
onChange: (e) => {
|
|
1575
|
+
if (e.target.files) addFiles(e.target.files);
|
|
1576
|
+
e.target.value = "";
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
),
|
|
1580
|
+
/* @__PURE__ */ jsx(
|
|
1581
|
+
"button",
|
|
1582
|
+
{
|
|
1583
|
+
className: "chatbotlite-attach-btn",
|
|
1584
|
+
onClick: () => fileInputRef.current?.click(),
|
|
1585
|
+
disabled: sending || files.length >= maxFiles,
|
|
1586
|
+
"aria-label": "Attach file",
|
|
1587
|
+
style: {
|
|
1588
|
+
width: 40,
|
|
1589
|
+
height: 40,
|
|
1590
|
+
borderRadius: 10,
|
|
1591
|
+
background: SURFACE_MUTED,
|
|
1592
|
+
border: `1px solid ${BORDER}`,
|
|
1593
|
+
cursor: sending || files.length >= maxFiles ? "default" : "pointer",
|
|
1594
|
+
opacity: sending || files.length >= maxFiles ? 0.4 : 1,
|
|
1595
|
+
fontSize: 16,
|
|
1596
|
+
transition: "background 120ms ease, transform 80ms ease"
|
|
1597
|
+
},
|
|
1598
|
+
children: "\u{1F4CE}"
|
|
1599
|
+
}
|
|
1600
|
+
)
|
|
1601
|
+
] }),
|
|
1602
|
+
voiceEnabled && speechSupported && /* @__PURE__ */ jsx(
|
|
1603
|
+
"button",
|
|
1604
|
+
{
|
|
1605
|
+
className: "chatbotlite-voice-btn",
|
|
1606
|
+
onClick: toggleVoice,
|
|
1607
|
+
disabled: sending,
|
|
1608
|
+
"aria-label": voiceListening ? "Stop recording" : "Start voice input",
|
|
1609
|
+
style: {
|
|
1610
|
+
width: 40,
|
|
1611
|
+
height: 40,
|
|
1612
|
+
borderRadius: 10,
|
|
1613
|
+
background: voiceListening ? primary : SURFACE_MUTED,
|
|
1614
|
+
color: voiceListening ? onPrimary : TEXT_BODY,
|
|
1615
|
+
border: `1px solid ${voiceListening ? primary : BORDER}`,
|
|
1616
|
+
cursor: sending ? "default" : "pointer",
|
|
1617
|
+
opacity: sending ? 0.4 : 1,
|
|
1618
|
+
fontSize: 16,
|
|
1619
|
+
transition: "background 120ms ease, color 120ms ease, border-color 120ms ease, transform 80ms ease"
|
|
1620
|
+
},
|
|
1621
|
+
children: "\u{1F399}\uFE0F"
|
|
1622
|
+
}
|
|
1623
|
+
),
|
|
1624
|
+
/* @__PURE__ */ jsx(
|
|
1625
|
+
"input",
|
|
1626
|
+
{
|
|
1627
|
+
ref: inputRef,
|
|
1628
|
+
className: "chatbotlite-input",
|
|
1629
|
+
type: "text",
|
|
1630
|
+
value: input,
|
|
1631
|
+
onChange: (e) => setInput(e.target.value),
|
|
1632
|
+
onKeyDown: (e) => {
|
|
1633
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
1634
|
+
e.preventDefault();
|
|
1635
|
+
void send();
|
|
1636
|
+
}
|
|
1637
|
+
},
|
|
1638
|
+
placeholder: "Type a message\u2026",
|
|
1639
|
+
disabled: sending,
|
|
1640
|
+
style: {
|
|
1641
|
+
flex: 1,
|
|
1642
|
+
padding: "10px 14px",
|
|
1643
|
+
borderRadius: 12,
|
|
1644
|
+
border: `1px solid ${BORDER}`,
|
|
1645
|
+
background: SURFACE_MUTED,
|
|
1646
|
+
fontSize: 14,
|
|
1647
|
+
fontFamily: FONT_STACK,
|
|
1648
|
+
color: TEXT_BODY,
|
|
1649
|
+
outline: "none",
|
|
1650
|
+
transition: "box-shadow 120ms ease, border-color 120ms ease"
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
),
|
|
1654
|
+
/* @__PURE__ */ jsx(
|
|
1655
|
+
"button",
|
|
1656
|
+
{
|
|
1657
|
+
className: "chatbotlite-send",
|
|
1658
|
+
onClick: () => void send(),
|
|
1659
|
+
disabled: sending || !input.trim() && files.length === 0,
|
|
1660
|
+
"aria-label": "Send message",
|
|
1661
|
+
style: {
|
|
1662
|
+
padding: "0 16px",
|
|
1663
|
+
height: 40,
|
|
1664
|
+
minWidth: 64,
|
|
1665
|
+
borderRadius: 12,
|
|
1666
|
+
background: primary,
|
|
1667
|
+
color: onPrimary,
|
|
1668
|
+
border: "none",
|
|
1669
|
+
fontSize: 14,
|
|
1670
|
+
fontWeight: 600,
|
|
1671
|
+
fontFamily: FONT_STACK,
|
|
1672
|
+
cursor: sending || !input.trim() && files.length === 0 ? "default" : "pointer",
|
|
1673
|
+
opacity: sending || !input.trim() && files.length === 0 ? 0.4 : 1,
|
|
1674
|
+
boxShadow: "0 2px 6px -1px rgba(15,23,42,0.18)"
|
|
1675
|
+
},
|
|
1676
|
+
children: "Send"
|
|
1677
|
+
}
|
|
1678
|
+
)
|
|
1679
|
+
] })
|
|
558
1680
|
] }),
|
|
559
1681
|
showBranding && /* @__PURE__ */ jsxs(
|
|
560
1682
|
"a",
|