vdb-ai-chat 1.0.78 → 1.0.80

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.
Files changed (79) hide show
  1. package/dist/chat-widget.js +189 -164
  2. package/dist/chat-widget.js.LICENSE.txt +2 -0
  3. package/lib/commonjs/api.js +84 -4
  4. package/lib/commonjs/api.js.map +1 -1
  5. package/lib/commonjs/components/ChatInput.js +227 -65
  6. package/lib/commonjs/components/ChatInput.js.map +1 -1
  7. package/lib/commonjs/components/ChatWidget.js +6 -2
  8. package/lib/commonjs/components/ChatWidget.js.map +1 -1
  9. package/lib/commonjs/components/MessageBubble.js +22 -2
  10. package/lib/commonjs/components/MessageBubble.js.map +1 -1
  11. package/lib/commonjs/components/icons/VDBCustomIcon.js +54 -0
  12. package/lib/commonjs/components/icons/VDBCustomIcon.js.map +1 -0
  13. package/lib/commonjs/components/icons/VDBIcon.js +141 -0
  14. package/lib/commonjs/components/icons/VDBIcon.js.map +1 -0
  15. package/lib/commonjs/components/icons/VDBIcons.js +16 -0
  16. package/lib/commonjs/components/icons/VDBIcons.js.map +1 -0
  17. package/lib/commonjs/components/icons/index.js +34 -0
  18. package/lib/commonjs/components/icons/index.js.map +1 -0
  19. package/lib/commonjs/index.js +29 -0
  20. package/lib/commonjs/index.js.map +1 -1
  21. package/lib/commonjs/index.native.js +30 -1
  22. package/lib/commonjs/index.native.js.map +1 -1
  23. package/lib/commonjs/routes.js +1 -0
  24. package/lib/commonjs/routes.js.map +1 -1
  25. package/lib/module/api.js +81 -5
  26. package/lib/module/api.js.map +1 -1
  27. package/lib/module/components/ChatInput.js +228 -66
  28. package/lib/module/components/ChatInput.js.map +1 -1
  29. package/lib/module/components/ChatWidget.js +6 -2
  30. package/lib/module/components/ChatWidget.js.map +1 -1
  31. package/lib/module/components/MessageBubble.js +22 -2
  32. package/lib/module/components/MessageBubble.js.map +1 -1
  33. package/lib/module/components/icons/VDBCustomIcon.js +47 -0
  34. package/lib/module/components/icons/VDBCustomIcon.js.map +1 -0
  35. package/lib/module/components/icons/VDBIcon.js +134 -0
  36. package/lib/module/components/icons/VDBIcon.js.map +1 -0
  37. package/lib/module/components/icons/VDBIcons.js +10 -0
  38. package/lib/module/components/icons/VDBIcons.js.map +1 -0
  39. package/lib/module/components/icons/index.js +3 -0
  40. package/lib/module/components/icons/index.js.map +1 -0
  41. package/lib/module/index.js +1 -1
  42. package/lib/module/index.js.map +1 -1
  43. package/lib/module/index.native.js +1 -0
  44. package/lib/module/index.native.js.map +1 -1
  45. package/lib/module/routes.js +1 -0
  46. package/lib/module/routes.js.map +1 -1
  47. package/lib/typescript/api.d.ts +22 -0
  48. package/lib/typescript/api.d.ts.map +1 -1
  49. package/lib/typescript/components/ChatInput.d.ts +4 -0
  50. package/lib/typescript/components/ChatInput.d.ts.map +1 -1
  51. package/lib/typescript/components/ChatWidget.d.ts.map +1 -1
  52. package/lib/typescript/components/MessageBubble.d.ts +2 -0
  53. package/lib/typescript/components/MessageBubble.d.ts.map +1 -1
  54. package/lib/typescript/components/icons/VDBCustomIcon.d.ts +11 -0
  55. package/lib/typescript/components/icons/VDBCustomIcon.d.ts.map +1 -0
  56. package/lib/typescript/components/icons/VDBIcon.d.ts +34 -0
  57. package/lib/typescript/components/icons/VDBIcon.d.ts.map +1 -0
  58. package/lib/typescript/components/icons/VDBIcons.d.ts +9 -0
  59. package/lib/typescript/components/icons/VDBIcons.d.ts.map +1 -0
  60. package/lib/typescript/components/icons/index.d.ts +5 -0
  61. package/lib/typescript/components/icons/index.d.ts.map +1 -0
  62. package/lib/typescript/index.d.ts +2 -0
  63. package/lib/typescript/index.d.ts.map +1 -1
  64. package/lib/typescript/index.native.d.ts +2 -0
  65. package/lib/typescript/index.native.d.ts.map +1 -1
  66. package/lib/typescript/routes.d.ts +1 -0
  67. package/lib/typescript/routes.d.ts.map +1 -1
  68. package/package.json +18 -2
  69. package/src/api.ts +147 -3
  70. package/src/components/ChatInput.tsx +360 -83
  71. package/src/components/ChatWidget.tsx +9 -0
  72. package/src/components/MessageBubble.tsx +27 -1
  73. package/src/components/icons/VDBCustomIcon.tsx +74 -0
  74. package/src/components/icons/VDBIcon.tsx +186 -0
  75. package/src/components/icons/VDBIcons.ts +9 -0
  76. package/src/components/icons/index.ts +4 -0
  77. package/src/index.native.tsx +12 -0
  78. package/src/index.ts +12 -0
  79. package/src/routes.ts +1 -0
@@ -1,20 +1,29 @@
1
- import React from "react";
2
- import { TextInput, StyleSheet, Platform, Image } from "react-native";
1
+ import React, { useRef, useState, useCallback, useEffect } from "react";
2
+ import { TextInput, StyleSheet, Platform } from "react-native";
3
3
  import type { ChatTheme } from "../types";
4
4
  import { useTheme } from "styled-components/native";
5
5
  import { DefaultTheme } from "styled-components/native";
6
6
  import styled from "styled-components/native";
7
7
  import { useTranslation } from "../i18n";
8
+ import { VDBIcons } from "./icons/VDBIcons";
9
+ import { IconsVariant, VDBIcon } from "./icons";
10
+ import {
11
+ transcribeAudio,
12
+ TranscribeError,
13
+ type TranscribeAudioFile,
14
+ type TranscribeResponse,
15
+ } from "../api";
8
16
 
9
- // Use expo-image on native if available, fallback to RN Image
10
- let ImageComponent: typeof Image = Image;
11
- if (Platform.OS !== "web") {
12
- try {
13
- const ExpoImage = require("expo-image").Image;
14
- if (ExpoImage) ImageComponent = ExpoImage;
15
- } catch {
16
- ImageComponent = Image;
17
- }
17
+ const stripMimeParams = (m: string): string => {
18
+ if (!m) return m;
19
+ const i = m.indexOf(";");
20
+ return (i >= 0 ? m.slice(0, i) : m).trim();
21
+ };
22
+
23
+ let ExpoAudio: any = null;
24
+ try {
25
+ ExpoAudio = require("expo-av").Audio;
26
+ } catch {
18
27
  }
19
28
 
20
29
  interface Props {
@@ -25,34 +34,12 @@ interface Props {
25
34
  placeholder?: string;
26
35
  theme: ChatTheme;
27
36
  inputRef?: React.RefObject<TextInput>;
37
+ apiUrl?: string;
38
+ conversationId?: string | number;
39
+ priceMode?: string | null;
40
+ onTranscribeError?: (error: Error, message: string) => void;
28
41
  }
29
42
 
30
- const SendIcon = ({
31
- disabled,
32
- isEmpty,
33
- }: {
34
- disabled?: boolean;
35
- isEmpty: boolean;
36
- }) => {
37
- const isInactive = disabled || isEmpty;
38
- const theme = useTheme();
39
- return (
40
- <ImageComponent
41
- source={{
42
- uri: "https://cdn.vdbapp.com/ai/chat-widget/assets/img/send.svg",
43
- }}
44
- resizeMode="contain"
45
- style={{
46
- ...styles.buttonIcon,
47
- tintColor: isInactive
48
- ? theme["core-04"] || "#ACACB3"
49
- : theme["core-05"] || "#020001",
50
- cursor: isInactive ? "auto" : "pointer",
51
- }}
52
- />
53
- );
54
- };
55
-
56
43
  export const ChatInput: React.FC<Props> = ({
57
44
  value,
58
45
  onChangeText,
@@ -60,38 +47,326 @@ export const ChatInput: React.FC<Props> = ({
60
47
  disabled,
61
48
  placeholder,
62
49
  inputRef,
50
+ apiUrl,
51
+ conversationId,
52
+ priceMode,
53
+ onTranscribeError,
63
54
  }) => {
64
55
  const theme = useTheme();
65
56
  const { t } = useTranslation();
66
- return (
67
- <InputContainer theme={theme}>
68
- <Input
69
- ref={inputRef}
70
- // @ts-ignore - supported on web via react-native-web
71
- id="chat-input"
72
- style={[styles.inputWeb]}
73
- autoComplete="off"
74
- placeholder={
75
- placeholder ||
76
- t(
77
- "chat.placeholder.text",
78
- "Describe the diamond you're looking for...",
79
- )
57
+
58
+ const [isRecording, setIsRecording] = useState(false);
59
+ const [isTranscribing, setIsTranscribing] = useState(false);
60
+ const [transcribeErrorMsg, setTranscribeErrorMsg] = useState<string | null>(
61
+ null,
62
+ );
63
+
64
+ const mediaRecorderRef = useRef<any>(null);
65
+ const audioChunksRef = useRef<Blob[]>([]);
66
+
67
+ const expoRecordingRef = useRef<any>(null);
68
+
69
+ useEffect(() => {
70
+ if (!transcribeErrorMsg) return;
71
+ const timer = setTimeout(() => setTranscribeErrorMsg(null), 5000);
72
+ return () => clearTimeout(timer);
73
+ }, [transcribeErrorMsg]);
74
+
75
+ const handleTranscribeFailure = useCallback(
76
+ (err: unknown) => {
77
+ const asError =
78
+ err instanceof Error ? err : new Error(String(err ?? "Unknown error"));
79
+
80
+ let userMessage = t(
81
+ "chat.transcribeError.text",
82
+ "Couldn't transcribe your recording. Please try again.",
83
+ );
84
+
85
+ if (err instanceof TranscribeError) {
86
+ if (err.errorCode && err.message) {
87
+ userMessage = err.message ?? t(
88
+ "chat.transcribeError.failed",
89
+ "Transcription failed. Please try recording again.",
90
+ );
91
+ } else if (err.status === 401 || err.status === 403) {
92
+ userMessage = t(
93
+ "chat.transcribeError.auth",
94
+ "You're not signed in. Please sign in and try again.",
95
+ );
96
+ } else if (err.status >= 500) {
97
+ userMessage = t(
98
+ "chat.transcribeError.server",
99
+ "Server error while transcribing. Please try again shortly.",
100
+ );
80
101
  }
81
- placeholderTextColor={theme["core-06"] || "#999"}
82
- value={value}
83
- onChangeText={onChangeText}
84
- onSubmitEditing={onSend}
85
- editable={!disabled}
86
- autoFocus={!theme.isTablet}
87
- />
88
- <SendButton onPress={onSend} disabled={disabled}>
89
- <SendIcon disabled={disabled} isEmpty={value.trim() === ""} />
90
- </SendButton>
91
- </InputContainer>
102
+ }
103
+
104
+ console.error("Failed to transcribe audio", err);
105
+ setTranscribeErrorMsg(userMessage);
106
+ onTranscribeError?.(asError, userMessage);
107
+ },
108
+ [t, onTranscribeError],
109
+ );
110
+
111
+ const transcribeAndAppend = useCallback(
112
+ async (file: TranscribeAudioFile, fileName: string, mimeType: string) => {
113
+ if (!apiUrl) return;
114
+ setIsTranscribing(true);
115
+ setTranscribeErrorMsg(null);
116
+ try {
117
+ const sanitizedMime = stripMimeParams(mimeType);
118
+ const data: TranscribeResponse = await transcribeAudio(apiUrl, file, {
119
+ conversationId,
120
+ priceMode,
121
+ fileName,
122
+ mimeType: sanitizedMime,
123
+ });
124
+ const transcript = data?.transcript ?? "";
125
+ if (transcript) {
126
+ onChangeText(value ? `${value} ${transcript}` : transcript);
127
+ }
128
+ } catch (err) {
129
+ handleTranscribeFailure(err);
130
+ } finally {
131
+ setIsTranscribing(false);
132
+ }
133
+ },
134
+ [
135
+ apiUrl,
136
+ conversationId,
137
+ priceMode,
138
+ value,
139
+ onChangeText,
140
+ handleTranscribeFailure,
141
+ ],
142
+ );
143
+
144
+ const startWebRecording = useCallback(async () => {
145
+ try {
146
+ const stream = await (navigator as any).mediaDevices.getUserMedia({
147
+ audio: true,
148
+ });
149
+ const preferredTypes = [
150
+ "audio/webm;codecs=opus",
151
+ "audio/webm",
152
+ "audio/ogg;codecs=opus",
153
+ "audio/mp4",
154
+ ];
155
+ const MR: any = (window as any).MediaRecorder;
156
+ const supportedType =
157
+ preferredTypes.find((tpe) => MR?.isTypeSupported?.(tpe)) || "";
158
+
159
+ const mediaRecorder = supportedType
160
+ ? new MR(stream, { mimeType: supportedType })
161
+ : new MR(stream);
162
+ mediaRecorderRef.current = mediaRecorder;
163
+ audioChunksRef.current = [];
164
+
165
+ mediaRecorder.ondataavailable = (e: any) => {
166
+ if (e.data && e.data.size > 0) audioChunksRef.current.push(e.data);
167
+ };
168
+
169
+ mediaRecorder.onstop = async () => {
170
+ stream.getTracks().forEach((track: any) => track.stop());
171
+ const chunkType =
172
+ mediaRecorder.mimeType ||
173
+ audioChunksRef.current[0]?.type ||
174
+ "audio/webm";
175
+ const audioBlob = new Blob(audioChunksRef.current, { type: chunkType });
176
+ const ext = chunkType.includes("mp4")
177
+ ? "mp4"
178
+ : chunkType.includes("ogg")
179
+ ? "ogg"
180
+ : "webm";
181
+ await transcribeAndAppend(
182
+ audioBlob,
183
+ `recording-${Date.now()}.${ext}`,
184
+ chunkType,
185
+ );
186
+ };
187
+
188
+ mediaRecorder.start();
189
+ setIsRecording(true);
190
+ setTranscribeErrorMsg(null);
191
+ } catch (err) {
192
+ console.error("Failed to start web recording", err);
193
+ setTranscribeErrorMsg(
194
+ t(
195
+ "chat.recordError.text",
196
+ "Microphone access failed. Please check permissions.",
197
+ ),
198
+ );
199
+ }
200
+ }, [transcribeAndAppend, t]);
201
+
202
+ const stopWebRecording = useCallback(() => {
203
+ if (mediaRecorderRef.current) {
204
+ mediaRecorderRef.current.stop();
205
+ mediaRecorderRef.current = null;
206
+ setIsRecording(false);
207
+ }
208
+ }, []);
209
+
210
+ const startNativeRecording = useCallback(async () => {
211
+ if (!ExpoAudio) return;
212
+ try {
213
+ const { granted } = await ExpoAudio.requestPermissionsAsync();
214
+ if (!granted) {
215
+ setTranscribeErrorMsg(
216
+ t(
217
+ "chat.recordError.permission",
218
+ "Microphone permission was denied.",
219
+ ),
220
+ );
221
+ return;
222
+ }
223
+
224
+ await ExpoAudio.setAudioModeAsync({
225
+ allowsRecordingIOS: true,
226
+ playsInSilentModeIOS: true,
227
+ });
228
+
229
+ const { recording } = await ExpoAudio.Recording.createAsync(
230
+ ExpoAudio.RecordingOptionsPresets.HIGH_QUALITY,
231
+ );
232
+ expoRecordingRef.current = recording;
233
+ setIsRecording(true);
234
+ setTranscribeErrorMsg(null);
235
+ } catch (err) {
236
+ console.error("Failed to start native recording", err);
237
+ setTranscribeErrorMsg(
238
+ t(
239
+ "chat.recordError.text",
240
+ "Microphone access failed. Please check permissions.",
241
+ ),
242
+ );
243
+ }
244
+ }, [t]);
245
+
246
+ const stopNativeRecording = useCallback(async () => {
247
+ const recording = expoRecordingRef.current;
248
+ if (!recording) return;
249
+
250
+ try {
251
+ await recording.stopAndUnloadAsync();
252
+ await ExpoAudio.setAudioModeAsync({ allowsRecordingIOS: false });
253
+
254
+ const uri = recording.getURI();
255
+ expoRecordingRef.current = null;
256
+ setIsRecording(false);
257
+
258
+ if (uri) {
259
+ const fileName = `recording-${Date.now()}.m4a`;
260
+ await transcribeAndAppend(
261
+ { uri, name: fileName, type: "audio/m4a" },
262
+ fileName,
263
+ "audio/m4a",
264
+ );
265
+ }
266
+ } catch (err) {
267
+ console.error("Failed to stop native recording", err);
268
+ expoRecordingRef.current = null;
269
+ setIsRecording(false);
270
+ }
271
+ }, [transcribeAndAppend]);
272
+
273
+ const startRecording = useCallback(() => {
274
+ if (Platform.OS === "web") {
275
+ return startWebRecording();
276
+ }
277
+ return startNativeRecording();
278
+ }, [startWebRecording, startNativeRecording]);
279
+
280
+ const stopRecording = useCallback(() => {
281
+ if (Platform.OS === "web") {
282
+ stopWebRecording();
283
+ } else {
284
+ stopNativeRecording();
285
+ }
286
+ }, [stopWebRecording, stopNativeRecording]);
287
+
288
+ const handleMicPress = useCallback(() => {
289
+ if (isRecording) {
290
+ stopRecording();
291
+ } else {
292
+ startRecording();
293
+ }
294
+ }, [isRecording, startRecording, stopRecording]);
295
+
296
+ const isMicAvailable =
297
+ !!apiUrl && (Platform.OS === "web" || !!ExpoAudio);
298
+
299
+ const isSendDisabled = disabled || isRecording || isTranscribing;
300
+
301
+ return (
302
+ <InputWrapper>
303
+ <InputContainer theme={theme}>
304
+ <Input
305
+ ref={inputRef}
306
+ id="chat-input"
307
+ style={[styles.inputWeb]}
308
+ autoComplete="off"
309
+ placeholder={
310
+ placeholder ||
311
+ t(
312
+ "chat.placeholder.text",
313
+ "Describe the diamond you're looking for...",
314
+ )
315
+ }
316
+ placeholderTextColor={theme["core-06"] || "#999"}
317
+ value={value}
318
+ onChangeText={onChangeText}
319
+ onSubmitEditing={onSend}
320
+ editable={!isSendDisabled}
321
+ autoFocus={!theme.isTablet}
322
+ />
323
+ {isMicAvailable && (
324
+ <MicButton onPress={handleMicPress} disabled={isTranscribing}>
325
+ <VDBIcon
326
+ color={
327
+ isTranscribing
328
+ ? theme["core-04"] || "#ACACB3"
329
+ : isRecording
330
+ ? theme["error-01"] || "#FF0000"
331
+ : theme["core-05"] || "#020001"
332
+ }
333
+ size={24}
334
+ variant={IconsVariant.VDBCustom}
335
+ name={isRecording ? VDBIcons.STOP_CIRCLE : VDBIcons.MIC}
336
+ />
337
+ </MicButton>
338
+ )}
339
+ <SendButton onPress={onSend} disabled={isSendDisabled}>
340
+ <VDBIcon
341
+ color={
342
+ isSendDisabled || value.trim() === ""
343
+ ? theme["core-04"]
344
+ : theme["core-05"]
345
+ }
346
+ size={24}
347
+ variant={IconsVariant.VDBCustom}
348
+ name={VDBIcons.SEND}
349
+ />
350
+ </SendButton>
351
+ </InputContainer>
352
+ {transcribeErrorMsg ? (
353
+ <ErrorBanner
354
+ accessibilityRole="alert"
355
+ onPress={() => setTranscribeErrorMsg(null)}
356
+ >
357
+ <ErrorText theme={theme} numberOfLines={2}>
358
+ {transcribeErrorMsg}
359
+ </ErrorText>
360
+ </ErrorBanner>
361
+ ) : null}
362
+ </InputWrapper>
92
363
  );
93
364
  };
94
365
 
366
+ const InputWrapper = styled.View`
367
+ align-self: stretch;
368
+ `;
369
+
95
370
  const InputContainer = styled.View<{ theme: DefaultTheme }>`
96
371
  flex-direction: row;
97
372
  padding-horizontal: 12px;
@@ -108,6 +383,18 @@ const InputContainer = styled.View<{ theme: DefaultTheme }>`
108
383
  height: 56px;
109
384
  `;
110
385
 
386
+ const ErrorBanner = styled.TouchableOpacity`
387
+ margin-top: 6px;
388
+ align-self: stretch;
389
+ `;
390
+
391
+ const ErrorText = styled.Text<{ theme: DefaultTheme }>`
392
+ font-size: 12px;
393
+ line-height: 16px;
394
+ color: ${({ theme }: { theme: DefaultTheme }) =>
395
+ theme["error-01"] || "#FF0000"};
396
+ `;
397
+
111
398
  const Input = styled.TextInput<{ theme: DefaultTheme }>`
112
399
  flex: 1;
113
400
  color: ${({ theme }: { theme: DefaultTheme }) =>
@@ -137,31 +424,21 @@ const SendButton = styled.TouchableOpacity<{ theme: DefaultTheme }>`
137
424
  theme["core-01"] || "#FFFFFF"};
138
425
  `;
139
426
 
427
+ const MicButton = styled.TouchableOpacity<{ theme: DefaultTheme }>`
428
+ margin-right: 16px;
429
+ padding: 0;
430
+ justify-content: center;
431
+ align-items: center;
432
+ height: 24;
433
+ aspect-ratio: 1;
434
+ border-radius: 0px;
435
+ background-color: transparent;
436
+ `;
437
+
140
438
  const styles = StyleSheet.create({
141
- webShadow: {
142
- ...(Platform.OS === "web"
143
- ? ({ boxShadow: "2px 8px 24px -6px rgba(55, 54, 64, 0.26)" } as any)
144
- : {}),
145
- },
146
- nativeShadow: {
147
- shadowColor: "#373640",
148
- shadowOffset: { width: 2, height: 8 },
149
- shadowOpacity: 0.26,
150
- shadowRadius: 12,
151
- elevation: 3,
152
- },
153
439
  inputWeb: {
154
440
  ...(Platform.OS === "web"
155
441
  ? ({ outlineStyle: "none", outlineWidth: 0, boxShadow: "none" } as any)
156
442
  : {}),
157
443
  },
158
- buttonIcon: {
159
- height: 24,
160
- width: 24,
161
- },
162
- sendIconText: {
163
- fontSize: 18,
164
- fontWeight: "700",
165
- textAlign: "center",
166
- },
167
444
  });
@@ -1402,6 +1402,8 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
1402
1402
  placeholder={placeholder}
1403
1403
  theme={themeProps}
1404
1404
  inputRef={inputRef}
1405
+ apiUrl={apiUrl}
1406
+ priceMode={priceMode}
1405
1407
  />
1406
1408
  <BetaNotice active={betaActive} />
1407
1409
  </BottomContainer>
@@ -1583,6 +1585,13 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
1583
1585
  onSuggestionSelect={handleSuggestionSelect}
1584
1586
  isLatest={isLatest}
1585
1587
  isTyping={typingMessageId === item.id}
1588
+ showShowResultsButton={
1589
+ fromFilterScreen &&
1590
+ item.role === "assistant" &&
1591
+ item.search_payload &&
1592
+ Object.keys(item.search_payload).length > 0
1593
+ }
1594
+ onViewAll={onViewAll}
1586
1595
  />
1587
1596
  {/* Trigger product fetch when this area enters viewport */}
1588
1597
  {item.role === "assistant" &&
@@ -1,5 +1,5 @@
1
1
  import React, { memo } from "react";
2
- import { View, Text, StyleSheet } from "react-native";
2
+ import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
3
3
  import SuggestionsRow from "./SuggestionsRow";
4
4
  import type { ChatMessage, ChatTheme } from "../types";
5
5
  import { useTheme } from "styled-components/native";
@@ -28,6 +28,8 @@ interface Props {
28
28
  onSuggestionSelect?: (value: string) => void;
29
29
  isLatest?: boolean;
30
30
  isTyping?: boolean;
31
+ showShowResultsButton?: boolean;
32
+ onViewAll?: (item: any) => void;
31
33
  }
32
34
 
33
35
  const MessageBubbleComponent: React.FC<Props> = ({
@@ -39,6 +41,8 @@ const MessageBubbleComponent: React.FC<Props> = ({
39
41
  onSuggestionSelect,
40
42
  isLatest,
41
43
  isTyping,
44
+ showShowResultsButton,
45
+ onViewAll,
42
46
  }) => {
43
47
  const { t } = useTranslation();
44
48
  const isUser = message.role === "user";
@@ -63,6 +67,9 @@ const MessageBubbleComponent: React.FC<Props> = ({
63
67
  Number(totalResults) > 1 ? t("product.pluralSuffix.text", "s") : "";
64
68
  const labelProductName = baseProductName + pairSuffix + pluralSuffix;
65
69
 
70
+ const handleViewAllPress = () => {
71
+ onViewAll?.(message);
72
+ };
66
73
  const renderWords = (text: string, isBold: boolean, keyPrefix: string) =>
67
74
  text
68
75
  .trim()
@@ -145,6 +152,13 @@ const MessageBubbleComponent: React.FC<Props> = ({
145
152
  <MessageText isUser={isUser} isBold={true} theme={theme}>
146
153
  {`${generateSavedSearchName(message.search_payload, true, t)?.title}`}
147
154
  </MessageText>
155
+ {showShowResultsButton && (
156
+ <ShowResultsButton onPress={handleViewAllPress}>
157
+ <ShowResultsText theme={theme}>
158
+ {t("chat.showResults.button", "Show Results")}
159
+ </ShowResultsText>
160
+ </ShowResultsButton>
161
+ )}
148
162
  </>
149
163
  ) : message.role === "assistant" &&
150
164
  message?.search_payload &&
@@ -311,3 +325,15 @@ const InlineContainer = styled.View`
311
325
  align-items: center;
312
326
  flex-wrap: wrap;
313
327
  `;
328
+
329
+ const ShowResultsButton = styled.TouchableOpacity`
330
+ margin-top: 4px;
331
+ `;
332
+
333
+ const ShowResultsText = styled.Text<{ theme: DefaultTheme }>`
334
+ color: ${({ theme }: { theme: DefaultTheme }) =>
335
+ theme["link-static"] || "#3378f6"};
336
+ font-size: 14px;
337
+ font-weight: 500;
338
+ font-family: Roboto-Medium;
339
+ `;
@@ -0,0 +1,74 @@
1
+ import React, { useEffect, useState } from "react";
2
+
3
+ const selectionJsonUrl = "https://cdn.vdbapp.com/vdb2/icons/selection.json";
4
+ const iconMoonIconUrl = "https://cdn.vdbapp.com/vdb2/icons/icomoon.ttf";
5
+
6
+ let createIconSetFromIcoMoon: any = null;
7
+ try {
8
+ const mod = require("@expo/vector-icons/build/createIconSetFromIcoMoon");
9
+ createIconSetFromIcoMoon = mod?.default ?? mod;
10
+ } catch {
11
+ }
12
+
13
+ let axiosLib: any = null;
14
+ try {
15
+ axiosLib = require("axios");
16
+ } catch {
17
+ }
18
+
19
+ let cachedIconSet: any = null;
20
+ let cachedPromise: Promise<any> | null = null;
21
+
22
+ const loadSelectionJson = async (): Promise<any> => {
23
+ if (axiosLib) {
24
+ const res = await (axiosLib.default || axiosLib).get(selectionJsonUrl);
25
+ return res?.data;
26
+ }
27
+ const res = await fetch(selectionJsonUrl);
28
+ return res.json();
29
+ };
30
+
31
+ export interface VDBCustomIconProps {
32
+ name: string;
33
+ size?: number;
34
+ color?: string;
35
+ style?: any;
36
+ [key: string]: any;
37
+ }
38
+
39
+ const VDBCustomIcon: React.FC<VDBCustomIconProps> = (props) => {
40
+ const [, setReady] = useState(false);
41
+
42
+ useEffect(() => {
43
+ if (cachedIconSet || !createIconSetFromIcoMoon) return;
44
+
45
+ if (!cachedPromise) {
46
+ cachedPromise = loadSelectionJson()
47
+ .then((data) => {
48
+ if (data && data.icons) {
49
+ cachedIconSet = createIconSetFromIcoMoon(
50
+ data,
51
+ "vdb",
52
+ iconMoonIconUrl,
53
+ );
54
+ } else {
55
+ console.error("Invalid IcoMoon JSON: missing 'icons' array");
56
+ }
57
+ })
58
+ .catch((err) => {
59
+ console.error("VDBCustomIcon: failed to load icon set", err);
60
+ });
61
+ }
62
+
63
+ cachedPromise.then(() => {
64
+ setReady((r) => !r);
65
+ });
66
+ }, []);
67
+
68
+ if (!cachedIconSet) return null;
69
+
70
+ const IconSet = cachedIconSet;
71
+ return <IconSet {...props} />;
72
+ };
73
+
74
+ export default VDBCustomIcon;