@curless/hotel-storefront 0.50.2 → 0.50.4
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/card.generated.d.ts +1 -1
- package/dist/card.generated.js +2 -2
- package/dist/card.generated.js.map +1 -1
- package/dist/config.d.ts +0 -27
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/demo-hotels.d.ts.map +1 -1
- package/dist/demo-hotels.js +9 -2
- package/dist/demo-hotels.js.map +1 -1
- package/dist/huazhu/backend.d.ts.map +1 -1
- package/dist/huazhu/backend.js +90 -23
- package/dist/huazhu/backend.js.map +1 -1
- package/dist/huazhu/sandbox.d.ts +7 -0
- package/dist/huazhu/sandbox.d.ts.map +1 -1
- package/dist/huazhu/sandbox.js +2 -2
- package/dist/huazhu/sandbox.js.map +1 -1
- package/dist/legal.d.ts.map +1 -1
- package/dist/legal.js +5 -1
- package/dist/legal.js.map +1 -1
- package/dist/overlap.d.ts +30 -0
- package/dist/overlap.d.ts.map +1 -0
- package/dist/overlap.js +45 -0
- package/dist/overlap.js.map +1 -0
- package/dist/present.d.ts +4 -0
- package/dist/present.d.ts.map +1 -1
- package/dist/present.js +8 -1
- package/dist/present.js.map +1 -1
- package/dist/server.d.ts +27 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +28 -10
- package/dist/server.js.map +1 -1
- package/dist/surface.d.ts +4 -3
- package/dist/surface.d.ts.map +1 -1
- package/dist/surface.js +13 -6
- package/dist/surface.js.map +1 -1
- package/dist/tools.d.ts +26 -5
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +177 -18
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +21 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/tools.js
CHANGED
|
@@ -5,6 +5,7 @@ import { DESCRIPTION_MAX, POLICY_MAX, hotelName, hotelText } from './fence.js';
|
|
|
5
5
|
import { clockOf, minutesOf, startProblem, stayWords, windowWords } from './hourly.js';
|
|
6
6
|
import { amountsIn, formatMoney, keepingAmounts, money, multiplyMoney, toMinor } from './money.js';
|
|
7
7
|
import { namedExactly, roomNamesOf, roomTypeNamed, withTraditional } from './names.js';
|
|
8
|
+
import { overlapsOf } from './overlap.js';
|
|
8
9
|
import { MAX_COMPARED, MAX_CONS, MAX_DAYS, MAX_ITEMS_PER_DAY, MAX_MOMENTS, MAX_PROS, buildItinerary, candidatesFor, compareHotels, namesMoreThanOne, } from './present.js';
|
|
9
10
|
import { regionOf } from './regions.js';
|
|
10
11
|
import { activeKeysOf, byRoomType, facetsOf, keyWords, matchesTerms, optionsOf, roomTermsOf, termWords, } from './room-filters.js';
|
|
@@ -96,13 +97,17 @@ export const buildTools = (n, v, caps = FULL_CAPABILITIES) => forCapabilities(al
|
|
|
96
97
|
* The list tool where the deployment is one hotel in all (2026-10-15, Huazhu's
|
|
97
98
|
* test system): asked with no place it answers with that hotel's page, not
|
|
98
99
|
* with places to choose from - and says so, or the model offers to look in
|
|
99
|
-
* another city (three smoke runs did).
|
|
100
|
+
* another city (three smoke runs did). Where it is a few (`HOME_LIST_MAX`,
|
|
101
|
+
* Huazhu's production, 2026-10-30), asked with no place it lists all of them.
|
|
102
|
+
* Never in a market.
|
|
100
103
|
*/
|
|
101
|
-
export const
|
|
104
|
+
export const homeListing = (t, brand, count) => {
|
|
102
105
|
const v = brand.vocab;
|
|
103
|
-
if (t.name !== brand.tools.listHotels || v.market)
|
|
106
|
+
if (t.name !== brand.tools.listHotels || v.market || count < 1 || count > HOME_LIST_MAX)
|
|
104
107
|
return t;
|
|
105
|
-
const one =
|
|
108
|
+
const one = count === 1
|
|
109
|
+
? `This deployment has ONE ${v.noun} in all: asked with no place, it answers with that ${v.noun}'s own page, and there is no other ${v.noun} or city to list or search. `
|
|
110
|
+
: `This deployment has ${count} ${v.nounPlural} in all: asked with no place, it lists every one of them, and there is no other ${v.noun} or city to list or search. `;
|
|
106
111
|
const places = /Asked with no place[\s\S]*?themselves\. /;
|
|
107
112
|
return {
|
|
108
113
|
...t,
|
|
@@ -1161,6 +1166,12 @@ const HOTEL_PAGE = {
|
|
|
1161
1166
|
* none - and never in what the assistant is told.
|
|
1162
1167
|
*/
|
|
1163
1168
|
soldOut: ARR,
|
|
1169
|
+
/**
|
|
1170
|
+
* Where some rates are listed but not booked here (`Room.notSoldHere`, paid
|
|
1171
|
+
* in advance on a deployment that takes no payment): what that means and
|
|
1172
|
+
* what to offer - the same sentence as the text.
|
|
1173
|
+
*/
|
|
1174
|
+
notSoldHere: STR,
|
|
1164
1175
|
};
|
|
1165
1176
|
/**
|
|
1166
1177
|
* What request_booking answers - and book_picked_room, which books the same way
|
|
@@ -1209,6 +1220,8 @@ const BOOKING_SCHEMA = {
|
|
|
1209
1220
|
stamp: STR,
|
|
1210
1221
|
/** The trip changed after the card that sent the form was drawn, so nothing was booked (`stampOf`). */
|
|
1211
1222
|
tripChanged: BOOL,
|
|
1223
|
+
/** Two stays at two hotels on the same nights, each with what the text says of it (`overlapsOf`). */
|
|
1224
|
+
overlaps: ARR,
|
|
1212
1225
|
},
|
|
1213
1226
|
required: ['view'],
|
|
1214
1227
|
};
|
|
@@ -1245,6 +1258,10 @@ export const buildOutputSchemas = (n) => ({
|
|
|
1245
1258
|
widened: OBJ,
|
|
1246
1259
|
/** The deployment's only hotel, answered with its page - see HOTEL_PAGE. */
|
|
1247
1260
|
only: BOOL,
|
|
1261
|
+
/** A deployment of a few, answered with the list of all of them (`HOME_LIST_MAX`). */
|
|
1262
|
+
home: BOOL,
|
|
1263
|
+
/** With `only` or `home`: every hotel there is, and that there are no others - as the text says it. */
|
|
1264
|
+
inAll: STR,
|
|
1248
1265
|
/** Only where the client shows no cards: nothing was drawn - see `LIST_NOT_DRAWN`. */
|
|
1249
1266
|
notDrawn: STR,
|
|
1250
1267
|
...HOTEL_PAGE,
|
|
@@ -1392,6 +1409,8 @@ export const buildOutputSchemas = (n) => ({
|
|
|
1392
1409
|
staysTotal: STR,
|
|
1393
1410
|
/** What the text says of a long stay at one hotel, in the same words. */
|
|
1394
1411
|
nudges: STRS,
|
|
1412
|
+
/** Two stays at two hotels on the same nights, each with what the text says of it (`overlapsOf`). */
|
|
1413
|
+
overlaps: ARR,
|
|
1395
1414
|
/** Only for a client that shows no cards: that nothing was drawn - `PLAN_NOT_DRAWN`. */
|
|
1396
1415
|
notDrawn: STR,
|
|
1397
1416
|
},
|
|
@@ -2160,10 +2179,22 @@ const LABEL_WORDS = {
|
|
|
2160
2179
|
const labelWords = (r) => {
|
|
2161
2180
|
const words = [
|
|
2162
2181
|
...(r.labels ?? []).map((l) => LABEL_WORDS[l]),
|
|
2163
|
-
...(r.payment === 'prepaid'
|
|
2182
|
+
...(r.payment === 'prepaid'
|
|
2183
|
+
? [r.notSoldHere ? 'paid in advance - not booked here' : 'paid in advance']
|
|
2184
|
+
: []),
|
|
2164
2185
|
];
|
|
2165
2186
|
return words.length ? ` [${words.join(', ')}]` : '';
|
|
2166
2187
|
};
|
|
2188
|
+
/**
|
|
2189
|
+
* What is said of rates listed but not booked here (`Room.notSoldHere`) - in
|
|
2190
|
+
* the text and, word for word, in the structure (`notSoldHere`): the model reads
|
|
2191
|
+
* the structure (2026-10-28). Listed and marked as the group sells them; the
|
|
2192
|
+
* card says no more than 预付 on them (robin, 2026-10-30), so the assistant is
|
|
2193
|
+
* the one who knows - and says it when the guest wants one.
|
|
2194
|
+
*/
|
|
2195
|
+
export const notSoldHereWords = (group) => `Rates marked paid in advance are listed as ${group} sells them, but a stay paid in advance cannot be booked here yet: this connector books rates paid at the hotel. If the guest wants one, say so plainly - nothing is booked or charged - and offer a rate paid at the hotel, other dates, or booking it with ${group} directly.`;
|
|
2196
|
+
/** The group a hotel is with, by name - the deployment's own brand outside a market. */
|
|
2197
|
+
const groupNameOf = (deps, hotelId) => holderOf(deps, deps.backend.supplierOf?.(hotelId)).name;
|
|
2167
2198
|
/** What a rate says about cancelling, as the text puts it. */
|
|
2168
2199
|
const cancelWords = (r) => r.freeCancellationUntil
|
|
2169
2200
|
? `free to cancel until ${freeUntilText(r)}`
|
|
@@ -2370,7 +2401,13 @@ const dateFlex = (dates, stay) => {
|
|
|
2370
2401
|
dates: dates.map((d) => ({
|
|
2371
2402
|
checkIn: d.checkIn,
|
|
2372
2403
|
checkOut: d.checkOut,
|
|
2373
|
-
|
|
2404
|
+
// "Sold out" only when it is (D2, robin 2026-10-30): a day with rooms
|
|
2405
|
+
// that cannot be booked here says why instead.
|
|
2406
|
+
...(d.from
|
|
2407
|
+
? { from: formatMoney(d.from) }
|
|
2408
|
+
: !d.why || d.why === 'sold-out'
|
|
2409
|
+
? { soldOut: true }
|
|
2410
|
+
: { noRoomsCode: d.why }),
|
|
2374
2411
|
...(d.chosen ? { chosen: true } : {}),
|
|
2375
2412
|
// The chosen date cannot match: `beatsBy` is a price strictly below it.
|
|
2376
2413
|
...(beatsBy !== undefined && d.from?.minor === beatsBy ? { best: true } : {}),
|
|
@@ -2665,6 +2702,19 @@ const currentTrip = async (deps, key, who, store = key) => {
|
|
|
2665
2702
|
const groupOf = deps.backend.supplierOf
|
|
2666
2703
|
? (id) => deps.backend.supplierOf?.(id)
|
|
2667
2704
|
: undefined;
|
|
2705
|
+
// Two hotels on the same nights - said, never refused (overlap.ts). In
|
|
2706
|
+
// both, in the same words.
|
|
2707
|
+
const overlaps = overlapsOf(parts.flatMap((p) => p.lines.flatMap((l) => l.kind === 'room' && l.stay && !l.hourly
|
|
2708
|
+
? [
|
|
2709
|
+
{
|
|
2710
|
+
hotelId: l.hotelId,
|
|
2711
|
+
name: hotelName(l.hotelName),
|
|
2712
|
+
checkIn: l.stay.checkIn,
|
|
2713
|
+
checkOut: l.stay.checkOut,
|
|
2714
|
+
},
|
|
2715
|
+
]
|
|
2716
|
+
: [])));
|
|
2717
|
+
const overlapping = overlaps.length ? { overlaps } : {};
|
|
2668
2718
|
if (parts.length === 1 && only) {
|
|
2669
2719
|
return {
|
|
2670
2720
|
structured: {
|
|
@@ -2673,10 +2723,12 @@ const currentTrip = async (deps, key, who, store = key) => {
|
|
|
2673
2723
|
...quoteForCard(only, groupOf),
|
|
2674
2724
|
...needs,
|
|
2675
2725
|
...stamp,
|
|
2726
|
+
...overlapping,
|
|
2676
2727
|
count: lines.length,
|
|
2677
2728
|
},
|
|
2678
2729
|
text: [
|
|
2679
2730
|
tripText(only, false),
|
|
2731
|
+
...overlaps.map((o) => o.said),
|
|
2680
2732
|
orphans.line,
|
|
2681
2733
|
deps.vocab.demo && lines.length ? noteFor(deps.vocab) : '',
|
|
2682
2734
|
]
|
|
@@ -2715,11 +2767,13 @@ const currentTrip = async (deps, key, who, store = key) => {
|
|
|
2715
2767
|
...needs,
|
|
2716
2768
|
...(shaped.some((p) => p.dropped) ? { dropped: shaped.flatMap((p) => p.dropped ?? []) } : {}),
|
|
2717
2769
|
...stamp,
|
|
2770
|
+
...overlapping,
|
|
2718
2771
|
count: lines.length,
|
|
2719
2772
|
},
|
|
2720
2773
|
text: [
|
|
2721
2774
|
`This trip is in ${parts.length} currencies, priced separately and not converted - each hotel is booked and paid in its own.`,
|
|
2722
2775
|
...parts.map((q) => tripText(q, false)),
|
|
2776
|
+
...overlaps.map((o) => o.said),
|
|
2723
2777
|
orphans.line,
|
|
2724
2778
|
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
2725
2779
|
]
|
|
@@ -2824,6 +2878,17 @@ dated = false) => {
|
|
|
2824
2878
|
* read better as hotels, grouped by city.
|
|
2825
2879
|
*/
|
|
2826
2880
|
export const CITY_PICKER_MIN = 4;
|
|
2881
|
+
/**
|
|
2882
|
+
* Up to how many hotels a whole deployment is a list rather than places to
|
|
2883
|
+
* choose from. Huazhu's production had two, in two cities, and "where to?"
|
|
2884
|
+
* over two city chips and "browse all 2" had the model list them all again -
|
|
2885
|
+
* two cards for one question (robin, 2026-10-30). Six is what the card shows
|
|
2886
|
+
* whole at every width, with no "Next" (its inline page is five rows on a
|
|
2887
|
+
* phone and lets two more in - see the card's `pageSize`): a place picker
|
|
2888
|
+
* over that many saves no one a tap. A group of nine in four cities still
|
|
2889
|
+
* chooses a city first.
|
|
2890
|
+
*/
|
|
2891
|
+
export const HOME_LIST_MAX = 6;
|
|
2827
2892
|
const suggestionsFor = async (deps, d) => {
|
|
2828
2893
|
const view = String(d.view ?? '');
|
|
2829
2894
|
const out = [];
|
|
@@ -3078,7 +3143,11 @@ const comparedInWords = (c) => {
|
|
|
3078
3143
|
: p.refundable
|
|
3079
3144
|
? 'no free cancellation'
|
|
3080
3145
|
: 'non-refundable',
|
|
3081
|
-
p.payment === 'prepaid'
|
|
3146
|
+
p.payment === 'prepaid'
|
|
3147
|
+
? p.notSoldHere
|
|
3148
|
+
? 'paid in advance - not booked here'
|
|
3149
|
+
: 'paid in advance'
|
|
3150
|
+
: '',
|
|
3082
3151
|
p.breakfast ? 'breakfast included' : 'no breakfast',
|
|
3083
3152
|
]
|
|
3084
3153
|
.filter(Boolean)
|
|
@@ -3493,7 +3562,10 @@ const putRoomLine = async (deps, t, ctx, key, store, args, detail) => {
|
|
|
3493
3562
|
let took = '';
|
|
3494
3563
|
const type = room ? undefined : roomTypeNamed(dated.rooms, roomRef);
|
|
3495
3564
|
if (type) {
|
|
3496
|
-
const
|
|
3565
|
+
const fitting = type.rooms.filter((r) => r.sleeps >= stay.guests && r.roomsLeft >= wanted);
|
|
3566
|
+
// A rate booked here before one only shown (`notSoldHere`), where the type has both.
|
|
3567
|
+
const sold = fitting.filter((r) => !r.notSoldHere);
|
|
3568
|
+
const fits = sold.length ? sold : fitting;
|
|
3497
3569
|
const [first, ...rest] = fits;
|
|
3498
3570
|
const cheapest = first
|
|
3499
3571
|
? rest.reduce((a, b) => (b.stayTotal.minor < a.stayTotal.minor ? b : a), first)
|
|
@@ -3535,6 +3607,45 @@ const putRoomLine = async (deps, t, ctx, key, store, args, detail) => {
|
|
|
3535
3607
|
text: `${type ? `${detail.hotel.name} has no ${type.name} that sleeps ${stay.guests} and has ${wanted === 1 ? 'a room' : `${wanted} rooms`} left for those nights.` : `${detail.hotel.name} has no room type by that name for those nights.`} It has: ${dated.rooms.map((r) => `${r.name} (${han && r.names?.zh ? `${r.names.zh}, ` : ''}\`${r.id}\`)`).join(', ')}. Call again with the id.`,
|
|
3536
3608
|
};
|
|
3537
3609
|
}
|
|
3610
|
+
// ⚠️ LISTED, NOT BOOKED HERE (robin, 2026-10-30): a rate paid in advance
|
|
3611
|
+
// where this deployment takes no payment goes in no trip and no pick. The
|
|
3612
|
+
// press on its Reserve - or the assistant asking for it - is answered with
|
|
3613
|
+
// what is true and what is left: nothing booked, nothing charged; the rates
|
|
3614
|
+
// here paid at the hotel, a press each; other dates; or the group itself.
|
|
3615
|
+
// Never a review that fails at Book, after the guest typed their name.
|
|
3616
|
+
if (room.notSoldHere) {
|
|
3617
|
+
const open = dated.rooms.filter((r) => !r.notSoldHere && r.sleeps >= stay.guests && r.roomsLeft >= wanted);
|
|
3618
|
+
const group = groupNameOf(deps, hotelId);
|
|
3619
|
+
const said = [
|
|
3620
|
+
`Nothing was booked or charged: ${room.name} is paid in advance, and a stay paid in advance cannot be booked here yet - this connector books rates paid at the hotel.`,
|
|
3621
|
+
open.length
|
|
3622
|
+
? `Paid at the hotel at ${detail.hotel.name} for those nights: ${open.map((r) => `${r.name} (\`${r.id}\`)`).join(', ')}.`
|
|
3623
|
+
: `No rate at ${detail.hotel.name} for those nights is paid at the hotel.`,
|
|
3624
|
+
`Other dates, or booking this rate with ${group} directly, are the other ways on.`,
|
|
3625
|
+
].join(' ');
|
|
3626
|
+
return {
|
|
3627
|
+
structured: {
|
|
3628
|
+
view: 'error',
|
|
3629
|
+
...common(deps, key),
|
|
3630
|
+
error: 'prepaid-not-here',
|
|
3631
|
+
room: { id: room.id, name: room.name, ...(room.names ? { names: room.names } : {}) },
|
|
3632
|
+
rooms: open.map((r) => ({
|
|
3633
|
+
id: r.id,
|
|
3634
|
+
name: r.name,
|
|
3635
|
+
...(r.names ? { names: r.names } : {}),
|
|
3636
|
+
})),
|
|
3637
|
+
hotel: {
|
|
3638
|
+
id: detail.hotel.id,
|
|
3639
|
+
name: detail.hotel.name,
|
|
3640
|
+
...(detail.hotel.names ? { names: detail.hotel.names } : {}),
|
|
3641
|
+
},
|
|
3642
|
+
stay,
|
|
3643
|
+
group,
|
|
3644
|
+
said,
|
|
3645
|
+
},
|
|
3646
|
+
text: said,
|
|
3647
|
+
};
|
|
3648
|
+
}
|
|
3538
3649
|
deps.trips.set(store, {
|
|
3539
3650
|
kind: 'room',
|
|
3540
3651
|
hotelId,
|
|
@@ -4091,17 +4202,33 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
4091
4202
|
const [sole] = hotels;
|
|
4092
4203
|
if (choosing && total === 1 && sole && !deps.backend.suppliers) {
|
|
4093
4204
|
const page = await answerTool(deps, t.getHotel, { hotel: sole.id, ...stayArgs }, ctx);
|
|
4205
|
+
// ⚠️ NOT "THE ONLY ONE HERE": read as "in this list", it had the
|
|
4206
|
+
// assistant ask which city to list the rest in (2026-10-15 smoke run).
|
|
4207
|
+
// And not "tell me another city": the second run said there was one
|
|
4208
|
+
// hotel, then offered to look in another city (2026-10-15).
|
|
4209
|
+
const inAll = `This connector has one ${branded(deps.vocab, deps.vocab.noun)} in all, ${sole.name}${sole.city ? ` in ${sole.city}` : ''}. There is no other hotel and no other city here - nothing else to search for or to offer.`;
|
|
4094
4210
|
return {
|
|
4095
4211
|
...page,
|
|
4096
|
-
|
|
4097
|
-
//
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
// hotel, then offered to look in another city (2026-10-15).
|
|
4101
|
-
text: `This connector has one ${branded(deps.vocab, deps.vocab.noun)} in all, ${sole.name}${sole.city ? ` in ${sole.city}` : ''}. There is no other hotel and no other city here - nothing else to search for or to offer. Its page:\n\n${page.text}`,
|
|
4212
|
+
// In both, in the same words: a host may hand its model this rather
|
|
4213
|
+
// than the text.
|
|
4214
|
+
structured: { ...page.structured, only: true, inAll },
|
|
4215
|
+
text: `${inAll} Its page:\n\n${page.text}`,
|
|
4102
4216
|
};
|
|
4103
4217
|
}
|
|
4104
|
-
|
|
4218
|
+
// ⚠️ A FEW HOTELS ARE NO CHOICE OF PLACES EITHER (robin, 2026-10-30):
|
|
4219
|
+
// Huazhu's production had two, in two cities, and its home was "where to?"
|
|
4220
|
+
// over two city chips and "browse all 2" - the model then listed them
|
|
4221
|
+
// again, a second card for the same question. Where the whole deployment
|
|
4222
|
+
// fits on one page - never a market, whose home is its groups - the home
|
|
4223
|
+
// is the list of all of them, marked `home` so the card keeps the home's
|
|
4224
|
+
// own ways in on it.
|
|
4225
|
+
const homeList = !!choosing &&
|
|
4226
|
+
!asked.city &&
|
|
4227
|
+
!deps.backend.suppliers &&
|
|
4228
|
+
total > 1 &&
|
|
4229
|
+
total <= HOME_LIST_MAX &&
|
|
4230
|
+
hotels.length === total;
|
|
4231
|
+
if (choosing && facets && !homeList) {
|
|
4105
4232
|
const countries = facets.countries.map((c) => ({
|
|
4106
4233
|
name: c.name,
|
|
4107
4234
|
count: c.count,
|
|
@@ -4151,7 +4278,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
4151
4278
|
...(ask.stay ? { stay: ask.stay } : {}),
|
|
4152
4279
|
...(today ? { today } : {}),
|
|
4153
4280
|
},
|
|
4154
|
-
text: `${total} ${branded(deps.vocab, n)} in ${facets.cities.length} cities across ${countries.length} countries, to choose from by region, country and city - ${regions.map((r) => `${r.name}: ${every(r.countries)}`).join('; ')}. A city or a country lists its ${n}. ${forPlans}`,
|
|
4281
|
+
text: `${total} ${branded(deps.vocab, n)} in ${facets.cities.length} cities across ${countries.length} ${countries.length === 1 ? 'country' : 'countries'}, to choose from by region, country and city - ${regions.map((r) => `${r.name}: ${every(r.countries)}`).join('; ')}. A city or a country lists its ${n}. ${forPlans}`,
|
|
4155
4282
|
};
|
|
4156
4283
|
}
|
|
4157
4284
|
if (hotels.length === 0) {
|
|
@@ -4286,10 +4413,25 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
4286
4413
|
: ''}.`
|
|
4287
4414
|
: '',
|
|
4288
4415
|
].filter(Boolean);
|
|
4416
|
+
// A price on the list that is not booked here (`notSoldHere`): said as on
|
|
4417
|
+
// the hotel's page, in both - see `notSoldHereWords`.
|
|
4418
|
+
const notSoldOn = hotels.find((h) => h.forStay?.notSoldHere);
|
|
4419
|
+
const notSold = notSoldOn ? notSoldHereWords(groupNameOf(deps, notSoldOn.id)) : '';
|
|
4420
|
+
if (notSold)
|
|
4421
|
+
priceNotes.push(notSold);
|
|
4422
|
+
// Every one by name and city, and that there are no others - or, told
|
|
4423
|
+
// only "2 hotels", the model asked for the list again to learn which.
|
|
4424
|
+
const inAll = homeList
|
|
4425
|
+
? `This connector has ${total} ${branded(deps.vocab, deps.vocab.nounPlural)} in all, and this answer lists every one: ${hotels
|
|
4426
|
+
.map((h) => `${hotelName(h.name)}${h.city ? ` in ${h.city}` : ''}`)
|
|
4427
|
+
.join('; ')}. There is no other ${deps.vocab.noun} and no other city here - nothing else to list, search for or offer.`
|
|
4428
|
+
: '';
|
|
4289
4429
|
return {
|
|
4290
4430
|
structured: {
|
|
4291
4431
|
view: 'hotels',
|
|
4292
4432
|
...common(deps, key),
|
|
4433
|
+
// The home, as a list - see `homeList`. The same sentence as the text.
|
|
4434
|
+
...(homeList ? { home: true, inAll } : {}),
|
|
4293
4435
|
hotels: forCard,
|
|
4294
4436
|
total,
|
|
4295
4437
|
shown: hotels.length,
|
|
@@ -4311,9 +4453,11 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
4311
4453
|
// In both, like a plan's: a host with no card may hand its model this
|
|
4312
4454
|
// rather than the text.
|
|
4313
4455
|
...(ctx.noCards ? { notDrawn: LIST_NOT_DRAWN } : {}),
|
|
4456
|
+
...(notSold ? { notSoldHere: notSold } : {}),
|
|
4314
4457
|
},
|
|
4315
4458
|
text: [
|
|
4316
|
-
|
|
4459
|
+
inAll ||
|
|
4460
|
+
`${total} ${branded(deps.vocab, total === 1 ? deps.vocab.noun : deps.vocab.nounPlural)}${where}.`,
|
|
4317
4461
|
// ⚠️ NO "BELOW", NO "TAP". This sentence is read out by a voice
|
|
4318
4462
|
// assistant as often as it is drawn next to a card, and a guest whose
|
|
4319
4463
|
// host has no card at all cannot tap the one it tells them to tap. The
|
|
@@ -4445,6 +4589,9 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
4445
4589
|
// card needs no call to the server.
|
|
4446
4590
|
const terms = roomTermsOf(args);
|
|
4447
4591
|
const soldOut = soldOutTypesOf(detail, enough, roomsWanted);
|
|
4592
|
+
const notSold = enough.some((r) => r.notSoldHere)
|
|
4593
|
+
? notSoldHereWords(groupNameOf(deps, h.id))
|
|
4594
|
+
: '';
|
|
4448
4595
|
const rooms = enough.map((r) => ({
|
|
4449
4596
|
...r,
|
|
4450
4597
|
facets: facetsOf(r),
|
|
@@ -4484,6 +4631,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
4484
4631
|
activeFilters: activeKeysOf(terms),
|
|
4485
4632
|
// Types with no room to book for these nights, said and not left out.
|
|
4486
4633
|
...(byTheHour || !soldOut.length ? {} : { soldOut }),
|
|
4634
|
+
...(notSold ? { notSoldHere: notSold } : {}),
|
|
4487
4635
|
...flex.structured,
|
|
4488
4636
|
},
|
|
4489
4637
|
text: [
|
|
@@ -4494,6 +4642,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
4494
4642
|
terms && flex.text
|
|
4495
4643
|
? `${flex.text} (That is the cheapest room of any kind, not only one with ${termWords(terms)}.)`
|
|
4496
4644
|
: flex.text,
|
|
4645
|
+
notSold,
|
|
4497
4646
|
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
4498
4647
|
]
|
|
4499
4648
|
.filter(Boolean)
|
|
@@ -5376,7 +5525,11 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5376
5525
|
// Each stay's rate says its terms where they bind - see `ItineraryItem.refundable`.
|
|
5377
5526
|
const termsOf = (x) => [
|
|
5378
5527
|
x.refundable === false ? 'non-refundable' : '',
|
|
5379
|
-
x.payment === 'prepaid'
|
|
5528
|
+
x.payment === 'prepaid'
|
|
5529
|
+
? x.notSoldHere
|
|
5530
|
+
? 'paid in advance - not booked here'
|
|
5531
|
+
: 'paid in advance'
|
|
5532
|
+
: '',
|
|
5380
5533
|
]
|
|
5381
5534
|
.filter(Boolean)
|
|
5382
5535
|
.join(', ');
|
|
@@ -5388,6 +5541,10 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5388
5541
|
// text said of a long stay. The model's part of the answer is the same on
|
|
5389
5542
|
// either side.
|
|
5390
5543
|
const nudges = stays.length ? await longStayNudges(deps, stayItems, plan.days) : [];
|
|
5544
|
+
// Two hotels on the same nights of the plan - said, never refused (overlap.ts).
|
|
5545
|
+
const overlaps = overlapsOf(stayItems.flatMap((x) => x.checkIn && x.checkOut
|
|
5546
|
+
? [{ hotelId: x.id, name: hotelName(x.name), checkIn: x.checkIn, checkOut: x.checkOut }]
|
|
5547
|
+
: []));
|
|
5391
5548
|
return {
|
|
5392
5549
|
structured: {
|
|
5393
5550
|
view: 'itinerary',
|
|
@@ -5395,6 +5552,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5395
5552
|
...plan,
|
|
5396
5553
|
gap,
|
|
5397
5554
|
...(nudges.length ? { nudges } : {}),
|
|
5555
|
+
...(overlaps.length ? { overlaps } : {}),
|
|
5398
5556
|
// In both, like the nudges: a host with no card may hand its model
|
|
5399
5557
|
// this rather than the text.
|
|
5400
5558
|
...(ctx.noCards ? { notDrawn: PLAN_NOT_DRAWN } : {}),
|
|
@@ -5409,6 +5567,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5409
5567
|
? [`Together, the stays not in the trip or booked yet: ${plan.staysTotal}.`]
|
|
5410
5568
|
: []),
|
|
5411
5569
|
...nudges,
|
|
5570
|
+
...overlaps.map((o) => o.said),
|
|
5412
5571
|
]
|
|
5413
5572
|
: []),
|
|
5414
5573
|
...plan.dropped.map((d) => `Left out: ${d.name} — ${d.why}`),
|