@vinaverse/react-native 0.1.0-alpha.3 → 0.1.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -2
- package/dist/index.cjs +342 -81
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +110 -64
- package/dist/index.d.ts +110 -64
- package/dist/index.js +336 -83
- package/dist/index.js.map +1 -1
- package/package.json +37 -5
package/README.md
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
A ready, themeable conversation with your Vinaverse agent for React Native and Expo apps. Drop it into a screen,
|
|
4
4
|
give it a conversation whose session comes from your backend, and it draws everything the realtime protocol defines:
|
|
5
|
-
streaming replies, cards (people, services, bookings, questions, proposals, next steps
|
|
5
|
+
streaming replies, cards (photo cards for people and places, services, bookings, questions, proposals, next steps,
|
|
6
|
+
your own help pages), held and unsent messages, a
|
|
6
7
|
person from your business taking over, and every way a session ends. No native module; New Architecture; FlashList 2.3.2 or later.
|
|
7
8
|
|
|
8
9
|
```tsx
|
|
9
10
|
import { createConversation } from '@vinaverse/client/conversation';
|
|
10
|
-
import { VinaverseChat } from '@vinaverse/react-native';
|
|
11
|
+
import { CardCarousel, VinaverseChat } from '@vinaverse/react-native';
|
|
11
12
|
|
|
12
13
|
// Outside the screen: it connects early and survives navigation.
|
|
13
14
|
const conversation = createConversation({
|
|
@@ -15,6 +16,8 @@ const conversation = createConversation({
|
|
|
15
16
|
locale: 'es-US',
|
|
16
17
|
deepLinkPrefixes: ['yourapp://bookings/'],
|
|
17
18
|
});
|
|
19
|
+
// Opt-in: a turn's listing cards in one swipeable row (without it they stack). Module level: a stable object.
|
|
20
|
+
const components = { CardGroup: CardCarousel };
|
|
18
21
|
|
|
19
22
|
export function AgentScreen() {
|
|
20
23
|
return (
|
|
@@ -22,6 +25,7 @@ export function AgentScreen() {
|
|
|
22
25
|
conversation={conversation}
|
|
23
26
|
locale="es-US"
|
|
24
27
|
theme={{ light: { primary: '#7A2E8E', personBubble: '#7A2E8E' } }}
|
|
28
|
+
components={components}
|
|
25
29
|
onProposal={(card, decision) => api.decideProposal(card, decision)} // your backend, the person's token
|
|
26
30
|
onOpenLink={(link) => router.push(link)}
|
|
27
31
|
/>
|
|
@@ -65,6 +69,12 @@ kit share one copy of `@vinaverse/client`.
|
|
|
65
69
|
- **Background**: the kit tells the conversation when the app goes to the background and comes back (`AppState`).
|
|
66
70
|
By default the connection stays, without pings; `createConversation({ closeInBackground: true, refreshSecret })`
|
|
67
71
|
closes it in the background and opens it again, with a fresh secret, in the foreground. Nothing typed is lost.
|
|
72
|
+
- **A session's end**: pass `expires_at` on from the session your backend created. Past it (the tenant's tool token
|
|
73
|
+
with it), the person's next message gets a new session for the same conversation from `getSession`; the old one is
|
|
74
|
+
never reopened with a fresh secret. A reconnect keeps everything drawn: the session's transcript merges by id.
|
|
75
|
+
- **Modes**: a customer and a professional each get their own conversation, `createConversationsByMode`, and the
|
|
76
|
+
screen draws `conversations.select(mode)`. Each mode keeps its own history on the device, and `getSession` gets the
|
|
77
|
+
`mode` so your backend creates the session for that mode's agent.
|
|
68
78
|
- **Language**: the agent answers in `subject.locale`, which your backend sets when it creates the session; the kit's
|
|
69
79
|
`locale` prop is only for its own words, dates and money (and is passed to `getSession` as a hint you may ignore).
|
|
70
80
|
|
|
@@ -103,6 +113,26 @@ part as `Default`. With neither, the kit draws both as before.
|
|
|
103
113
|
| `retry` | try again, when it can be: the person's text once more, for a turn the server allows to retry (once); a new session for `unavailable`. Undefined otherwise |
|
|
104
114
|
| `conversationId` | the conversation, once a session named it: for your support records |
|
|
105
115
|
|
|
116
|
+
## Listings, carousels, links and the welcome
|
|
117
|
+
|
|
118
|
+
- **Professionals and spaces** draw as photo cards: the hero photo (through your `Image` slot, so `expo-image`'s cache
|
|
119
|
+
applies), the name's monogram when there is none or it fails, rating with its count, "From $45" and the next open
|
|
120
|
+
time.
|
|
121
|
+
- **A carousel** for the cards a turn draws together: add the kit's `CardCarousel`. Without it they draw one under
|
|
122
|
+
another; it is opt-in so an app that does not use it does not ship it.
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
import { CardCarousel, VinaverseChat } from '@vinaverse/react-native';
|
|
126
|
+
const components = { Image: ExpoImage, CardGroup: CardCarousel }; // module level: a stable object
|
|
127
|
+
<VinaverseChat conversation={conversation} components={components} onOpenLink={openLink} />;
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- **Links**: when your scope keeps the agent from answering, it shows your own help, support, policy, contact or
|
|
131
|
+
feedback pages. Each opens through `onOpenLink` (an https page too: your in-app browser), a deep link only when it
|
|
132
|
+
passes your deep-link prefixes.
|
|
133
|
+
- **The welcome**: pass `welcome` on from the session your backend created; the kit greets an empty conversation with
|
|
134
|
+
it and its chips (no model call). A chip sends its message as the person's own. An `EmptyState` slot gets both.
|
|
135
|
+
|
|
106
136
|
## The keyboard
|
|
107
137
|
|
|
108
138
|
`keyboardBehavior` keeps the composer above the keyboard: `padding` on iOS by default, nothing on Android (the window
|
package/dist/index.cjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var flashList = require('@shopify/flash-list');
|
|
4
|
+
var react$1 = require('@vinaverse/react');
|
|
3
5
|
var reactNative = require('react-native');
|
|
4
6
|
var react = require('react');
|
|
5
7
|
var jsxRuntime = require('react/jsx-runtime');
|
|
6
8
|
var tokens = require('@vinaverse/tokens');
|
|
7
|
-
var react$1 = require('@vinaverse/react');
|
|
8
9
|
var conversation = require('@vinaverse/client/conversation');
|
|
9
|
-
var flashList = require('@shopify/flash-list');
|
|
10
10
|
|
|
11
|
-
// src/cards/
|
|
11
|
+
// src/cards/CardCarousel.tsx
|
|
12
12
|
var ChatContext = react.createContext(null);
|
|
13
13
|
function ChatContextProvider(props) {
|
|
14
14
|
return /* @__PURE__ */ jsxRuntime.jsx(ChatContext.Provider, { value: props.value, children: props.children });
|
|
@@ -57,6 +57,61 @@ function ThemeProvider(props) {
|
|
|
57
57
|
function useTheme() {
|
|
58
58
|
return react.useContext(ThemeContext);
|
|
59
59
|
}
|
|
60
|
+
var Button = react.memo(function Button2(props) {
|
|
61
|
+
const { colors: colors2, radius: radius2, space: space2, size: size3, font: font2 } = useTheme();
|
|
62
|
+
const variant = props.variant ?? "secondary";
|
|
63
|
+
const inactive = props.disabled === true || props.busy === true;
|
|
64
|
+
const filled = variant === "primary" || props.selected === true;
|
|
65
|
+
const background = filled ? colors2.primary : variant === "secondary" ? colors2.surface : "transparent";
|
|
66
|
+
const foreground = filled ? colors2.onPrimary : colors2.primary;
|
|
67
|
+
const checkable = props.role === "radio" || props.role === "checkbox";
|
|
68
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
69
|
+
reactNative.Pressable,
|
|
70
|
+
{
|
|
71
|
+
accessibilityRole: props.role ?? "button",
|
|
72
|
+
accessibilityLabel: props.label,
|
|
73
|
+
...props.hint ? { accessibilityHint: props.hint } : {},
|
|
74
|
+
accessibilityState: {
|
|
75
|
+
disabled: inactive,
|
|
76
|
+
busy: props.busy === true,
|
|
77
|
+
...checkable ? props.role === "radio" ? { selected: props.selected === true } : { checked: props.selected === true } : {}
|
|
78
|
+
},
|
|
79
|
+
disabled: inactive,
|
|
80
|
+
onPress: props.onPress,
|
|
81
|
+
...props.testID ? { testID: props.testID } : {},
|
|
82
|
+
style: ({ pressed }) => ({
|
|
83
|
+
minHeight: size3.touchTarget,
|
|
84
|
+
minWidth: size3.touchTarget,
|
|
85
|
+
paddingHorizontal: space2.md,
|
|
86
|
+
paddingVertical: space2.sm,
|
|
87
|
+
borderRadius: radius2.md,
|
|
88
|
+
borderWidth: variant === "text" ? 0 : 1,
|
|
89
|
+
borderColor: filled ? colors2.primary : colors2.border,
|
|
90
|
+
backgroundColor: background,
|
|
91
|
+
opacity: inactive ? 0.55 : pressed ? 0.8 : 1,
|
|
92
|
+
alignItems: "center",
|
|
93
|
+
justifyContent: "center",
|
|
94
|
+
flexDirection: "row",
|
|
95
|
+
gap: space2.xs
|
|
96
|
+
}),
|
|
97
|
+
children: [
|
|
98
|
+
props.busy ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: foreground }) : null,
|
|
99
|
+
props.children ?? /* @__PURE__ */ jsxRuntime.jsx(
|
|
100
|
+
reactNative.Text,
|
|
101
|
+
{
|
|
102
|
+
style: {
|
|
103
|
+
color: foreground,
|
|
104
|
+
fontSize: font2.size.md,
|
|
105
|
+
fontWeight: "500",
|
|
106
|
+
...font2.family ? { fontFamily: font2.family } : {}
|
|
107
|
+
},
|
|
108
|
+
children: props.label
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
});
|
|
60
115
|
function cardOf(held) {
|
|
61
116
|
return held.card;
|
|
62
117
|
}
|
|
@@ -125,64 +180,20 @@ function useLink(link, held) {
|
|
|
125
180
|
if (!link || !store.checkLink(link)) return null;
|
|
126
181
|
return () => handlers.current?.onOpenLink?.(link, held);
|
|
127
182
|
}
|
|
183
|
+
function ratingText(valuePpm, locale) {
|
|
184
|
+
const value = valuePpm / 1e6;
|
|
185
|
+
try {
|
|
186
|
+
return new Intl.NumberFormat(locale, {
|
|
187
|
+
minimumFractionDigits: 1,
|
|
188
|
+
maximumFractionDigits: 1
|
|
189
|
+
}).format(value);
|
|
190
|
+
} catch {
|
|
191
|
+
return value.toFixed(1);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
128
194
|
function safeImage(url) {
|
|
129
195
|
return url && /^https:\/\//i.test(url) ? url : null;
|
|
130
196
|
}
|
|
131
|
-
var Button = react.memo(function Button2(props) {
|
|
132
|
-
const { colors: colors2, radius: radius2, space: space2, size: size3, font: font2 } = useTheme();
|
|
133
|
-
const variant = props.variant ?? "secondary";
|
|
134
|
-
const inactive = props.disabled === true || props.busy === true;
|
|
135
|
-
const filled = variant === "primary" || props.selected === true;
|
|
136
|
-
const background = filled ? colors2.primary : variant === "secondary" ? colors2.surface : "transparent";
|
|
137
|
-
const foreground = filled ? colors2.onPrimary : colors2.primary;
|
|
138
|
-
const checkable = props.role === "radio" || props.role === "checkbox";
|
|
139
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
140
|
-
reactNative.Pressable,
|
|
141
|
-
{
|
|
142
|
-
accessibilityRole: props.role ?? "button",
|
|
143
|
-
accessibilityLabel: props.label,
|
|
144
|
-
...props.hint ? { accessibilityHint: props.hint } : {},
|
|
145
|
-
accessibilityState: {
|
|
146
|
-
disabled: inactive,
|
|
147
|
-
busy: props.busy === true,
|
|
148
|
-
...checkable ? props.role === "radio" ? { selected: props.selected === true } : { checked: props.selected === true } : {}
|
|
149
|
-
},
|
|
150
|
-
disabled: inactive,
|
|
151
|
-
onPress: props.onPress,
|
|
152
|
-
...props.testID ? { testID: props.testID } : {},
|
|
153
|
-
style: ({ pressed }) => ({
|
|
154
|
-
minHeight: size3.touchTarget,
|
|
155
|
-
minWidth: size3.touchTarget,
|
|
156
|
-
paddingHorizontal: space2.md,
|
|
157
|
-
paddingVertical: space2.sm,
|
|
158
|
-
borderRadius: radius2.md,
|
|
159
|
-
borderWidth: variant === "text" ? 0 : 1,
|
|
160
|
-
borderColor: filled ? colors2.primary : colors2.border,
|
|
161
|
-
backgroundColor: background,
|
|
162
|
-
opacity: inactive ? 0.55 : pressed ? 0.8 : 1,
|
|
163
|
-
alignItems: "center",
|
|
164
|
-
justifyContent: "center",
|
|
165
|
-
flexDirection: "row",
|
|
166
|
-
gap: space2.xs
|
|
167
|
-
}),
|
|
168
|
-
children: [
|
|
169
|
-
props.busy ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: foreground }) : null,
|
|
170
|
-
props.children ?? /* @__PURE__ */ jsxRuntime.jsx(
|
|
171
|
-
reactNative.Text,
|
|
172
|
-
{
|
|
173
|
-
style: {
|
|
174
|
-
color: foreground,
|
|
175
|
-
fontSize: font2.size.md,
|
|
176
|
-
fontWeight: "500",
|
|
177
|
-
...font2.family ? { fontFamily: font2.family } : {}
|
|
178
|
-
},
|
|
179
|
-
children: props.label
|
|
180
|
-
}
|
|
181
|
-
)
|
|
182
|
-
]
|
|
183
|
-
}
|
|
184
|
-
);
|
|
185
|
-
});
|
|
186
197
|
function InfoCard(props) {
|
|
187
198
|
const card = cardOf(props.card);
|
|
188
199
|
const { locale, t } = react$1.useCopy();
|
|
@@ -191,7 +202,7 @@ function InfoCard(props) {
|
|
|
191
202
|
const image = safeImage(card.image_url);
|
|
192
203
|
const Img = slots.Image ?? reactNative.Image;
|
|
193
204
|
const money = card.price ?? card.price_from;
|
|
194
|
-
const rating = card.rating ? (card.rating.value_ppm
|
|
205
|
+
const rating = card.rating ? ratingText(card.rating.value_ppm, locale) : null;
|
|
195
206
|
const amount = money ? react$1.formatMoney(money.amount, money.currency, locale) : null;
|
|
196
207
|
const price = amount && !card.price ? t("priceFrom", { price: amount }) : amount;
|
|
197
208
|
const when = card.starts_at ? react$1.formatTimeRange(card.starts_at, void 0, locale, timeZone) : null;
|
|
@@ -234,27 +245,32 @@ function InfoCard(props) {
|
|
|
234
245
|
] })
|
|
235
246
|
] }),
|
|
236
247
|
card.body ? /* @__PURE__ */ jsxRuntime.jsx(Body, { children: card.body }) : null,
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
{
|
|
240
|
-
style: {
|
|
241
|
-
color: colors2.text,
|
|
242
|
-
backgroundColor: colors2.surface,
|
|
243
|
-
borderRadius: radius2.pill,
|
|
244
|
-
paddingHorizontal: space2.sm,
|
|
245
|
-
paddingVertical: space2.xxs,
|
|
246
|
-
fontSize: font2.size.xs
|
|
247
|
-
},
|
|
248
|
-
children: b
|
|
249
|
-
},
|
|
250
|
-
b
|
|
251
|
-
)) }) : null,
|
|
252
|
-
props.restored ? null : /* @__PURE__ */ jsxRuntime.jsx(Actions, { ...props })
|
|
248
|
+
/* @__PURE__ */ jsxRuntime.jsx(Badges, { badges: card.badges }),
|
|
249
|
+
props.restored ? null : /* @__PURE__ */ jsxRuntime.jsx(CardActions, { ...props })
|
|
253
250
|
]
|
|
254
251
|
}
|
|
255
252
|
);
|
|
256
253
|
}
|
|
257
|
-
function
|
|
254
|
+
function Badges(props) {
|
|
255
|
+
const { colors: colors2, radius: radius2, space: space2, font: font2 } = useTheme();
|
|
256
|
+
if (!props.badges?.length) return null;
|
|
257
|
+
return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { flexDirection: "row", flexWrap: "wrap", gap: space2.xs }, children: props.badges.map((b) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
258
|
+
reactNative.Text,
|
|
259
|
+
{
|
|
260
|
+
style: {
|
|
261
|
+
color: colors2.text,
|
|
262
|
+
backgroundColor: colors2.surface,
|
|
263
|
+
borderRadius: radius2.pill,
|
|
264
|
+
paddingHorizontal: space2.sm,
|
|
265
|
+
paddingVertical: space2.xxs,
|
|
266
|
+
fontSize: font2.size.xs
|
|
267
|
+
},
|
|
268
|
+
children: b
|
|
269
|
+
},
|
|
270
|
+
b
|
|
271
|
+
)) });
|
|
272
|
+
}
|
|
273
|
+
function CardActions(props) {
|
|
258
274
|
const card = cardOf(props.card);
|
|
259
275
|
const { space: space2 } = useTheme();
|
|
260
276
|
if (!card.actions?.length) return null;
|
|
@@ -286,6 +302,120 @@ function Action(props) {
|
|
|
286
302
|
}
|
|
287
303
|
);
|
|
288
304
|
}
|
|
305
|
+
function LinksCard(props) {
|
|
306
|
+
const card = cardOf(props.card);
|
|
307
|
+
const { store, handlers } = useChat();
|
|
308
|
+
const links = (card.links ?? []).filter(
|
|
309
|
+
(l) => /^https:\/\//i.test(l.url) || store.checkLink(l.url)
|
|
310
|
+
);
|
|
311
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
312
|
+
CardFrame,
|
|
313
|
+
{
|
|
314
|
+
label: [card.title, ...links.map((l) => l.label)].join(", "),
|
|
315
|
+
restored: props.restored,
|
|
316
|
+
testID: `vinaverse-card-${card.id}`,
|
|
317
|
+
children: [
|
|
318
|
+
/* @__PURE__ */ jsxRuntime.jsx(Title, { children: card.title }),
|
|
319
|
+
links.map((l) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
320
|
+
Button,
|
|
321
|
+
{
|
|
322
|
+
label: l.label,
|
|
323
|
+
role: "link",
|
|
324
|
+
variant: "secondary",
|
|
325
|
+
disabled: props.restored,
|
|
326
|
+
onPress: () => {
|
|
327
|
+
const open = handlers.current?.onOpenLink;
|
|
328
|
+
if (open) open(l.url, props.card);
|
|
329
|
+
else void reactNative.Linking.openURL(l.url);
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
l.url
|
|
333
|
+
))
|
|
334
|
+
]
|
|
335
|
+
}
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
function ListingCard(props) {
|
|
339
|
+
const card = cardOf(props.card);
|
|
340
|
+
const { locale, t } = react$1.useCopy();
|
|
341
|
+
const { slots, timeZone } = useChat();
|
|
342
|
+
const { colors: colors2, radius: radius2, space: space2, font: font2 } = useTheme();
|
|
343
|
+
const [failed, setFailed] = react.useState(null);
|
|
344
|
+
const photoUrl = safeImage(card.images?.[0]?.url ?? card.image_url);
|
|
345
|
+
const hero = photoUrl === failed ? null : photoUrl;
|
|
346
|
+
const Img = slots.Image ?? reactNative.Image;
|
|
347
|
+
const rating = card.rating ? `\u2605 ${ratingText(card.rating.value_ppm, locale)} (${card.rating.count})` : null;
|
|
348
|
+
const price = card.price_from ? t("priceFrom", {
|
|
349
|
+
price: react$1.formatMoney(
|
|
350
|
+
card.price_from.amount,
|
|
351
|
+
card.price_from.currency,
|
|
352
|
+
locale
|
|
353
|
+
)
|
|
354
|
+
}) : null;
|
|
355
|
+
const next = card.next_available ? t("nextAvailable", {
|
|
356
|
+
time: react$1.formatTimeRange(card.next_available, void 0, locale, timeZone)
|
|
357
|
+
}) : null;
|
|
358
|
+
const photo = {
|
|
359
|
+
width: "100%",
|
|
360
|
+
aspectRatio: 1.6,
|
|
361
|
+
borderRadius: radius2.md
|
|
362
|
+
};
|
|
363
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
364
|
+
CardFrame,
|
|
365
|
+
{
|
|
366
|
+
label: [card.title, card.subtitle, rating, price, next].filter(Boolean).join(", "),
|
|
367
|
+
restored: props.restored,
|
|
368
|
+
testID: `vinaverse-card-${card.id}`,
|
|
369
|
+
children: [
|
|
370
|
+
hero ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
371
|
+
Img,
|
|
372
|
+
{
|
|
373
|
+
source: { uri: hero },
|
|
374
|
+
onError: () => setFailed(hero),
|
|
375
|
+
style: { ...photo, backgroundColor: colors2.surface },
|
|
376
|
+
accessible: false,
|
|
377
|
+
testID: `vinaverse-photo-${card.id}`
|
|
378
|
+
}
|
|
379
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
380
|
+
reactNative.View,
|
|
381
|
+
{
|
|
382
|
+
accessible: false,
|
|
383
|
+
importantForAccessibility: "no-hide-descendants",
|
|
384
|
+
testID: `vinaverse-monogram-${card.id}`,
|
|
385
|
+
style: {
|
|
386
|
+
...photo,
|
|
387
|
+
backgroundColor: colors2.surface,
|
|
388
|
+
alignItems: "center",
|
|
389
|
+
justifyContent: "center"
|
|
390
|
+
},
|
|
391
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
392
|
+
reactNative.Text,
|
|
393
|
+
{
|
|
394
|
+
style: {
|
|
395
|
+
color: colors2.textMuted,
|
|
396
|
+
fontSize: font2.size.xl,
|
|
397
|
+
fontWeight: "600"
|
|
398
|
+
},
|
|
399
|
+
children: monogram(card.title)
|
|
400
|
+
}
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
),
|
|
404
|
+
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { gap: space2.xxs }, children: [
|
|
405
|
+
/* @__PURE__ */ jsxRuntime.jsx(Title, { numberOfLines: 2, children: card.title }),
|
|
406
|
+
card.subtitle ? /* @__PURE__ */ jsxRuntime.jsx(Body, { muted: true, children: card.subtitle }) : null,
|
|
407
|
+
rating || price ? /* @__PURE__ */ jsxRuntime.jsx(Body, { muted: true, children: [rating, price].filter(Boolean).join(" \xB7 ") }) : null
|
|
408
|
+
] }),
|
|
409
|
+
card.body ? /* @__PURE__ */ jsxRuntime.jsx(Body, { children: card.body }) : null,
|
|
410
|
+
/* @__PURE__ */ jsxRuntime.jsx(Badges, { badges: next ? [next, ...card.badges ?? []] : card.badges }),
|
|
411
|
+
props.restored ? null : /* @__PURE__ */ jsxRuntime.jsx(CardActions, { ...props })
|
|
412
|
+
]
|
|
413
|
+
}
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
function monogram(title) {
|
|
417
|
+
return title.split(/\s+/).filter(Boolean).slice(0, 2).map((w) => [...w][0]?.toUpperCase() ?? "").join("");
|
|
418
|
+
}
|
|
289
419
|
function NextStepCard(props) {
|
|
290
420
|
const card = cardOf(props.card);
|
|
291
421
|
const action = card.actions?.[0];
|
|
@@ -1154,7 +1284,10 @@ var DEFAULTS = {
|
|
|
1154
1284
|
proposal: ProposalCard,
|
|
1155
1285
|
question: QuestionCard,
|
|
1156
1286
|
next_step: NextStepCard,
|
|
1157
|
-
time_slots: TimeSlotsCard
|
|
1287
|
+
time_slots: TimeSlotsCard,
|
|
1288
|
+
professional: ListingCard,
|
|
1289
|
+
workspace: ListingCard,
|
|
1290
|
+
links: LinksCard
|
|
1158
1291
|
};
|
|
1159
1292
|
function defaultCard(kind) {
|
|
1160
1293
|
return DEFAULTS[kind] ?? InfoCard;
|
|
@@ -1167,6 +1300,35 @@ var CardView = react.memo(function CardView2(props) {
|
|
|
1167
1300
|
const own = { card, busy, restored: props.item.restored === true };
|
|
1168
1301
|
return Custom ? /* @__PURE__ */ jsxRuntime.jsx(Custom, { ...own, Default }) : /* @__PURE__ */ jsxRuntime.jsx(Default, { ...own });
|
|
1169
1302
|
});
|
|
1303
|
+
var keyOf = (item) => item.key;
|
|
1304
|
+
var kindOf = (item) => item.card.kind;
|
|
1305
|
+
function CardCarousel(props) {
|
|
1306
|
+
const { width } = reactNative.useWindowDimensions();
|
|
1307
|
+
const { t } = react$1.useCopy();
|
|
1308
|
+
const size3 = Math.min(Math.round(width * 0.8), 360);
|
|
1309
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1310
|
+
reactNative.View,
|
|
1311
|
+
{
|
|
1312
|
+
accessibilityRole: "list",
|
|
1313
|
+
accessibilityLabel: t("cardsInRow", {
|
|
1314
|
+
count: String(props.items.length)
|
|
1315
|
+
}),
|
|
1316
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1317
|
+
flashList.FlashList,
|
|
1318
|
+
{
|
|
1319
|
+
horizontal: true,
|
|
1320
|
+
data: props.items,
|
|
1321
|
+
keyExtractor: keyOf,
|
|
1322
|
+
getItemType: kindOf,
|
|
1323
|
+
renderItem: ({ item }) => /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { width: size3 }, children: /* @__PURE__ */ jsxRuntime.jsx(CardView, { item }) }),
|
|
1324
|
+
snapToInterval: size3,
|
|
1325
|
+
decelerationRate: "fast",
|
|
1326
|
+
showsHorizontalScrollIndicator: false
|
|
1327
|
+
}
|
|
1328
|
+
)
|
|
1329
|
+
}
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1170
1332
|
var ActivityLine = react.memo(function ActivityLine2() {
|
|
1171
1333
|
const { store } = useChat();
|
|
1172
1334
|
const { t } = react$1.useCopy();
|
|
@@ -1724,7 +1886,44 @@ function Line(props) {
|
|
|
1724
1886
|
}
|
|
1725
1887
|
);
|
|
1726
1888
|
}
|
|
1727
|
-
var
|
|
1889
|
+
var Welcome = react.memo(function Welcome2(props) {
|
|
1890
|
+
const { colors: colors2, radius: radius2, space: space2, font: font2 } = useTheme();
|
|
1891
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1892
|
+
reactNative.View,
|
|
1893
|
+
{
|
|
1894
|
+
style: { padding: space2.lg, gap: space2.md },
|
|
1895
|
+
testID: "vinaverse-welcome",
|
|
1896
|
+
children: [
|
|
1897
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1898
|
+
reactNative.Text,
|
|
1899
|
+
{
|
|
1900
|
+
style: {
|
|
1901
|
+
alignSelf: "flex-start",
|
|
1902
|
+
maxWidth: "85%",
|
|
1903
|
+
color: colors2.agentText,
|
|
1904
|
+
backgroundColor: colors2.agentBubble,
|
|
1905
|
+
borderRadius: radius2.lg,
|
|
1906
|
+
paddingHorizontal: space2.md,
|
|
1907
|
+
paddingVertical: space2.sm,
|
|
1908
|
+
fontSize: font2.size.md,
|
|
1909
|
+
lineHeight: font2.size.md * font2.lineHeight
|
|
1910
|
+
},
|
|
1911
|
+
children: props.welcome.text
|
|
1912
|
+
}
|
|
1913
|
+
),
|
|
1914
|
+
props.welcome.chips.length ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { flexDirection: "row", flexWrap: "wrap", gap: space2.sm }, children: props.welcome.chips.map((chip) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1915
|
+
Button,
|
|
1916
|
+
{
|
|
1917
|
+
label: chip.label,
|
|
1918
|
+
onPress: () => props.onChip(chip.message)
|
|
1919
|
+
},
|
|
1920
|
+
chip.label
|
|
1921
|
+
)) }) : null
|
|
1922
|
+
]
|
|
1923
|
+
}
|
|
1924
|
+
);
|
|
1925
|
+
});
|
|
1926
|
+
var keyOf2 = (item) => item.key;
|
|
1728
1927
|
function itemType(item) {
|
|
1729
1928
|
if (item.type === "card") return `card:${item.card.kind}`;
|
|
1730
1929
|
if (item.type === "message")
|
|
@@ -1734,8 +1933,39 @@ function itemType(item) {
|
|
|
1734
1933
|
function authorOf(item) {
|
|
1735
1934
|
return item?.type === "message" ? item.role : null;
|
|
1736
1935
|
}
|
|
1936
|
+
function carousels(items, previous) {
|
|
1937
|
+
const runs = /* @__PURE__ */ new Map();
|
|
1938
|
+
const inside = /* @__PURE__ */ new Set();
|
|
1939
|
+
const groupOf = (item) => {
|
|
1940
|
+
if (item?.type !== "card") return null;
|
|
1941
|
+
const group = item.card.card.group;
|
|
1942
|
+
return group?.layout === "carousel" && typeof group.id === "string" ? group.id : null;
|
|
1943
|
+
};
|
|
1944
|
+
for (let i = 0; i < items.length; i++) {
|
|
1945
|
+
const id = groupOf(items[i]);
|
|
1946
|
+
if (id === null) continue;
|
|
1947
|
+
let end = i + 1;
|
|
1948
|
+
while (end < items.length && groupOf(items[end]) === id) end++;
|
|
1949
|
+
if (end - i > 1) {
|
|
1950
|
+
const run = items.slice(i, end);
|
|
1951
|
+
const key = run[0]?.key;
|
|
1952
|
+
const before = previous.get(key);
|
|
1953
|
+
runs.set(
|
|
1954
|
+
key,
|
|
1955
|
+
before && before.length === run.length && before.every((c, n) => c === run[n]) ? before : run
|
|
1956
|
+
);
|
|
1957
|
+
for (const card of run.slice(1)) inside.add(card.key);
|
|
1958
|
+
}
|
|
1959
|
+
i = end - 1;
|
|
1960
|
+
}
|
|
1961
|
+
return { runs, inside };
|
|
1962
|
+
}
|
|
1737
1963
|
var Row = react.memo(function Row2(props) {
|
|
1738
1964
|
const { item } = props;
|
|
1965
|
+
const { slots } = useChat();
|
|
1966
|
+
const Group = slots.CardGroup;
|
|
1967
|
+
if (props.inside) return null;
|
|
1968
|
+
if (props.run && Group) return /* @__PURE__ */ jsxRuntime.jsx(Group, { items: props.run });
|
|
1739
1969
|
if (item.type === "card") return /* @__PURE__ */ jsxRuntime.jsx(CardView, { item });
|
|
1740
1970
|
if (item.type === "system") return /* @__PURE__ */ jsxRuntime.jsx(SystemRow, { item });
|
|
1741
1971
|
return item.state === "streaming" ? /* @__PURE__ */ jsxRuntime.jsx(StreamingBubble, { item, showAuthor: props.showAuthor }) : /* @__PURE__ */ jsxRuntime.jsx(MessageBubble, { item, showAuthor: props.showAuthor });
|
|
@@ -1751,26 +1981,49 @@ var MessageList = react.memo(function MessageList2() {
|
|
|
1751
1981
|
(v) => v.status === "starting" || v.status === "connecting"
|
|
1752
1982
|
);
|
|
1753
1983
|
const agentName = react$1.useConversationSelector(store, (v) => v.agent.name);
|
|
1984
|
+
const welcome = react$1.useConversationSelector(store, (v) => v.welcome);
|
|
1754
1985
|
useAnnounceReplies(items);
|
|
1986
|
+
const Group = slots.CardGroup;
|
|
1987
|
+
const runsRef = react.useRef(/* @__PURE__ */ new Map());
|
|
1988
|
+
const grouping = react.useMemo(
|
|
1989
|
+
() => Group ? carousels(items, runsRef.current) : null,
|
|
1990
|
+
[items, Group]
|
|
1991
|
+
);
|
|
1992
|
+
runsRef.current = grouping?.runs ?? /* @__PURE__ */ new Map();
|
|
1755
1993
|
const itemsRef = react.useRef(items);
|
|
1756
1994
|
itemsRef.current = items;
|
|
1995
|
+
const groupingRef = react.useRef(grouping);
|
|
1996
|
+
groupingRef.current = grouping;
|
|
1757
1997
|
const renderItem = react.useCallback(
|
|
1758
1998
|
({ item, index }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1759
1999
|
Row,
|
|
1760
2000
|
{
|
|
1761
2001
|
item,
|
|
1762
|
-
showAuthor: authorOf(item) !== authorOf(itemsRef.current[index - 1])
|
|
2002
|
+
showAuthor: authorOf(item) !== authorOf(itemsRef.current[index - 1]),
|
|
2003
|
+
run: groupingRef.current?.runs.get(item.key),
|
|
2004
|
+
inside: groupingRef.current?.inside.has(item.key) === true
|
|
1763
2005
|
}
|
|
1764
2006
|
),
|
|
1765
2007
|
[]
|
|
1766
2008
|
);
|
|
2009
|
+
const onChip = react.useCallback(
|
|
2010
|
+
(message) => void store.send(message),
|
|
2011
|
+
[store]
|
|
2012
|
+
);
|
|
1767
2013
|
const Empty = slots.EmptyState;
|
|
1768
|
-
const empty = opening ? /* @__PURE__ */ jsxRuntime.jsx(ChatSkeleton, {}) : Empty ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
2014
|
+
const empty = opening ? /* @__PURE__ */ jsxRuntime.jsx(ChatSkeleton, {}) : Empty ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
2015
|
+
Empty,
|
|
2016
|
+
{
|
|
2017
|
+
agentName: agentName ?? t("agentFallbackName"),
|
|
2018
|
+
welcome,
|
|
2019
|
+
onChip
|
|
2020
|
+
}
|
|
2021
|
+
) : welcome ? /* @__PURE__ */ jsxRuntime.jsx(Welcome, { welcome, onChip }) : null;
|
|
1769
2022
|
const props = react.useMemo(
|
|
1770
2023
|
() => ({
|
|
1771
2024
|
data: items,
|
|
1772
2025
|
renderItem,
|
|
1773
|
-
keyExtractor:
|
|
2026
|
+
keyExtractor: keyOf2,
|
|
1774
2027
|
getItemType: itemType,
|
|
1775
2028
|
maintainVisibleContentPosition: {
|
|
1776
2029
|
startRenderingFromBottom: true,
|
|
@@ -2003,6 +2256,8 @@ function sameSlots(a, b) {
|
|
|
2003
2256
|
exports.ActivityLine = ActivityLine;
|
|
2004
2257
|
exports.Avatar = Avatar;
|
|
2005
2258
|
exports.Button = Button;
|
|
2259
|
+
exports.CardActions = CardActions;
|
|
2260
|
+
exports.CardCarousel = CardCarousel;
|
|
2006
2261
|
exports.CardFrame = CardFrame;
|
|
2007
2262
|
exports.CardView = CardView;
|
|
2008
2263
|
exports.ChatHeader = ChatHeader;
|
|
@@ -2016,6 +2271,8 @@ exports.DefaultUnavailable = DefaultUnavailable;
|
|
|
2016
2271
|
exports.DisclosureBar = DisclosureBar;
|
|
2017
2272
|
exports.FailureActionButton = FailureActionButton;
|
|
2018
2273
|
exports.InfoCard = InfoCard;
|
|
2274
|
+
exports.LinksCard = LinksCard;
|
|
2275
|
+
exports.ListingCard = ListingCard;
|
|
2019
2276
|
exports.MessageBubble = MessageBubble;
|
|
2020
2277
|
exports.MessageList = MessageList;
|
|
2021
2278
|
exports.NextStepCard = NextStepCard;
|
|
@@ -2029,12 +2286,16 @@ exports.ThemeProvider = ThemeProvider;
|
|
|
2029
2286
|
exports.TimeSlotsCard = TimeSlotsCard;
|
|
2030
2287
|
exports.VinaverseChat = VinaverseChat;
|
|
2031
2288
|
exports.VinaverseChatProvider = VinaverseChatProvider;
|
|
2289
|
+
exports.Welcome = Welcome;
|
|
2032
2290
|
exports.announce = announce;
|
|
2033
2291
|
exports.answerText = answerText;
|
|
2292
|
+
exports.carousels = carousels;
|
|
2034
2293
|
exports.createTheme = createTheme;
|
|
2035
2294
|
exports.defaultCard = defaultCard;
|
|
2036
2295
|
exports.fieldText = fieldText;
|
|
2037
2296
|
exports.itemType = itemType;
|
|
2297
|
+
exports.monogram = monogram;
|
|
2298
|
+
exports.ratingText = ratingText;
|
|
2038
2299
|
exports.themeProblems = themeProblems;
|
|
2039
2300
|
exports.useChat = useChat;
|
|
2040
2301
|
exports.useNativeLifecycle = useNativeLifecycle;
|