refactor: improve UI polish with empty state, spacing, and remove status messages
```
This commit is contained in:
Mohamed Boudra
2025-10-22 19:42:13 +02:00
parent 8b9561b379
commit ce07b54920
3 changed files with 73 additions and 79 deletions
+72 -55
View File
@@ -23,7 +23,7 @@ import { router } from "expo-router";
import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake"; import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { theme as defaultTheme } from "../styles/theme"; import { theme as defaultTheme, theme } from "../styles/theme";
// Simple unique ID generator // Simple unique ID generator
let messageIdCounter = 0; let messageIdCounter = 0;
@@ -154,7 +154,7 @@ function RealtimeCircle({ volume, onClose }: RealtimeCircleProps) {
// Update volume scale when volume changes // Update volume scale when volume changes
useEffect(() => { useEffect(() => {
// Map volume (0-1) to scale range (MIN_SCALE - MAX_SCALE) // Map volume (0-1) to scale range (MIN_SCALE - MAX_SCALE)
const targetScale = MIN_SCALE + (volume * (MAX_SCALE - MIN_SCALE)); const targetScale = MIN_SCALE + volume * (MAX_SCALE - MIN_SCALE);
volumeScale.value = withSpring(targetScale, { volumeScale.value = withSpring(targetScale, {
damping: 15, damping: 15,
stiffness: 150, stiffness: 150,
@@ -199,7 +199,13 @@ function RealtimeCircle({ volume, onClose }: RealtimeCircleProps) {
justifyContent: "center", justifyContent: "center",
}} }}
> >
<View style={{ position: "relative", alignItems: "center", justifyContent: "center" }}> <View
style={{
position: "relative",
alignItems: "center",
justifyContent: "center",
}}
>
{/* Outer pulsating circle */} {/* Outer pulsating circle */}
<ReanimatedAnimated.View <ReanimatedAnimated.View
style={[ style={[
@@ -610,7 +616,11 @@ export default function VoiceAssistantScreen() {
// Drift protection: Don't play audio generated in different mode // Drift protection: Don't play audio generated in different mode
if (data.isRealtimeMode !== currentIsRealtimeMode) { if (data.isRealtimeMode !== currentIsRealtimeMode) {
console.log( console.log(
`[App] Skipping audio playback due to mode drift (generated in ${data.isRealtimeMode ? "realtime" : "normal"} mode, currently in ${currentIsRealtimeMode ? "realtime" : "normal"} mode)` `[App] Skipping audio playback due to mode drift (generated in ${
data.isRealtimeMode ? "realtime" : "normal"
} mode, currently in ${
currentIsRealtimeMode ? "realtime" : "normal"
} mode)`
); );
// Still send confirmation to prevent server from waiting // Still send confirmation to prevent server from waiting
@@ -771,7 +781,6 @@ export default function VoiceAssistantScreen() {
}; };
}, [ws, audioPlayer]); }, [ws, audioPlayer]);
// Voice button handler // Voice button handler
async function handleVoicePress() { async function handleVoicePress() {
if (!ws.isConnected) return; if (!ws.isConnected) return;
@@ -984,29 +993,8 @@ export default function VoiceAssistantScreen() {
}, },
}; };
ws.send(modeMessage); ws.send(modeMessage);
setMessages((prev) => [
...prev,
{
type: "activity",
id: generateMessageId(),
timestamp: Date.now(),
activityType: "success",
message: "Realtime mode started - speak anytime!",
},
]);
} catch (error: any) { } catch (error: any) {
console.error("[App] Failed to start realtime mode:", error); console.error("[App] Failed to start realtime mode:", error);
setMessages((prev) => [
...prev,
{
type: "activity",
id: generateMessageId(),
timestamp: Date.now(),
activityType: "error",
message: `Failed to start realtime mode: ${error.message}`,
},
]);
} }
} else { } else {
// Stop realtime mode // Stop realtime mode
@@ -1024,17 +1012,6 @@ export default function VoiceAssistantScreen() {
}, },
}; };
ws.send(modeMessage); ws.send(modeMessage);
setMessages((prev) => [
...prev,
{
type: "activity",
id: generateMessageId(),
timestamp: Date.now(),
activityType: "info",
message: "Realtime mode stopped",
},
]);
} catch (error: any) { } catch (error: any) {
console.error("[App] Failed to stop realtime mode:", error); console.error("[App] Failed to stop realtime mode:", error);
} }
@@ -1138,10 +1115,22 @@ export default function VoiceAssistantScreen() {
<ScrollView <ScrollView
ref={scrollViewRef} ref={scrollViewRef}
style={styles.scrollView} style={styles.scrollView}
contentContainerStyle={styles.scrollContent} contentContainerStyle={[
styles.scrollContent,
messages.length === 0 && styles.emptyStateContainer,
]}
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag" keyboardDismissMode="on-drag"
> >
{messages.length === 0 && !currentAssistantMessage && (
<View style={styles.emptyState}>
<Text style={styles.emptyStateTitle}>OnTheGo</Text>
<Text style={styles.emptyStateSubtitle}>
What would you like to work on?
</Text>
</View>
)}
{messages.map((msg) => { {messages.map((msg) => {
if (msg.type === "user") { if (msg.type === "user") {
return ( return (
@@ -1226,8 +1215,19 @@ export default function VoiceAssistantScreen() {
> >
{isRealtimeMode ? ( {isRealtimeMode ? (
// Realtime mode - show volume meter and mute button // Realtime mode - show volume meter and mute button
<View style={[styles.inputArea, { minHeight: 200 }]}> <View
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}> style={[
styles.inputArea,
{ minHeight: 200, padding: theme.spacing[4] },
]}
>
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
}}
>
<VolumeMeter <VolumeMeter
volume={realtimeAudio.volume} volume={realtimeAudio.volume}
isMuted={realtimeAudio.isMuted} isMuted={realtimeAudio.isMuted}
@@ -1329,14 +1329,12 @@ export default function VoiceAssistantScreen() {
isRealtimeMode && styles.realtimeButtonActive, isRealtimeMode && styles.realtimeButtonActive,
]} ]}
> >
<Animated.View style={{ transform: [{ scale: pulseAnim }] }}> <Animated.View
style={{ transform: [{ scale: pulseAnim }] }}
>
<AudioLines <AudioLines
size={20} size={20}
color={ color={defaultTheme.colors.background}
isRealtimeMode
? defaultTheme.colors.accentForeground
: defaultTheme.colors.background
}
/> />
</Animated.View> </Animated.View>
</Pressable> </Pressable>
@@ -1408,25 +1406,42 @@ const styles = StyleSheet.create((theme) => ({
paddingBottom: theme.spacing[4], paddingBottom: theme.spacing[4],
flexGrow: 1, flexGrow: 1,
}, },
emptyStateContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
emptyState: {
alignItems: "center",
justifyContent: "center",
paddingHorizontal: theme.spacing[6],
},
emptyStateTitle: {
fontSize: theme.fontSize["4xl"],
fontWeight: "700",
color: theme.colors.foreground,
marginBottom: theme.spacing[2],
},
emptyStateSubtitle: {
fontSize: theme.fontSize.lg,
color: theme.colors.mutedForeground,
textAlign: "center",
},
inputAreaWrapper: { inputAreaWrapper: {
borderTopRightRadius: theme.borderRadius["2xl"], borderTopRightRadius: theme.borderRadius["2xl"],
borderTopLeftRadius: theme.borderRadius["2xl"], borderTopLeftRadius: theme.borderRadius["2xl"],
paddingTop: theme.spacing[2],
paddingHorizontal: theme.spacing[2],
borderTopWidth: theme.borderWidth[1], borderTopWidth: theme.borderWidth[1],
borderTopColor: theme.colors.border, borderTopColor: theme.colors.border,
backgroundColor: theme.colors.muted, backgroundColor: theme.colors.muted,
}, },
inputArea: { inputArea: {},
borderRadius: theme.borderRadius["2xl"],
paddingHorizontal: theme.spacing[2],
paddingVertical: theme.spacing[3],
},
textInput: { textInput: {
paddingTop: theme.spacing[4],
paddingHorizontal: theme.spacing[4],
borderRadius: theme.borderRadius["2xl"],
paddingVertical: theme.spacing[3],
backgroundColor: "transparent", backgroundColor: "transparent",
color: theme.colors.foreground, color: theme.colors.foreground,
paddingHorizontal: 0,
paddingVertical: 0,
fontSize: theme.fontSize.lg, fontSize: theme.fontSize.lg,
marginBottom: theme.spacing[3], marginBottom: theme.spacing[3],
maxHeight: 128, maxHeight: 128,
@@ -1437,6 +1452,8 @@ const styles = StyleSheet.create((theme) => ({
justifyContent: "flex-end", justifyContent: "flex-end",
gap: theme.spacing[2], gap: theme.spacing[2],
marginBottom: theme.spacing[1], marginBottom: theme.spacing[1],
paddingHorizontal: theme.spacing[4],
paddingBottom: theme.spacing[2],
}, },
realtimeButton: { realtimeButton: {
width: 40, width: 40,
+1
View File
@@ -170,6 +170,7 @@ const commonTheme = {
xl: 18, xl: 18,
"2xl": 20, "2xl": 20,
"3xl": 24, "3xl": 24,
"4xl": 32,
}, },
fontWeight: { fontWeight: {
@@ -82,30 +82,6 @@ export class VoiceAssistantWebSocketServer {
})` })`
); );
// Send welcome message
this.sendToClient(
ws,
wrapSessionMessage({
type: "status",
payload: {
status: "connected",
message: "WebSocket connection established",
},
})
);
// Send conversation confirmation (whether new or loaded)
this.sendToClient(
ws,
wrapSessionMessage({
type: "conversation_loaded",
payload: {
conversationId: session.getConversationId(),
messageCount: initialMessages?.length || 0,
},
})
);
// Set up message handler // Set up message handler
ws.on("message", (data) => { ws.on("message", (data) => {
this.handleMessage(ws, data); this.handleMessage(ws, data);