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