A React Native app for the ultimate thinking partner.
at bf2a37bf805fa5ccc8b5badeeb098f842186e615 326 lines 9.0 kB view raw
1/** 2 * AppSidebar Component 3 * 4 * MIGRATION STATUS: ✅ EXTRACTED - Ready for use 5 * 6 * REPLACES: App.tsx.monolithic lines 1924-2079 7 * - Animated slide-in drawer menu 8 * - Navigation to Memory and Settings 9 * - Theme toggle (light/dark mode) 10 * - Open agent in browser 11 * - Refresh Co agent (developer mode only) 12 * - Logout button 13 * 14 * FEATURES: 15 * - Animated slide-in from left (0-280px width) 16 * - Menu items with icons 17 * - Conditional items (developer mode) 18 * - Safe area inset support 19 * - Theme-aware styling 20 * 21 * MENU ITEMS: 22 * 1. Memory - Navigate to knowledge view 23 * 2. Settings - Navigate to settings view 24 * 3. Light/Dark Mode Toggle 25 * 4. Open in Browser - Opens agent in Letta web app 26 * 5. Refresh Co (dev mode only) - Deletes and recreates agent 27 * 6. Logout - Signs out user 28 * 29 * DEPENDENCIES: 30 * - React Native Animated API 31 * - Ionicons 32 * - react-native-safe-area-context 33 * - Theme system 34 * - Linking (for browser navigation) 35 * 36 * USED BY: (not yet integrated) 37 * - [ ] App.new.tsx (planned) 38 * 39 * RELATED COMPONENTS: 40 * - AppHeader.tsx (menu button triggers this) 41 * - SettingsView.tsx (navigated to from Settings item) 42 * - KnowledgeView.tsx (navigated to from Memory item) 43 */ 44 45import React from 'react'; 46import { 47 View, 48 Text, 49 TouchableOpacity, 50 StyleSheet, 51 Platform, 52 Alert, 53 Linking, 54 FlatList, 55 Animated, 56} from 'react-native'; 57import { Ionicons } from '@expo/vector-icons'; 58import { useSafeAreaInsets } from 'react-native-safe-area-context'; 59import type { Theme } from '../theme'; 60 61interface AppSidebarProps { 62 theme: Theme; 63 colorScheme: 'light' | 'dark'; 64 visible: boolean; 65 animationValue: Animated.Value; // 0 = hidden, 1 = visible 66 developerMode: boolean; 67 agentId?: string; 68 onClose: () => void; 69 onMemoryPress: () => void; 70 onSettingsPress: () => void; 71 onThemeToggle: () => void; 72 onRefreshAgent: () => Promise<void>; 73 onLogout: () => void; 74} 75 76export function AppSidebar({ 77 theme, 78 colorScheme, 79 visible, 80 animationValue, 81 developerMode, 82 agentId, 83 onClose, 84 onMemoryPress, 85 onSettingsPress, 86 onThemeToggle, 87 onRefreshAgent, 88 onLogout, 89}: AppSidebarProps) { 90 const insets = useSafeAreaInsets(); 91 92 const handleRefreshAgent = async () => { 93 const confirmed = 94 Platform.OS === 'web' 95 ? window.confirm( 96 'This will delete the current co agent and create a new one. All conversation history will be lost. Are you sure?' 97 ) 98 : await new Promise<boolean>((resolve) => { 99 Alert.alert( 100 'Refresh Co Agent', 101 'This will delete the current co agent and create a new one. All conversation history will be lost. Are you sure?', 102 [ 103 { text: 'Cancel', style: 'cancel', onPress: () => resolve(false) }, 104 { text: 'Refresh', style: 'destructive', onPress: () => resolve(true) }, 105 ] 106 ); 107 }); 108 109 if (!confirmed) return; 110 111 onClose(); 112 await onRefreshAgent(); 113 }; 114 115 const handleOpenInBrowser = () => { 116 if (agentId) { 117 Linking.openURL(`https://app.letta.com/agents/${agentId}`); 118 } 119 }; 120 121 return ( 122 <Animated.View 123 style={[ 124 styles.sidebarContainer, 125 { 126 paddingTop: insets.top, 127 backgroundColor: theme.colors.background.secondary, 128 borderRightColor: theme.colors.border.primary, 129 width: animationValue.interpolate({ 130 inputRange: [0, 1], 131 outputRange: [0, 280], 132 }), 133 }, 134 ]} 135 > 136 <View 137 style={[ 138 styles.sidebarHeader, 139 { borderBottomColor: theme.colors.border.primary }, 140 ]} 141 > 142 <Text style={[styles.sidebarTitle, { color: theme.colors.text.primary }]}> 143 Menu 144 </Text> 145 <TouchableOpacity onPress={onClose} style={styles.closeSidebar}> 146 <Ionicons name="close" size={24} color={theme.colors.text.primary} /> 147 </TouchableOpacity> 148 </View> 149 150 <FlatList 151 style={{ flex: 1 }} 152 contentContainerStyle={{ flexGrow: 1 }} 153 ListHeaderComponent={ 154 <View style={styles.menuItems}> 155 {/* Memory */} 156 <TouchableOpacity 157 style={[ 158 styles.menuItem, 159 { borderBottomColor: theme.colors.border.primary }, 160 ]} 161 onPress={() => { 162 onClose(); 163 onMemoryPress(); 164 }} 165 > 166 <Ionicons 167 name="library-outline" 168 size={24} 169 color={theme.colors.text.primary} 170 /> 171 <Text style={[styles.menuItemText, { color: theme.colors.text.primary }]}> 172 Memory 173 </Text> 174 </TouchableOpacity> 175 176 {/* Settings */} 177 <TouchableOpacity 178 style={[ 179 styles.menuItem, 180 { borderBottomColor: theme.colors.border.primary }, 181 ]} 182 onPress={() => { 183 onClose(); 184 onSettingsPress(); 185 }} 186 > 187 <Ionicons 188 name="settings-outline" 189 size={24} 190 color={theme.colors.text.primary} 191 /> 192 <Text style={[styles.menuItemText, { color: theme.colors.text.primary }]}> 193 Settings 194 </Text> 195 </TouchableOpacity> 196 197 {/* Theme Toggle */} 198 <TouchableOpacity 199 style={[ 200 styles.menuItem, 201 { borderBottomColor: theme.colors.border.primary }, 202 ]} 203 onPress={onThemeToggle} 204 > 205 <Ionicons 206 name={colorScheme === 'dark' ? 'sunny-outline' : 'moon-outline'} 207 size={24} 208 color={theme.colors.text.primary} 209 /> 210 <Text style={[styles.menuItemText, { color: theme.colors.text.primary }]}> 211 {colorScheme === 'dark' ? 'Light Mode' : 'Dark Mode'} 212 </Text> 213 </TouchableOpacity> 214 215 {/* Open in Browser */} 216 <TouchableOpacity 217 style={[ 218 styles.menuItem, 219 { borderBottomColor: theme.colors.border.primary }, 220 ]} 221 onPress={handleOpenInBrowser} 222 disabled={!agentId} 223 > 224 <Ionicons 225 name="open-outline" 226 size={24} 227 color={theme.colors.text.primary} 228 /> 229 <Text style={[styles.menuItemText, { color: theme.colors.text.primary }]}> 230 Open in Browser 231 </Text> 232 </TouchableOpacity> 233 234 {/* Refresh Co (Developer Mode Only) */} 235 {developerMode && ( 236 <TouchableOpacity 237 style={[ 238 styles.menuItem, 239 { borderBottomColor: theme.colors.border.primary }, 240 ]} 241 onPress={handleRefreshAgent} 242 > 243 <Ionicons 244 name="refresh-outline" 245 size={24} 246 color={theme.colors.status.error} 247 /> 248 <Text 249 style={[ 250 styles.menuItemText, 251 { color: theme.colors.status.error }, 252 ]} 253 > 254 Refresh Co 255 </Text> 256 </TouchableOpacity> 257 )} 258 259 {/* Logout */} 260 <TouchableOpacity 261 style={[ 262 styles.menuItem, 263 { borderBottomColor: theme.colors.border.primary }, 264 ]} 265 onPress={() => { 266 onClose(); 267 onLogout(); 268 }} 269 > 270 <Ionicons 271 name="log-out-outline" 272 size={24} 273 color={theme.colors.text.primary} 274 /> 275 <Text style={[styles.menuItemText, { color: theme.colors.text.primary }]}> 276 Logout 277 </Text> 278 </TouchableOpacity> 279 </View> 280 } 281 data={[]} 282 renderItem={() => null} 283 /> 284 </Animated.View> 285 ); 286} 287 288const styles = StyleSheet.create({ 289 sidebarContainer: { 290 height: '100%', 291 borderRightWidth: 1, 292 overflow: 'hidden', 293 }, 294 sidebarHeader: { 295 flexDirection: 'row', 296 justifyContent: 'space-between', 297 alignItems: 'center', 298 paddingHorizontal: 16, 299 paddingVertical: 16, 300 borderBottomWidth: 1, 301 }, 302 closeSidebar: { 303 padding: 8, 304 }, 305 sidebarTitle: { 306 fontSize: 24, 307 fontFamily: 'Lexend_700Bold', 308 }, 309 menuItems: { 310 paddingTop: 8, 311 }, 312 menuItem: { 313 flexDirection: 'row', 314 alignItems: 'center', 315 paddingHorizontal: 20, 316 paddingVertical: 16, 317 borderBottomWidth: StyleSheet.hairlineWidth, 318 }, 319 menuItemText: { 320 fontSize: 16, 321 fontFamily: 'Lexend_400Regular', 322 marginLeft: 16, 323 }, 324}); 325 326export default AppSidebar;