@infuro/cms-core 1.0.19 → 1.0.20

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/dist/admin.cjs CHANGED
@@ -401,7 +401,7 @@ var defaultValue = {
401
401
  var AdminConfigContext = (0, import_react4.createContext)(defaultValue);
402
402
 
403
403
  // src/lib/cms-version.ts
404
- var CMS_VERSION = true ? "1.0.19" : "0.0.0";
404
+ var CMS_VERSION = true ? "1.0.20" : "0.0.0";
405
405
 
406
406
  // src/components/Admin/Sidebar.tsx
407
407
  var import_jsx_runtime4 = require("react/jsx-runtime");
@@ -546,6 +546,10 @@ function AdminSidebar({ variant = "sidebar" }) {
546
546
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react3.Shield, { className: `h-4 w-4 mr-2 ${isActive("/admin/roles") ? iconActive : iconInactive}` }),
547
547
  "Roles"
548
548
  ] }) }),
549
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_link2.default, { href: "/admin/llm_agents", className: `${linkCls} ${isActive("/admin/llm_agents") ? linkActive : linkInactive}`, children: [
550
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react3.Bot, { className: `h-4 w-4 mr-2 ${isActive("/admin/llm_agents") ? iconActive : iconInactive}` }),
551
+ "LLM agents"
552
+ ] }) }),
549
553
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_link2.default, { href: "/admin/plugins", className: `${linkCls} ${isActive("/admin/plugins") ? linkActive : linkInactive}`, children: [
550
554
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react3.Puzzle, { className: `h-4 w-4 mr-2 ${isActive("/admin/plugins") ? iconActive : iconInactive}` }),
551
555
  "Plugins"
@@ -1404,7 +1408,9 @@ function CreateEditForm({ isOpen, onClose, apiEndpoint, columns, existingData })
1404
1408
  if (existingData) {
1405
1409
  setFormData(existingData);
1406
1410
  } else {
1407
- setFormData(columns.reduce((acc, col) => ({ ...acc, [col.field]: col.defaultValue || "" }), {}));
1411
+ setFormData(
1412
+ columns.filter((col) => !col.hideInForm).reduce((acc, col) => ({ ...acc, [col.field]: col.defaultValue || "" }), {})
1413
+ );
1408
1414
  }
1409
1415
  }, [existingData, columns]);
1410
1416
  const handleChange = (e, field) => {
@@ -1418,7 +1424,7 @@ function CreateEditForm({ isOpen, onClose, apiEndpoint, columns, existingData })
1418
1424
  };
1419
1425
  const validateForm = () => {
1420
1426
  let newErrors = {};
1421
- columns.forEach((col) => {
1427
+ columns.filter((col) => !col.hideInForm).forEach((col) => {
1422
1428
  if (col.validation?.required && !formData[col.field]) {
1423
1429
  newErrors[col.field] = `${col.displayName} is required`;
1424
1430
  }
@@ -1458,7 +1464,7 @@ Note: ${result.note}`);
1458
1464
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Button, { type: "button", variant: "ghost", size: "icon", onClick: onClose, className: "h-8 w-8", "aria-label": "Close", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react8.X, { className: "h-5 w-5" }) })
1459
1465
  ] }),
1460
1466
  /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("form", { onSubmit: handleSubmit, className: "flex flex-col flex-1 min-h-0", children: [
1461
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "flex-1 min-h-0 overflow-y-auto p-6 space-y-4", children: columns.map((col) => /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { children: [
1467
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "flex-1 min-h-0 overflow-y-auto p-6 space-y-4", children: columns.filter((col) => !col.hideInForm).map((col) => /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { children: [
1462
1468
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Label3, { className: "block text-sm font-medium mb-1", children: col.displayName }),
1463
1469
  col.relationApi ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1464
1470
  RelationAutocomplete,
@@ -1470,10 +1476,35 @@ Note: ${result.note}`);
1470
1476
  valueField: col.relationValueField ?? "id",
1471
1477
  placeholder: `Select ${col.displayName}`
1472
1478
  }
1473
- ) : col.type === "textarea" ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Textarea, { value: formData[col.field] || "", onChange: (e) => handleChange(e, col.field) }) : col.type === "select" ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(Select, { onValueChange: (value) => setFormData({ ...formData, [col.field]: value }), children: [
1479
+ ) : col.type === "textarea" ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1480
+ Textarea,
1481
+ {
1482
+ rows: typeof col.textareaRows === "number" ? col.textareaRows : 4,
1483
+ className: "min-h-[80px] font-mono text-sm",
1484
+ value: formData[col.field] ?? "",
1485
+ onChange: (e) => handleChange(e, col.field),
1486
+ placeholder: col.placeholder
1487
+ }
1488
+ ) : col.type === "select" ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(Select, { onValueChange: (value) => setFormData({ ...formData, [col.field]: value }), children: [
1474
1489
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(SelectTrigger, { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(SelectValue, { placeholder: formData[col.field] || "Select an option" }) }),
1475
1490
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(SelectContent, { children: col.options?.map((option) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(SelectItem, { value: option.value, children: option.label }, option.value)) })
1476
- ] }) : col.type === "boolean" ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Switch, { checked: formData[col.field] || false, onCheckedChange: () => handleToggleChange(col.field) }) : col.type === "date" ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Input, { type: "date", value: formData[col.field] || "", onChange: (e) => handleChange(e, col.field) }) : col.type === "password" ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Input, { type: "password", value: formData[col.field] || "", onChange: (e) => handleChange(e, col.field) }) : col.type === "number" ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Input, { type: "number", value: formData[col.field] || "", onChange: (e) => handleChange(e, col.field) }) : col.type === "file" ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(FileUpload, { onUploadSuccess: (url) => handleFileUpload(col.field, url) }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Input, { type: col.type || "text", value: formData[col.field] || "", onChange: (e) => handleChange(e, col.field) }),
1491
+ ] }) : col.type === "boolean" ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Switch, { checked: formData[col.field] || false, onCheckedChange: () => handleToggleChange(col.field) }) : col.type === "date" ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Input, { type: "date", value: formData[col.field] || "", onChange: (e) => handleChange(e, col.field) }) : col.type === "password" ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Input, { type: "password", value: formData[col.field] || "", onChange: (e) => handleChange(e, col.field) }) : col.type === "number" ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1492
+ Input,
1493
+ {
1494
+ type: "number",
1495
+ placeholder: col.placeholder,
1496
+ value: formData[col.field] ?? "",
1497
+ onChange: (e) => handleChange(e, col.field)
1498
+ }
1499
+ ) : col.type === "file" ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(FileUpload, { onUploadSuccess: (url) => handleFileUpload(col.field, url) }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1500
+ Input,
1501
+ {
1502
+ type: col.type || "text",
1503
+ placeholder: col.placeholder,
1504
+ value: formData[col.field] ?? "",
1505
+ onChange: (e) => handleChange(e, col.field)
1506
+ }
1507
+ ),
1477
1508
  errors[col.field] && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { className: "text-red-500 text-sm mt-1", children: errors[col.field] })
1478
1509
  ] }, col.field)) }),
1479
1510
  /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex gap-2 p-4 border-t border-gray-200 shrink-0 bg-white", children: [
@@ -2076,6 +2107,10 @@ function AdminCRUD({
2076
2107
  const hasLoadedRef = (0, import_react13.useRef)(false);
2077
2108
  const isMobile = useIsMobile();
2078
2109
  const showGroupColumn = !!manageUserGroups && roleOptions.length > 0;
2110
+ const listColumns = (0, import_react13.useMemo)(
2111
+ () => Array.isArray(columns) ? columns.filter((c) => !c.hideInTable) : [],
2112
+ [columns]
2113
+ );
2079
2114
  (0, import_react13.useEffect)(() => {
2080
2115
  const timeoutId = setTimeout(() => {
2081
2116
  if (searchInput !== searchQuery) {
@@ -2526,7 +2561,7 @@ function AdminCRUD({
2526
2561
  /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "ml-2", children: "Refreshing list..." })
2527
2562
  ] }),
2528
2563
  isMobile ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { className: "flex flex-col gap-2 min-w-0", children: data && data.length > 0 ? data.map((item, index) => {
2529
- const displayCols = columns?.slice(0, 4) ?? [];
2564
+ const displayCols = listColumns?.slice(0, 4) ?? [];
2530
2565
  const primary = displayCols[0];
2531
2566
  const primaryVal = primary ? getNestedValue(item, primary.field || primary.key) : null;
2532
2567
  return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
@@ -2600,7 +2635,7 @@ function AdminCRUD({
2600
2635
  );
2601
2636
  }) : /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { className: "rounded-md border border-gray-200 p-6 text-center text-gray-500", children: "No data found" }) }) : /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { className: "overflow-x-auto min-w-0 rounded-md border border-gray-200", children: /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(Table, { children: [
2602
2637
  /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(TableHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(TableRow, { children: [
2603
- columns && columns.map((col) => /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
2638
+ listColumns && listColumns.map((col) => /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
2604
2639
  TableHead,
2605
2640
  {
2606
2641
  className: "cursor-pointer",
@@ -2622,7 +2657,7 @@ function AdminCRUD({
2622
2657
  className: "cursor-pointer",
2623
2658
  onClick: () => handleRowClick(item),
2624
2659
  children: [
2625
- columns && columns.map((col, colIndex) => {
2660
+ listColumns && listColumns.map((col, colIndex) => {
2626
2661
  const fieldKey = col.field || col.key;
2627
2662
  const value = getNestedValue(item, fieldKey);
2628
2663
  return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(TableCell, { children: formatCellValue(value, col) }, `${item.id}-${colIndex}-${fieldKey}`);
@@ -2706,7 +2741,7 @@ function AdminCRUD({
2706
2741
  )) : /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(TableRow, { children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
2707
2742
  TableCell,
2708
2743
  {
2709
- colSpan: columns ? columns.length + 1 + (showGroupColumn ? 1 : 0) : 1,
2744
+ colSpan: listColumns.length ? listColumns.length + 1 + (showGroupColumn ? 1 : 0) : 1,
2710
2745
  className: "text-center py-8",
2711
2746
  children: "No data found"
2712
2747
  }
@@ -9270,6 +9305,10 @@ Checkbox.displayName = CheckboxPrimitive.Root.displayName;
9270
9305
  // src/admin/pages/PluginsPage.tsx
9271
9306
  var import_sonner7 = require("sonner");
9272
9307
  var import_jsx_runtime56 = require("react/jsx-runtime");
9308
+ function slugifyAgentKey(name) {
9309
+ const s = name.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
9310
+ return s || "agent";
9311
+ }
9273
9312
  function normalizeChatMode(raw) {
9274
9313
  if (raw === "external" || raw === "llm") return raw;
9275
9314
  return "whatsapp";
@@ -9429,6 +9468,25 @@ function PluginSettingsPanel({
9429
9468
  const [chatMode, setChatMode] = (0, import_react41.useState)("whatsapp");
9430
9469
  const [whatsappPhone, setWhatsappPhone] = (0, import_react41.useState)("");
9431
9470
  const [externalChatSnippet, setExternalChatSnippet] = (0, import_react41.useState)("");
9471
+ const [attachedAgentSlug, setAttachedAgentSlug] = (0, import_react41.useState)("");
9472
+ const [llmAgents, setLlmAgents] = (0, import_react41.useState)([]);
9473
+ const [agentId, setAgentId] = (0, import_react41.useState)(null);
9474
+ const [agentName, setAgentName] = (0, import_react41.useState)("");
9475
+ const [agentSlug, setAgentSlug] = (0, import_react41.useState)("");
9476
+ const [agentSystem, setAgentSystem] = (0, import_react41.useState)("");
9477
+ const [agentModel, setAgentModel] = (0, import_react41.useState)("");
9478
+ const [agentTemp, setAgentTemp] = (0, import_react41.useState)("");
9479
+ const [agentMaxTokens, setAgentMaxTokens] = (0, import_react41.useState)("");
9480
+ const [agentValidationJson, setAgentValidationJson] = (0, import_react41.useState)("");
9481
+ const [agentLoading, setAgentLoading] = (0, import_react41.useState)(false);
9482
+ const [agentSaving, setAgentSaving] = (0, import_react41.useState)(false);
9483
+ const [kbCatalog, setKbCatalog] = (0, import_react41.useState)([]);
9484
+ const [attachedAgentKnowledge, setAttachedAgentKnowledge] = (0, import_react41.useState)([]);
9485
+ const [attachedKbLoading, setAttachedKbLoading] = (0, import_react41.useState)(false);
9486
+ const [attachExistingDocId, setAttachExistingDocId] = (0, import_react41.useState)("__none__");
9487
+ const [uploadingAttachedKbFile, setUploadingAttachedKbFile] = (0, import_react41.useState)(false);
9488
+ const [uploadingAttachedKbLink, setUploadingAttachedKbLink] = (0, import_react41.useState)(false);
9489
+ const [attachedKbInputKey, setAttachedKbInputKey] = (0, import_react41.useState)(0);
9432
9490
  const [erpPipelineName, setErpPipelineName] = (0, import_react41.useState)("");
9433
9491
  const [erpPipelineStageName, setErpPipelineStageName] = (0, import_react41.useState)("");
9434
9492
  const [erpFormsCatalog, setErpFormsCatalog] = (0, import_react41.useState)([]);
@@ -9452,6 +9510,7 @@ function PluginSettingsPanel({
9452
9510
  setIconImageUrl(data.iconImageUrl ?? "");
9453
9511
  setIconBackgroundColor(data.iconBackgroundColor ?? "#6366f1");
9454
9512
  setHeaderColor(data.headerColor ?? "#6366f1");
9513
+ setAttachedAgentSlug(data.attachedAgentSlug ?? "");
9455
9514
  }
9456
9515
  if (isErp) {
9457
9516
  setErpPipelineName(data.pipelineName ?? data.pipelineId ?? "");
@@ -9506,6 +9565,116 @@ function PluginSettingsPanel({
9506
9565
  setErpFormsCatalog(rows);
9507
9566
  }).catch(() => setErpFormsCatalog([]));
9508
9567
  }, [isErp, loading]);
9568
+ const fetchLlmAgents = (0, import_react41.useCallback)(async () => {
9569
+ const res = await fetch("/api/llm_agents?limit=100&sortField=name&sortOrder=asc");
9570
+ if (!res.ok) {
9571
+ setLlmAgents([]);
9572
+ return;
9573
+ }
9574
+ const j = await res.json();
9575
+ const list = (j.data ?? []).map((r) => ({
9576
+ id: r.id,
9577
+ name: String(r.name ?? ""),
9578
+ slug: String(r.slug ?? ""),
9579
+ enabled: r.enabled !== false
9580
+ }));
9581
+ setLlmAgents(list);
9582
+ const pick = list.find((a) => a.enabled) ?? list[0];
9583
+ const fullRow = (j.data ?? []).find((r) => r.id === pick?.id);
9584
+ if (pick && fullRow) {
9585
+ setAgentId(pick.id);
9586
+ setAgentName(pick.name);
9587
+ setAgentSlug(pick.slug);
9588
+ setAgentSystem(String(fullRow.systemInstruction ?? ""));
9589
+ setAgentModel(String(fullRow.model ?? ""));
9590
+ setAgentTemp(fullRow.temperature != null ? String(fullRow.temperature) : "");
9591
+ setAgentMaxTokens(fullRow.maxTokens != null ? String(fullRow.maxTokens) : "");
9592
+ setAgentValidationJson(String(fullRow.validationRules ?? ""));
9593
+ setAttachedAgentSlug(pick.slug);
9594
+ }
9595
+ }, []);
9596
+ const fetchKbCatalog = (0, import_react41.useCallback)(async () => {
9597
+ try {
9598
+ const res = await fetch("/api/knowledge_base_documents?limit=300&sortField=name&sortOrder=asc");
9599
+ if (!res.ok) {
9600
+ setKbCatalog([]);
9601
+ return;
9602
+ }
9603
+ const j = await res.json();
9604
+ setKbCatalog(
9605
+ (j.data ?? []).map((r) => ({
9606
+ id: typeof r.id === "number" ? r.id : Number(r.id),
9607
+ name: String(r.name ?? "")
9608
+ })).filter((r) => Number.isInteger(r.id) && r.id > 0)
9609
+ );
9610
+ } catch {
9611
+ setKbCatalog([]);
9612
+ }
9613
+ }, []);
9614
+ const fetchAttachedKnowledge = (0, import_react41.useCallback)(async (slug) => {
9615
+ if (!slug.trim()) {
9616
+ setAttachedAgentKnowledge([]);
9617
+ return;
9618
+ }
9619
+ setAttachedKbLoading(true);
9620
+ try {
9621
+ const res = await fetch(`/api/llm_agents/${encodeURIComponent(slug.trim())}/knowledge`);
9622
+ if (!res.ok) {
9623
+ setAttachedAgentKnowledge([]);
9624
+ return;
9625
+ }
9626
+ const j = await res.json();
9627
+ setAttachedAgentKnowledge(
9628
+ (j.documents ?? []).map((d) => ({
9629
+ id: typeof d.id === "number" ? d.id : Number(d.id),
9630
+ name: String(d.name ?? "")
9631
+ }))
9632
+ );
9633
+ } catch {
9634
+ setAttachedAgentKnowledge([]);
9635
+ } finally {
9636
+ setAttachedKbLoading(false);
9637
+ }
9638
+ }, []);
9639
+ (0, import_react41.useEffect)(() => {
9640
+ if (!isLlm || loading || chatMode !== "llm") return;
9641
+ setAgentLoading(true);
9642
+ void (async () => {
9643
+ try {
9644
+ await fetchLlmAgents();
9645
+ const listRes = await fetch("/api/llm_agents?limit=1");
9646
+ const listJ = listRes.ok ? await listRes.json() : { data: [] };
9647
+ if (!listJ.data?.length) {
9648
+ const defaultName = botName.trim() || "Assistant";
9649
+ const defaultSlug = slugifyAgentKey(defaultName);
9650
+ const createRes = await fetch("/api/llm_agents", {
9651
+ method: "POST",
9652
+ headers: { "Content-Type": "application/json" },
9653
+ body: JSON.stringify({ name: defaultName, slug: defaultSlug, systemInstruction: "", enabled: true })
9654
+ });
9655
+ if (createRes.ok) {
9656
+ await fetchLlmAgents();
9657
+ }
9658
+ }
9659
+ } finally {
9660
+ setAgentLoading(false);
9661
+ }
9662
+ })();
9663
+ }, [isLlm, loading, chatMode]);
9664
+ (0, import_react41.useEffect)(() => {
9665
+ if (!isLlm || loading || chatMode !== "llm") return;
9666
+ void fetchKbCatalog();
9667
+ }, [isLlm, loading, chatMode, fetchKbCatalog]);
9668
+ (0, import_react41.useEffect)(() => {
9669
+ if (!isLlm || loading || chatMode !== "llm" || !attachedAgentSlug.trim()) {
9670
+ setAttachedAgentKnowledge([]);
9671
+ return;
9672
+ }
9673
+ void fetchAttachedKnowledge(attachedAgentSlug);
9674
+ }, [isLlm, loading, chatMode, attachedAgentSlug, fetchAttachedKnowledge]);
9675
+ (0, import_react41.useEffect)(() => {
9676
+ setAttachExistingDocId("__none__");
9677
+ }, [attachedAgentSlug]);
9509
9678
  const buildPayload = () => {
9510
9679
  if (isErp) {
9511
9680
  const sortedIds = [...new Set(erpOpportunityFormIds.filter((n) => Number.isInteger(n) && n > 0))].sort(
@@ -9540,6 +9709,7 @@ function PluginSettingsPanel({
9540
9709
  payload.iconImageUrl = { value: iconImageUrl, type: "public" };
9541
9710
  payload.iconBackgroundColor = { value: iconBackgroundColor, type: "public" };
9542
9711
  payload.headerColor = { value: headerColor, type: "public" };
9712
+ payload.attachedAgentSlug = { value: attachedAgentSlug.trim(), type: "public" };
9543
9713
  }
9544
9714
  if (isEmail) {
9545
9715
  payload.salesTeamEmails = { value: serializeEmailRecipients(salesTeamEmails), type: "public" };
@@ -9555,6 +9725,56 @@ function PluginSettingsPanel({
9555
9725
  }
9556
9726
  return payload;
9557
9727
  };
9728
+ const handleSaveAgent = async () => {
9729
+ if (!agentId) {
9730
+ import_sonner7.toast.error("No agent to save");
9731
+ return;
9732
+ }
9733
+ const name = agentName.trim();
9734
+ if (!name) {
9735
+ import_sonner7.toast.error("Agent name is required");
9736
+ return;
9737
+ }
9738
+ const tempRaw = agentTemp.trim();
9739
+ const maxRaw = agentMaxTokens.trim();
9740
+ const temperature = tempRaw === "" ? null : Number(tempRaw);
9741
+ const maxTokens = maxRaw === "" ? null : parseInt(maxRaw, 10);
9742
+ if (tempRaw !== "" && !Number.isFinite(temperature)) {
9743
+ import_sonner7.toast.error("Temperature must be a number");
9744
+ return;
9745
+ }
9746
+ if (maxRaw !== "" && (!Number.isFinite(maxTokens) || maxTokens < 1)) {
9747
+ import_sonner7.toast.error("Max tokens must be a positive integer");
9748
+ return;
9749
+ }
9750
+ setAgentSaving(true);
9751
+ try {
9752
+ const res = await fetch(`/api/llm_agents/${agentId}`, {
9753
+ method: "PUT",
9754
+ headers: { "Content-Type": "application/json" },
9755
+ body: JSON.stringify({
9756
+ name,
9757
+ systemInstruction: agentSystem.trim(),
9758
+ model: agentModel.trim() || null,
9759
+ temperature,
9760
+ maxTokens,
9761
+ validationRules: agentValidationJson.trim() || null,
9762
+ enabled: true
9763
+ })
9764
+ });
9765
+ if (!res.ok) {
9766
+ const err = await res.json().catch(() => ({}));
9767
+ import_sonner7.toast.error(err.error || "Failed to update agent");
9768
+ return;
9769
+ }
9770
+ setAttachedAgentSlug(agentSlug);
9771
+ import_sonner7.toast.success("Agent saved");
9772
+ } catch {
9773
+ import_sonner7.toast.error("Failed to update agent");
9774
+ } finally {
9775
+ setAgentSaving(false);
9776
+ }
9777
+ };
9558
9778
  const handleSave = async () => {
9559
9779
  setSaving(true);
9560
9780
  try {
@@ -9588,6 +9808,78 @@ function PluginSettingsPanel({
9588
9808
  setSaving(false);
9589
9809
  }
9590
9810
  };
9811
+ const uploadKbFileToAttached = async (file) => {
9812
+ const slug = agentSlug.trim() || attachedAgentSlug.trim();
9813
+ if (!slug) {
9814
+ import_sonner7.toast.error("No agent configured");
9815
+ return;
9816
+ }
9817
+ setUploadingAttachedKbFile(true);
9818
+ try {
9819
+ const fd = new FormData();
9820
+ const stem = file.name.replace(/\.[^/.]+$/, "").trim() || "Upload";
9821
+ fd.append("name", stem);
9822
+ fd.append("file", file);
9823
+ const r = await fetch(`/api/llm_agents/${encodeURIComponent(slug)}/knowledge`, { method: "POST", body: fd });
9824
+ if (!r.ok) {
9825
+ const err = await r.json().catch(() => ({}));
9826
+ import_sonner7.toast.error(err.error || "Upload failed");
9827
+ return;
9828
+ }
9829
+ import_sonner7.toast.success("Knowledge added and linked");
9830
+ setAttachedKbInputKey((k) => k + 1);
9831
+ await fetchAttachedKnowledge(slug);
9832
+ await fetchKbCatalog();
9833
+ } finally {
9834
+ setUploadingAttachedKbFile(false);
9835
+ }
9836
+ };
9837
+ const onAttachedKbFileChange = (e) => {
9838
+ const file = e.target.files?.[0];
9839
+ if (!file) return;
9840
+ const slug = agentSlug.trim() || attachedAgentSlug.trim();
9841
+ if (!slug) {
9842
+ import_sonner7.toast.error("No agent configured");
9843
+ e.target.value = "";
9844
+ return;
9845
+ }
9846
+ void uploadKbFileToAttached(file);
9847
+ };
9848
+ const handleAttachExistingToAttached = async () => {
9849
+ const slug = agentSlug.trim() || attachedAgentSlug.trim();
9850
+ if (!slug || attachExistingDocId === "__none__") return;
9851
+ const docId = parseInt(attachExistingDocId, 10);
9852
+ if (!Number.isFinite(docId)) return;
9853
+ setUploadingAttachedKbLink(true);
9854
+ try {
9855
+ const r = await fetch(`/api/llm_agents/${encodeURIComponent(slug)}/knowledge`, {
9856
+ method: "POST",
9857
+ headers: { "Content-Type": "application/json" },
9858
+ body: JSON.stringify({ documentId: docId })
9859
+ });
9860
+ if (!r.ok) {
9861
+ const err = await r.json().catch(() => ({}));
9862
+ import_sonner7.toast.error(err.error || "Attach failed");
9863
+ return;
9864
+ }
9865
+ import_sonner7.toast.success("Document attached");
9866
+ setAttachExistingDocId("__none__");
9867
+ await fetchAttachedKnowledge(slug);
9868
+ } finally {
9869
+ setUploadingAttachedKbLink(false);
9870
+ }
9871
+ };
9872
+ const handleUnlinkKbDoc = async (docId) => {
9873
+ const slug = agentSlug.trim() || attachedAgentSlug.trim();
9874
+ if (!slug) return;
9875
+ const r = await fetch(`/api/llm_agents/${encodeURIComponent(slug)}/knowledge/${docId}`, { method: "DELETE" });
9876
+ if (!r.ok) {
9877
+ import_sonner7.toast.error("Could not remove link");
9878
+ return;
9879
+ }
9880
+ import_sonner7.toast.success("Removed from agent");
9881
+ await fetchAttachedKnowledge(slug);
9882
+ };
9591
9883
  if (loading) return /* @__PURE__ */ (0, import_jsx_runtime56.jsx)("div", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Loading..." });
9592
9884
  if (isErp) {
9593
9885
  return /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("div", { className: "space-y-4", children: [
@@ -10029,8 +10321,206 @@ function PluginSettingsPanel({
10029
10321
  /* @__PURE__ */ (0, import_jsx_runtime56.jsx)("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: "Only paste code from sources you trust." })
10030
10322
  ] }),
10031
10323
  chatMode === "llm" && /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)(import_jsx_runtime56.Fragment, { children: [
10324
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("div", { className: "rounded-lg border border-gray-200 dark:border-gray-600 bg-gray-50/80 dark:bg-gray-800/40 p-3 space-y-3", children: [
10325
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("div", { className: "flex items-center gap-2 text-sm font-medium text-gray-900 dark:text-white", children: [
10326
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(import_lucide_react32.Bot, { className: "h-4 w-4" }),
10327
+ "Chat assistant agent"
10328
+ ] }),
10329
+ agentLoading ? /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("div", { className: "flex items-center gap-2 text-sm text-gray-500", children: [
10330
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(import_lucide_react32.Loader2, { className: "h-4 w-4 animate-spin" }),
10331
+ "Loading agent\u2026"
10332
+ ] }) : !agentId ? /* @__PURE__ */ (0, import_jsx_runtime56.jsx)("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: "No agent found. Save settings to auto-create one." }) : /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)(import_jsx_runtime56.Fragment, { children: [
10333
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("div", { className: "space-y-1", children: [
10334
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(Label3, { htmlFor: "agent-name", className: "text-sm", children: "Name" }),
10335
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
10336
+ Input,
10337
+ {
10338
+ id: "agent-name",
10339
+ value: agentName,
10340
+ onChange: (e) => setAgentName(e.target.value),
10341
+ placeholder: "e.g. Sales assistant",
10342
+ className: "h-8 text-sm"
10343
+ }
10344
+ )
10345
+ ] }),
10346
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("div", { className: "space-y-1", children: [
10347
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(Label3, { htmlFor: "agent-slug", className: "text-sm", children: "Slug" }),
10348
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
10349
+ Input,
10350
+ {
10351
+ id: "agent-slug",
10352
+ value: agentSlug,
10353
+ disabled: true,
10354
+ className: "h-8 text-sm font-mono bg-gray-100 dark:bg-gray-700"
10355
+ }
10356
+ ),
10357
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)("p", { className: "text-[11px] text-gray-500", children: "Auto-generated. Used in API routes." })
10358
+ ] }),
10359
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("div", { className: "space-y-1", children: [
10360
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(Label3, { htmlFor: "agent-system", className: "text-sm", children: "System instruction" }),
10361
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
10362
+ Textarea,
10363
+ {
10364
+ id: "agent-system",
10365
+ value: agentSystem,
10366
+ onChange: (e) => setAgentSystem(e.target.value),
10367
+ rows: 5,
10368
+ placeholder: "How the model should behave\u2026",
10369
+ className: "text-sm"
10370
+ }
10371
+ )
10372
+ ] }),
10373
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("div", { className: "grid grid-cols-2 gap-2", children: [
10374
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("div", { className: "space-y-1", children: [
10375
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(Label3, { htmlFor: "agent-model", className: "text-sm", children: "Model (optional)" }),
10376
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
10377
+ Input,
10378
+ {
10379
+ id: "agent-model",
10380
+ value: agentModel,
10381
+ onChange: (e) => setAgentModel(e.target.value),
10382
+ placeholder: "Gateway model id",
10383
+ className: "h-8 text-sm font-mono"
10384
+ }
10385
+ )
10386
+ ] }),
10387
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("div", { className: "space-y-1", children: [
10388
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(Label3, { htmlFor: "agent-temp", className: "text-sm", children: "Temperature" }),
10389
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
10390
+ Input,
10391
+ {
10392
+ id: "agent-temp",
10393
+ value: agentTemp,
10394
+ onChange: (e) => setAgentTemp(e.target.value),
10395
+ placeholder: "e.g. 0.7",
10396
+ className: "h-8 text-sm"
10397
+ }
10398
+ )
10399
+ ] })
10400
+ ] }),
10401
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("div", { className: "space-y-1", children: [
10402
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(Label3, { htmlFor: "agent-max", className: "text-sm", children: "Max tokens" }),
10403
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
10404
+ Input,
10405
+ {
10406
+ id: "agent-max",
10407
+ value: agentMaxTokens,
10408
+ onChange: (e) => setAgentMaxTokens(e.target.value.replace(/\D/g, "")),
10409
+ placeholder: "e.g. 1024",
10410
+ className: "h-8 text-sm"
10411
+ }
10412
+ )
10413
+ ] }),
10414
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("div", { className: "space-y-1", children: [
10415
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(Label3, { htmlFor: "agent-validation", className: "text-sm", children: "Validation & output guardrails" }),
10416
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
10417
+ Textarea,
10418
+ {
10419
+ id: "agent-validation",
10420
+ value: agentValidationJson,
10421
+ onChange: (e) => setAgentValidationJson(e.target.value),
10422
+ rows: 4,
10423
+ placeholder: 'Plain text or JSON: {"guardrails":"Never promise refunds.","maxUserChars":2000}',
10424
+ className: "text-xs font-mono"
10425
+ }
10426
+ )
10427
+ ] }),
10428
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
10429
+ Button,
10430
+ {
10431
+ type: "button",
10432
+ size: "sm",
10433
+ className: "gap-1",
10434
+ disabled: agentSaving,
10435
+ onClick: () => void handleSaveAgent(),
10436
+ children: agentSaving ? /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("span", { className: "inline-flex items-center gap-1.5", children: [
10437
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(import_lucide_react32.Loader2, { className: "h-3.5 w-3.5 animate-spin" }),
10438
+ "Saving\u2026"
10439
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)(import_jsx_runtime56.Fragment, { children: [
10440
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(import_lucide_react32.Save, { className: "h-3.5 w-3.5" }),
10441
+ "Save agent"
10442
+ ] })
10443
+ }
10444
+ )
10445
+ ] }),
10446
+ agentId && agentSlug.trim() ? /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("div", { className: "rounded-md border border-dashed border-gray-300 dark:border-gray-600 bg-white/60 dark:bg-gray-900/30 p-3 space-y-3", children: [
10447
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("div", { className: "flex items-center gap-2 text-xs font-medium text-gray-800 dark:text-gray-200", children: [
10448
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(import_lucide_react32.FileUp, { className: "h-3.5 w-3.5 shrink-0" }),
10449
+ "Knowledge for this agent"
10450
+ ] }),
10451
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: "Upload text (.txt, .md, .json) or PDF (.pdf), or link documents already in the knowledge base." }),
10452
+ attachedKbLoading ? /* @__PURE__ */ (0, import_jsx_runtime56.jsx)("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: "Loading linked documents\u2026" }) : attachedAgentKnowledge.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime56.jsx)("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: "No documents linked yet." }) : /* @__PURE__ */ (0, import_jsx_runtime56.jsx)("ul", { className: "max-h-32 space-y-1.5 overflow-y-auto", children: attachedAgentKnowledge.map((d) => /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("li", { className: "flex items-center justify-between gap-2 text-sm", children: [
10453
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)("span", { className: "min-w-0 truncate", title: d.name, children: d.name }),
10454
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
10455
+ Button,
10456
+ {
10457
+ type: "button",
10458
+ variant: "ghost",
10459
+ size: "sm",
10460
+ className: "h-7 shrink-0 text-xs text-red-600 hover:text-red-700 dark:text-red-400",
10461
+ onClick: () => void handleUnlinkKbDoc(d.id),
10462
+ children: "Remove"
10463
+ }
10464
+ )
10465
+ ] }, d.id)) }),
10466
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("div", { className: "min-w-[180px] space-y-1", children: [
10467
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(Label3, { className: "text-xs", children: "Upload file" }),
10468
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("div", { className: "relative", children: [
10469
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
10470
+ Input,
10471
+ {
10472
+ type: "file",
10473
+ accept: ".txt,.md,.json,.pdf,text/plain,text/markdown,application/json,application/pdf",
10474
+ disabled: uploadingAttachedKbFile || uploadingAttachedKbLink,
10475
+ className: "h-8 cursor-pointer text-xs disabled:opacity-60",
10476
+ onChange: onAttachedKbFileChange
10477
+ },
10478
+ attachedKbInputKey
10479
+ ),
10480
+ uploadingAttachedKbFile ? /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)(
10481
+ "div",
10482
+ {
10483
+ className: "pointer-events-none absolute inset-0 flex items-center justify-center gap-2 rounded-md bg-background/85 text-xs font-medium text-gray-700 dark:text-gray-200",
10484
+ "aria-live": "polite",
10485
+ children: [
10486
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(import_lucide_react32.Loader2, { className: "h-4 w-4 shrink-0 animate-spin" }),
10487
+ "Saving & linking\u2026"
10488
+ ]
10489
+ }
10490
+ ) : null
10491
+ ] }),
10492
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)("p", { className: "text-[11px] text-gray-500 dark:text-gray-400", children: "Pick a file to upload immediately (chunking, embeddings if configured, then link to this agent)." })
10493
+ ] }),
10494
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("div", { className: "flex flex-wrap items-end gap-2", children: [
10495
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("div", { className: "min-w-[200px] flex-1 space-y-1", children: [
10496
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(Label3, { className: "text-xs", children: "Attach existing document" }),
10497
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)(Select, { value: attachExistingDocId, onValueChange: setAttachExistingDocId, children: [
10498
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(SelectTrigger, { className: "h-8 text-xs", children: /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(SelectValue, { placeholder: "Choose a document" }) }),
10499
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)(SelectContent, { children: [
10500
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(SelectItem, { value: "__none__", children: "\u2014 Select \u2014" }),
10501
+ kbCatalog.filter((d) => !attachedAgentKnowledge.some((a) => a.id === d.id)).map((d) => /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(SelectItem, { value: String(d.id), children: d.name }, d.id))
10502
+ ] })
10503
+ ] })
10504
+ ] }),
10505
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
10506
+ Button,
10507
+ {
10508
+ type: "button",
10509
+ size: "sm",
10510
+ className: "h-8",
10511
+ disabled: uploadingAttachedKbFile || uploadingAttachedKbLink || attachExistingDocId === "__none__",
10512
+ onClick: () => void handleAttachExistingToAttached(),
10513
+ children: uploadingAttachedKbLink ? /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("span", { className: "inline-flex items-center gap-1.5", children: [
10514
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(import_lucide_react32.Loader2, { className: "h-3.5 w-3.5 animate-spin" }),
10515
+ "Linking\u2026"
10516
+ ] }) : "Attach"
10517
+ }
10518
+ )
10519
+ ] })
10520
+ ] }) : null
10521
+ ] }),
10032
10522
  /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("div", { className: "space-y-1", children: [
10033
- /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(Label3, { htmlFor: `${settingsGroup}-botName`, className: "text-sm", children: "Name" }),
10523
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(Label3, { htmlFor: `${settingsGroup}-botName`, className: "text-sm", children: "Widget title" }),
10034
10524
  /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
10035
10525
  Input,
10036
10526
  {
@@ -12366,6 +12856,39 @@ var CRUD_CONFIGS = {
12366
12856
  { field: "createdAt", displayName: "Created", type: "date" }
12367
12857
  ],
12368
12858
  addEditPageUrl: ""
12859
+ },
12860
+ llm_agents: {
12861
+ title: "LLM agents",
12862
+ apiEndpoint: "/api/llm_agents",
12863
+ defaultSortField: "name",
12864
+ defaultSortOrder: "asc",
12865
+ columns: [
12866
+ { field: "name", displayName: "Name" },
12867
+ { field: "slug", displayName: "Slug" },
12868
+ {
12869
+ field: "systemInstruction",
12870
+ displayName: "System instruction",
12871
+ type: "textarea",
12872
+ hideInTable: true,
12873
+ textareaRows: 10,
12874
+ placeholder: "How the model should behave for this agent (sent as system prompt to the LLM)."
12875
+ },
12876
+ { field: "model", displayName: "Model", placeholder: "Optional gateway model id" },
12877
+ { field: "temperature", displayName: "Temperature", type: "number" },
12878
+ { field: "maxTokens", displayName: "Max tokens", type: "number" },
12879
+ {
12880
+ field: "validationRules",
12881
+ displayName: "Validation & output guardrails",
12882
+ type: "textarea",
12883
+ hideInTable: true,
12884
+ textareaRows: 8,
12885
+ placeholder: "Plain text: rules appended to the system prompt. Or JSON: guardrails, maxUserChars, blockedSubstrings, etc."
12886
+ },
12887
+ { field: "enabled", displayName: "Enabled", type: "boolean" },
12888
+ { field: "createdAt", displayName: "Created", type: "date", hideInForm: true },
12889
+ { field: "updatedAt", displayName: "Updated", type: "datetime", hideInForm: true }
12890
+ ],
12891
+ addEditPageUrl: ""
12369
12892
  }
12370
12893
  };
12371
12894
  function BlogEditorWrapper({ blogId }) {