@brokr/sdk 2.0.0 → 2.1.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.
Files changed (77) hide show
  1. package/dist/feature.js +22 -2
  2. package/dist/feature.mjs +22 -2
  3. package/dist/index.js +22 -2
  4. package/dist/index.mjs +22 -2
  5. package/dist/next.js +22 -2
  6. package/dist/next.mjs +22 -2
  7. package/dist/react-notifications.js +63 -50
  8. package/dist/react-notifications.mjs +63 -50
  9. package/dist/react-styles.js +386 -169
  10. package/dist/react-styles.mjs +386 -169
  11. package/dist/react-theme.js +6 -4
  12. package/dist/react-theme.mjs +6 -4
  13. package/dist/react.js +644 -317
  14. package/dist/react.mjs +682 -355
  15. package/dist/runtime.js +22 -2
  16. package/dist/runtime.mjs +22 -2
  17. package/dist/src/chat/config.d.ts +2 -0
  18. package/dist/src/chat/config.d.ts.map +1 -1
  19. package/dist/src/gateway.d.ts.map +1 -1
  20. package/dist/src/models.d.ts +2 -0
  21. package/dist/src/models.d.ts.map +1 -1
  22. package/dist/src/react/account/AccountPanel.d.ts.map +1 -1
  23. package/dist/src/react/account/UserButton.d.ts.map +1 -1
  24. package/dist/src/react/chat/AIChat.d.ts.map +1 -1
  25. package/dist/src/react/chat/ChatContext.d.ts +3 -6
  26. package/dist/src/react/chat/ChatContext.d.ts.map +1 -1
  27. package/dist/src/react/chat/ChatInput.d.ts.map +1 -1
  28. package/dist/src/react/chat/MarkdownRenderer.d.ts.map +1 -1
  29. package/dist/src/react/chat/MessagePane.d.ts.map +1 -1
  30. package/dist/src/react/chat/ModelSelector.d.ts +1 -1
  31. package/dist/src/react/chat/ModelSelector.d.ts.map +1 -1
  32. package/dist/src/react/chat/ThreadSidebar.d.ts.map +1 -1
  33. package/dist/src/react/chat/memory.d.ts +12 -0
  34. package/dist/src/react/chat/memory.d.ts.map +1 -0
  35. package/dist/src/react/chat/types.d.ts +6 -0
  36. package/dist/src/react/chat/types.d.ts.map +1 -1
  37. package/dist/src/react/chat/useChat.d.ts +8 -6
  38. package/dist/src/react/chat/useChat.d.ts.map +1 -1
  39. package/dist/src/react/composites/FabAI.d.ts +6 -1
  40. package/dist/src/react/composites/FabAI.d.ts.map +1 -1
  41. package/dist/src/react/composites/fab-context.d.ts +26 -0
  42. package/dist/src/react/composites/fab-context.d.ts.map +1 -0
  43. package/dist/src/react/css/account.d.ts +1 -1
  44. package/dist/src/react/css/account.d.ts.map +1 -1
  45. package/dist/src/react/css/auth.d.ts +1 -1
  46. package/dist/src/react/css/auth.d.ts.map +1 -1
  47. package/dist/src/react/css/chat-extras.d.ts +1 -1
  48. package/dist/src/react/css/chat-extras.d.ts.map +1 -1
  49. package/dist/src/react/css/chat.d.ts +1 -1
  50. package/dist/src/react/css/chat.d.ts.map +1 -1
  51. package/dist/src/react/css/composites.d.ts +1 -1
  52. package/dist/src/react/css/composites.d.ts.map +1 -1
  53. package/dist/src/react/css/gates.d.ts +1 -1
  54. package/dist/src/react/css/gates.d.ts.map +1 -1
  55. package/dist/src/react/css/markdown.d.ts +1 -1
  56. package/dist/src/react/css/markdown.d.ts.map +1 -1
  57. package/dist/src/react/css/notifications.d.ts +1 -1
  58. package/dist/src/react/css/notifications.d.ts.map +1 -1
  59. package/dist/src/react/css/primitives.d.ts +1 -1
  60. package/dist/src/react/css/primitives.d.ts.map +1 -1
  61. package/dist/src/react/css/reset.d.ts +1 -1
  62. package/dist/src/react/css/reset.d.ts.map +1 -1
  63. package/dist/src/react/css/responsive.d.ts +1 -1
  64. package/dist/src/react/css/responsive.d.ts.map +1 -1
  65. package/dist/src/react/css/skeleton.d.ts +1 -1
  66. package/dist/src/react/css/skeleton.d.ts.map +1 -1
  67. package/dist/src/react/css/tokens.d.ts +1 -1
  68. package/dist/src/react/css/tokens.d.ts.map +1 -1
  69. package/dist/src/react/notifications/NotificationBell.d.ts.map +1 -1
  70. package/dist/src/react/notifications/NotificationList.d.ts.map +1 -1
  71. package/dist/src/react/notifications/Toast.d.ts.map +1 -1
  72. package/dist/src/react/payments/Plans.d.ts.map +1 -1
  73. package/dist/src/react/theme.d.ts.map +1 -1
  74. package/dist/src/react/types.d.ts +4 -4
  75. package/dist/src/react/types.d.ts.map +1 -1
  76. package/dist/tsconfig.tsbuildinfo +1 -1
  77. package/package.json +1 -1
package/dist/react.mjs CHANGED
@@ -4173,10 +4173,12 @@ function getBrokrRootStyle(theme) {
4173
4173
  ...theme.colors.error ? { ["--brokr-error"]: theme.colors.error } : {},
4174
4174
  ...theme.colors.success ? { ["--brokr-success"]: theme.colors.success } : {},
4175
4175
  ...theme.colors.warning ? { ["--brokr-warning"]: theme.colors.warning } : {},
4176
- ...theme.colors.toastBg ? { ["--brokr-toast-bg"]: theme.colors.toastBg } : {},
4177
- ...theme.colors.toastText ? { ["--brokr-toast-text"]: theme.colors.toastText } : {},
4178
- ...theme.colors.toastTextSecondary ? { ["--brokr-toast-text-secondary"]: theme.colors.toastTextSecondary } : {},
4179
- ...theme.colors.toastBorder ? { ["--brokr-toast-border"]: theme.colors.toastBorder } : {},
4176
+ // Toast vars always emitted with smart defaults — toasts stay light in dark mode.
4177
+ // Users can override via theme.colors.toastBg etc.
4178
+ ["--brokr-toast-bg"]: theme.colors.toastBg ?? "#ffffff",
4179
+ ["--brokr-toast-text"]: theme.colors.toastText ?? "#0a0a0a",
4180
+ ["--brokr-toast-text-secondary"]: theme.colors.toastTextSecondary ?? "#52525b",
4181
+ ["--brokr-toast-border"]: theme.colors.toastBorder ?? "#e4e4e7",
4180
4182
  ["--brokr-radius-card"]: `${theme.radii.card}px`,
4181
4183
  ["--brokr-radius-input"]: `${theme.radii.input}px`,
4182
4184
  ["--brokr-radius-button"]: `${theme.radii.button}px`
@@ -4195,6 +4197,7 @@ import React2, {
4195
4197
  } from "react";
4196
4198
 
4197
4199
  // src/models.ts
4200
+ var STACK_DEFAULT = "__stack_default__";
4198
4201
  var providers = [
4199
4202
  { id: "deepseek", label: "Deepseek", model: "deepseek-chat", color: "#0EA5E9", free: true, logo: "https://assets.brokr.sh/sdk/deepseek_logo.png" },
4200
4203
  { id: "openai", label: "ChatGPT", model: "gpt-5.4-mini", color: "#10B981", free: false, logo: "https://assets.brokr.sh/sdk/gpt_logo.png" },
@@ -4298,6 +4301,37 @@ function resolveNotificationType(registry, type, data) {
4298
4301
 
4299
4302
  // src/react/notifications/Toast.tsx
4300
4303
  import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
4304
+ function ToastItem({
4305
+ notification,
4306
+ exiting,
4307
+ registry,
4308
+ onDismiss
4309
+ }) {
4310
+ const handleDismiss = useCallback(() => onDismiss(notification.id), [notification.id, onDismiss]);
4311
+ const notifData = notification.data ?? {};
4312
+ const notifType = notifData.type ?? "default";
4313
+ const resolved = resolveNotificationType(registry, notifType, notifData);
4314
+ return /* @__PURE__ */ React.createElement(
4315
+ "div",
4316
+ {
4317
+ className: `brokr-toast brokr-toast--${notification.variant}${exiting ? " brokr-toast--exit" : ""}`,
4318
+ role: "alert",
4319
+ "aria-live": "polite"
4320
+ },
4321
+ resolved.image ? /* @__PURE__ */ React.createElement("img", { src: resolved.image.url, alt: resolved.image.alt, className: "brokr-toast-provider-logo" }) : /* @__PURE__ */ React.createElement("span", { className: `brokr-toast-icon brokr-toast-icon--${notification.variant}` }),
4322
+ /* @__PURE__ */ React.createElement("div", { className: "brokr-toast-content" }, /* @__PURE__ */ React.createElement("span", { className: "brokr-toast-title" }, notification.title), /* @__PURE__ */ React.createElement("span", { className: "brokr-toast-message" }, notification.message)),
4323
+ /* @__PURE__ */ React.createElement(
4324
+ "button",
4325
+ {
4326
+ type: "button",
4327
+ className: "brokr-toast-dismiss",
4328
+ onClick: handleDismiss,
4329
+ "aria-label": "Dismiss notification"
4330
+ },
4331
+ /* @__PURE__ */ React.createElement("svg", { "aria-hidden": "true", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }, /* @__PURE__ */ React.createElement("path", { d: "M18 6 6 18M6 6l12 12" }))
4332
+ )
4333
+ );
4334
+ }
4301
4335
  var DEFAULT_DURATION = 4e3;
4302
4336
  var DEFAULT_MAX_VISIBLE = 3;
4303
4337
  var EXIT_ANIMATION_MS = 300;
@@ -4308,6 +4342,26 @@ function ToastLayer({ toasts, config, registry, onDismiss }) {
4308
4342
  const duration = config.toast?.duration ?? DEFAULT_DURATION;
4309
4343
  const maxVisible = config.toast?.maxVisible ?? DEFAULT_MAX_VISIBLE;
4310
4344
  const position = config.toast?.position ?? "top-right";
4345
+ const onDismissRef = useRef(onDismiss);
4346
+ onDismissRef.current = onDismiss;
4347
+ const exitingRef = useRef(/* @__PURE__ */ new Set());
4348
+ const dismissToast = useCallback((id) => {
4349
+ if (exitingRef.current.has(id)) return;
4350
+ exitingRef.current.add(id);
4351
+ setEntries(
4352
+ (prev) => prev.map((e) => e.notification.id === id ? { ...e, exiting: true } : e)
4353
+ );
4354
+ setTimeout(() => {
4355
+ setEntries((prev) => prev.filter((e) => e.notification.id !== id));
4356
+ onDismissRef.current(id);
4357
+ exitingRef.current.delete(id);
4358
+ }, EXIT_ANIMATION_MS);
4359
+ const timer = timersRef.current.get(id);
4360
+ if (timer) {
4361
+ clearTimeout(timer);
4362
+ timersRef.current.delete(id);
4363
+ }
4364
+ }, []);
4311
4365
  useEffect(() => {
4312
4366
  for (const toast of toasts) {
4313
4367
  if (processedRef.current.has(toast.id)) continue;
@@ -4326,21 +4380,7 @@ function ToastLayer({ toasts, config, registry, onDismiss }) {
4326
4380
  timersRef.current.set(toast.id, timer);
4327
4381
  }
4328
4382
  }
4329
- }, [toasts, duration, maxVisible]);
4330
- const dismissToast = useCallback((id) => {
4331
- setEntries(
4332
- (prev) => prev.map((e) => e.notification.id === id ? { ...e, exiting: true } : e)
4333
- );
4334
- setTimeout(() => {
4335
- setEntries((prev) => prev.filter((e) => e.notification.id !== id));
4336
- onDismiss(id);
4337
- }, EXIT_ANIMATION_MS);
4338
- const timer = timersRef.current.get(id);
4339
- if (timer) {
4340
- clearTimeout(timer);
4341
- timersRef.current.delete(id);
4342
- }
4343
- }, [onDismiss]);
4383
+ }, [toasts, duration, maxVisible, dismissToast]);
4344
4384
  useEffect(() => {
4345
4385
  return () => {
4346
4386
  for (const timer of timersRef.current.values()) {
@@ -4360,39 +4400,16 @@ function ToastLayer({ toasts, config, registry, onDismiss }) {
4360
4400
  return map[position] ?? "brokr-toast-layer--bottom-right";
4361
4401
  }, [position]);
4362
4402
  if (entries.length === 0) return null;
4363
- return /* @__PURE__ */ React.createElement("div", { className: `brokr-toast-layer ${positionClass}` }, entries.map(({ notification, exiting }) => {
4364
- const notifData = notification.data ?? {};
4365
- const notifType = notifData.type ?? "default";
4366
- const resolved = resolveNotificationType(registry, notifType, notifData);
4367
- return /* @__PURE__ */ React.createElement(
4368
- "div",
4369
- {
4370
- key: notification.id,
4371
- className: `brokr-toast brokr-toast--${notification.variant}${exiting ? " brokr-toast--exit" : ""}`,
4372
- role: "alert",
4373
- "aria-live": "polite"
4374
- },
4375
- resolved.image ? /* @__PURE__ */ React.createElement(
4376
- "img",
4377
- {
4378
- src: resolved.image.url,
4379
- alt: resolved.image.alt,
4380
- className: "brokr-toast-provider-logo"
4381
- }
4382
- ) : /* @__PURE__ */ React.createElement("span", { className: `brokr-toast-icon brokr-toast-icon--${notification.variant}` }),
4383
- /* @__PURE__ */ React.createElement("div", { className: "brokr-toast-content" }, /* @__PURE__ */ React.createElement("span", { className: "brokr-toast-title" }, notification.title), /* @__PURE__ */ React.createElement("span", { className: "brokr-toast-message" }, notification.message)),
4384
- /* @__PURE__ */ React.createElement(
4385
- "button",
4386
- {
4387
- type: "button",
4388
- className: "brokr-toast-dismiss",
4389
- onClick: () => dismissToast(notification.id),
4390
- "aria-label": "Dismiss notification"
4391
- },
4392
- /* @__PURE__ */ React.createElement("svg", { "aria-hidden": "true", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }, /* @__PURE__ */ React.createElement("path", { d: "M18 6 6 18M6 6l12 12" }))
4393
- )
4394
- );
4395
- }));
4403
+ return /* @__PURE__ */ React.createElement("div", { className: `brokr-toast-layer ${positionClass}` }, entries.map(({ notification, exiting }) => /* @__PURE__ */ React.createElement(
4404
+ ToastItem,
4405
+ {
4406
+ key: notification.id,
4407
+ notification,
4408
+ exiting,
4409
+ registry,
4410
+ onDismiss: dismissToast
4411
+ }
4412
+ )));
4396
4413
  }
4397
4414
 
4398
4415
  // src/react/notifications/provider.tsx
@@ -5515,6 +5532,26 @@ function filterDefined(defaults, overrides) {
5515
5532
  }
5516
5533
 
5517
5534
  // src/react/account/AccountPanel.tsx
5535
+ function AccountTabButton({
5536
+ tab,
5537
+ active,
5538
+ icon,
5539
+ label,
5540
+ onSelect
5541
+ }) {
5542
+ const handleClick = useCallback8(() => onSelect(tab), [tab, onSelect]);
5543
+ return /* @__PURE__ */ React10.createElement(
5544
+ "button",
5545
+ {
5546
+ className: "brokr-account-tab",
5547
+ "data-active": active,
5548
+ onClick: handleClick,
5549
+ type: "button"
5550
+ },
5551
+ /* @__PURE__ */ React10.createElement("span", { className: "brokr-account-tab-icon" }, icon),
5552
+ /* @__PURE__ */ React10.createElement("span", null, label)
5553
+ );
5554
+ }
5518
5555
  var TAB_META = {
5519
5556
  general: {
5520
5557
  label: "General"
@@ -5646,16 +5683,15 @@ function AccountPanel(inlineProps) {
5646
5683
  }
5647
5684
  }, []);
5648
5685
  return /* @__PURE__ */ React10.createElement("section", { className: "brokr-account-panel", "data-density": density }, /* @__PURE__ */ React10.createElement("aside", { className: "brokr-account-sidebar" }, groupedTabs.map((group) => /* @__PURE__ */ React10.createElement("div", { className: "brokr-account-sidebar-group", key: group.label }, /* @__PURE__ */ React10.createElement("span", { className: "brokr-account-sidebar-kicker" }, group.label), /* @__PURE__ */ React10.createElement("nav", { className: "brokr-account-tab-list", "aria-label": `${group.label} settings` }, group.tabs.map((tab) => /* @__PURE__ */ React10.createElement(
5649
- "button",
5686
+ AccountTabButton,
5650
5687
  {
5651
- className: "brokr-account-tab",
5652
- "data-active": activeTab === tab,
5653
5688
  key: tab,
5654
- onClick: () => setActiveTab(tab),
5655
- type: "button"
5656
- },
5657
- /* @__PURE__ */ React10.createElement("span", { className: "brokr-account-tab-icon" }, getTabIcon(tab)),
5658
- /* @__PURE__ */ React10.createElement("span", null, TAB_META[tab].label)
5689
+ tab,
5690
+ active: activeTab === tab,
5691
+ icon: getTabIcon(tab),
5692
+ label: TAB_META[tab].label,
5693
+ onSelect: setActiveTab
5694
+ }
5659
5695
  )))))), /* @__PURE__ */ React10.createElement("div", { className: "brokr-account-surface" }, /* @__PURE__ */ React10.createElement("header", { className: "brokr-account-surface-header" }, /* @__PURE__ */ React10.createElement("h2", { className: "brokr-title" }, activeTabMeta.label)), activeTab === "general" ? /* @__PURE__ */ React10.createElement("form", { className: "brokr-account-form", onSubmit: handleSaveProfile }, /* @__PURE__ */ React10.createElement("section", { className: "brokr-account-card" }, /* @__PURE__ */ React10.createElement("div", { className: "brokr-account-photo-row" }, /* @__PURE__ */ React10.createElement(ProfilePhotoButton, { size: 88 }), /* @__PURE__ */ React10.createElement("div", { className: "brokr-account-photo-copy" }, /* @__PURE__ */ React10.createElement("strong", null, "Profile photo"), /* @__PURE__ */ React10.createElement("span", { className: "brokr-copy" }, "PNG, JPEG, and GIF under 10MB"), /* @__PURE__ */ React10.createElement("span", { className: "brokr-account-photo-hint" }, /* @__PURE__ */ React10.createElement(UploadIcon, { size: 14 }), "Upload new picture")))), /* @__PURE__ */ React10.createElement("section", { className: "brokr-account-card brokr-account-field-list" }, /* @__PURE__ */ React10.createElement("div", { className: "brokr-account-field-row" }, /* @__PURE__ */ React10.createElement("div", { className: "brokr-account-field-head" }, /* @__PURE__ */ React10.createElement("label", { className: "brokr-label", htmlFor: "brokr-account-name" }, "Name")), /* @__PURE__ */ React10.createElement("div", { className: "brokr-account-field-body" }, /* @__PURE__ */ React10.createElement(
5660
5696
  "input",
5661
5697
  {
@@ -5763,6 +5799,9 @@ function UserButton({
5763
5799
  setOpen(false);
5764
5800
  await signOut();
5765
5801
  }, [signOut]);
5802
+ const stopPropagation = useCallback9((event) => {
5803
+ event.stopPropagation();
5804
+ }, []);
5766
5805
  useEffect7(() => {
5767
5806
  if (!open) return void 0;
5768
5807
  const handlePointerDown = (event) => {
@@ -5796,13 +5835,13 @@ function UserButton({
5796
5835
  "data-align": align,
5797
5836
  role: "dialog"
5798
5837
  },
5799
- /* @__PURE__ */ React12.createElement("div", { className: "brokr-account-menu" }, /* @__PURE__ */ React12.createElement("div", { className: "brokr-account-menu-header" }, /* @__PURE__ */ React12.createElement(Avatar, { email: summary.email, name: summary.name, src: user.image, size: 44 }), /* @__PURE__ */ React12.createElement("div", { className: "brokr-account-menu-copy" }, /* @__PURE__ */ React12.createElement("strong", null, summary.name), /* @__PURE__ */ React12.createElement("span", null, summary.email))), /* @__PURE__ */ React12.createElement("div", { className: "brokr-account-menu-actions" }, /* @__PURE__ */ React12.createElement("button", { className: "brokr-account-menu-action", onClick: openSettings, type: "button" }, /* @__PURE__ */ React12.createElement("span", { className: "brokr-account-menu-action-icon" }, /* @__PURE__ */ React12.createElement(SettingsIcon, { size: 15 })), /* @__PURE__ */ React12.createElement("span", null, "Settings")), /* @__PURE__ */ React12.createElement("button", { className: "brokr-account-menu-action", onClick: () => void handleSignOut(), type: "button" }, /* @__PURE__ */ React12.createElement("span", { className: "brokr-account-menu-action-icon" }, /* @__PURE__ */ React12.createElement(LogoutIcon, { size: 15 })), /* @__PURE__ */ React12.createElement("span", null, "Sign out"))))
5838
+ /* @__PURE__ */ React12.createElement("div", { className: "brokr-account-menu" }, /* @__PURE__ */ React12.createElement("div", { className: "brokr-account-menu-header" }, /* @__PURE__ */ React12.createElement(Avatar, { email: summary.email, name: summary.name, src: user.image, size: 44 }), /* @__PURE__ */ React12.createElement("div", { className: "brokr-account-menu-copy" }, /* @__PURE__ */ React12.createElement("strong", null, summary.name), /* @__PURE__ */ React12.createElement("span", null, summary.email))), /* @__PURE__ */ React12.createElement("div", { className: "brokr-account-menu-actions" }, /* @__PURE__ */ React12.createElement("button", { className: "brokr-account-menu-action", onClick: openSettings, type: "button" }, /* @__PURE__ */ React12.createElement("span", { className: "brokr-account-menu-action-icon" }, /* @__PURE__ */ React12.createElement(SettingsIcon, { size: 15 })), /* @__PURE__ */ React12.createElement("span", null, "Settings")), /* @__PURE__ */ React12.createElement("button", { className: "brokr-account-menu-action", onClick: handleSignOut, type: "button" }, /* @__PURE__ */ React12.createElement("span", { className: "brokr-account-menu-action-icon" }, /* @__PURE__ */ React12.createElement(LogoutIcon, { size: 15 })), /* @__PURE__ */ React12.createElement("span", null, "Sign out"))))
5800
5839
  ) : null, settingsOpen ? /* @__PURE__ */ React12.createElement("div", { className: "brokr-modal-backdrop", role: "presentation", onClick: closeSettings }, /* @__PURE__ */ React12.createElement(
5801
5840
  "div",
5802
5841
  {
5803
5842
  "aria-modal": "true",
5804
5843
  className: "brokr-panel brokr-modal-dialog brokr-account-settings-dialog",
5805
- onClick: (event) => event.stopPropagation(),
5844
+ onClick: stopPropagation,
5806
5845
  role: "dialog"
5807
5846
  },
5808
5847
  /* @__PURE__ */ React12.createElement("div", { className: "brokr-modal-toolbar" }, /* @__PURE__ */ React12.createElement("button", { className: "brokr-button-ghost", onClick: closeSettings, type: "button" }, /* @__PURE__ */ React12.createElement(CloseIcon, { size: 16 }))),
@@ -5998,6 +6037,28 @@ function AuthWall({ children }) {
5998
6037
 
5999
6038
  // src/react/payments/Plans.tsx
6000
6039
  import React21, { useCallback as useCallback12, useMemo as useMemo13, useState as useState12 } from "react";
6040
+ function PlanCard({
6041
+ plan,
6042
+ highlight,
6043
+ isPending,
6044
+ onSelect
6045
+ }) {
6046
+ const isHighlighted = highlight === plan.slug;
6047
+ const features = Object.entries(plan.features);
6048
+ const handleClick = useCallback12(() => {
6049
+ void onSelect(plan.slug);
6050
+ }, [plan.slug, onSelect]);
6051
+ return /* @__PURE__ */ React21.createElement("article", { className: "brokr-card brokr-plan-card", "data-highlight": isHighlighted, key: plan.slug }, /* @__PURE__ */ React21.createElement("div", { className: "brokr-section", style: { gap: "var(--brokr-space-3)" } }, /* @__PURE__ */ React21.createElement("div", { className: "brokr-brand-row", style: { justifyContent: "space-between" } }, /* @__PURE__ */ React21.createElement("strong", null, plan.name), plan.isCurrent ? /* @__PURE__ */ React21.createElement("span", { className: "brokr-badge" }, "Current") : null, isHighlighted && !plan.isCurrent ? /* @__PURE__ */ React21.createElement("span", { className: "brokr-badge" }, "Recommended") : null), /* @__PURE__ */ React21.createElement("div", { className: "brokr-plan-price" }, /* @__PURE__ */ React21.createElement("span", { className: "brokr-plan-value" }, plan.price === 0 ? "$0" : `$${(plan.price / 100).toFixed(0)}`), /* @__PURE__ */ React21.createElement("span", { className: "brokr-copy" }, "/", plan.interval)), plan.trialDays ? /* @__PURE__ */ React21.createElement("span", { className: "brokr-copy" }, plan.trialDays, " day trial included.") : null), /* @__PURE__ */ React21.createElement("ul", { className: "brokr-feature-list" }, features.map(([key, value]) => /* @__PURE__ */ React21.createElement("li", { className: "brokr-feature-item", key }, /* @__PURE__ */ React21.createElement(CheckIcon, { size: 14 }), /* @__PURE__ */ React21.createElement("span", null, featureLabel(key), " \xB7 ", featureValueLabel(value))))), /* @__PURE__ */ React21.createElement(
6052
+ "button",
6053
+ {
6054
+ className: plan.isCurrent ? "brokr-button-secondary" : "brokr-button",
6055
+ disabled: plan.isCurrent || isPending,
6056
+ onClick: handleClick,
6057
+ type: "button"
6058
+ },
6059
+ plan.isCurrent ? "Current plan" : isPending ? "Redirecting" : "Choose plan"
6060
+ ));
6061
+ }
6001
6062
  function Plans({
6002
6063
  columns,
6003
6064
  highlight,
@@ -6028,34 +6089,16 @@ function Plans({
6028
6089
  setPendingPlan(null);
6029
6090
  }
6030
6091
  }, [checkout, onSelect]);
6031
- return /* @__PURE__ */ React21.createElement("div", { className: "brokr-section" }, paymentsMode === "sandbox" ? /* @__PURE__ */ React21.createElement("div", { className: "brokr-inline-message" }, "Sandbox mode \u2014 test cards only.") : null, error ? /* @__PURE__ */ React21.createElement("div", { className: "brokr-inline-message", "data-tone": "error" }, error) : null, /* @__PURE__ */ React21.createElement("div", { className: "brokr-grid-auto", style: layoutStyle }, plans.map((plan) => {
6032
- const features = Object.entries(plan.features);
6033
- const isHighlighted = highlight === plan.slug;
6034
- const isPending = pendingPlan === plan.slug;
6035
- const handleClick = () => {
6036
- void handleSelect(plan.slug);
6037
- };
6038
- return /* @__PURE__ */ React21.createElement(
6039
- "article",
6040
- {
6041
- className: "brokr-card brokr-plan-card",
6042
- "data-highlight": isHighlighted,
6043
- key: plan.slug
6044
- },
6045
- /* @__PURE__ */ React21.createElement("div", { className: "brokr-section", style: { gap: "0.75rem" } }, /* @__PURE__ */ React21.createElement("div", { className: "brokr-brand-row", style: { justifyContent: "space-between" } }, /* @__PURE__ */ React21.createElement("strong", null, plan.name), plan.isCurrent ? /* @__PURE__ */ React21.createElement("span", { className: "brokr-badge" }, "Current") : null, isHighlighted && !plan.isCurrent ? /* @__PURE__ */ React21.createElement("span", { className: "brokr-badge" }, "Recommended") : null), /* @__PURE__ */ React21.createElement("div", { className: "brokr-plan-price" }, /* @__PURE__ */ React21.createElement("span", { className: "brokr-plan-value" }, plan.price === 0 ? "$0" : `$${(plan.price / 100).toFixed(0)}`), /* @__PURE__ */ React21.createElement("span", { className: "brokr-copy" }, "/", plan.interval)), plan.trialDays ? /* @__PURE__ */ React21.createElement("span", { className: "brokr-copy" }, plan.trialDays, " day trial included.") : null),
6046
- /* @__PURE__ */ React21.createElement(
6047
- "button",
6048
- {
6049
- className: plan.isCurrent ? "brokr-button-secondary" : "brokr-button",
6050
- disabled: plan.isCurrent || isPending,
6051
- onClick: handleClick,
6052
- type: "button"
6053
- },
6054
- plan.isCurrent ? "Current plan" : isPending ? "Redirecting" : "Choose plan"
6055
- ),
6056
- /* @__PURE__ */ React21.createElement("ul", { className: "brokr-feature-list" }, features.map(([key, value]) => /* @__PURE__ */ React21.createElement("li", { className: "brokr-feature-item", key }, /* @__PURE__ */ React21.createElement(CheckIcon, { size: 14 }), /* @__PURE__ */ React21.createElement("span", null, featureLabel(key), " \xB7 ", featureValueLabel(value)))))
6057
- );
6058
- })));
6092
+ return /* @__PURE__ */ React21.createElement("div", { className: "brokr-section" }, paymentsMode === "sandbox" ? /* @__PURE__ */ React21.createElement("div", { className: "brokr-inline-message" }, "Sandbox mode \u2014 test cards only.") : null, error ? /* @__PURE__ */ React21.createElement("div", { className: "brokr-inline-message", "data-tone": "error" }, error) : null, /* @__PURE__ */ React21.createElement("div", { className: "brokr-grid-auto", style: layoutStyle }, plans.map((plan) => /* @__PURE__ */ React21.createElement(
6093
+ PlanCard,
6094
+ {
6095
+ key: plan.slug,
6096
+ plan,
6097
+ highlight,
6098
+ isPending: pendingPlan === plan.slug,
6099
+ onSelect: handleSelect
6100
+ }
6101
+ ))));
6059
6102
  }
6060
6103
 
6061
6104
  // src/react/payments/FeatureMeter.tsx
@@ -6193,7 +6236,7 @@ function AutoReloadToggle({ onChange }) {
6193
6236
  setIsPending(false);
6194
6237
  }
6195
6238
  }, [enabled, onChange]);
6196
- return /* @__PURE__ */ React26.createElement("div", { className: "brokr-card brokr-gate-card" }, /* @__PURE__ */ React26.createElement("div", { className: "brokr-brand-row", style: { justifyContent: "space-between" } }, /* @__PURE__ */ React26.createElement("div", { className: "brokr-section", style: { gap: "0.35rem" } }, /* @__PURE__ */ React26.createElement("strong", null, "Auto reload"), /* @__PURE__ */ React26.createElement("span", { className: "brokr-copy" }, helperText)), /* @__PURE__ */ React26.createElement(
6239
+ return /* @__PURE__ */ React26.createElement("div", { className: "brokr-card brokr-gate-card" }, /* @__PURE__ */ React26.createElement("div", { className: "brokr-brand-row", style: { justifyContent: "space-between" } }, /* @__PURE__ */ React26.createElement("div", { className: "brokr-section", style: { gap: "var(--brokr-space-1)" } }, /* @__PURE__ */ React26.createElement("strong", null, "Auto reload"), /* @__PURE__ */ React26.createElement("span", { className: "brokr-copy" }, helperText)), /* @__PURE__ */ React26.createElement(
6197
6240
  "button",
6198
6241
  {
6199
6242
  className: "brokr-button-secondary",
@@ -6243,7 +6286,7 @@ function CancelSubscription({ onCancel }) {
6243
6286
 
6244
6287
  // src/react/chat/AIChat.tsx
6245
6288
  import React35, {
6246
- useCallback as useCallback22,
6289
+ useCallback as useCallback23,
6247
6290
  useEffect as useEffect12,
6248
6291
  useMemo as useMemo23,
6249
6292
  useRef as useRef7,
@@ -6368,6 +6411,39 @@ function isSSEResponse(response) {
6368
6411
  return ct.includes("text/event-stream") && response.body !== null;
6369
6412
  }
6370
6413
 
6414
+ // src/react/chat/memory.ts
6415
+ var MAX_MEMORY_CHARS = 2e3;
6416
+ function serializeMemory(obj, prefix = "") {
6417
+ const lines = [];
6418
+ for (const [key, value] of Object.entries(obj)) {
6419
+ if (value === null || value === void 0) continue;
6420
+ const label = prefix ? `${prefix}.${key}` : key;
6421
+ if (Array.isArray(value)) {
6422
+ lines.push(`${label}: ${value.join(", ")}`);
6423
+ } else if (typeof value === "object") {
6424
+ lines.push(...serializeMemory(value, label));
6425
+ } else {
6426
+ lines.push(`${label}: ${String(value)}`);
6427
+ }
6428
+ }
6429
+ return lines;
6430
+ }
6431
+ function buildEffectivePrompt(prompt, memory) {
6432
+ if (!memory || Object.keys(memory).length === 0) return prompt;
6433
+ let block = serializeMemory(memory).join("\n");
6434
+ if (block.length > MAX_MEMORY_CHARS) {
6435
+ block = block.slice(0, MAX_MEMORY_CHARS);
6436
+ if (typeof console !== "undefined") {
6437
+ console.warn("[brokr] memory exceeded 2000 char limit, truncated");
6438
+ }
6439
+ }
6440
+ const memSection = `[User context]
6441
+ ${block}`;
6442
+ return prompt ? `${memSection}
6443
+
6444
+ ${prompt}` : memSection;
6445
+ }
6446
+
6371
6447
  // src/react/chat/useChat.ts
6372
6448
  function makeId(prefix) {
6373
6449
  return `${prefix}_${crypto.randomUUID()}`;
@@ -6385,7 +6461,9 @@ function useChat(config) {
6385
6461
  const {
6386
6462
  endpoint,
6387
6463
  prompt,
6388
- model: activeModel,
6464
+ explicitModel,
6465
+ displayModel: activeModel,
6466
+ memory,
6389
6467
  persist,
6390
6468
  surface,
6391
6469
  subject,
@@ -6421,9 +6499,25 @@ function useChat(config) {
6421
6499
  const scrollContainerRef = useRef5(null);
6422
6500
  const sentinelRef = useRef5(null);
6423
6501
  const displaySidebarItems = useMemo18(() => {
6424
- if (isControlled) return threadsProp?.map((t) => ({ id: t.id, title: t.title })) ?? [];
6425
- if (isPersist) return serverThreads.map((t) => ({ id: t.id, title: t.title }));
6426
- return memConversations.map((c) => ({ id: c.id, title: c.title || "New chat" }));
6502
+ if (isControlled) {
6503
+ return threadsProp?.map((t) => ({
6504
+ id: t.id,
6505
+ title: t.title,
6506
+ updatedAt: t.updatedAt ?? null
6507
+ })) ?? [];
6508
+ }
6509
+ if (isPersist) {
6510
+ return serverThreads.map((t) => ({
6511
+ id: t.id,
6512
+ title: t.title,
6513
+ updatedAt: t.updatedAt ?? null
6514
+ }));
6515
+ }
6516
+ return memConversations.map((c) => ({
6517
+ id: c.id,
6518
+ title: c.title || "New chat",
6519
+ updatedAt: new Date(c.updatedAt).toISOString()
6520
+ }));
6427
6521
  }, [isControlled, isPersist, threadsProp, serverThreads, memConversations]);
6428
6522
  const activeId = useMemo18(() => {
6429
6523
  if (isControlled) return activeThreadIdProp ?? null;
@@ -6604,8 +6698,11 @@ function useChat(config) {
6604
6698
  messages: trimToTokenBudget(
6605
6699
  nextMessages.filter((m) => m.role === "user" || m.role === "assistant" && m.content).map(({ content: mc, role }) => ({ role, content: mc }))
6606
6700
  ),
6607
- model: activeModel,
6608
- ...prompt ? { systemPrompt: prompt } : {}
6701
+ ...explicitModel ? { model: explicitModel } : {},
6702
+ ...(() => {
6703
+ const ep = buildEffectivePrompt(prompt, memory);
6704
+ return ep ? { systemPrompt: ep } : {};
6705
+ })()
6609
6706
  })
6610
6707
  });
6611
6708
  if (!response.ok) {
@@ -6650,8 +6747,10 @@ function useChat(config) {
6650
6747
  ensureMemConversation,
6651
6748
  memConversations,
6652
6749
  endpoint,
6750
+ explicitModel,
6653
6751
  activeModel,
6654
6752
  prompt,
6753
+ memory,
6655
6754
  updateMemMessages,
6656
6755
  appendMemDelta,
6657
6756
  animateTitle,
@@ -6673,9 +6772,12 @@ function useChat(config) {
6673
6772
  content,
6674
6773
  surface,
6675
6774
  subject: subject ?? null,
6676
- model: activeModel,
6775
+ ...explicitModel ? { model: explicitModel } : {},
6677
6776
  userId: userId ?? void 0,
6678
- ...prompt ? { systemPrompt: prompt } : {}
6777
+ ...(() => {
6778
+ const ep = buildEffectivePrompt(prompt, memory);
6779
+ return ep ? { systemPrompt: ep } : {};
6780
+ })()
6679
6781
  })
6680
6782
  });
6681
6783
  if (!response.ok) {
@@ -6730,8 +6832,10 @@ function useChat(config) {
6730
6832
  endpoint,
6731
6833
  surface,
6732
6834
  subject,
6835
+ explicitModel,
6733
6836
  activeModel,
6734
6837
  prompt,
6838
+ memory,
6735
6839
  userId,
6736
6840
  setServerMessages,
6737
6841
  setServerActiveId,
@@ -6889,6 +6993,37 @@ function useChat(config) {
6889
6993
 
6890
6994
  // src/react/chat/ModelSelector.tsx
6891
6995
  import React29, { useCallback as useCallback17, useEffect as useEffect10, useMemo as useMemo19, useRef as useRef6, useState as useState17 } from "react";
6996
+ function ModelOption({
6997
+ provider,
6998
+ isActive,
6999
+ isLocked,
7000
+ onSelect,
7001
+ onCheckout
7002
+ }) {
7003
+ const handleClick = useCallback17(() => {
7004
+ if (isLocked) {
7005
+ void onCheckout({ plan: "pro" });
7006
+ } else {
7007
+ onSelect(provider.model);
7008
+ }
7009
+ }, [isLocked, onCheckout, onSelect, provider.model]);
7010
+ return /* @__PURE__ */ React29.createElement(
7011
+ "button",
7012
+ {
7013
+ "aria-selected": isActive,
7014
+ className: "brokr-model-option",
7015
+ "data-active": isActive,
7016
+ "data-locked": isLocked,
7017
+ disabled: isLocked,
7018
+ onClick: handleClick,
7019
+ title: isLocked ? "Add credits to unlock" : void 0,
7020
+ type: "button"
7021
+ },
7022
+ /* @__PURE__ */ React29.createElement("img", { alt: "", className: "brokr-model-logo", src: provider.logo }),
7023
+ /* @__PURE__ */ React29.createElement("span", { className: "brokr-model-option-label" }, provider.label),
7024
+ isLocked ? /* @__PURE__ */ React29.createElement("span", { className: "brokr-model-lock", "aria-hidden": "true" }, /* @__PURE__ */ React29.createElement(LockIcon, { size: 13 })) : null
7025
+ );
7026
+ }
6892
7027
  function ModelSelector({
6893
7028
  activeModel,
6894
7029
  setSelectedModel,
@@ -6897,10 +7032,19 @@ function ModelSelector({
6897
7032
  }) {
6898
7033
  const [selectorOpen, setSelectorOpen] = useState17(false);
6899
7034
  const selectorRef = useRef6(null);
7035
+ const isAuto = activeModel === STACK_DEFAULT || !providers.some((p) => p.model === activeModel);
6900
7036
  const activeProvider = useMemo19(
6901
- () => providers.find((p) => p.model === activeModel) ?? providers[0],
6902
- [activeModel]
7037
+ () => isAuto ? void 0 : providers.find((p) => p.model === activeModel) ?? providers[0],
7038
+ [activeModel, isAuto]
6903
7039
  );
7040
+ const handleModelSelect = useCallback17((model) => {
7041
+ setSelectedModel(model === STACK_DEFAULT ? null : model);
7042
+ setSelectorOpen(false);
7043
+ }, [setSelectedModel]);
7044
+ const handleAutoSelect = useCallback17(() => {
7045
+ setSelectedModel(null);
7046
+ setSelectorOpen(false);
7047
+ }, [setSelectedModel]);
6904
7048
  useEffect10(() => {
6905
7049
  if (!selectorOpen) return;
6906
7050
  const onMouseDown = (e) => {
@@ -6928,40 +7072,79 @@ function ModelSelector({
6928
7072
  onClick: handleToggle,
6929
7073
  type: "button"
6930
7074
  },
6931
- activeProvider ? /* @__PURE__ */ React29.createElement("img", { alt: "", className: "brokr-model-logo", src: activeProvider.logo }) : /* @__PURE__ */ React29.createElement("span", { className: "brokr-model-dot", style: { background: "currentColor" } }),
6932
- activeProvider?.label ?? "Model",
7075
+ activeProvider ? /* @__PURE__ */ React29.createElement("img", { alt: "", className: "brokr-model-logo", src: activeProvider.logo }) : /* @__PURE__ */ React29.createElement("span", { className: "brokr-model-dot", style: { background: "#6B7280" } }),
7076
+ isAuto ? "Auto" : activeProvider?.label ?? "Model",
6933
7077
  /* @__PURE__ */ React29.createElement(ChevronDownIcon, { size: 13 })
6934
- ), selectorOpen ? /* @__PURE__ */ React29.createElement("div", { className: "brokr-model-dropdown", role: "listbox" }, providers.map((p) => {
6935
- const locked = !availableProviders.some((a) => a.id === p.id);
6936
- return /* @__PURE__ */ React29.createElement(
6937
- "button",
6938
- {
6939
- "aria-selected": p.model === activeModel,
6940
- className: "brokr-model-option",
6941
- "data-active": p.model === activeModel,
6942
- "data-locked": locked,
6943
- disabled: locked,
6944
- key: p.id,
6945
- onClick: () => {
6946
- if (locked) {
6947
- void checkout({ plan: "pro" });
6948
- } else {
6949
- setSelectedModel(p.model);
6950
- setSelectorOpen(false);
6951
- }
6952
- },
6953
- title: locked ? "Add credits to unlock" : void 0,
6954
- type: "button"
6955
- },
6956
- /* @__PURE__ */ React29.createElement("img", { alt: "", className: "brokr-model-logo", src: p.logo }),
6957
- /* @__PURE__ */ React29.createElement("span", { className: "brokr-model-option-label" }, p.label),
6958
- locked ? /* @__PURE__ */ React29.createElement("span", { className: "brokr-model-lock", "aria-hidden": "true" }, /* @__PURE__ */ React29.createElement(LockIcon, { size: 13 })) : null
6959
- );
6960
- })) : null);
7078
+ ), selectorOpen ? /* @__PURE__ */ React29.createElement("div", { className: "brokr-model-dropdown", role: "listbox" }, /* @__PURE__ */ React29.createElement(
7079
+ "button",
7080
+ {
7081
+ "aria-selected": isAuto,
7082
+ className: "brokr-model-option",
7083
+ "data-active": isAuto,
7084
+ onClick: handleAutoSelect,
7085
+ type: "button"
7086
+ },
7087
+ /* @__PURE__ */ React29.createElement("span", { className: "brokr-model-dot", style: { background: "#6B7280" } }),
7088
+ /* @__PURE__ */ React29.createElement("span", { className: "brokr-model-option-label" }, "Stack Default")
7089
+ ), providers.map((p) => /* @__PURE__ */ React29.createElement(
7090
+ ModelOption,
7091
+ {
7092
+ key: p.id,
7093
+ provider: p,
7094
+ isActive: !isAuto && p.model === activeModel,
7095
+ isLocked: !availableProviders.some((a) => a.id === p.id),
7096
+ onSelect: handleModelSelect,
7097
+ onCheckout: checkout
7098
+ }
7099
+ ))) : null);
6961
7100
  }
6962
7101
 
6963
7102
  // src/react/chat/ThreadSidebar.tsx
6964
7103
  import React30, { useCallback as useCallback18, useMemo as useMemo20 } from "react";
7104
+ function ThreadItemButton({ id, title, isActive, onSelect }) {
7105
+ const handleClick = useCallback18(() => onSelect(id), [id, onSelect]);
7106
+ return /* @__PURE__ */ React30.createElement(
7107
+ "button",
7108
+ {
7109
+ className: "brokr-ai-chat-conversation",
7110
+ "data-active": isActive,
7111
+ onClick: handleClick,
7112
+ type: "button"
7113
+ },
7114
+ /* @__PURE__ */ React30.createElement(MessageIcon, { size: 12 }),
7115
+ /* @__PURE__ */ React30.createElement("span", { className: "brokr-ai-chat-conversation-label" }, title)
7116
+ );
7117
+ }
7118
+ var SIDEBAR_GROUP_ORDER = [
7119
+ "today",
7120
+ "yesterday",
7121
+ "this_week",
7122
+ "this_month",
7123
+ "earlier",
7124
+ "undated"
7125
+ ];
7126
+ var SIDEBAR_GROUP_LABELS = {
7127
+ today: "Today",
7128
+ yesterday: "Yesterday",
7129
+ this_week: "This Week",
7130
+ this_month: "This Month",
7131
+ earlier: "Earlier",
7132
+ undated: "Recent"
7133
+ };
7134
+ function resolveSidebarDateGroup(updatedAt) {
7135
+ if (!updatedAt) return "undated";
7136
+ const parsed = new Date(updatedAt);
7137
+ if (Number.isNaN(parsed.getTime())) return "undated";
7138
+ const now = /* @__PURE__ */ new Date();
7139
+ const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
7140
+ const startOfTarget = new Date(parsed.getFullYear(), parsed.getMonth(), parsed.getDate());
7141
+ const diffDays = Math.floor((startOfToday.getTime() - startOfTarget.getTime()) / 864e5);
7142
+ if (diffDays <= 0) return "today";
7143
+ if (diffDays === 1) return "yesterday";
7144
+ if (diffDays < 7) return "this_week";
7145
+ if (diffDays < 30) return "this_month";
7146
+ return "earlier";
7147
+ }
6965
7148
  function ThreadSidebar() {
6966
7149
  const {
6967
7150
  startNewChat,
@@ -6993,14 +7176,31 @@ function ThreadSidebar() {
6993
7176
  const handleRenameChange = useCallback18((e) => {
6994
7177
  setRenameValue(e.target.value);
6995
7178
  }, [setRenameValue]);
7179
+ const groupedSidebarItems = useMemo20(() => {
7180
+ const grouped = /* @__PURE__ */ new Map();
7181
+ for (const item of displaySidebarItems) {
7182
+ const key = resolveSidebarDateGroup(item.updatedAt);
7183
+ const bucket = grouped.get(key);
7184
+ if (bucket) {
7185
+ bucket.push(item);
7186
+ } else {
7187
+ grouped.set(key, [item]);
7188
+ }
7189
+ }
7190
+ return SIDEBAR_GROUP_ORDER.map((key) => ({
7191
+ key,
7192
+ label: SIDEBAR_GROUP_LABELS[key],
7193
+ items: grouped.get(key) ?? []
7194
+ })).filter((group) => group.items.length > 0);
7195
+ }, [displaySidebarItems]);
6996
7196
  const content = useMemo20(() => {
6997
7197
  if (threadsLoading && displaySidebarItems.length === 0) {
6998
- return /* @__PURE__ */ React30.createElement("div", { style: { display: "grid", gap: "0.5rem", padding: "0 0.65rem" } }, /* @__PURE__ */ React30.createElement(Skeleton, { width: "75%", height: 14, radius: 6 }), /* @__PURE__ */ React30.createElement(Skeleton, { width: "60%", height: 14, radius: 6 }), /* @__PURE__ */ React30.createElement(Skeleton, { width: "85%", height: 14, radius: 6 }));
7198
+ return /* @__PURE__ */ React30.createElement("div", { className: "brokr-ai-chat-sidebar-skeleton" }, /* @__PURE__ */ React30.createElement(Skeleton, { width: "75%", height: 14, radius: 6 }), /* @__PURE__ */ React30.createElement(Skeleton, { width: "60%", height: 14, radius: 6 }), /* @__PURE__ */ React30.createElement(Skeleton, { width: "85%", height: 14, radius: 6 }));
6999
7199
  }
7000
7200
  if (displaySidebarItems.length === 0) {
7001
7201
  return /* @__PURE__ */ React30.createElement("div", { className: "brokr-ai-chat-sidebar-empty" }, /* @__PURE__ */ React30.createElement("p", { className: "brokr-ai-chat-sidebar-empty-text" }, "No conversations yet. Start one above."));
7002
7202
  }
7003
- return /* @__PURE__ */ React30.createElement("div", { className: "brokr-ai-chat-conversations" }, displaySidebarItems.map((item) => renamingId === item.id ? /* @__PURE__ */ React30.createElement(
7203
+ return /* @__PURE__ */ React30.createElement("div", { className: "brokr-ai-chat-sidebar-groups" }, groupedSidebarItems.map((group) => /* @__PURE__ */ React30.createElement("section", { className: "brokr-ai-chat-sidebar-group", key: group.key }, /* @__PURE__ */ React30.createElement("span", { className: "brokr-ai-chat-sidebar-kicker" }, group.label), /* @__PURE__ */ React30.createElement("div", { className: "brokr-ai-chat-conversations" }, group.items.map((item) => renamingId === item.id ? /* @__PURE__ */ React30.createElement(
7004
7204
  "input",
7005
7205
  {
7006
7206
  autoFocus: true,
@@ -7012,32 +7212,32 @@ function ThreadSidebar() {
7012
7212
  value: renameValue
7013
7213
  }
7014
7214
  ) : /* @__PURE__ */ React30.createElement(
7015
- "button",
7215
+ ThreadItemButton,
7016
7216
  {
7017
- className: "brokr-ai-chat-conversation",
7018
- "data-active": item.id === activeId,
7019
7217
  key: item.id,
7020
- onClick: () => selectThreadAndCloseSidebar(item.id),
7021
- type: "button"
7022
- },
7023
- item.title
7024
- )));
7218
+ id: item.id,
7219
+ title: item.title,
7220
+ isActive: item.id === activeId,
7221
+ onSelect: selectThreadAndCloseSidebar
7222
+ }
7223
+ ))))));
7025
7224
  }, [
7026
7225
  threadsLoading,
7027
7226
  displaySidebarItems,
7028
7227
  renamingId,
7029
7228
  renameValue,
7229
+ groupedSidebarItems,
7030
7230
  activeId,
7031
7231
  selectThreadAndCloseSidebar,
7032
7232
  handleRenameBlur,
7033
7233
  handleRenameChange,
7034
7234
  handleRenameKeyDown
7035
7235
  ]);
7036
- return /* @__PURE__ */ React30.createElement(React30.Fragment, null, /* @__PURE__ */ React30.createElement("button", { className: "brokr-ai-chat-sidebar-button", onClick: handleNewChat, type: "button" }, /* @__PURE__ */ React30.createElement(MessageIcon, { size: 16 }), "New chat"), content);
7236
+ return /* @__PURE__ */ React30.createElement(React30.Fragment, null, /* @__PURE__ */ React30.createElement("button", { className: "brokr-ai-chat-sidebar-new-chat", onClick: handleNewChat, type: "button" }, /* @__PURE__ */ React30.createElement(MessageIcon, { size: 16 }), "New chat"), content);
7037
7237
  }
7038
7238
 
7039
7239
  // src/react/chat/MessagePane.tsx
7040
- import React33, { useMemo as useMemo22 } from "react";
7240
+ import React33, { useCallback as useCallback21, useMemo as useMemo22 } from "react";
7041
7241
 
7042
7242
  // src/react/chat/MessageBubble.tsx
7043
7243
  import React32, { useCallback as useCallback20 } from "react";
@@ -7061,6 +7261,12 @@ function parseInline(text) {
7061
7261
  remaining = remaining.slice(boldMatch[0].length);
7062
7262
  continue;
7063
7263
  }
7264
+ const strikethroughMatch = remaining.match(/^~~(.+?)~~/);
7265
+ if (strikethroughMatch) {
7266
+ nodes.push(/* @__PURE__ */ React31.createElement("del", { key: key++ }, strikethroughMatch[1]));
7267
+ remaining = remaining.slice(strikethroughMatch[0].length);
7268
+ continue;
7269
+ }
7064
7270
  const italicMatch = remaining.match(/^\*(.+?)\*/);
7065
7271
  if (italicMatch) {
7066
7272
  nodes.push(/* @__PURE__ */ React31.createElement("em", { key: key++ }, italicMatch[1]));
@@ -7077,7 +7283,7 @@ function parseInline(text) {
7077
7283
  remaining = remaining.slice(linkMatch[0].length);
7078
7284
  continue;
7079
7285
  }
7080
- const nextSpecial = remaining.search(/[`*\[]/);
7286
+ const nextSpecial = remaining.search(/[`*~\[]/);
7081
7287
  if (nextSpecial === -1) {
7082
7288
  nodes.push(remaining);
7083
7289
  break;
@@ -7108,6 +7314,15 @@ function CodeBlock({ code, language }) {
7108
7314
  "Copy"
7109
7315
  )), /* @__PURE__ */ React31.createElement("pre", { className: "brokr-md-codeblock-pre" }, /* @__PURE__ */ React31.createElement("code", null, code)));
7110
7316
  }
7317
+ function looksLikeTableSeparator(line) {
7318
+ const trimmed = line.trim();
7319
+ return /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$/.test(trimmed);
7320
+ }
7321
+ function splitTableRow(line) {
7322
+ const trimmed = line.trim();
7323
+ const withoutEdges = trimmed.replace(/^\|/, "").replace(/\|$/, "");
7324
+ return withoutEdges.split("|").map((cell) => cell.trim());
7325
+ }
7111
7326
  function parseBlocks(text) {
7112
7327
  const blocks = [];
7113
7328
  const lines = text.split("\n");
@@ -7137,22 +7352,44 @@ function parseBlocks(text) {
7137
7352
  i++;
7138
7353
  continue;
7139
7354
  }
7355
+ if (line.includes("|") && i + 1 < lines.length && looksLikeTableSeparator(lines[i + 1] ?? "")) {
7356
+ const headers = splitTableRow(line);
7357
+ const rows = [];
7358
+ i += 2;
7359
+ while (i < lines.length) {
7360
+ const candidate = lines[i] ?? "";
7361
+ if (!candidate.trim() || !candidate.includes("|")) break;
7362
+ rows.push(splitTableRow(candidate));
7363
+ i++;
7364
+ }
7365
+ blocks.push({ type: "table", content: "", headers, rows });
7366
+ continue;
7367
+ }
7368
+ if (/^[\s]*>\s?/.test(line)) {
7369
+ const quoteLines = [];
7370
+ while (i < lines.length && /^[\s]*>\s?/.test(lines[i])) {
7371
+ quoteLines.push(lines[i].replace(/^[\s]*>\s?/, ""));
7372
+ i++;
7373
+ }
7374
+ blocks.push({ type: "quote", content: "", items: quoteLines });
7375
+ continue;
7376
+ }
7140
7377
  if (/^[\s]*[-*]\s+/.test(line)) {
7141
7378
  const items = [];
7142
7379
  while (i < lines.length && /^[\s]*[-*]\s+/.test(lines[i])) {
7143
7380
  items.push(lines[i].replace(/^[\s]*[-*]\s+/, ""));
7144
7381
  i++;
7145
7382
  }
7146
- blocks.push({ type: "list", content: "", items });
7383
+ blocks.push({ type: "list", content: "", items, ordered: false });
7147
7384
  continue;
7148
7385
  }
7149
- if (/^[\s]*\d+\.\s+/.test(line)) {
7386
+ if (/^[\s]*\d+[.)]\s+/.test(line)) {
7150
7387
  const items = [];
7151
- while (i < lines.length && /^[\s]*\d+\.\s+/.test(lines[i])) {
7152
- items.push(lines[i].replace(/^[\s]*\d+\.\s+/, ""));
7388
+ while (i < lines.length && /^[\s]*\d+[.)]\s+/.test(lines[i])) {
7389
+ items.push(lines[i].replace(/^[\s]*\d+[.)]\s+/, ""));
7153
7390
  i++;
7154
7391
  }
7155
- blocks.push({ type: "list", content: "", items });
7392
+ blocks.push({ type: "list", content: "", items, ordered: true });
7156
7393
  continue;
7157
7394
  }
7158
7395
  if (!line.trim()) {
@@ -7161,7 +7398,7 @@ function parseBlocks(text) {
7161
7398
  }
7162
7399
  const paraLines = [line];
7163
7400
  i++;
7164
- while (i < lines.length && lines[i].trim() && !lines[i].trimStart().startsWith("```") && !lines[i].match(/^#{1,6}\s/) && !/^(-{3,}|\*{3,}|_{3,})\s*$/.test(lines[i].trim()) && !/^[\s]*[-*]\s+/.test(lines[i]) && !/^[\s]*\d+\.\s+/.test(lines[i])) {
7401
+ while (i < lines.length && lines[i].trim() && !lines[i].trimStart().startsWith("```") && !lines[i].match(/^#{1,6}\s/) && !(lines[i].includes("|") && i + 1 < lines.length && looksLikeTableSeparator(lines[i + 1] ?? "")) && !/^(-{3,}|\*{3,}|_{3,})\s*$/.test(lines[i].trim()) && !/^[\s]*>\s?/.test(lines[i]) && !/^[\s]*[-*]\s+/.test(lines[i]) && !/^[\s]*\d+[.)]\s+/.test(lines[i])) {
7165
7402
  paraLines.push(lines[i]);
7166
7403
  i++;
7167
7404
  }
@@ -7181,7 +7418,14 @@ function renderBlocks(blocks) {
7181
7418
  return /* @__PURE__ */ React31.createElement(Tag, { className: `brokr-md-heading brokr-md-h${block.level}`, key: idx }, parseInline(block.content));
7182
7419
  }
7183
7420
  case "list":
7184
- return /* @__PURE__ */ React31.createElement("ul", { className: "brokr-md-list", key: idx }, block.items?.map((item, li) => /* @__PURE__ */ React31.createElement("li", { key: li }, parseInline(item))));
7421
+ if (block.ordered) {
7422
+ return /* @__PURE__ */ React31.createElement("ol", { className: "brokr-md-list brokr-md-list-ordered", key: idx }, block.items?.map((item, li) => /* @__PURE__ */ React31.createElement("li", { key: li }, parseInline(item))));
7423
+ }
7424
+ return /* @__PURE__ */ React31.createElement("ul", { className: "brokr-md-list brokr-md-list-unordered", key: idx }, block.items?.map((item, li) => /* @__PURE__ */ React31.createElement("li", { key: li }, parseInline(item))));
7425
+ case "quote":
7426
+ return /* @__PURE__ */ React31.createElement("blockquote", { className: "brokr-md-quote", key: idx }, block.items?.map((line, lineIndex) => /* @__PURE__ */ React31.createElement("p", { className: "brokr-md-quote-line", key: lineIndex }, parseInline(line))));
7427
+ case "table":
7428
+ return /* @__PURE__ */ React31.createElement("div", { className: "brokr-md-table-wrap", key: idx }, /* @__PURE__ */ React31.createElement("table", { className: "brokr-md-table" }, /* @__PURE__ */ React31.createElement("thead", null, /* @__PURE__ */ React31.createElement("tr", null, block.headers?.map((header, headerIndex) => /* @__PURE__ */ React31.createElement("th", { key: headerIndex }, parseInline(header))))), /* @__PURE__ */ React31.createElement("tbody", null, block.rows?.map((row, rowIndex) => /* @__PURE__ */ React31.createElement("tr", { key: rowIndex }, row.map((cell, cellIndex) => /* @__PURE__ */ React31.createElement("td", { key: cellIndex }, parseInline(cell))))))));
7185
7429
  case "paragraph":
7186
7430
  default:
7187
7431
  return /* @__PURE__ */ React31.createElement("p", { className: "brokr-md-paragraph", key: idx }, parseInline(block.content));
@@ -7220,6 +7464,12 @@ function MessageBubble({ message, isTyping, user }) {
7220
7464
  }
7221
7465
 
7222
7466
  // src/react/chat/MessagePane.tsx
7467
+ function StarterPromptButton({ prompt, onSend }) {
7468
+ const handleClick = useCallback21(() => {
7469
+ void onSend(prompt);
7470
+ }, [prompt, onSend]);
7471
+ return /* @__PURE__ */ React33.createElement("button", { className: "brokr-ai-chat-starter", onClick: handleClick, type: "button" }, prompt);
7472
+ }
7223
7473
  function MessagePane({ starterPrompts, emptyTitle, emptyCopy, subtitle }) {
7224
7474
  const {
7225
7475
  displayMessages,
@@ -7250,20 +7500,17 @@ function MessagePane({ starterPrompts, emptyTitle, emptyCopy, subtitle }) {
7250
7500
  );
7251
7501
  });
7252
7502
  }, [visibleMessages, isSubmitting, user]);
7253
- return /* @__PURE__ */ React33.createElement("div", { className: "brokr-ai-chat-thread", "data-empty": isEmpty, ref: scrollContainerRef }, isEmpty ? /* @__PURE__ */ React33.createElement("div", { className: "brokr-ai-chat-empty" }, /* @__PURE__ */ React33.createElement(SparkIcon, { size: 28 }), /* @__PURE__ */ React33.createElement("h2", { className: "brokr-title" }, emptyTitle), subtitle ?? emptyCopy ? /* @__PURE__ */ React33.createElement("p", { className: "brokr-copy" }, subtitle ?? emptyCopy) : null, starterPrompts.length > 0 ? /* @__PURE__ */ React33.createElement("div", { className: "brokr-ai-chat-starters" }, starterPrompts.map((sp) => /* @__PURE__ */ React33.createElement(
7254
- "button",
7255
- {
7256
- className: "brokr-ai-chat-starter",
7257
- key: sp,
7258
- onClick: () => void sendMessage(sp),
7259
- type: "button"
7260
- },
7261
- sp
7262
- ))) : null) : /* @__PURE__ */ React33.createElement("div", { className: "brokr-ai-chat-thread-inner" }, (isPersist ? hasMoreMessages : memHasMore) ? /* @__PURE__ */ React33.createElement("div", { ref: sentinelRef, className: "brokr-ai-chat-sentinel" }, loadingOlder ? /* @__PURE__ */ React33.createElement("div", { style: { display: "flex", justifyContent: "center", padding: "0.5rem" } }, /* @__PURE__ */ React33.createElement(Skeleton, { width: 120, height: 12, radius: 6 })) : null) : null, messageElements, /* @__PURE__ */ React33.createElement("div", { ref: bottomRef })));
7503
+ return /* @__PURE__ */ React33.createElement("div", { className: "brokr-ai-chat-thread", "data-empty": isEmpty, ref: scrollContainerRef }, isEmpty ? /* @__PURE__ */ React33.createElement("div", { className: "brokr-ai-chat-empty" }, /* @__PURE__ */ React33.createElement(SparkIcon, { size: 28 }), /* @__PURE__ */ React33.createElement("h2", { className: "brokr-title" }, emptyTitle), subtitle ?? emptyCopy ? /* @__PURE__ */ React33.createElement("p", { className: "brokr-copy" }, subtitle ?? emptyCopy) : null, starterPrompts.length > 0 ? /* @__PURE__ */ React33.createElement("div", { className: "brokr-ai-chat-starters" }, starterPrompts.map((sp) => /* @__PURE__ */ React33.createElement(StarterPromptButton, { key: sp, prompt: sp, onSend: sendMessage }))) : null) : /* @__PURE__ */ React33.createElement("div", { className: "brokr-ai-chat-thread-inner" }, (isPersist ? hasMoreMessages : memHasMore) ? /* @__PURE__ */ React33.createElement("div", { ref: sentinelRef, className: "brokr-ai-chat-sentinel" }, loadingOlder ? /* @__PURE__ */ React33.createElement("div", { style: { display: "flex", justifyContent: "center", padding: "0.5rem" } }, /* @__PURE__ */ React33.createElement(Skeleton, { width: 120, height: 12, radius: 6 })) : null) : null, messageElements, /* @__PURE__ */ React33.createElement("div", { ref: bottomRef })));
7263
7504
  }
7264
7505
 
7265
7506
  // src/react/chat/ChatInput.tsx
7266
- import React34, { useCallback as useCallback21, useEffect as useEffect11 } from "react";
7507
+ import React34, { useCallback as useCallback22, useEffect as useEffect11 } from "react";
7508
+ function CommandButton({ cmd, chatContext }) {
7509
+ const handleClick = useCallback22(() => {
7510
+ void cmd.run(chatContext);
7511
+ }, [cmd, chatContext]);
7512
+ return /* @__PURE__ */ React34.createElement("button", { className: "brokr-ai-chat-sidebar-button", key: cmd.id, onClick: handleClick, type: "button" }, cmd.text);
7513
+ }
7267
7514
  function ChatInput() {
7268
7515
  const {
7269
7516
  input,
@@ -7281,29 +7528,20 @@ function ChatInput() {
7281
7528
  el.style.height = "auto";
7282
7529
  el.style.height = `${Math.min(el.scrollHeight, 168)}px`;
7283
7530
  }, [input, textareaRef]);
7284
- const handleSubmit = useCallback21((e) => {
7531
+ const handleSubmit = useCallback22((e) => {
7285
7532
  e.preventDefault();
7286
7533
  void sendMessage();
7287
7534
  }, [sendMessage]);
7288
- const handleKeyDown = useCallback21((e) => {
7535
+ const handleKeyDown = useCallback22((e) => {
7289
7536
  if (e.key === "Enter" && !e.shiftKey) {
7290
7537
  e.preventDefault();
7291
7538
  void sendMessage();
7292
7539
  }
7293
7540
  }, [sendMessage]);
7294
- const handleChange = useCallback21((e) => {
7541
+ const handleChange = useCallback22((e) => {
7295
7542
  setInput(e.target.value);
7296
7543
  }, [setInput]);
7297
- return /* @__PURE__ */ React34.createElement("form", { className: "brokr-ai-chat-input-area", onSubmit: handleSubmit }, /* @__PURE__ */ React34.createElement("div", { className: "brokr-ai-chat-input-container" }, composerCommands.length > 0 ? /* @__PURE__ */ React34.createElement("div", { className: "brokr-ai-chat-composer-actions" }, composerCommands.map((cmd) => /* @__PURE__ */ React34.createElement(
7298
- "button",
7299
- {
7300
- className: "brokr-ai-chat-sidebar-button",
7301
- key: cmd.id,
7302
- onClick: () => void cmd.run(chatContext),
7303
- type: "button"
7304
- },
7305
- cmd.text
7306
- ))) : null, /* @__PURE__ */ React34.createElement("div", { className: "brokr-ai-chat-input-row" }, /* @__PURE__ */ React34.createElement(
7544
+ return /* @__PURE__ */ React34.createElement("form", { className: "brokr-ai-chat-input-area", onSubmit: handleSubmit }, /* @__PURE__ */ React34.createElement("div", { className: "brokr-ai-chat-input-container" }, composerCommands.length > 0 ? /* @__PURE__ */ React34.createElement("div", { className: "brokr-ai-chat-composer-actions" }, composerCommands.map((cmd) => /* @__PURE__ */ React34.createElement(CommandButton, { key: cmd.id, cmd, chatContext }))) : null, /* @__PURE__ */ React34.createElement("div", { className: "brokr-ai-chat-input-row" }, /* @__PURE__ */ React34.createElement(
7307
7545
  "textarea",
7308
7546
  {
7309
7547
  className: "brokr-ai-chat-textarea",
@@ -7344,6 +7582,7 @@ function AIChat(inlineProps) {
7344
7582
  subtitle,
7345
7583
  model: modelProp,
7346
7584
  modelSelector,
7585
+ memory,
7347
7586
  variant = 1,
7348
7587
  sidebar: sidebarProp,
7349
7588
  threadMenu: threadMenuProp,
@@ -7367,16 +7606,12 @@ function AIChat(inlineProps) {
7367
7606
  const headerVisible = variant !== 3;
7368
7607
  const threadMenuVisible = threadMenuProp !== void 0 ? threadMenuProp : variant !== 3;
7369
7608
  const modelSelectorVisible = (modelSelector !== void 0 ? modelSelector : true) && !modelProp;
7370
- const [selectedModel, setSelectedModel] = useState18(() => {
7371
- if (modelProp) return modelProp;
7372
- return providers.find((p) => p.free)?.model ?? providers[0]?.model ?? "";
7373
- });
7374
- useEffect12(() => {
7375
- if (modelProp) setSelectedModel(modelProp);
7376
- }, [modelProp]);
7377
- const activeModel = modelProp ?? selectedModel;
7609
+ const [userSelectedModel, setUserSelectedModel] = useState18(null);
7610
+ const explicitModel = modelProp ?? userSelectedModel ?? void 0;
7611
+ const displayModel = explicitModel ?? providers.find((p) => p.free)?.model ?? providers[0]?.model ?? "";
7612
+ const activeModel = explicitModel ?? STACK_DEFAULT;
7378
7613
  const activeProvider = useMemo23(
7379
- () => providers.find((p) => p.model === activeModel) ?? providers[0],
7614
+ () => activeModel === STACK_DEFAULT ? void 0 : providers.find((p) => p.model === activeModel) ?? providers[0],
7380
7615
  [activeModel]
7381
7616
  );
7382
7617
  const hasBalance = useMemo23(
@@ -7390,7 +7625,9 @@ function AIChat(inlineProps) {
7390
7625
  const chat = useChat({
7391
7626
  endpoint,
7392
7627
  prompt,
7393
- model: activeModel,
7628
+ explicitModel,
7629
+ displayModel,
7630
+ memory,
7394
7631
  persist,
7395
7632
  surface,
7396
7633
  subject,
@@ -7437,11 +7674,19 @@ function AIChat(inlineProps) {
7437
7674
  const [sidebarOpen, setSidebarOpen] = useState18(false);
7438
7675
  const [threadMenuOpenId, setThreadMenuOpenId] = useState18(null);
7439
7676
  const threadMenuRef = useRef7(null);
7440
- const closeSidebar = useCallback22(() => setSidebarOpen(false), []);
7441
- const selectThreadAndCloseSidebar = useCallback22((id) => {
7677
+ const closeSidebar = useCallback23(() => setSidebarOpen(false), []);
7678
+ const selectThreadAndCloseSidebar = useCallback23((id) => {
7442
7679
  chat.selectThread(id);
7443
7680
  setSidebarOpen(false);
7444
7681
  }, [chat.selectThread]);
7682
+ const handleCopy = useCallback23((content) => {
7683
+ navigator.clipboard.writeText(content).catch(() => {
7684
+ });
7685
+ }, []);
7686
+ const handleStartRename = useCallback23((threadId) => {
7687
+ setThreadMenuOpenId(null);
7688
+ chat.startRename(threadId);
7689
+ }, [chat.startRename]);
7445
7690
  useEffect12(() => {
7446
7691
  if (!threadMenuOpenId) return;
7447
7692
  const onMouseDown = (e) => {
@@ -7490,26 +7735,20 @@ function AIChat(inlineProps) {
7490
7735
  activeThread: chat.activeThread,
7491
7736
  renderedTitle: finalTitle,
7492
7737
  isTitleLoading: chat.isTitleLoading,
7493
- activeModel,
7738
+ activeModel: displayModel,
7494
7739
  activeProvider,
7495
- selectedModel,
7496
- setSelectedModel,
7740
+ selectedModel: displayModel,
7741
+ setSelectedModel: setUserSelectedModel,
7497
7742
  availableProviders,
7498
7743
  sendMessage: chat.sendMessage,
7499
7744
  startNewChat: chat.startNewChat,
7500
7745
  selectThread: chat.selectThread,
7501
7746
  deleteThread: chat.deleteThread,
7502
- handleCopy: (content) => {
7503
- navigator.clipboard.writeText(content).catch(() => {
7504
- });
7505
- },
7747
+ handleCopy,
7506
7748
  renamingId: chat.renamingId,
7507
7749
  renameValue: chat.renameValue,
7508
7750
  setRenameValue: chat.setRenameValue,
7509
- startRename: (threadId) => {
7510
- setThreadMenuOpenId(null);
7511
- chat.startRename(threadId);
7512
- },
7751
+ startRename: handleStartRename,
7513
7752
  submitRename: chat.submitRename,
7514
7753
  displaySidebarItems: chat.displaySidebarItems,
7515
7754
  threadsLoading: chat.threadsLoading,
@@ -7539,9 +7778,9 @@ function AIChat(inlineProps) {
7539
7778
  }), [
7540
7779
  chat,
7541
7780
  finalTitle,
7542
- activeModel,
7781
+ displayModel,
7543
7782
  activeProvider,
7544
- selectedModel,
7783
+ userSelectedModel,
7545
7784
  availableProviders,
7546
7785
  sidebarOpen,
7547
7786
  closeSidebar,
@@ -7578,8 +7817,8 @@ function AIChat(inlineProps) {
7578
7817
  threadMenuRef,
7579
7818
  threadMenuVisible,
7580
7819
  setThreadMenuOpenId,
7581
- activeModel,
7582
- setSelectedModel,
7820
+ activeModel: displayModel,
7821
+ setSelectedModel: setUserSelectedModel,
7583
7822
  availableProviders,
7584
7823
  startRename: chatState.startRename,
7585
7824
  deleteThread: chat.deleteThread
@@ -7595,6 +7834,19 @@ function AIChat(inlineProps) {
7595
7834
  ), /* @__PURE__ */ React35.createElement(ChatInput, null))
7596
7835
  ));
7597
7836
  }
7837
+ function CommandButton2({ cmd, chatContext }) {
7838
+ const handleClick = useCallback23(() => {
7839
+ void cmd.run(chatContext);
7840
+ }, [cmd, chatContext]);
7841
+ return /* @__PURE__ */ React35.createElement("button", { className: "brokr-ai-chat-sidebar-button", onClick: handleClick, type: "button" }, cmd.text);
7842
+ }
7843
+ function MenuCommandItem({ cmd, chatContext, onClose }) {
7844
+ const handleClick = useCallback23(() => {
7845
+ onClose();
7846
+ void cmd.run(chatContext);
7847
+ }, [cmd, chatContext, onClose]);
7848
+ return /* @__PURE__ */ React35.createElement("button", { className: "brokr-ai-chat-thread-dropdown-item", onClick: handleClick, type: "button" }, cmd.text);
7849
+ }
7598
7850
  function ChatHeader({
7599
7851
  activeId,
7600
7852
  sidebarVisible,
@@ -7614,10 +7866,20 @@ function ChatHeader({
7614
7866
  startRename,
7615
7867
  deleteThread
7616
7868
  }) {
7617
- const handleOpenSidebar = useCallback22(() => setSidebarOpen(true), [setSidebarOpen]);
7618
- const handleToggleMenu = useCallback22(() => {
7869
+ const handleOpenSidebar = useCallback23(() => setSidebarOpen(true), [setSidebarOpen]);
7870
+ const handleToggleMenu = useCallback23(() => {
7619
7871
  setThreadMenuOpenId(threadMenuOpenId ? null : activeId);
7620
7872
  }, [setThreadMenuOpenId, threadMenuOpenId, activeId]);
7873
+ const closeMenu = useCallback23(() => setThreadMenuOpenId(null), [setThreadMenuOpenId]);
7874
+ const handleRename = useCallback23(() => {
7875
+ if (activeId) startRename(activeId);
7876
+ }, [activeId, startRename]);
7877
+ const handleDelete = useCallback23(() => {
7878
+ if (activeId) {
7879
+ setThreadMenuOpenId(null);
7880
+ void deleteThread(activeId);
7881
+ }
7882
+ }, [activeId, deleteThread, setThreadMenuOpenId]);
7621
7883
  return /* @__PURE__ */ React35.createElement("header", { className: "brokr-ai-chat-topbar" }, /* @__PURE__ */ React35.createElement("div", { className: "brokr-ai-chat-topbar-left" }, sidebarVisible ? /* @__PURE__ */ React35.createElement(
7622
7884
  "button",
7623
7885
  {
@@ -7627,16 +7889,7 @@ function ChatHeader({
7627
7889
  type: "button"
7628
7890
  },
7629
7891
  /* @__PURE__ */ React35.createElement(MenuIcon, { size: 18 })
7630
- ) : null), /* @__PURE__ */ React35.createElement("div", { className: "brokr-ai-chat-topbar-actions" }, headerCommands.map((cmd) => /* @__PURE__ */ React35.createElement(
7631
- "button",
7632
- {
7633
- className: "brokr-ai-chat-sidebar-button",
7634
- key: cmd.id,
7635
- onClick: () => void cmd.run(chatContext),
7636
- type: "button"
7637
- },
7638
- cmd.text
7639
- )), modelSelectorVisible ? /* @__PURE__ */ React35.createElement(
7892
+ ) : null), /* @__PURE__ */ React35.createElement("div", { className: "brokr-ai-chat-topbar-actions" }, headerCommands.map((cmd) => /* @__PURE__ */ React35.createElement(CommandButton2, { key: cmd.id, cmd, chatContext })), modelSelectorVisible ? /* @__PURE__ */ React35.createElement(
7640
7893
  ModelSelector,
7641
7894
  {
7642
7895
  activeModel,
@@ -7657,32 +7910,25 @@ function ChatHeader({
7657
7910
  "button",
7658
7911
  {
7659
7912
  className: "brokr-ai-chat-thread-dropdown-item",
7660
- onClick: () => startRename(activeId),
7913
+ onClick: handleRename,
7661
7914
  type: "button"
7662
7915
  },
7663
7916
  /* @__PURE__ */ React35.createElement(PencilIcon, { size: 13 }),
7664
7917
  "Rename"
7665
7918
  ), threadMenuCommands.map((cmd) => /* @__PURE__ */ React35.createElement(
7666
- "button",
7919
+ MenuCommandItem,
7667
7920
  {
7668
- className: "brokr-ai-chat-thread-dropdown-item",
7669
7921
  key: cmd.id,
7670
- onClick: () => {
7671
- setThreadMenuOpenId(null);
7672
- void cmd.run(chatContext);
7673
- },
7674
- type: "button"
7675
- },
7676
- cmd.text
7922
+ cmd,
7923
+ chatContext,
7924
+ onClose: closeMenu
7925
+ }
7677
7926
  )), /* @__PURE__ */ React35.createElement(
7678
7927
  "button",
7679
7928
  {
7680
7929
  className: "brokr-ai-chat-thread-dropdown-item",
7681
7930
  "data-tone": "danger",
7682
- onClick: () => {
7683
- setThreadMenuOpenId(null);
7684
- void deleteThread(activeId);
7685
- },
7931
+ onClick: handleDelete,
7686
7932
  type: "button"
7687
7933
  },
7688
7934
  /* @__PURE__ */ React35.createElement(TrashIcon, { size: 13 }),
@@ -7691,20 +7937,63 @@ function ChatHeader({
7691
7937
  }
7692
7938
 
7693
7939
  // src/react/composites/FabAI.tsx
7694
- import React36, { useCallback as useCallback23, useMemo as useMemo24, useState as useState19 } from "react";
7940
+ import React36, { useCallback as useCallback24, useMemo as useMemo24, useRef as useRef8, useState as useState19 } from "react";
7941
+
7942
+ // src/react/composites/fab-context.ts
7943
+ function buildFabSystemPrompt(appContext, brandName, existingPrompt) {
7944
+ if (appContext === false) return existingPrompt;
7945
+ const name = appContext?.name ?? brandName;
7946
+ if (!name && !appContext) return existingPrompt;
7947
+ const parts = [];
7948
+ if (name) parts.push(`You are an AI assistant embedded in ${name}.`);
7949
+ if (appContext?.description) parts.push(`This app is ${appContext.description}.`);
7950
+ if (appContext?.currentPage) parts.push(`The user is currently on: ${appContext.currentPage}.`);
7951
+ if (appContext?.facts?.length) parts.push(...appContext.facts);
7952
+ const autoPrompt = parts.join(" ");
7953
+ return existingPrompt ? `${autoPrompt}
7954
+
7955
+ ${existingPrompt}` : autoPrompt;
7956
+ }
7957
+
7958
+ // src/react/composites/FabAI.tsx
7959
+ function ensureAssistantReply(messages, content) {
7960
+ const normalized = content.trim() ? content : "No response received.";
7961
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
7962
+ if (messages[index]?.role !== "assistant") continue;
7963
+ const next = [...messages];
7964
+ next[index] = { ...next[index], content: normalized };
7965
+ return next;
7966
+ }
7967
+ return [...messages, { role: "assistant", content: normalized }];
7968
+ }
7969
+ function appendAssistantDelta(messages, delta) {
7970
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
7971
+ if (messages[index]?.role !== "assistant") continue;
7972
+ const next = [...messages];
7973
+ next[index] = {
7974
+ ...next[index],
7975
+ content: `${contentToText(next[index].content)}${delta}`
7976
+ };
7977
+ return next;
7978
+ }
7979
+ return [...messages, { role: "assistant", content: delta }];
7980
+ }
7695
7981
  function FabAI({
7982
+ appContext,
7696
7983
  model,
7984
+ memory,
7697
7985
  onSendMessage,
7698
7986
  position = "bottom-right",
7699
- starterPrompts = [],
7700
7987
  systemPrompt
7701
7988
  }) {
7702
- const { can, user } = useBrokr();
7989
+ const { can, user, theme } = useBrokr();
7990
+ const brandName = theme?.brand?.name;
7703
7991
  const [isOpen, setIsOpen] = useState19(false);
7704
7992
  const [input, setInput] = useState19("");
7705
7993
  const [error, setError] = useState19(null);
7706
7994
  const [isSending, setIsSending] = useState19(false);
7707
7995
  const [messages, setMessages] = useState19([]);
7996
+ const conversationIdRef = useRef8(`fab_${crypto.randomUUID()}`);
7708
7997
  const launcherStyle = useMemo24(
7709
7998
  () => ({
7710
7999
  left: position === "bottom-left" ? "var(--brokr-space-6)" : void 0,
@@ -7713,100 +8002,125 @@ function FabAI({
7713
8002
  [position]
7714
8003
  );
7715
8004
  const canChat = useMemo24(() => can("ai.chat"), [can]);
7716
- const toggleOpen = useCallback23(() => {
8005
+ const toggleOpen = useCallback24(() => {
7717
8006
  if (!canChat) {
7718
8007
  redirectTo("/pricing");
7719
8008
  return;
7720
8009
  }
7721
8010
  setIsOpen((current) => !current);
7722
8011
  }, [canChat]);
7723
- const handleInputChange = useCallback23((event) => {
8012
+ const handleInputChange = useCallback24((event) => {
7724
8013
  setInput(event.target.value);
7725
8014
  }, []);
7726
- const handleClose = useCallback23(() => {
8015
+ const handleClose = useCallback24(() => {
7727
8016
  setIsOpen(false);
7728
8017
  }, []);
7729
- const sendPrompt = useCallback23(async (prompt) => {
8018
+ const sendPrompt = useCallback24(async (prompt) => {
7730
8019
  const nextPrompt = prompt.trim();
7731
- if (!nextPrompt) return;
7732
- const nextMessages = [...messages, { role: "user", content: nextPrompt }];
8020
+ if (!nextPrompt || isSending) return;
8021
+ const userMessage = { role: "user", content: nextPrompt };
8022
+ const nextMessages = [...messages, userMessage];
8023
+ const optimisticMessages = [...nextMessages, { role: "assistant", content: "" }];
7733
8024
  try {
7734
8025
  setError(null);
7735
8026
  setIsSending(true);
7736
- setMessages(nextMessages);
8027
+ setMessages(optimisticMessages);
7737
8028
  setInput("");
7738
- let responseText = "";
7739
8029
  if (onSendMessage) {
7740
- responseText = await onSendMessage({ messages: nextMessages, model, systemPrompt });
7741
- } else {
7742
- const payload = await postJson(
7743
- "/api/brokr/chat",
7744
- {
7745
- messages: nextMessages,
7746
- model,
7747
- systemPrompt
8030
+ const responseText2 = await onSendMessage({ messages: nextMessages, model, systemPrompt });
8031
+ setMessages((current) => ensureAssistantReply(current, responseText2));
8032
+ return;
8033
+ }
8034
+ const response = await fetch("/api/brokr/chat", {
8035
+ method: "POST",
8036
+ credentials: "include",
8037
+ headers: { "Content-Type": "application/json" },
8038
+ body: JSON.stringify({
8039
+ conversationId: conversationIdRef.current,
8040
+ messages: trimToTokenBudget(
8041
+ nextMessages.filter((message) => message.role === "user" || message.role === "assistant" && Boolean(contentToText(message.content))).map((message) => ({
8042
+ role: message.role,
8043
+ content: contentToText(message.content)
8044
+ }))
8045
+ ),
8046
+ ...model !== void 0 ? { model } : {},
8047
+ ...(() => {
8048
+ const withContext = buildFabSystemPrompt(appContext, brandName, systemPrompt);
8049
+ const ep = buildEffectivePrompt(withContext, memory);
8050
+ return ep ? { systemPrompt: ep } : {};
8051
+ })()
8052
+ })
8053
+ });
8054
+ if (!response.ok) {
8055
+ const payload2 = await response.json().catch(() => ({}));
8056
+ throw new Error(payload2.message ?? payload2.error ?? `Chat failed (${response.status})`);
8057
+ }
8058
+ if (isSSEResponse(response)) {
8059
+ let hasDelta = false;
8060
+ for await (const event of parseSSEStream(response)) {
8061
+ if (event.type === "conversation") {
8062
+ conversationIdRef.current = event.id;
8063
+ } else if (event.type === "delta") {
8064
+ hasDelta = true;
8065
+ setMessages((current) => appendAssistantDelta(current, event.delta));
8066
+ } else if (event.type === "error") {
8067
+ throw new Error(event.message);
7748
8068
  }
7749
- );
7750
- responseText = payload.content ?? payload.response ?? "";
8069
+ }
8070
+ if (!hasDelta) {
8071
+ setMessages((current) => ensureAssistantReply(current, "No response received."));
8072
+ }
8073
+ return;
8074
+ }
8075
+ const payload = await response.json().catch(() => ({}));
8076
+ if (payload.error || payload.message) {
8077
+ throw new Error(payload.message ?? payload.error ?? "AI request failed.");
7751
8078
  }
7752
- setMessages((current) => [...current, {
7753
- role: "assistant",
7754
- content: responseText || "No response received."
7755
- }]);
8079
+ const responseText = payload.content ?? payload.response ?? payload.text ?? "";
8080
+ setMessages((current) => ensureAssistantReply(current, responseText));
7756
8081
  } catch (cause) {
7757
- setError(cause instanceof Error ? cause.message : "Could not send message.");
8082
+ const message = cause instanceof Error ? cause.message : "Could not send message.";
8083
+ setError(message);
8084
+ setMessages((current) => ensureAssistantReply(current, `Error: ${message}`));
7758
8085
  } finally {
7759
8086
  setIsSending(false);
7760
8087
  }
7761
- }, [messages, model, onSendMessage, systemPrompt]);
7762
- const handleSubmit = useCallback23(async (event) => {
8088
+ }, [isSending, messages, model, onSendMessage, systemPrompt]);
8089
+ const handleSubmit = useCallback24(async (event) => {
7763
8090
  event.preventDefault();
7764
8091
  await sendPrompt(input);
7765
8092
  }, [input, sendPrompt]);
7766
- const handleKeyDown = useCallback23((event) => {
8093
+ const handleKeyDown = useCallback24((event) => {
7767
8094
  if (event.key === "Enter" && !event.shiftKey) {
7768
8095
  event.preventDefault();
7769
8096
  void sendPrompt(input);
7770
8097
  }
7771
8098
  }, [input, sendPrompt]);
7772
- const handleStarterPrompt = useCallback23((prompt) => {
7773
- void sendPrompt(prompt);
7774
- }, [sendPrompt]);
7775
8099
  return /* @__PURE__ */ React36.createElement(React36.Fragment, null, /* @__PURE__ */ React36.createElement("div", { className: "brokr-chat-fab", style: launcherStyle }, /* @__PURE__ */ React36.createElement(
7776
8100
  "button",
7777
8101
  {
8102
+ "aria-label": isOpen ? "Close AI chat" : "Open AI chat",
7778
8103
  "aria-expanded": isOpen,
7779
8104
  "aria-haspopup": "dialog",
7780
- className: "brokr-button",
8105
+ className: "brokr-chat-fab-trigger",
7781
8106
  onClick: toggleOpen,
7782
8107
  type: "button"
7783
8108
  },
7784
- /* @__PURE__ */ React36.createElement(MessageIcon, { size: 16 }),
7785
- "Ask AI"
7786
- )), isOpen ? /* @__PURE__ */ React36.createElement("div", { className: "brokr-panel brokr-chat-panel", role: "dialog" }, /* @__PURE__ */ React36.createElement("div", { className: "brokr-brand-row", style: { justifyContent: "space-between" } }, /* @__PURE__ */ React36.createElement("div", { className: "brokr-section", style: { gap: "0.25rem" } }, /* @__PURE__ */ React36.createElement("strong", null, "AI Chat"), user?.name ? /* @__PURE__ */ React36.createElement("span", { className: "brokr-copy" }, user.name) : null), /* @__PURE__ */ React36.createElement("button", { className: "brokr-button-ghost", onClick: handleClose, type: "button" }, /* @__PURE__ */ React36.createElement(CloseIcon, { size: 16 }))), /* @__PURE__ */ React36.createElement(
8109
+ /* @__PURE__ */ React36.createElement(SparkIcon, { size: 18 })
8110
+ )), isOpen ? /* @__PURE__ */ React36.createElement("div", { className: "brokr-panel brokr-chat-panel", role: "dialog" }, /* @__PURE__ */ React36.createElement("div", { className: "brokr-brand-row", style: { justifyContent: "space-between" } }, /* @__PURE__ */ React36.createElement("div", { className: "brokr-section", style: { gap: "var(--brokr-space-1)" } }, /* @__PURE__ */ React36.createElement("strong", null, "AI Chat"), user?.name ? /* @__PURE__ */ React36.createElement("span", { className: "brokr-copy" }, user.name) : null), /* @__PURE__ */ React36.createElement("button", { className: "brokr-button-ghost", onClick: handleClose, type: "button" }, /* @__PURE__ */ React36.createElement(CloseIcon, { size: 16 }))), /* @__PURE__ */ React36.createElement(
7787
8111
  "div",
7788
8112
  {
7789
8113
  className: "brokr-chat-messages",
7790
8114
  "data-empty": messages.length === 0
7791
8115
  },
7792
- messages.length === 0 ? /* @__PURE__ */ React36.createElement("div", { className: "brokr-chat-empty" }, /* @__PURE__ */ React36.createElement(SparkIcon, { size: 18 }), /* @__PURE__ */ React36.createElement("div", { className: "brokr-section", style: { gap: "0.35rem" } }, /* @__PURE__ */ React36.createElement("strong", null, "Send a message to chat with the AI."), /* @__PURE__ */ React36.createElement("span", { className: "brokr-copy" }, "Ask a question or drop in a starter prompt below.")), starterPrompts.length > 0 ? /* @__PURE__ */ React36.createElement("div", { className: "brokr-chat-starters" }, starterPrompts.map((prompt) => {
7793
- const handleClick = () => {
7794
- handleStarterPrompt(prompt);
7795
- };
7796
- return /* @__PURE__ */ React36.createElement(
7797
- "button",
7798
- {
7799
- className: "brokr-chat-starter",
7800
- key: prompt,
7801
- onClick: handleClick,
7802
- type: "button"
7803
- },
7804
- prompt
7805
- );
7806
- })) : null) : null,
7807
- messages.map((message, index) => /* @__PURE__ */ React36.createElement("div", { className: "brokr-chat-bubble", "data-role": message.role, key: `${message.role}-${index}` }, contentToText(message.content))),
8116
+ messages.length === 0 ? /* @__PURE__ */ React36.createElement("div", { className: "brokr-chat-empty" }, /* @__PURE__ */ React36.createElement(SparkIcon, { size: 18 }), /* @__PURE__ */ React36.createElement("div", { className: "brokr-section", style: { gap: "var(--brokr-space-1)" } }, /* @__PURE__ */ React36.createElement("strong", null, "Send a message to chat with the AI."), /* @__PURE__ */ React36.createElement("span", { className: "brokr-copy" }, "Ask a question or drop in a starter prompt below."))) : null,
8117
+ messages.map((message, index) => {
8118
+ const text = contentToText(message.content);
8119
+ const isTyping = message.role === "assistant" && !text && isSending && index === messages.length - 1;
8120
+ return /* @__PURE__ */ React36.createElement("div", { className: "brokr-chat-bubble", "data-role": message.role, key: `${message.role}-${index}` }, isTyping ? /* @__PURE__ */ React36.createElement("div", { className: "brokr-ai-chat-typing", "aria-label": "AI is typing" }, /* @__PURE__ */ React36.createElement("span", null), /* @__PURE__ */ React36.createElement("span", null), /* @__PURE__ */ React36.createElement("span", null)) : message.role === "assistant" ? /* @__PURE__ */ React36.createElement(MarkdownRenderer, { content: text }) : text);
8121
+ }),
7808
8122
  error ? /* @__PURE__ */ React36.createElement("div", { className: "brokr-inline-message", "data-tone": "error" }, error) : null
7809
- ), /* @__PURE__ */ React36.createElement("form", { className: "brokr-section", onSubmit: handleSubmit, style: { gap: "0.75rem" } }, /* @__PURE__ */ React36.createElement(
8123
+ ), /* @__PURE__ */ React36.createElement("form", { className: "brokr-section", onSubmit: handleSubmit, style: { gap: "var(--brokr-space-3)" } }, /* @__PURE__ */ React36.createElement(
7810
8124
  "textarea",
7811
8125
  {
7812
8126
  className: "brokr-textarea brokr-chat-input",
@@ -7816,15 +8130,15 @@ function FabAI({
7816
8130
  rows: 2,
7817
8131
  value: input
7818
8132
  }
7819
- ), /* @__PURE__ */ React36.createElement("button", { className: "brokr-button", disabled: isSending, type: "submit" }, isSending ? "Thinking" : "Send"))) : null);
8133
+ ), /* @__PURE__ */ React36.createElement("button", { className: "brokr-button", disabled: isSending || !input.trim(), type: "submit" }, isSending ? "Thinking" : "Send"))) : null);
7820
8134
  }
7821
8135
 
7822
8136
  // src/react/composites/SmartUpload.tsx
7823
8137
  import React37, {
7824
- useCallback as useCallback24,
8138
+ useCallback as useCallback25,
7825
8139
  useId as useId2,
7826
8140
  useMemo as useMemo25,
7827
- useRef as useRef8,
8141
+ useRef as useRef9,
7828
8142
  useState as useState20
7829
8143
  } from "react";
7830
8144
  function SmartUpload({
@@ -7835,7 +8149,7 @@ function SmartUpload({
7835
8149
  }) {
7836
8150
  const { paymentsMode } = useBrokr();
7837
8151
  const inputId = useId2();
7838
- const inputRef = useRef8(null);
8152
+ const inputRef = useRef9(null);
7839
8153
  const [dragActive, setDragActive] = useState20(false);
7840
8154
  const [error, setError] = useState20(null);
7841
8155
  const [fileName, setFileName] = useState20(null);
@@ -7844,7 +8158,7 @@ function SmartUpload({
7844
8158
  const helperText = useMemo25(() => {
7845
8159
  return `${Math.round(maxSize / (1024 * 1024))} MB max file size.`;
7846
8160
  }, [maxSize]);
7847
- const beginUpload = useCallback24((file) => {
8161
+ const beginUpload = useCallback25((file) => {
7848
8162
  if (file.size > maxSize) {
7849
8163
  setError(`That file is larger than ${Math.round(maxSize / (1024 * 1024))} MB.`);
7850
8164
  return;
@@ -7881,31 +8195,31 @@ function SmartUpload({
7881
8195
  body.append("purpose", purpose);
7882
8196
  request.send(body);
7883
8197
  }, [maxSize, onUpload, purpose]);
7884
- const handleInputChange = useCallback24((event) => {
8198
+ const handleInputChange = useCallback25((event) => {
7885
8199
  const file = event.target.files?.[0];
7886
8200
  event.target.value = "";
7887
8201
  if (!file) return;
7888
8202
  beginUpload(file);
7889
8203
  }, [beginUpload]);
7890
- const handleDrop = useCallback24((event) => {
8204
+ const handleDrop = useCallback25((event) => {
7891
8205
  event.preventDefault();
7892
8206
  setDragActive(false);
7893
8207
  const file = event.dataTransfer.files?.[0];
7894
8208
  if (!file) return;
7895
8209
  beginUpload(file);
7896
8210
  }, [beginUpload]);
7897
- const handleDragEnter = useCallback24((event) => {
8211
+ const handleDragEnter = useCallback25((event) => {
7898
8212
  event.preventDefault();
7899
8213
  setDragActive(true);
7900
8214
  }, []);
7901
- const handleDragLeave = useCallback24((event) => {
8215
+ const handleDragLeave = useCallback25((event) => {
7902
8216
  event.preventDefault();
7903
8217
  setDragActive(false);
7904
8218
  }, []);
7905
- const handleBrowse = useCallback24(() => {
8219
+ const handleBrowse = useCallback25(() => {
7906
8220
  inputRef.current?.click();
7907
8221
  }, []);
7908
- return /* @__PURE__ */ React37.createElement("div", { className: "brokr-card brokr-upload-shell" }, /* @__PURE__ */ React37.createElement("div", { className: "brokr-brand-row", style: { justifyContent: "space-between" } }, /* @__PURE__ */ React37.createElement("div", { className: "brokr-section", style: { gap: "0.35rem" } }, /* @__PURE__ */ React37.createElement("strong", null, "Upload files"), /* @__PURE__ */ React37.createElement("span", { className: "brokr-copy" }, "Drop a file and let Brokr handle the boring part.")), paymentsMode === "sandbox" ? /* @__PURE__ */ React37.createElement("span", { className: "brokr-badge brokr-badge-sandbox" }, "Sandbox") : null), /* @__PURE__ */ React37.createElement(
8222
+ return /* @__PURE__ */ React37.createElement("div", { className: "brokr-card brokr-upload-shell" }, /* @__PURE__ */ React37.createElement("div", { className: "brokr-brand-row", style: { justifyContent: "space-between" } }, /* @__PURE__ */ React37.createElement("div", { className: "brokr-section", style: { gap: "var(--brokr-space-1)" } }, /* @__PURE__ */ React37.createElement("strong", null, "Upload files"), /* @__PURE__ */ React37.createElement("span", { className: "brokr-copy" }, "Drop a file and let Brokr handle the boring part.")), paymentsMode === "sandbox" ? /* @__PURE__ */ React37.createElement("span", { className: "brokr-badge brokr-badge-sandbox" }, "Sandbox") : null), /* @__PURE__ */ React37.createElement(
7909
8223
  "label",
7910
8224
  {
7911
8225
  className: "brokr-upload-dropzone",
@@ -7930,11 +8244,11 @@ function SmartUpload({
7930
8244
  ref: inputRef,
7931
8245
  type: "file"
7932
8246
  }
7933
- ), fileName ? /* @__PURE__ */ React37.createElement("div", { className: "brokr-card brokr-upload-file" }, /* @__PURE__ */ React37.createElement("div", { className: "brokr-section", style: { gap: "0.35rem" } }, /* @__PURE__ */ React37.createElement("strong", null, fileName), /* @__PURE__ */ React37.createElement("span", { className: "brokr-copy" }, isUploading ? `Uploading ${progress}%` : progress === 100 ? "Processed" : "Queued")), /* @__PURE__ */ React37.createElement("div", { className: "brokr-meter-bar", style: { width: "7rem" } }, /* @__PURE__ */ React37.createElement("div", { className: "brokr-meter-fill", style: { width: `${progress}%` } }))) : null, error ? /* @__PURE__ */ React37.createElement("div", { className: "brokr-inline-message", "data-tone": "error" }, error) : null);
8247
+ ), fileName ? /* @__PURE__ */ React37.createElement("div", { className: "brokr-card brokr-upload-file" }, /* @__PURE__ */ React37.createElement("div", { className: "brokr-section", style: { gap: "var(--brokr-space-1)" } }, /* @__PURE__ */ React37.createElement("strong", null, fileName), /* @__PURE__ */ React37.createElement("span", { className: "brokr-copy" }, isUploading ? `Uploading ${progress}%` : progress === 100 ? "Processed" : "Queued")), /* @__PURE__ */ React37.createElement("div", { className: "brokr-meter-bar" }, /* @__PURE__ */ React37.createElement("div", { className: "brokr-meter-fill", style: { width: `${progress}%` } }))) : null, error ? /* @__PURE__ */ React37.createElement("div", { className: "brokr-inline-message", "data-tone": "error" }, error) : null);
7934
8248
  }
7935
8249
 
7936
8250
  // src/react/composites/FeedbackWidget.tsx
7937
- import React38, { useCallback as useCallback25, useState as useState21 } from "react";
8251
+ import React38, { useCallback as useCallback26, useState as useState21 } from "react";
7938
8252
  function FeedbackWidget({
7939
8253
  context,
7940
8254
  onSubmit
@@ -7945,16 +8259,16 @@ function FeedbackWidget({
7945
8259
  const [error, setError] = useState21(null);
7946
8260
  const [message, setMessage] = useState21(null);
7947
8261
  const [isPending, setIsPending] = useState21(false);
7948
- const handleTextChange = useCallback25((event) => {
8262
+ const handleTextChange = useCallback26((event) => {
7949
8263
  setText(event.target.value);
7950
8264
  }, []);
7951
- const handleRateUp = useCallback25(() => {
8265
+ const handleRateUp = useCallback26(() => {
7952
8266
  setRating("up");
7953
8267
  }, []);
7954
- const handleRateDown = useCallback25(() => {
8268
+ const handleRateDown = useCallback26(() => {
7955
8269
  setRating("down");
7956
8270
  }, []);
7957
- const handleSubmit = useCallback25(async (event) => {
8271
+ const handleSubmit = useCallback26(async (event) => {
7958
8272
  event.preventDefault();
7959
8273
  if (!rating) {
7960
8274
  setError("Choose a direction first.");
@@ -8270,7 +8584,7 @@ var BrokrErrorBoundary = class extends React39.Component {
8270
8584
  };
8271
8585
 
8272
8586
  // src/react/notifications/NotificationBell.tsx
8273
- import React40, { useCallback as useCallback26, useEffect as useEffect13, useMemo as useMemo26, useRef as useRef9, useState as useState22 } from "react";
8587
+ import React40, { useCallback as useCallback27, useEffect as useEffect13, useMemo as useMemo26, useRef as useRef10, useState as useState22 } from "react";
8274
8588
 
8275
8589
  // src/react/notifications/use-notifications.ts
8276
8590
  import { useContext as useContext4 } from "react";
@@ -8295,12 +8609,34 @@ function timeAgo(iso) {
8295
8609
  const days = Math.floor(hours / 24);
8296
8610
  return `${days}d ago`;
8297
8611
  }
8612
+ function NotifDropdownItem({
8613
+ notif,
8614
+ registry,
8615
+ onClick
8616
+ }) {
8617
+ const handleClick = useCallback27(() => onClick(notif), [notif, onClick]);
8618
+ const notifData = notif.data ?? {};
8619
+ const notifType = notifData.type ?? "default";
8620
+ const resolved = resolveNotificationType(registry, notifType, notifData);
8621
+ return /* @__PURE__ */ React40.createElement(
8622
+ "button",
8623
+ {
8624
+ type: "button",
8625
+ className: `brokr-notif-item${notif.read ? "" : " brokr-notif-item--unread"}`,
8626
+ onClick: handleClick,
8627
+ role: "menuitem"
8628
+ },
8629
+ resolved.image ? /* @__PURE__ */ React40.createElement("img", { src: resolved.image.url, alt: resolved.image.alt, className: "brokr-notif-item-logo" }) : /* @__PURE__ */ React40.createElement("span", { className: `brokr-notif-item-dot brokr-notif-item-dot--${notif.variant}` }),
8630
+ /* @__PURE__ */ React40.createElement("div", { className: "brokr-notif-item-body" }, /* @__PURE__ */ React40.createElement("span", { className: "brokr-notif-item-title" }, notif.title), /* @__PURE__ */ React40.createElement("span", { className: "brokr-notif-item-message" }, notif.message)),
8631
+ /* @__PURE__ */ React40.createElement("span", { className: "brokr-notif-item-time" }, timeAgo(notif.createdAt))
8632
+ );
8633
+ }
8298
8634
  function NotificationBell() {
8299
8635
  const { notifications, unreadCount, markRead, markAllRead, isLoading, registry } = useNotifications();
8300
8636
  const [open, setOpen] = useState22(false);
8301
- const containerRef = useRef9(null);
8302
- const markReadTimerRef = useRef9(null);
8303
- const toggle = useCallback26(() => setOpen((o) => !o), []);
8637
+ const containerRef = useRef10(null);
8638
+ const markReadTimerRef = useRef10(null);
8639
+ const toggle = useCallback27(() => setOpen((o) => !o), []);
8304
8640
  useEffect13(() => {
8305
8641
  if (markReadTimerRef.current) {
8306
8642
  clearTimeout(markReadTimerRef.current);
@@ -8342,7 +8678,7 @@ function NotificationBell() {
8342
8678
  ),
8343
8679
  [notifications]
8344
8680
  );
8345
- const handleItemClick = useCallback26((notif) => {
8681
+ const handleItemClick = useCallback27((notif) => {
8346
8682
  if (!notif.read) markRead(notif.id);
8347
8683
  const notifData = notif.data ?? {};
8348
8684
  const notifType = notif.type ?? notifData.type ?? "default";
@@ -8359,7 +8695,8 @@ function NotificationBell() {
8359
8695
  className: "brokr-notif-bell",
8360
8696
  onClick: toggle,
8361
8697
  "aria-label": `Notifications${unreadCount > 0 ? ` (${unreadCount} unread)` : ""}`,
8362
- "aria-expanded": open
8698
+ "aria-expanded": open,
8699
+ "aria-haspopup": "menu"
8363
8700
  },
8364
8701
  /* @__PURE__ */ React40.createElement("svg", { "aria-hidden": "true", width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }, /* @__PURE__ */ React40.createElement("path", { d: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" }), /* @__PURE__ */ React40.createElement("path", { d: "M10.3 21a1.94 1.94 0 0 0 3.4 0" })),
8365
8702
  unreadCount > 0 && /* @__PURE__ */ React40.createElement("span", { className: "brokr-notif-badge" }, unreadCount > 99 ? "99+" : unreadCount)
@@ -8371,35 +8708,19 @@ function NotificationBell() {
8371
8708
  onClick: markAllRead
8372
8709
  },
8373
8710
  "Mark all read"
8374
- )), /* @__PURE__ */ React40.createElement("div", { className: "brokr-notif-dropdown-list" }, isLoading ? /* @__PURE__ */ React40.createElement("div", { className: "brokr-notif-empty" }, /* @__PURE__ */ React40.createElement("span", { className: "brokr-notif-empty-text" }, "Loading\u2026")) : sorted.length === 0 ? /* @__PURE__ */ React40.createElement("div", { className: "brokr-notif-empty" }, /* @__PURE__ */ React40.createElement("span", { className: "brokr-notif-empty-text" }, "No notifications yet")) : sorted.map((notif) => {
8375
- const notifData = notif.data ?? {};
8376
- const notifType = notifData.type ?? "default";
8377
- const resolved = resolveNotificationType(registry, notifType, notifData);
8378
- return /* @__PURE__ */ React40.createElement(
8379
- "button",
8380
- {
8381
- key: notif.id,
8382
- type: "button",
8383
- className: `brokr-notif-item${notif.read ? "" : " brokr-notif-item--unread"}`,
8384
- onClick: () => handleItemClick(notif),
8385
- role: "menuitem"
8386
- },
8387
- resolved.image ? /* @__PURE__ */ React40.createElement(
8388
- "img",
8389
- {
8390
- src: resolved.image.url,
8391
- alt: resolved.image.alt,
8392
- className: "brokr-notif-item-logo"
8393
- }
8394
- ) : /* @__PURE__ */ React40.createElement("span", { className: `brokr-notif-item-dot brokr-notif-item-dot--${notif.variant}` }),
8395
- /* @__PURE__ */ React40.createElement("div", { className: "brokr-notif-item-body" }, /* @__PURE__ */ React40.createElement("span", { className: "brokr-notif-item-title" }, notif.title), /* @__PURE__ */ React40.createElement("span", { className: "brokr-notif-item-message" }, notif.message)),
8396
- /* @__PURE__ */ React40.createElement("span", { className: "brokr-notif-item-time" }, timeAgo(notif.createdAt))
8397
- );
8398
- }))));
8711
+ )), /* @__PURE__ */ React40.createElement("div", { className: "brokr-notif-dropdown-list" }, isLoading ? /* @__PURE__ */ React40.createElement("div", { className: "brokr-notif-empty" }, /* @__PURE__ */ React40.createElement("span", { className: "brokr-notif-empty-text" }, "Loading\u2026")) : sorted.length === 0 ? /* @__PURE__ */ React40.createElement("div", { className: "brokr-notif-empty" }, /* @__PURE__ */ React40.createElement("span", { className: "brokr-notif-empty-text" }, "No notifications yet")) : sorted.map((notif) => /* @__PURE__ */ React40.createElement(
8712
+ NotifDropdownItem,
8713
+ {
8714
+ key: notif.id,
8715
+ notif,
8716
+ registry,
8717
+ onClick: handleItemClick
8718
+ }
8719
+ )))));
8399
8720
  }
8400
8721
 
8401
8722
  // src/react/notifications/NotificationList.tsx
8402
- import React41, { useCallback as useCallback27, useMemo as useMemo27 } from "react";
8723
+ import React41, { useCallback as useCallback28, useMemo as useMemo27 } from "react";
8403
8724
  function formatTimestamp(iso) {
8404
8725
  const date = new Date(iso);
8405
8726
  return new Intl.DateTimeFormat("en-US", {
@@ -8412,6 +8733,27 @@ function formatTimestamp(iso) {
8412
8733
  function NotificationListSkeleton() {
8413
8734
  return /* @__PURE__ */ React41.createElement("div", { className: "brokr-notif-list-items" }, [1, 2, 3].map((i) => /* @__PURE__ */ React41.createElement("div", { key: i, className: "brokr-notif-list-row brokr-notif-list-row--skeleton" }, /* @__PURE__ */ React41.createElement("span", { className: "brokr-notif-item-dot brokr-notif-item-dot--skeleton" }), /* @__PURE__ */ React41.createElement("div", { className: "brokr-notif-item-body" }, /* @__PURE__ */ React41.createElement("span", { className: "brokr-notif-item-title brokr-skeleton-line", style: { width: "60%" } }), /* @__PURE__ */ React41.createElement("span", { className: "brokr-notif-item-message brokr-skeleton-line", style: { width: "80%" } })), /* @__PURE__ */ React41.createElement("span", { className: "brokr-notif-item-time brokr-skeleton-line", style: { width: 48 } }))));
8414
8735
  }
8736
+ function NotifListItem({
8737
+ notif,
8738
+ registry,
8739
+ onClick
8740
+ }) {
8741
+ const handleClick = useCallback28(() => onClick(notif), [notif, onClick]);
8742
+ const notifData = notif.data ?? {};
8743
+ const notifType = notif.type ?? notifData.type ?? "default";
8744
+ const resolved = resolveNotificationType(registry, notifType, notifData);
8745
+ return /* @__PURE__ */ React41.createElement(
8746
+ "button",
8747
+ {
8748
+ type: "button",
8749
+ className: `brokr-notif-list-row${notif.read ? "" : " brokr-notif-list-row--unread"}`,
8750
+ onClick: handleClick
8751
+ },
8752
+ resolved.image ? /* @__PURE__ */ React41.createElement("img", { src: resolved.image.url, alt: resolved.image.alt, className: "brokr-notif-item-logo" }) : /* @__PURE__ */ React41.createElement("span", { className: `brokr-notif-item-dot brokr-notif-item-dot--${notif.variant}` }),
8753
+ /* @__PURE__ */ React41.createElement("div", { className: "brokr-notif-item-body" }, /* @__PURE__ */ React41.createElement("span", { className: "brokr-notif-item-title" }, notif.title), /* @__PURE__ */ React41.createElement("span", { className: "brokr-notif-item-message" }, notif.message)),
8754
+ /* @__PURE__ */ React41.createElement("span", { className: "brokr-notif-item-time" }, formatTimestamp(notif.createdAt))
8755
+ );
8756
+ }
8415
8757
  function NotificationList() {
8416
8758
  const { notifications, unreadCount, markRead, markAllRead, isLoading, registry } = useNotifications();
8417
8759
  const sorted = useMemo27(
@@ -8420,7 +8762,7 @@ function NotificationList() {
8420
8762
  ),
8421
8763
  [notifications]
8422
8764
  );
8423
- const handleClick = useCallback27((notif) => {
8765
+ const handleClick = useCallback28((notif) => {
8424
8766
  if (!notif.read) markRead(notif.id);
8425
8767
  const notifData = notif.data ?? {};
8426
8768
  const notifType = notif.type ?? notifData.type ?? "default";
@@ -8438,30 +8780,15 @@ function NotificationList() {
8438
8780
  onClick: markAllRead
8439
8781
  },
8440
8782
  "Mark all read"
8441
- )), isLoading ? /* @__PURE__ */ React41.createElement(NotificationListSkeleton, null) : sorted.length === 0 ? /* @__PURE__ */ React41.createElement("div", { className: "brokr-notif-empty" }, /* @__PURE__ */ React41.createElement("svg", { "aria-hidden": "true", width: "40", height: "40", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1", strokeLinecap: "round", strokeLinejoin: "round", style: { opacity: 0.3 } }, /* @__PURE__ */ React41.createElement("path", { d: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" }), /* @__PURE__ */ React41.createElement("path", { d: "M10.3 21a1.94 1.94 0 0 0 3.4 0" })), /* @__PURE__ */ React41.createElement("span", { className: "brokr-notif-empty-text" }, "No notifications yet")) : /* @__PURE__ */ React41.createElement("div", { className: "brokr-notif-list-items" }, sorted.map((notif) => {
8442
- const notifData = notif.data ?? {};
8443
- const notifType = notif.type ?? notifData.type ?? "default";
8444
- const resolved = resolveNotificationType(registry, notifType, notifData);
8445
- return /* @__PURE__ */ React41.createElement(
8446
- "button",
8447
- {
8448
- key: notif.id,
8449
- type: "button",
8450
- className: `brokr-notif-list-row${notif.read ? "" : " brokr-notif-list-row--unread"}`,
8451
- onClick: () => handleClick(notif)
8452
- },
8453
- resolved.image ? /* @__PURE__ */ React41.createElement(
8454
- "img",
8455
- {
8456
- src: resolved.image.url,
8457
- alt: resolved.image.alt,
8458
- className: "brokr-notif-item-logo"
8459
- }
8460
- ) : /* @__PURE__ */ React41.createElement("span", { className: `brokr-notif-item-dot brokr-notif-item-dot--${notif.variant}` }),
8461
- /* @__PURE__ */ React41.createElement("div", { className: "brokr-notif-item-body" }, /* @__PURE__ */ React41.createElement("span", { className: "brokr-notif-item-title" }, notif.title), /* @__PURE__ */ React41.createElement("span", { className: "brokr-notif-item-message" }, notif.message)),
8462
- /* @__PURE__ */ React41.createElement("span", { className: "brokr-notif-item-time" }, formatTimestamp(notif.createdAt))
8463
- );
8464
- })));
8783
+ )), isLoading ? /* @__PURE__ */ React41.createElement(NotificationListSkeleton, null) : sorted.length === 0 ? /* @__PURE__ */ React41.createElement("div", { className: "brokr-notif-empty" }, /* @__PURE__ */ React41.createElement("svg", { "aria-hidden": "true", width: "40", height: "40", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1", strokeLinecap: "round", strokeLinejoin: "round", style: { opacity: 0.3 } }, /* @__PURE__ */ React41.createElement("path", { d: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" }), /* @__PURE__ */ React41.createElement("path", { d: "M10.3 21a1.94 1.94 0 0 0 3.4 0" })), /* @__PURE__ */ React41.createElement("span", { className: "brokr-notif-empty-text" }, "No notifications yet")) : /* @__PURE__ */ React41.createElement("div", { className: "brokr-notif-list-items" }, sorted.map((notif) => /* @__PURE__ */ React41.createElement(
8784
+ NotifListItem,
8785
+ {
8786
+ key: notif.id,
8787
+ notif,
8788
+ registry,
8789
+ onClick: handleClick
8790
+ }
8791
+ ))));
8465
8792
  }
8466
8793
 
8467
8794
  // src/react/hooks/use-user.ts