@v-office/website-chat 1.0.0 → 1.1.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.
- package/README.md +8 -3
- package/dist/{catalog-RXKmEmMW.js → catalog-y3xQ2jXW.js} +16 -9
- package/dist/catalog.d.ts +19 -0
- package/dist/catalog.js +2 -2
- package/dist/{icons-B5PAY3aU.js → icons-B4hI9ipR.js} +98 -5
- package/dist/index.d.ts +18 -4
- package/dist/index.js +211 -76
- package/dist/{panel-D8pKi4RZ.js → panel-hLRBoh8t.js} +200 -115
- package/instructions/CHANGELOG.md +2 -0
- package/instructions/MIGRATION.md +2 -0
- package/instructions/versions/1.0.1/CHANGELOG.md +14 -0
- package/instructions/versions/1.0.1/MIGRATION.md +9 -0
- package/instructions/versions/1.1.0/CHANGELOG.md +52 -0
- package/instructions/versions/1.1.0/MIGRATION.md +22 -0
- package/package.json +2 -2
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
import { i as SESSION_TTL_S, r as MAX_MESSAGE_CHARS } from "./protocol-BuNqZAMt.js";
|
|
2
|
-
import { a as clearConversation,
|
|
3
|
-
import {
|
|
2
|
+
import { a as Restart, c as clearConversation, f as sendMessage, i as Cross, l as loadConversation, n as Check, r as Chevron, s as TopicIcon, t as Arrow, u as saveConversation } from "./icons-B4hI9ipR.js";
|
|
3
|
+
import { r as chatCatalog, u as statePath } from "./catalog-y3xQ2jXW.js";
|
|
4
4
|
import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
5
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
import { JSONUIProvider, Renderer, defineRegistry, useStateValue } from "@json-render/react";
|
|
7
7
|
import { DEFAULT_MAP_STYLE_URL, partyPairs } from "@v-office/website-headless";
|
|
8
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
|
+
];
|
|
9
17
|
/** See {@link StayHref}. */
|
|
10
18
|
const stayHref = (page, stay, searchPath) => {
|
|
11
19
|
const path = page ?? searchPath;
|
|
12
|
-
|
|
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
|
+
return stay ? withQuery(path, stayPairs(stay)) : path;
|
|
20
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}`])]);
|
|
21
24
|
//#endregion
|
|
22
25
|
//#region src/map.tsx
|
|
23
26
|
/** What the style's sources ask to be credited, with their links, as MapLibre's attribution control
|
|
@@ -58,8 +61,10 @@ const MapCard = ({ rental, label, notice, load, children }) => {
|
|
|
58
61
|
if (!shown || !element || !location) return;
|
|
59
62
|
let removed = false;
|
|
60
63
|
let map;
|
|
61
|
-
import("maplibre-gl").then((
|
|
64
|
+
import("maplibre-gl").then((module) => {
|
|
62
65
|
if (removed) return;
|
|
66
|
+
const library = module;
|
|
67
|
+
const { Map, Marker } = library.default ?? library;
|
|
63
68
|
const center = [location.lng, location.lat];
|
|
64
69
|
const created = new Map({
|
|
65
70
|
container: element,
|
|
@@ -140,30 +145,13 @@ const partyText = (stay, copy, locale) => {
|
|
|
140
145
|
const children = stay.party.children + stay.party.babies;
|
|
141
146
|
return [counted(copy.adults, stay.party.adults, locale), ...children ? [counted(copy.children, children, locale)] : []].join(", ");
|
|
142
147
|
};
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
};
|
|
148
|
+
/** A bookable stay goes to the rental's booking page with everything chosen, where the site has one;
|
|
149
|
+
* otherwise to the rental page (or the search) with the stay filled in. */
|
|
164
150
|
const useBookHref = (rentalId, stay) => {
|
|
165
151
|
const { searchPath } = useChat();
|
|
166
152
|
const page = useStateValue(statePath("pages", rentalId)) ?? null;
|
|
153
|
+
const booking = useStateValue(statePath("bookingPages", rentalId)) ?? null;
|
|
154
|
+
if (stay?.bookable && booking) return bookingHref(booking, stay);
|
|
167
155
|
return stayHref(page, stay?.bookable ? stay : null, searchPath);
|
|
168
156
|
};
|
|
169
157
|
/** A link out of the chat. It waits while a reply streams: leaving the page then aborts the turn,
|
|
@@ -246,35 +234,83 @@ const Snapper = ({ count, className, label, children }) => {
|
|
|
246
234
|
})]
|
|
247
235
|
});
|
|
248
236
|
};
|
|
237
|
+
/** A rental's photos, swiped or stepped through with the arrows a pointer brings up; the dots say
|
|
238
|
+
* which one shows. */
|
|
249
239
|
const Gallery = ({ images, name }) => {
|
|
250
240
|
const { copy } = useChat();
|
|
241
|
+
const row = useRef(null);
|
|
242
|
+
const [index, setIndex] = useState(0);
|
|
251
243
|
if (images.length === 0) return null;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
244
|
+
const go = (to) => {
|
|
245
|
+
const element = row.current;
|
|
246
|
+
if (!element) return;
|
|
247
|
+
const still = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
248
|
+
element.scrollTo({
|
|
249
|
+
left: to * element.clientWidth,
|
|
250
|
+
behavior: still ? "auto" : "smooth"
|
|
251
|
+
});
|
|
252
|
+
};
|
|
253
|
+
const many = images.length > 1;
|
|
254
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
255
|
+
className: "gallery-wrap",
|
|
256
|
+
role: "group",
|
|
257
|
+
"aria-roledescription": "carousel",
|
|
258
|
+
"aria-label": name,
|
|
259
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
260
|
+
ref: row,
|
|
261
|
+
className: "gallery",
|
|
262
|
+
onScroll: (event) => {
|
|
263
|
+
const element = event.currentTarget;
|
|
264
|
+
setIndex(Math.round(element.scrollLeft / Math.max(1, element.clientWidth)));
|
|
265
|
+
},
|
|
266
|
+
children: images.map((image, imageIndex) => /* @__PURE__ */ jsx("img", {
|
|
267
|
+
src: image.src,
|
|
268
|
+
alt: image.alt || `${name}, ${copy.photo} ${imageIndex + 1}`,
|
|
269
|
+
loading: "lazy"
|
|
270
|
+
}, image.src))
|
|
271
|
+
}), many && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
272
|
+
/* @__PURE__ */ jsx("button", {
|
|
273
|
+
type: "button",
|
|
274
|
+
className: "gallery-arrow",
|
|
275
|
+
"data-side": "prev",
|
|
276
|
+
"aria-label": copy.previous,
|
|
277
|
+
disabled: index === 0,
|
|
278
|
+
onClick: () => go(index - 1),
|
|
279
|
+
children: /* @__PURE__ */ jsx(Chevron, { side: "prev" })
|
|
280
|
+
}),
|
|
281
|
+
/* @__PURE__ */ jsx("button", {
|
|
282
|
+
type: "button",
|
|
283
|
+
className: "gallery-arrow",
|
|
284
|
+
"data-side": "next",
|
|
285
|
+
"aria-label": copy.next,
|
|
286
|
+
disabled: index >= images.length - 1,
|
|
287
|
+
onClick: () => go(index + 1),
|
|
288
|
+
children: /* @__PURE__ */ jsx(Chevron, { side: "next" })
|
|
289
|
+
}),
|
|
290
|
+
/* @__PURE__ */ jsx("span", {
|
|
291
|
+
className: "dots",
|
|
292
|
+
"aria-hidden": "true",
|
|
293
|
+
children: images.map((image, imageIndex) => /* @__PURE__ */ jsx("i", { "data-current": imageIndex === index || void 0 }, image.src))
|
|
294
|
+
}),
|
|
295
|
+
/* @__PURE__ */ jsx("span", {
|
|
296
|
+
className: "sr-only",
|
|
297
|
+
"aria-live": "polite",
|
|
298
|
+
children: `${copy.photo} ${Math.min(index + 1, images.length)} / ${images.length}`
|
|
299
|
+
})
|
|
300
|
+
] })]
|
|
261
301
|
});
|
|
262
302
|
};
|
|
303
|
+
/** The PMS's score as a chip; its number of reviews is read out, not shown. */
|
|
263
304
|
const Rating = ({ rental }) => {
|
|
264
305
|
const { copy, locale } = useChat();
|
|
265
306
|
if (!rental.rating) return null;
|
|
266
307
|
const { value, count } = rental.rating;
|
|
267
|
-
return /* @__PURE__ */
|
|
268
|
-
className: "
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}),
|
|
274
|
-
" (",
|
|
275
|
-
counted(copy.reviews, reviewCount(count), locale),
|
|
276
|
-
")"
|
|
277
|
-
]
|
|
308
|
+
return /* @__PURE__ */ jsx("span", {
|
|
309
|
+
className: "score-chip",
|
|
310
|
+
role: "img",
|
|
311
|
+
title: counted(copy.reviews, reviewCount(count), locale),
|
|
312
|
+
"aria-label": `${value}, ${counted(copy.reviews, reviewCount(count), locale)}`,
|
|
313
|
+
children: value
|
|
278
314
|
});
|
|
279
315
|
};
|
|
280
316
|
/** The visitor's wishes the latest search that found the rental applied, which it therefore has. */
|
|
@@ -285,7 +321,42 @@ const Wishes = ({ wishes }) => {
|
|
|
285
321
|
return /* @__PURE__ */ jsx("ul", {
|
|
286
322
|
className: "wishes",
|
|
287
323
|
"aria-label": copy.matchesWishes,
|
|
288
|
-
children: wishes.map((wish) => /* @__PURE__ */
|
|
324
|
+
children: wishes.map((wish) => /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx(Check, {}), wish] }, wish))
|
|
325
|
+
});
|
|
326
|
+
};
|
|
327
|
+
/** "7 Nächte · 26.12.–2.1.": a stay's length and dates, short. */
|
|
328
|
+
const stayMeta = (stay, copy, locale) => {
|
|
329
|
+
const nights = Math.round((day(stay.end).getTime() - day(stay.start).getTime()) / 864e5);
|
|
330
|
+
const short = new Intl.DateTimeFormat(locale, {
|
|
331
|
+
day: "numeric",
|
|
332
|
+
month: "numeric",
|
|
333
|
+
timeZone: "UTC"
|
|
334
|
+
});
|
|
335
|
+
return `${counted(copy.nights, nights, locale)} · ${short.format(day(stay.start))}–${short.format(day(stay.end))}`;
|
|
336
|
+
};
|
|
337
|
+
/** A card's foot: the stay's total and its length, and the way to book or to the rental. */
|
|
338
|
+
const CardFoot = ({ rentalId, stay }) => {
|
|
339
|
+
const { copy, locale } = useChat();
|
|
340
|
+
const href = useBookHref(rentalId, stay);
|
|
341
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
342
|
+
className: "card-foot",
|
|
343
|
+
children: [stay && /* @__PURE__ */ jsxs("div", {
|
|
344
|
+
className: "foot-price",
|
|
345
|
+
children: [!stay.bookable ? /* @__PURE__ */ jsx("span", {
|
|
346
|
+
className: "muted",
|
|
347
|
+
children: copy.notBookable
|
|
348
|
+
}) : stay.total && /* @__PURE__ */ jsx("span", {
|
|
349
|
+
className: "price",
|
|
350
|
+
children: stay.total
|
|
351
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
352
|
+
className: "foot-meta",
|
|
353
|
+
children: stayMeta(stay, copy, locale)
|
|
354
|
+
})]
|
|
355
|
+
}), /* @__PURE__ */ jsx(ReplyLink, {
|
|
356
|
+
className: "button compact",
|
|
357
|
+
href,
|
|
358
|
+
children: stay?.bookable ? copy.book : copy.view
|
|
359
|
+
})]
|
|
289
360
|
});
|
|
290
361
|
};
|
|
291
362
|
const Card = ({ rentalId, stayId }) => {
|
|
@@ -295,41 +366,45 @@ const Card = ({ rentalId, stayId }) => {
|
|
|
295
366
|
const wishes = useWishes(rentalId);
|
|
296
367
|
if (!rental) return null;
|
|
297
368
|
return /* @__PURE__ */ jsxs("article", {
|
|
298
|
-
className: "card",
|
|
299
|
-
children: [
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
369
|
+
className: "card rental-card",
|
|
370
|
+
children: [
|
|
371
|
+
/* @__PURE__ */ jsxs("div", {
|
|
372
|
+
className: "card-media",
|
|
373
|
+
children: [/* @__PURE__ */ jsx(Gallery, {
|
|
374
|
+
images: rental.images,
|
|
375
|
+
name: rental.name
|
|
376
|
+
}), wishes.length > 0 ? /* @__PURE__ */ jsx("span", {
|
|
377
|
+
className: "badge",
|
|
378
|
+
"aria-hidden": "true",
|
|
379
|
+
children: copy.matchBadge
|
|
380
|
+
}) : null]
|
|
381
|
+
}),
|
|
382
|
+
/* @__PURE__ */ jsxs("div", {
|
|
383
|
+
className: "card-body",
|
|
384
|
+
children: [
|
|
385
|
+
/* @__PURE__ */ jsxs("div", {
|
|
386
|
+
className: "card-meta",
|
|
387
|
+
children: [rental.place && /* @__PURE__ */ jsx("span", {
|
|
388
|
+
className: "place",
|
|
389
|
+
children: rental.place
|
|
390
|
+
}), /* @__PURE__ */ jsx(Rating, { rental })]
|
|
391
|
+
}),
|
|
392
|
+
/* @__PURE__ */ jsx("h3", {
|
|
393
|
+
className: "card-title",
|
|
394
|
+
children: rental.name
|
|
395
|
+
}),
|
|
396
|
+
rental.highlights.length > 0 && /* @__PURE__ */ jsx("p", {
|
|
397
|
+
className: "specs",
|
|
398
|
+
children: rental.highlights.slice(0, 3).join(" · ")
|
|
399
|
+
}),
|
|
400
|
+
/* @__PURE__ */ jsx(Wishes, { wishes })
|
|
401
|
+
]
|
|
402
|
+
}),
|
|
403
|
+
/* @__PURE__ */ jsx(CardFoot, {
|
|
404
|
+
rentalId: rental.id,
|
|
405
|
+
stay
|
|
406
|
+
})
|
|
407
|
+
]
|
|
333
408
|
});
|
|
334
409
|
};
|
|
335
410
|
/** Monday-first weeks of one month, as `YYYY-MM-DD` days and `null` padding. */
|
|
@@ -685,25 +760,25 @@ const { registry } = defineRegistry(chatCatalog, { components: {
|
|
|
685
760
|
});
|
|
686
761
|
},
|
|
687
762
|
StayQuote: ({ props }) => {
|
|
763
|
+
const { copy, locale } = useChat();
|
|
688
764
|
const stay = useStay(props.stayId);
|
|
689
765
|
const rental = useRental(stay?.rentalId ?? "");
|
|
690
766
|
if (!stay) return null;
|
|
691
|
-
return /* @__PURE__ */
|
|
692
|
-
className: "card",
|
|
693
|
-
children: /* @__PURE__ */ jsxs("div", {
|
|
767
|
+
return /* @__PURE__ */ jsxs("article", {
|
|
768
|
+
className: "card stay-card",
|
|
769
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
694
770
|
className: "card-body",
|
|
695
|
-
children: [
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
})
|
|
771
|
+
children: [rental && /* @__PURE__ */ jsx("h3", {
|
|
772
|
+
className: "card-title",
|
|
773
|
+
children: rental.name
|
|
774
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
775
|
+
className: "specs",
|
|
776
|
+
children: partyText(stay, copy, locale)
|
|
777
|
+
})]
|
|
778
|
+
}), /* @__PURE__ */ jsx(CardFoot, {
|
|
779
|
+
rentalId: stay.rentalId,
|
|
780
|
+
stay
|
|
781
|
+
})]
|
|
707
782
|
});
|
|
708
783
|
},
|
|
709
784
|
PriceBreakdown: ({ props }) => {
|
|
@@ -782,12 +857,12 @@ const { registry } = defineRegistry(chatCatalog, { components: {
|
|
|
782
857
|
if (!send) return null;
|
|
783
858
|
return /* @__PURE__ */ jsx("div", {
|
|
784
859
|
className: "quick-replies",
|
|
785
|
-
children: props.options.map((option) => /* @__PURE__ */
|
|
860
|
+
children: props.options.map((option) => /* @__PURE__ */ jsxs("button", {
|
|
786
861
|
type: "button",
|
|
787
862
|
className: "chip",
|
|
788
863
|
disabled: busy,
|
|
789
864
|
onClick: () => send(option),
|
|
790
|
-
children: option
|
|
865
|
+
children: [/* @__PURE__ */ jsx(TopicIcon, { text: option }), option]
|
|
791
866
|
}, option))
|
|
792
867
|
});
|
|
793
868
|
}
|
|
@@ -835,6 +910,9 @@ const lockPageScroll = () => {
|
|
|
835
910
|
root.setAttribute(SCROLL_LOCK, "");
|
|
836
911
|
return () => root.removeAttribute(SCROLL_LOCK);
|
|
837
912
|
};
|
|
913
|
+
/** The assistant's text, its `**bold**` (a rental's name, the dates) as strong text: nothing else
|
|
914
|
+
* is read as markup, so the model's text never becomes HTML. */
|
|
915
|
+
const RichText = ({ text }) => /* @__PURE__ */ jsx(Fragment, { children: text.split(/\*\*(.+?)\*\*/g).map((piece, index) => index % 2 === 1 ? /* @__PURE__ */ jsx("strong", { children: piece }, index) : piece) });
|
|
838
916
|
const SpecView = ({ spec }) => /* @__PURE__ */ jsx(JSONUIProvider, {
|
|
839
917
|
registry,
|
|
840
918
|
initialState: spec.state ?? {},
|
|
@@ -846,6 +924,11 @@ const SpecView = ({ spec }) => /* @__PURE__ */ jsx(JSONUIProvider, {
|
|
|
846
924
|
const Consent = ({ copy, pending, onAccept, buttonRef }) => /* @__PURE__ */ jsxs("div", {
|
|
847
925
|
className: "consent",
|
|
848
926
|
children: [
|
|
927
|
+
/* @__PURE__ */ jsx("p", {
|
|
928
|
+
className: "message",
|
|
929
|
+
"data-role": "assistant",
|
|
930
|
+
children: copy.greeting
|
|
931
|
+
}),
|
|
849
932
|
pending && /* @__PURE__ */ jsxs("figure", {
|
|
850
933
|
className: "pending",
|
|
851
934
|
children: [/* @__PURE__ */ jsx("figcaption", {
|
|
@@ -853,13 +936,15 @@ const Consent = ({ copy, pending, onAccept, buttonRef }) => /* @__PURE__ */ jsxs
|
|
|
853
936
|
children: copy.yourMessage
|
|
854
937
|
}), /* @__PURE__ */ jsx("p", { children: pending })]
|
|
855
938
|
}),
|
|
856
|
-
/* @__PURE__ */
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
939
|
+
/* @__PURE__ */ jsxs("div", {
|
|
940
|
+
className: "consent-card",
|
|
941
|
+
children: [/* @__PURE__ */ jsx("p", { children: copy.consent }), /* @__PURE__ */ jsx("button", {
|
|
942
|
+
ref: buttonRef,
|
|
943
|
+
type: "button",
|
|
944
|
+
className: "button compact",
|
|
945
|
+
onClick: onAccept,
|
|
946
|
+
children: copy.consentAccept
|
|
947
|
+
})]
|
|
863
948
|
})
|
|
864
949
|
]
|
|
865
950
|
});
|
|
@@ -1082,7 +1167,7 @@ function Panel(props) {
|
|
|
1082
1167
|
children: message.parts.map((part, partIndex) => part.kind === "text" ? /* @__PURE__ */ jsx("div", {
|
|
1083
1168
|
className: "message",
|
|
1084
1169
|
"data-role": message.role,
|
|
1085
|
-
children: part.text
|
|
1170
|
+
children: message.role === "assistant" ? /* @__PURE__ */ jsx(RichText, { text: part.text }) : part.text
|
|
1086
1171
|
}, partIndex) : /* @__PURE__ */ jsx(SpecView, { spec: part.spec }, partIndex))
|
|
1087
1172
|
}, index)),
|
|
1088
1173
|
waiting && /* @__PURE__ */ jsx(Thinking, { label: copy.thinking }),
|
|
@@ -1113,11 +1198,11 @@ function Panel(props) {
|
|
|
1113
1198
|
}),
|
|
1114
1199
|
!hasMessages && copy.suggestions.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
1115
1200
|
className: "suggestions",
|
|
1116
|
-
children: copy.suggestions.map((suggestion) => /* @__PURE__ */
|
|
1201
|
+
children: copy.suggestions.map((suggestion) => /* @__PURE__ */ jsxs("button", {
|
|
1117
1202
|
type: "button",
|
|
1118
1203
|
className: "suggestion",
|
|
1119
1204
|
onClick: () => void submit(suggestion),
|
|
1120
|
-
children: suggestion
|
|
1205
|
+
children: [/* @__PURE__ */ jsx(TopicIcon, { text: suggestion }), suggestion]
|
|
1121
1206
|
}, suggestion))
|
|
1122
1207
|
}),
|
|
1123
1208
|
/* @__PURE__ */ jsxs("form", {
|
|
@@ -4,4 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
Release notes of `@v-office/website-chat`, newest first. Each version has its own directory.
|
|
6
6
|
|
|
7
|
+
- `versions/1.1.0/CHANGELOG.md`: release notes for 1.1.0
|
|
8
|
+
- `versions/1.0.1/CHANGELOG.md`: release notes for 1.0.1
|
|
7
9
|
- `versions/1.0.0/CHANGELOG.md`: release notes for 1.0.0
|
|
@@ -4,4 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
Migration guides of `@v-office/website-chat`, newest first. Apply them in order when skipping versions.
|
|
6
6
|
|
|
7
|
+
- `versions/1.1.0/MIGRATION.md`: Migration: 1.0.1 to 1.1.0
|
|
8
|
+
- `versions/1.0.1/MIGRATION.md`: Migration: 1.0.0 to 1.0.1
|
|
7
9
|
- `versions/1.0.0/MIGRATION.md`: Migration: 1.0.0
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog: 1.0.1
|
|
2
|
+
|
|
3
|
+
The chat's map card now shows its map. In 1.0.0, tapping "Karte laden" brought the button back
|
|
4
|
+
instead of a map on every site.
|
|
5
|
+
|
|
6
|
+
## Fixed
|
|
7
|
+
|
|
8
|
+
- `RentalMap` loads MapLibre as bundlers hand it over: maplibre-gl ships a UMD bundle, which
|
|
9
|
+
arrives as the module's default export, and 1.0.0 looked for named exports only, so the map
|
|
10
|
+
could not start and the card offered its tap again.
|
|
11
|
+
|
|
12
|
+
## Changed
|
|
13
|
+
|
|
14
|
+
- Nothing else. Props, copy and the map's tap-to-load behaviour are unchanged.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Changelog: 1.1.0
|
|
2
|
+
|
|
3
|
+
The chat looks like the site it is on and like AI Studio's booking assistant prototype. It takes
|
|
4
|
+
its colours and corners from the site's shadcn tokens, which every AI Studio template defines,
|
|
5
|
+
instead of its own blue, and has calmer cards, a two-tone brand and a friendlier start. Nothing to
|
|
6
|
+
change on a site.
|
|
7
|
+
|
|
8
|
+
## Added
|
|
9
|
+
|
|
10
|
+
- `colors.accent`: the colour of the small marks (the chat's name and sparkle, a card's place, the
|
|
11
|
+
icons, the ring round the entry pill). The site's primary when unset; a site with a second brand
|
|
12
|
+
colour sets it here.
|
|
13
|
+
- Suggestions and quick replies carry an icon for their topic (dates, prices, pets, people,
|
|
14
|
+
places, booking).
|
|
15
|
+
- The assistant's **bold** (a rental's name, the dates) shows as bold text; nothing else in its
|
|
16
|
+
text is read as markup.
|
|
17
|
+
- A stay can carry the rate and extras the visitor chose in the chat (`ChatStay.policyId`,
|
|
18
|
+
`ChatStay.services`), and the state the rental's booking page (`ChatState.bookingPages`). A
|
|
19
|
+
bookable stay's booking button then opens the booking page with the dates, the party, the rate
|
|
20
|
+
and the extras (`?policy=`, `?service=<id>:<count>`); the visitor enters their details there.
|
|
21
|
+
Without a booking page the button opens the rental page as before.
|
|
22
|
+
|
|
23
|
+
## Changed
|
|
24
|
+
|
|
25
|
+
- Colours come from the site: `--primary` and `--primary-foreground` for buttons and the chat's
|
|
26
|
+
mark, `--background`, `--foreground`, `--muted`, `--muted-foreground` and `--border` for the
|
|
27
|
+
panel and its text. The visitor's messages, suggestions and chips are the site's primary,
|
|
28
|
+
lightened. A site in dark mode gets a dark chat.
|
|
29
|
+
- Corners come from the site's `--radius`; the themes scale it (`classic` 1.6×, `soft` 2.4×,
|
|
30
|
+
`minimal` 0.6×). Buttons and chips stay pills unless the site's corners are square
|
|
31
|
+
(`--radius: 0`); `minimal` has no pills.
|
|
32
|
+
- The calendar's free days, the price bars and the map pin use the site's colours.
|
|
33
|
+
- Without the tokens the chat looks as in 1.0.
|
|
34
|
+
- Rental cards: the place and the rating on top, the name larger, the facts and the wishes as
|
|
35
|
+
chips, then the stay's total with "7 Nächte · 26.12.–2.1." and a short "Buchen" button below a
|
|
36
|
+
line. Photos show dots, and arrows under a pointer, instead of "1 / 3".
|
|
37
|
+
- A stay quote has the same foot as a card.
|
|
38
|
+
- The panel's head has no line under it and a filled close button; the AI notice is one quiet line.
|
|
39
|
+
- A warmer greeting, and the start step as a card under it with a compact "Chat starten".
|
|
40
|
+
- The panel is 700px wide on wide screens (`--chat-width` still sets it), the assistant's text a
|
|
41
|
+
little larger.
|
|
42
|
+
|
|
43
|
+
## Fixed
|
|
44
|
+
|
|
45
|
+
- A site whose tokens are HSL triples (`221 83% 53%`, an older shadcn setup) keeps the chat's own
|
|
46
|
+
colours for them instead of getting colourless buttons.
|
|
47
|
+
|
|
48
|
+
## Unchanged
|
|
49
|
+
|
|
50
|
+
- `colors={{ primary, secondary }}` still overrides, with white text on `primary` and dark text on
|
|
51
|
+
`secondary` as before (not the site's foregrounds, which suit the site's own colours). `theme`
|
|
52
|
+
takes the same values, and every other prop is as before.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Migration: 1.0.1 to 1.1.0
|
|
2
|
+
|
|
3
|
+
No changes required: the chat takes the site's design tokens by itself.
|
|
4
|
+
|
|
5
|
+
## 1. Upgrade
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add --save-exact @v-office/website-chat@1.1.0
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 2. Check `colors` (optional)
|
|
12
|
+
|
|
13
|
+
A site that passed `colors` to match its brand can drop it: the chat now reads the same colour from
|
|
14
|
+
`--primary`. Keep it only where the chat should differ from the site's buttons.
|
|
15
|
+
|
|
16
|
+
## 3. A second brand colour (optional)
|
|
17
|
+
|
|
18
|
+
A site whose design has a second colour for small accents (labels, icons) passes it:
|
|
19
|
+
|
|
20
|
+
```astro
|
|
21
|
+
<WebsiteChat client:idle ... colors={{ accent: "var(--color-accent)" }} />
|
|
22
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@v-office/website-chat",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "The AI chat of v-office websites: a launcher and a lazily loaded chat panel in a Shadow DOM, talking to the AI Studio chat service, and the json-render catalog (./catalog) the service places its rental cards, stays and links from. Styled through CSS custom properties and curated template themes; consumers need no Tailwind",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"@types/node": "26.5.1",
|
|
35
35
|
"@types/react": "19.3.0",
|
|
36
36
|
"@types/react-dom": "19.3.0",
|
|
37
|
-
"@v-office/website-headless": "1.
|
|
37
|
+
"@v-office/website-headless": "1.2.0",
|
|
38
38
|
"@v-office/website-sdk": "2.34.0",
|
|
39
39
|
"jsdom": "30.0.1",
|
|
40
40
|
"oxfmt": "0.67.0",
|