mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
fix: smooth transition between text and realtime controls with coordinated animations
- Keep AgentInputArea always mounted, animate opacity (0→1 / 1→0) instead of unmounting - Use controlled opacity animation for GlobalFooter's RealtimeControls on agent screen - Both controls fade in/out over 250ms, synchronized for smooth cross-fade - Disable pointer events on hidden controls to prevent interaction issues - Keep FadeIn/FadeOut behavior on non-agent screens (home, orchestrator) unchanged This eliminates the layout "push" effect where GlobalFooter would take up space while fading in, causing visible jank. Now both controls overlay and cross-fade smoothly without affecting layout. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -68,13 +68,11 @@ export default function AgentScreen() {
|
|||||||
{/* Status bar - always visible, floating above controls */}
|
{/* Status bar - always visible, floating above controls */}
|
||||||
<AgentStatusBar agentId={id!} />
|
<AgentStatusBar agentId={id!} />
|
||||||
|
|
||||||
{/* Controls - only show AgentInputArea when not in realtime mode */}
|
{/* Controls - AgentInputArea always mounted, fades in/out */}
|
||||||
{/* When in realtime mode, GlobalFooter handles showing RealtimeControls */}
|
{/* When in realtime mode, GlobalFooter's RealtimeControls overlays this */}
|
||||||
{!isRealtimeMode && (
|
<View style={styles.controlsContainer}>
|
||||||
<View style={styles.controlsContainer}>
|
<AgentInputArea agentId={id!} isRealtimeMode={isRealtimeMode} />
|
||||||
<AgentInputArea agentId={id!} />
|
</View>
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</ReanimatedAnimated.View>
|
</ReanimatedAnimated.View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
import { View, TextInput, Pressable } from "react-native";
|
import { View, TextInput, Pressable } from "react-native";
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { Mic, ArrowUp, AudioLines, Square } from "lucide-react-native";
|
import { Mic, ArrowUp, AudioLines, Square } from "lucide-react-native";
|
||||||
import { useSession } from "@/contexts/session-context";
|
import { useSession } from "@/contexts/session-context";
|
||||||
import { useRealtime } from "@/contexts/realtime-context";
|
import { useRealtime } from "@/contexts/realtime-context";
|
||||||
import { useAudioRecorder } from "@/hooks/use-audio-recorder";
|
import { useAudioRecorder } from "@/hooks/use-audio-recorder";
|
||||||
|
import Animated, {
|
||||||
|
useAnimatedStyle,
|
||||||
|
withTiming,
|
||||||
|
useSharedValue,
|
||||||
|
} from "react-native-reanimated";
|
||||||
|
|
||||||
interface AgentInputAreaProps {
|
interface AgentInputAreaProps {
|
||||||
agentId: string;
|
agentId: string;
|
||||||
|
isRealtimeMode: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AgentInputArea({ agentId }: AgentInputAreaProps) {
|
export function AgentInputArea({ agentId, isRealtimeMode }: AgentInputAreaProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const { ws, sendAgentMessage, sendAgentAudio } = useSession();
|
const { ws, sendAgentMessage, sendAgentAudio } = useSession();
|
||||||
const { startRealtime } = useRealtime();
|
const { startRealtime } = useRealtime();
|
||||||
@@ -20,6 +26,21 @@ export function AgentInputArea({ agentId }: AgentInputAreaProps) {
|
|||||||
const [isRecording, setIsRecording] = useState(false);
|
const [isRecording, setIsRecording] = useState(false);
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
|
||||||
|
// Animated opacity for smooth transitions
|
||||||
|
const opacity = useSharedValue(isRealtimeMode ? 0 : 1);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
opacity.value = withTiming(isRealtimeMode ? 0 : 1, { duration: 250 });
|
||||||
|
}, [isRealtimeMode]);
|
||||||
|
|
||||||
|
const animatedStyle = useAnimatedStyle(() => {
|
||||||
|
return {
|
||||||
|
opacity: opacity.value,
|
||||||
|
// When hidden, disable pointer events so GlobalFooter's controls are interactive
|
||||||
|
pointerEvents: opacity.value < 0.5 ? ("none" as const) : ("auto" as const),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
async function handleSendMessage() {
|
async function handleSendMessage() {
|
||||||
if (!userInput.trim() || !ws.isConnected) return;
|
if (!userInput.trim() || !ws.isConnected) return;
|
||||||
|
|
||||||
@@ -76,7 +97,7 @@ export function AgentInputArea({ agentId }: AgentInputAreaProps) {
|
|||||||
const hasText = userInput.trim().length > 0;
|
const hasText = userInput.trim().length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<Animated.View style={[styles.container, animatedStyle]}>
|
||||||
{/* Text input */}
|
{/* Text input */}
|
||||||
<TextInput
|
<TextInput
|
||||||
value={userInput}
|
value={userInput}
|
||||||
@@ -136,7 +157,7 @@ export function AgentInputArea({ agentId }: AgentInputAreaProps) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
import { View, Pressable } from "react-native";
|
import { View, Pressable } from "react-native";
|
||||||
import { usePathname } from "expo-router";
|
import { usePathname } from "expo-router";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
@@ -6,7 +7,13 @@ import { AudioLines } from "lucide-react-native";
|
|||||||
import { useRealtime } from "@/contexts/realtime-context";
|
import { useRealtime } from "@/contexts/realtime-context";
|
||||||
import { useSession } from "@/contexts/session-context";
|
import { useSession } from "@/contexts/session-context";
|
||||||
import { RealtimeControls } from "./realtime-controls";
|
import { RealtimeControls } from "./realtime-controls";
|
||||||
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
|
import Animated, {
|
||||||
|
FadeIn,
|
||||||
|
FadeOut,
|
||||||
|
useAnimatedStyle,
|
||||||
|
withTiming,
|
||||||
|
useSharedValue,
|
||||||
|
} from "react-native-reanimated";
|
||||||
|
|
||||||
export function GlobalFooter() {
|
export function GlobalFooter() {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
@@ -22,13 +29,36 @@ export function GlobalFooter() {
|
|||||||
// Hidden when: on agent screen AND realtime is off
|
// Hidden when: on agent screen AND realtime is off
|
||||||
const shouldHide = isAgentScreen && !isRealtimeMode;
|
const shouldHide = isAgentScreen && !isRealtimeMode;
|
||||||
|
|
||||||
|
// Controlled opacity for agent screen transitions (synced with AgentInputArea)
|
||||||
|
const realtimeOpacity = useSharedValue(isRealtimeMode ? 1 : 0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAgentScreen) {
|
||||||
|
// On agent screen, use controlled animation for smooth cross-fade
|
||||||
|
realtimeOpacity.value = withTiming(isRealtimeMode ? 1 : 0, { duration: 250 });
|
||||||
|
}
|
||||||
|
}, [isRealtimeMode, isAgentScreen]);
|
||||||
|
|
||||||
|
const realtimeAnimatedStyle = useAnimatedStyle(() => {
|
||||||
|
return {
|
||||||
|
opacity: realtimeOpacity.value,
|
||||||
|
pointerEvents: realtimeOpacity.value > 0.5 ? ("auto" as const) : ("none" as const),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// If realtime is active, show realtime controls
|
// If realtime is active, show realtime controls
|
||||||
if (isRealtimeMode) {
|
if (isRealtimeMode) {
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
entering={FadeIn.duration(400)}
|
style={[
|
||||||
exiting={FadeOut.duration(250)}
|
styles.container,
|
||||||
style={[styles.container, { paddingBottom: insets.bottom }]}
|
{ paddingBottom: insets.bottom },
|
||||||
|
// Use controlled opacity on agent screen for smooth transition
|
||||||
|
isAgentScreen && realtimeAnimatedStyle
|
||||||
|
]}
|
||||||
|
// Keep FadeIn/FadeOut for non-agent screens (home, orchestrator, etc)
|
||||||
|
entering={!isAgentScreen ? FadeIn.duration(400) : undefined}
|
||||||
|
exiting={!isAgentScreen ? FadeOut.duration(250) : undefined}
|
||||||
>
|
>
|
||||||
<RealtimeControls />
|
<RealtimeControls />
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|||||||
Reference in New Issue
Block a user