@v-office/website-chat 1.1.3 → 1.3.0

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.
@@ -1,1247 +0,0 @@
1
- import { i as SESSION_TTL_S, r as MAX_MESSAGE_CHARS } from "./protocol-BuNqZAMt.js";
2
- import { a as House, c as TopicIcon, d as saveConversation, i as Cross, l as clearConversation, n as Check, o as Restart, p as sendMessage, r as Chevron, t as Arrow, u as loadConversation } from "./icons-DIqOmKoe.js";
3
- import { r as chatCatalog, u as statePath } from "./catalog-y3xQ2jXW.js";
4
- import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
5
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
- import { JSONUIProvider, Renderer, defineRegistry, useStateValue } from "@json-render/react";
7
- import { DEFAULT_MAP_STYLE_URL, partyPairs } from "@v-office/website-headless";
8
- //#region src/links.ts
9
- const withQuery = (path, pairs) => `${path}${path.includes("?") ? "&" : "?"}${new URLSearchParams(pairs)}`;
10
- /** The stay's dates, party and chosen rate, as the rental page and the checkout both read them. */
11
- const stayPairs = (stay) => [
12
- ["start", stay.start],
13
- ["end", stay.end],
14
- ...partyPairs(stay.party, "pets").map(([key, value]) => [key, value]),
15
- ...stay.policyId ? [["policy", stay.policyId]] : []
16
- ];
17
- /** See {@link StayHref}. */
18
- const stayHref = (page, stay, searchPath) => {
19
- const path = page ?? searchPath;
20
- return stay ? withQuery(path, stayPairs(stay)) : path;
21
- };
22
- /** See {@link BookingHref}. */
23
- const bookingHref = (bookingPage, stay) => withQuery(bookingPage, [...stayPairs(stay), ...(stay.services ?? []).filter((service) => service.count > 0).map((service) => ["service", `${service.id}:${service.count}`])]);
24
- //#endregion
25
- //#region src/map.tsx
26
- /** MapLibre and its worker, loaded on the first tap. MapLibre finds its worker beside its own module
27
- * by default; bundled, the module is a hashed chunk with nothing beside it, so `?worker&url` has the
28
- * site's bundler emit the worker as one self-contained file (MapLibre's own Vite setup), and
29
- * `setWorkerUrl` points MapLibre at it before the first map reads it. */
30
- const loadMapLibre = () => Promise.all([import("maplibre-gl"), import("maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url")]).then(([library, worker]) => {
31
- library.setWorkerUrl(worker.default);
32
- return library;
33
- });
34
- /** What the style's sources ask to be credited, with their links, as MapLibre's attribution control
35
- * would show it (OpenStreetMap's to its copyright page). */
36
- const Attribution = () => /* @__PURE__ */ jsxs("p", {
37
- className: "map-attribution",
38
- children: [
39
- /* @__PURE__ */ jsx("a", {
40
- href: "https://openfreemap.org",
41
- target: "_blank",
42
- rel: "noopener noreferrer",
43
- children: "OpenFreeMap"
44
- }),
45
- " ",
46
- /* @__PURE__ */ jsx("a", {
47
- href: "https://www.openmaptiles.org/",
48
- target: "_blank",
49
- rel: "noopener noreferrer",
50
- children: "© OpenMapTiles"
51
- }),
52
- " ",
53
- "Data from",
54
- " ",
55
- /* @__PURE__ */ jsx("a", {
56
- href: "https://www.openstreetmap.org/copyright",
57
- target: "_blank",
58
- rel: "noopener noreferrer",
59
- children: "OpenStreetMap"
60
- })
61
- ]
62
- });
63
- const MapCard = ({ rental, label, notice, load, children }) => {
64
- const [shown, setShown] = useState(false);
65
- const box = useRef(null);
66
- const location = rental.location;
67
- useEffect(() => {
68
- const element = box.current;
69
- if (!shown || !element || !location) return;
70
- let removed = false;
71
- let map;
72
- loadMapLibre().then(({ Map, Marker }) => {
73
- if (removed) return;
74
- const center = [location.lng, location.lat];
75
- const created = new Map({
76
- container: element,
77
- style: DEFAULT_MAP_STYLE_URL,
78
- center,
79
- zoom: 13,
80
- attributionControl: false,
81
- scrollZoom: false
82
- });
83
- const pin = document.createElement("span");
84
- pin.className = "map-pin";
85
- new Marker({ element: pin }).setLngLat(center).addTo(created);
86
- map = created;
87
- }).catch(() => {
88
- if (!removed) setShown(false);
89
- });
90
- return () => {
91
- removed = true;
92
- map?.remove();
93
- };
94
- }, [shown, location]);
95
- if (!location) return null;
96
- return /* @__PURE__ */ jsxs("article", {
97
- className: "card map-card",
98
- children: [shown ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
99
- ref: box,
100
- className: "map",
101
- role: "group",
102
- "aria-label": label
103
- }), /* @__PURE__ */ jsx(Attribution, {})] }) : /* @__PURE__ */ jsxs("div", {
104
- className: "map-gate",
105
- children: [/* @__PURE__ */ jsx("p", {
106
- className: "muted",
107
- children: notice
108
- }), /* @__PURE__ */ jsx("button", {
109
- type: "button",
110
- className: "button",
111
- onClick: () => setShown(true),
112
- children: load
113
- })]
114
- }), children]
115
- });
116
- };
117
- //#endregion
118
- //#region src/registry.tsx
119
- const ChatContext = createContext(null);
120
- const useChat = () => {
121
- const value = useContext(ChatContext);
122
- if (!value) throw new Error("A chat component rendered outside the chat panel.");
123
- return value;
124
- };
125
- /** A stay from the state; with a `rentalId`, only a stay of that rental, so a price never shows
126
- * under another rental's name, whatever a spec pairs. */
127
- const useStay = (stayId, rentalId) => {
128
- const stay = useStateValue(statePath("stays", stayId ?? "")) ?? null;
129
- return stay && (rentalId === void 0 || stay.rentalId === rentalId) ? stay : null;
130
- };
131
- const useRental = (rentalId) => useStateValue(statePath("rentals", rentalId)) ?? null;
132
- const day = (value) => /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
133
- /** "12.–19. Juli 2027" from two `YYYY-MM-DD` days. */
134
- const period = (stay, locale) => new Intl.DateTimeFormat(locale, {
135
- day: "numeric",
136
- month: "short",
137
- year: "numeric",
138
- timeZone: "UTC"
139
- }).formatRange(day(stay.start), day(stay.end));
140
- /** "14.–21. Juli": the short form for choices next to each other. */
141
- const shortPeriod = (stay, locale) => new Intl.DateTimeFormat(locale, {
142
- day: "numeric",
143
- month: "short",
144
- timeZone: "UTC"
145
- }).formatRange(day(stay.start), day(stay.end));
146
- /** "1 Kind", "2 Kinder": a count in its plural form. */
147
- const counted = (forms, n, locale) => (forms[new Intl.PluralRules(locale).select(n)] ?? forms.other).replace("{n}", String(n));
148
- /** A review count as the PMS formats it ("36", or "1 Bewertungen" on v10), as the number it states. */
149
- const reviewCount = (count) => Number(count.replace(/\D/g, ""));
150
- const partyText = (stay, copy, locale) => {
151
- const children = stay.party.children + stay.party.babies;
152
- return [counted(copy.adults, stay.party.adults, locale), ...children ? [counted(copy.children, children, locale)] : []].join(", ");
153
- };
154
- /** A bookable stay goes to the rental's booking page with everything chosen, where the site has one;
155
- * otherwise to the rental page (or the search) with the stay filled in. */
156
- const useBookHref = (rentalId, stay) => {
157
- const { searchPath } = useChat();
158
- const page = useStateValue(statePath("pages", rentalId)) ?? null;
159
- const booking = useStateValue(statePath("bookingPages", rentalId)) ?? null;
160
- if (stay?.bookable && booking) return bookingHref(booking, stay);
161
- return stayHref(page, stay?.bookable ? stay : null, searchPath);
162
- };
163
- /** A link out of the chat. It waits while a reply streams: leaving the page then aborts the turn,
164
- * which neither the service nor the widget keeps, and a first conversation would be lost. */
165
- const ReplyLink = ({ href, className, children }) => {
166
- const { busy } = useChat();
167
- return busy ? /* @__PURE__ */ jsx("span", {
168
- className,
169
- "aria-disabled": "true",
170
- children
171
- }) : /* @__PURE__ */ jsx("a", {
172
- className,
173
- href,
174
- children
175
- });
176
- };
177
- const BookLink = ({ rentalId, stay }) => {
178
- const { copy } = useChat();
179
- return /* @__PURE__ */ jsx(ReplyLink, {
180
- className: "button",
181
- href: useBookHref(rentalId, stay),
182
- children: stay?.bookable ? copy.bookStay : copy.viewRental
183
- });
184
- };
185
- /** A row that snaps item by item, with arrow buttons and "n / total"; swipe, scroll or keys move it. */
186
- const Snapper = ({ count, className, label, children }) => {
187
- const { copy } = useChat();
188
- const row = useRef(null);
189
- const [index, setIndex] = useState(0);
190
- const range = (element) => Math.max(1, element.scrollWidth - element.clientWidth);
191
- const go = (step) => {
192
- const element = row.current;
193
- if (!element) return;
194
- const still = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
195
- const [first, second] = element.children;
196
- const card = first && second ? second.offsetLeft - first.offsetLeft : element.clientWidth;
197
- element.scrollBy({
198
- left: step * card,
199
- behavior: still ? "auto" : "smooth"
200
- });
201
- };
202
- return /* @__PURE__ */ jsxs("div", {
203
- className: `${className}-wrap`,
204
- role: "group",
205
- "aria-roledescription": "carousel",
206
- "aria-label": label,
207
- children: [/* @__PURE__ */ jsx("div", {
208
- ref: row,
209
- className,
210
- onScroll: (event) => {
211
- const element = event.currentTarget;
212
- setIndex(Math.round(element.scrollLeft / range(element) * (count - 1)));
213
- },
214
- children
215
- }), count > 1 && /* @__PURE__ */ jsxs("div", {
216
- className: "snap-nav",
217
- children: [
218
- /* @__PURE__ */ jsx("button", {
219
- type: "button",
220
- "aria-label": copy.previous,
221
- disabled: index === 0,
222
- onClick: () => go(-1),
223
- children: "‹"
224
- }),
225
- /* @__PURE__ */ jsxs("span", {
226
- className: "muted",
227
- children: [
228
- Math.min(index + 1, count),
229
- " / ",
230
- count
231
- ]
232
- }),
233
- /* @__PURE__ */ jsx("button", {
234
- type: "button",
235
- "aria-label": copy.next,
236
- disabled: index >= count - 1,
237
- onClick: () => go(1),
238
- children: "›"
239
- })
240
- ]
241
- })]
242
- });
243
- };
244
- /** A rental's photos, swiped or stepped through with the arrows a pointer brings up; the dots say
245
- * which one shows. */
246
- const Gallery = ({ images, name }) => {
247
- const { copy } = useChat();
248
- const row = useRef(null);
249
- const [index, setIndex] = useState(0);
250
- if (images.length === 0) return null;
251
- const go = (to) => {
252
- const element = row.current;
253
- if (!element) return;
254
- const still = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
255
- element.scrollTo({
256
- left: to * element.clientWidth,
257
- behavior: still ? "auto" : "smooth"
258
- });
259
- };
260
- const many = images.length > 1;
261
- return /* @__PURE__ */ jsxs("div", {
262
- className: "gallery-wrap",
263
- role: "group",
264
- "aria-roledescription": "carousel",
265
- "aria-label": name,
266
- children: [/* @__PURE__ */ jsx("div", {
267
- ref: row,
268
- className: "gallery",
269
- onScroll: (event) => {
270
- const element = event.currentTarget;
271
- setIndex(Math.round(element.scrollLeft / Math.max(1, element.clientWidth)));
272
- },
273
- children: images.map((image, imageIndex) => /* @__PURE__ */ jsx("img", {
274
- src: image.src,
275
- alt: image.alt || `${name}, ${copy.photo} ${imageIndex + 1}`,
276
- loading: "lazy"
277
- }, image.src))
278
- }), many && /* @__PURE__ */ jsxs(Fragment, { children: [
279
- /* @__PURE__ */ jsx("button", {
280
- type: "button",
281
- className: "gallery-arrow",
282
- "data-side": "prev",
283
- "aria-label": copy.previous,
284
- disabled: index === 0,
285
- onClick: () => go(index - 1),
286
- children: /* @__PURE__ */ jsx(Chevron, { side: "prev" })
287
- }),
288
- /* @__PURE__ */ jsx("button", {
289
- type: "button",
290
- className: "gallery-arrow",
291
- "data-side": "next",
292
- "aria-label": copy.next,
293
- disabled: index >= images.length - 1,
294
- onClick: () => go(index + 1),
295
- children: /* @__PURE__ */ jsx(Chevron, { side: "next" })
296
- }),
297
- /* @__PURE__ */ jsx("span", {
298
- className: "dots",
299
- "aria-hidden": "true",
300
- children: images.map((image, imageIndex) => /* @__PURE__ */ jsx("i", { "data-current": imageIndex === index || void 0 }, image.src))
301
- }),
302
- /* @__PURE__ */ jsx("span", {
303
- className: "sr-only",
304
- "aria-live": "polite",
305
- children: `${copy.photo} ${Math.min(index + 1, images.length)} / ${images.length}`
306
- })
307
- ] })]
308
- });
309
- };
310
- /** The PMS's score as a chip; its number of reviews is read out, not shown. */
311
- const Rating = ({ rental }) => {
312
- const { copy, locale } = useChat();
313
- if (!rental.rating) return null;
314
- const { value, count } = rental.rating;
315
- return /* @__PURE__ */ jsx("span", {
316
- className: "score-chip",
317
- role: "img",
318
- title: counted(copy.reviews, reviewCount(count), locale),
319
- "aria-label": `${value}, ${counted(copy.reviews, reviewCount(count), locale)}`,
320
- children: value
321
- });
322
- };
323
- /** The visitor's wishes the latest search that found the rental applied, which it therefore has. */
324
- const useWishes = (rentalId) => useStateValue(statePath("wishes", rentalId)) ?? [];
325
- const Wishes = ({ wishes }) => {
326
- const { copy } = useChat();
327
- if (wishes.length === 0) return null;
328
- return /* @__PURE__ */ jsx("ul", {
329
- className: "wishes",
330
- "aria-label": copy.matchesWishes,
331
- children: wishes.map((wish) => /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx(Check, {}), wish] }, wish))
332
- });
333
- };
334
- /** "7 Nächte · 26.12.–2.1.": a stay's length and dates, short. */
335
- const stayMeta = (stay, copy, locale) => {
336
- const nights = Math.round((day(stay.end).getTime() - day(stay.start).getTime()) / 864e5);
337
- const short = new Intl.DateTimeFormat(locale, {
338
- day: "numeric",
339
- month: "numeric",
340
- timeZone: "UTC"
341
- });
342
- return `${counted(copy.nights, nights, locale)} · ${short.format(day(stay.start))}–${short.format(day(stay.end))}`;
343
- };
344
- /** A card's foot: the stay's total and its length, and the way to book or to the rental. */
345
- const CardFoot = ({ rentalId, stay }) => {
346
- const { copy, locale } = useChat();
347
- const href = useBookHref(rentalId, stay);
348
- return /* @__PURE__ */ jsxs("div", {
349
- className: "card-foot",
350
- children: [stay && /* @__PURE__ */ jsx("div", {
351
- className: "foot-price",
352
- children: !stay.bookable ? /* @__PURE__ */ jsx("span", {
353
- className: "muted",
354
- children: copy.notBookable
355
- }) : /* @__PURE__ */ jsxs(Fragment, { children: [stay.total && /* @__PURE__ */ jsx("span", {
356
- className: "price",
357
- children: stay.total
358
- }), /* @__PURE__ */ jsx("span", {
359
- className: "foot-meta",
360
- children: stayMeta(stay, copy, locale)
361
- })] })
362
- }), /* @__PURE__ */ jsx(ReplyLink, {
363
- className: "button compact",
364
- href,
365
- children: stay?.bookable ? copy.book : copy.view
366
- })]
367
- });
368
- };
369
- const Card = ({ rentalId, stayId }) => {
370
- const rental = useRental(rentalId);
371
- const stay = useStay(stayId, rentalId);
372
- const wishes = useWishes(rentalId);
373
- if (!rental) return null;
374
- return /* @__PURE__ */ jsxs("article", {
375
- className: "card rental-card",
376
- children: [
377
- rental.images.length > 0 ? /* @__PURE__ */ jsx(Gallery, {
378
- images: rental.images,
379
- name: rental.name
380
- }) : /* @__PURE__ */ jsx("div", {
381
- className: "no-photo",
382
- "aria-hidden": "true",
383
- children: /* @__PURE__ */ jsx(House, {})
384
- }),
385
- /* @__PURE__ */ jsxs("div", {
386
- className: "card-body",
387
- children: [
388
- /* @__PURE__ */ jsxs("div", {
389
- className: "card-meta",
390
- children: [rental.place && /* @__PURE__ */ jsx("span", {
391
- className: "place",
392
- children: rental.place
393
- }), /* @__PURE__ */ jsx(Rating, { rental })]
394
- }),
395
- /* @__PURE__ */ jsx("h3", {
396
- className: "card-title",
397
- children: rental.name
398
- }),
399
- rental.highlights.length > 0 && /* @__PURE__ */ jsx("p", {
400
- className: "specs",
401
- children: rental.highlights.slice(0, 3).join(" · ")
402
- }),
403
- /* @__PURE__ */ jsx(Wishes, { wishes })
404
- ]
405
- }),
406
- /* @__PURE__ */ jsx(CardFoot, {
407
- rentalId: rental.id,
408
- stay
409
- })
410
- ]
411
- });
412
- };
413
- /** Monday-first weeks of one month, as `YYYY-MM-DD` days and `null` padding. */
414
- const monthGrid = (month) => {
415
- const first = day(`${month}-01`);
416
- const days = new Date(Date.UTC(first.getUTCFullYear(), first.getUTCMonth() + 1, 0)).getUTCDate();
417
- const lead = (first.getUTCDay() + 6) % 7;
418
- return [...Array.from({ length: lead }, () => null), ...Array.from({ length: days }, (_, i) => `${month}-${String(i + 1).padStart(2, "0")}`)];
419
- };
420
- const monthsOf = (calendar) => {
421
- const months = [];
422
- for (let month = calendar.from.slice(0, 7); month <= calendar.through.slice(0, 7);) {
423
- months.push(month);
424
- const next = day(`${month}-01`);
425
- next.setUTCMonth(next.getUTCMonth() + 1);
426
- month = next.toISOString().slice(0, 7);
427
- }
428
- return months;
429
- };
430
- const Calendar = ({ rentalId, stayId }) => {
431
- const { copy, locale } = useChat();
432
- const rental = useRental(rentalId);
433
- const stay = useStay(stayId, rentalId);
434
- const calendar = useStateValue(statePath("availability", rentalId)) ?? null;
435
- const months = calendar ? monthsOf(calendar) : [];
436
- const [shown, setShown] = useState(() => Math.max(0, stay ? months.indexOf(stay.start.slice(0, 7)) : 0));
437
- if (!calendar || months.length === 0) return null;
438
- const month = months[Math.min(shown, months.length - 1)];
439
- const weekdays = Array.from({ length: 7 }, (_, i) => new Intl.DateTimeFormat(locale, {
440
- weekday: "short",
441
- timeZone: "UTC"
442
- }).format(day(`2024-01-0${i + 1}`)));
443
- const title = new Intl.DateTimeFormat(locale, {
444
- month: "long",
445
- year: "numeric",
446
- timeZone: "UTC"
447
- }).format(day(`${month}-01`));
448
- const inStay = (date) => Boolean(stay && date >= stay.start && date <= stay.end);
449
- const fullDate = new Intl.DateTimeFormat(locale, {
450
- weekday: "long",
451
- day: "numeric",
452
- month: "long",
453
- timeZone: "UTC"
454
- });
455
- const statusText = {
456
- free: copy.free,
457
- booked: copy.booked,
458
- closed: copy.closed
459
- };
460
- return /* @__PURE__ */ jsxs("div", {
461
- className: "calendar",
462
- children: [
463
- /* @__PURE__ */ jsxs("div", {
464
- className: "calendar-head",
465
- children: [
466
- /* @__PURE__ */ jsx("button", {
467
- type: "button",
468
- "aria-label": copy.previous,
469
- disabled: shown === 0,
470
- onClick: () => setShown(shown - 1),
471
- children: "‹"
472
- }),
473
- /* @__PURE__ */ jsxs("span", { children: [title, rental ? ` · ${rental.name}` : ""] }),
474
- /* @__PURE__ */ jsx("button", {
475
- type: "button",
476
- "aria-label": copy.next,
477
- disabled: shown >= months.length - 1,
478
- onClick: () => setShown(shown + 1),
479
- children: "›"
480
- })
481
- ]
482
- }),
483
- /* @__PURE__ */ jsxs("div", {
484
- className: "calendar-grid",
485
- children: [weekdays.map((weekday) => /* @__PURE__ */ jsx("span", {
486
- className: "weekday",
487
- children: weekday
488
- }, weekday)), monthGrid(month).map((date, index) => {
489
- if (!date) return /* @__PURE__ */ jsx("span", {}, `pad-${index}`);
490
- const status = calendar.days[date] ?? "closed";
491
- return /* @__PURE__ */ jsxs("span", {
492
- className: "day",
493
- "data-status": status,
494
- "data-stay": inStay(date) || void 0,
495
- children: [/* @__PURE__ */ jsx("span", {
496
- "aria-hidden": "true",
497
- children: Number(date.slice(8))
498
- }), /* @__PURE__ */ jsx("span", {
499
- className: "sr-only",
500
- children: [
501
- fullDate.format(day(date)),
502
- statusText[status],
503
- ...inStay(date) ? [copy.yourStay] : []
504
- ].join(", ")
505
- })]
506
- }, date);
507
- })]
508
- }),
509
- /* @__PURE__ */ jsxs("div", {
510
- className: "legend muted",
511
- children: [
512
- /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx("i", { "data-status": "free" }), copy.free] }),
513
- /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx("i", { "data-status": "booked" }), copy.booked] }),
514
- stay && /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx("i", { "data-stay": "true" }), copy.yourStay] })
515
- ]
516
- })
517
- ]
518
- });
519
- };
520
- const CompareColumn = ({ rentalId, stayId }) => {
521
- const { copy, locale } = useChat();
522
- const rental = useRental(rentalId);
523
- const stay = useStay(stayId, rentalId);
524
- const href = useBookHref(rentalId, stay);
525
- if (!rental) return null;
526
- return /* @__PURE__ */ jsxs("div", {
527
- className: "compare-col",
528
- children: [
529
- /* @__PURE__ */ jsx("strong", { children: rental.name }),
530
- /* @__PURE__ */ jsxs("dl", { children: [
531
- /* @__PURE__ */ jsx("dt", { children: copy.compareRating }),
532
- /* @__PURE__ */ jsx("dd", { children: rental.rating ? `★ ${rental.rating.value} (${counted(copy.reviews, reviewCount(rental.rating.count), locale)})` : "–" }),
533
- /* @__PURE__ */ jsx("dt", { children: copy.compareRooms }),
534
- /* @__PURE__ */ jsx("dd", { children: rental.rooms.slice(0, 3).join(", ") || "–" }),
535
- /* @__PURE__ */ jsx("dt", { children: copy.compareHighlights }),
536
- /* @__PURE__ */ jsx("dd", { children: rental.highlights.slice(0, 4).join(", ") || "–" }),
537
- stay && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("dt", { children: copy.compareStay }), /* @__PURE__ */ jsxs("dd", { children: [
538
- shortPeriod(stay, locale),
539
- /* @__PURE__ */ jsx("br", {}),
540
- !stay.bookable ? /* @__PURE__ */ jsx("span", {
541
- className: "muted",
542
- children: copy.notBookable
543
- }) : stay.total && /* @__PURE__ */ jsx("span", {
544
- className: "price",
545
- children: stay.total
546
- })
547
- ] })] })
548
- ] }),
549
- /* @__PURE__ */ jsx(ReplyLink, {
550
- className: "button",
551
- href,
552
- children: stay?.bookable ? copy.bookStay : copy.viewRental
553
- })
554
- ]
555
- });
556
- };
557
- const AlternativeChip = ({ rentalId, stayId }) => {
558
- const { locale } = useChat();
559
- const stay = useStay(stayId, rentalId);
560
- const href = useBookHref(rentalId, stay);
561
- if (!stay?.bookable) return null;
562
- return /* @__PURE__ */ jsxs(ReplyLink, {
563
- className: "chip-link",
564
- href,
565
- children: [/* @__PURE__ */ jsx("strong", { children: shortPeriod(stay, locale) }), stay.total ? ` · ${stay.total}` : ""]
566
- });
567
- };
568
- /** One arrival of a price calendar: its bar, the PMS's total, the day; tapping asks for that stay. */
569
- const PriceColumn = ({ rentalId, option, asked, cheapest }) => {
570
- const { copy, locale, send, busy } = useChat();
571
- const rental = useRental(rentalId);
572
- const stay = useStay(option.stayId, rentalId);
573
- const free = Boolean(stay?.bookable && stay.total);
574
- const arrival = new Intl.DateTimeFormat(locale, {
575
- weekday: "short",
576
- day: "numeric",
577
- month: "numeric",
578
- timeZone: "UTC"
579
- }).format(day(option.start));
580
- const dates = period(option, locale);
581
- return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
582
- type: "button",
583
- className: "price-column",
584
- "data-asked": asked || void 0,
585
- "data-cheapest": cheapest || void 0,
586
- "data-free": free || void 0,
587
- disabled: !free || !send || busy,
588
- "aria-label": [
589
- dates,
590
- free ? stay.total : copy.closed,
591
- ...cheapest ? [copy.cheapest] : [],
592
- ...asked ? [copy.yourStay] : []
593
- ].join(", "),
594
- onClick: () => send?.(rental ? `${rental.name}: ${dates}` : dates),
595
- children: [
596
- /* @__PURE__ */ jsx("span", {
597
- className: "bar-track",
598
- "aria-hidden": "true",
599
- children: free && /* @__PURE__ */ jsx("span", {
600
- className: "bar",
601
- style: { height: `${25 + 75 * (option.level ?? 1)}%` }
602
- })
603
- }),
604
- /* @__PURE__ */ jsx("span", {
605
- className: "price-text",
606
- "aria-hidden": "true",
607
- children: free ? stay.total : copy.closed
608
- }),
609
- /* @__PURE__ */ jsx("span", {
610
- className: "muted",
611
- "aria-hidden": "true",
612
- children: arrival
613
- })
614
- ]
615
- }) });
616
- };
617
- const PriceCalendarView = ({ rentalId }) => {
618
- const { copy, locale } = useChat();
619
- const rental = useRental(rentalId);
620
- const calendar = useStateValue(statePath("priceCalendars", rentalId)) ?? null;
621
- if (!calendar || calendar.options.length === 0) return null;
622
- return /* @__PURE__ */ jsx("section", {
623
- className: "card price-calendar",
624
- "aria-label": copy.priceCalendar,
625
- children: /* @__PURE__ */ jsxs("div", {
626
- className: "card-body",
627
- children: [
628
- /* @__PURE__ */ jsx("h3", {
629
- className: "card-title",
630
- children: copy.priceCalendar
631
- }),
632
- /* @__PURE__ */ jsxs("span", {
633
- className: "muted",
634
- children: [counted(copy.priceCalendarNights, calendar.nights, locale), rental ? ` · ${rental.name}` : ""]
635
- }),
636
- /* @__PURE__ */ jsx("ul", {
637
- className: "price-columns",
638
- children: calendar.options.map((option) => /* @__PURE__ */ jsx(PriceColumn, {
639
- rentalId,
640
- option,
641
- asked: option.start === calendar.asked,
642
- cheapest: option.stayId !== null && option.stayId === calendar.cheapest
643
- }, option.start))
644
- }),
645
- /* @__PURE__ */ jsxs("div", {
646
- className: "legend muted",
647
- children: [/* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx("i", { "data-cheapest": "true" }), copy.cheapest] }), /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx("i", { "data-asked": "true" }), copy.yourStay] })]
648
- })
649
- ]
650
- })
651
- });
652
- };
653
- const ReviewsView = ({ rentalId }) => {
654
- const { copy, locale } = useChat();
655
- const rental = useRental(rentalId);
656
- const reviews = useStateValue(statePath("reviews", rentalId)) ?? null;
657
- if (!reviews) return null;
658
- return /* @__PURE__ */ jsx("section", {
659
- className: "card reviews",
660
- "aria-label": copy.reviewsTitle,
661
- children: /* @__PURE__ */ jsxs("div", {
662
- className: "card-body",
663
- children: [
664
- /* @__PURE__ */ jsxs("h3", {
665
- className: "card-title",
666
- children: [copy.reviewsTitle, rental ? ` · ${rental.name}` : ""]
667
- }),
668
- /* @__PURE__ */ jsxs("div", {
669
- className: "score",
670
- children: [/* @__PURE__ */ jsx("span", {
671
- className: "score-value",
672
- children: reviews.rating
673
- }), /* @__PURE__ */ jsxs("span", { children: [reviews.grade && /* @__PURE__ */ jsx("strong", { children: reviews.grade }), /* @__PURE__ */ jsxs("span", {
674
- className: "muted",
675
- children: [" ", counted(copy.reviews, reviewCount(reviews.count), locale)]
676
- })] })]
677
- }),
678
- reviews.categories.length > 0 && /* @__PURE__ */ jsx("dl", {
679
- className: "review-categories",
680
- children: reviews.categories.map((category) => /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: category.label }), /* @__PURE__ */ jsx("dd", { children: category.rating })] }, category.label))
681
- }),
682
- reviews.excerpts.map((excerpt) => /* @__PURE__ */ jsx("blockquote", {
683
- className: "excerpt",
684
- children: /* @__PURE__ */ jsx("p", { children: excerpt.text })
685
- }, excerpt.text))
686
- ]
687
- })
688
- });
689
- };
690
- const FactsView = ({ rentalId, groups }) => {
691
- const rental = useRental(rentalId);
692
- const known = useStateValue(statePath("facts", rentalId)) ?? [];
693
- const shown = groups.flatMap((label) => {
694
- const group = known.find((candidate) => candidate.label.toLowerCase() === label.toLowerCase());
695
- return group ? [group] : [];
696
- });
697
- if (!rental || shown.length === 0) return null;
698
- return /* @__PURE__ */ jsx("section", {
699
- className: "card facts",
700
- "aria-label": rental.name,
701
- children: /* @__PURE__ */ jsxs("div", {
702
- className: "card-body",
703
- children: [/* @__PURE__ */ jsx("h3", {
704
- className: "card-title",
705
- children: rental.name
706
- }), shown.map((group) => /* @__PURE__ */ jsxs("div", {
707
- className: "fact-group",
708
- children: [/* @__PURE__ */ jsx("h4", { children: group.label }), /* @__PURE__ */ jsx("ul", { children: group.items.map((item) => /* @__PURE__ */ jsx("li", { children: item }, item)) })]
709
- }, group.label))]
710
- })
711
- });
712
- };
713
- const RentalMapView = ({ rentalId }) => {
714
- const { copy } = useChat();
715
- const rental = useRental(rentalId);
716
- const href = useBookHref(rentalId, null);
717
- if (!rental) return null;
718
- return /* @__PURE__ */ jsx(MapCard, {
719
- rental,
720
- label: copy.mapLabel.replace("{name}", rental.name),
721
- notice: copy.mapNotice,
722
- load: copy.mapLoad,
723
- children: /* @__PURE__ */ jsxs("div", {
724
- className: "card-body",
725
- children: [
726
- /* @__PURE__ */ jsx("h3", {
727
- className: "card-title",
728
- children: rental.name
729
- }),
730
- rental.place && /* @__PURE__ */ jsx("span", {
731
- className: "muted",
732
- children: rental.place
733
- }),
734
- /* @__PURE__ */ jsx(ReplyLink, {
735
- className: "button",
736
- href,
737
- children: copy.viewRental
738
- })
739
- ]
740
- })
741
- });
742
- };
743
- /** The registry the panel renders specs with. */
744
- const { registry } = defineRegistry(chatCatalog, { components: {
745
- Stack: ({ children }) => /* @__PURE__ */ jsx("div", {
746
- className: "ui",
747
- children
748
- }),
749
- RentalCard: ({ props }) => /* @__PURE__ */ jsx(Card, {
750
- rentalId: props.rentalId,
751
- stayId: props.stayId
752
- }),
753
- RentalCarousel: ({ props }) => {
754
- const { copy } = useChat();
755
- return /* @__PURE__ */ jsx(Snapper, {
756
- count: props.items.length,
757
- className: "carousel",
758
- label: copy.title,
759
- children: props.items.map((item) => /* @__PURE__ */ jsx(Card, {
760
- rentalId: item.rentalId,
761
- stayId: item.stayId
762
- }, `${item.rentalId}:${item.stayId}`))
763
- });
764
- },
765
- StayQuote: ({ props }) => {
766
- const { copy, locale } = useChat();
767
- const stay = useStay(props.stayId);
768
- const rental = useRental(stay?.rentalId ?? "");
769
- if (!stay) return null;
770
- return /* @__PURE__ */ jsxs("article", {
771
- className: "card stay-card",
772
- children: [/* @__PURE__ */ jsxs("div", {
773
- className: "card-body",
774
- children: [rental && /* @__PURE__ */ jsx("h3", {
775
- className: "card-title",
776
- children: rental.name
777
- }), /* @__PURE__ */ jsx("p", {
778
- className: "specs",
779
- children: partyText(stay, copy, locale)
780
- })]
781
- }), /* @__PURE__ */ jsx(CardFoot, {
782
- rentalId: stay.rentalId,
783
- stay
784
- })]
785
- });
786
- },
787
- PriceBreakdown: ({ props }) => {
788
- const { copy } = useChat();
789
- const stay = useStay(props.stayId);
790
- if (!stay?.bookable || !stay.total) return null;
791
- return /* @__PURE__ */ jsx("section", {
792
- className: "card breakdown",
793
- "aria-label": copy.priceBreakdown,
794
- children: /* @__PURE__ */ jsxs("div", {
795
- className: "card-body",
796
- children: [
797
- /* @__PURE__ */ jsx("span", {
798
- className: "muted",
799
- children: copy.priceBreakdown
800
- }),
801
- stay.lines.map((line) => /* @__PURE__ */ jsxs("div", {
802
- className: "line",
803
- children: [/* @__PURE__ */ jsx("span", { children: line.label }), /* @__PURE__ */ jsx("span", { children: line.amount })]
804
- }, `${line.label}:${line.amount}`)),
805
- /* @__PURE__ */ jsxs("div", {
806
- className: "line total",
807
- children: [/* @__PURE__ */ jsx("span", { children: copy.total }), /* @__PURE__ */ jsx("span", { children: stay.total })]
808
- })
809
- ]
810
- })
811
- });
812
- },
813
- AlternativeStays: ({ props }) => {
814
- const { copy } = useChat();
815
- return /* @__PURE__ */ jsx("div", {
816
- className: "alternatives",
817
- role: "group",
818
- "aria-label": copy.otherDates,
819
- children: props.stayIds.map((stayId) => /* @__PURE__ */ jsx(AlternativeChip, {
820
- rentalId: props.rentalId,
821
- stayId
822
- }, stayId))
823
- });
824
- },
825
- AvailabilityCalendar: ({ props }) => /* @__PURE__ */ jsx(Calendar, {
826
- rentalId: props.rentalId,
827
- stayId: props.stayId
828
- }),
829
- PriceCalendar: ({ props }) => /* @__PURE__ */ jsx(PriceCalendarView, { rentalId: props.rentalId }),
830
- ReviewSummary: ({ props }) => /* @__PURE__ */ jsx(ReviewsView, { rentalId: props.rentalId }),
831
- RentalFacts: ({ props }) => /* @__PURE__ */ jsx(FactsView, {
832
- rentalId: props.rentalId,
833
- groups: props.groups
834
- }),
835
- RentalMap: ({ props }) => /* @__PURE__ */ jsx(RentalMapView, { rentalId: props.rentalId }),
836
- Compare: ({ props }) => /* @__PURE__ */ jsx("div", {
837
- className: "compare",
838
- children: props.items.map((item) => /* @__PURE__ */ jsx(CompareColumn, {
839
- rentalId: item.rentalId,
840
- stayId: item.stayId
841
- }, `${item.rentalId}:${item.stayId}`))
842
- }),
843
- StayLink: ({ props }) => {
844
- const stay = useStay(props.stayId, props.rentalId);
845
- return /* @__PURE__ */ jsx(BookLink, {
846
- rentalId: props.rentalId,
847
- stay
848
- });
849
- },
850
- ContactLink: () => {
851
- const { copy, contactPath } = useChat();
852
- return /* @__PURE__ */ jsx(ReplyLink, {
853
- className: "link",
854
- href: contactPath,
855
- children: copy.contactLink
856
- });
857
- },
858
- QuickReplies: ({ props }) => {
859
- const { send, busy } = useChat();
860
- if (!send) return null;
861
- return /* @__PURE__ */ jsx("div", {
862
- className: "quick-replies",
863
- children: props.options.map((option) => /* @__PURE__ */ jsxs("button", {
864
- type: "button",
865
- className: "chip",
866
- disabled: busy,
867
- onClick: () => send(option),
868
- children: [/* @__PURE__ */ jsx(TopicIcon, { text: option }), option]
869
- }, option))
870
- });
871
- }
872
- } });
873
- //#endregion
874
- //#region src/panel.tsx
875
- /** Appends one event's content to the reply being streamed (the last message). */
876
- const appendToReply = (messages, part) => {
877
- const last = messages.at(-1);
878
- if (!last || last.role !== "assistant") return [...messages, {
879
- role: "assistant",
880
- parts: [part],
881
- unconfirmed: true
882
- }];
883
- const tail = last.parts.at(-1);
884
- const parts = part.kind === "text" && tail?.kind === "text" ? [...last.parts.slice(0, -1), {
885
- kind: "text",
886
- text: tail.text + part.text
887
- }] : [...last.parts, part];
888
- return [...messages.slice(0, -1), {
889
- ...last,
890
- parts
891
- }];
892
- };
893
- /** The service finished the turn: its messages, from its visitor message on, are the
894
- * conversation's now. An earlier turn it refused stays unconfirmed, on this page only. */
895
- const confirmed = (messages) => {
896
- const turn = messages.findLastIndex((message) => message.role === "visitor");
897
- return messages.map((message, index) => index >= turn && message.unconfirmed ? {
898
- role: message.role,
899
- parts: message.parts
900
- } : message);
901
- };
902
- const SCROLL_LOCK = "data-v-office-chat-open";
903
- /** Locks the page's scroll until the returned function runs. The rule lives in the page's head (a
904
- * shadow root's styles cannot reach <html>) and is added once. */
905
- const lockPageScroll = () => {
906
- const root = document.documentElement;
907
- if (!document.getElementById(SCROLL_LOCK)) {
908
- const style = document.createElement("style");
909
- style.id = SCROLL_LOCK;
910
- style.textContent = `html[${SCROLL_LOCK}] { overflow: hidden; }`;
911
- document.head.append(style);
912
- }
913
- root.setAttribute(SCROLL_LOCK, "");
914
- return () => root.removeAttribute(SCROLL_LOCK);
915
- };
916
- /** The assistant's text, its `**bold**` (a rental's name, the dates) as strong text: nothing else
917
- * is read as markup, so the model's text never becomes HTML. */
918
- const RichText = ({ text }) => /* @__PURE__ */ jsx(Fragment, { children: text.split(/\*\*(.+?)\*\*/g).map((piece, index) => index % 2 === 1 ? /* @__PURE__ */ jsx("strong", { children: piece }, index) : piece) });
919
- const SpecView = ({ spec }) => /* @__PURE__ */ jsx(JSONUIProvider, {
920
- registry,
921
- initialState: spec.state ?? {},
922
- children: /* @__PURE__ */ jsx(Renderer, {
923
- spec,
924
- registry
925
- })
926
- });
927
- const Consent = ({ copy, pending, onAccept, buttonRef }) => /* @__PURE__ */ jsxs("div", {
928
- className: "consent",
929
- children: [
930
- /* @__PURE__ */ jsx("p", {
931
- className: "message",
932
- "data-role": "assistant",
933
- children: copy.greeting
934
- }),
935
- pending && /* @__PURE__ */ jsxs("figure", {
936
- className: "pending",
937
- children: [/* @__PURE__ */ jsx("figcaption", {
938
- className: "muted",
939
- children: copy.yourMessage
940
- }), /* @__PURE__ */ jsx("p", { children: pending })]
941
- }),
942
- /* @__PURE__ */ jsxs("div", {
943
- className: "consent-card",
944
- children: [/* @__PURE__ */ jsx("p", { children: copy.consent }), /* @__PURE__ */ jsx("button", {
945
- ref: buttonRef,
946
- type: "button",
947
- className: "button compact",
948
- onClick: onAccept,
949
- children: copy.consentAccept
950
- })]
951
- })
952
- ]
953
- });
954
- const Thinking = ({ label }) => /* @__PURE__ */ jsxs("div", {
955
- className: "thinking",
956
- role: "status",
957
- children: [
958
- /* @__PURE__ */ jsx("i", {}),
959
- /* @__PURE__ */ jsx("i", {}),
960
- /* @__PURE__ */ jsx("i", {}),
961
- /* @__PURE__ */ jsx("span", {
962
- className: "sr-only",
963
- children: label
964
- })
965
- ]
966
- });
967
- function Panel(props) {
968
- const { copy, endpoint, open, pending, onSent, onStarted, onClose } = props;
969
- const stored = useMemo(() => loadConversation(endpoint), [endpoint]);
970
- const [accepted, setAccepted] = useState(stored !== null);
971
- const [messages, setMessages] = useState(stored?.messages ?? []);
972
- const [draft, setDraft] = useState("");
973
- const [streaming, setStreaming] = useState(false);
974
- const [unavailable, setUnavailable] = useState(null);
975
- const session = useRef(stored?.sessionId ?? null);
976
- const abort = useRef(null);
977
- const end = useRef(null);
978
- const start = useRef(null);
979
- const composer = useRef(null);
980
- const panel = useRef(null);
981
- const lastActivity = useRef(stored?.savedAt ?? Date.now());
982
- const hasMessages = messages.length > 0;
983
- const expired = () => Date.now() - lastActivity.current >= SESSION_TTL_S * 1e3;
984
- useEffect(() => () => abort.current?.abort(), []);
985
- useEffect(() => {
986
- if (open) end.current?.scrollIntoView({ block: "end" });
987
- }, [messages, open]);
988
- useEffect(() => {
989
- if (open) (accepted ? composer : start).current?.focus();
990
- }, [accepted, open]);
991
- useEffect(() => {
992
- onStarted(hasMessages);
993
- if (messages.at(-1)?.unconfirmed) return;
994
- const kept = messages.filter((message) => !message.unconfirmed);
995
- if (kept.length > 0) saveConversation(endpoint, {
996
- sessionId: session.current,
997
- messages: kept
998
- }, lastActivity.current);
999
- }, [
1000
- endpoint,
1001
- hasMessages,
1002
- messages,
1003
- onStarted
1004
- ]);
1005
- /** Sends one visitor message, typed or tapped (QuickReplies); nothing while a reply streams. */
1006
- const submit = async (text) => {
1007
- const message = text.trim().slice(0, MAX_MESSAGE_CHARS);
1008
- if (!message || streaming) return;
1009
- const fresh = expired();
1010
- if (fresh) {
1011
- session.current = null;
1012
- clearConversation(endpoint);
1013
- }
1014
- setDraft("");
1015
- setUnavailable(null);
1016
- setStreaming(true);
1017
- setMessages((current) => [...fresh ? [] : current, {
1018
- role: "visitor",
1019
- parts: [{
1020
- kind: "text",
1021
- text: message
1022
- }],
1023
- unconfirmed: true
1024
- }]);
1025
- const sentAt = Date.now();
1026
- const controller = new AbortController();
1027
- abort.current = controller;
1028
- try {
1029
- await sendMessage(endpoint, {
1030
- sessionId: session.current,
1031
- message,
1032
- locale: props.locale,
1033
- clientVersion: props.clientVersion
1034
- }, (chatEvent) => {
1035
- if (controller.signal.aborted) return;
1036
- if (chatEvent.type === "session") session.current = chatEvent.sessionId;
1037
- else if (chatEvent.type === "text") setMessages((current) => appendToReply(current, {
1038
- kind: "text",
1039
- text: chatEvent.delta
1040
- }));
1041
- else if (chatEvent.type === "ui") setMessages((current) => appendToReply(current, {
1042
- kind: "ui",
1043
- spec: chatEvent.spec
1044
- }));
1045
- else if (chatEvent.type === "unavailable") setUnavailable(chatEvent.reason);
1046
- else if (chatEvent.type === "done") {
1047
- lastActivity.current = sentAt;
1048
- setMessages(confirmed);
1049
- }
1050
- }, controller.signal);
1051
- } catch {
1052
- if (!controller.signal.aborted) setUnavailable("error");
1053
- } finally {
1054
- if (abort.current === controller) setStreaming(false);
1055
- }
1056
- };
1057
- useEffect(() => {
1058
- if (!open || !accepted || pending === null) return;
1059
- onSent();
1060
- submit(pending);
1061
- }, [
1062
- open,
1063
- accepted,
1064
- pending,
1065
- onSent
1066
- ]);
1067
- /** A new conversation: the service starts a new session at the next message. */
1068
- const restart = () => {
1069
- abort.current?.abort();
1070
- abort.current = null;
1071
- setStreaming(false);
1072
- setMessages([]);
1073
- setUnavailable(null);
1074
- session.current = null;
1075
- clearConversation(endpoint);
1076
- composer.current?.focus();
1077
- };
1078
- useEffect(() => {
1079
- if (!open) return;
1080
- return lockPageScroll();
1081
- }, [open]);
1082
- useEffect(() => {
1083
- if (!open) return;
1084
- document.addEventListener("astro:before-swap", onClose);
1085
- return () => document.removeEventListener("astro:before-swap", onClose);
1086
- }, [open, onClose]);
1087
- useEffect(() => {
1088
- if (open && hasMessages && !streaming && expired()) restart();
1089
- }, [open]);
1090
- /** Tab stays in the panel while it is open: the page behind the scrim is out of reach. */
1091
- const trapTab = (event) => {
1092
- const element = panel.current;
1093
- if (event.key !== "Tab" || !element) return;
1094
- const focusable = [...element.querySelectorAll("a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled])")];
1095
- const active = element.getRootNode().activeElement;
1096
- const edge = event.shiftKey ? focusable[0] : focusable.at(-1);
1097
- if (focusable.length === 0 || active === edge || !element.contains(active)) {
1098
- event.preventDefault();
1099
- (event.shiftKey ? focusable.at(-1) : focusable[0])?.focus();
1100
- }
1101
- };
1102
- const send = (event) => {
1103
- event.preventDefault();
1104
- submit(draft);
1105
- };
1106
- const waiting = streaming && messages.at(-1)?.role === "visitor";
1107
- return /* @__PURE__ */ jsxs(ChatContext.Provider, {
1108
- value: {
1109
- ...props,
1110
- send: (text) => void submit(text),
1111
- busy: streaming
1112
- },
1113
- children: [/* @__PURE__ */ jsx("div", {
1114
- className: "scrim",
1115
- hidden: !open,
1116
- "aria-hidden": "true",
1117
- onClick: onClose
1118
- }), /* @__PURE__ */ jsxs("section", {
1119
- className: "panel",
1120
- role: "dialog",
1121
- "aria-modal": "true",
1122
- "aria-label": copy.title,
1123
- ref: panel,
1124
- hidden: !open,
1125
- inert: !open,
1126
- onKeyDown: (event) => {
1127
- if (event.key === "Escape") onClose();
1128
- trapTab(event);
1129
- },
1130
- children: [
1131
- /* @__PURE__ */ jsxs("header", {
1132
- className: "header",
1133
- children: [
1134
- /* @__PURE__ */ jsx("h2", {
1135
- className: "title",
1136
- children: copy.title
1137
- }),
1138
- hasMessages && /* @__PURE__ */ jsx("button", {
1139
- type: "button",
1140
- className: "icon-button restart",
1141
- "aria-label": copy.newConversation,
1142
- title: copy.newConversation,
1143
- onClick: restart,
1144
- children: /* @__PURE__ */ jsx(Restart, {})
1145
- }),
1146
- /* @__PURE__ */ jsx("button", {
1147
- type: "button",
1148
- className: "icon-button close",
1149
- "aria-label": copy.close,
1150
- onClick: onClose,
1151
- children: /* @__PURE__ */ jsx(Cross, {})
1152
- })
1153
- ]
1154
- }),
1155
- /* @__PURE__ */ jsx("p", {
1156
- className: "notice",
1157
- children: copy.notice
1158
- }),
1159
- accepted ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
1160
- className: "messages",
1161
- "aria-live": "polite",
1162
- children: [
1163
- /* @__PURE__ */ jsx("div", {
1164
- className: "message",
1165
- "data-role": "assistant",
1166
- children: copy.greeting
1167
- }),
1168
- messages.map((message, index) => /* @__PURE__ */ jsx("div", {
1169
- className: message.role === "assistant" ? "ui" : "turn",
1170
- children: message.parts.map((part, partIndex) => part.kind === "text" ? /* @__PURE__ */ jsx("div", {
1171
- className: "message",
1172
- "data-role": message.role,
1173
- children: message.role === "assistant" ? /* @__PURE__ */ jsx(RichText, { text: part.text }) : part.text
1174
- }, partIndex) : /* @__PURE__ */ jsx(SpecView, { spec: part.spec }, partIndex))
1175
- }, index)),
1176
- waiting && /* @__PURE__ */ jsx(Thinking, { label: copy.thinking }),
1177
- /* @__PURE__ */ jsx("div", { ref: end })
1178
- ]
1179
- }), /* @__PURE__ */ jsxs("div", {
1180
- className: "dock",
1181
- children: [
1182
- unavailable && /* @__PURE__ */ jsxs("div", {
1183
- className: "fallback",
1184
- role: "status",
1185
- children: [
1186
- /* @__PURE__ */ jsx("p", {
1187
- className: "muted",
1188
- children: unavailable === "busy" ? copy.busy : copy.unavailable
1189
- }),
1190
- /* @__PURE__ */ jsx("a", {
1191
- className: "link",
1192
- href: props.searchPath,
1193
- children: copy.searchLink
1194
- }),
1195
- /* @__PURE__ */ jsx("a", {
1196
- className: "link",
1197
- href: props.contactPath,
1198
- children: copy.contactLink
1199
- })
1200
- ]
1201
- }),
1202
- !hasMessages && copy.suggestions.length > 0 && /* @__PURE__ */ jsx("div", {
1203
- className: "suggestions",
1204
- children: copy.suggestions.map((suggestion) => /* @__PURE__ */ jsxs("button", {
1205
- type: "button",
1206
- className: "suggestion",
1207
- onClick: () => void submit(suggestion),
1208
- children: [/* @__PURE__ */ jsx(TopicIcon, { text: suggestion }), suggestion]
1209
- }, suggestion))
1210
- }),
1211
- /* @__PURE__ */ jsxs("form", {
1212
- className: "composer",
1213
- onSubmit: send,
1214
- children: [/* @__PURE__ */ jsx("textarea", {
1215
- ref: composer,
1216
- rows: 1,
1217
- value: draft,
1218
- maxLength: MAX_MESSAGE_CHARS,
1219
- placeholder: copy.placeholder,
1220
- "aria-label": copy.placeholder,
1221
- onChange: (event) => setDraft(event.target.value),
1222
- onKeyDown: (event) => {
1223
- if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return;
1224
- event.preventDefault();
1225
- event.currentTarget.form?.requestSubmit();
1226
- }
1227
- }), /* @__PURE__ */ jsx("button", {
1228
- type: "submit",
1229
- className: "send",
1230
- "aria-label": copy.send,
1231
- disabled: streaming || !draft.trim(),
1232
- children: /* @__PURE__ */ jsx(Arrow, {})
1233
- })]
1234
- })
1235
- ]
1236
- })] }) : /* @__PURE__ */ jsx(Consent, {
1237
- copy,
1238
- pending,
1239
- onAccept: () => setAccepted(true),
1240
- buttonRef: start
1241
- })
1242
- ]
1243
- })]
1244
- });
1245
- }
1246
- //#endregion
1247
- export { Panel as default };