@curless/hotel-storefront 0.51.4 → 0.51.6
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/present.d.ts +8 -0
- package/dist/present.d.ts.map +1 -1
- package/dist/present.js +5 -0
- package/dist/present.js.map +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js +2 -2
- package/dist/surface.d.ts +7 -5
- package/dist/surface.d.ts.map +1 -1
- package/dist/surface.js +32 -13
- package/dist/surface.js.map +1 -1
- package/dist/tools.d.ts +9 -9
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +172 -54
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
package/dist/tools.js
CHANGED
|
@@ -167,7 +167,7 @@ const forCapabilities = (tools, n, v, caps) => {
|
|
|
167
167
|
if (t.name === n.viewTrip) {
|
|
168
168
|
out = {
|
|
169
169
|
...out,
|
|
170
|
-
description: 'The trip as it stands: every room line with its own total, the taxes and fees, and the all-in total, laid out day by day on the card.',
|
|
170
|
+
description: 'The trip as it stands: every room line with its own total, the taxes and fees, and the all-in total, laid out day by day (on the card, in an app that draws one).',
|
|
171
171
|
};
|
|
172
172
|
}
|
|
173
173
|
if (t.name === n.lookUp) {
|
|
@@ -362,7 +362,7 @@ const allTools = (n, v) => {
|
|
|
362
362
|
// description says it too, as what the card is, never as an order: the
|
|
363
363
|
// directory rule (tool-descriptions.test.ts) holds, and "first" is one of
|
|
364
364
|
// its words, hence "the other way round".
|
|
365
|
-
description: `${upFirst(catalogue(v))}, shown as a card
|
|
365
|
+
description: `${upFirst(catalogue(v))}, shown to a guest - as a card, in an app that draws one - to look through and pick one ${v.noun} to book. It holds no plan of days: a day-by-day plan (行程, 安排, an itinerary) is an answer of its own, laid out by the days, with a ${v.noun} for each part of the trip read up as text. Asked with no place - or a country of many cities - it answers with the places to choose from: regions, countries and cities, each with how many ${v.nounPlural} it has; \`all\` lists the ${v.nounPlural} themselves. \`city\` (a city or a country) or \`query\` answers with the ${v.nounPlural}: a picture, the city and area, a rating where the ${v.noun} publishes one, and a from-price, and a city usually holds several. With \`checkIn\` and \`checkOut\` every ${v.noun} is priced for those nights - its cheapest room that takes the party, \`rooms\` of it, for the whole stay with taxes - or marked as having no room for them. \`sort\` and \`maxNightly\` order and cap by that price, or by the from-price without dates, where the matches share one currency. \`brand\`, \`facilities\`, \`freeCancellation\`, \`breakfast\` and \`bed\` narrow the list, and the answer counts what each would leave. A long list comes a page at a time: the answer says how many more there are and the \`offset\` of the next page. Words or a place that match nothing are widened once, and the answer names what it widened to.${v.hints.search ? ` ${v.hints.search}` : ''}`,
|
|
366
366
|
annotations: reads(`Browse ${catalogue(v)}`, open),
|
|
367
367
|
inputSchema: {
|
|
368
368
|
type: 'object',
|
|
@@ -441,7 +441,7 @@ const allTools = (n, v) => {
|
|
|
441
441
|
},
|
|
442
442
|
{
|
|
443
443
|
name: n.getHotel,
|
|
444
|
-
description: `One ${v.noun} in full: description, amenities, address, and check-in and check-out times where the ${v.noun} publishes them. Given \`checkIn\` and \`checkOut\` it also returns the room types that can be booked for those nights, one line per type at its lowest rate: the nightly rate, the stay total including taxes and fees, whether it can be cancelled free and until when, and how many rates it has. \`rooms\` leaves out the types that have fewer than that many left. \`bed\`, \`breakfast\`, \`freeCancellation\`, \`window\`, \`payment\` and \`family\` keep only the rates that meet all of those given;
|
|
444
|
+
description: `One ${v.noun} in full: description, amenities, address, and check-in and check-out times where the ${v.noun} publishes them. Given \`checkIn\` and \`checkOut\` it also returns the room types that can be booked for those nights, one line per type at its lowest rate: the nightly rate, the stay total including taxes and fees, whether it can be cancelled free and until when, and how many rates it has. \`rooms\` leaves out the types that have fewer than that many left. \`bed\`, \`breakfast\`, \`freeCancellation\`, \`window\`, \`payment\` and \`family\` keep only the rates that meet all of those given; a card, in an app that draws one, still holds every room, with the given filters selected. Without dates it returns the ${v.noun} only, because a room has no price until the nights are known. The ${v.noun} is shown to the guest - as a card, in an app that draws one.${v.hints.hotel ? ` ${v.hints.hotel}` : ''}`,
|
|
445
445
|
annotations: reads(`View one ${catalogue(v, true)}`, open),
|
|
446
446
|
inputSchema: {
|
|
447
447
|
type: 'object',
|
|
@@ -521,7 +521,7 @@ const allTools = (n, v) => {
|
|
|
521
521
|
},
|
|
522
522
|
{
|
|
523
523
|
name: n.listExtras,
|
|
524
|
-
description: `What one ${v.noun} offers beside the room: airport transfers, dinners, guided experiences, spa and family add-ons, each with its price and whether it is charged per stay, per person or per night,
|
|
524
|
+
description: `What one ${v.noun} offers beside the room: airport transfers, dinners, guided experiences, spa and family add-ons, each with its price and whether it is charged per stay, per person or per night, shown to the guest - as a card, in an app that draws one.`,
|
|
525
525
|
annotations: reads(v.market ? `Add-ons on ${v.brand}` : `${v.brand} add-ons`, open),
|
|
526
526
|
inputSchema: {
|
|
527
527
|
type: 'object',
|
|
@@ -574,7 +574,7 @@ const allTools = (n, v) => {
|
|
|
574
574
|
},
|
|
575
575
|
{
|
|
576
576
|
name: n.viewTrip,
|
|
577
|
-
description: 'The trip as it stands: every room line and add-on with its own total, the taxes and fees, and the all-in total, laid out day by day on the card.',
|
|
577
|
+
description: 'The trip as it stands: every room line and add-on with its own total, the taxes and fees, and the all-in total, laid out day by day (on the card, in an app that draws one).',
|
|
578
578
|
annotations: reads(`View the ${v.brand} trip`, open),
|
|
579
579
|
inputSchema: { type: 'object', properties: {} },
|
|
580
580
|
},
|
|
@@ -771,7 +771,7 @@ const allTools = (n, v) => {
|
|
|
771
771
|
// prose; every figure on the card is read off the hotel's own record. A
|
|
772
772
|
// model that could send a price could one day send the wrong one, onto a
|
|
773
773
|
// card that looks official.
|
|
774
|
-
description: `Puts two to four ${v.nounPlural} side by side as one card. Each column's price, rate, cancellation terms, breakfast and rooms left come from the ${v.noun}'s own record; the arguments carry only the ids, one line on who each suits, and its pros and cons. With \`checkIn\`/\`checkOut\` the columns carry each ${v.noun}'s cheapest room for those nights; without them, a from-price.`,
|
|
774
|
+
description: `Puts two to four ${v.nounPlural} side by side - as one card, in an app that draws one. Each column's price, rate, cancellation terms, breakfast and rooms left come from the ${v.noun}'s own record; the arguments carry only the ids, one line on who each suits, and its pros and cons. With \`checkIn\`/\`checkOut\` the columns carry each ${v.noun}'s cheapest room for those nights; without them, a from-price.`,
|
|
775
775
|
annotations: reads(`Compare ${catalogue(v)}`, open),
|
|
776
776
|
inputSchema: {
|
|
777
777
|
type: 'object',
|
|
@@ -796,7 +796,7 @@ const allTools = (n, v) => {
|
|
|
796
796
|
},
|
|
797
797
|
},
|
|
798
798
|
recommended: { type: 'string', description: `The one ${v.noun} to mark as the pick` },
|
|
799
|
-
title: { type: 'string', description: 'A heading for the
|
|
799
|
+
title: { type: 'string', description: 'A heading for the plan' },
|
|
800
800
|
...stayProps,
|
|
801
801
|
},
|
|
802
802
|
required: ['entries'],
|
|
@@ -804,7 +804,7 @@ const allTools = (n, v) => {
|
|
|
804
804
|
},
|
|
805
805
|
{
|
|
806
806
|
name: n.itinerary,
|
|
807
|
-
description: `A day-by-day plan (行程)
|
|
807
|
+
description: `A day-by-day plan (行程) - one card, in an app that draws one - for a trip of several days, or for days a guest asked to have planned (a city with a span of dates and 行程 is one): the days are laid out by where they are spent, and each part of the trip has a ${v.noun} near those days, chosen from the ${v.nounPlural} read up as text. A week in a big city is usually two or three stays: its far-out sights fall on neighbouring days, and those nights have a ${v.noun} near them, with the reason in that day's \`why\` (Beijing's Summer Palace, Yuanmingyuan, Fragrant Hills and the Peking and Tsinghua campuses - 颐和园, 圆明园, 香山, 北大清华 - are days side by side in the north-west); a day out of town, such as the Great Wall, is a day of its own, there and back; one ${v.noun} for the whole week suits days that are all within about 45 minutes of it. Nothing here knows a sight's opening days: each day is laid out as it is given, with its date (many museums close on Mondays, the Palace Museum - 故宫 - among them). Each day carries its own label, an optional note, the moments of the day in order, and the ${v.nounPlural} and add-ons attached to it, with their details filled in from their own records - a ${v.noun} with its photograph and, given dates, the price of its nights. Each stay is marked with whether it is booked, in the trip or only in the plan, and the answer says how many of each. Nothing is added to the trip.`,
|
|
808
808
|
annotations: reads(v.market ? `Lay out an itinerary on ${v.brand}` : `Lay out a ${v.brand} itinerary`, open),
|
|
809
809
|
inputSchema: {
|
|
810
810
|
type: 'object',
|
|
@@ -819,7 +819,7 @@ const allTools = (n, v) => {
|
|
|
819
819
|
properties: {
|
|
820
820
|
label: {
|
|
821
821
|
type: 'string',
|
|
822
|
-
description: `e.g. "Arrive${places[0] ? ` in ${places[0]}` : ''}" -
|
|
822
|
+
description: `e.g. "Arrive${places[0] ? ` in ${places[0]}` : ''}" - each day's date is printed beside it, so leave the date out`,
|
|
823
823
|
},
|
|
824
824
|
note: {
|
|
825
825
|
type: 'string',
|
|
@@ -827,7 +827,7 @@ const allTools = (n, v) => {
|
|
|
827
827
|
},
|
|
828
828
|
why: {
|
|
829
829
|
type: 'string',
|
|
830
|
-
description: `Why the ${v.noun} named on this day suits the days of that stay - near what, good for what - one sentence, in your own words, with no lead-in such as "My pick:" - the
|
|
830
|
+
description: `Why the ${v.noun} named on this day suits the days of that stay - near what, good for what - one sentence, in your own words, with no lead-in such as "My pick:" - the plan has its own`,
|
|
831
831
|
},
|
|
832
832
|
items: {
|
|
833
833
|
type: 'array',
|
|
@@ -1976,6 +1976,24 @@ const langOf = (deps, _key) => deps.lang;
|
|
|
1976
1976
|
*/
|
|
1977
1977
|
const freeUntilText = (r) => r.freeCancellationLocal ??
|
|
1978
1978
|
`${(r.freeCancellationUntil ?? '').slice(0, 16).replace('T', ' ')} UTC`;
|
|
1979
|
+
/** Any Han character: a name already in Chinese needs none beside it. */
|
|
1980
|
+
const HAN = /\p{Script=Han}/u;
|
|
1981
|
+
/**
|
|
1982
|
+
* A name as the words to a client that draws no card say it: the plain name,
|
|
1983
|
+
* then the group's own Chinese name in () where it has one the plain name is
|
|
1984
|
+
* not.
|
|
1985
|
+
*
|
|
1986
|
+
* ⚠️ THE GROUP'S NAME, NOT THE MODEL'S (2026-11-02 audit, P1-1). The Chinese
|
|
1987
|
+
* names were only in the structure, and a model that is handed the text alone
|
|
1988
|
+
* told a Chinese guest 「浦东凯里酒店」 for 上海浦东嘉里大酒店 - a name nobody
|
|
1989
|
+
* can find the hotel by. A card host is told the plain name as ever: its card
|
|
1990
|
+
* says the Chinese.
|
|
1991
|
+
*/
|
|
1992
|
+
const withZh = (name, names, noCards) => {
|
|
1993
|
+
const plain = hotelName(name);
|
|
1994
|
+
const zh = noCards && names?.zh ? hotelName(names.zh) : '';
|
|
1995
|
+
return zh && zh !== plain && !HAN.test(plain) ? `${plain}(${zh})` : plain;
|
|
1996
|
+
};
|
|
1979
1997
|
/**
|
|
1980
1998
|
* What a signed-in member's prices are, said once at the top of a hotel's
|
|
1981
1999
|
* rooms - with the cheapest one's public and member figures, so "member rate"
|
|
@@ -2163,11 +2181,11 @@ const memberTripLines = (q) => {
|
|
|
2163
2181
|
: []),
|
|
2164
2182
|
].filter(Boolean);
|
|
2165
2183
|
};
|
|
2166
|
-
const tripText = (quote, demo) => {
|
|
2184
|
+
const tripText = (quote, demo, noCards) => {
|
|
2167
2185
|
if (quote.lines.length === 0)
|
|
2168
2186
|
return 'The trip is empty.';
|
|
2169
2187
|
const lines = quote.lines.map((l) => l.kind === 'room'
|
|
2170
|
-
? `- ${l.quantity ?? 1}× ${l.title} at ${l.hotelName}, ${l.hourly ? stayWords(l.stay, l.hourly) : `${stayWords(l.stay)} (${l.nights} ${l.nights === 1 ? 'night' : 'nights'})`} — ${formatMoney(l.total)}`
|
|
2188
|
+
? `- ${l.quantity ?? 1}× ${withZh(l.title, l.titleNames, noCards)} at ${withZh(l.hotelName, l.hotelNames, noCards)}, ${l.hourly ? stayWords(l.stay, l.hourly) : `${stayWords(l.stay)} (${l.nights} ${l.nights === 1 ? 'night' : 'nights'})`} — ${formatMoney(l.total)}`
|
|
2171
2189
|
: `- ${l.ordered ?? l.quantity}× ${l.title}${l.ordered !== undefined && l.quantity !== undefined && l.quantity !== l.ordered ? ` (${l.quantity} in all, over the stay)` : ''} — ${formatMoney(l.total)}`);
|
|
2172
2190
|
return [
|
|
2173
2191
|
...lines,
|
|
@@ -2390,9 +2408,17 @@ const offerCount = (rooms) => {
|
|
|
2390
2408
|
: `${rooms.length} rates on ${types} room ${types === 1 ? 'type' : 'types'}`;
|
|
2391
2409
|
};
|
|
2392
2410
|
/** What one room costs, as the text says it: nights, or the hours of a room by the hour. */
|
|
2393
|
-
const priceWords = (r) => r.hourly
|
|
2411
|
+
const priceWords = (r, noCards) => r.hourly
|
|
2394
2412
|
? `${formatMoney(r.stayTotal)} for ${r.hourly.hours} hours incl. taxes and fees, starting between ${r.hourly.earliestStart} and ${r.hourly.latestStart} today and out by ${r.hourly.latestEnd}`
|
|
2395
|
-
: `${formatMoney(r.nightly)}
|
|
2413
|
+
: `${formatMoney(r.nightly)} ${nightWords(noCards)}, ${formatMoney(r.stayTotal)} for the stay incl. taxes and fees`;
|
|
2414
|
+
/**
|
|
2415
|
+
* "a night" - and to a client that draws no card, "a night before taxes".
|
|
2416
|
+
*
|
|
2417
|
+
* ⚠️ THE NIGHTLY RATE HAS NO TAX IN IT, the stay total has (2026-11-02 audit,
|
|
2418
|
+
* P3-1): read side by side with "incl. taxes and fees", a model told the guest
|
|
2419
|
+
* "含税费 … 每晚 ¥1,668". The card says 每晚价不含税 under its prices (0.50.1).
|
|
2420
|
+
*/
|
|
2421
|
+
const nightWords = (noCards) => (noCards ? 'a night before taxes' : 'a night');
|
|
2396
2422
|
const LABEL_WORDS = {
|
|
2397
2423
|
'room-only': 'room only',
|
|
2398
2424
|
breakfast: 'breakfast',
|
|
@@ -2449,9 +2475,9 @@ const roomFactsWords = (r, rate = false) => {
|
|
|
2449
2475
|
return ` (${facts})`;
|
|
2450
2476
|
};
|
|
2451
2477
|
/** The cheapest price a rate quotes, as the text says it: "from" a night, or the hours of a block. */
|
|
2452
|
-
const fromWords = (r) => r.hourly
|
|
2478
|
+
const fromWords = (r, noCards) => r.hourly
|
|
2453
2479
|
? `${formatMoney(r.stayTotal)} for ${r.hourly.hours} hours incl. taxes and fees`
|
|
2454
|
-
: `${formatMoney(r.nightly)}
|
|
2480
|
+
: `${formatMoney(r.nightly)} ${nightWords(noCards)}, ${formatMoney(r.stayTotal)} for the stay incl. taxes and fees`;
|
|
2455
2481
|
/**
|
|
2456
2482
|
* A hotel's rooms for the nights, one line per ROOM TYPE.
|
|
2457
2483
|
*
|
|
@@ -2464,7 +2490,13 @@ const fromWords = (r) => r.hourly
|
|
|
2464
2490
|
*/
|
|
2465
2491
|
const roomTypeLines = (rooms, roomsWanted,
|
|
2466
2492
|
/** The terms already applied: what they say is true of every rate here, so it is not counted again. */
|
|
2467
|
-
terms
|
|
2493
|
+
terms,
|
|
2494
|
+
/**
|
|
2495
|
+
* To a client that draws no card, each line also says the id the next call
|
|
2496
|
+
* takes and the group's Chinese name (2026-11-02 audit, P1-1, P1-4): the
|
|
2497
|
+
* card is what books a room for a card host.
|
|
2498
|
+
*/
|
|
2499
|
+
noCards) => {
|
|
2468
2500
|
const forRooms = (r) => roomsWanted > 1
|
|
2469
2501
|
? ` (${formatMoney(multiplyMoney(r.stayTotal, roomsWanted))} for ${roomsWanted} rooms)`
|
|
2470
2502
|
: '';
|
|
@@ -2472,7 +2504,9 @@ terms) => {
|
|
|
2472
2504
|
const cheapest = type.rooms.reduce((a, b) => (b.stayTotal.minor < a.stayTotal.minor ? b : a));
|
|
2473
2505
|
const n = type.rooms.length;
|
|
2474
2506
|
if (n === 1)
|
|
2475
|
-
return
|
|
2507
|
+
return noCards
|
|
2508
|
+
? `- ${withZh(cheapest.name, cheapest.names, true)}${labelWords(cheapest)}${roomFactsWords(cheapest, true)}, room \`${cheapest.id}\` — ${priceWords(cheapest, true)}${forRooms(cheapest)}, ${cancelWords(cheapest)}`
|
|
2509
|
+
: `- ${cheapest.name}${labelWords(cheapest)}${roomFactsWords(cheapest, true)} — ${priceWords(cheapest)}${forRooms(cheapest)}, ${cancelWords(cheapest)}`;
|
|
2476
2510
|
const some = (k, all, part) => k === n ? all : k > 0 ? `${k} of ${n} ${part}` : '';
|
|
2477
2511
|
const also = [
|
|
2478
2512
|
terms?.breakfast
|
|
@@ -2482,7 +2516,9 @@ terms) => {
|
|
|
2482
2516
|
? ''
|
|
2483
2517
|
: some(type.rooms.filter((r) => facetsOf(r).freeCancellation).length, 'all are free to cancel', 'are free to cancel'),
|
|
2484
2518
|
].filter(Boolean);
|
|
2485
|
-
return
|
|
2519
|
+
return noCards
|
|
2520
|
+
? `- ${withZh(type.name, cheapest.roomType?.names, true)}${roomFactsWords(cheapest)}, room type \`${type.id}\` — ${n} rates from ${fromWords(cheapest, true)}${forRooms(cheapest)}. Cheapest: "${withZh(cheapest.name, cheapest.names, true)}", room \`${cheapest.id}\`${labelWords(cheapest)}, ${cancelWords(cheapest)}.${also.length ? ` ${also.join('; ')}.` : ''}`
|
|
2521
|
+
: `- ${type.name}${roomFactsWords(cheapest)} — ${n} rates from ${fromWords(cheapest)}${forRooms(cheapest)}. Cheapest: "${cheapest.name}"${labelWords(cheapest)}, ${cancelWords(cheapest)}.${also.length ? ` ${also.join('; ')}.` : ''}`;
|
|
2486
2522
|
});
|
|
2487
2523
|
};
|
|
2488
2524
|
/**
|
|
@@ -2495,13 +2531,16 @@ const roomsText = (hotelName,
|
|
|
2495
2531
|
/** "3 room types for 2026-11-04 to 2026-11-06, 2 guests" - what the rooms are, counted. */
|
|
2496
2532
|
counted,
|
|
2497
2533
|
/** "2026-11-04 to 2026-11-06" - when, for the sentence that says none fit. */
|
|
2498
|
-
when, enough, roomsWanted, terms) => {
|
|
2534
|
+
when, enough, roomsWanted, terms, noCards) => {
|
|
2499
2535
|
// A type with several rates is priced from its cheapest: say how to reach another.
|
|
2500
2536
|
const lines = (rs) => [
|
|
2501
|
-
...roomTypeLines(rs, roomsWanted, terms),
|
|
2537
|
+
...roomTypeLines(rs, roomsWanted, terms, noCards),
|
|
2502
2538
|
...(byRoomType(rs).some((t) => t.rooms.length > 1)
|
|
2503
2539
|
? [
|
|
2504
|
-
|
|
2540
|
+
noCards
|
|
2541
|
+
? // ⚠️ NO CARD LISTS THE OTHERS (2026-11-02 audit, P2-1).
|
|
2542
|
+
'A type with several rates is priced from its cheapest one; bed, breakfast, freeCancellation, window, payment and family narrow to the others - asked again with them, this answer lists those rates.'
|
|
2543
|
+
: 'A type with several rates is priced from its cheapest one; bed, breakfast, freeCancellation, window, payment and family narrow to the others (the card lists every rate).',
|
|
2505
2544
|
]
|
|
2506
2545
|
: []),
|
|
2507
2546
|
];
|
|
@@ -2563,14 +2602,36 @@ const paymentWords = (b) => {
|
|
|
2563
2602
|
return `${formatMoney(p.refundAmount)} of ${formatMoney(p.amount)} ${p.status === 'refunded' ? 'refunded' : 'going back'} - the rest was the fee`;
|
|
2564
2603
|
return ((p.method === 'unionpay' ? UNIONPAY_WORDS[p.status] : undefined) ?? PAYMENT_WORDS[p.status]);
|
|
2565
2604
|
};
|
|
2566
|
-
|
|
2605
|
+
/**
|
|
2606
|
+
* What a booking holds and what cancelling it costs, in a line - to a client
|
|
2607
|
+
* that draws no card, where the words are all the guest has of it.
|
|
2608
|
+
*
|
|
2609
|
+
* ⚠️ A CODE, A HOTEL AND A DATE WERE ALL IT SAID (2026-11-02 audit, P3-3): in
|
|
2610
|
+
* the same conversation a model fills in the room from what came before, but
|
|
2611
|
+
* a booking looked up later by its code had no room and no terms beside it.
|
|
2612
|
+
*/
|
|
2613
|
+
const heldRoomsWords = (b) => b.quote.lines
|
|
2614
|
+
.filter((l) => l.kind === 'room')
|
|
2615
|
+
.map((l) => `${(l.quantity ?? 1) > 1 ? `${l.quantity}× ` : ''}${withZh(l.title, l.titleNames, true)}${b.status === 'cancelled' || b.status === 'expired' || b.status === 'failed'
|
|
2616
|
+
? ''
|
|
2617
|
+
: l.freeCancellationUntil
|
|
2618
|
+
? `, free to cancel until ${freeUntilText(l)}`
|
|
2619
|
+
: l.refundable === false
|
|
2620
|
+
? ', non-refundable'
|
|
2621
|
+
: l.refundable
|
|
2622
|
+
? ', past its free cancellation - cancelling has a fee'
|
|
2623
|
+
: ''}`)
|
|
2624
|
+
.join('; ');
|
|
2625
|
+
const bookingLine = (b, noCards) => {
|
|
2567
2626
|
const kinds = b.requests?.kinds ?? [];
|
|
2627
|
+
const held = noCards ? heldRoomsWords(b) : '';
|
|
2568
2628
|
return [
|
|
2569
|
-
`${b.confirmationCode} · ${b.hotelName} · ${stayWords(b.stay, b.hourly)} · ${
|
|
2629
|
+
`${b.confirmationCode} · ${withZh(b.hotelName, b.hotelNames, noCards)} · ${stayWords(b.stay, b.hourly)} · ${
|
|
2570
2630
|
// ⚠️ SAID AS WHAT IT IS - see BookingStatus. Neither booked nor failed.
|
|
2571
2631
|
b.status === 'unconfirmed'
|
|
2572
2632
|
? 'unconfirmed: whether it was made is not known yet, and a person is confirming it'
|
|
2573
2633
|
: b.status}`,
|
|
2634
|
+
held ? ` · ${held}` : '',
|
|
2574
2635
|
kinds.length ? ` · asked for ${kinds.map(bare).join(', ')}` : '',
|
|
2575
2636
|
b.quote.coupon
|
|
2576
2637
|
? ` · coupon ${couponName(b.quote.coupon)} −${formatMoney(b.quote.coupon.off)}${b.couponReturned === true
|
|
@@ -2946,7 +3007,7 @@ const needsRoomIn = (t, lines, parts) => {
|
|
|
2946
3007
|
* `store`) the picked room's slot. `key` stays the conversation's trip key: it
|
|
2947
3008
|
* is the one `common` reads, so a header's trip count never counts a pick.
|
|
2948
3009
|
*/
|
|
2949
|
-
const currentTrip = async (deps, key, who, store = key, coupon) => {
|
|
3010
|
+
const currentTrip = async (deps, key, who, store = key, coupon, ctx) => {
|
|
2950
3011
|
await refreshMember(deps, key);
|
|
2951
3012
|
const member = deps.members.read(key);
|
|
2952
3013
|
const lines = deps.trips.read(store);
|
|
@@ -2977,6 +3038,11 @@ const currentTrip = async (deps, key, who, store = key, coupon) => {
|
|
|
2977
3038
|
]
|
|
2978
3039
|
: [])));
|
|
2979
3040
|
const overlapping = overlaps.length ? { overlaps } : {};
|
|
3041
|
+
// ⚠️ WHAT COMES NEXT, IN WORDS (2026-11-02 audit, P2-4): the card has a
|
|
3042
|
+
// Book button under the trip, and the words had nothing in its place.
|
|
3043
|
+
const nextStep = ctx?.noCards && parts.some((p) => p.lines.some((l) => l.kind === 'room'))
|
|
3044
|
+
? `Nothing in the trip is booked yet. To book it, ${deps.tools.requestBooking} takes the guest's own name and email - ask for them if the guest has not typed them in this conversation.`
|
|
3045
|
+
: '';
|
|
2980
3046
|
if (parts.length === 1 && only) {
|
|
2981
3047
|
return {
|
|
2982
3048
|
structured: {
|
|
@@ -2987,11 +3053,13 @@ const currentTrip = async (deps, key, who, store = key, coupon) => {
|
|
|
2987
3053
|
...stamp,
|
|
2988
3054
|
...overlapping,
|
|
2989
3055
|
count: lines.length,
|
|
3056
|
+
...(nextStep ? { nextStep } : {}),
|
|
2990
3057
|
},
|
|
2991
3058
|
text: [
|
|
2992
|
-
tripText(only, false),
|
|
3059
|
+
tripText(only, false, ctx?.noCards),
|
|
2993
3060
|
...overlaps.map((o) => o.said),
|
|
2994
3061
|
orphans.line,
|
|
3062
|
+
nextStep,
|
|
2995
3063
|
deps.vocab.demo && lines.length ? noteFor(deps.vocab) : '',
|
|
2996
3064
|
]
|
|
2997
3065
|
.filter(Boolean)
|
|
@@ -3031,12 +3099,14 @@ const currentTrip = async (deps, key, who, store = key, coupon) => {
|
|
|
3031
3099
|
...stamp,
|
|
3032
3100
|
...overlapping,
|
|
3033
3101
|
count: lines.length,
|
|
3102
|
+
...(nextStep ? { nextStep } : {}),
|
|
3034
3103
|
},
|
|
3035
3104
|
text: [
|
|
3036
3105
|
`This trip is in ${parts.length} currencies, priced separately and not converted - each hotel is booked and paid in its own.`,
|
|
3037
|
-
...parts.map((q) => tripText(q, false)),
|
|
3106
|
+
...parts.map((q) => tripText(q, false, ctx?.noCards)),
|
|
3038
3107
|
...overlaps.map((o) => o.said),
|
|
3039
3108
|
orphans.line,
|
|
3109
|
+
nextStep,
|
|
3040
3110
|
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
3041
3111
|
]
|
|
3042
3112
|
.filter(Boolean)
|
|
@@ -3364,20 +3434,42 @@ export const COMPARISON_NOT_DRAWN = "Nothing was drawn: this app shows no cards,
|
|
|
3364
3434
|
* the card - so a client that draws none had a count to tell the guest.
|
|
3365
3435
|
*/
|
|
3366
3436
|
export const LIST_NOT_DRAWN = "Nothing was drawn: this app shows no cards, so the guest has seen none of these hotels. Tell them in your reply - each one's name, area and price as given here:";
|
|
3367
|
-
/**
|
|
3437
|
+
/**
|
|
3438
|
+
* One hotel of a list as a line, for a client that shows no cards.
|
|
3439
|
+
*
|
|
3440
|
+
* ⚠️ WHETHER IT CAN BE BOOKED HERE IS ON THE LINE (2026-11-02 audit, P1-5): a
|
|
3441
|
+
* Huazhu hotel whose only rate tonight is paid in advance was listed as
|
|
3442
|
+
* "¥783.02 for the stay", with the sentence that such rates are not booked
|
|
3443
|
+
* here said once at the top - a model reading the line could tell the guest it
|
|
3444
|
+
* can be booked. Each priced line says how its rate is paid and what it costs
|
|
3445
|
+
* to cancel, as the card's tile does.
|
|
3446
|
+
*/
|
|
3368
3447
|
const listedInWords = (h) => {
|
|
3369
3448
|
const where = [h.area, h.city]
|
|
3370
3449
|
.filter((x) => !!x)
|
|
3371
3450
|
.map((x) => hotelName(x))
|
|
3372
3451
|
.join(', ');
|
|
3373
|
-
const
|
|
3374
|
-
|
|
3452
|
+
const f = h.forStay;
|
|
3453
|
+
const terms = f
|
|
3454
|
+
? [
|
|
3455
|
+
f.refundable === false ? 'non-refundable' : f.freeCancellation ? 'free to cancel' : '',
|
|
3456
|
+
f.payment === 'prepaid'
|
|
3457
|
+
? f.notSoldHere
|
|
3458
|
+
? 'paid in advance - not booked here'
|
|
3459
|
+
: 'paid in advance'
|
|
3460
|
+
: f.payment === 'at-hotel'
|
|
3461
|
+
? 'paid at the hotel'
|
|
3462
|
+
: '',
|
|
3463
|
+
].filter(Boolean)
|
|
3464
|
+
: [];
|
|
3465
|
+
const price = f
|
|
3466
|
+
? `${formatMoney(f.total)} for the stay incl. taxes and fees, ${withZh(f.room, f.roomNames, true)}${f.rooms > 1 ? ` × ${f.rooms}` : ''}${terms.length ? ` [${terms.join(', ')}]` : ''}`
|
|
3375
3467
|
: h.noRoomsForStay
|
|
3376
3468
|
? 'no room for those nights'
|
|
3377
3469
|
: h.fromNightly
|
|
3378
|
-
? `from ${formatMoney(h.fromNightly)}
|
|
3470
|
+
? `from ${formatMoney(h.fromNightly)} ${nightWords(true)}`
|
|
3379
3471
|
: '';
|
|
3380
|
-
return `- ${
|
|
3472
|
+
return `- ${withZh(h.name, h.names?.name, true)} (\`${h.id}\`)${where ? ` · ${where}` : ''}${price ? `: ${price}` : ''}`;
|
|
3381
3473
|
};
|
|
3382
3474
|
/**
|
|
3383
3475
|
* One column of a comparison as a line, for a client that shows no cards:
|
|
@@ -3399,7 +3491,7 @@ const comparedInWords = (c) => {
|
|
|
3399
3491
|
const p = c.price;
|
|
3400
3492
|
const priced = p
|
|
3401
3493
|
? [
|
|
3402
|
-
`${
|
|
3494
|
+
`${withZh(p.room, p.roomNames, true)} at ${p.nightly} ${nightWords(true)}, ${p.stayTotal} for the stay incl. taxes and fees`,
|
|
3403
3495
|
p.freeCancellationUntil
|
|
3404
3496
|
? `free to cancel until ${freeUntilText(p)}`
|
|
3405
3497
|
: p.refundable
|
|
@@ -3417,7 +3509,7 @@ const comparedInWords = (c) => {
|
|
|
3417
3509
|
: c.from
|
|
3418
3510
|
? `from ${c.from} a night; rooms are priced once there are dates`
|
|
3419
3511
|
: 'not priced without dates';
|
|
3420
|
-
return `- ${
|
|
3512
|
+
return `- ${withZh(h.name, h.names?.name, true)} (\`${h.id}\`)${about ? ` · ${about}` : ''}: ${priced}`;
|
|
3421
3513
|
};
|
|
3422
3514
|
/** An area's names as a day might say them: "Haidian", "海淀区" and "海淀". */
|
|
3423
3515
|
const areaWords = (h) => {
|
|
@@ -3709,7 +3801,7 @@ const putRoomLine = async (deps, t, ctx, key, store, args, detail) => {
|
|
|
3709
3801
|
: !!asked && l.stay.checkIn === asked.checkIn && l.stay.checkOut === asked.checkOut));
|
|
3710
3802
|
if (held) {
|
|
3711
3803
|
deps.trips.set(store, unset(held));
|
|
3712
|
-
return currentTrip(deps, key, who, store, couponOf(deps, args));
|
|
3804
|
+
return currentTrip(deps, key, who, store, couponOf(deps, args), ctx);
|
|
3713
3805
|
}
|
|
3714
3806
|
}
|
|
3715
3807
|
const today = (startTime || !asStay(args)
|
|
@@ -3769,7 +3861,7 @@ const putRoomLine = async (deps, t, ctx, key, store, args, detail) => {
|
|
|
3769
3861
|
hours: hourlyRoom.hourly.hours,
|
|
3770
3862
|
},
|
|
3771
3863
|
});
|
|
3772
|
-
return currentTrip(deps, key, who, store, couponOf(deps, args));
|
|
3864
|
+
return currentTrip(deps, key, who, store, couponOf(deps, args), ctx);
|
|
3773
3865
|
}
|
|
3774
3866
|
const stay = asStay(args);
|
|
3775
3867
|
// ⚠️ NO DATES, NO LINE. Guessing "tonight" or "this weekend" here is how a
|
|
@@ -3915,7 +4007,7 @@ const putRoomLine = async (deps, t, ctx, key, store, args, detail) => {
|
|
|
3915
4007
|
stay,
|
|
3916
4008
|
rooms: clampCount(args.rooms ?? 1, MAX_ROOMS_PER_LINE),
|
|
3917
4009
|
});
|
|
3918
|
-
const now = await currentTrip(deps, key, identity(deps, ctx, key), store, couponOf(deps, args));
|
|
4010
|
+
const now = await currentTrip(deps, key, identity(deps, ctx, key), store, couponOf(deps, args), ctx);
|
|
3919
4011
|
// Which rate was taken, in the structure too and in the same words - see
|
|
3920
4012
|
// the itinerary's `nudges` (2026-10-27 audit, F11).
|
|
3921
4013
|
return took
|
|
@@ -4209,7 +4301,8 @@ const bookTripLines = async (deps, t, ctx, key, store, args, nothingThere) => {
|
|
|
4209
4301
|
? `${booked.length} bookings, one for each stay - each hotel keeps its own nights under its own code, and each is ${holderOf(deps, first.booking.supplier).caps.changes ? 'changed or cancelled' : 'cancelled'} on its own.`
|
|
4210
4302
|
: '',
|
|
4211
4303
|
...booked.flatMap(({ booking, asked }) => [
|
|
4212
|
-
`**${confirming(deps, booking) ? 'Being confirmed' : booking.confirmationCode}** · ${booking.hotelName} · ${stayWords(booking.stay, booking.hourly)}`,
|
|
4304
|
+
`**${confirming(deps, booking) ? 'Being confirmed' : booking.confirmationCode}** · ${withZh(booking.hotelName, booking.hotelNames, ctx.noCards)} · ${stayWords(booking.stay, booking.hourly)}`,
|
|
4305
|
+
ctx.noCards ? heldRoomsWords(booking) : '',
|
|
4213
4306
|
bookedLine(deps, booking),
|
|
4214
4307
|
...asked,
|
|
4215
4308
|
]),
|
|
@@ -4509,7 +4602,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
4509
4602
|
};
|
|
4510
4603
|
const priced = await answerTool(deps, t.listHotels, { ...args, ...tonight }, ctx);
|
|
4511
4604
|
// In both, in the same words: the model reads the structure.
|
|
4512
|
-
const stayDefault = `No dates were asked for, so this opens on tonight by default: ${tonight.checkIn} to ${tonight.checkOut} (one night, on the hotels' own date), ${tonight.guests} ${tonight.guests === 1 ? 'guest' : 'guests'}, one room - every price here is for that night. Say so: the prices shown are tonight's, and the guest can change the dates on the card or name others. Do not ask for dates before showing them.`;
|
|
4605
|
+
const stayDefault = `No dates were asked for, so this opens on tonight by default: ${tonight.checkIn} to ${tonight.checkOut} (one night, on the hotels' own date), ${tonight.guests} ${tonight.guests === 1 ? 'guest' : 'guests'}, one room - every price here is for that night. Say so: the prices shown are tonight's, and the guest can ${ctx.noCards ? 'name other dates' : 'change the dates on the card or name others'}. Do not ask for dates before showing them.`;
|
|
4513
4606
|
// Right after what is here - "every one, and no others" stays the first thing said.
|
|
4514
4607
|
const inAll = String(priced.structured.inAll ?? '');
|
|
4515
4608
|
return {
|
|
@@ -4570,6 +4663,14 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
4570
4663
|
// "the most are in" three of forty cities left the other thirty-seven
|
|
4571
4664
|
// for a guest to guess at.
|
|
4572
4665
|
const every = (xs) => xs.map((x) => `${x.name} (${x.count})`).join(', ');
|
|
4666
|
+
// ⚠️ AND TO A CLIENT THAT DRAWS NO CARD, EVERY CITY (2026-11-02 audit,
|
|
4667
|
+
// P3-2): told to name the places, a model had "China (54)" and nothing
|
|
4668
|
+
// to name. A country of one city named like it says no more.
|
|
4669
|
+
const everyWithCities = (xs) => xs
|
|
4670
|
+
.map((c) => c.cities.length > 1 || (c.cities[0] && c.cities[0].name !== c.name)
|
|
4671
|
+
? `${c.name} (${c.count}: ${c.cities.map((x) => `${x.name} ${x.count}`).join(', ')})`
|
|
4672
|
+
: `${c.name} (${c.count})`)
|
|
4673
|
+
.join(', ');
|
|
4573
4674
|
const n = deps.vocab.nounPlural;
|
|
4574
4675
|
if (namedCountry) {
|
|
4575
4676
|
const [only] = countries;
|
|
@@ -4607,7 +4708,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
4607
4708
|
...(ask.stay ? { stay: ask.stay } : {}),
|
|
4608
4709
|
...(today ? { today } : {}),
|
|
4609
4710
|
},
|
|
4610
|
-
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}`,
|
|
4711
|
+
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}: ${ctx.noCards ? everyWithCities(r.countries) : every(r.countries)}`).join('; ')}. A city or a country lists its ${n}. ${forPlans}`,
|
|
4611
4712
|
};
|
|
4612
4713
|
}
|
|
4613
4714
|
if (hotels.length === 0) {
|
|
@@ -4877,7 +4978,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
4877
4978
|
activeFilters: activeKeysOf(roomTermsOf(args)),
|
|
4878
4979
|
},
|
|
4879
4980
|
text: [
|
|
4880
|
-
`**${h.name}** · ${[h.area, h.city].filter(Boolean).join(', ')}`,
|
|
4981
|
+
`**${ctx.noCards ? withZh(h.name, h.names?.name, true) : h.name}** · ${[h.area, h.city].filter(Boolean).join(', ')}`,
|
|
4881
4982
|
description.fenced,
|
|
4882
4983
|
'Rooms and prices need the dates - which nights is the guest staying?',
|
|
4883
4984
|
]
|
|
@@ -4990,9 +5091,9 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
4990
5091
|
},
|
|
4991
5092
|
text: [
|
|
4992
5093
|
memberRates?.said ?? '',
|
|
4993
|
-
...roomsText(h.name, byTheHour
|
|
5094
|
+
...roomsText(ctx.noCards ? withZh(h.name, h.names?.name, true) : h.name, byTheHour
|
|
4994
5095
|
? (rs) => `${rs.length} ${rs.length === 1 ? 'room' : 'rooms'} by the hour for today, ${stay.checkIn}, ${stay.guests} guests`
|
|
4995
|
-
: (rs) => `${offerCount(rs)} for ${stay.checkIn} to ${stay.checkOut}, ${stay.guests} guests${roomsWanted > 1 ? `, ${roomsWanted} rooms` : ''}`, byTheHour ? `today, ${stay.checkIn}` : `${stay.checkIn} to ${stay.checkOut}`, enough, roomsWanted, terms),
|
|
5096
|
+
: (rs) => `${offerCount(rs)} for ${stay.checkIn} to ${stay.checkOut}, ${stay.guests} guests${roomsWanted > 1 ? `, ${roomsWanted} rooms` : ''}`, byTheHour ? `today, ${stay.checkIn}` : `${stay.checkIn} to ${stay.checkOut}`, enough, roomsWanted, terms, ctx.noCards),
|
|
4996
5097
|
// The other arrivals are priced on the cheapest room of any kind.
|
|
4997
5098
|
terms && flex.text
|
|
4998
5099
|
? `${flex.text} (That is the cheapest room of any kind, not only one with ${termWords(terms)}.)`
|
|
@@ -5079,7 +5180,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5079
5180
|
const extras = await deps.backend.extrasFor(h.id, stay);
|
|
5080
5181
|
const rooms = (detail.rooms ?? []).filter((r) => r.roomsLeft >= roomsWanted);
|
|
5081
5182
|
blocks.push([
|
|
5082
|
-
`### ${
|
|
5183
|
+
`### ${withZh(h.name, h.names?.name, ctx.noCards)} · id \`${h.id}\``,
|
|
5083
5184
|
[
|
|
5084
5185
|
[h.area, h.city, h.country].filter(Boolean).join(', '),
|
|
5085
5186
|
h.brand,
|
|
@@ -5090,12 +5191,19 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5090
5191
|
about.fenced,
|
|
5091
5192
|
!stay
|
|
5092
5193
|
? h.fromNightly
|
|
5093
|
-
? `From ${formatMoney(h.fromNightly)}
|
|
5194
|
+
? `From ${formatMoney(h.fromNightly)} ${nightWords(ctx.noCards)}; rooms are priced once there are dates.`
|
|
5094
5195
|
: ''
|
|
5095
5196
|
: rooms.length
|
|
5096
5197
|
? [
|
|
5097
5198
|
`Rooms for ${stay.checkIn} to ${stay.checkOut}, ${stay.guests} guests${roomsWanted > 1 ? `, ${roomsWanted} rooms` : ''}:`,
|
|
5098
|
-
|
|
5199
|
+
// ⚠️ ONE LINE A ROOM TYPE, WITH ITS ID, to a client that
|
|
5200
|
+
// draws no card (2026-11-02 audit, P1-4, P3-4): every rate of
|
|
5201
|
+
// four Atour hotels was 16,213 characters, and not one room
|
|
5202
|
+
// id - which is what the trip takes. As the hotel's own
|
|
5203
|
+
// answer says them.
|
|
5204
|
+
...(ctx.noCards
|
|
5205
|
+
? roomTypeLines(rooms, roomsWanted, undefined, true)
|
|
5206
|
+
: rooms.map((r) => `- ${r.name} · sleeps ${r.sleeps}${r.breakfast ? ' · breakfast' : ''} · ${formatMoney(r.stayTotal)} for the stay incl. taxes and fees${roomsWanted > 1 ? ` (${formatMoney(multiplyMoney(r.stayTotal, roomsWanted))} for ${roomsWanted} rooms)` : ''}${r.freeCancellationUntil ? `, free to cancel until ${freeUntilText(r)}` : r.refundable ? '' : ', non-refundable'}`)),
|
|
5099
5207
|
].join('\n')
|
|
5100
5208
|
: (detail.noRooms ?? `No room type here has ${roomsWanted} left for those nights.`),
|
|
5101
5209
|
extras.length
|
|
@@ -5127,10 +5235,14 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5127
5235
|
// "Draw the answer now as ONE card", an instruction in a tool result.
|
|
5128
5236
|
// The rule itself lives in the server instructions; what stays here is
|
|
5129
5237
|
// what each next call would draw, which is what kept it to one card.
|
|
5130
|
-
`These ids are what ${t.itinerary} (a plan or days - 行程, recommended or arranged) and ${t.compare} (which ${deps.vocab.noun} to stay at) take; each of those draws one card. ${listAnd(
|
|
5238
|
+
`These ids are what ${t.itinerary} (a plan or days - 行程, recommended or arranged) and ${t.compare} (which ${deps.vocab.noun} to stay at) take${ctx.noCards ? '' : '; each of those draws one card'}. ${listAnd(
|
|
5131
5239
|
// Only tools this deployment offers: a name it withholds is one the
|
|
5132
5240
|
// model would try (2026-10-05 audit, P2 - Huazhu has no add-ons).
|
|
5133
|
-
[t.listHotels, t.getHotel, t.listExtras].filter((n) => !deps.offered || deps.offered.has(n)))}
|
|
5241
|
+
[t.listHotels, t.getHotel, t.listExtras].filter((n) => !deps.offered || deps.offered.has(n)))} ${
|
|
5242
|
+
// ⚠️ NOTHING IS DRAWN WHERE NO CARD IS (2026-11-02 audit, P2-1).
|
|
5243
|
+
ctx.noCards
|
|
5244
|
+
? 'answer with what the guest is told, for a guest who asked to see a list, a hotel or its add-ons.'
|
|
5245
|
+
: 'would each draw a card of their own.'}`,
|
|
5134
5246
|
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
5135
5247
|
]
|
|
5136
5248
|
.filter(Boolean)
|
|
@@ -5214,7 +5326,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5214
5326
|
},
|
|
5215
5327
|
text: extras.length
|
|
5216
5328
|
? extras
|
|
5217
|
-
.map((e) => `- ${e.name} — ${formatMoney(e.price)} ${unitWords(e.per)}${held[e.id] ? ` (${booked?.confirmationCode} holds ${held[e.id]})` : ''}`)
|
|
5329
|
+
.map((e) => `- ${e.name}${ctx.noCards ? ` (\`${e.id}\`)` : ''} — ${formatMoney(e.price)} ${unitWords(e.per)}${held[e.id] ? ` (${booked?.confirmationCode} holds ${held[e.id]})` : ''}`)
|
|
5218
5330
|
.concat(deps.vocab.demo ? [noteFor(deps.vocab)] : [])
|
|
5219
5331
|
.join('\n')
|
|
5220
5332
|
: `${detail.hotel.name} has no add-ons listed here.`,
|
|
@@ -5251,7 +5363,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5251
5363
|
...(isDay(str(args.day)) ? { day: str(args.day) } : {}),
|
|
5252
5364
|
};
|
|
5253
5365
|
deps.trips.set(key, line);
|
|
5254
|
-
return currentTrip(deps, key, identity(deps, ctx, key));
|
|
5366
|
+
return currentTrip(deps, key, identity(deps, ctx, key), key, undefined, ctx);
|
|
5255
5367
|
}
|
|
5256
5368
|
return putRoomLine(deps, t, ctx, key, key, args, detail);
|
|
5257
5369
|
}
|
|
@@ -5273,7 +5385,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5273
5385
|
return direct(await putRoomLine(deps, t, ctx, key, slot, args, found.detail));
|
|
5274
5386
|
}
|
|
5275
5387
|
if (name === t.viewTrip)
|
|
5276
|
-
return currentTrip(deps, key, identity(deps, ctx, key), key, couponOf(deps, args));
|
|
5388
|
+
return currentTrip(deps, key, identity(deps, ctx, key), key, couponOf(deps, args), ctx);
|
|
5277
5389
|
if (name === t.clearTrip) {
|
|
5278
5390
|
deps.trips.clear(key);
|
|
5279
5391
|
return {
|
|
@@ -5327,7 +5439,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5327
5439
|
text: [
|
|
5328
5440
|
missed ? missLine : '',
|
|
5329
5441
|
bookings.length
|
|
5330
|
-
? bookings.map(bookingLine).join('\n')
|
|
5442
|
+
? bookings.map((b) => bookingLine(b, ctx.noCards)).join('\n')
|
|
5331
5443
|
: code
|
|
5332
5444
|
? ''
|
|
5333
5445
|
: 'No bookings in this conversation. A booking made elsewhere is found with its confirmation code and the email it was made with.',
|
|
@@ -5890,8 +6002,14 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5890
6002
|
.flatMap((day) => day.items)
|
|
5891
6003
|
.filter((x) => x.kind === 'hotel' && (x.night ?? 1) === 1 && x.checkIn && x.roomId);
|
|
5892
6004
|
// Each stay's rate says its terms where they bind - see `ItineraryItem.refundable`.
|
|
6005
|
+
// With no card, until when it is free to cancel too: the reply is told to
|
|
6006
|
+
// give each stay's terms (2026-11-02 audit, P2-3).
|
|
5893
6007
|
const termsOf = (x) => [
|
|
5894
|
-
x.refundable === false
|
|
6008
|
+
x.refundable === false
|
|
6009
|
+
? 'non-refundable'
|
|
6010
|
+
: ctx.noCards && x.freeCancellationUntil
|
|
6011
|
+
? `free to cancel until ${freeUntilText(x)}`
|
|
6012
|
+
: '',
|
|
5895
6013
|
x.payment === 'prepaid'
|
|
5896
6014
|
? x.notSoldHere
|
|
5897
6015
|
? 'paid in advance - not booked here'
|
|
@@ -5902,7 +6020,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5902
6020
|
.join(', ');
|
|
5903
6021
|
// With no card, each stay's own price too: the card shows it beside the
|
|
5904
6022
|
// stay, and the reply is now the only place the guest reads it.
|
|
5905
|
-
const stays = stayItems.map((x, i) => `Stay ${i + 1}: ${
|
|
6023
|
+
const stays = stayItems.map((x, i) => `Stay ${i + 1}: ${withZh(x.name, x.names?.name, ctx.noCards)} (\`${x.id}\`), room \`${x.roomId}\`${x.roomName ? ` (${withZh(x.roomName, x.roomNames, ctx.noCards)}${termsOf(x) ? `; ${termsOf(x)}` : ''})` : termsOf(x) ? ` (${termsOf(x)})` : ''}, ${x.checkIn} to ${x.checkOut}${x.nights ? ` (${x.nights} ${x.nights === 1 ? 'night' : 'nights'})` : ''}${ctx.noCards && x.stayTotal ? `, ${x.stayTotal} for the stay incl. taxes and fees` : ''}${x.booked ? ` - booked, ${x.booked}` : x.inTrip ? ' - in the trip' : ''}`);
|
|
5906
6024
|
// ⚠️ IN BOTH, IN THE SAME WORDS (2026-10-27 audit, F11): a host that hands
|
|
5907
6025
|
// the model `structuredContent` rather than the text never saw what the
|
|
5908
6026
|
// text said of a long stay. The model's part of the answer is the same on
|