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.
- package/dist/chat-widget.js +189 -164
- package/dist/chat-widget.js.LICENSE.txt +2 -0
- package/lib/commonjs/api.js +84 -4
- package/lib/commonjs/api.js.map +1 -1
- package/lib/commonjs/components/ChatInput.js +227 -65
- package/lib/commonjs/components/ChatInput.js.map +1 -1
- package/lib/commonjs/components/ChatWidget.js +6 -2
- package/lib/commonjs/components/ChatWidget.js.map +1 -1
- package/lib/commonjs/components/MessageBubble.js +22 -2
- package/lib/commonjs/components/MessageBubble.js.map +1 -1
- package/lib/commonjs/components/icons/VDBCustomIcon.js +54 -0
- package/lib/commonjs/components/icons/VDBCustomIcon.js.map +1 -0
- package/lib/commonjs/components/icons/VDBIcon.js +141 -0
- package/lib/commonjs/components/icons/VDBIcon.js.map +1 -0
- package/lib/commonjs/components/icons/VDBIcons.js +16 -0
- package/lib/commonjs/components/icons/VDBIcons.js.map +1 -0
- package/lib/commonjs/components/icons/index.js +34 -0
- package/lib/commonjs/components/icons/index.js.map +1 -0
- package/lib/commonjs/index.js +29 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.native.js +30 -1
- package/lib/commonjs/index.native.js.map +1 -1
- package/lib/commonjs/routes.js +1 -0
- package/lib/commonjs/routes.js.map +1 -1
- package/lib/module/api.js +81 -5
- package/lib/module/api.js.map +1 -1
- package/lib/module/components/ChatInput.js +228 -66
- package/lib/module/components/ChatInput.js.map +1 -1
- package/lib/module/components/ChatWidget.js +6 -2
- package/lib/module/components/ChatWidget.js.map +1 -1
- package/lib/module/components/MessageBubble.js +22 -2
- package/lib/module/components/MessageBubble.js.map +1 -1
- package/lib/module/components/icons/VDBCustomIcon.js +47 -0
- package/lib/module/components/icons/VDBCustomIcon.js.map +1 -0
- package/lib/module/components/icons/VDBIcon.js +134 -0
- package/lib/module/components/icons/VDBIcon.js.map +1 -0
- package/lib/module/components/icons/VDBIcons.js +10 -0
- package/lib/module/components/icons/VDBIcons.js.map +1 -0
- package/lib/module/components/icons/index.js +3 -0
- package/lib/module/components/icons/index.js.map +1 -0
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.native.js +1 -0
- package/lib/module/index.native.js.map +1 -1
- package/lib/module/routes.js +1 -0
- package/lib/module/routes.js.map +1 -1
- package/lib/typescript/api.d.ts +22 -0
- package/lib/typescript/api.d.ts.map +1 -1
- package/lib/typescript/components/ChatInput.d.ts +4 -0
- package/lib/typescript/components/ChatInput.d.ts.map +1 -1
- package/lib/typescript/components/ChatWidget.d.ts.map +1 -1
- package/lib/typescript/components/MessageBubble.d.ts +2 -0
- package/lib/typescript/components/MessageBubble.d.ts.map +1 -1
- package/lib/typescript/components/icons/VDBCustomIcon.d.ts +11 -0
- package/lib/typescript/components/icons/VDBCustomIcon.d.ts.map +1 -0
- package/lib/typescript/components/icons/VDBIcon.d.ts +34 -0
- package/lib/typescript/components/icons/VDBIcon.d.ts.map +1 -0
- package/lib/typescript/components/icons/VDBIcons.d.ts +9 -0
- package/lib/typescript/components/icons/VDBIcons.d.ts.map +1 -0
- package/lib/typescript/components/icons/index.d.ts +5 -0
- package/lib/typescript/components/icons/index.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +2 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.native.d.ts +2 -0
- package/lib/typescript/index.native.d.ts.map +1 -1
- package/lib/typescript/routes.d.ts +1 -0
- package/lib/typescript/routes.d.ts.map +1 -1
- package/package.json +18 -2
- package/src/api.ts +147 -3
- package/src/components/ChatInput.tsx +360 -83
- package/src/components/ChatWidget.tsx +9 -0
- package/src/components/MessageBubble.tsx +27 -1
- package/src/components/icons/VDBCustomIcon.tsx +74 -0
- package/src/components/icons/VDBIcon.tsx +186 -0
- package/src/components/icons/VDBIcons.ts +9 -0
- package/src/components/icons/index.ts +4 -0
- package/src/index.native.tsx +12 -0
- package/src/index.ts +12 -0
- package/src/routes.ts +1 -0
|
@@ -1,20 +1,29 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { TextInput, StyleSheet, Platform
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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;
|