suna/apps/mobile/components/RightPanel.tsx

417 lines
15 KiB
TypeScript
Raw Normal View History

2025-06-24 05:15:02 +08:00
import Slider from '@react-native-community/slider';
import { ChevronLeft, ChevronRight, CircleDashed, Computer, X } from 'lucide-react-native';
import React, { useCallback, useEffect } from 'react';
import { Dimensions, ScrollView, TouchableOpacity, View } from 'react-native';
2025-06-17 04:21:36 +08:00
import { useSafeAreaInsets } from 'react-native-safe-area-context';
2025-06-24 05:15:02 +08:00
import { Message } from '@/api/chat-api';
import { useTheme, useThemedStyles } from '@/hooks/useThemeColor';
import {
useCloseToolView,
useIsGenerating,
useJumpToLatest,
useNavigateToSnapshot,
useSetNavigationMode,
useToolViewState,
useUpdateToolSnapshots
} from '@/stores/ui-store';
2025-07-11 06:27:59 +08:00
import { ToolView } from './ToolViews/ToolViewRegistry';
2025-06-24 05:15:02 +08:00
import { Body, Caption, H4 } from './Typography';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
2025-06-17 04:21:36 +08:00
interface RightPanelProps {
isVisible: boolean;
onClose: () => void;
2025-06-24 05:15:02 +08:00
messages?: Message[];
2025-07-11 06:27:59 +08:00
sandboxId?: string;
2025-06-17 04:21:36 +08:00
}
2025-07-11 06:27:59 +08:00
export const RightPanel: React.FC<RightPanelProps> = ({ isVisible, onClose, messages = [], sandboxId }) => {
2025-06-17 04:21:36 +08:00
const insets = useSafeAreaInsets();
2025-06-24 05:15:02 +08:00
const theme = useTheme();
const toolViewState = useToolViewState();
const closeToolView = useCloseToolView();
const updateToolSnapshots = useUpdateToolSnapshots();
const navigateToSnapshot = useNavigateToSnapshot();
const jumpToLatest = useJumpToLatest();
const setNavigationMode = useSetNavigationMode();
const isGenerating = useIsGenerating();
const {
toolCallSnapshots,
currentSnapshotIndex,
navigationMode,
selectedToolCall
} = toolViewState;
// Update tool snapshots when messages change
useEffect(() => {
if (messages.length > 0) {
updateToolSnapshots(messages);
}
}, [messages, updateToolSnapshots]);
2025-06-17 04:21:36 +08:00
const styles = useThemedStyles((theme) => ({
panel: {
backgroundColor: theme.sidebar,
flex: 1,
height: '100%' as const,
2025-06-24 05:15:02 +08:00
paddingTop: insets.top,
2025-06-25 04:43:44 +08:00
paddingBottom: 0, // Remove bottom padding to let timeline handle it
2025-06-17 04:21:36 +08:00
},
header: {
flexDirection: 'row' as const,
justifyContent: 'space-between' as const,
alignItems: 'center' as const,
2025-06-24 05:15:02 +08:00
paddingHorizontal: 16,
paddingVertical: 12,
2025-06-17 04:21:36 +08:00
borderBottomWidth: 1,
borderBottomColor: theme.border,
2025-06-24 05:15:02 +08:00
},
headerLeft: {
flexDirection: 'row' as const,
alignItems: 'center' as const,
flex: 1,
},
headerActions: {
flexDirection: 'row' as const,
alignItems: 'center' as const,
2025-06-17 04:21:36 +08:00
},
title: {
color: theme.foreground,
2025-06-24 05:15:02 +08:00
marginLeft: 8,
2025-06-17 04:21:36 +08:00
},
closeButton: {
width: 32,
height: 32,
borderRadius: 16,
2025-06-24 05:15:02 +08:00
backgroundColor: theme.muted + '20',
2025-06-17 04:21:36 +08:00
justifyContent: 'center' as const,
alignItems: 'center' as const,
},
content: {
flex: 1,
},
2025-06-25 04:43:44 +08:00
toolViewContainer: {
flexGrow: 1,
backgroundColor: theme.background,
},
2025-06-24 05:15:02 +08:00
emptyContainer: {
flex: 1,
justifyContent: 'center' as const,
alignItems: 'center' as const,
paddingHorizontal: 32,
},
emptyIcon: {
width: 64,
height: 64,
borderRadius: 32,
backgroundColor: theme.muted + '20',
justifyContent: 'center' as const,
alignItems: 'center' as const,
marginBottom: 16,
},
emptyTitle: {
color: theme.foreground,
textAlign: 'center' as const,
marginBottom: 8,
},
emptySubtitle: {
color: theme.mutedForeground,
textAlign: 'center' as const,
lineHeight: 20,
},
// Timeline controls at bottom
timelineContainer: {
borderTopWidth: 1,
borderTopColor: theme.border,
2025-06-25 04:43:44 +08:00
backgroundColor: theme.sidebar,
paddingHorizontal: 20,
paddingVertical: 16,
paddingBottom: 16 + insets.bottom, // Add safe area padding
2025-06-24 05:15:02 +08:00
},
2025-06-25 04:43:44 +08:00
timelineHeader: {
2025-06-24 05:15:02 +08:00
flexDirection: 'row' as const,
2025-06-25 04:43:44 +08:00
justifyContent: 'space-between' as const,
2025-06-24 05:15:02 +08:00
alignItems: 'center' as const,
2025-06-25 04:43:44 +08:00
marginBottom: 12,
2025-06-24 05:15:02 +08:00
},
counter: {
2025-06-25 04:43:44 +08:00
color: theme.foreground,
fontSize: 13,
fontWeight: '600' as const,
2025-06-24 05:15:02 +08:00
fontFamily: 'monospace',
},
2025-06-25 04:43:44 +08:00
statusBadge: {
2025-06-24 05:15:02 +08:00
flexDirection: 'row' as const,
alignItems: 'center' as const,
2025-06-25 04:43:44 +08:00
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 16,
borderWidth: 1,
2025-06-24 05:15:02 +08:00
},
statusDot: {
width: 6,
height: 6,
borderRadius: 3,
marginRight: 6,
},
statusText: {
fontSize: 11,
fontWeight: '600' as const,
},
2025-06-25 04:43:44 +08:00
sliderContainer: {
flexDirection: 'row' as const,
alignItems: 'center' as const,
marginBottom: 8,
},
navButton: {
width: 32,
height: 32,
2025-07-15 04:52:04 +08:00
borderRadius: 12,
2025-06-25 04:43:44 +08:00
backgroundColor: theme.muted,
justifyContent: 'center' as const,
alignItems: 'center' as const,
},
navButtonDisabled: {
opacity: 0.3,
backgroundColor: theme.muted + '50',
},
slider: {
flex: 1,
height: 24,
marginHorizontal: 16,
},
2025-06-24 05:15:02 +08:00
runningBadge: {
flexDirection: 'row' as const,
alignItems: 'center' as const,
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
backgroundColor: theme.accent + '20',
marginRight: 8,
},
runningText: {
color: theme.accent,
fontSize: 11,
fontWeight: '600' as const,
marginLeft: 4,
2025-06-17 04:21:36 +08:00
},
}));
2025-06-24 05:15:02 +08:00
const handleClose = () => {
closeToolView();
onClose();
};
const currentSnapshot = toolCallSnapshots[currentSnapshotIndex];
const totalSnapshots = toolCallSnapshots.length;
const canGoPrevious = currentSnapshotIndex > 0;
const canGoNext = currentSnapshotIndex < totalSnapshots - 1;
const isLiveMode = navigationMode === 'live';
const showJumpToLatest = navigationMode === 'manual' && !isGenerating;
const showJumpToLive = navigationMode === 'manual' && isGenerating;
const handlePrevious = useCallback(() => {
if (canGoPrevious) {
navigateToSnapshot(currentSnapshotIndex - 1);
}
}, [canGoPrevious, currentSnapshotIndex, navigateToSnapshot]);
const handleNext = useCallback(() => {
if (canGoNext) {
navigateToSnapshot(currentSnapshotIndex + 1);
}
}, [canGoNext, currentSnapshotIndex, navigateToSnapshot]);
const handleSliderChange = useCallback((value: number) => {
const index = Math.round(value);
navigateToSnapshot(index);
}, [navigateToSnapshot]);
const handleJumpToLatest = useCallback(() => {
jumpToLatest();
}, [jumpToLatest]);
2025-06-25 04:43:44 +08:00
const renderStatusBadge = () => {
const isOnLatest = currentSnapshotIndex === totalSnapshots - 1;
2025-09-02 00:56:56 +08:00
if (isLiveMode) {
if (isGenerating) {
return (
<View style={[styles.statusBadge, {
backgroundColor: theme.primary + '10',
borderColor: theme.primary + '30'
}]}>
<View style={[styles.statusDot, { backgroundColor: theme.primary }]} />
<Caption style={[styles.statusText, { color: theme.primary }]}>Live Updates</Caption>
</View>
);
} else {
return (
<View style={[styles.statusBadge, {
backgroundColor: theme.accent + '10',
borderColor: theme.accent + '30'
}]}>
<View style={[styles.statusDot, { backgroundColor: theme.accent }]} />
<Caption style={[styles.statusText, { color: theme.accent }]}>Latest Tool</Caption>
</View>
);
}
2025-06-24 05:15:02 +08:00
} else {
2025-09-02 00:56:56 +08:00
if (isGenerating) {
return (
<TouchableOpacity
style={[styles.statusBadge, {
backgroundColor: theme.primary + '10',
borderColor: theme.primary + '30'
}]}
onPress={handleJumpToLatest}
>
<View style={[styles.statusDot, { backgroundColor: theme.primary }]} />
<Caption style={[styles.statusText, { color: theme.primary }]}>Jump to Live</Caption>
</TouchableOpacity>
);
} else {
return (
<TouchableOpacity
style={[styles.statusBadge, {
backgroundColor: theme.muted + '20',
borderColor: theme.border
}]}
onPress={handleJumpToLatest}
>
<View style={[styles.statusDot, { backgroundColor: theme.mutedForeground }]} />
<Caption style={[styles.statusText, { color: theme.mutedForeground }]}>Jump to latest</Caption>
</TouchableOpacity>
);
}
2025-06-24 05:15:02 +08:00
}
};
2025-06-17 04:21:36 +08:00
if (!isVisible) return null;
2025-06-24 05:15:02 +08:00
// Empty state when no tools
if (totalSnapshots === 0) {
return (
<View style={styles.panel}>
<View style={styles.header}>
<View style={styles.headerLeft}>
<Computer size={16} color={styles.title.color} />
2025-07-11 06:27:59 +08:00
<H4 style={styles.title}>Suna&apos;s Computer</H4>
2025-06-24 05:15:02 +08:00
</View>
<TouchableOpacity style={styles.closeButton} onPress={handleClose}>
<X size={16} color={styles.title.color} />
</TouchableOpacity>
</View>
<View style={styles.emptyContainer}>
<View style={styles.emptyIcon}>
<Computer size={32} color={styles.emptySubtitle.color} />
</View>
<Body style={styles.emptyTitle}>No tool activity</Body>
<Body style={styles.emptySubtitle}>
2025-07-11 06:27:59 +08:00
Tool calls and computer interactions will appear here when they&apos;re being executed.
2025-06-24 05:15:02 +08:00
</Body>
</View>
</View>
);
}
2025-07-14 04:30:44 +08:00
2025-06-17 04:21:36 +08:00
return (
<View style={styles.panel}>
<View style={styles.header}>
2025-06-24 05:15:02 +08:00
<View style={styles.headerLeft}>
2025-07-11 06:27:59 +08:00
<H4 style={styles.title}>Suna&apos;s Computer</H4>
2025-06-24 05:15:02 +08:00
</View>
2025-06-17 04:21:36 +08:00
2025-06-24 05:15:02 +08:00
<View style={styles.headerActions}>
{isGenerating && (
<View style={styles.runningBadge}>
<CircleDashed size={12} color={theme.accent} />
<Caption style={styles.runningText}>Running</Caption>
</View>
)}
<TouchableOpacity style={styles.closeButton} onPress={handleClose}>
<X size={16} color={styles.title.color} />
</TouchableOpacity>
</View>
2025-06-17 04:21:36 +08:00
</View>
2025-06-24 05:15:02 +08:00
2025-06-25 04:43:44 +08:00
<ScrollView
style={styles.content}
contentContainerStyle={styles.toolViewContainer}
showsVerticalScrollIndicator={false}
scrollEventThrottle={16}
>
2025-07-14 04:30:44 +08:00
{(() => {
console.log('📤 PASSING TO TOOL VIEW:', currentSnapshot?.toolCall?.functionName, 'HAS RESULT:', !!currentSnapshot?.toolResult);
if (currentSnapshot?.toolResult) {
console.log('📦 RAW TOOL CONTENT:', currentSnapshot.toolResult.substring(0, 200) + '...');
}
return (
<ToolView
name={currentSnapshot?.toolCall?.functionName}
toolCall={currentSnapshot?.toolCall}
isStreaming={isGenerating}
isSuccess={true}
sandboxId={sandboxId}
messages={messages}
assistantContent={currentSnapshot?.toolResult}
toolContent={currentSnapshot?.toolResult}
browserState={currentSnapshot?.browserState}
toolTimestamp={currentSnapshot?.timestamp ? new Date(currentSnapshot.timestamp).toISOString() : undefined}
/>
);
})()}
2025-06-24 05:15:02 +08:00
</ScrollView>
{/* Timeline controls at bottom */}
{totalSnapshots > 1 && (
<View style={styles.timelineContainer}>
2025-06-25 04:43:44 +08:00
<View style={styles.timelineHeader}>
2025-06-24 05:15:02 +08:00
<Caption style={styles.counter}>
2025-06-25 04:43:44 +08:00
{currentSnapshotIndex + 1} / {totalSnapshots}
2025-06-24 05:15:02 +08:00
</Caption>
2025-06-25 04:43:44 +08:00
{renderStatusBadge()}
</View>
2025-06-24 05:15:02 +08:00
2025-06-25 04:43:44 +08:00
<View style={styles.sliderContainer}>
2025-06-24 05:15:02 +08:00
<TouchableOpacity
2025-06-25 04:43:44 +08:00
style={[styles.navButton, !canGoPrevious && styles.navButtonDisabled]}
onPress={handlePrevious}
disabled={!canGoPrevious}
2025-06-24 05:15:02 +08:00
>
2025-06-25 04:43:44 +08:00
<ChevronLeft size={16} color={canGoPrevious ? theme.foreground : theme.mutedForeground} />
2025-06-24 05:15:02 +08:00
</TouchableOpacity>
<Slider
style={styles.slider}
minimumValue={0}
maximumValue={totalSnapshots - 1}
step={1}
value={currentSnapshotIndex}
onValueChange={handleSliderChange}
minimumTrackTintColor={theme.primary}
2025-06-25 04:43:44 +08:00
maximumTrackTintColor={theme.muted + '40'}
2025-06-24 05:15:02 +08:00
thumbTintColor={theme.primary}
/>
2025-06-25 04:43:44 +08:00
<TouchableOpacity
style={[styles.navButton, !canGoNext && styles.navButtonDisabled]}
onPress={handleNext}
disabled={!canGoNext}
>
<ChevronRight size={16} color={canGoNext ? theme.foreground : theme.mutedForeground} />
</TouchableOpacity>
2025-06-24 05:15:02 +08:00
</View>
</View>
)}
2025-06-17 04:21:36 +08:00
</View>
);
};