chatbotlite 0.2.0 → 0.3.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.
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ var fs = require('fs');
4
+ var path = require('path');
5
+
6
+ // src/node/index.ts
7
+ function knowledgeFromDir(dir, opts = {}) {
8
+ const exts = (opts.exts ?? [".md", ".markdown", ".txt"]).map((e) => e.toLowerCase());
9
+ const useHeaders = opts.headers ?? true;
10
+ const abs = path.resolve(dir);
11
+ const stat = fs.statSync(abs);
12
+ if (!stat.isDirectory()) {
13
+ throw new Error(`chatbotlite: ${abs} is not a directory.`);
14
+ }
15
+ const files = fs.readdirSync(abs).filter((f) => exts.includes(path.extname(f).toLowerCase())).sort();
16
+ if (files.length === 0) {
17
+ throw new Error(`chatbotlite: no ${exts.join("/")} files found in ${abs}.`);
18
+ }
19
+ const parts = [];
20
+ for (const f of files) {
21
+ const content = fs.readFileSync(path.join(abs, f), "utf8").trim();
22
+ if (useHeaders) {
23
+ const heading = path.basename(f, path.extname(f));
24
+ parts.push(`# ${heading}
25
+
26
+ ${content}`);
27
+ } else {
28
+ parts.push(content);
29
+ }
30
+ }
31
+ return parts.join("\n\n");
32
+ }
33
+ function knowledgeFromFile(path$1) {
34
+ const abs = path.resolve(path$1);
35
+ return fs.readFileSync(abs, "utf8");
36
+ }
37
+
38
+ exports.knowledgeFromDir = knowledgeFromDir;
39
+ exports.knowledgeFromFile = knowledgeFromFile;
40
+ //# sourceMappingURL=index.cjs.map
41
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/node/index.ts"],"names":["resolve","statSync","readdirSync","extname","readFileSync","join","basename","path"],"mappings":";;;;;;AA2BO,SAAS,gBAAA,CACd,GAAA,EACA,IAAA,GAA+C,EAAC,EACxC;AACR,EAAA,MAAM,IAAA,GAAA,CAAQ,IAAA,CAAK,IAAA,IAAQ,CAAC,KAAA,EAAO,WAAA,EAAa,MAAM,CAAA,EAAG,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,aAAa,CAAA;AACnF,EAAA,MAAM,UAAA,GAAa,KAAK,OAAA,IAAW,IAAA;AACnC,EAAA,MAAM,GAAA,GAAMA,aAAQ,GAAG,CAAA;AACvB,EAAA,MAAM,IAAA,GAAOC,YAAS,GAAG,CAAA;AACzB,EAAA,IAAI,CAAC,IAAA,CAAK,WAAA,EAAY,EAAG;AACvB,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,aAAA,EAAgB,GAAG,CAAA,oBAAA,CAAsB,CAAA;AAAA,EAC3D;AACA,EAAA,MAAM,QAAQC,cAAA,CAAY,GAAG,CAAA,CAC1B,MAAA,CAAO,CAAC,CAAA,KAAM,IAAA,CAAK,QAAA,CAASC,YAAA,CAAQ,CAAC,CAAA,CAAE,WAAA,EAAa,CAAC,EACrD,IAAA,EAAK;AACR,EAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AACtB,IAAA,MAAM,IAAI,MAAM,CAAA,gBAAA,EAAmB,IAAA,CAAK,KAAK,GAAG,CAAC,CAAA,gBAAA,EAAmB,GAAG,CAAA,CAAA,CAAG,CAAA;AAAA,EAC5E;AACA,EAAA,MAAM,QAAkB,EAAC;AACzB,EAAA,KAAA,MAAW,KAAK,KAAA,EAAO;AACrB,IAAA,MAAM,OAAA,GAAUC,gBAAaC,SAAA,CAAK,GAAA,EAAK,CAAC,CAAA,EAAG,MAAM,EAAE,IAAA,EAAK;AACxD,IAAA,IAAI,UAAA,EAAY;AACd,MAAA,MAAM,OAAA,GAAUC,aAAA,CAAS,CAAA,EAAGH,YAAA,CAAQ,CAAC,CAAC,CAAA;AACtC,MAAA,KAAA,CAAM,IAAA,CAAK,KAAK,OAAO;;AAAA,EAAO,OAAO,CAAA,CAAE,CAAA;AAAA,IACzC,CAAA,MAAO;AACL,MAAA,KAAA,CAAM,KAAK,OAAO,CAAA;AAAA,IACpB;AAAA,EACF;AACA,EAAA,OAAO,KAAA,CAAM,KAAK,MAAM,CAAA;AAC1B;AAWO,SAAS,kBAAkBI,MAAA,EAAsB;AACtD,EAAA,MAAM,GAAA,GAAMP,aAAQO,MAAI,CAAA;AACxB,EAAA,OAAOH,eAAA,CAAa,KAAK,MAAM,CAAA;AACjC","file":"index.cjs","sourcesContent":["// Node-only helpers — use these in your server code.\n// Browser code should pass knowledge as a string directly.\n\nimport { readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { join, basename, extname, resolve } from \"node:path\";\n\n/**\n * Load a folder of markdown / text files as a single concatenated knowledge string.\n *\n * Each file becomes a top-level section in the output, headed by its filename\n * (without extension). Files are concatenated in alphabetical order.\n *\n * @example\n * ```ts\n * import { ChatBot } from \"chatbotlite\";\n * import { knowledgeFromDir } from \"chatbotlite/node\";\n *\n * const bot = new ChatBot({\n * knowledge: knowledgeFromDir(\"./kb\"),\n * providers: { keys: { openai: process.env.OPENAI_API_KEY! } }\n * });\n * ```\n *\n * @param dir Path to the folder (absolute or relative to cwd).\n * @param opts.exts File extensions to include (default `[\".md\", \".markdown\", \".txt\"]`).\n * @param opts.headers If true (default), wrap each file's content in a `# filename` heading.\n */\nexport function knowledgeFromDir(\n dir: string,\n opts: { exts?: string[]; headers?: boolean } = {}\n): string {\n const exts = (opts.exts ?? [\".md\", \".markdown\", \".txt\"]).map((e) => e.toLowerCase());\n const useHeaders = opts.headers ?? true;\n const abs = resolve(dir);\n const stat = statSync(abs);\n if (!stat.isDirectory()) {\n throw new Error(`chatbotlite: ${abs} is not a directory.`);\n }\n const files = readdirSync(abs)\n .filter((f) => exts.includes(extname(f).toLowerCase()))\n .sort();\n if (files.length === 0) {\n throw new Error(`chatbotlite: no ${exts.join(\"/\")} files found in ${abs}.`);\n }\n const parts: string[] = [];\n for (const f of files) {\n const content = readFileSync(join(abs, f), \"utf8\").trim();\n if (useHeaders) {\n const heading = basename(f, extname(f));\n parts.push(`# ${heading}\\n\\n${content}`);\n } else {\n parts.push(content);\n }\n }\n return parts.join(\"\\n\\n\");\n}\n\n/**\n * Load a single markdown/text file as a knowledge string.\n *\n * @example\n * ```ts\n * import { knowledgeFromFile } from \"chatbotlite/node\";\n * const knowledge = knowledgeFromFile(\"./business.md\");\n * ```\n */\nexport function knowledgeFromFile(path: string): string {\n const abs = resolve(path);\n return readFileSync(abs, \"utf8\");\n}\n"]}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Load a folder of markdown / text files as a single concatenated knowledge string.
3
+ *
4
+ * Each file becomes a top-level section in the output, headed by its filename
5
+ * (without extension). Files are concatenated in alphabetical order.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { ChatBot } from "chatbotlite";
10
+ * import { knowledgeFromDir } from "chatbotlite/node";
11
+ *
12
+ * const bot = new ChatBot({
13
+ * knowledge: knowledgeFromDir("./kb"),
14
+ * providers: { keys: { openai: process.env.OPENAI_API_KEY! } }
15
+ * });
16
+ * ```
17
+ *
18
+ * @param dir Path to the folder (absolute or relative to cwd).
19
+ * @param opts.exts File extensions to include (default `[".md", ".markdown", ".txt"]`).
20
+ * @param opts.headers If true (default), wrap each file's content in a `# filename` heading.
21
+ */
22
+ declare function knowledgeFromDir(dir: string, opts?: {
23
+ exts?: string[];
24
+ headers?: boolean;
25
+ }): string;
26
+ /**
27
+ * Load a single markdown/text file as a knowledge string.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * import { knowledgeFromFile } from "chatbotlite/node";
32
+ * const knowledge = knowledgeFromFile("./business.md");
33
+ * ```
34
+ */
35
+ declare function knowledgeFromFile(path: string): string;
36
+
37
+ export { knowledgeFromDir, knowledgeFromFile };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Load a folder of markdown / text files as a single concatenated knowledge string.
3
+ *
4
+ * Each file becomes a top-level section in the output, headed by its filename
5
+ * (without extension). Files are concatenated in alphabetical order.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { ChatBot } from "chatbotlite";
10
+ * import { knowledgeFromDir } from "chatbotlite/node";
11
+ *
12
+ * const bot = new ChatBot({
13
+ * knowledge: knowledgeFromDir("./kb"),
14
+ * providers: { keys: { openai: process.env.OPENAI_API_KEY! } }
15
+ * });
16
+ * ```
17
+ *
18
+ * @param dir Path to the folder (absolute or relative to cwd).
19
+ * @param opts.exts File extensions to include (default `[".md", ".markdown", ".txt"]`).
20
+ * @param opts.headers If true (default), wrap each file's content in a `# filename` heading.
21
+ */
22
+ declare function knowledgeFromDir(dir: string, opts?: {
23
+ exts?: string[];
24
+ headers?: boolean;
25
+ }): string;
26
+ /**
27
+ * Load a single markdown/text file as a knowledge string.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * import { knowledgeFromFile } from "chatbotlite/node";
32
+ * const knowledge = knowledgeFromFile("./business.md");
33
+ * ```
34
+ */
35
+ declare function knowledgeFromFile(path: string): string;
36
+
37
+ export { knowledgeFromDir, knowledgeFromFile };
@@ -0,0 +1,38 @@
1
+ import { statSync, readdirSync, readFileSync } from 'fs';
2
+ import { resolve, extname, join, basename } from 'path';
3
+
4
+ // src/node/index.ts
5
+ function knowledgeFromDir(dir, opts = {}) {
6
+ const exts = (opts.exts ?? [".md", ".markdown", ".txt"]).map((e) => e.toLowerCase());
7
+ const useHeaders = opts.headers ?? true;
8
+ const abs = resolve(dir);
9
+ const stat = statSync(abs);
10
+ if (!stat.isDirectory()) {
11
+ throw new Error(`chatbotlite: ${abs} is not a directory.`);
12
+ }
13
+ const files = readdirSync(abs).filter((f) => exts.includes(extname(f).toLowerCase())).sort();
14
+ if (files.length === 0) {
15
+ throw new Error(`chatbotlite: no ${exts.join("/")} files found in ${abs}.`);
16
+ }
17
+ const parts = [];
18
+ for (const f of files) {
19
+ const content = readFileSync(join(abs, f), "utf8").trim();
20
+ if (useHeaders) {
21
+ const heading = basename(f, extname(f));
22
+ parts.push(`# ${heading}
23
+
24
+ ${content}`);
25
+ } else {
26
+ parts.push(content);
27
+ }
28
+ }
29
+ return parts.join("\n\n");
30
+ }
31
+ function knowledgeFromFile(path) {
32
+ const abs = resolve(path);
33
+ return readFileSync(abs, "utf8");
34
+ }
35
+
36
+ export { knowledgeFromDir, knowledgeFromFile };
37
+ //# sourceMappingURL=index.js.map
38
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/node/index.ts"],"names":[],"mappings":";;;;AA2BO,SAAS,gBAAA,CACd,GAAA,EACA,IAAA,GAA+C,EAAC,EACxC;AACR,EAAA,MAAM,IAAA,GAAA,CAAQ,IAAA,CAAK,IAAA,IAAQ,CAAC,KAAA,EAAO,WAAA,EAAa,MAAM,CAAA,EAAG,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,aAAa,CAAA;AACnF,EAAA,MAAM,UAAA,GAAa,KAAK,OAAA,IAAW,IAAA;AACnC,EAAA,MAAM,GAAA,GAAM,QAAQ,GAAG,CAAA;AACvB,EAAA,MAAM,IAAA,GAAO,SAAS,GAAG,CAAA;AACzB,EAAA,IAAI,CAAC,IAAA,CAAK,WAAA,EAAY,EAAG;AACvB,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,aAAA,EAAgB,GAAG,CAAA,oBAAA,CAAsB,CAAA;AAAA,EAC3D;AACA,EAAA,MAAM,QAAQ,WAAA,CAAY,GAAG,CAAA,CAC1B,MAAA,CAAO,CAAC,CAAA,KAAM,IAAA,CAAK,QAAA,CAAS,OAAA,CAAQ,CAAC,CAAA,CAAE,WAAA,EAAa,CAAC,EACrD,IAAA,EAAK;AACR,EAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AACtB,IAAA,MAAM,IAAI,MAAM,CAAA,gBAAA,EAAmB,IAAA,CAAK,KAAK,GAAG,CAAC,CAAA,gBAAA,EAAmB,GAAG,CAAA,CAAA,CAAG,CAAA;AAAA,EAC5E;AACA,EAAA,MAAM,QAAkB,EAAC;AACzB,EAAA,KAAA,MAAW,KAAK,KAAA,EAAO;AACrB,IAAA,MAAM,OAAA,GAAU,aAAa,IAAA,CAAK,GAAA,EAAK,CAAC,CAAA,EAAG,MAAM,EAAE,IAAA,EAAK;AACxD,IAAA,IAAI,UAAA,EAAY;AACd,MAAA,MAAM,OAAA,GAAU,QAAA,CAAS,CAAA,EAAG,OAAA,CAAQ,CAAC,CAAC,CAAA;AACtC,MAAA,KAAA,CAAM,IAAA,CAAK,KAAK,OAAO;;AAAA,EAAO,OAAO,CAAA,CAAE,CAAA;AAAA,IACzC,CAAA,MAAO;AACL,MAAA,KAAA,CAAM,KAAK,OAAO,CAAA;AAAA,IACpB;AAAA,EACF;AACA,EAAA,OAAO,KAAA,CAAM,KAAK,MAAM,CAAA;AAC1B;AAWO,SAAS,kBAAkB,IAAA,EAAsB;AACtD,EAAA,MAAM,GAAA,GAAM,QAAQ,IAAI,CAAA;AACxB,EAAA,OAAO,YAAA,CAAa,KAAK,MAAM,CAAA;AACjC","file":"index.js","sourcesContent":["// Node-only helpers — use these in your server code.\n// Browser code should pass knowledge as a string directly.\n\nimport { readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { join, basename, extname, resolve } from \"node:path\";\n\n/**\n * Load a folder of markdown / text files as a single concatenated knowledge string.\n *\n * Each file becomes a top-level section in the output, headed by its filename\n * (without extension). Files are concatenated in alphabetical order.\n *\n * @example\n * ```ts\n * import { ChatBot } from \"chatbotlite\";\n * import { knowledgeFromDir } from \"chatbotlite/node\";\n *\n * const bot = new ChatBot({\n * knowledge: knowledgeFromDir(\"./kb\"),\n * providers: { keys: { openai: process.env.OPENAI_API_KEY! } }\n * });\n * ```\n *\n * @param dir Path to the folder (absolute or relative to cwd).\n * @param opts.exts File extensions to include (default `[\".md\", \".markdown\", \".txt\"]`).\n * @param opts.headers If true (default), wrap each file's content in a `# filename` heading.\n */\nexport function knowledgeFromDir(\n dir: string,\n opts: { exts?: string[]; headers?: boolean } = {}\n): string {\n const exts = (opts.exts ?? [\".md\", \".markdown\", \".txt\"]).map((e) => e.toLowerCase());\n const useHeaders = opts.headers ?? true;\n const abs = resolve(dir);\n const stat = statSync(abs);\n if (!stat.isDirectory()) {\n throw new Error(`chatbotlite: ${abs} is not a directory.`);\n }\n const files = readdirSync(abs)\n .filter((f) => exts.includes(extname(f).toLowerCase()))\n .sort();\n if (files.length === 0) {\n throw new Error(`chatbotlite: no ${exts.join(\"/\")} files found in ${abs}.`);\n }\n const parts: string[] = [];\n for (const f of files) {\n const content = readFileSync(join(abs, f), \"utf8\").trim();\n if (useHeaders) {\n const heading = basename(f, extname(f));\n parts.push(`# ${heading}\\n\\n${content}`);\n } else {\n parts.push(content);\n }\n }\n return parts.join(\"\\n\\n\");\n}\n\n/**\n * Load a single markdown/text file as a knowledge string.\n *\n * @example\n * ```ts\n * import { knowledgeFromFile } from \"chatbotlite/node\";\n * const knowledge = knowledgeFromFile(\"./business.md\");\n * ```\n */\nexport function knowledgeFromFile(path: string): string {\n const abs = resolve(path);\n return readFileSync(abs, \"utf8\");\n}\n"]}
@@ -6,56 +6,21 @@ var jsxRuntime = require('react/jsx-runtime');
6
6
  // src/react/ChatWidget.tsx
7
7
 
8
8
  // src/core/prompts.ts
9
- function buildSystemPrompt(business) {
10
- const parts = [];
11
- parts.push(`You are an AI receptionist for ${business.name}.`);
12
- if (business.description) parts.push(business.description);
13
- parts.push("");
14
- if (business.services && business.services.length > 0) {
15
- parts.push("Services offered (only these \u2014 do not invent others):");
16
- for (const s of business.services) {
17
- const price = s.price ? ` \u2014 ${s.price}` : "";
18
- const notes = s.notes ? ` (${s.notes})` : "";
19
- parts.push(`- ${s.name}${price}${notes}`);
20
- }
21
- parts.push("");
22
- }
23
- if (business.hours) {
24
- parts.push(`Hours: ${business.hours}`);
25
- }
26
- if (business.serviceArea && business.serviceArea.length > 0) {
27
- parts.push(`Service area: ${business.serviceArea.join(", ")}.`);
28
- parts.push("If the caller is outside this area, say so and recommend owner review.");
29
- }
30
- if (business.policies && business.policies.length > 0) {
31
- parts.push("");
32
- parts.push("Known policies (use these exact answers when asked):");
33
- for (const p of business.policies) {
34
- parts.push(`- ${p.topic}: ${p.answer}`);
35
- }
36
- }
37
- parts.push("");
38
- parts.push("Rules:");
39
- parts.push("- Reply in 1-2 short sentences, conversational tone.");
40
- parts.push("- NEVER invent prices, availability, dispatch times, or appointment confirmations.");
41
- parts.push("- For anything not covered in the setup above, say it needs owner review.");
42
- parts.push('- 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."');
43
- parts.push(`- If wrong number or asked to stop, say: "Sorry about that. We won't text again."`);
44
- if (business.doNotPromise && business.doNotPromise.length > 0) {
45
- parts.push("");
46
- parts.push("Never promise:");
47
- for (const p of business.doNotPromise) parts.push(`- ${p}`);
48
- }
49
- if (business.customInstructions) {
50
- parts.push("");
51
- parts.push("Additional instructions:");
52
- parts.push(business.customInstructions);
53
- }
54
- if (business.language && business.language !== "en") {
55
- parts.push("");
56
- parts.push(`Reply in ${business.language}.`);
57
- }
58
- return parts.join("\n");
9
+ function buildSystemPrompt(knowledge) {
10
+ return [
11
+ "You are an AI assistant on a business website. Use ONLY the knowledge below to answer.",
12
+ "",
13
+ "## Business knowledge",
14
+ knowledge.trim(),
15
+ "",
16
+ "## Reply rules",
17
+ "- Reply in 1-2 short sentences, conversational tone.",
18
+ "- NEVER invent prices, availability, dispatch times, appointment confirmations, or facts not present in the business knowledge above.",
19
+ "- For anything not covered in the knowledge above, say the owner will follow up \u2014 do NOT guess.",
20
+ '- 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."',
21
+ `- If wrong number or asked to stop, say: "Sorry about that. We won't text again."`,
22
+ "- Match the caller's language automatically."
23
+ ].join("\n");
59
24
  }
60
25
 
61
26
  // src/core/guards.ts
@@ -119,24 +84,6 @@ var PROVIDER_NAMES = /* @__PURE__ */ new Set([
119
84
  "openrouter",
120
85
  "moonshot"
121
86
  ]);
122
- function parseChainSpec(spec) {
123
- const slash = spec.indexOf("/");
124
- if (slash === -1) {
125
- if (!PROVIDER_NAMES.has(spec)) {
126
- throw new Error(`chatbotlite: unknown provider "${spec}". Use "provider/model" or a known provider name.`);
127
- }
128
- return { provider: spec, model: null };
129
- }
130
- const provider = spec.slice(0, slash);
131
- const model = spec.slice(slash + 1);
132
- if (!PROVIDER_NAMES.has(provider)) {
133
- throw new Error(`chatbotlite: unknown provider "${provider}" in chain spec "${spec}".`);
134
- }
135
- if (!model) {
136
- throw new Error(`chatbotlite: empty model name in chain spec "${spec}".`);
137
- }
138
- return { provider, model };
139
- }
140
87
  function isKnownProvider(name) {
141
88
  return PROVIDER_NAMES.has(name);
142
89
  }
@@ -162,19 +109,20 @@ function isRetryableError(err) {
162
109
 
163
110
  // src/client/chatbot.ts
164
111
  var ChatBot = class {
165
- business;
166
112
  steps;
167
113
  keys;
168
114
  fetcher;
169
115
  timeoutMs;
170
116
  cachedSystemPrompt;
171
117
  constructor(init) {
172
- this.business = init.business;
118
+ if (!init.knowledge || typeof init.knowledge !== "string" || init.knowledge.trim().length === 0) {
119
+ throw new Error("chatbotlite: knowledge is required (a non-empty markdown string).");
120
+ }
173
121
  this.keys = init.providers.keys ?? {};
174
122
  this.steps = resolveChain(init.providers);
175
123
  this.fetcher = init.options?.fetch ?? globalThis.fetch.bind(globalThis);
176
124
  this.timeoutMs = init.options?.timeoutMs ?? 3e4;
177
- this.cachedSystemPrompt = buildSystemPrompt(this.business);
125
+ this.cachedSystemPrompt = buildSystemPrompt(init.knowledge);
178
126
  }
179
127
  async reply(message, opts = {}) {
180
128
  const systemPrompt = opts.systemPrompt ?? this.cachedSystemPrompt;
@@ -211,7 +159,7 @@ var ChatBot = class {
211
159
  latencyMs: Date.now() - t0
212
160
  });
213
161
  if (!isRetryableError(err)) {
214
- throw new Error(`chatbotlite: ${step.provider}/${step.model} failed (non-retryable). ${errMsg}`);
162
+ throw new Error(`chatbotlite: ${step.label} failed (non-retryable). ${errMsg}`);
215
163
  }
216
164
  }
217
165
  }
@@ -268,30 +216,19 @@ function resolveChain(providers) {
268
216
  return orderedProviders.map((provider) => ({
269
217
  provider,
270
218
  model: PROVIDER_ENDPOINTS[provider].defaultModel,
271
- spec: `${provider}/${PROVIDER_ENDPOINTS[provider].defaultModel}`
219
+ label: `${provider}/${PROVIDER_ENDPOINTS[provider].defaultModel}`
272
220
  }));
273
221
  }
274
222
  function normalizeChainEntry(entry, keys) {
275
- let provider;
276
- let model;
277
- let spec;
278
- if (typeof entry === "string") {
279
- const parsed = parseChainSpec(entry);
280
- provider = parsed.provider;
281
- model = parsed.model ?? PROVIDER_ENDPOINTS[provider].defaultModel;
282
- spec = entry;
283
- } else {
284
- if (!isKnownProvider(entry.provider)) {
285
- throw new Error(`chatbotlite: unknown provider "${entry.provider}" in chain entry.`);
286
- }
287
- provider = entry.provider;
288
- model = entry.model ?? PROVIDER_ENDPOINTS[provider].defaultModel;
289
- spec = `${provider}/${model}`;
223
+ if (!isKnownProvider(entry.provider)) {
224
+ throw new Error(`chatbotlite: unknown provider "${entry.provider}" in chain entry.`);
290
225
  }
226
+ const provider = entry.provider;
227
+ const model = entry.model ?? PROVIDER_ENDPOINTS[provider].defaultModel;
291
228
  if (!keys[provider]) {
292
- throw new Error(`chatbotlite: chain step "${spec}" needs a key for provider "${provider}" but none was provided.`);
229
+ throw new Error(`chatbotlite: chain entry for "${provider}" needs a matching key in providers.keys.`);
293
230
  }
294
- return { provider, model, spec };
231
+ return { provider, model, label: `${provider}/${model}` };
295
232
  }
296
233
  var BOLT = "\u26A1";
297
234
  var DEFAULT_PRIMARY = "#0f172a";
@@ -342,8 +279,8 @@ function ChatWidget(props) {
342
279
  position = "bottom-right"
343
280
  } = props;
344
281
  const isEndpointMode = "endpoint" in props && typeof props.endpoint === "string";
345
- const resolvedTitle = title ?? props.business?.name ?? "Chat";
346
- const resolvedGreeting = greeting ?? (props.business ? `Hi! I'm here for ${props.business.name}. How can we help?` : "Hi! How can we help?");
282
+ const resolvedTitle = title ?? "Chat";
283
+ const resolvedGreeting = greeting ?? "Hi! How can we help?";
347
284
  const primary = themeOverrides?.primary ?? DEFAULT_PRIMARY;
348
285
  const onPrimary = themeOverrides?.onPrimary ?? DEFAULT_ON_PRIMARY;
349
286
  const [open, setOpen] = react.useState(false);
@@ -359,9 +296,9 @@ function ChatWidget(props) {
359
296
  }, []);
360
297
  const bot = react.useMemo(() => {
361
298
  if (isEndpointMode) return null;
362
- if (!props.business || !props.providers) return null;
363
- return new ChatBot({ business: props.business, providers: props.providers });
364
- }, [isEndpointMode, props.business, props.providers]);
299
+ if (!props.knowledge || !props.providers) return null;
300
+ return new ChatBot({ knowledge: props.knowledge, providers: props.providers });
301
+ }, [isEndpointMode, props.knowledge, props.providers]);
365
302
  react.useEffect(() => {
366
303
  if (scrollRef.current) {
367
304
  scrollRef.current.scrollTop = scrollRef.current.scrollHeight;