@swift-food-services/catering-widget 0.2.0-beta.5 → 0.2.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -5292,9 +5292,6 @@ styleInject(`/*! tailwindcss v4.2.4 | MIT License | https://tailwindcss.com */
5292
5292
  .swift-catering-widget .pt-8 {
5293
5293
  padding-top: calc(var(--spacing) * 8);
5294
5294
  }
5295
- .swift-catering-widget .pt-14 {
5296
- padding-top: calc(var(--spacing) * 14);
5297
- }
5298
5295
  .swift-catering-widget .pr-1 {
5299
5296
  padding-right: calc(var(--spacing) * 1);
5300
5297
  }
@@ -5355,6 +5352,9 @@ styleInject(`/*! tailwindcss v4.2.4 | MIT License | https://tailwindcss.com */
5355
5352
  .swift-catering-widget .pl-4 {
5356
5353
  padding-left: calc(var(--spacing) * 4);
5357
5354
  }
5355
+ .swift-catering-widget .pl-6 {
5356
+ padding-left: calc(var(--spacing) * 6);
5357
+ }
5358
5358
  .swift-catering-widget .pl-9 {
5359
5359
  padding-left: calc(var(--spacing) * 9);
5360
5360
  }
@@ -9465,7 +9465,7 @@ function MenuItemModal({
9465
9465
  onClick: () => handleSaveAiPick(true),
9466
9466
  disabled: isMinSelectionsUnmet,
9467
9467
  className: `w-full py-3 rounded-lg font-medium transition-all text-sm ${isMinSelectionsUnmet ? "bg-base-300 text-base-content/50 cursor-not-allowed" : "bg-primary hover:opacity-90 text-white"}`,
9468
- children: isPickedInAiMenu ? "Save selections" : "Pick \xB7 Save selections"
9468
+ children: addButtonLabel ?? (isPickedInAiMenu ? "Save selections" : "Pick \xB7 Save selections")
9469
9469
  }
9470
9470
  ),
9471
9471
  isPickedInAiMenu && /* @__PURE__ */ jsxRuntime.jsx(
@@ -13491,7 +13491,7 @@ function ChatSessionProvider({
13491
13491
  if (!chat.sessionId) setActiveViewedPreview(null);
13492
13492
  }, [chat.sessionId]);
13493
13493
  const hasResults = react.useMemo(
13494
- () => chat.latestMealSessionParts.length > 0,
13494
+ () => chat.latestMealSessionParts.some((p) => p.intentBlocks.length > 0),
13495
13495
  [chat.latestMealSessionParts]
13496
13496
  );
13497
13497
  const prevPartsRef = react.useRef(chat.latestMealSessionParts);
@@ -16282,7 +16282,8 @@ var FIELD_LABEL = {
16282
16282
  sessionDate: "Date",
16283
16283
  eventTime: "Food arrives by",
16284
16284
  deliveryLocation: "Location",
16285
- guestCount: "Guests"
16285
+ guestCount: "Guests",
16286
+ budget: "Budget"
16286
16287
  };
16287
16288
  function MissingFieldsClarifier({
16288
16289
  missing,
@@ -16340,6 +16341,26 @@ function getOverlayRoot() {
16340
16341
  }
16341
16342
  return document.querySelector("[data-swift-overlay-root]") ?? document.body;
16342
16343
  }
16344
+ function swapOptionToMenuItem(o, restaurantId) {
16345
+ return {
16346
+ id: o.menuItemId,
16347
+ menuItemName: o.name,
16348
+ description: o.description ?? void 0,
16349
+ price: String(o.unitPrice),
16350
+ discountPrice: o.discountPrice != null ? String(o.discountPrice) : void 0,
16351
+ isDiscount: o.isDiscount,
16352
+ image: o.imageUrl ?? void 0,
16353
+ allergens: o.allergens,
16354
+ dietaryFilters: o.dietaryFilters,
16355
+ cateringQuantityUnit: o.cateringQuantityUnit,
16356
+ feedsPerUnit: o.feedsPerUnit,
16357
+ minOrderQuantity: o.minOrderQuantity,
16358
+ restaurantId,
16359
+ groupTitle: o.groupTitle ?? void 0,
16360
+ itemDisplayOrder: 0,
16361
+ addons: o.addons ?? []
16362
+ };
16363
+ }
16343
16364
  function SwapModal({
16344
16365
  open,
16345
16366
  sessionId,
@@ -16357,10 +16378,12 @@ function SwapModal({
16357
16378
  const [options, setOptions] = react.useState(null);
16358
16379
  const [loading, setLoading] = react.useState(false);
16359
16380
  const [error, setError] = react.useState(null);
16381
+ const [selected, setSelected] = react.useState(null);
16360
16382
  react.useEffect(() => {
16361
16383
  if (!open || !restaurantId || !category) {
16362
16384
  setOptions(null);
16363
16385
  setError(null);
16386
+ setSelected(null);
16364
16387
  return;
16365
16388
  }
16366
16389
  let cancelled = false;
@@ -16385,6 +16408,24 @@ function SwapModal({
16385
16408
  };
16386
16409
  }, [open, sessionId, restaurantId, category, excludeIds.join(","), intentPhrase]);
16387
16410
  if (typeof document === "undefined") return null;
16411
+ if (selected && restaurantId) {
16412
+ return /* @__PURE__ */ jsxRuntime.jsx(
16413
+ MenuItemModal,
16414
+ {
16415
+ item: swapOptionToMenuItem(selected, restaurantId),
16416
+ isOpen: true,
16417
+ onClose: () => setSelected(null),
16418
+ viewOnly: true,
16419
+ aiAddonMode: true,
16420
+ addButtonLabel: "Confirm Swap",
16421
+ isPickedInAiMenu: false,
16422
+ onSaveAiPickAddons: (selections) => {
16423
+ onPick(selected, selections);
16424
+ setSelected(null);
16425
+ }
16426
+ }
16427
+ );
16428
+ }
16388
16429
  const modal = /* @__PURE__ */ jsxRuntime.jsx(react$1.AnimatePresence, { children: open && /* @__PURE__ */ jsxRuntime.jsx(
16389
16430
  react$1.motion.div,
16390
16431
  {
@@ -16466,7 +16507,7 @@ function SwapModal({
16466
16507
  visible: { transition: { staggerChildren: 0.04, delayChildren: 0.02 } }
16467
16508
  },
16468
16509
  style: { display: "flex", flexDirection: "column" },
16469
- children: options.map((opt, i) => /* @__PURE__ */ jsxRuntime.jsx(SwapCard, { option: opt, onPick: () => onPick(opt), showDivider: i < options.length - 1 }, opt.menuItemId))
16510
+ children: options.map((opt, i) => /* @__PURE__ */ jsxRuntime.jsx(SwapCard, { option: opt, onPick: () => setSelected(opt), showDivider: i < options.length - 1 }, opt.menuItemId))
16470
16511
  }
16471
16512
  )
16472
16513
  ] })
@@ -16658,6 +16699,15 @@ var GOOGLE_MAPS_CONFIG = {
16658
16699
  "name"
16659
16700
  ]
16660
16701
  };
16702
+ var LONDON_DELIVERY_BOUNDS = {
16703
+ north: 51.75,
16704
+ south: 51.2,
16705
+ east: 0.35,
16706
+ west: -0.55
16707
+ };
16708
+ function isWithinLondonBounds(lat, lng) {
16709
+ return lat >= LONDON_DELIVERY_BOUNDS.south && lat <= LONDON_DELIVERY_BOUNDS.north && lng >= LONDON_DELIVERY_BOUNDS.west && lng <= LONDON_DELIVERY_BOUNDS.east;
16710
+ }
16661
16711
 
16662
16712
  // src/utils/google-maps-loader.ts
16663
16713
  var isLoading = false;
@@ -16702,8 +16752,10 @@ function loadGoogleMapsScript(apiKey) {
16702
16752
  }
16703
16753
 
16704
16754
  // src/hooks/useAddressAutocomplete.ts
16755
+ var OUT_OF_ZONE_MESSAGE = "We only deliver inside London right now \u2014 please pick a London address.";
16705
16756
  function useAddressAutocomplete(onPlaceSelect, options = {}) {
16706
16757
  const countryRestriction = options.countryRestriction === void 0 ? GOOGLE_MAPS_CONFIG.COUNTRY_RESTRICTION : options.countryRestriction;
16758
+ const restrictToDeliveryZone = options.restrictToDeliveryZone ?? true;
16707
16759
  const { googleMapsApiKey } = useCateringConfig();
16708
16760
  const inputRef = react.useRef(null);
16709
16761
  const containerRef = react.useRef(null);
@@ -16715,6 +16767,7 @@ function useAddressAutocomplete(onPlaceSelect, options = {}) {
16715
16767
  const [predictions, setPredictions] = react.useState([]);
16716
16768
  const [open, setOpen] = react.useState(false);
16717
16769
  const [activeIndex, setActiveIndex] = react.useState(-1);
16770
+ const [outOfZoneError, setOutOfZoneError] = react.useState(null);
16718
16771
  react.useEffect(() => {
16719
16772
  loadGoogleMapsScript(googleMapsApiKey).then(() => {
16720
16773
  if (!window.google?.maps?.places) return;
@@ -16725,6 +16778,7 @@ function useAddressAutocomplete(onPlaceSelect, options = {}) {
16725
16778
  }, [googleMapsApiKey]);
16726
16779
  react.useEffect(() => {
16727
16780
  if (debounceRef.current) clearTimeout(debounceRef.current);
16781
+ setOutOfZoneError(null);
16728
16782
  if (justSelectedRef.current) {
16729
16783
  justSelectedRef.current = false;
16730
16784
  return;
@@ -16739,7 +16793,14 @@ function useAddressAutocomplete(onPlaceSelect, options = {}) {
16739
16793
  autocompleteServiceRef.current.getPlacePredictions(
16740
16794
  {
16741
16795
  input: query,
16742
- ...countryRestriction ? { componentRestrictions: { country: countryRestriction } } : {}
16796
+ ...countryRestriction ? { componentRestrictions: { country: countryRestriction } } : {},
16797
+ // Delivery pickers bias predictions toward the London box so
16798
+ // local results rank first. NB: this is `bounds` (a bias the
16799
+ // legacy getPlacePredictions honors), NOT `locationRestriction`
16800
+ // — that field is silently rejected here and kills the dropdown.
16801
+ // The hard out-of-zone rejection lives on the on-select
16802
+ // coordinate guard below + the backend bbox.
16803
+ ...restrictToDeliveryZone ? { bounds: LONDON_DELIVERY_BOUNDS } : {}
16743
16804
  },
16744
16805
  (results, status) => {
16745
16806
  if (status === google.maps.places.PlacesServiceStatus.OK && results) {
@@ -16768,6 +16829,13 @@ function useAddressAutocomplete(onPlaceSelect, options = {}) {
16768
16829
  { placeId: prediction.place_id, fields: GOOGLE_MAPS_CONFIG.FIELDS },
16769
16830
  (place, status) => {
16770
16831
  if (status === google.maps.places.PlacesServiceStatus.OK && place?.geometry) {
16832
+ const lat = place.geometry.location?.lat();
16833
+ const lng = place.geometry.location?.lng();
16834
+ if (restrictToDeliveryZone && typeof lat === "number" && typeof lng === "number" && !isWithinLondonBounds(lat, lng)) {
16835
+ setOutOfZoneError(OUT_OF_ZONE_MESSAGE);
16836
+ return;
16837
+ }
16838
+ setOutOfZoneError(null);
16771
16839
  onPlaceSelect(place);
16772
16840
  }
16773
16841
  }
@@ -16805,6 +16873,7 @@ function useAddressAutocomplete(onPlaceSelect, options = {}) {
16805
16873
  setPredictions([]);
16806
16874
  setActiveIndex(-1);
16807
16875
  setOpen(false);
16876
+ setOutOfZoneError(null);
16808
16877
  };
16809
16878
  return {
16810
16879
  inputRef,
@@ -16817,7 +16886,8 @@ function useAddressAutocomplete(onPlaceSelect, options = {}) {
16817
16886
  setActiveIndex,
16818
16887
  handleSelect,
16819
16888
  handleKeyDown,
16820
- clear
16889
+ clear,
16890
+ outOfZoneError
16821
16891
  };
16822
16892
  }
16823
16893
  function ChatAddressInput({
@@ -16839,7 +16909,8 @@ function ChatAddressInput({
16839
16909
  activeIndex,
16840
16910
  setActiveIndex,
16841
16911
  handleSelect,
16842
- handleKeyDown
16912
+ handleKeyDown,
16913
+ outOfZoneError
16843
16914
  } = useAddressAutocomplete(onPlaceSelect);
16844
16915
  react.useEffect(() => {
16845
16916
  if (initialQuery) setQuery(initialQuery);
@@ -16918,6 +16989,7 @@ function ChatAddressInput({
16918
16989
  }
16919
16990
  )
16920
16991
  ] }),
16992
+ outOfZoneError && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 pl-6 text-xs text-red-500", children: outOfZoneError }),
16921
16993
  open && predictions.length > 0 && dropdownRect && typeof document !== "undefined" && reactDom.createPortal(
16922
16994
  /* Portal to the widget's shared overlay root so the
16923
16995
  dropdown escapes the bar pill's overflow-hidden +
@@ -16975,135 +17047,8 @@ function ChatAddressInput({
16975
17047
  )
16976
17048
  ] });
16977
17049
  }
16978
- var GATE_PROMPT = "What is the code to use the AI chat and test our beta version?";
16979
- function AIChatBetaGate({ onUnlock }) {
16980
- const textareaRef = react.useRef(null);
16981
- const [input, setInput] = react.useState("");
16982
- const [error, setError] = react.useState(null);
16983
- function submit() {
16984
- const trimmed = input.trim();
16985
- if (!trimmed) return;
16986
- if (onUnlock(trimmed)) return;
16987
- setError("Incorrect code. Try again.");
16988
- setInput("");
16989
- textareaRef.current?.focus();
16990
- }
16991
- function handleSubmit(e) {
16992
- e.preventDefault();
16993
- submit();
16994
- }
16995
- function handleKeyDown(e) {
16996
- if (e.key === "Enter" && !e.shiftKey) {
16997
- e.preventDefault();
16998
- submit();
16999
- }
17000
- }
17001
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "swift-chat-design bg-white rounded-xl shadow-sm border border-base-200 flex h-full min-h-0 flex-col overflow-hidden", children: [
17002
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-3 py-2.5 flex items-center gap-3 border-b border-base-200", children: [
17003
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-9 h-9 rounded-full bg-primary flex items-center justify-center flex-shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sparkles, { className: "w-4 h-4 text-white" }) }),
17004
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
17005
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-semibold text-gray-800", children: "AI Assistant" }),
17006
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500", children: "Beta access" })
17007
- ] })
17008
- ] }),
17009
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-h-0 overflow-y-auto p-3", children: [
17010
- /* @__PURE__ */ jsxRuntime.jsx(TextBubble, { sender: "bot", text: GATE_PROMPT }),
17011
- error && /* @__PURE__ */ jsxRuntime.jsx(
17012
- "p",
17013
- {
17014
- role: "alert",
17015
- className: "mt-2 text-xs text-red-600",
17016
- children: error
17017
- }
17018
- )
17019
- ] }),
17020
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-t border-base-200 p-2.5", children: /* @__PURE__ */ jsxRuntime.jsxs(
17021
- "form",
17022
- {
17023
- onSubmit: handleSubmit,
17024
- className: "flex items-end gap-2 rounded-xl border border-base-300 bg-white px-3 py-2 focus-within:border-primary transition-colors",
17025
- children: [
17026
- /* @__PURE__ */ jsxRuntime.jsx(
17027
- "textarea",
17028
- {
17029
- ref: textareaRef,
17030
- rows: 1,
17031
- value: input,
17032
- onChange: (e) => {
17033
- setInput(e.target.value);
17034
- if (error) setError(null);
17035
- },
17036
- onKeyDown: handleKeyDown,
17037
- placeholder: "Enter access code\u2026",
17038
- "aria-label": "Beta access code",
17039
- autoFocus: true,
17040
- className: "flex-1 bg-transparent text-sm text-gray-800 placeholder:text-gray-400 outline-none resize-none leading-snug py-1 overflow-hidden"
17041
- }
17042
- ),
17043
- /* @__PURE__ */ jsxRuntime.jsx(
17044
- "button",
17045
- {
17046
- type: "submit",
17047
- disabled: !input.trim(),
17048
- className: "flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full bg-primary text-white transition-colors hover:bg-primary/90 disabled:opacity-50",
17049
- title: "Submit code",
17050
- "aria-label": "Submit code",
17051
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Send, { className: "w-3.5 h-3.5" })
17052
- }
17053
- )
17054
- ]
17055
- }
17056
- ) })
17057
- ] });
17058
- }
17059
- var STORAGE_KEY3 = "swift-food-chat-beta-unlocked";
17060
- var BETA_CODE = "1423";
17061
- function readUnlocked() {
17062
- if (typeof window === "undefined") return false;
17063
- try {
17064
- return window.localStorage.getItem(STORAGE_KEY3) === "1";
17065
- } catch {
17066
- return false;
17067
- }
17068
- }
17069
- var unlockedValue = readUnlocked();
17070
- var subscribers = /* @__PURE__ */ new Set();
17071
- function setUnlocked(next) {
17072
- if (unlockedValue === next) return;
17073
- unlockedValue = next;
17074
- subscribers.forEach((cb) => cb());
17075
- }
17076
- function subscribe(cb) {
17077
- subscribers.add(cb);
17078
- return () => {
17079
- subscribers.delete(cb);
17080
- };
17081
- }
17082
- function useBetaUnlock() {
17083
- const unlocked = react.useSyncExternalStore(
17084
- subscribe,
17085
- () => unlockedValue,
17086
- () => false
17087
- );
17088
- const tryUnlock = react.useCallback((code) => {
17089
- if (code.trim() !== BETA_CODE) return false;
17090
- setUnlocked(true);
17091
- if (typeof window !== "undefined") {
17092
- try {
17093
- window.localStorage.setItem(STORAGE_KEY3, "1");
17094
- } catch {
17095
- }
17096
- }
17097
- return true;
17098
- }, []);
17099
- return { unlocked, tryUnlock };
17100
- }
17101
17050
  var MAX_INPUT_HEIGHT = 140;
17102
17051
  function AIChat() {
17103
- const { unlocked, tryUnlock } = useBetaUnlock();
17104
- if (!unlocked) {
17105
- return /* @__PURE__ */ jsxRuntime.jsx(AIChatBetaGate, { onUnlock: tryUnlock });
17106
- }
17107
17052
  return /* @__PURE__ */ jsxRuntime.jsx(AIChatBody, {});
17108
17053
  }
17109
17054
  function AIChatBody() {
@@ -17114,6 +17059,9 @@ function AIChatBody() {
17114
17059
  sessionId,
17115
17060
  latestChips,
17116
17061
  missingFields,
17062
+ latestMealSessions,
17063
+ latestActiveMealSessionIndex,
17064
+ latestSummary,
17117
17065
  sendText,
17118
17066
  applyEditField,
17119
17067
  handleChip,
@@ -17182,6 +17130,24 @@ function AIChatBody() {
17182
17130
  setPendingInputFocus(false);
17183
17131
  }, [pendingInputFocus, gateActive, pillEditing, bootstrapping, sessionId]);
17184
17132
  const inFeedbackMode = feedbackTarget !== null;
17133
+ const activeMeal = latestMealSessions[latestActiveMealSessionIndex];
17134
+ const derivedMissing = [];
17135
+ if (latestMealSessions.length > 0) {
17136
+ for (const f of activeMeal?.missingFields ?? []) {
17137
+ if (f === "sessionDate" || f === "eventTime" || f === "guestCount") {
17138
+ derivedMissing.push(f);
17139
+ }
17140
+ }
17141
+ if ((latestSummary?.taxonomy?.budget ?? null) == null) {
17142
+ derivedMissing.push("budget");
17143
+ }
17144
+ if (!hasAddress) derivedMissing.push("deliveryLocation");
17145
+ }
17146
+ const effectiveMissingFields = missingFields && missingFields.fields.length > 0 ? missingFields : derivedMissing.length > 0 ? {
17147
+ fields: derivedMissing,
17148
+ reason: missingFields?.reason ?? "Add these and I'll build your menu."
17149
+ } : null;
17150
+ const mandatoryFieldsLocked = !inFeedbackMode && !!effectiveMissingFields && effectiveMissingFields.fields.length > 0;
17185
17151
  const RATING_LABELS = ["Terrible", "Poor", "Okay", "Good", "Great"];
17186
17152
  react.useEffect(() => {
17187
17153
  if (!feedbackTarget) return;
@@ -17259,6 +17225,7 @@ function AIChatBody() {
17259
17225
  }
17260
17226
  if (sending || bootstrapping || !sessionId || !input.trim()) return;
17261
17227
  if (gateActive && !hasAddress) return;
17228
+ if (mandatoryFieldsLocked) return;
17262
17229
  void sendText(input, buildChatAddress());
17263
17230
  setInput("");
17264
17231
  }
@@ -17426,10 +17393,10 @@ function AIChatBody() {
17426
17393
  ) })
17427
17394
  ] }),
17428
17395
  actionChips.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-shrink-0 border-t border-base-200 px-3 py-2", children: /* @__PURE__ */ jsxRuntime.jsx(ChipGroup, { chips: actionChips, onAction: handleChipClick }) }),
17429
- missingFields && missingFields.fields.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(
17396
+ effectiveMissingFields && effectiveMissingFields.fields.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(
17430
17397
  MissingFieldsClarifier,
17431
17398
  {
17432
- missing: missingFields,
17399
+ missing: effectiveMissingFields,
17433
17400
  onEditField: (field) => {
17434
17401
  if (field === "deliveryLocation") {
17435
17402
  setPillEditing(true);
@@ -17576,7 +17543,7 @@ function AIChatBody() {
17576
17543
  value: inFeedbackMode ? feedbackNote : input,
17577
17544
  onChange: (e) => inFeedbackMode ? setFeedbackNote(e.target.value) : setInput(e.target.value),
17578
17545
  onKeyDown: handleKeyDown,
17579
- placeholder: inFeedbackMode ? feedbackRating <= 2 ? "What went wrong? (optional)" : "Add a note (optional)" : "Ask for menu suggestions...",
17546
+ placeholder: inFeedbackMode ? feedbackRating <= 2 ? "What went wrong? (optional)" : "Add a note (optional)" : mandatoryFieldsLocked ? "Add the details above to continue\u2026" : "Ask for menu suggestions...",
17580
17547
  disabled: inFeedbackMode ? feedbackState === "sending" : bootstrapping || !sessionId,
17581
17548
  className: "flex-1 bg-transparent text-sm text-gray-800 placeholder:text-gray-400 outline-none resize-none leading-snug py-1 overflow-hidden"
17582
17549
  }
@@ -17585,7 +17552,7 @@ function AIChatBody() {
17585
17552
  "button",
17586
17553
  {
17587
17554
  type: "submit",
17588
- disabled: inFeedbackMode ? feedbackRating < 1 || feedbackState === "sending" : sending || bootstrapping || !input.trim() || !sessionId,
17555
+ disabled: inFeedbackMode ? feedbackRating < 1 || feedbackState === "sending" : sending || bootstrapping || !input.trim() || !sessionId || mandatoryFieldsLocked,
17589
17556
  className: "flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full bg-primary text-white transition-colors hover:bg-primary/90 disabled:opacity-50",
17590
17557
  title: inFeedbackMode ? "Submit feedback" : "Send",
17591
17558
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Send, { className: "w-3.5 h-3.5" })
@@ -17610,7 +17577,7 @@ function AIChatBody() {
17610
17577
  intentExcludes: swapTarget?.intentExcludes ?? null,
17611
17578
  itemName: swapTarget?.itemName ?? "",
17612
17579
  onClose: () => setSwapTarget(null),
17613
- onPick: (replacement) => {
17580
+ onPick: (replacement, addonSelections) => {
17614
17581
  const target = swapTarget;
17615
17582
  if (!target) return;
17616
17583
  setSwapTarget(null);
@@ -17627,10 +17594,15 @@ function AIChatBody() {
17627
17594
  allergens: replacement.allergens,
17628
17595
  dietaryFilters: replacement.dietaryFilters,
17629
17596
  feedsPerUnit: replacement.feedsPerUnit,
17630
- cateringQuantityUnit: 1,
17631
- minOrderQuantity: 1,
17632
- addons: []
17597
+ cateringQuantityUnit: replacement.cateringQuantityUnit,
17598
+ minOrderQuantity: replacement.minOrderQuantity,
17599
+ addons: replacement.addons
17633
17600
  });
17601
+ cart.setAddonSelections(
17602
+ target.intentId,
17603
+ replacement.menuItemId,
17604
+ addonSelections
17605
+ );
17634
17606
  }
17635
17607
  }
17636
17608
  )
@@ -17653,7 +17625,8 @@ function MobileAddressPickerSurface({
17653
17625
  activeIndex,
17654
17626
  setActiveIndex,
17655
17627
  handleSelect,
17656
- handleKeyDown
17628
+ handleKeyDown,
17629
+ outOfZoneError
17657
17630
  } = useAddressAutocomplete(onPlaceSelect);
17658
17631
  react.useEffect(() => {
17659
17632
  if (initialQuery) setQuery(initialQuery);
@@ -17779,6 +17752,7 @@ function MobileAddressPickerSurface({
17779
17752
  ] })
17780
17753
  }
17781
17754
  ),
17755
+ outOfZoneError && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 flex-shrink-0 text-center text-xs text-red-600", children: outOfZoneError }),
17782
17756
  predictions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
17783
17757
  react$1.motion.div,
17784
17758
  {
@@ -20135,7 +20109,9 @@ function EmptyIntentState({ message }) {
20135
20109
  var MISSING_FIELD_LABELS = {
20136
20110
  guestCount: "guest count",
20137
20111
  sessionDate: "date",
20138
- eventTime: "time"
20112
+ eventTime: "time",
20113
+ deliveryLocation: "delivery location",
20114
+ budget: "budget"
20139
20115
  };
20140
20116
  function formatMissingFieldList(fields) {
20141
20117
  const labels = fields.map((f) => MISSING_FIELD_LABELS[f] ?? f);
@@ -21087,7 +21063,8 @@ function InlineAddressInput({
21087
21063
  activeIndex,
21088
21064
  setActiveIndex,
21089
21065
  handleSelect,
21090
- handleKeyDown
21066
+ handleKeyDown,
21067
+ outOfZoneError
21091
21068
  } = useAddressAutocomplete(onPlaceSelect);
21092
21069
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: containerRef, className: "relative mt-1.5 pt-1.5 border-t border-base-200", children: [
21093
21070
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
@@ -21125,7 +21102,8 @@ function InlineAddressInput({
21125
21102
  ]
21126
21103
  },
21127
21104
  p.place_id
21128
- )) })
21105
+ )) }),
21106
+ outOfZoneError && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-[10px] text-red-500", children: outOfZoneError })
21129
21107
  ] });
21130
21108
  }
21131
21109
  function PricingSummary({
@@ -24705,8 +24683,10 @@ function AddressAutocomplete({
24705
24683
  setActiveIndex,
24706
24684
  handleSelect,
24707
24685
  handleKeyDown,
24708
- clear
24686
+ clear,
24687
+ outOfZoneError
24709
24688
  } = useAddressAutocomplete(onPlaceSelect);
24689
+ const shownError = error || outOfZoneError || void 0;
24710
24690
  const handleClear = () => {
24711
24691
  clear();
24712
24692
  if (onClearAddress) onClearAddress();
@@ -24727,7 +24707,7 @@ function AddressAutocomplete({
24727
24707
  onKeyDown: handleKeyDown,
24728
24708
  placeholder: "Start typing an address...",
24729
24709
  autoComplete: "new-password",
24730
- className: `address-search-input w-full bg-gray-50 border rounded-lg px-4 py-2.5 text-base text-base-content placeholder:text-base-content/50 focus:outline-none focus:ring-2 focus:ring-dark-pink/20 focus:border-dark-pink transition-all ${error ? "border-error" : hasValidAddress ? "border-success" : "border-base-300"}`
24710
+ className: `address-search-input w-full bg-gray-50 border rounded-lg px-4 py-2.5 text-base text-base-content placeholder:text-base-content/50 focus:outline-none focus:ring-2 focus:ring-dark-pink/20 focus:border-dark-pink transition-all ${shownError ? "border-error" : hasValidAddress ? "border-success" : "border-base-300"}`
24731
24711
  }
24732
24712
  ),
24733
24713
  hasValidAddress && /* @__PURE__ */ jsxRuntime.jsx(
@@ -24760,9 +24740,9 @@ function AddressAutocomplete({
24760
24740
  p.place_id
24761
24741
  )) })
24762
24742
  ] }),
24763
- error && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-xs text-error", children: error }),
24764
- hasValidAddress && !error && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-xs text-success", children: "Address selected" }),
24765
- !hasValidAddress && !error && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-xs text-base-content/60", children: "Please select an address from the dropdown" })
24743
+ shownError && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-xs text-error", children: shownError }),
24744
+ hasValidAddress && !shownError && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-xs text-success", children: "Address selected" }),
24745
+ !hasValidAddress && !shownError && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-xs text-base-content/60", children: "Please select an address from the dropdown" })
24766
24746
  ] });
24767
24747
  }
24768
24748
  function DeliveryAddressForm({
@@ -24900,7 +24880,11 @@ function BillingAddressAutocomplete({
24900
24880
  setActiveIndex,
24901
24881
  handleSelect,
24902
24882
  handleKeyDown
24903
- } = useAddressAutocomplete(onPlaceSelect, { countryRestriction: null });
24883
+ } = useAddressAutocomplete(onPlaceSelect, {
24884
+ countryRestriction: null,
24885
+ // Billing can be anywhere — only delivery is London-restricted.
24886
+ restrictToDeliveryZone: false
24887
+ });
24904
24888
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "col-span-full", ref: containerRef, children: [
24905
24889
  /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "block text-[10px] font-bold text-base-content/60 uppercase tracking-widest mb-1.5", children: [
24906
24890
  "Search Address",
@@ -27646,7 +27630,6 @@ function CateringOrderBuilder() {
27646
27630
  const [mobileAIInput, setMobileAIInput] = react.useState("");
27647
27631
  const [mobileAIInputHeight, setMobileAIInputHeight] = react.useState(44);
27648
27632
  const mobileAIInputRef = react.useRef(null);
27649
- const [mobileGateError, setMobileGateError] = react.useState(null);
27650
27633
  const mobileChatScrollRef = react.useRef(null);
27651
27634
  const [mobileAddressPillEditing, setMobileAddressPillEditing] = react.useState(false);
27652
27635
  const buildChatAddress = useChatAddressFromContext();
@@ -27841,20 +27824,6 @@ function CateringOrderBuilder() {
27841
27824
  });
27842
27825
  setMobileAddressPillEditing(false);
27843
27826
  };
27844
- const handleMobileGateSubmit = () => {
27845
- const code = mobileAIInput.trim();
27846
- if (!code) return;
27847
- if (tryUnlockAiBeta(code)) {
27848
- setMobileAIInput("");
27849
- resetMobileAIInputHeight();
27850
- setMobileGateError(null);
27851
- return;
27852
- }
27853
- setMobileGateError("Incorrect code. Try again.");
27854
- setMobileAIInput("");
27855
- resetMobileAIInputHeight();
27856
- mobileAIInputRef.current?.focus();
27857
- };
27858
27827
  const closeMobileAIChat = () => {
27859
27828
  mobileAIInputRef.current?.blur();
27860
27829
  setTimeout(() => {
@@ -27951,9 +27920,8 @@ function CateringOrderBuilder() {
27951
27920
  setExpandedItemId(item.id);
27952
27921
  };
27953
27922
  const { stickyTopOffset = 0, publishableKey, aiEnabled = false } = useCateringConfig();
27954
- const { unlocked: aiBetaUnlocked, tryUnlock: tryUnlockAiBeta } = useBetaUnlock();
27955
- const mobileAddressGateActive = isMobileAIChatOpen && aiBetaUnlocked && !hasChatAddress;
27956
- const mobileAddressEditorOpen = mobileAddressGateActive || aiBetaUnlocked && mobileAddressPillEditing;
27923
+ const mobileAddressGateActive = isMobileAIChatOpen && !hasChatAddress;
27924
+ const mobileAddressEditorOpen = mobileAddressGateActive || mobileAddressPillEditing;
27957
27925
  const effectiveRightPanelTab = aiEnabled ? rightPanelTab : "cart";
27958
27926
  const [overlayVisible, setOverlayVisible] = react.useState(false);
27959
27927
  const basketColumnRef = react.useRef(null);
@@ -29040,7 +29008,7 @@ function CateringOrderBuilder() {
29040
29008
  transition: { duration: 0.18, ease: "easeOut" },
29041
29009
  className: "absolute inset-0",
29042
29010
  children: [
29043
- aiBetaUnlocked && hasChatAddress && mobileChatView === "chat" && /* @__PURE__ */ jsxRuntime.jsxs(
29011
+ hasChatAddress && mobileChatView === "chat" && /* @__PURE__ */ jsxRuntime.jsxs(
29044
29012
  react$1.motion.button,
29045
29013
  {
29046
29014
  type: "button",
@@ -29080,16 +29048,7 @@ function CateringOrderBuilder() {
29080
29048
  "data-allow-touch": true,
29081
29049
  className: "absolute inset-x-0 top-0 overflow-y-auto overscroll-contain",
29082
29050
  style: { bottom: 64 },
29083
- children: !aiBetaUnlocked ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 pt-14 pb-4 flex flex-col gap-3", children: [
29084
- /* @__PURE__ */ jsxRuntime.jsx(
29085
- TextBubble,
29086
- {
29087
- sender: "bot",
29088
- text: "What is the code to use the AI chat and test our beta version?"
29089
- }
29090
- ),
29091
- mobileGateError && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "text-xs text-red-600", children: mobileGateError })
29092
- ] }) : mobileChatView === "results" ? /* @__PURE__ */ jsxRuntime.jsx(
29051
+ children: mobileChatView === "results" ? /* @__PURE__ */ jsxRuntime.jsx(
29093
29052
  MobileResultsView,
29094
29053
  {
29095
29054
  onBack: () => setMobileChatView("chat")
@@ -29097,12 +29056,12 @@ function CateringOrderBuilder() {
29097
29056
  ) : /* @__PURE__ */ jsxRuntime.jsx(
29098
29057
  MobileChatThread,
29099
29058
  {
29100
- hasAddressTopPill: hasChatAddress && aiBetaUnlocked
29059
+ hasAddressTopPill: hasChatAddress
29101
29060
  }
29102
29061
  )
29103
29062
  }
29104
29063
  ),
29105
- aiBetaUnlocked && mobileChatView === "chat" && /* @__PURE__ */ jsxRuntime.jsx(MobileChatFloatingChips, { bottomOffset: 64 + 8, inputRef: mobileAIInputRef })
29064
+ mobileChatView === "chat" && /* @__PURE__ */ jsxRuntime.jsx(MobileChatFloatingChips, { bottomOffset: 64 + 8, inputRef: mobileAIInputRef })
29106
29065
  ]
29107
29066
  },
29108
29067
  "chat-surface"
@@ -29265,10 +29224,7 @@ function CateringOrderBuilder() {
29265
29224
  {
29266
29225
  inputRef: mobileAIInputRef,
29267
29226
  value: mobileAIInput,
29268
- onChange: (v) => {
29269
- setMobileAIInput(v);
29270
- if (mobileGateError) setMobileGateError(null);
29271
- },
29227
+ onChange: setMobileAIInput,
29272
29228
  onInputResize: handleMobileAIInput,
29273
29229
  onResetHeight: resetMobileAIInputHeight,
29274
29230
  tabbable: isMobileAIChatOpen,
@@ -29278,8 +29234,6 @@ function CateringOrderBuilder() {
29278
29234
  setChatInputFocused(true);
29279
29235
  },
29280
29236
  onBlur: () => setChatInputFocused(false),
29281
- gateMode: !aiBetaUnlocked,
29282
- onGateSubmit: handleMobileGateSubmit,
29283
29237
  buildAddress: buildChatAddress
29284
29238
  }
29285
29239
  )
@@ -29770,8 +29724,6 @@ function MobileAIInput({
29770
29724
  isOpen,
29771
29725
  onFocus,
29772
29726
  onBlur,
29773
- gateMode = false,
29774
- onGateSubmit,
29775
29727
  buildAddress,
29776
29728
  submitBlocked = false
29777
29729
  }) {
@@ -29788,7 +29740,7 @@ function MobileAIInput({
29788
29740
  }, [feedbackTarget]);
29789
29741
  const chatDisabled = sending || bootstrapping || !sessionId;
29790
29742
  const inputDisabled = inFeedbackMode ? feedbackState === "sending" : bootstrapping || !sessionId;
29791
- const sendDisabled = inFeedbackMode ? feedbackState === "sending" : submitBlocked || (gateMode ? !value.trim() : chatDisabled || !value.trim());
29743
+ const sendDisabled = inFeedbackMode ? feedbackState === "sending" : submitBlocked || (chatDisabled || !value.trim());
29792
29744
  function submit() {
29793
29745
  if (inFeedbackMode) {
29794
29746
  void handleFeedbackSubmit();
@@ -29796,10 +29748,6 @@ function MobileAIInput({
29796
29748
  }
29797
29749
  if (!value.trim()) return;
29798
29750
  if (submitBlocked) return;
29799
- if (gateMode) {
29800
- onGateSubmit?.();
29801
- return;
29802
- }
29803
29751
  if (chatDisabled) return;
29804
29752
  void sendText(value, buildAddress?.() ?? void 0);
29805
29753
  onChange("");
@@ -29836,9 +29784,9 @@ function MobileAIInput({
29836
29784
  onKeyDown: handleKeyDown,
29837
29785
  onFocus,
29838
29786
  onBlur,
29839
- placeholder: inFeedbackMode ? "Add a note (optional)" : gateMode ? "Enter access code\u2026" : "How can I help?",
29787
+ placeholder: inFeedbackMode ? "Add a note (optional)" : "How can I help?",
29840
29788
  tabIndex: tabbable ? 0 : -1,
29841
- disabled: gateMode ? false : inputDisabled,
29789
+ disabled: inputDisabled,
29842
29790
  autoComplete: "off",
29843
29791
  autoCorrect: "off",
29844
29792
  autoCapitalize: "off",