Creating a RN example for simple-chatbot

This commit is contained in:
Filipi Fuchter
2025-05-05 18:17:39 -03:00
parent b323a7bd88
commit 40154824e8
26 changed files with 12366 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
import React from "react"
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import PreJoinView from './views/PreJoinView';
import MeetingView from './views/MeetingView';
import { VoiceClientProvider } from './context/VoiceClientContext';
import Toast from 'react-native-toast-message';
import { useVoiceClientNavigation } from './hooks/useVoiceClientNavigation';
const Stack = createStackNavigator();
const NavigationManager: React.FC = () => {
useVoiceClientNavigation(); // This hook now controls the navigation based on the connection state.
return null; // This component doesn't render anything but manages navigation.
};
const App: React.FC = () => {
return (
<VoiceClientProvider>
<NavigationContainer>
<Stack.Navigator initialRouteName="Prejoin">
<Stack.Screen name="Prejoin" component={PreJoinView} options={{ headerShown: false }}/>
<Stack.Screen name="Meeting" component={MeetingView} options={{ headerShown: false }}/>
</Stack.Navigator>
<NavigationManager />
<Toast />
</NavigationContainer>
</VoiceClientProvider>
);
};
export default App;

View File

@@ -0,0 +1,113 @@
import { View, Image, StyleSheet, LayoutChangeEvent, ImageStyle, ViewStyle } from 'react-native';
import React, { useMemo, useState } from 'react';
import { Icons } from '../theme/Assets';
import Colors from '../theme/Colors';
import { useVoiceClient } from '../context/VoiceClientContext';
import { VoiceClientVideoView } from '@pipecat-ai/react-native-daily-transport';
interface CameraButtonViewProps {
style?: ViewStyle; // Optional additional styles for the button container
}
const CameraButtonView: React.FC<CameraButtonViewProps> = ({ style }) => {
const { videoTrack, isCamEnabled } = useVoiceClient();
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const onLayout = (event: LayoutChangeEvent) => {
const { width, height } = event.nativeEvent.layout;
setDimensions({ width, height });
};
const mediaComponent = useMemo(() => {
return (
<VoiceClientVideoView
videoTrack={videoTrack || null}
audioTrack={null}
mirror={true}
zOrder={1}
style={styles.media}
objectFit="cover"
/>
);
}, [videoTrack]);
const { width } = dimensions;
const circleSize = width * 0.9;
const innerCircleSize = width * 0.82;
return (
<View style={[styles.container, style]} onLayout={onLayout}>
<View
style={[
styles.outerCircle,
{ width: circleSize, height: circleSize, borderRadius: circleSize / 2 },
]}
>
{isCamEnabled ? (
<View style={[styles.videoView, { borderRadius: circleSize / 2 }]}>
{mediaComponent}
</View>
) : (
<>
<View
style={[
styles.innerCircle,
{
width: innerCircleSize,
height: innerCircleSize,
borderRadius: innerCircleSize / 2,
},
]}
/>
<Image
source={Icons.vision}
style={[
styles.image,
{
width: width * 0.3,
height: width * 0.3,
tintColor: 'green',
},
]}
resizeMode="contain"
/>
</>
)}
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
justifyContent: 'center',
alignItems: 'center',
} as ViewStyle,
outerCircle: {
borderWidth: 1,
borderColor: Colors.buttonsBorder,
justifyContent: 'center',
alignItems: 'center',
} as ViewStyle,
innerCircle: {
backgroundColor: Colors.disabledVision,
position: 'absolute',
} as ViewStyle,
videoView: {
aspectRatio: 1,
width: '100%',
height: '100%',
overflow: 'hidden',
} as ViewStyle,
image: {} as ImageStyle,
media: {
width: '100%',
height: '100%',
position: 'absolute',
},
});
export default CameraButtonView;

View File

@@ -0,0 +1,94 @@
import React, { useState, useMemo } from 'react';
import { View, StyleSheet, LayoutChangeEvent, ViewStyle } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import Colors from '../theme/Colors';
import { useVoiceClient } from '../context/VoiceClientContext';
interface MicrophoneViewProps {
style?: ViewStyle; // Optional additional styles for the button container
}
const MicrophoneView: React.FC<MicrophoneViewProps> = ({ style }) => {
const { isMicEnabled, localAudioLevel: audioLevel } = useVoiceClient();
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const onLayout = (event: LayoutChangeEvent) => {
const { width, height } = event.nativeEvent.layout;
setDimensions({ width, height });
};
const { width } = dimensions;
const circleSize = useMemo(() => width * 0.9, [width]);
const innerCircleSize = useMemo(() => width * 0.82, [width]);
const audioCircleSize = useMemo(() => audioLevel * width * 0.95, [audioLevel, width]);
return (
<View style={[styles.container, style]} onLayout={onLayout}>
<View
style={[
styles.outerCircle,
{ width: circleSize, height: circleSize, borderRadius: circleSize / 2 },
]}
>
<View
style={[
styles.innerCircle,
{
backgroundColor: !isMicEnabled ? Colors.disabledMic : Colors.backgroundCircle,
width: innerCircleSize,
height: innerCircleSize,
borderRadius: innerCircleSize / 2,
},
]}
/>
{isMicEnabled && (
<View
style={[
styles.audioCircle,
{
width: audioCircleSize,
height: audioCircleSize,
borderRadius: audioCircleSize / 2,
},
]}
/>
)}
<MaterialIcons
name={!isMicEnabled ? "mic-off" : "mic"}
size={width * 0.2}
color="white"
style={styles.micIcon}
/>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
justifyContent: 'center',
alignItems: 'center',
} as ViewStyle,
outerCircle: {
borderWidth: 1,
borderColor: Colors.buttonsBorder,
justifyContent: 'center',
alignItems: 'center',
} as ViewStyle,
innerCircle: {
position: 'absolute',
} as ViewStyle,
audioCircle: {
position: 'absolute',
backgroundColor: Colors.micVolume,
opacity: 0.5,
} as ViewStyle,
micIcon: {
position: 'absolute',
},
});
export default MicrophoneView;

View File

@@ -0,0 +1,128 @@
import React, { useEffect, useState } from 'react';
import { LayoutChangeEvent, StyleSheet, Text, View, ViewStyle } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import Colors from '../theme/Colors';
import { useVoiceClient } from '../context/VoiceClientContext';
const dotCount = 5;
const WaveformView: React.FC = () => {
const [audioLevels, setAudioLevels] = useState(Array(dotCount).fill(0));
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const { currentState: voiceClientStatus, botReady: isBotReady, remoteAudioLevel: audioLevel } = useVoiceClient();
const onLayout = (event: LayoutChangeEvent) => {
const { width, height } = event.nativeEvent.layout;
setDimensions({ width, height });
};
useEffect(() => {
setAudioLevels((prevLevels) => [...prevLevels.slice(1), audioLevel]);
}, [audioLevel]);
const { width, height } = dimensions;
const circleSize = width * 0.9;
const innerCircleSize = width * 0.82;
const barWidth = (width * 0.5) / dotCount;
return (
<View style={styles.container} onLayout={onLayout}>
<View style={[styles.outerCircle, { width: circleSize, height: circleSize, borderRadius: circleSize / 2 }]}>
<View
style={[
styles.innerCircle,
{
backgroundColor: isBotReady ? Colors.backgroundCircle : Colors.backgroundCircleNotConnected,
width: innerCircleSize,
height: innerCircleSize,
borderRadius: innerCircleSize / 2,
},
]}
>
{isBotReady ? (
audioLevel > 0 ? (
<View style={[styles.waveformContainer, { width: width * 0.5, height: width * 0.5 }]}>
{audioLevels.map((level, index) => (
<View
key={index}
style={[
styles.waveformBar,
{
width: barWidth - 10, // Subtract some margin
height: level * height,
},
]}
/>
))}
</View>
) : (
<View style={[styles.dotContainer, { width: width * 0.5, height: height * 0.5 }]}>
{Array(dotCount)
.fill(0)
.map((_, index) => (
<View key={index} style={[styles.dot, { width: height * 0.1, height: height * 0.1 }]} />
))}
</View>
)
) : (
<View style={styles.notReadyContainer}>
<MaterialIcons name="hourglass-empty" size={32} color="white" />
<Text style={styles.voiceClientStatusText}>{voiceClientStatus}</Text>
</View>
)}
</View>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
justifyContent: 'center',
alignItems: 'center',
width: "100%",
} as ViewStyle,
outerCircle: {
borderWidth: 1,
borderColor: 'gray',
justifyContent: 'center',
alignItems: 'center',
} as ViewStyle,
innerCircle: {
justifyContent: 'center',
alignItems: 'center',
position: 'relative',
} as ViewStyle,
waveformContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
} as ViewStyle,
waveformBar: {
backgroundColor: 'white',
maxHeight: '100%',
borderRadius: 12,
} as ViewStyle,
dotContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
} as ViewStyle,
dot: {
backgroundColor: 'white',
borderRadius: 50,
} as ViewStyle,
notReadyContainer: {
justifyContent: 'center',
alignItems: 'center',
} as ViewStyle,
voiceClientStatusText: {
color: 'white',
marginTop: 10,
fontSize: 16,
fontWeight: 'bold',
} as ViewStyle,
});
export default WaveformView;

View File

@@ -0,0 +1,229 @@
import React, { createContext, useState, useContext, ReactNode, useCallback, useMemo, useRef, useEffect } from 'react'
import Toast from 'react-native-toast-message'
import { RNDailyTransport } from '@pipecat-ai/react-native-daily-transport'
import { RTVIClient, TransportState, RTVIMessage, Participant } from '@pipecat-ai/client-js'
import { MediaStreamTrack } from '@daily-co/react-native-webrtc'
import { SettingsManager } from '../settings/SettingsManager';
interface VoiceClientContextProps {
voiceClient: RTVIClient | null
inCall: boolean
currentState: string
botReady: boolean
localAudioLevel: number
remoteAudioLevel: number
isMicEnabled: boolean
isCamEnabled: boolean
videoTrack?: MediaStreamTrack
timerCountDown: number
// methods
start: (url: string) => Promise<void>
leave: () => void
toggleMicInput: () => void
toggleCamInput: () => void
}
export const VoiceClientContext = createContext<VoiceClientContextProps | undefined>(undefined)
interface VoiceClientProviderProps {
children: ReactNode
}
export const VoiceClientProvider: React.FC<VoiceClientProviderProps> = ({ children }) => {
const [voiceClient, setVoiceClient] = useState<RTVIClient | null>(null)
const [inCall, setInCall] = useState<boolean>(false)
const [currentState, setCurrentState] = useState<TransportState>("disconnected")
const [botReady, setBotReady] = useState<boolean>(false)
const [isMicEnabled, setIsMicEnabled] = useState<boolean>(false)
const [isCamEnabled, setIsCamEnabled] = useState<boolean>(false)
const [videoTrack, setVideoTrack] = useState<MediaStreamTrack>()
const [localAudioLevel, setLocalAudioLevel] = useState<number>(0)
const [remoteAudioLevel, setRemoteAudioLevel] = useState<number>(0)
const [timerCountDown, setTimerCountDown] = useState<number>(0)
const botSpeakingRef = useRef(false)
let meetingTimer: NodeJS.Timeout | null
const createVoiceClient = useCallback((url: string): RTVIClient => {
return new RTVIClient({
transport: new RNDailyTransport(),
params: {
baseUrl: url,
endpoints: {
connect: "/connect"
}
},
enableMic: true,
enableCam: false
})
}, [])
const handleError = useCallback((error: any) => {
console.log("Error occurred:", error)
const errorMessage = error.message || error.data?.error || "An unexpected error occurred"
Toast.show({
type: 'error',
text1: errorMessage,
})
}, [])
const setupListeners = useCallback((voiceClient: RTVIClient): void => {
const inCallStates = new Set(["authenticating", "connecting", "connected", "ready"])
voiceClient
.on("transportStateChanged", (state: TransportState) => {
setCurrentState(voiceClient.state)
setInCall(inCallStates.has(state))
})
.on("error", (error: RTVIMessage) => {
handleError(error)
})
.on("botReady", () => {
setBotReady(true)
let expirationTime = voiceClient.transportExpiry
if (expirationTime) {
startTimer(expirationTime)
}
})
.on("disconnected", () => {
setBotReady(false)
stopTimer()
setIsMicEnabled(false)
setIsCamEnabled(false)
})
.on("localAudioLevel", (level: number) => {
setLocalAudioLevel(level)
})
.on("remoteAudioLevel", (level: number) => {
if (botSpeakingRef.current) {
setRemoteAudioLevel(level)
}
})
.on("userStartedSpeaking", () => {
// nothing to do here
})
.on("userStoppedSpeaking", () => {
setLocalAudioLevel(0)
})
.on("botStartedSpeaking", () => {
botSpeakingRef.current = true
})
.on("botStoppedSpeaking", () => {
botSpeakingRef.current = false
setRemoteAudioLevel(0)
})
.on("connected", () => {
setIsMicEnabled(voiceClient.isMicEnabled)
setIsCamEnabled(voiceClient.isCamEnabled)
})
.on("trackStarted", (track: MediaStreamTrack, p?: Participant) => {
if (p?.local && track.kind === 'video'){
setVideoTrack(track)
}
})
}, [handleError])
const start = useCallback(async (url: string): Promise<void> => {
const client = createVoiceClient(url)
setVoiceClient(client)
setupListeners(client)
try {
await client.connect()
// updating the preferences
const newSettings = await SettingsManager.getSettings();
newSettings.backendURL = url
await SettingsManager.updateSettings(newSettings)
} catch (error) {
handleError(error)
}
}, [createVoiceClient, setupListeners, handleError])
const leave = useCallback(async (): Promise<void> => {
if (voiceClient) {
await voiceClient.disconnect()
setVoiceClient(null)
}
}, [voiceClient])
const toggleMicInput = useCallback(async (): Promise<void> => {
if (voiceClient) {
try {
let enableMic = !isMicEnabled
voiceClient.enableMic(enableMic)
setIsMicEnabled(enableMic)
} catch (e) {
handleError(e)
}
}
}, [voiceClient, isMicEnabled])
const toggleCamInput = useCallback(async (): Promise<void> => {
if (voiceClient) {
try {
let enableCam = !isCamEnabled
voiceClient.enableCam(enableCam)
setIsCamEnabled(enableCam)
} catch (e) {
handleError(e)
}
}
}, [voiceClient, isCamEnabled])
const startTimer = (expirationTime: number): void => {
const currentTime = Math.floor(Date.now() / 1000)
const leftTime = expirationTime - currentTime
setTimerCountDown(leftTime)
meetingTimer = setInterval(() => {
setTimerCountDown((prevCountDown) => {
return prevCountDown - 1
})
}, 1000)
}
const stopTimer = (): void => {
if (meetingTimer) {
clearInterval(meetingTimer)
meetingTimer = null
}
setTimerCountDown(0)
}
useEffect(() => {
return () => {
if (voiceClient) {
voiceClient.removeAllListeners() // Cleanup on unmount
}
}
}, [voiceClient])
const contextValue = useMemo(() => ({
voiceClient,
inCall,
currentState,
botReady,
isMicEnabled,
isCamEnabled,
localAudioLevel,
remoteAudioLevel,
videoTrack,
timerCountDown,
start,
leave,
toggleMicInput,
toggleCamInput
}), [voiceClient, inCall, currentState, botReady, isMicEnabled, isCamEnabled, localAudioLevel, remoteAudioLevel, videoTrack, timerCountDown, start, leave, toggleMicInput, toggleCamInput])
return (
<VoiceClientContext.Provider value={contextValue}>
{children}
</VoiceClientContext.Provider>
)
}
export const useVoiceClient = (): VoiceClientContextProps => {
const context = useContext(VoiceClientContext)
if (!context) {
throw new Error('useVoiceClient must be used within a VoiceClientProvider')
}
return context
}

View File

@@ -0,0 +1,21 @@
import { useEffect } from 'react';
import { useNavigation } from '@react-navigation/native';
import { useVoiceClient } from '../context/VoiceClientContext';
export const useVoiceClientNavigation = () => {
const navigation = useNavigation();
const { inCall } = useVoiceClient();
useEffect(() => {
if (inCall) {
// TODO, refactor this
// @ts-ignore
navigation.navigate('Meeting');
} else {
// TODO, refactor this
// @ts-ignore
navigation.navigate('Prejoin');
}
}, [inCall, navigation]);
};

View File

@@ -0,0 +1,42 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
export interface SettingsManager {
enableCam: boolean;
enableMic: boolean;
backendURL: string;
}
// Define the settings object
const defaultSettings: SettingsManager = {
enableCam: false,
enableMic: true,
backendURL: process.env.EXPO_PUBLIC_BASE_URL || "https://api.daily.co/v1/bots",
};
export class SettingsManager {
private static preferencesKey = 'settingsPreference';
static async getSettings(): Promise<SettingsManager> {
try {
const data = await AsyncStorage.getItem(this.preferencesKey);
if (data !== null) {
return JSON.parse(data) as SettingsManager;
} else {
return defaultSettings;
}
} catch (error) {
console.error("Failed to load settings:", error);
return defaultSettings;
}
}
static async updateSettings(settings: SettingsManager): Promise<void> {
try {
const data = JSON.stringify(settings);
await AsyncStorage.setItem(this.preferencesKey, data);
} catch (error) {
console.error("Failed to save settings:", error);
}
}
}

View File

@@ -0,0 +1,7 @@
export const Images = {
dailyBot: require('../../assets/images/pipecat.png'),
};
export const Icons = {
vision: require('../../assets/icons/vision.png'),
};

View File

@@ -0,0 +1,27 @@
type ColorsType = {
white: string;
black: string;
backgroundCircle: string;
backgroundCircleNotConnected: string;
backgroundApp: string;
buttonsBorder: string;
micVolume: string;
timer: string;
disabledMic: string;
disabledVision: string;
};
const Colors: ColorsType = {
white: '#ffffff',
black: '#000000',
backgroundCircle: '#374151',
backgroundCircleNotConnected: '#D1D5DB',
backgroundApp: '#F9FAFB',
buttonsBorder: '#E5E7EB',
micVolume: '#86EFAC',
timer: '#E5E7EB',
disabledMic: '#ee6b6e',
disabledVision: '#BBF7D0',
};
export default Colors;

View File

@@ -0,0 +1,62 @@
import React from 'react';
import { TouchableOpacity, Text, StyleSheet, ViewStyle, TextStyle, GestureResponderEvent } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
interface CustomButtonProps {
title: string;
onPress: (event: GestureResponderEvent) => void;
backgroundColor?: string; // Optional prop for background color
textColor?: string; // Optional prop for text color
style?: ViewStyle; // Optional additional styles for the button container
textStyle?: TextStyle; // Optional additional styles for the text
iconName?: string; // Optional prop for the icon name from MaterialIcons
iconPosition?: 'left' | 'right'; // Optional prop to control icon position
iconSize?: number; // Optional prop for icon size
iconColor?: string; // Optional prop for icon color
}
const CustomButton: React.FC<CustomButtonProps> = ({
title,
onPress,
backgroundColor = 'black',
textColor = 'white',
style,
textStyle,
iconName,
iconPosition = 'left',
iconSize = 24,
iconColor = 'white',
}) => {
return (
<TouchableOpacity
onPress={onPress}
style={[styles.button, { backgroundColor }, style]}>
{iconName && iconPosition === 'left' && (
<MaterialIcons name={iconName as keyof typeof MaterialIcons.glyphMap} size={iconSize} color={iconColor} style={styles.icon} />
)}
<Text style={[styles.text, { color: textColor }, textStyle]}>{title}</Text>
{iconName && iconPosition === 'right' && (
<MaterialIcons name={iconName as keyof typeof MaterialIcons.glyphMap} size={iconSize} color={iconColor} style={styles.icon} />
)}
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
button: {
padding: 12,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row', // Ensures icon and text are aligned in a row
},
text: {
fontSize: 16,
fontWeight: 'bold',
},
icon: {
marginHorizontal: 5, // Adds space between the icon and text
},
});
export default CustomButton;

View File

@@ -0,0 +1,159 @@
import {
View,
StyleSheet,
Text,
Image,
TouchableOpacity,
} from 'react-native';
import React from "react"
import { useVoiceClient } from '../context/VoiceClientContext';
import { Images } from '../theme/Assets';
import { MaterialIcons } from '@expo/vector-icons';
import WaveformView from '../components/WaveformView';
import MicrophoneView from '../components/MicrophoneView';
import CameraButtonView from '../components/CameraButtonView';
import { SafeAreaView } from 'react-native-safe-area-context';
import Colors from '../theme/Colors';
import CustomButton from '../theme/CustomButton';
const MeetingView: React.FC = () => {
const { leave, toggleMicInput, toggleCamInput, timerCountDown } = useVoiceClient();
const timerString = (count: number): string => {
const hours = Math.floor(count / 3600);
const minutes = Math.floor((count % 3600) / 60);
const seconds = count % 60;
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
};
return (
<SafeAreaView style={styles.safeArea}>
<View style={styles.container}>
<View style={styles.header}>
<Image source={Images.dailyBot} style={styles.botImage} />
<View style={styles.timerContainer}>
<MaterialIcons name="timelapse" size={24} color="black" />
<Text style={styles.timerText}>{timerString(timerCountDown)}</Text>
</View>
</View>
<View style={styles.mainPanel}>
<WaveformView/>
<View style={styles.bottomControls}>
<TouchableOpacity onPress={toggleMicInput}>
<MicrophoneView
style={styles.microphone}
/>
</TouchableOpacity>
<TouchableOpacity onPress={toggleCamInput}>
<CameraButtonView
style={styles.camera}
/>
</TouchableOpacity>
</View>
</View>
{/* Bottom Panel */}
<View style={styles.bottomPanel}>
<CustomButton
title="End"
iconName={"exit-to-app"}
onPress={leave}
backgroundColor={Colors.black}
/>
</View>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
safeArea: {
flex: 1,
width: "100%",
backgroundColor: Colors.backgroundApp,
},
container: {
flex: 1,
padding: 20,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingBottom: 10,
},
botImage: {
width: 48,
height: 48,
},
timerContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: Colors.timer,
padding: 10,
borderRadius: 12,
},
timerText: {
color: 'black',
fontWeight: '500',
fontSize: 18,
marginLeft: 5,
},
mainPanel: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
bottomControls: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
width: '100%',
paddingBottom: 20,
},
microphone: {
width: 160,
height: 160,
},
camera: {
width: 120,
height: 120,
},
bottomPanel: {
paddingVertical: 10,
},
settingsButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
borderColor: '#ccc', // Replace with Color.buttonsBorder equivalent
borderRadius: 12,
padding: 10,
marginBottom: 10,
},
settingsText: {
marginLeft: 5,
color: 'black',
},
endButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'black',
borderRadius: 12,
padding: 10,
},
endText: {
marginLeft: 5,
color: 'white',
},
});
export default MeetingView;

View File

@@ -0,0 +1,82 @@
import {
View,
StyleSheet,
Text,
TextInput,
Image
} from "react-native"
import React, { useEffect, useState } from 'react';
import { useVoiceClient } from '../context/VoiceClientContext';
import Colors from '../theme/Colors';
import { Images } from '../theme/Assets';
import CustomButton from '../theme/CustomButton';
import { SettingsManager } from '../settings/SettingsManager';
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
backgroundColor: Colors.backgroundApp,
justifyContent: 'center',
alignItems: 'center',
},
image: {
width: 64,
height: 64,
marginBottom: 20,
},
header: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 20,
},
textInput: {
width: '100%',
padding: 10,
borderColor: Colors.buttonsBorder,
backgroundColor: Colors.white,
borderWidth: 1,
borderRadius: 5,
marginBottom: 10,
},
lastTextInput: {
marginBottom: 20,
},
});
const PreJoinView: React.FC = () => {
const { start } = useVoiceClient();
const [backendURL, setBackendURL] = useState<string>('')
useEffect(() => {
const loadSettings = async () => {
const loadedSettings = await SettingsManager.getSettings();
setBackendURL(loadedSettings.backendURL)
};
loadSettings();
}, []);
return (
<View style={styles.container}>
<Image source={Images.dailyBot} style={styles.image} />
<Text style={styles.header}>Connect to Pipecat.</Text>
<TextInput
placeholder="Server URL"
value={backendURL}
onChangeText={setBackendURL}
style={[styles.textInput, styles.lastTextInput]}
/>
<CustomButton
title="Connect"
onPress={() => start(backendURL)}
backgroundColor={Colors.backgroundCircle}
/>
</View>
)
};
export default PreJoinView;