srs-heritage-chatbot 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +194 -0
- package/lib/commonjs/assets/chat-icon-mobile.svg +1 -0
- package/lib/commonjs/assets/heritage.png +0 -0
- package/lib/commonjs/assets/posiden.svg +51 -0
- package/lib/commonjs/components/LoadingTips.js +104 -0
- package/lib/commonjs/components/LoadingTips.js.map +1 -0
- package/lib/commonjs/components/email.js +461 -0
- package/lib/commonjs/components/email.js.map +1 -0
- package/lib/commonjs/components/feedback.js +114 -0
- package/lib/commonjs/components/feedback.js.map +1 -0
- package/lib/commonjs/components/header.js +126 -0
- package/lib/commonjs/components/header.js.map +1 -0
- package/lib/commonjs/components/input.js +144 -0
- package/lib/commonjs/components/input.js.map +1 -0
- package/lib/commonjs/components/productCard.js +688 -0
- package/lib/commonjs/components/productCard.js.map +1 -0
- package/lib/commonjs/components/progressCircle.js +99 -0
- package/lib/commonjs/components/progressCircle.js.map +1 -0
- package/lib/commonjs/components/testing.js +74 -0
- package/lib/commonjs/components/testing.js.map +1 -0
- package/lib/commonjs/components/voice.js +184 -0
- package/lib/commonjs/components/voice.js.map +1 -0
- package/lib/commonjs/components/welcomeButton.js +149 -0
- package/lib/commonjs/components/welcomeButton.js.map +1 -0
- package/lib/commonjs/components/welcomeInput.js +137 -0
- package/lib/commonjs/components/welcomeInput.js.map +1 -0
- package/lib/commonjs/contexts/AppContext.js +552 -0
- package/lib/commonjs/contexts/AppContext.js.map +1 -0
- package/lib/commonjs/hooks/Stream.js +599 -0
- package/lib/commonjs/hooks/Stream.js.map +1 -0
- package/lib/commonjs/hooks/useAsyncStorage.js +36 -0
- package/lib/commonjs/hooks/useAsyncStorage.js.map +1 -0
- package/lib/commonjs/index.js +44 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/layout/disclaimer.js +208 -0
- package/lib/commonjs/layout/disclaimer.js.map +1 -0
- package/lib/commonjs/layout/ex.js +254 -0
- package/lib/commonjs/layout/ex.js.map +1 -0
- package/lib/commonjs/layout/icon.js +118 -0
- package/lib/commonjs/layout/icon.js.map +1 -0
- package/lib/commonjs/layout/layout.js +168 -0
- package/lib/commonjs/layout/layout.js.map +1 -0
- package/lib/commonjs/layout/welcome.js +160 -0
- package/lib/commonjs/layout/welcome.js.map +1 -0
- package/lib/commonjs/layout/window.js +396 -0
- package/lib/commonjs/layout/window.js.map +1 -0
- package/lib/commonjs/utils/audioRecorder.js +412 -0
- package/lib/commonjs/utils/audioRecorder.js.map +1 -0
- package/lib/commonjs/utils/cloudinary.js +69 -0
- package/lib/commonjs/utils/cloudinary.js.map +1 -0
- package/lib/commonjs/utils/storage.js +76 -0
- package/lib/commonjs/utils/storage.js.map +1 -0
- package/lib/commonjs/utils/textToSpeech.js +53 -0
- package/lib/commonjs/utils/textToSpeech.js.map +1 -0
- package/lib/module/assets/chat-icon-mobile.svg +1 -0
- package/lib/module/assets/heritage.png +0 -0
- package/lib/module/assets/posiden.svg +51 -0
- package/lib/module/components/LoadingTips.js +95 -0
- package/lib/module/components/LoadingTips.js.map +1 -0
- package/lib/module/components/email.js +452 -0
- package/lib/module/components/email.js.map +1 -0
- package/lib/module/components/feedback.js +105 -0
- package/lib/module/components/feedback.js.map +1 -0
- package/lib/module/components/header.js +117 -0
- package/lib/module/components/header.js.map +1 -0
- package/lib/module/components/input.js +135 -0
- package/lib/module/components/input.js.map +1 -0
- package/lib/module/components/productCard.js +679 -0
- package/lib/module/components/productCard.js.map +1 -0
- package/lib/module/components/progressCircle.js +91 -0
- package/lib/module/components/progressCircle.js.map +1 -0
- package/lib/module/components/testing.js +66 -0
- package/lib/module/components/testing.js.map +1 -0
- package/lib/module/components/voice.js +175 -0
- package/lib/module/components/voice.js.map +1 -0
- package/lib/module/components/welcomeButton.js +140 -0
- package/lib/module/components/welcomeButton.js.map +1 -0
- package/lib/module/components/welcomeInput.js +128 -0
- package/lib/module/components/welcomeInput.js.map +1 -0
- package/lib/module/contexts/AppContext.js +542 -0
- package/lib/module/contexts/AppContext.js.map +1 -0
- package/lib/module/hooks/Stream.js +592 -0
- package/lib/module/hooks/Stream.js.map +1 -0
- package/lib/module/hooks/useAsyncStorage.js +29 -0
- package/lib/module/hooks/useAsyncStorage.js.map +1 -0
- package/lib/module/index.js +36 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/layout/disclaimer.js +199 -0
- package/lib/module/layout/disclaimer.js.map +1 -0
- package/lib/module/layout/ex.js +253 -0
- package/lib/module/layout/ex.js.map +1 -0
- package/lib/module/layout/icon.js +108 -0
- package/lib/module/layout/icon.js.map +1 -0
- package/lib/module/layout/layout.js +160 -0
- package/lib/module/layout/layout.js.map +1 -0
- package/lib/module/layout/welcome.js +150 -0
- package/lib/module/layout/welcome.js.map +1 -0
- package/lib/module/layout/window.js +387 -0
- package/lib/module/layout/window.js.map +1 -0
- package/lib/module/utils/audioRecorder.js +398 -0
- package/lib/module/utils/audioRecorder.js.map +1 -0
- package/lib/module/utils/cloudinary.js +61 -0
- package/lib/module/utils/cloudinary.js.map +1 -0
- package/lib/module/utils/storage.js +67 -0
- package/lib/module/utils/storage.js.map +1 -0
- package/lib/module/utils/textToSpeech.js +43 -0
- package/lib/module/utils/textToSpeech.js.map +1 -0
- package/lib/typescript/components/LoadingTips.d.ts +3 -0
- package/lib/typescript/components/LoadingTips.d.ts.map +1 -0
- package/lib/typescript/components/email.d.ts +6 -0
- package/lib/typescript/components/email.d.ts.map +1 -0
- package/lib/typescript/components/feedback.d.ts +6 -0
- package/lib/typescript/components/feedback.d.ts.map +1 -0
- package/lib/typescript/components/header.d.ts +3 -0
- package/lib/typescript/components/header.d.ts.map +1 -0
- package/lib/typescript/components/input.d.ts +6 -0
- package/lib/typescript/components/input.d.ts.map +1 -0
- package/lib/typescript/components/productCard.d.ts +7 -0
- package/lib/typescript/components/productCard.d.ts.map +1 -0
- package/lib/typescript/components/progressCircle.d.ts +3 -0
- package/lib/typescript/components/progressCircle.d.ts.map +1 -0
- package/lib/typescript/components/testing.d.ts +6 -0
- package/lib/typescript/components/testing.d.ts.map +1 -0
- package/lib/typescript/components/voice.d.ts +5 -0
- package/lib/typescript/components/voice.d.ts.map +1 -0
- package/lib/typescript/components/welcomeButton.d.ts +4 -0
- package/lib/typescript/components/welcomeButton.d.ts.map +1 -0
- package/lib/typescript/components/welcomeInput.d.ts +6 -0
- package/lib/typescript/components/welcomeInput.d.ts.map +1 -0
- package/lib/typescript/contexts/AppContext.d.ts +10 -0
- package/lib/typescript/contexts/AppContext.d.ts.map +1 -0
- package/lib/typescript/hooks/Stream.d.ts +2 -0
- package/lib/typescript/hooks/Stream.d.ts.map +1 -0
- package/lib/typescript/hooks/useAsyncStorage.d.ts +2 -0
- package/lib/typescript/hooks/useAsyncStorage.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +8 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/layout/disclaimer.d.ts +5 -0
- package/lib/typescript/layout/disclaimer.d.ts.map +1 -0
- package/lib/typescript/layout/ex.d.ts +1 -0
- package/lib/typescript/layout/ex.d.ts.map +1 -0
- package/lib/typescript/layout/icon.d.ts +3 -0
- package/lib/typescript/layout/icon.d.ts.map +1 -0
- package/lib/typescript/layout/layout.d.ts +3 -0
- package/lib/typescript/layout/layout.d.ts.map +1 -0
- package/lib/typescript/layout/welcome.d.ts +6 -0
- package/lib/typescript/layout/welcome.d.ts.map +1 -0
- package/lib/typescript/layout/window.d.ts +5 -0
- package/lib/typescript/layout/window.d.ts.map +1 -0
- package/lib/typescript/utils/audioRecorder.d.ts +9 -0
- package/lib/typescript/utils/audioRecorder.d.ts.map +1 -0
- package/lib/typescript/utils/cloudinary.d.ts +17 -0
- package/lib/typescript/utils/cloudinary.d.ts.map +1 -0
- package/lib/typescript/utils/storage.d.ts +29 -0
- package/lib/typescript/utils/storage.d.ts.map +1 -0
- package/lib/typescript/utils/textToSpeech.d.ts +2 -0
- package/lib/typescript/utils/textToSpeech.d.ts.map +1 -0
- package/package.json +109 -0
- package/src/assets/chat-icon-mobile.svg +1 -0
- package/src/assets/heritage.png +0 -0
- package/src/assets/posiden.svg +51 -0
- package/src/components/LoadingTips.js +99 -0
- package/src/components/email.js +467 -0
- package/src/components/feedback.js +114 -0
- package/src/components/header.js +119 -0
- package/src/components/input.js +133 -0
- package/src/components/productCard.js +815 -0
- package/src/components/progressCircle.js +88 -0
- package/src/components/testing.js +60 -0
- package/src/components/voice.js +228 -0
- package/src/components/welcomeButton.js +161 -0
- package/src/components/welcomeInput.js +133 -0
- package/src/contexts/AppContext.js +678 -0
- package/src/hooks/Stream.js +655 -0
- package/src/hooks/useAsyncStorage.js +33 -0
- package/src/index.js +30 -0
- package/src/layout/disclaimer.js +231 -0
- package/src/layout/ex.js +252 -0
- package/src/layout/icon.js +105 -0
- package/src/layout/layout.js +160 -0
- package/src/layout/welcome.js +172 -0
- package/src/layout/window.js +476 -0
- package/src/utils/audioRecorder.js +445 -0
- package/src/utils/cloudinary.js +61 -0
- package/src/utils/storage.ts +89 -0
- package/src/utils/textToSpeech.js +49 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useContext } from "react";
|
|
2
|
+
import { View, Animated, StyleSheet, Pressable, Text } from "react-native";
|
|
3
|
+
import { AppContext } from "../contexts/AppContext";
|
|
4
|
+
import { LoadingTips } from "./LoadingTips";
|
|
5
|
+
|
|
6
|
+
export const ProgressCircle = () => {
|
|
7
|
+
const { stopGenerating } = useContext(AppContext);
|
|
8
|
+
const spinValue = useRef(new Animated.Value(0)).current;
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const spinAnimation = Animated.loop(
|
|
12
|
+
Animated.timing(spinValue, {
|
|
13
|
+
toValue: 1,
|
|
14
|
+
duration: 1400,
|
|
15
|
+
useNativeDriver: true,
|
|
16
|
+
})
|
|
17
|
+
);
|
|
18
|
+
spinAnimation.start();
|
|
19
|
+
|
|
20
|
+
return () => spinAnimation.stop(); // Cleanup on unmount
|
|
21
|
+
}, [spinValue]);
|
|
22
|
+
|
|
23
|
+
const spin = spinValue.interpolate({
|
|
24
|
+
inputRange: [0, 1],
|
|
25
|
+
outputRange: ["0deg", "360deg"],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
<Text style={styles.textBeta}>Beta version. AI Assistant is still learning!</Text>
|
|
31
|
+
<View style={styles.container}>
|
|
32
|
+
<LoadingTips />
|
|
33
|
+
<Pressable style={styles.circleContainer} onPress={stopGenerating}>
|
|
34
|
+
<Animated.View style={[styles.circle, { transform: [{ rotate: spin }] }]} />
|
|
35
|
+
<View style={styles.stopSquare} />
|
|
36
|
+
</Pressable>
|
|
37
|
+
</View>
|
|
38
|
+
</>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const styles = StyleSheet.create({
|
|
43
|
+
container: {
|
|
44
|
+
flexDirection: "row",
|
|
45
|
+
justifyContent: "flex-end",
|
|
46
|
+
alignItems: "center",
|
|
47
|
+
paddingHorizontal: 6,
|
|
48
|
+
paddingVertical: 6,
|
|
49
|
+
backgroundColor: "#f6f6f6",
|
|
50
|
+
borderTopWidth: 1,
|
|
51
|
+
borderTopColor: "rgba(0, 0, 0, 0.1)",
|
|
52
|
+
paddingBottom: 35,
|
|
53
|
+
paddingTop: 15
|
|
54
|
+
},
|
|
55
|
+
circleContainer: {
|
|
56
|
+
justifyContent: "center",
|
|
57
|
+
alignItems: "center",
|
|
58
|
+
width: 50,
|
|
59
|
+
height: 50,
|
|
60
|
+
marginRight: 20,
|
|
61
|
+
position: "relative",
|
|
62
|
+
},
|
|
63
|
+
circle: {
|
|
64
|
+
borderWidth: 4,
|
|
65
|
+
borderColor: "#dddbd9",
|
|
66
|
+
borderTopColor: "#666666",
|
|
67
|
+
borderRadius: 50,
|
|
68
|
+
width: 40,
|
|
69
|
+
height: 40,
|
|
70
|
+
},
|
|
71
|
+
stopSquare: {
|
|
72
|
+
width: 13,
|
|
73
|
+
height: 13,
|
|
74
|
+
backgroundColor: "#666666",
|
|
75
|
+
position: "absolute",
|
|
76
|
+
top: "50%",
|
|
77
|
+
left: "50%",
|
|
78
|
+
transform: [{ translateX: -6.5 }, { translateY: -6.5 }],
|
|
79
|
+
},
|
|
80
|
+
textBeta: {
|
|
81
|
+
textAlign: 'center',
|
|
82
|
+
fontSize: 11,
|
|
83
|
+
color: '#808080',
|
|
84
|
+
fontWeight: '400',
|
|
85
|
+
marginTop: 5,
|
|
86
|
+
marginBottom: 2
|
|
87
|
+
},
|
|
88
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Button, StyleSheet, TouchableOpacity, Text } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export const Testing = ({ onProductCardClick, onAddToCartClick }) => {
|
|
5
|
+
|
|
6
|
+
const product = {
|
|
7
|
+
"part_number": "AEQO177",
|
|
8
|
+
"inventory_info": {
|
|
9
|
+
"default_uom": "EA",
|
|
10
|
+
"is_valid": true,
|
|
11
|
+
"info_by_uom": {
|
|
12
|
+
"EA": {
|
|
13
|
+
"gross_price": 7.83,
|
|
14
|
+
"net_price": 7.83,
|
|
15
|
+
"is_on_sale": false,
|
|
16
|
+
"quantity_available": 0,
|
|
17
|
+
"discounts": null
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"product_details": {
|
|
22
|
+
"product_name": "Aladdin Hayward SPX1600S SuperPump Lid Gasket | O-177-2",
|
|
23
|
+
"part_number": "AEQO177",
|
|
24
|
+
"manufacturer_id": "O-177-2",
|
|
25
|
+
"heritage_link": "https://www.heritagepoolplus.com/aeqo177-aladdin-o-ring-o-177-o-177-2-o-177-2",
|
|
26
|
+
"image_url": "https://media.heritageplus.com/image/upload/v1668157956/image/AEQO177_0.jpg"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<View style={styles.container}>
|
|
32
|
+
<TouchableOpacity style={styles.button} onPress={() => onProductCardClick(product)}>
|
|
33
|
+
<Text style={styles.buttonText}>Product Card Click</Text>
|
|
34
|
+
</TouchableOpacity>
|
|
35
|
+
<TouchableOpacity style={styles.button} onPress={() => onAddToCartClick({"quantity":1,"product":product})}>
|
|
36
|
+
<Text style={styles.buttonText}>Add to Cart</Text>
|
|
37
|
+
</TouchableOpacity>
|
|
38
|
+
</View>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const styles = StyleSheet.create({
|
|
43
|
+
container: {
|
|
44
|
+
flexDirection: 'row',
|
|
45
|
+
alignItems: 'center',
|
|
46
|
+
justifyContent: 'space-evenly',
|
|
47
|
+
paddingHorizontal: 16,
|
|
48
|
+
borderTopWidth: 1,
|
|
49
|
+
borderTopColor: '#DDD',
|
|
50
|
+
},
|
|
51
|
+
button: {
|
|
52
|
+
backgroundColor: "#d4d4d4",
|
|
53
|
+
paddingVertical: 12,
|
|
54
|
+
paddingHorizontal: 12,
|
|
55
|
+
borderRadius: 5,
|
|
56
|
+
alignItems: "center",
|
|
57
|
+
marginBottom: 10,
|
|
58
|
+
marginTop: 10,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// VoiceButton.js
|
|
2
|
+
|
|
3
|
+
import React, { useState, useContext, useEffect } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
TouchableOpacity,
|
|
6
|
+
ActivityIndicator,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
Alert,
|
|
9
|
+
Linking,
|
|
10
|
+
Platform,
|
|
11
|
+
} from 'react-native';
|
|
12
|
+
import Ionicons from 'react-native-vector-icons/Ionicons';
|
|
13
|
+
import useAsyncStorage from '../hooks/useAsyncStorage';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
startRecording,
|
|
17
|
+
stopRecording,
|
|
18
|
+
cancelRecording,
|
|
19
|
+
requestAudioPermission,
|
|
20
|
+
cleanup,
|
|
21
|
+
initVoice,
|
|
22
|
+
resetStoredPermission,
|
|
23
|
+
setPermissionStatusHandlers,
|
|
24
|
+
} from '../utils/audioRecorder';
|
|
25
|
+
import { AppContext } from '../contexts/AppContext';
|
|
26
|
+
|
|
27
|
+
const PERMISSION_STORAGE_KEY = '@voice_permission_status';
|
|
28
|
+
|
|
29
|
+
export const VoiceButton = ({ setInput }) => {
|
|
30
|
+
const { handleVoiceSend, isListening, setIsListening } =
|
|
31
|
+
useContext(AppContext);
|
|
32
|
+
const [loading, setLoading] = useState(false);
|
|
33
|
+
const [permissionChecked, setPermissionChecked] = useState(false);
|
|
34
|
+
const [hasPermission, setHasPermission] = useState(null);
|
|
35
|
+
|
|
36
|
+
// Use your custom AsyncStorage hook
|
|
37
|
+
const [permissionStatus, setPermissionStatus] = useAsyncStorage(
|
|
38
|
+
PERMISSION_STORAGE_KEY,
|
|
39
|
+
null,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
// Register our permission handlers
|
|
44
|
+
setPermissionStatusHandlers(() => permissionStatus, setPermissionStatus);
|
|
45
|
+
|
|
46
|
+
const setupVoice = async () => {
|
|
47
|
+
try {
|
|
48
|
+
// Check stored permission first
|
|
49
|
+
if (permissionStatus === 'denied') {
|
|
50
|
+
// We already know permission was denied, don't show alert again
|
|
51
|
+
setHasPermission(false);
|
|
52
|
+
setPermissionChecked(true);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Only request permission if not already denied
|
|
57
|
+
const permissionResult = await requestAudioPermission();
|
|
58
|
+
setHasPermission(permissionResult);
|
|
59
|
+
|
|
60
|
+
if (permissionResult) {
|
|
61
|
+
const initialized = await initVoice(
|
|
62
|
+
// Final result callback
|
|
63
|
+
(result, error) => {
|
|
64
|
+
console.log('Voice final result:', result, 'Error:', error);
|
|
65
|
+
|
|
66
|
+
// Always reset states when the recognition ends
|
|
67
|
+
setIsListening(false);
|
|
68
|
+
setLoading(false);
|
|
69
|
+
|
|
70
|
+
if (error) {
|
|
71
|
+
// Don't show alert for permission errors since we handle that elsewhere
|
|
72
|
+
if (!error.includes('permission')) {
|
|
73
|
+
Alert.alert('Error', error);
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (result) {
|
|
79
|
+
if (setInput) {
|
|
80
|
+
// For live transcription mode: just update input, don't auto-send
|
|
81
|
+
setInput(result);
|
|
82
|
+
} else {
|
|
83
|
+
// For original mode: send the message automatically
|
|
84
|
+
handleVoiceSend(null, result);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
// Partial result callback for live transcription
|
|
89
|
+
setInput
|
|
90
|
+
? partialResult => {
|
|
91
|
+
if (partialResult) {
|
|
92
|
+
setInput(partialResult);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
: null,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
if (!initialized) {
|
|
99
|
+
// Only show this alert once per session
|
|
100
|
+
if (!permissionChecked) {
|
|
101
|
+
Alert.alert(
|
|
102
|
+
'Error',
|
|
103
|
+
'Speech recognition is not available on this device',
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error('Error in setupVoice:', error);
|
|
110
|
+
} finally {
|
|
111
|
+
setPermissionChecked(true);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (!permissionChecked) {
|
|
116
|
+
setupVoice();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return () => {
|
|
120
|
+
// Optional: only if the entire feature is being removed from the app
|
|
121
|
+
// cleanup();
|
|
122
|
+
};
|
|
123
|
+
}, [permissionStatus, permissionChecked]);
|
|
124
|
+
|
|
125
|
+
const toggleRecording = async () => {
|
|
126
|
+
try {
|
|
127
|
+
if (!isListening) {
|
|
128
|
+
// If we already know we don't have permission, show a better message
|
|
129
|
+
if (hasPermission === false) {
|
|
130
|
+
Alert.alert(
|
|
131
|
+
'Permission Required',
|
|
132
|
+
'Voice recognition requires microphone permission. Would you like to update your settings?',
|
|
133
|
+
[
|
|
134
|
+
{
|
|
135
|
+
text: 'Cancel',
|
|
136
|
+
style: 'cancel',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
text: 'Settings',
|
|
140
|
+
onPress: () => {
|
|
141
|
+
// Reset stored permission so we can check again
|
|
142
|
+
setPermissionStatus(null);
|
|
143
|
+
// Open device settings
|
|
144
|
+
openAppSettings();
|
|
145
|
+
// Reset our permission check
|
|
146
|
+
setPermissionChecked(false);
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
setLoading(true);
|
|
155
|
+
const checkPermission = await requestAudioPermission();
|
|
156
|
+
if (!checkPermission) {
|
|
157
|
+
setHasPermission(false);
|
|
158
|
+
setLoading(false);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const started = await startRecording();
|
|
163
|
+
if (started) {
|
|
164
|
+
setIsListening(true);
|
|
165
|
+
} else {
|
|
166
|
+
// Only show error if not permission related
|
|
167
|
+
Alert.alert('Error', 'Failed to start voice recognition');
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
setLoading(true);
|
|
171
|
+
setIsListening(false);
|
|
172
|
+
await stopRecording();
|
|
173
|
+
}
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.error('Error in toggleRecording:', error);
|
|
176
|
+
Alert.alert(
|
|
177
|
+
'Error',
|
|
178
|
+
'An error occurred while managing voice recognition',
|
|
179
|
+
);
|
|
180
|
+
setIsListening(false);
|
|
181
|
+
// Don't call cleanup here - let the natural session cleanup handle it
|
|
182
|
+
} finally {
|
|
183
|
+
setLoading(false);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const openAppSettings = async () => {
|
|
188
|
+
try {
|
|
189
|
+
if (Platform.OS === 'ios') {
|
|
190
|
+
// For iOS
|
|
191
|
+
await Linking.openURL('app-settings://');
|
|
192
|
+
} else {
|
|
193
|
+
// For Android
|
|
194
|
+
await Linking.openSettings();
|
|
195
|
+
}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.error('Cannot open settings', error);
|
|
198
|
+
Alert.alert(
|
|
199
|
+
'Error',
|
|
200
|
+
'Unable to open settings. Please open settings manually.',
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<TouchableOpacity
|
|
207
|
+
style={styles.button}
|
|
208
|
+
onPress={toggleRecording}
|
|
209
|
+
disabled={loading}>
|
|
210
|
+
{loading ? (
|
|
211
|
+
<ActivityIndicator size="small" color="#8E8E93" />
|
|
212
|
+
) : (
|
|
213
|
+
<Ionicons
|
|
214
|
+
name={isListening ? 'stop-circle' : 'mic-outline'}
|
|
215
|
+
size={24}
|
|
216
|
+
color="#161616"
|
|
217
|
+
/>
|
|
218
|
+
)}
|
|
219
|
+
</TouchableOpacity>
|
|
220
|
+
);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const styles = StyleSheet.create({
|
|
224
|
+
button: {
|
|
225
|
+
justifyContent: 'center',
|
|
226
|
+
alignItems: 'center',
|
|
227
|
+
},
|
|
228
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// WelcomeButton.js
|
|
2
|
+
import React, { useState, useContext } from 'react';
|
|
3
|
+
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
|
4
|
+
import { AppContext } from '../contexts/AppContext';
|
|
5
|
+
import Ionicons from 'react-native-vector-icons/Ionicons';
|
|
6
|
+
|
|
7
|
+
const landscapeQuestions = [
|
|
8
|
+
{ text: 'What are the hours of my current branch?' },
|
|
9
|
+
{ text: 'Do you have part # RBLESPLXME2 in stock?' },
|
|
10
|
+
{ text: 'How do I install the seal on B82456?' },
|
|
11
|
+
{ text: 'Where can I find my POs?' },
|
|
12
|
+
{ text: '¿Puedes ayudarme en español?' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const poolQuestions = [
|
|
16
|
+
{ text: 'What are the hours of my current branch?' },
|
|
17
|
+
{ text: 'Do you have part # JNDDEV48 in stock?' },
|
|
18
|
+
{ text: 'Do you carry Hayward super pumps?' },
|
|
19
|
+
{ text: 'Which grid assembly goes with the Hayward DE4820 filter?' },
|
|
20
|
+
{ text: '¿Puedes ayudarme en español?' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const ButtonComponent = () => {
|
|
24
|
+
const {
|
|
25
|
+
toggleLanguage,
|
|
26
|
+
getCurrentTranslations,
|
|
27
|
+
handleButtonClick,
|
|
28
|
+
theme,
|
|
29
|
+
data,
|
|
30
|
+
} = useContext(AppContext);
|
|
31
|
+
const isPool = data?.brand_version === 'pool';
|
|
32
|
+
const suggestedQuestions = isPool ? poolQuestions : landscapeQuestions;
|
|
33
|
+
|
|
34
|
+
const [suggestedQuestionsFromCategory, setSuggestedQuestionsFromCategory] =
|
|
35
|
+
useState([]);
|
|
36
|
+
|
|
37
|
+
const translations = getCurrentTranslations();
|
|
38
|
+
|
|
39
|
+
const starterCategories = [
|
|
40
|
+
{
|
|
41
|
+
key: 'products',
|
|
42
|
+
text: 'Products',
|
|
43
|
+
relatedQuestions: translations.followUpQuestions.products,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
key: 'orders',
|
|
47
|
+
text: 'Orders',
|
|
48
|
+
relatedQuestions: translations.followUpQuestions.orders,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
key: 'invoices',
|
|
52
|
+
text: 'Invoices',
|
|
53
|
+
relatedQuestions: translations.followUpQuestions.invoices,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
key: 'branchInfo',
|
|
57
|
+
text: 'Branch Information',
|
|
58
|
+
relatedQuestions: translations.followUpQuestions.branchInfo,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: 'spanish',
|
|
62
|
+
text: 'Ayuda es Español',
|
|
63
|
+
relatedQuestions: [],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
key: 'reset',
|
|
67
|
+
text: '',
|
|
68
|
+
relatedQuestions: [],
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const handleCategoryPick = item => {
|
|
73
|
+
console.log(item);
|
|
74
|
+
|
|
75
|
+
if (item.key === 'spanish') {
|
|
76
|
+
console.log('switching to spanish');
|
|
77
|
+
toggleLanguage();
|
|
78
|
+
} else if (item.key === 'reset') {
|
|
79
|
+
setSuggestedQuestionsFromCategory(item.relatedQuestions);
|
|
80
|
+
} else {
|
|
81
|
+
setSuggestedQuestionsFromCategory(item.relatedQuestions);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<View style={styles.buttonContainer}>
|
|
87
|
+
{suggestedQuestionsFromCategory.length !== 0 ? (
|
|
88
|
+
<View>
|
|
89
|
+
<TouchableOpacity
|
|
90
|
+
onPress={() =>
|
|
91
|
+
setSuggestedQuestionsFromCategory(
|
|
92
|
+
starterCategories[starterCategories.length - 1]
|
|
93
|
+
.relatedQuestions,
|
|
94
|
+
)
|
|
95
|
+
}>
|
|
96
|
+
<Ionicons
|
|
97
|
+
name="arrow-back-outline"
|
|
98
|
+
size={16}
|
|
99
|
+
color="#367CB6"
|
|
100
|
+
style={{ marginBottom: 10 }}
|
|
101
|
+
/>
|
|
102
|
+
</TouchableOpacity>
|
|
103
|
+
{suggestedQuestionsFromCategory.map((item, index) => (
|
|
104
|
+
<TouchableOpacity
|
|
105
|
+
key={index}
|
|
106
|
+
style={[styles.button, { borderColor: '#004687' }]}
|
|
107
|
+
onPress={() => handleButtonClick(item)}>
|
|
108
|
+
<Text style={[styles.buttonTextQuestion, { color: '#004687' }]}>
|
|
109
|
+
{item}
|
|
110
|
+
</Text>
|
|
111
|
+
</TouchableOpacity>
|
|
112
|
+
))}
|
|
113
|
+
</View>
|
|
114
|
+
) : (
|
|
115
|
+
starterCategories.map((item, index) => {
|
|
116
|
+
if (item.key === 'reset') {
|
|
117
|
+
return null;
|
|
118
|
+
} else {
|
|
119
|
+
return (
|
|
120
|
+
<TouchableOpacity
|
|
121
|
+
key={index}
|
|
122
|
+
style={[styles.button, { borderColor: '#004687' }]}
|
|
123
|
+
onPress={() => handleCategoryPick(item)}>
|
|
124
|
+
<Text style={[styles.buttonText, { color: '#004687' }]}>
|
|
125
|
+
{item.text}
|
|
126
|
+
</Text>
|
|
127
|
+
</TouchableOpacity>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
)}
|
|
132
|
+
</View>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const styles = StyleSheet.create({
|
|
137
|
+
buttonContainer: {},
|
|
138
|
+
button: {
|
|
139
|
+
backgroundColor: 'white',
|
|
140
|
+
borderColor: '#004687',
|
|
141
|
+
borderWidth: 1,
|
|
142
|
+
borderRadius: 18,
|
|
143
|
+
paddingVertical: 14,
|
|
144
|
+
paddingHorizontal: 16,
|
|
145
|
+
marginBottom: 10,
|
|
146
|
+
},
|
|
147
|
+
buttonText: {
|
|
148
|
+
color: '#004687',
|
|
149
|
+
fontSize: 13,
|
|
150
|
+
fontWeight: '400',
|
|
151
|
+
textAlign: 'center',
|
|
152
|
+
},
|
|
153
|
+
buttonTextQuestion: {
|
|
154
|
+
color: '#004687',
|
|
155
|
+
fontSize: 13,
|
|
156
|
+
fontWeight: '400',
|
|
157
|
+
textAlign: 'left',
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
export default ButtonComponent;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import React, { useState, useEffect, useContext, useRef } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Text,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
View,
|
|
6
|
+
TextInput,
|
|
7
|
+
TouchableOpacity,
|
|
8
|
+
Platform,
|
|
9
|
+
KeyboardAvoidingView,
|
|
10
|
+
Keyboard,
|
|
11
|
+
} from 'react-native';
|
|
12
|
+
import { Header } from '../components/header';
|
|
13
|
+
import { AppContext } from '../contexts/AppContext';
|
|
14
|
+
import Ionicons from 'react-native-vector-icons/Ionicons';
|
|
15
|
+
import { VoiceButton } from './voice';
|
|
16
|
+
|
|
17
|
+
export const WelcomeInput = ({ onProductCardClick, onAddToCartClick }) => {
|
|
18
|
+
const { data, handleSend, input, setInput, showModal, theme, isListening } =
|
|
19
|
+
useContext(AppContext);
|
|
20
|
+
const inputRef = useRef(null);
|
|
21
|
+
|
|
22
|
+
const handleKeyPress = ({ nativeEvent }) => {
|
|
23
|
+
if (nativeEvent.key === 'return' && !nativeEvent.shiftKey) {
|
|
24
|
+
nativeEvent.preventDefault && nativeEvent.preventDefault();
|
|
25
|
+
handleSend(input);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const onSubmitEditing = () => {
|
|
31
|
+
if (input.trim()) {
|
|
32
|
+
handleSend(input);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<View style={styles.ringContainer}>
|
|
38
|
+
<View style={styles.inputContainer}>
|
|
39
|
+
<TextInput
|
|
40
|
+
ref={inputRef}
|
|
41
|
+
style={styles.input}
|
|
42
|
+
value={input}
|
|
43
|
+
onChangeText={setInput}
|
|
44
|
+
placeholder={isListening && !input ? 'Listening...' : 'Ask away...'}
|
|
45
|
+
placeholderTextColor="#999"
|
|
46
|
+
editable={!isListening}
|
|
47
|
+
caretHidden={isListening}
|
|
48
|
+
showSoftInputOnFocus={!isListening}
|
|
49
|
+
multiline={false}
|
|
50
|
+
returnKeyType="send"
|
|
51
|
+
enablesReturnKeyAutomatically={true}
|
|
52
|
+
onKeyPress={handleKeyPress}
|
|
53
|
+
onSubmitEditing={onSubmitEditing}
|
|
54
|
+
selection={undefined}
|
|
55
|
+
// blurOnSubmit={false}
|
|
56
|
+
/>
|
|
57
|
+
<VoiceButton
|
|
58
|
+
setInput={text => {
|
|
59
|
+
setInput(text);
|
|
60
|
+
if (!isListening && inputRef.current) {
|
|
61
|
+
if (Platform.OS === 'android') {
|
|
62
|
+
setTimeout(() => {
|
|
63
|
+
if (inputRef.current) {
|
|
64
|
+
inputRef.current.setSelection(text.length, text.length);
|
|
65
|
+
}
|
|
66
|
+
}, 10);
|
|
67
|
+
} else {
|
|
68
|
+
inputRef.current.setSelection(text.length, text.length);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}}
|
|
72
|
+
/>
|
|
73
|
+
<TouchableOpacity
|
|
74
|
+
style={styles.sendButton}
|
|
75
|
+
onPress={() => handleSend(input)}
|
|
76
|
+
disabled={!input.trim()}>
|
|
77
|
+
<Ionicons
|
|
78
|
+
name="paper-plane-outline"
|
|
79
|
+
size={24}
|
|
80
|
+
color={input.trim() ? theme.primaryColor : '#161616'}
|
|
81
|
+
/>
|
|
82
|
+
</TouchableOpacity>
|
|
83
|
+
</View>
|
|
84
|
+
</View>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const styles = StyleSheet.create({
|
|
89
|
+
ringContainer: {
|
|
90
|
+
paddingHorizontal: 0.5,
|
|
91
|
+
paddingVertical: 0.5,
|
|
92
|
+
borderRadius: 18,
|
|
93
|
+
borderWidth: 3,
|
|
94
|
+
borderColor: '#FFFFFF', // Or your theme color
|
|
95
|
+
backgroundColor: '#FFFFFF',
|
|
96
|
+
},
|
|
97
|
+
inputContainer: {
|
|
98
|
+
flexDirection: 'row',
|
|
99
|
+
alignItems: 'center',
|
|
100
|
+
paddingHorizontal: 8,
|
|
101
|
+
paddingVertical: 8,
|
|
102
|
+
backgroundColor: '#FFFFFF',
|
|
103
|
+
borderRadius: 16,
|
|
104
|
+
shadowColor: '#000',
|
|
105
|
+
shadowOffset: {
|
|
106
|
+
width: 0,
|
|
107
|
+
height: 2,
|
|
108
|
+
},
|
|
109
|
+
shadowOpacity: 0.1,
|
|
110
|
+
shadowRadius: 4,
|
|
111
|
+
elevation: 3,
|
|
112
|
+
borderColor: '#dadada',
|
|
113
|
+
borderWidth: 1,
|
|
114
|
+
borderRadius: 14,
|
|
115
|
+
},
|
|
116
|
+
input: {
|
|
117
|
+
flex: 1,
|
|
118
|
+
fontSize: 16,
|
|
119
|
+
paddingVertical: 8,
|
|
120
|
+
paddingHorizontal: 12,
|
|
121
|
+
color: '#000000',
|
|
122
|
+
},
|
|
123
|
+
inputButton: {
|
|
124
|
+
padding: 6,
|
|
125
|
+
},
|
|
126
|
+
sendButton: {
|
|
127
|
+
padding: 6,
|
|
128
|
+
marginLeft: 'auto',
|
|
129
|
+
},
|
|
130
|
+
disabledButton: {
|
|
131
|
+
opacity: 0.7,
|
|
132
|
+
},
|
|
133
|
+
});
|