chatbotlite 0.4.0 → 0.5.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.
@@ -3,8 +3,60 @@ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
3
3
 
4
4
  // src/react/ChatWidget.tsx
5
5
 
6
+ // src/core/tools.ts
7
+ var MARKER_RE = /\[SKILL:(\w+)((?:\s+\w+=(?:"[^"]*"|[\w./@*+,:-]+))*)\s*\]/g;
8
+ var ARG_RE = /(\w+)=("([^"]*)"|([\w./@*+,:-]+))/g;
9
+ function coerce(value) {
10
+ if (value === "true") return true;
11
+ if (value === "false") return false;
12
+ if (/^-?\d+(?:\.\d+)?$/.test(value)) return Number(value);
13
+ return value;
14
+ }
15
+ function parseToolMarkers(text) {
16
+ const markers = [];
17
+ let m;
18
+ MARKER_RE.lastIndex = 0;
19
+ while ((m = MARKER_RE.exec(text)) !== null) {
20
+ const name = m[1];
21
+ const argsRaw = m[2] ?? "";
22
+ const args = {};
23
+ let a;
24
+ ARG_RE.lastIndex = 0;
25
+ while ((a = ARG_RE.exec(argsRaw)) !== null) {
26
+ const key = a[1];
27
+ const value = a[3] ?? a[4] ?? "";
28
+ args[key] = coerce(value);
29
+ }
30
+ markers.push({ name, args, raw: m[0] });
31
+ }
32
+ return markers;
33
+ }
34
+ function stripToolMarkers(text) {
35
+ return text.replace(MARKER_RE, "").replace(/\s+\n/g, "\n").trim();
36
+ }
37
+ function buildToolsPromptAddendum(enabledTools) {
38
+ if (enabledTools.length === 0) return "";
39
+ const examples = {
40
+ uploadForReview: '[SKILL:uploadForReview purpose="T4 slip" accept="image/*,application/pdf" maxMb=10] \u2014 collect a document for human review (bytes go to webhook, you never see content)',
41
+ scheduleCallback: '[SKILL:scheduleCallback durationMin=15 timezone="America/Vancouver"] \u2014 let the user pick a callback time slot',
42
+ requestPayment: '[SKILL:requestPayment amount=4250 currency="cad" reason="initial deposit"] \u2014 collect payment via inline card'
43
+ };
44
+ const lines = enabledTools.filter((t) => examples[t]).map((t) => `- ${examples[t]}`);
45
+ if (lines.length === 0) return "";
46
+ return [
47
+ "",
48
+ "## Available tools",
49
+ "When you need one of these workflows, emit the marker INLINE in your reply.",
50
+ "Write a short message first, THEN the marker. The marker will be replaced by an interactive card.",
51
+ "Pause the conversation after emitting \u2014 wait for the tool result before continuing.",
52
+ "",
53
+ ...lines
54
+ ].join("\n");
55
+ }
56
+
6
57
  // src/core/prompts.ts
7
- function buildSystemPrompt(knowledge) {
58
+ function buildSystemPrompt(knowledge, enabledTools = []) {
59
+ const toolsAddendum = buildToolsPromptAddendum(enabledTools);
8
60
  return [
9
61
  "You are an AI assistant on a business website. Use ONLY the knowledge below to answer.",
10
62
  "",
@@ -17,8 +69,9 @@ function buildSystemPrompt(knowledge) {
17
69
  "- For anything not covered in the knowledge above, say the owner will follow up \u2014 do NOT guess.",
18
70
  '- If the caller is clearly a vendor/sales pitch, say: "This does not look like a customer service request, so we will not continue this thread."',
19
71
  `- If wrong number or asked to stop, say: "Sorry about that. We won't text again."`,
20
- "- Match the caller's language automatically."
21
- ].join("\n");
72
+ "- Match the caller's language automatically.",
73
+ toolsAddendum
74
+ ].filter(Boolean).join("\n");
22
75
  }
23
76
 
24
77
  // src/core/guards.ts
@@ -141,10 +194,12 @@ var ChatBot = class {
141
194
  timeoutMs;
142
195
  cachedSystemPrompt;
143
196
  guards;
197
+ knowledge;
144
198
  constructor(init) {
145
199
  if (!init.knowledge || typeof init.knowledge !== "string" || init.knowledge.trim().length === 0) {
146
200
  throw new Error("chatbotlite: knowledge is required (a non-empty markdown string).");
147
201
  }
202
+ this.knowledge = init.knowledge;
148
203
  this.keys = init.providers.keys ?? {};
149
204
  this.steps = resolveChain(init.providers);
150
205
  this.fetcher = init.options?.fetch ?? globalThis.fetch.bind(globalThis);
@@ -152,6 +207,14 @@ var ChatBot = class {
152
207
  this.cachedSystemPrompt = buildSystemPrompt(init.knowledge);
153
208
  this.guards = init.guards ?? {};
154
209
  }
210
+ /** Build system prompt for given opts — uses cached if no enabledTools, else rebuilds. */
211
+ resolveSystemPrompt(opts) {
212
+ if (opts.systemPrompt) return opts.systemPrompt;
213
+ if (opts.enabledTools && opts.enabledTools.length > 0) {
214
+ return buildSystemPrompt(this.knowledge, opts.enabledTools);
215
+ }
216
+ return this.cachedSystemPrompt;
217
+ }
155
218
  /** Run an LLM judge against content. Fail-open on errors. */
156
219
  async judge(config, content) {
157
220
  const endpoint = PROVIDER_ENDPOINTS[config.provider];
@@ -188,7 +251,7 @@ var ChatBot = class {
188
251
  * event: error data: {"message":"...","attempts":[...]}
189
252
  */
190
253
  async replyStream(message, opts = {}) {
191
- const systemPrompt = opts.systemPrompt ?? this.cachedSystemPrompt;
254
+ const systemPrompt = this.resolveSystemPrompt(opts);
192
255
  const messages = [
193
256
  { role: "system", content: systemPrompt },
194
257
  ...opts.history ?? [],
@@ -301,7 +364,7 @@ data: ${data}
301
364
  return this.reply(message, opts);
302
365
  }
303
366
  const dataUrls = await Promise.all(images.map(fileToDataUrl));
304
- const systemPrompt = opts.systemPrompt ?? this.cachedSystemPrompt;
367
+ const systemPrompt = this.resolveSystemPrompt(opts);
305
368
  const userContent = [];
306
369
  if (message) userContent.push({ type: "text", text: message });
307
370
  for (const url of dataUrls) userContent.push({ type: "image_url", image_url: { url } });
@@ -381,7 +444,7 @@ data: ${data}
381
444
  };
382
445
  }
383
446
  }
384
- const systemPrompt = opts.systemPrompt ?? this.cachedSystemPrompt;
447
+ const systemPrompt = this.resolveSystemPrompt(opts);
385
448
  const messages = [
386
449
  { role: "system", content: systemPrompt },
387
450
  ...opts.history ?? [],
@@ -494,38 +557,6 @@ function normalizeChainEntry(entry, keys) {
494
557
  }
495
558
  return { provider, model, label: `${provider}/${model}` };
496
559
  }
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
560
  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
561
  function UploadForReview(props) {
531
562
  const {
@@ -982,12 +1013,15 @@ var BOLT = "\u26A1";
982
1013
  var DEFAULT_PRIMARY = "#0f172a";
983
1014
  var DEFAULT_ON_PRIMARY = "#ffffff";
984
1015
  var SURFACE = "#ffffff";
985
- var SURFACE_MUTED = "#fafbfc";
1016
+ var CHAT_BG = "#f5f1eb";
1017
+ var BUBBLE_BOT = "#ffffff";
1018
+ var INPUT_BG = "#f1f3f5";
986
1019
  var BORDER = "#e5e7eb";
1020
+ var BORDER_LIGHT = "rgba(15,23,42,0.06)";
987
1021
  var TEXT_BODY = "#0f172a";
988
1022
  var TEXT_MUTED = "#64748b";
989
1023
  var TEXT_FAINT = "#94a3b8";
990
- var FONT_STACK = `'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif`;
1024
+ var FONT_STACK = `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif`;
991
1025
  var STYLE_TAG_ID = "chatbotlite-widget-styles";
992
1026
  var KEYFRAMES = `
993
1027
  @keyframes chatbotlite-pop { 0% { opacity: 0; transform: scale(0.6); } 100% { opacity: 1; transform: scale(1); } }
@@ -999,8 +1033,8 @@ var KEYFRAMES = `
999
1033
  .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; }
1000
1034
  .chatbotlite-launcher:hover { transform: translateY(-2px) scale(1.04); }
1001
1035
  .chatbotlite-launcher:active { transform: translateY(0) scale(0.98); }
1002
- .chatbotlite-close { transition: background 120ms ease; }
1003
- .chatbotlite-close:hover { background: rgba(255,255,255,0.16); }
1036
+ .chatbotlite-close { transition: background 120ms ease, color 120ms ease; }
1037
+ .chatbotlite-close:hover { background: rgba(15,23,42,0.06); color: ${TEXT_BODY}; }
1004
1038
  .chatbotlite-send { transition: transform 120ms ease, opacity 120ms ease, box-shadow 120ms ease; }
1005
1039
  .chatbotlite-send:not(:disabled):hover { transform: translateY(-1px); }
1006
1040
  .chatbotlite-send:not(:disabled):active { transform: translateY(0); }
@@ -1008,8 +1042,8 @@ var KEYFRAMES = `
1008
1042
  .chatbotlite-msg { animation: chatbotlite-fade-in 220ms cubic-bezier(0.4, 0, 0.2, 1); }
1009
1043
  .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
1044
  .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); }
1045
+ .chatbotlite-icon-btn:hover:not(:disabled) { background: rgba(15,23,42,0.06) !important; opacity: 1 !important; }
1046
+ .chatbotlite-icon-btn:active:not(:disabled) { transform: scale(0.92); }
1013
1047
  .chatbotlite-dot:nth-child(2) { animation-delay: 0.15s; }
1014
1048
  .chatbotlite-dot:nth-child(3) { animation-delay: 0.3s; margin-right: 0; }
1015
1049
  .chatbotlite-brand:hover { color: ${TEXT_MUTED} !important; }
@@ -1156,17 +1190,19 @@ function ChatWidget(props) {
1156
1190
  return void 0;
1157
1191
  }, [open]);
1158
1192
  async function fetchReplyFromEndpoint(text, history, attachedFiles, onToken) {
1193
+ const enabledTools = Object.keys(tools);
1159
1194
  let body;
1160
1195
  const headers = { Accept: "text/event-stream, application/json" };
1161
1196
  if (attachedFiles.length > 0) {
1162
1197
  const form = new FormData();
1163
1198
  form.append("message", text);
1164
1199
  form.append("transcript", JSON.stringify(history));
1200
+ form.append("enabledTools", JSON.stringify(enabledTools));
1165
1201
  for (const f of attachedFiles) form.append("attachments", f, f.name);
1166
1202
  body = form;
1167
1203
  } else {
1168
1204
  headers["Content-Type"] = "application/json";
1169
- body = JSON.stringify({ message: text, transcript: history });
1205
+ body = JSON.stringify({ message: text, transcript: history, enabledTools });
1170
1206
  }
1171
1207
  const res = await fetch(props.endpoint, { method: "POST", headers, body });
1172
1208
  if (!res.ok) throw new Error(`Endpoint ${res.status}: ${await res.text().catch(() => "")}`);
@@ -1323,17 +1359,34 @@ function ChatWidget(props) {
1323
1359
  },
1324
1360
  children: [
1325
1361
  /* @__PURE__ */ jsxs("header", { style: {
1326
- padding: "16px 18px",
1327
- background: primary,
1328
- color: onPrimary,
1362
+ padding: "14px 16px",
1363
+ background: SURFACE,
1364
+ color: TEXT_BODY,
1329
1365
  display: "flex",
1330
1366
  justifyContent: "space-between",
1331
1367
  alignItems: "center",
1332
- gap: 12
1368
+ gap: 12,
1369
+ borderBottom: `1px solid ${BORDER_LIGHT}`
1333
1370
  }, children: [
1334
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", lineHeight: 1.2, minWidth: 0 }, children: [
1335
- /* @__PURE__ */ jsx("span", { style: { fontWeight: 600, fontSize: 15, letterSpacing: "-0.01em", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: resolvedTitle }),
1336
- subtitle && /* @__PURE__ */ jsx("span", { style: { fontSize: 12, opacity: 0.7, marginTop: 2, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: subtitle })
1371
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 10, minWidth: 0 }, children: [
1372
+ /* @__PURE__ */ jsx("div", { style: {
1373
+ width: 36,
1374
+ height: 36,
1375
+ borderRadius: "50%",
1376
+ background: primary,
1377
+ color: onPrimary,
1378
+ display: "flex",
1379
+ alignItems: "center",
1380
+ justifyContent: "center",
1381
+ fontSize: 16,
1382
+ fontWeight: 600,
1383
+ flexShrink: 0,
1384
+ letterSpacing: "-0.02em"
1385
+ }, children: resolvedTitle.charAt(0).toUpperCase() }),
1386
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", lineHeight: 1.2, minWidth: 0 }, children: [
1387
+ /* @__PURE__ */ jsx("span", { style: { fontWeight: 600, fontSize: 15, letterSpacing: "-0.01em", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: TEXT_BODY }, children: resolvedTitle }),
1388
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 12, color: TEXT_MUTED, marginTop: 2, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: subtitle ?? (sending ? "typing\u2026" : "online") })
1389
+ ] })
1337
1390
  ] }),
1338
1391
  /* @__PURE__ */ jsx(
1339
1392
  "button",
@@ -1344,7 +1397,7 @@ function ChatWidget(props) {
1344
1397
  style: {
1345
1398
  background: "transparent",
1346
1399
  border: "none",
1347
- color: onPrimary,
1400
+ color: TEXT_MUTED,
1348
1401
  width: 32,
1349
1402
  height: 32,
1350
1403
  borderRadius: 10,
@@ -1371,7 +1424,7 @@ function ChatWidget(props) {
1371
1424
  display: "flex",
1372
1425
  flexDirection: "column",
1373
1426
  gap: 8,
1374
- background: SURFACE_MUTED
1427
+ background: CHAT_BG
1375
1428
  },
1376
1429
  children: [
1377
1430
  messages.map((m) => /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 6, alignItems: m.role === "user" ? "flex-end" : "stretch" }, children: [
@@ -1381,18 +1434,18 @@ function ChatWidget(props) {
1381
1434
  className: "chatbotlite-msg",
1382
1435
  style: {
1383
1436
  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,
1437
+ maxWidth: "78%",
1438
+ padding: "8px 12px",
1439
+ borderRadius: 18,
1440
+ background: m.role === "user" ? primary : BUBBLE_BOT,
1388
1441
  color: m.role === "user" ? onPrimary : TEXT_BODY,
1389
- border: m.role === "user" ? "none" : `1px solid ${BORDER}`,
1390
- fontSize: 14,
1391
- lineHeight: 1.5,
1442
+ border: "none",
1443
+ fontSize: 14.5,
1444
+ lineHeight: 1.4,
1392
1445
  letterSpacing: "-0.005em",
1393
1446
  whiteSpace: "pre-wrap",
1394
1447
  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)"
1448
+ boxShadow: m.role === "user" ? "none" : "0 1px 0.5px rgba(15,23,42,0.05)"
1396
1449
  },
1397
1450
  children: [
1398
1451
  m.content,
@@ -1407,7 +1460,7 @@ function ChatWidget(props) {
1407
1460
  onPrimary,
1408
1461
  border: BORDER,
1409
1462
  surface: SURFACE,
1410
- surfaceMuted: SURFACE_MUTED,
1463
+ surfaceMuted: CHAT_BG,
1411
1464
  textBody: TEXT_BODY,
1412
1465
  textMuted: TEXT_MUTED
1413
1466
  };
@@ -1490,17 +1543,16 @@ function ChatWidget(props) {
1490
1543
  return null;
1491
1544
  })
1492
1545
  ] }, m.id)),
1493
- sending && /* @__PURE__ */ jsxs(
1546
+ sending && messages[messages.length - 1]?.content === "" && /* @__PURE__ */ jsxs(
1494
1547
  "div",
1495
1548
  {
1496
1549
  className: "chatbotlite-msg",
1497
1550
  style: {
1498
1551
  alignSelf: "flex-start",
1499
- padding: "12px 14px",
1500
- borderRadius: "18px 18px 18px 4px",
1501
- background: SURFACE,
1502
- border: `1px solid ${BORDER}`,
1503
- boxShadow: "0 1px 2px rgba(15,23,42,0.04)"
1552
+ padding: "10px 14px",
1553
+ borderRadius: 18,
1554
+ background: BUBBLE_BOT,
1555
+ boxShadow: "0 1px 0.5px rgba(15,23,42,0.05)"
1504
1556
  },
1505
1557
  children: [
1506
1558
  /* @__PURE__ */ jsx("span", { className: "chatbotlite-dot" }),
@@ -1512,172 +1564,199 @@ function ChatWidget(props) {
1512
1564
  ]
1513
1565
  }
1514
1566
  ),
1515
- /* @__PURE__ */ jsxs("div", { style: {
1516
- display: "flex",
1517
- flexDirection: "column",
1518
- padding: 12,
1519
- gap: 8,
1567
+ files.length > 0 && /* @__PURE__ */ jsx("div", { style: {
1568
+ padding: "8px 12px 0",
1520
1569
  background: SURFACE,
1521
- borderTop: `1px solid ${BORDER}`
1522
- }, children: [
1523
- files.length > 0 && /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 6 }, children: files.map((f, i) => /* @__PURE__ */ jsxs(
1524
- "span",
1525
- {
1526
- style: {
1527
- display: "inline-flex",
1528
- alignItems: "center",
1529
- gap: 6,
1530
- padding: "4px 8px 4px 10px",
1531
- borderRadius: 8,
1532
- background: SURFACE_MUTED,
1533
- border: `1px solid ${BORDER}`,
1534
- fontSize: 12,
1535
- color: TEXT_BODY,
1536
- maxWidth: 200
1537
- },
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
- ]
1570
+ display: "flex",
1571
+ flexWrap: "wrap",
1572
+ gap: 6
1573
+ }, children: files.map((f, i) => /* @__PURE__ */ jsxs(
1574
+ "span",
1575
+ {
1576
+ style: {
1577
+ display: "inline-flex",
1578
+ alignItems: "center",
1579
+ gap: 6,
1580
+ padding: "4px 8px 4px 10px",
1581
+ borderRadius: 999,
1582
+ background: INPUT_BG,
1583
+ fontSize: 12,
1584
+ color: TEXT_BODY,
1585
+ maxWidth: 200
1561
1586
  },
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
- ),
1587
+ children: [
1588
+ /* @__PURE__ */ jsxs("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: [
1589
+ "\u{1F4CE} ",
1590
+ f.name
1591
+ ] }),
1580
1592
  /* @__PURE__ */ jsx(
1581
1593
  "button",
1582
1594
  {
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}"
1595
+ onClick: () => removeFile(i),
1596
+ "aria-label": `Remove ${f.name}`,
1597
+ style: { background: "transparent", border: "none", cursor: "pointer", color: TEXT_MUTED, fontSize: 14, lineHeight: 1, padding: 0 },
1598
+ children: "\xD7"
1599
1599
  }
1600
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
- ),
1601
+ ]
1602
+ },
1603
+ `${f.name}-${i}`
1604
+ )) }),
1605
+ /* @__PURE__ */ jsx("div", { style: {
1606
+ padding: "10px 12px 12px",
1607
+ background: SURFACE
1608
+ }, children: /* @__PURE__ */ jsxs("div", { style: {
1609
+ display: "flex",
1610
+ alignItems: "flex-end",
1611
+ gap: 6,
1612
+ padding: "6px 6px 6px 10px",
1613
+ background: INPUT_BG,
1614
+ borderRadius: 22,
1615
+ transition: "background 120ms ease"
1616
+ }, children: [
1617
+ attachEnabled && /* @__PURE__ */ jsxs(Fragment, { children: [
1624
1618
  /* @__PURE__ */ jsx(
1625
1619
  "input",
1626
1620
  {
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"
1621
+ ref: fileInputRef,
1622
+ type: "file",
1623
+ multiple: true,
1624
+ accept: acceptAttr,
1625
+ style: { display: "none" },
1626
+ onChange: (e) => {
1627
+ if (e.target.files) addFiles(e.target.files);
1628
+ e.target.value = "";
1651
1629
  }
1652
1630
  }
1653
1631
  ),
1654
1632
  /* @__PURE__ */ jsx(
1655
1633
  "button",
1656
1634
  {
1657
- className: "chatbotlite-send",
1658
- onClick: () => void send(),
1659
- disabled: sending || !input.trim() && files.length === 0,
1660
- "aria-label": "Send message",
1635
+ className: "chatbotlite-icon-btn",
1636
+ onClick: () => fileInputRef.current?.click(),
1637
+ disabled: sending || files.length >= maxFiles,
1638
+ "aria-label": "Attach file",
1661
1639
  style: {
1662
- padding: "0 16px",
1663
- height: 40,
1664
- minWidth: 64,
1665
- borderRadius: 12,
1666
- background: primary,
1667
- color: onPrimary,
1640
+ width: 32,
1641
+ height: 32,
1642
+ borderRadius: "50%",
1643
+ background: "transparent",
1668
1644
  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)"
1645
+ cursor: sending || files.length >= maxFiles ? "default" : "pointer",
1646
+ opacity: sending || files.length >= maxFiles ? 0.35 : 0.7,
1647
+ fontSize: 18,
1648
+ lineHeight: 1,
1649
+ padding: 0,
1650
+ display: "flex",
1651
+ alignItems: "center",
1652
+ justifyContent: "center",
1653
+ flexShrink: 0,
1654
+ alignSelf: "center",
1655
+ transition: "opacity 120ms ease, background 120ms ease"
1675
1656
  },
1676
- children: "Send"
1657
+ children: "\u{1F4CE}"
1677
1658
  }
1678
1659
  )
1679
- ] })
1680
- ] }),
1660
+ ] }),
1661
+ voiceEnabled && speechSupported && /* @__PURE__ */ jsx(
1662
+ "button",
1663
+ {
1664
+ className: "chatbotlite-icon-btn",
1665
+ onClick: toggleVoice,
1666
+ disabled: sending,
1667
+ "aria-label": voiceListening ? "Stop recording" : "Start voice input",
1668
+ style: {
1669
+ width: 32,
1670
+ height: 32,
1671
+ borderRadius: "50%",
1672
+ background: voiceListening ? primary : "transparent",
1673
+ color: voiceListening ? onPrimary : "inherit",
1674
+ border: "none",
1675
+ cursor: sending ? "default" : "pointer",
1676
+ opacity: sending ? 0.35 : voiceListening ? 1 : 0.7,
1677
+ fontSize: 16,
1678
+ lineHeight: 1,
1679
+ padding: 0,
1680
+ display: "flex",
1681
+ alignItems: "center",
1682
+ justifyContent: "center",
1683
+ flexShrink: 0,
1684
+ alignSelf: "center",
1685
+ transition: "opacity 120ms ease, background 120ms ease, color 120ms ease"
1686
+ },
1687
+ children: "\u{1F399}\uFE0F"
1688
+ }
1689
+ ),
1690
+ /* @__PURE__ */ jsx(
1691
+ "textarea",
1692
+ {
1693
+ ref: inputRef,
1694
+ className: "chatbotlite-input",
1695
+ rows: 1,
1696
+ value: input,
1697
+ onChange: (e) => {
1698
+ setInput(e.target.value);
1699
+ const el = e.currentTarget;
1700
+ el.style.height = "auto";
1701
+ el.style.height = Math.min(el.scrollHeight, 100) + "px";
1702
+ },
1703
+ onKeyDown: (e) => {
1704
+ if (e.key === "Enter" && !e.shiftKey) {
1705
+ e.preventDefault();
1706
+ void send();
1707
+ }
1708
+ },
1709
+ placeholder: "Message",
1710
+ disabled: sending,
1711
+ style: {
1712
+ flex: 1,
1713
+ padding: "7px 4px",
1714
+ border: "none",
1715
+ background: "transparent",
1716
+ fontSize: 14.5,
1717
+ fontFamily: FONT_STACK,
1718
+ color: TEXT_BODY,
1719
+ outline: "none",
1720
+ resize: "none",
1721
+ lineHeight: 1.35,
1722
+ maxHeight: 100,
1723
+ minHeight: 20
1724
+ }
1725
+ }
1726
+ ),
1727
+ /* @__PURE__ */ jsx(
1728
+ "button",
1729
+ {
1730
+ className: "chatbotlite-send",
1731
+ onClick: () => void send(),
1732
+ disabled: sending || !input.trim() && files.length === 0,
1733
+ "aria-label": "Send message",
1734
+ style: {
1735
+ width: 34,
1736
+ height: 34,
1737
+ borderRadius: "50%",
1738
+ background: primary,
1739
+ color: onPrimary,
1740
+ border: "none",
1741
+ fontSize: 14,
1742
+ fontWeight: 600,
1743
+ fontFamily: FONT_STACK,
1744
+ cursor: sending || !input.trim() && files.length === 0 ? "default" : "pointer",
1745
+ opacity: sending || !input.trim() && files.length === 0 ? 0.35 : 1,
1746
+ display: "flex",
1747
+ alignItems: "center",
1748
+ justifyContent: "center",
1749
+ flexShrink: 0,
1750
+ padding: 0,
1751
+ transition: "opacity 120ms ease, transform 80ms ease"
1752
+ },
1753
+ children: /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
1754
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "19", x2: "12", y2: "5" }),
1755
+ /* @__PURE__ */ jsx("polyline", { points: "5 12 12 5 19 12" })
1756
+ ] })
1757
+ }
1758
+ )
1759
+ ] }) }),
1681
1760
  showBranding && /* @__PURE__ */ jsxs(
1682
1761
  "a",
1683
1762
  {