A React Native app for the ultimate thinking partner.

chore: remove legacy screens and unused components

-3205
-40
AGENTS.md
··· 1 - # Repository Guidelines 2 - 3 - ## Project Structure & Module Organization 4 - - Root Expo React Native app: `App.tsx`, `src/` (feature folders: `api/`, `components/`, `navigation/`, `screens/`, `store/`, `theme/`, `types/`, `utils/`), assets in `assets/`. 5 - - Web demo (Next.js) lives in `letta-chatbot-example/` with its own tooling and config. 6 - - API client/service: `src/api/lettaApi.ts`. State: `src/store/appStore.ts` (Zustand). Theming tokens: `src/theme/`. 7 - 8 - ## Build, Test, and Development Commands 9 - - React Native (Expo) from repo root: 10 - - `npm start` – start Expo dev server. 11 - - `npm run ios` / `npm run android` / `npm run web` – launch platform targets. 12 - - Web demo: 13 - - `cd letta-chatbot-example && npm run dev` – start Next.js dev server. 14 - - `npm run build` / `npm start` – production build/serve. 15 - - `npm run lint` / `npm run format[:fix]` – linting/formatting (web demo only). 16 - - Testing: 17 - - Web demo includes Cypress; run via `cd letta-chatbot-example && npx cypress open` or `npx cypress run`. 18 - 19 - ## Coding Style & Naming Conventions 20 - - Language: TypeScript throughout. 21 - - Components/files: PascalCase for React components (e.g., `MessageBubble.tsx`), camelCase for variables/functions, UPPER_SNAKE_CASE for constants. 22 - - Keep modules focused; colocate UI in `src/components`, navigation in `src/navigation`, theme tokens in `src/theme`. 23 - - Formatting: follow existing style in the RN app; the web demo enforces Prettier/ESLint via project scripts. 24 - 25 - ## Testing Guidelines 26 - - Prefer integration/behavior tests in the web demo (Cypress). 27 - - Name tests to mirror source files and intent (e.g., `feature-name.cy.ts`). 28 - - For RN app, add test scaffolding only where it adds value; document how to run it in PRs if introduced. 29 - 30 - ## Commit & Pull Request Guidelines 31 - - Use clear, imperative commit messages. Conventional Commits are encouraged: `feat: …`, `fix: …`, `chore: …`. 32 - - PRs should include: 33 - - Summary, screenshots/screencasts for UI changes, and steps to verify. 34 - - Linked issues, scope of impact, and migration notes (if any). 35 - - Platform checked (iOS/Android/Web) when touching RN UI. 36 - 37 - ## Security & Configuration Tips 38 - - Don’t commit secrets. The web demo provides `.env.template`; copy to `.env.local` and fill values. 39 - - Expo app persists tokens via Secure Store/AsyncStorage; keep keys in `src/utils/storage.ts` consistent. 40 - - Network/API configuration should go through `src/api/lettaApi.ts`.
-370
AgentSelectorScreen.tsx
··· 1 - import React, { useState, useEffect } from 'react'; 2 - import { 3 - View, 4 - Text, 5 - StyleSheet, 6 - TouchableOpacity, 7 - ScrollView, 8 - SafeAreaView, 9 - RefreshControl, 10 - Alert, 11 - useColorScheme, 12 - } from 'react-native'; 13 - import { StatusBar } from 'expo-status-bar'; 14 - import lettaApi from './src/api/lettaApi'; 15 - import { darkTheme } from './src/theme'; 16 - import type { LettaAgent, Project } from './src/types/letta'; 17 - 18 - interface AgentSelectorScreenProps { 19 - currentProject: Project | null; 20 - onAgentSelect: (agent: LettaAgent) => void; 21 - onProjectPress: () => void; 22 - onCreateAgent: () => void; 23 - onLogout: () => void; 24 - } 25 - 26 - export default function AgentSelectorScreen({ 27 - currentProject, 28 - onAgentSelect, 29 - onProjectPress, 30 - onCreateAgent, 31 - onLogout, 32 - }: AgentSelectorScreenProps) { 33 - const colorScheme = useColorScheme(); 34 - const [agents, setAgents] = useState<LettaAgent[]>([]); 35 - const [isLoading, setIsLoading] = useState(true); 36 - const [isRefreshing, setIsRefreshing] = useState(false); 37 - const logoSource = colorScheme === 'dark' 38 - ? require('./assets/animations/Dark-sygnetrotate2.json') 39 - : require('./assets/animations/Light-sygnetrotate2.json'); 40 - const LogoLoader = require('./src/components/LogoLoader').default; 41 - 42 - const loadAgents = async (isRefresh = false) => { 43 - if (!currentProject) { 44 - setAgents([]); 45 - setIsLoading(false); 46 - return; 47 - } 48 - 49 - try { 50 - if (!isRefresh) setIsLoading(true); 51 - 52 - const agentList = await lettaApi.listAgentsForProject(currentProject.id, { 53 - sortBy: 'last_run_completion', 54 - limit: 50, 55 - }); 56 - 57 - setAgents(agentList); 58 - } catch (error: any) { 59 - console.error('Failed to load agents:', error); 60 - Alert.alert('Error', 'Failed to load agents: ' + error.message); 61 - } finally { 62 - setIsLoading(false); 63 - if (isRefresh) setIsRefreshing(false); 64 - } 65 - }; 66 - 67 - const onRefresh = async () => { 68 - setIsRefreshing(true); 69 - await loadAgents(true); 70 - }; 71 - 72 - useEffect(() => { 73 - loadAgents(); 74 - }, [currentProject]); 75 - 76 - const formatLastActive = (lastRunCompletion?: string): string => { 77 - if (!lastRunCompletion) return 'Never active'; 78 - 79 - const now = new Date(); 80 - const lastActive = new Date(lastRunCompletion); 81 - const diffMs = now.getTime() - lastActive.getTime(); 82 - const diffMinutes = Math.floor(diffMs / 60000); 83 - const diffHours = Math.floor(diffMinutes / 60); 84 - const diffDays = Math.floor(diffHours / 24); 85 - 86 - if (diffMinutes < 1) return 'Just now'; 87 - if (diffMinutes < 60) return `${diffMinutes}m ago`; 88 - if (diffHours < 24) return `${diffHours}h ago`; 89 - if (diffDays === 1) return 'Yesterday'; 90 - if (diffDays < 7) return `${diffDays}d ago`; 91 - 92 - return lastActive.toLocaleDateString(); 93 - }; 94 - 95 - const getAgentInitials = (name: string): string => { 96 - return name 97 - .split(' ') 98 - .map(word => word.charAt(0)) 99 - .join('') 100 - .toUpperCase() 101 - .slice(0, 2); 102 - }; 103 - 104 - if (isLoading && !isRefreshing) { 105 - return ( 106 - <SafeAreaView style={styles.container}> 107 - <View style={styles.header}> 108 - <TouchableOpacity style={styles.projectSelector} onPress={onProjectPress}> 109 - <Text style={styles.projectName}> 110 - {currentProject ? currentProject.name : 'Select Project'} 111 - </Text> 112 - <Text style={styles.projectInfo}>Tap to change project</Text> 113 - </TouchableOpacity> 114 - <TouchableOpacity onPress={onLogout}> 115 - <Text style={styles.logoutButton}>Logout</Text> 116 - </TouchableOpacity> 117 - </View> 118 - 119 - <View style={styles.loadingContainer}> 120 - <LogoLoader source={logoSource} size={120} /> 121 - <Text style={styles.loadingText}>Loading agents...</Text> 122 - </View> 123 - <StatusBar style="auto" /> 124 - </SafeAreaView> 125 - ); 126 - } 127 - 128 - return ( 129 - <SafeAreaView style={styles.container}> 130 - <View style={styles.header}> 131 - <TouchableOpacity style={styles.projectSelector} onPress={onProjectPress}> 132 - <Text style={styles.projectName}> 133 - {currentProject ? currentProject.name : 'Select Project'} 134 - </Text> 135 - <Text style={styles.projectInfo}> 136 - {agents.length} agent{agents.length !== 1 ? 's' : ''} 137 - </Text> 138 - </TouchableOpacity> 139 - 140 - <View style={styles.headerButtons}> 141 - <TouchableOpacity onPress={onCreateAgent} style={styles.createButton}> 142 - <Text style={styles.createButtonText}>+</Text> 143 - </TouchableOpacity> 144 - <TouchableOpacity onPress={onLogout}> 145 - <Text style={styles.logoutButton}>Logout</Text> 146 - </TouchableOpacity> 147 - </View> 148 - </View> 149 - 150 - <ScrollView 151 - style={styles.agentList} 152 - refreshControl={ 153 - <RefreshControl refreshing={isRefreshing} onRefresh={onRefresh} /> 154 - } 155 - > 156 - {!currentProject ? ( 157 - <View style={styles.emptyContainer}> 158 - <Text style={styles.emptyText}>Select a project to view agents</Text> 159 - </View> 160 - ) : agents.length === 0 ? ( 161 - <View style={styles.emptyContainer}> 162 - <Text style={styles.emptyText}>No agents found</Text> 163 - <TouchableOpacity onPress={onCreateAgent} style={styles.createFirstButton}> 164 - <Text style={styles.createFirstButtonText}>Create your first agent</Text> 165 - </TouchableOpacity> 166 - </View> 167 - ) : ( 168 - agents.map((agent) => ( 169 - <TouchableOpacity 170 - key={agent.id} 171 - style={styles.agentItem} 172 - onPress={() => onAgentSelect(agent)} 173 - > 174 - <View style={styles.agentAvatar}> 175 - <Text style={styles.agentAvatarText}> 176 - {getAgentInitials(agent.name)} 177 - </Text> 178 - </View> 179 - 180 - <View style={styles.agentInfo}> 181 - <View style={styles.agentHeader}> 182 - <Text style={styles.agentName}>{agent.name}</Text> 183 - <Text style={styles.lastActive}> 184 - {formatLastActive(agent.last_run_completion)} 185 - </Text> 186 - </View> 187 - 188 - <Text style={styles.agentDescription} numberOfLines={1}> 189 - {agent.description || agent.system || 'No description'} 190 - </Text> 191 - 192 - {agent.tags && agent.tags.length > 0 && ( 193 - <View style={styles.tagsContainer}> 194 - {agent.tags.slice(0, 3).map((tag, index) => ( 195 - <View key={index} style={styles.tag}> 196 - <Text style={styles.tagText}>{tag}</Text> 197 - </View> 198 - ))} 199 - {agent.tags.length > 3 && ( 200 - <Text style={styles.moreTagsText}>+{agent.tags.length - 3}</Text> 201 - )} 202 - </View> 203 - )} 204 - </View> 205 - </TouchableOpacity> 206 - )) 207 - )} 208 - </ScrollView> 209 - 210 - <StatusBar style="auto" /> 211 - </SafeAreaView> 212 - ); 213 - } 214 - 215 - const styles = StyleSheet.create({ 216 - container: { 217 - flex: 1, 218 - backgroundColor: darkTheme.colors.background.primary, 219 - }, 220 - header: { 221 - flexDirection: 'row', 222 - justifyContent: 'space-between', 223 - alignItems: 'center', 224 - paddingHorizontal: 16, 225 - paddingVertical: 12, 226 - backgroundColor: darkTheme.colors.background.tertiary, 227 - borderBottomWidth: 1, 228 - borderBottomColor: darkTheme.colors.border.primary, 229 - }, 230 - projectSelector: { 231 - flex: 1, 232 - }, 233 - projectName: { 234 - fontSize: 20, 235 - fontWeight: '600', 236 - color: darkTheme.colors.text.primary, 237 - }, 238 - projectInfo: { 239 - fontSize: 12, 240 - color: darkTheme.colors.text.secondary, 241 - marginTop: 2, 242 - }, 243 - headerButtons: { 244 - flexDirection: 'row', 245 - alignItems: 'center', 246 - }, 247 - createButton: { 248 - marginRight: 16, 249 - width: 32, 250 - height: 32, 251 - borderRadius: 16, 252 - backgroundColor: '#007AFF', 253 - justifyContent: 'center', 254 - alignItems: 'center', 255 - }, 256 - createButtonText: { 257 - fontSize: 20, 258 - color: '#fff', 259 - fontWeight: '300', 260 - }, 261 - logoutButton: { 262 - fontSize: 16, 263 - color: '#007AFF', 264 - }, 265 - loadingContainer: { 266 - flex: 1, 267 - justifyContent: 'center', 268 - alignItems: 'center', 269 - padding: 20, 270 - }, 271 - loadingText: { 272 - marginTop: 12, 273 - fontSize: 16, 274 - color: '#666', 275 - }, 276 - agentList: { 277 - flex: 1, 278 - }, 279 - agentItem: { 280 - flexDirection: 'row', 281 - padding: 16, 282 - backgroundColor: '#fff', 283 - borderBottomWidth: 1, 284 - borderBottomColor: '#e5e5ea', 285 - alignItems: 'center', 286 - }, 287 - agentAvatar: { 288 - width: 50, 289 - height: 50, 290 - borderRadius: 25, 291 - backgroundColor: '#007AFF', 292 - justifyContent: 'center', 293 - alignItems: 'center', 294 - marginRight: 12, 295 - }, 296 - agentAvatarText: { 297 - color: '#fff', 298 - fontSize: 16, 299 - fontWeight: '600', 300 - }, 301 - agentInfo: { 302 - flex: 1, 303 - }, 304 - agentHeader: { 305 - flexDirection: 'row', 306 - justifyContent: 'space-between', 307 - alignItems: 'center', 308 - marginBottom: 4, 309 - }, 310 - agentName: { 311 - fontSize: 16, 312 - fontWeight: '600', 313 - color: '#000', 314 - flex: 1, 315 - }, 316 - lastActive: { 317 - fontSize: 12, 318 - color: '#666', 319 - }, 320 - agentDescription: { 321 - fontSize: 14, 322 - color: '#666', 323 - marginBottom: 4, 324 - }, 325 - tagsContainer: { 326 - flexDirection: 'row', 327 - alignItems: 'center', 328 - flexWrap: 'wrap', 329 - }, 330 - tag: { 331 - backgroundColor: '#f0f0f0', 332 - borderRadius: 12, 333 - paddingHorizontal: 8, 334 - paddingVertical: 2, 335 - marginRight: 4, 336 - marginTop: 2, 337 - }, 338 - tagText: { 339 - fontSize: 11, 340 - color: '#666', 341 - }, 342 - moreTagsText: { 343 - fontSize: 11, 344 - color: '#666', 345 - marginTop: 2, 346 - }, 347 - emptyContainer: { 348 - flex: 1, 349 - justifyContent: 'center', 350 - alignItems: 'center', 351 - padding: 40, 352 - }, 353 - emptyText: { 354 - fontSize: 16, 355 - color: '#666', 356 - textAlign: 'center', 357 - marginBottom: 20, 358 - }, 359 - createFirstButton: { 360 - backgroundColor: '#007AFF', 361 - borderRadius: 8, 362 - paddingHorizontal: 20, 363 - paddingVertical: 12, 364 - }, 365 - createFirstButtonText: { 366 - color: '#fff', 367 - fontSize: 16, 368 - fontWeight: '600', 369 - }, 370 - });
-600
CreateAgentScreen.tsx
··· 1 - import React, { useState, useEffect } from 'react'; 2 - import { 3 - View, 4 - Text, 5 - StyleSheet, 6 - TouchableOpacity, 7 - Alert, 8 - TextInput, 9 - ScrollView, 10 - SafeAreaView, 11 - ActivityIndicator, 12 - Switch, 13 - useColorScheme, 14 - } from 'react-native'; 15 - import { StatusBar } from 'expo-status-bar'; 16 - import lettaApi from './src/api/lettaApi'; 17 - import type { LettaAgent, LettaTool, LettaModel, MemoryBlock, CreateAgentRequest } from './src/types/letta'; 18 - 19 - interface CreateAgentScreenProps { 20 - onAgentCreated: (agent: LettaAgent) => void; 21 - onCancel: () => void; 22 - } 23 - 24 - export default function CreateAgentScreen({ onAgentCreated, onCancel }: CreateAgentScreenProps) { 25 - const colorScheme = useColorScheme(); 26 - // Form state 27 - const [name, setName] = useState(''); 28 - const [description, setDescription] = useState(''); 29 - const [memoryBlocks, setMemoryBlocks] = useState<MemoryBlock[]>([ 30 - { label: 'human', value: 'The user is chatting via a mobile app.' }, 31 - { label: 'persona', value: 'I am a helpful AI assistant.' } 32 - ]); 33 - const [selectedTools, setSelectedTools] = useState<string[]>([]); 34 - const [selectedModel, setSelectedModel] = useState(''); 35 - const [sleepTimeEnabled, setSleepTimeEnabled] = useState(true); 36 - 37 - // Data state 38 - const [tools, setTools] = useState<LettaTool[]>([]); 39 - const [models, setModels] = useState<LettaModel[]>([]); 40 - 41 - // UI state 42 - const [isLoading, setIsLoading] = useState(true); 43 - const [isCreating, setIsCreating] = useState(false); 44 - const [showToolPicker, setShowToolPicker] = useState(false); 45 - const [showModelPicker, setShowModelPicker] = useState(false); 46 - 47 - useEffect(() => { 48 - loadData(); 49 - }, []); 50 - 51 - const loadData = async () => { 52 - setIsLoading(true); 53 - try { 54 - // Load tools and models with individual error handling 55 - const results = await Promise.allSettled([ 56 - lettaApi.listTools(), 57 - lettaApi.listModels() 58 - ]); 59 - 60 - // Handle tools 61 - if (results[0].status === 'fulfilled') { 62 - setTools(results[0].value); 63 - } else { 64 - console.warn('Failed to load tools:', results[0].reason); 65 - } 66 - 67 - // Handle models 68 - if (results[1].status === 'fulfilled') { 69 - setModels(results[1].value); 70 - if (results[1].value.length > 0) { 71 - const firstModel = results[1].value[0]; 72 - setSelectedModel(`${firstModel.provider_name}/${firstModel.model}`); 73 - } 74 - } else { 75 - console.warn('Failed to load models:', results[1].reason); 76 - Alert.alert('Warning', 'Could not load available models. You can still create an agent with default settings.'); 77 - } 78 - } catch (error: any) { 79 - console.error('Failed to load data:', error); 80 - Alert.alert('Error', 'Failed to load configuration data. You can still create an agent with default settings.'); 81 - } finally { 82 - setIsLoading(false); 83 - } 84 - }; 85 - 86 - const addMemoryBlock = () => { 87 - setMemoryBlocks([...memoryBlocks, { label: '', value: '' }]); 88 - }; 89 - 90 - const removeMemoryBlock = (index: number) => { 91 - setMemoryBlocks(memoryBlocks.filter((_, i) => i !== index)); 92 - }; 93 - 94 - const updateMemoryBlock = (index: number, field: 'label' | 'value', text: string) => { 95 - const updated = [...memoryBlocks]; 96 - updated[index][field] = text; 97 - setMemoryBlocks(updated); 98 - }; 99 - 100 - const toggleTool = (toolName: string) => { 101 - setSelectedTools(prev => 102 - prev.includes(toolName) 103 - ? prev.filter(name => name !== toolName) 104 - : [...prev, toolName] 105 - ); 106 - }; 107 - 108 - const createAgent = async () => { 109 - if (!name.trim()) { 110 - Alert.alert('Error', 'Please enter a name for your agent'); 111 - return; 112 - } 113 - 114 - setIsCreating(true); 115 - try { 116 - const agentData: CreateAgentRequest = { 117 - name: name.trim(), 118 - }; 119 - 120 - // Add optional fields only if they have values 121 - if (description.trim()) { 122 - agentData.description = description.trim(); 123 - } 124 - 125 - if (memoryBlocks.some(block => block.label && block.value)) { 126 - agentData.memoryBlocks = memoryBlocks.filter(block => block.label && block.value); 127 - } 128 - 129 - if (selectedTools.length > 0) { 130 - agentData.tools = selectedTools; 131 - } 132 - 133 - if (selectedModel) { 134 - agentData.model = selectedModel; 135 - } 136 - 137 - agentData.sleeptimeEnable = sleepTimeEnabled; 138 - 139 - console.log('Creating agent with data:', agentData); 140 - console.log('Available models:', models.map(m => ({name: m.model, provider: m.provider_name}))); 141 - const agent = await lettaApi.createAgent(agentData); 142 - onAgentCreated(agent); 143 - } catch (error: any) { 144 - console.error('Failed to create agent:', error); 145 - console.error('Full error object:', JSON.stringify(error, null, 2)); 146 - console.error('Error details:', { 147 - message: error.message, 148 - status: error.status, 149 - code: error.code, 150 - response: error.response, 151 - responseData: error.response?.data, 152 - responseStatus: error.response?.status, 153 - responseHeaders: error.response?.headers 154 - }); 155 - 156 - let errorMessage = 'Failed to create agent'; 157 - if (error.response?.data?.message) { 158 - errorMessage += ': ' + error.response.data.message; 159 - } else if (error.response?.data?.error) { 160 - errorMessage += ': ' + error.response.data.error; 161 - } else if (error.response?.data) { 162 - errorMessage += ': ' + JSON.stringify(error.response.data); 163 - } else if (error.message) { 164 - errorMessage += ': ' + error.message; 165 - } 166 - 167 - Alert.alert('Error', errorMessage); 168 - } finally { 169 - setIsCreating(false); 170 - } 171 - }; 172 - 173 - if (isLoading) { 174 - return ( 175 - <SafeAreaView style={styles.container}> 176 - <View style={styles.loadingContainer}> 177 - {(() => { 178 - const LogoLoader = require('./src/components/LogoLoader').default; 179 - const logoSource = colorScheme === 'dark' 180 - ? require('./assets/animations/Dark-sygnetrotate2.json') 181 - : require('./assets/animations/Light-sygnetrotate2.json'); 182 - return <LogoLoader source={logoSource} size={120} />; 183 - })()} 184 - <Text style={styles.loadingText}>Loading configuration...</Text> 185 - </View> 186 - <StatusBar style="auto" /> 187 - </SafeAreaView> 188 - ); 189 - } 190 - 191 - return ( 192 - <SafeAreaView style={styles.container}> 193 - <View style={styles.header}> 194 - <TouchableOpacity onPress={onCancel}> 195 - <Text style={styles.cancelButton}>Cancel</Text> 196 - </TouchableOpacity> 197 - <Text style={styles.headerTitle}>Create Agent</Text> 198 - <TouchableOpacity 199 - onPress={createAgent} 200 - disabled={isCreating || !name.trim()} 201 - style={[styles.createButton, (!name.trim() || isCreating) && styles.createButtonDisabled]} 202 - > 203 - {isCreating ? ( 204 - <ActivityIndicator size="small" color="#fff" /> 205 - ) : ( 206 - <Text style={styles.createButtonText}>Create</Text> 207 - )} 208 - </TouchableOpacity> 209 - </View> 210 - 211 - <ScrollView style={styles.content}> 212 - {/* Name */} 213 - <View style={styles.section}> 214 - <Text style={styles.sectionTitle}>Name *</Text> 215 - <TextInput 216 - style={styles.input} 217 - placeholder="Enter agent name" 218 - value={name} 219 - onChangeText={setName} 220 - autoFocus 221 - /> 222 - </View> 223 - 224 - {/* Description */} 225 - <View style={styles.section}> 226 - <Text style={styles.sectionTitle}>Description</Text> 227 - <TextInput 228 - style={[styles.input, styles.textArea]} 229 - placeholder="Enter agent description (optional)" 230 - value={description} 231 - onChangeText={setDescription} 232 - multiline 233 - numberOfLines={3} 234 - /> 235 - </View> 236 - 237 - {/* Model */} 238 - <View style={styles.section}> 239 - <Text style={styles.sectionTitle}>Language Model</Text> 240 - <TouchableOpacity 241 - style={styles.picker} 242 - onPress={() => setShowModelPicker(!showModelPicker)} 243 - > 244 - <Text style={styles.pickerText}> 245 - {selectedModel || 'Select model'} 246 - </Text> 247 - <Text style={styles.pickerArrow}>{showModelPicker ? '▲' : '▼'}</Text> 248 - </TouchableOpacity> 249 - 250 - {showModelPicker && ( 251 - <ScrollView style={styles.pickerOptions} nestedScrollEnabled={true}> 252 - {models.map((model, index) => { 253 - const modelId = `${model.provider_name}/${model.model}`; 254 - return ( 255 - <TouchableOpacity 256 - key={`${model.model}-${index}`} 257 - style={[styles.option, selectedModel === modelId && styles.selectedOption]} 258 - onPress={() => { 259 - setSelectedModel(modelId); 260 - setShowModelPicker(false); 261 - }} 262 - > 263 - <Text style={styles.optionText}>{modelId}</Text> 264 - <Text style={styles.optionSubtext}> 265 - {model.provider_name} • {model.context_window} tokens 266 - </Text> 267 - </TouchableOpacity> 268 - ); 269 - })} 270 - </ScrollView> 271 - )} 272 - </View> 273 - 274 - 275 - {/* Memory Blocks */} 276 - <View style={styles.section}> 277 - <View style={styles.sectionHeader}> 278 - <Text style={styles.sectionTitle}>Memory Blocks</Text> 279 - <TouchableOpacity onPress={addMemoryBlock}> 280 - <Text style={styles.addButton}>+ Add</Text> 281 - </TouchableOpacity> 282 - </View> 283 - 284 - {memoryBlocks.map((block, index) => ( 285 - <View key={index} style={styles.memoryBlock}> 286 - <View style={styles.memoryBlockHeader}> 287 - <TextInput 288 - style={styles.memoryBlockLabel} 289 - placeholder="Label" 290 - value={block.label} 291 - onChangeText={(text) => updateMemoryBlock(index, 'label', text)} 292 - /> 293 - {memoryBlocks.length > 1 && ( 294 - <TouchableOpacity onPress={() => removeMemoryBlock(index)}> 295 - <Text style={styles.removeButton}>✕</Text> 296 - </TouchableOpacity> 297 - )} 298 - </View> 299 - <TextInput 300 - style={[styles.input, styles.textArea]} 301 - placeholder="Memory block content" 302 - value={block.value} 303 - onChangeText={(text) => updateMemoryBlock(index, 'value', text)} 304 - multiline 305 - numberOfLines={2} 306 - /> 307 - </View> 308 - ))} 309 - </View> 310 - 311 - {/* Tools */} 312 - <View style={styles.section}> 313 - <View style={styles.sectionHeader}> 314 - <Text style={styles.sectionTitle}>Tools</Text> 315 - <TouchableOpacity onPress={() => setShowToolPicker(!showToolPicker)}> 316 - <Text style={styles.addButton}> 317 - {showToolPicker ? 'Hide' : 'Select'} ({selectedTools.length}) 318 - </Text> 319 - </TouchableOpacity> 320 - </View> 321 - 322 - {showToolPicker && ( 323 - <ScrollView style={styles.toolList} nestedScrollEnabled={true}> 324 - {tools.map((tool) => ( 325 - <TouchableOpacity 326 - key={tool.id} 327 - style={styles.toolItem} 328 - onPress={() => toggleTool(tool.name)} 329 - > 330 - <View style={styles.toolInfo}> 331 - <Text style={styles.toolName}>{tool.name}</Text> 332 - {tool.description && ( 333 - <Text style={styles.toolDescription}>{tool.description}</Text> 334 - )} 335 - </View> 336 - <View style={[styles.checkbox, selectedTools.includes(tool.name) && styles.checkboxSelected]}> 337 - {selectedTools.includes(tool.name) && <Text style={styles.checkmark}>✓</Text>} 338 - </View> 339 - </TouchableOpacity> 340 - ))} 341 - </ScrollView> 342 - )} 343 - </View> 344 - 345 - {/* Sleep-time Compute */} 346 - <View style={styles.section}> 347 - <View style={styles.settingRow}> 348 - <View style={styles.settingInfo}> 349 - <Text style={styles.sectionTitle}>Sleep-time Compute</Text> 350 - <Text style={styles.settingDescription}> 351 - Enable background learning during idle periods 352 - </Text> 353 - </View> 354 - <Switch 355 - value={sleepTimeEnabled} 356 - onValueChange={setSleepTimeEnabled} 357 - trackColor={{ false: '#ddd', true: '#007AFF' }} 358 - thumbColor="#fff" 359 - /> 360 - </View> 361 - </View> 362 - 363 - </ScrollView> 364 - 365 - <StatusBar style="auto" /> 366 - </SafeAreaView> 367 - ); 368 - } 369 - 370 - const styles = StyleSheet.create({ 371 - container: { 372 - flex: 1, 373 - backgroundColor: '#f8f8f8', 374 - }, 375 - loadingContainer: { 376 - flex: 1, 377 - justifyContent: 'center', 378 - alignItems: 'center', 379 - padding: 20, 380 - }, 381 - loadingText: { 382 - marginTop: 12, 383 - fontSize: 16, 384 - color: '#666', 385 - }, 386 - header: { 387 - flexDirection: 'row', 388 - justifyContent: 'space-between', 389 - alignItems: 'center', 390 - paddingHorizontal: 16, 391 - paddingVertical: 12, 392 - backgroundColor: '#fff', 393 - borderBottomWidth: 1, 394 - borderBottomColor: '#e5e5ea', 395 - }, 396 - cancelButton: { 397 - fontSize: 16, 398 - color: '#007AFF', 399 - }, 400 - headerTitle: { 401 - fontSize: 18, 402 - fontWeight: '600', 403 - color: '#000', 404 - }, 405 - createButton: { 406 - backgroundColor: '#007AFF', 407 - paddingHorizontal: 16, 408 - paddingVertical: 8, 409 - borderRadius: 8, 410 - minWidth: 60, 411 - alignItems: 'center', 412 - }, 413 - createButtonDisabled: { 414 - backgroundColor: '#cccccc', 415 - }, 416 - createButtonText: { 417 - color: '#fff', 418 - fontSize: 16, 419 - fontWeight: '600', 420 - }, 421 - content: { 422 - flex: 1, 423 - padding: 16, 424 - }, 425 - section: { 426 - marginBottom: 24, 427 - }, 428 - sectionHeader: { 429 - flexDirection: 'row', 430 - justifyContent: 'space-between', 431 - alignItems: 'center', 432 - marginBottom: 8, 433 - }, 434 - sectionTitle: { 435 - fontSize: 16, 436 - fontWeight: '600', 437 - color: '#000', 438 - marginBottom: 8, 439 - }, 440 - input: { 441 - borderWidth: 1, 442 - borderColor: '#ddd', 443 - borderRadius: 8, 444 - paddingHorizontal: 12, 445 - paddingVertical: 10, 446 - backgroundColor: '#fff', 447 - fontSize: 16, 448 - }, 449 - textArea: { 450 - height: 80, 451 - textAlignVertical: 'top', 452 - }, 453 - picker: { 454 - flexDirection: 'row', 455 - justifyContent: 'space-between', 456 - alignItems: 'center', 457 - borderWidth: 1, 458 - borderColor: '#ddd', 459 - borderRadius: 8, 460 - paddingHorizontal: 12, 461 - paddingVertical: 12, 462 - backgroundColor: '#fff', 463 - }, 464 - pickerText: { 465 - fontSize: 16, 466 - color: '#000', 467 - flex: 1, 468 - }, 469 - pickerArrow: { 470 - fontSize: 12, 471 - color: '#666', 472 - }, 473 - pickerOptions: { 474 - marginTop: 8, 475 - backgroundColor: '#fff', 476 - borderRadius: 8, 477 - borderWidth: 1, 478 - borderColor: '#ddd', 479 - maxHeight: 200, 480 - overflow: 'scroll', 481 - }, 482 - option: { 483 - paddingHorizontal: 12, 484 - paddingVertical: 12, 485 - borderBottomWidth: 1, 486 - borderBottomColor: '#f0f0f0', 487 - }, 488 - selectedOption: { 489 - backgroundColor: '#f0f8ff', 490 - }, 491 - optionText: { 492 - fontSize: 16, 493 - color: '#000', 494 - fontWeight: '500', 495 - }, 496 - optionSubtext: { 497 - fontSize: 12, 498 - color: '#666', 499 - marginTop: 2, 500 - }, 501 - addButton: { 502 - fontSize: 16, 503 - color: '#007AFF', 504 - fontWeight: '500', 505 - }, 506 - memoryBlock: { 507 - backgroundColor: '#fff', 508 - borderRadius: 8, 509 - borderWidth: 1, 510 - borderColor: '#ddd', 511 - padding: 12, 512 - marginBottom: 8, 513 - }, 514 - memoryBlockHeader: { 515 - flexDirection: 'row', 516 - alignItems: 'center', 517 - marginBottom: 8, 518 - }, 519 - memoryBlockLabel: { 520 - flex: 1, 521 - fontSize: 14, 522 - fontWeight: '600', 523 - color: '#000', 524 - borderBottomWidth: 1, 525 - borderBottomColor: '#e0e0e0', 526 - paddingBottom: 4, 527 - }, 528 - removeButton: { 529 - fontSize: 18, 530 - color: '#ff3b30', 531 - marginLeft: 12, 532 - }, 533 - toolList: { 534 - backgroundColor: '#fff', 535 - borderRadius: 8, 536 - borderWidth: 1, 537 - borderColor: '#ddd', 538 - maxHeight: 200, 539 - }, 540 - toolItem: { 541 - flexDirection: 'row', 542 - alignItems: 'center', 543 - paddingHorizontal: 12, 544 - paddingVertical: 12, 545 - borderBottomWidth: 1, 546 - borderBottomColor: '#f0f0f0', 547 - }, 548 - toolInfo: { 549 - flex: 1, 550 - }, 551 - toolName: { 552 - fontSize: 16, 553 - fontWeight: '500', 554 - color: '#000', 555 - }, 556 - toolDescription: { 557 - fontSize: 12, 558 - color: '#666', 559 - marginTop: 2, 560 - }, 561 - checkbox: { 562 - width: 24, 563 - height: 24, 564 - borderRadius: 4, 565 - borderWidth: 2, 566 - borderColor: '#ddd', 567 - alignItems: 'center', 568 - justifyContent: 'center', 569 - marginLeft: 12, 570 - }, 571 - checkboxSelected: { 572 - backgroundColor: '#007AFF', 573 - borderColor: '#007AFF', 574 - }, 575 - checkmark: { 576 - color: '#fff', 577 - fontSize: 16, 578 - fontWeight: 'bold', 579 - }, 580 - settingRow: { 581 - flexDirection: 'row', 582 - justifyContent: 'space-between', 583 - alignItems: 'center', 584 - backgroundColor: '#fff', 585 - borderRadius: 8, 586 - borderWidth: 1, 587 - borderColor: '#ddd', 588 - paddingHorizontal: 12, 589 - paddingVertical: 12, 590 - }, 591 - settingInfo: { 592 - flex: 1, 593 - marginRight: 12, 594 - }, 595 - settingDescription: { 596 - fontSize: 12, 597 - color: '#666', 598 - marginTop: 2, 599 - }, 600 - });
-337
ProjectSelectorModal.tsx
··· 1 - import React, { useState, useEffect } from 'react'; 2 - import { 3 - View, 4 - Text, 5 - StyleSheet, 6 - TouchableOpacity, 7 - Modal, 8 - TextInput, 9 - ScrollView, 10 - ActivityIndicator, 11 - Alert, 12 - useColorScheme, 13 - Platform, 14 - } from 'react-native'; 15 - import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; 16 - import { StatusBar } from 'expo-status-bar'; 17 - import lettaApi from './src/api/lettaApi'; 18 - import { darkTheme } from './src/theme'; 19 - import type { Project, ListProjectsParams } from './src/types/letta'; 20 - 21 - interface ProjectSelectorModalProps { 22 - visible: boolean; 23 - currentProject: Project | null; 24 - onProjectSelect: (project: Project) => void; 25 - onClose: () => void; 26 - } 27 - 28 - export default function ProjectSelectorModal({ 29 - visible, 30 - currentProject, 31 - onProjectSelect, 32 - onClose, 33 - }: ProjectSelectorModalProps) { 34 - const colorScheme = useColorScheme(); 35 - const insets = useSafeAreaInsets(); 36 - const LogoLoader = require('./src/components/LogoLoader').default; 37 - const logoSource = colorScheme === 'dark' 38 - ? require('./assets/animations/Dark-sygnetrotate2.json') 39 - : require('./assets/animations/Light-sygnetrotate2.json'); 40 - const [projects, setProjects] = useState<Project[]>([]); 41 - const [isLoading, setIsLoading] = useState(false); 42 - const [searchQuery, setSearchQuery] = useState(''); 43 - const [hasNextPage, setHasNextPage] = useState(false); 44 - const [isLoadingMore, setIsLoadingMore] = useState(false); 45 - const [offset, setOffset] = useState(0); 46 - 47 - const LIMIT = 19; 48 - 49 - const loadProjects = async (isLoadMore = false, query = '') => { 50 - try { 51 - if (!isLoadMore) { 52 - setIsLoading(true); 53 - setOffset(0); 54 - } else { 55 - setIsLoadingMore(true); 56 - } 57 - 58 - const params: ListProjectsParams = { 59 - limit: LIMIT, 60 - offset: isLoadMore ? offset : 0, 61 - }; 62 - 63 - if (query.trim()) { 64 - params.name = query.trim(); 65 - } 66 - 67 - const response = await lettaApi.listProjects(params); 68 - 69 - if (isLoadMore) { 70 - setProjects(prev => [...prev, ...response.projects]); 71 - setOffset(prev => prev + LIMIT); 72 - } else { 73 - setProjects(response.projects); 74 - setOffset(LIMIT); 75 - } 76 - 77 - setHasNextPage(response.hasNextPage); 78 - } catch (error: any) { 79 - console.error('Failed to load projects:', error); 80 - Alert.alert('Error', 'Failed to load projects: ' + error.message); 81 - } finally { 82 - setIsLoading(false); 83 - setIsLoadingMore(false); 84 - } 85 - }; 86 - 87 - const handleSearch = (query: string) => { 88 - setSearchQuery(query); 89 - // Debounce search 90 - const timeoutId = setTimeout(() => { 91 - loadProjects(false, query); 92 - }, 300); 93 - 94 - return () => clearTimeout(timeoutId); 95 - }; 96 - 97 - const loadMoreProjects = () => { 98 - if (hasNextPage && !isLoadingMore) { 99 - loadProjects(true, searchQuery); 100 - } 101 - }; 102 - 103 - const handleProjectSelect = (project: Project) => { 104 - onProjectSelect(project); 105 - onClose(); 106 - }; 107 - 108 - const resetAndLoad = () => { 109 - setSearchQuery(''); 110 - setProjects([]); 111 - setOffset(0); 112 - loadProjects(false, ''); 113 - }; 114 - 115 - useEffect(() => { 116 - if (visible) { 117 - resetAndLoad(); 118 - } 119 - }, [visible]); 120 - 121 - useEffect(() => { 122 - const cleanup = handleSearch(searchQuery); 123 - return cleanup; 124 - }, [searchQuery]); 125 - 126 - return ( 127 - <Modal 128 - visible={visible} 129 - animationType="slide" 130 - transparent={false} 131 - presentationStyle={Platform.OS === 'ios' ? 'fullScreen' : 'overFullScreen'} 132 - onRequestClose={onClose} 133 - > 134 - <SafeAreaView style={styles.container}> 135 - <View style={[styles.header, { paddingTop: insets.top + 12 }]}> 136 - <TouchableOpacity onPress={onClose}> 137 - <Text style={styles.cancelButton}>Cancel</Text> 138 - </TouchableOpacity> 139 - <Text style={styles.title}>Select Project</Text> 140 - <View style={styles.placeholder} /> 141 - </View> 142 - 143 - <View style={styles.searchContainer}> 144 - <TextInput 145 - style={styles.searchInput} 146 - placeholder="Search projects..." 147 - value={searchQuery} 148 - onChangeText={setSearchQuery} 149 - autoCapitalize="none" 150 - autoCorrect={false} 151 - /> 152 - </View> 153 - 154 - {isLoading ? ( 155 - <View style={styles.loadingContainer}> 156 - <LogoLoader source={logoSource} size={120} /> 157 - <Text style={styles.loadingText}>Loading projects...</Text> 158 - </View> 159 - ) : ( 160 - <ScrollView style={styles.projectList}> 161 - {projects.length === 0 ? ( 162 - <View style={styles.emptyContainer}> 163 - <Text style={styles.emptyText}> 164 - {searchQuery ? 'No projects found matching your search' : 'No projects available'} 165 - </Text> 166 - </View> 167 - ) : ( 168 - <> 169 - {projects.map((project) => ( 170 - <TouchableOpacity 171 - key={project.id} 172 - style={[ 173 - styles.projectItem, 174 - currentProject?.id === project.id && styles.selectedProject, 175 - ]} 176 - onPress={() => handleProjectSelect(project)} 177 - > 178 - <View style={styles.projectInfo}> 179 - <Text style={styles.projectName}>{project.name}</Text> 180 - <Text style={styles.projectSlug}>#{project.slug}</Text> 181 - </View> 182 - 183 - {currentProject?.id === project.id && ( 184 - <View style={styles.checkmark}> 185 - <Text style={styles.checkmarkText}>✓</Text> 186 - </View> 187 - )} 188 - </TouchableOpacity> 189 - ))} 190 - 191 - {hasNextPage && ( 192 - <TouchableOpacity 193 - style={styles.loadMoreButton} 194 - onPress={loadMoreProjects} 195 - disabled={isLoadingMore} 196 - > 197 - {isLoadingMore ? ( 198 - <ActivityIndicator size="small" color="#007AFF" /> 199 - ) : ( 200 - <Text style={styles.loadMoreText}>Load More</Text> 201 - )} 202 - </TouchableOpacity> 203 - )} 204 - </> 205 - )} 206 - </ScrollView> 207 - )} 208 - <StatusBar style="light" /> 209 - </SafeAreaView> 210 - </Modal> 211 - ); 212 - } 213 - 214 - const styles = StyleSheet.create({ 215 - container: { 216 - flex: 1, 217 - backgroundColor: darkTheme.colors.background.primary, 218 - }, 219 - header: { 220 - flexDirection: 'row', 221 - justifyContent: 'space-between', 222 - alignItems: 'center', 223 - paddingHorizontal: 16, 224 - paddingVertical: 12, 225 - backgroundColor: darkTheme.colors.background.secondary, 226 - borderBottomWidth: 1, 227 - borderBottomColor: darkTheme.colors.border.primary, 228 - }, 229 - cancelButton: { 230 - fontSize: 16, 231 - color: '#007AFF', 232 - width: 60, 233 - }, 234 - title: { 235 - fontSize: 18, 236 - fontWeight: '600', 237 - color: darkTheme.colors.text.primary, 238 - }, 239 - placeholder: { 240 - width: 60, 241 - }, 242 - searchContainer: { 243 - padding: 16, 244 - backgroundColor: darkTheme.colors.background.secondary, 245 - borderBottomWidth: 1, 246 - borderBottomColor: darkTheme.colors.border.primary, 247 - }, 248 - searchInput: { 249 - height: 36, 250 - borderWidth: 1, 251 - borderColor: darkTheme.colors.border.primary, 252 - borderRadius: 8, 253 - paddingHorizontal: 12, 254 - fontSize: 16, 255 - backgroundColor: darkTheme.colors.background.surface, 256 - color: darkTheme.colors.text.primary, 257 - }, 258 - loadingContainer: { 259 - flex: 1, 260 - justifyContent: 'center', 261 - alignItems: 'center', 262 - padding: 20, 263 - }, 264 - loadingText: { 265 - marginTop: 12, 266 - fontSize: 16, 267 - color: '#666', 268 - }, 269 - projectList: { 270 - flex: 1, 271 - }, 272 - projectItem: { 273 - flexDirection: 'row', 274 - alignItems: 'center', 275 - justifyContent: 'space-between', 276 - padding: 16, 277 - backgroundColor: darkTheme.colors.background.secondary, 278 - borderBottomWidth: 1, 279 - borderBottomColor: darkTheme.colors.border.primary, 280 - }, 281 - selectedProject: { 282 - backgroundColor: darkTheme.colors.background.selected, 283 - }, 284 - projectInfo: { 285 - flex: 1, 286 - }, 287 - projectName: { 288 - fontSize: 16, 289 - fontWeight: '600', 290 - color: darkTheme.colors.text.primary, 291 - marginBottom: 2, 292 - }, 293 - projectSlug: { 294 - fontSize: 14, 295 - color: darkTheme.colors.text.secondary, 296 - }, 297 - checkmark: { 298 - width: 24, 299 - height: 24, 300 - borderRadius: 12, 301 - backgroundColor: '#007AFF', 302 - justifyContent: 'center', 303 - alignItems: 'center', 304 - }, 305 - checkmarkText: { 306 - color: '#fff', 307 - fontSize: 14, 308 - fontWeight: 'bold', 309 - }, 310 - loadMoreButton: { 311 - padding: 16, 312 - backgroundColor: darkTheme.colors.background.secondary, 313 - borderBottomWidth: 1, 314 - borderBottomColor: darkTheme.colors.border.primary, 315 - alignItems: 'center', 316 - justifyContent: 'center', 317 - minHeight: 50, 318 - }, 319 - loadMoreText: { 320 - fontSize: 16, 321 - color: '#007AFF', 322 - fontWeight: '500', 323 - }, 324 - emptyContainer: { 325 - flex: 1, 326 - justifyContent: 'center', 327 - alignItems: 'center', 328 - padding: 40, 329 - backgroundColor: darkTheme.colors.background.primary, 330 - }, 331 - emptyText: { 332 - fontSize: 16, 333 - color: '#666', 334 - textAlign: 'center', 335 - lineHeight: 22, 336 - }, 337 - });
-154
SETUP.md
··· 1 - # Letta Chat - Setup Guide 2 - 3 - ## ✅ Project Status: COMPLETED 4 - 5 - This React Native application is now fully functional and ready for development and production use. 6 - 7 - ## 🚀 Quick Start 8 - 9 - To run the application immediately: 10 - 11 - ```bash 12 - cd /Users/cameron/letta/chat/ 13 - npm install # If not already done 14 - npm run web # For web version 15 - ``` 16 - 17 - The app will be available at: **http://localhost:8081** 18 - 19 - ## 🔧 What Was Fixed & Implemented 20 - 21 - ### ✅ Structural Issues 22 - - **Project Location**: Moved all files from `letta-chat/` subdirectory to `/Users/cameron/letta/chat/` 23 - - **Directory Structure**: Clean, professional folder organization 24 - - **Package Management**: All dependencies properly installed and configured 25 - 26 - ### ✅ Technical Fixes 27 - - **Dependency Compatibility**: Fixed all Expo SDK 53 version conflicts 28 - - **TypeScript**: Zero compilation errors 29 - - **Web Support**: Full web compatibility with proper bundling 30 - - **Cross-Platform**: Ready for iOS, Android, and web deployment 31 - 32 - ### ✅ Features Implemented 33 - - **Authentication**: Secure API token management with AsyncStorage persistence 34 - - **Agent Management**: Create, list, and select Letta agents via drawer navigation 35 - - **Chat Interface**: Real-time messaging with proper message bubbles and history 36 - - **Error Handling**: Comprehensive network error handling with user-friendly messages 37 - - **Cross-Platform Compatibility**: Web-compatible prompts and alerts 38 - - **State Management**: Zustand for efficient state management 39 - - **UI/UX**: Modern iOS-style interface with proper loading states 40 - 41 - ### ✅ Key Components 42 - - **API Service Layer**: Complete integration with Letta's REST API endpoints 43 - - **Navigation**: Drawer navigation with agent selection 44 - - **Chat Screen**: Message display with input and history 45 - - **Settings Screen**: API token configuration 46 - - **Message Bubbles**: User and assistant message display 47 - - **Agent Cards**: Visual agent selection interface 48 - - **Cross-Platform Prompts**: Web and mobile compatible dialogs 49 - 50 - ## 📱 Platform Status 51 - 52 - ### Web ✅ 53 - - **Status**: Fully working 54 - - **URL**: http://localhost:19006 55 - - **Features**: All functionality working including prompts and navigation 56 - 57 - ### Mobile 🟨 58 - - **Status**: Ready for testing 59 - - **iOS**: Run `npm start` and press `i` (requires Xcode) 60 - - **Android**: Run `npm start` and press `a` (requires Android Studio) 61 - - **Expo Go**: Run `npm start` and scan QR code 62 - 63 - ## 🔑 Usage Instructions 64 - 65 - 1. **Start the application**: 66 - ```bash 67 - npm run web 68 - ``` 69 - 70 - 2. **Configure API access**: 71 - - Get your Letta API token from the Letta dashboard 72 - - Open the app settings (gear icon in drawer) 73 - - Enter your API token and click "Save & Connect" 74 - 75 - 3. **Create/Select agents**: 76 - - Open the drawer (hamburger menu) 77 - - Click "+" to create a new agent or select existing one 78 - - Agent creation uses cross-platform prompts 79 - 80 - 4. **Start chatting**: 81 - - Select an agent from the drawer 82 - - Type messages in the input field 83 - - View conversation history with pull-to-refresh 84 - 85 - ## 🛠️ Development Commands 86 - 87 - ```bash 88 - # Start development server 89 - npm start 90 - 91 - # Web development 92 - npm run web 93 - 94 - # iOS simulator 95 - npm run ios 96 - 97 - # Android emulator 98 - npm run android 99 - 100 - # Type checking 101 - npx tsc --noEmit 102 - 103 - # Clear cache and restart 104 - npx expo start -c 105 - ``` 106 - 107 - ## 📁 Project Structure 108 - 109 - ``` 110 - /Users/cameron/letta/chat/ 111 - ├── src/ 112 - │ ├── api/ # Letta API integration 113 - │ ├── components/ # Reusable UI components 114 - │ ├── screens/ # Main app screens 115 - │ ├── navigation/ # React Navigation setup 116 - │ ├── store/ # Zustand state management 117 - │ ├── types/ # TypeScript definitions 118 - │ └── utils/ # Helper functions 119 - ├── App.tsx # Main app component 120 - ├── package.json # Dependencies and scripts 121 - ├── README.md # Main documentation 122 - └── SETUP.md # This file 123 - ``` 124 - 125 - ## ✨ Next Steps 126 - 127 - The application is production-ready. Optional enhancements could include: 128 - - Push notifications 129 - - Dark mode theme 130 - - Message search functionality 131 - - File upload support 132 - - Voice message recording 133 - - Agent customization options 134 - 135 - ## 🐛 Troubleshooting 136 - 137 - If you encounter issues: 138 - 139 - 1. **Clear cache**: `npx expo start -c` 140 - 2. **Reinstall dependencies**: `rm -rf node_modules && npm install` 141 - 3. **Check API token**: Ensure it's valid and has proper permissions 142 - 4. **Network issues**: Check internet connection and Letta service status 143 - 144 - ## 📞 Support 145 - 146 - - Letta Documentation: https://docs.letta.com 147 - - React Native Docs: https://reactnative.dev 148 - - Expo Documentation: https://docs.expo.dev 149 - 150 - --- 151 - 152 - **Status**: ✅ READY FOR USE 153 - **Last Updated**: September 3, 2025 154 - **Version**: 1.0.0
-258
src/components/AgentsDrawerContent.tsx
··· 1 - import React, { useEffect } from 'react'; 2 - import { 3 - View, 4 - Text, 5 - ScrollView, 6 - TouchableOpacity, 7 - StyleSheet, 8 - useColorScheme, 9 - ActivityIndicator, 10 - } from 'react-native'; 11 - import { DrawerContentComponentProps } from '@react-navigation/drawer'; 12 - import { Ionicons } from '@expo/vector-icons'; 13 - import { useSafeAreaInsets } from 'react-native-safe-area-context'; 14 - import AgentCard from './AgentCard'; 15 - import useAppStore from '../store/appStore'; 16 - import { darkTheme } from '../theme'; 17 - import { showPrompt, showAlert } from '../utils/prompts'; 18 - 19 - const AgentsDrawerContent: React.FC<DrawerContentComponentProps> = (props) => { 20 - const insets = useSafeAreaInsets(); 21 - const { 22 - agents, 23 - currentAgentId, 24 - isLoading, 25 - error, 26 - setCurrentAgent, 27 - createAgent, 28 - fetchAgents, 29 - toggleFavorite, 30 - isFavorite, 31 - } = useAppStore(); 32 - 33 - useEffect(() => { 34 - fetchAgents(); 35 - }, [fetchAgents]); 36 - 37 - const handleAgentPress = (agentId: string) => { 38 - setCurrentAgent(agentId); 39 - props.navigation.closeDrawer(); 40 - }; 41 - 42 - const handleCreateAgent = () => { 43 - showPrompt({ 44 - title: 'Create New Agent', 45 - message: 'Enter a name for your new agent:', 46 - placeholder: 'Agent name', 47 - onConfirm: async (name) => { 48 - try { 49 - await createAgent(name); 50 - } catch (error) { 51 - showAlert('Error', 'Failed to create agent. Please try again.'); 52 - } 53 - }, 54 - }); 55 - }; 56 - 57 - const currentAgent = agents.find(agent => agent.id === currentAgentId); 58 - const colorScheme = useColorScheme(); 59 - 60 - return ( 61 - <View style={[styles.container, { paddingTop: insets.top }]}> 62 - <View style={styles.header}> 63 - <Text style={styles.title}>Agents</Text> 64 - <TouchableOpacity 65 - style={styles.createButton} 66 - onPress={handleCreateAgent} 67 - disabled={isLoading} 68 - > 69 - <Ionicons name="add" size={24} color="#007AFF" /> 70 - </TouchableOpacity> 71 - </View> 72 - 73 - {error && ( 74 - <View style={styles.errorContainer}> 75 - <Text style={styles.errorText}>{error}</Text> 76 - <TouchableOpacity onPress={fetchAgents} style={styles.retryButton}> 77 - <Text style={styles.retryText}>Retry</Text> 78 - </TouchableOpacity> 79 - </View> 80 - )} 81 - 82 - {!currentAgentId && !isLoading && agents.length > 0 && ( 83 - <View style={styles.noSelectionContainer}> 84 - <Text style={styles.noSelectionText}> 85 - Select an agent to start chatting 86 - </Text> 87 - </View> 88 - )} 89 - 90 - <ScrollView style={styles.agentsList} showsVerticalScrollIndicator={false}> 91 - {agents.map((agent) => ( 92 - <AgentCard 93 - key={agent.id} 94 - agent={agent} 95 - isSelected={agent.id === currentAgentId} 96 - onPress={() => handleAgentPress(agent.id)} 97 - isFavorited={isFavorite(agent.id)} 98 - onToggleFavorite={() => toggleFavorite(agent.id)} 99 - /> 100 - ))} 101 - 102 - {agents.length === 0 && !isLoading && !error && ( 103 - <View style={styles.emptyContainer}> 104 - <Ionicons name="people-outline" size={48} color="#8E8E93" /> 105 - <Text style={styles.emptyTitle}>No agents yet</Text> 106 - <Text style={styles.emptySubtitle}> 107 - Create your first agent to get started 108 - </Text> 109 - <TouchableOpacity 110 - style={styles.emptyCreateButton} 111 - onPress={handleCreateAgent} 112 - > 113 - <Text style={styles.emptyCreateButtonText}>Create Agent</Text> 114 - </TouchableOpacity> 115 - </View> 116 - )} 117 - 118 - {isLoading && ( 119 - <View style={styles.loadingContainer}> 120 - <ActivityIndicator size="small" color={darkTheme.colors.text.secondary} /> 121 - <Text style={styles.loadingText}>Loading agents…</Text> 122 - </View> 123 - )} 124 - </ScrollView> 125 - 126 - {currentAgent && ( 127 - <View style={styles.currentAgentInfo}> 128 - <Text style={styles.currentAgentLabel}>Current:</Text> 129 - <Text style={styles.currentAgentName} numberOfLines={1}> 130 - {currentAgent.name} 131 - </Text> 132 - </View> 133 - )} 134 - </View> 135 - ); 136 - }; 137 - 138 - const styles = StyleSheet.create({ 139 - container: { 140 - flex: 1, 141 - backgroundColor: darkTheme.colors.background.primary, 142 - }, 143 - header: { 144 - flexDirection: 'row', 145 - alignItems: 'center', 146 - justifyContent: 'space-between', 147 - paddingHorizontal: 16, 148 - paddingVertical: 16, 149 - borderBottomWidth: 1, 150 - borderBottomColor: darkTheme.colors.border.primary, 151 - backgroundColor: darkTheme.colors.background.tertiary, 152 - }, 153 - title: { 154 - fontSize: 24, 155 - fontWeight: '700', 156 - color: darkTheme.colors.text.primary, 157 - }, 158 - createButton: { 159 - padding: 8, 160 - }, 161 - errorContainer: { 162 - margin: 16, 163 - padding: 12, 164 - backgroundColor: darkTheme.colors.status.error + '20', 165 - borderRadius: 8, 166 - borderLeftWidth: 4, 167 - borderLeftColor: darkTheme.colors.status.error, 168 - }, 169 - errorText: { 170 - fontSize: 14, 171 - color: darkTheme.colors.status.error, 172 - marginBottom: 8, 173 - }, 174 - retryButton: { 175 - alignSelf: 'flex-start', 176 - }, 177 - retryText: { 178 - fontSize: 14, 179 - color: darkTheme.colors.interactive.primary, 180 - fontWeight: '600', 181 - }, 182 - noSelectionContainer: { 183 - margin: 16, 184 - padding: 12, 185 - backgroundColor: darkTheme.colors.background.tertiary, 186 - borderRadius: 8, 187 - borderLeftWidth: 4, 188 - borderLeftColor: darkTheme.colors.interactive.primary, 189 - }, 190 - noSelectionText: { 191 - fontSize: 14, 192 - color: darkTheme.colors.text.secondary, 193 - textAlign: 'center', 194 - }, 195 - agentsList: { 196 - flex: 1, 197 - }, 198 - emptyContainer: { 199 - flex: 1, 200 - alignItems: 'center', 201 - justifyContent: 'center', 202 - paddingHorizontal: 32, 203 - paddingVertical: 64, 204 - }, 205 - emptyTitle: { 206 - fontSize: 18, 207 - fontWeight: '600', 208 - color: darkTheme.colors.text.secondary, 209 - marginTop: 16, 210 - marginBottom: 8, 211 - }, 212 - emptySubtitle: { 213 - fontSize: 14, 214 - color: darkTheme.colors.text.secondary, 215 - textAlign: 'center', 216 - marginBottom: 24, 217 - }, 218 - emptyCreateButton: { 219 - backgroundColor: darkTheme.colors.interactive.primary, 220 - paddingHorizontal: 24, 221 - paddingVertical: 12, 222 - borderRadius: 8, 223 - }, 224 - emptyCreateButtonText: { 225 - color: darkTheme.colors.text.inverse, 226 - fontSize: 16, 227 - fontWeight: '600', 228 - }, 229 - loadingContainer: { 230 - flexDirection: 'row', 231 - alignItems: 'center', 232 - paddingVertical: 12, 233 - paddingHorizontal: 16, 234 - }, 235 - loadingText: { 236 - fontSize: 14, 237 - color: darkTheme.colors.text.secondary, 238 - marginLeft: 8, 239 - }, 240 - currentAgentInfo: { 241 - padding: 16, 242 - backgroundColor: darkTheme.colors.background.tertiary, 243 - borderTopWidth: 1, 244 - borderTopColor: darkTheme.colors.border.primary, 245 - }, 246 - currentAgentLabel: { 247 - fontSize: 12, 248 - color: darkTheme.colors.text.secondary, 249 - marginBottom: 4, 250 - }, 251 - currentAgentName: { 252 - fontSize: 16, 253 - fontWeight: '600', 254 - color: darkTheme.colors.text.primary, 255 - }, 256 - }); 257 - 258 - export default AgentsDrawerContent;
-497
src/components/Sidebar.tsx
··· 1 - import React, { useState, useEffect } from 'react'; 2 - import { 3 - View, 4 - Text, 5 - StyleSheet, 6 - TouchableOpacity, 7 - ScrollView, 8 - RefreshControl, 9 - Alert, 10 - useColorScheme, 11 - ActivityIndicator, 12 - } from 'react-native'; 13 - import lettaApi from '../api/lettaApi'; 14 - import { darkTheme } from '../theme'; 15 - import Wordmark from './Wordmark'; 16 - import { Ionicons } from '@expo/vector-icons'; 17 - import type { LettaAgent, Project } from '../types/letta'; 18 - import useAppStore from '../store/appStore'; 19 - 20 - interface SidebarProps { 21 - currentProject: Project | null; 22 - currentAgent: LettaAgent | null; 23 - onAgentSelect: (agent: LettaAgent) => void; 24 - onProjectPress: () => void; 25 - onCreateAgent: () => void; 26 - onLogout: () => void; 27 - isVisible: boolean; 28 - onTabChange?: (tab: 'project' | 'favorites' | 'memory') => void; 29 - } 30 - 31 - export default function Sidebar({ 32 - currentProject, 33 - currentAgent, 34 - onAgentSelect, 35 - onProjectPress, 36 - onCreateAgent, 37 - onLogout, 38 - isVisible, 39 - onTabChange, 40 - }: SidebarProps) { 41 - const colorScheme = useColorScheme(); 42 - const [agents, setAgents] = useState<LettaAgent[]>([]); 43 - const [isLoading, setIsLoading] = useState(true); 44 - const [isRefreshing, setIsRefreshing] = useState(false); 45 - const [activeTab, setActiveTab] = useState<'project' | 'favorites'>('project'); 46 - const { favorites } = useAppStore(); 47 - const [favoriteAgents, setFavoriteAgents] = useState<LettaAgent[]>([]); 48 - // Simple, minimal loading indicator (no animated logo) 49 - 50 - const loadAgents = async (isRefresh = false) => { 51 - if (!currentProject) { 52 - setAgents([]); 53 - setIsLoading(false); 54 - return; 55 - } 56 - 57 - try { 58 - if (!isRefresh) setIsLoading(true); 59 - 60 - const agentList = await lettaApi.listAgentsForProject(currentProject.id, { 61 - sortBy: 'last_run_completion', 62 - limit: 50, 63 - }); 64 - 65 - setAgents(agentList); 66 - console.log('Loaded agents for sidebar:', agentList.length); 67 - } catch (error: any) { 68 - console.error('Failed to load agents:', error); 69 - Alert.alert('Error', 'Failed to load agents: ' + error.message); 70 - } finally { 71 - setIsLoading(false); 72 - setIsRefreshing(false); 73 - } 74 - }; 75 - 76 - const handleRefresh = () => { 77 - setIsRefreshing(true); 78 - if (activeTab === 'favorites') { 79 - loadFavoriteAgents(true); 80 - } else { 81 - loadAgents(true); 82 - } 83 - }; 84 - 85 - useEffect(() => { 86 - if (currentProject) { 87 - loadAgents(); 88 - } 89 - }, [currentProject]); 90 - 91 - useEffect(() => { 92 - onTabChange && onTabChange(activeTab); 93 - }, [activeTab]); 94 - 95 - // Load favorited agents across projects when switching to Favorites tab 96 - const loadFavoriteAgents = async (isRefresh = false) => { 97 - try { 98 - if (!isRefresh) setIsLoading(true); 99 - const results = await Promise.all( 100 - favorites.map(async (id) => { 101 - try { 102 - return await lettaApi.getAgent(id); 103 - } catch (e) { 104 - console.warn('Failed to fetch favorited agent', id, e); 105 - return null; 106 - } 107 - }) 108 - ); 109 - const list = results.filter((a): a is LettaAgent => !!a); 110 - // Keep same ordering as favorites list 111 - const ordered = favorites 112 - .map((id) => list.find((a) => a.id === id)) 113 - .filter((a): a is LettaAgent => !!a); 114 - setFavoriteAgents(ordered); 115 - } finally { 116 - setIsLoading(false); 117 - setIsRefreshing(false); 118 - } 119 - }; 120 - 121 - useEffect(() => { 122 - if (activeTab === 'favorites') { 123 - loadFavoriteAgents(); 124 - } 125 - }, [activeTab, JSON.stringify(favorites)]); 126 - 127 - if (!isVisible) return null; 128 - 129 - return ( 130 - <View style={styles.sidebar}> 131 - {/* Brand at top-left */} 132 - <View style={styles.brandBar}> 133 - <Wordmark width={240} height={40} /> 134 - </View> 135 - {/* Project Header moved to bottom */} 136 - 137 - {/* Agents List */} 138 - <View style={styles.agentListContainer}> 139 - <Text style={styles.sectionTitle}>Agents</Text> 140 - <View style={styles.tabsRow}> 141 - <TouchableOpacity 142 - onPress={() => setActiveTab('project')} 143 - style={[styles.tabButton, activeTab === 'project' && styles.tabButtonActive]} 144 - > 145 - <Text style={[styles.tabText, activeTab === 'project' && styles.tabTextActive]}>Project</Text> 146 - </TouchableOpacity> 147 - <TouchableOpacity 148 - onPress={() => setActiveTab('favorites')} 149 - style={[styles.tabButton, activeTab === 'favorites' && styles.tabButtonActive]} 150 - > 151 - <Text style={[styles.tabText, activeTab === 'favorites' && styles.tabTextActive]}>Favorites</Text> 152 - </TouchableOpacity> 153 - </View> 154 - {isLoading ? ( 155 - <View style={styles.loadingContainer}> 156 - <ActivityIndicator size="small" color={darkTheme.colors.text.secondary} /> 157 - <Text style={styles.loadingText}>Loading agents…</Text> 158 - </View> 159 - ) : ( 160 - <ScrollView 161 - style={styles.agentList} 162 - refreshControl={ 163 - <RefreshControl refreshing={isRefreshing} onRefresh={handleRefresh} /> 164 - } 165 - > 166 - {activeTab === 'favorites' ? ( 167 - favorites.length === 0 ? ( 168 - <View style={styles.emptyContainer}> 169 - <Text style={styles.emptyText}>No favorited agents</Text> 170 - </View> 171 - ) : favoriteAgents.length === 0 ? ( 172 - <View style={styles.emptyContainer}> 173 - <Text style={styles.emptyText}>Loading favorites…</Text> 174 - </View> 175 - ) : ( 176 - favoriteAgents.map((agent) => { 177 - const starred = favorites.includes(agent.id) 178 - return ( 179 - <TouchableOpacity 180 - key={agent.id} 181 - style={[ 182 - styles.agentItem, 183 - currentAgent?.id === agent.id && styles.selectedAgentItem 184 - ]} 185 - onPress={() => onAgentSelect(agent)} 186 - > 187 - <View style={styles.rowHeader}> 188 - <Text style={[ 189 - styles.agentName, 190 - currentAgent?.id === agent.id && styles.selectedAgentName 191 - ]}> 192 - {agent.name} 193 - </Text> 194 - <TouchableOpacity 195 - style={styles.starBtn} 196 - onPress={() => useAppStore.getState().toggleFavorite(agent.id)} 197 - accessibilityLabel={starred ? 'Unfavorite agent' : 'Favorite agent'} 198 - > 199 - <Ionicons name={starred ? 'star' : 'star-outline'} size={16} color={starred ? '#ffd166' : darkTheme.colors.text.secondary} /> 200 - </TouchableOpacity> 201 - </View> 202 - <Text style={styles.agentMeta}> 203 - {agent.last_run_completion 204 - ? `Last run: ${new Date(agent.last_run_completion).toLocaleDateString()}` 205 - : 'Never run'} 206 - </Text> 207 - </TouchableOpacity> 208 - ) 209 - }) 210 - ) 211 - ) : agents.length === 0 ? ( 212 - <View style={styles.emptyContainer}> 213 - <Text style={styles.emptyText}>No agents found</Text> 214 - <TouchableOpacity style={styles.createButton} onPress={onCreateAgent}> 215 - <Text style={styles.createButtonText}>Create Agent</Text> 216 - </TouchableOpacity> 217 - </View> 218 - ) : ( 219 - agents.map((agent) => { 220 - const starred = favorites.includes(agent.id) 221 - return ( 222 - <TouchableOpacity 223 - key={agent.id} 224 - style={[ 225 - styles.agentItem, 226 - currentAgent?.id === agent.id && styles.selectedAgentItem 227 - ]} 228 - onPress={() => onAgentSelect(agent)} 229 - > 230 - <View style={styles.rowHeader}> 231 - <Text style={[ 232 - styles.agentName, 233 - currentAgent?.id === agent.id && styles.selectedAgentName 234 - ]}> 235 - {agent.name} 236 - </Text> 237 - <TouchableOpacity 238 - style={styles.starBtn} 239 - onPress={() => useAppStore.getState().toggleFavorite(agent.id)} 240 - accessibilityLabel={starred ? 'Unfavorite agent' : 'Favorite agent'} 241 - > 242 - <Ionicons name={starred ? 'star' : 'star-outline'} size={16} color={starred ? '#ffd166' : darkTheme.colors.text.secondary} /> 243 - </TouchableOpacity> 244 - </View> 245 - <Text style={styles.agentMeta}> 246 - {agent.last_run_completion 247 - ? `Last run: ${new Date(agent.last_run_completion).toLocaleDateString()}` 248 - : 'Never run'} 249 - </Text> 250 - </TouchableOpacity> 251 - ) 252 - }) 253 - )} 254 - </ScrollView> 255 - )} 256 - </View> 257 - 258 - {/* Bottom Actions */} 259 - <View style={styles.bottomActions}> 260 - <TouchableOpacity style={styles.createButton} onPress={onCreateAgent}> 261 - <Text style={styles.createButtonText}>+ New Agent</Text> 262 - </TouchableOpacity> 263 - 264 - <TouchableOpacity style={styles.bottomProjectSelector} onPress={onProjectPress}> 265 - <View style={styles.bottomProjectText}> 266 - <Text style={styles.bottomProjectLabel}>Project</Text> 267 - <Text style={styles.bottomProjectName} numberOfLines={1}> 268 - {currentProject?.name || 'Select Project'} 269 - </Text> 270 - </View> 271 - <Ionicons name="chevron-forward" size={16} color={darkTheme.colors.text.secondary} /> 272 - </TouchableOpacity> 273 - 274 - <TouchableOpacity style={styles.logoutButton} onPress={onLogout}> 275 - <Text style={styles.logoutButtonText}>Logout</Text> 276 - </TouchableOpacity> 277 - </View> 278 - </View> 279 - ); 280 - } 281 - 282 - const styles = StyleSheet.create({ 283 - sidebar: { 284 - width: darkTheme.layout.sidebarWidth, 285 - backgroundColor: darkTheme.colors.background.secondary, 286 - borderRightWidth: 1, 287 - borderRightColor: darkTheme.colors.border.primary, 288 - flexDirection: 'column', 289 - height: '100%', 290 - }, 291 - brandBar: { 292 - paddingHorizontal: darkTheme.spacing[2], 293 - height: darkTheme.layout.headerHeight, 294 - justifyContent: 'center', 295 - borderBottomWidth: 1, 296 - borderBottomColor: darkTheme.colors.border.primary, 297 - backgroundColor: darkTheme.colors.background.secondary, 298 - }, 299 - projectHeader: { 300 - display: 'none', 301 - }, 302 - projectHeaderText: { 303 - display: 'none', 304 - }, 305 - projectLabel: { 306 - fontSize: darkTheme.typography.label.fontSize, 307 - fontFamily: darkTheme.typography.label.fontFamily, 308 - fontWeight: darkTheme.typography.label.fontWeight, 309 - color: darkTheme.colors.text.secondary, 310 - textTransform: 'uppercase', 311 - letterSpacing: darkTheme.typography.label.letterSpacing, 312 - marginBottom: darkTheme.spacing[0.5], 313 - }, 314 - projectName: { 315 - fontSize: darkTheme.typography.h6.fontSize, 316 - fontWeight: darkTheme.typography.h6.fontWeight, 317 - fontFamily: darkTheme.typography.h6.fontFamily, 318 - color: darkTheme.colors.text.primary, 319 - letterSpacing: darkTheme.typography.h6.letterSpacing, 320 - }, 321 - projectSubtext: { 322 - fontSize: darkTheme.typography.caption.fontSize, 323 - fontFamily: darkTheme.typography.caption.fontFamily, 324 - color: darkTheme.colors.text.secondary, 325 - marginTop: darkTheme.spacing[0.5], 326 - }, 327 - agentListContainer: { 328 - flex: 1, 329 - paddingHorizontal: darkTheme.spacing[2.5] || darkTheme.spacing[2], 330 - }, 331 - sectionTitle: { 332 - fontSize: darkTheme.typography.technical.fontSize, 333 - fontWeight: darkTheme.typography.technical.fontWeight, 334 - fontFamily: darkTheme.typography.technical.fontFamily, 335 - color: darkTheme.colors.text.secondary, 336 - textTransform: darkTheme.typography.technical.textTransform, 337 - letterSpacing: darkTheme.typography.technical.letterSpacing, 338 - marginTop: darkTheme.spacing[2], 339 - marginBottom: darkTheme.spacing[1.5], 340 - }, 341 - tabsRow: { 342 - flexDirection: 'row', 343 - gap: darkTheme.spacing[1], 344 - marginBottom: darkTheme.spacing[1], 345 - }, 346 - tabButton: { 347 - paddingVertical: darkTheme.spacing[0.75] || 6, 348 - paddingHorizontal: darkTheme.spacing[1.5] || 10, 349 - borderRadius: darkTheme.layout.borderRadius.medium, 350 - borderWidth: 1, 351 - borderColor: darkTheme.colors.border.primary, 352 - backgroundColor: darkTheme.colors.background.surface, 353 - }, 354 - tabButtonActive: { 355 - backgroundColor: darkTheme.colors.interactive.secondary, 356 - borderColor: darkTheme.colors.interactive.secondary, 357 - }, 358 - tabText: { 359 - color: darkTheme.colors.text.secondary, 360 - fontSize: darkTheme.typography.caption.fontSize, 361 - fontFamily: darkTheme.typography.caption.fontFamily, 362 - }, 363 - tabTextActive: { 364 - color: darkTheme.colors.text.inverse, 365 - fontWeight: '600', 366 - }, 367 - loadingContainer: { 368 - flexDirection: 'row', 369 - alignItems: 'center', 370 - paddingVertical: darkTheme.spacing[1.5], 371 - paddingHorizontal: darkTheme.spacing[2], 372 - }, 373 - loadingText: { 374 - marginLeft: darkTheme.spacing[1], 375 - fontSize: darkTheme.typography.bodySmall.fontSize, 376 - fontFamily: darkTheme.typography.bodySmall.fontFamily, 377 - color: darkTheme.colors.text.secondary, 378 - }, 379 - agentList: { 380 - flex: 1, 381 - }, 382 - emptyContainer: { 383 - alignItems: 'center', 384 - justifyContent: 'center', 385 - paddingVertical: darkTheme.spacing[5], 386 - }, 387 - emptyText: { 388 - fontSize: darkTheme.typography.bodySmall.fontSize, 389 - fontFamily: darkTheme.typography.bodySmall.fontFamily, 390 - color: darkTheme.colors.text.secondary, 391 - marginBottom: darkTheme.spacing[2], 392 - textAlign: 'center', 393 - }, 394 - agentItem: { 395 - paddingVertical: darkTheme.spacing[2], 396 - paddingHorizontal: darkTheme.spacing[1.75] || darkTheme.spacing[2], 397 - marginBottom: darkTheme.spacing[1.5] || darkTheme.spacing[1.25], 398 - borderRadius: 0, 399 - // Keep a 1px transparent border so size doesn't change on selection 400 - borderWidth: 1, 401 - borderColor: 'transparent', 402 - backgroundColor: 'transparent', 403 - }, 404 - selectedAgentItem: { 405 - backgroundColor: (darkTheme as any).colors.background.selected || darkTheme.colors.background.surface, 406 - // Simple 1px outline on all sides; 90-degree corners 407 - borderWidth: 1, 408 - borderColor: darkTheme.colors.border.primary, 409 - borderRadius: 0, 410 - }, 411 - rowHeader: { 412 - flexDirection: 'row', 413 - alignItems: 'center', 414 - justifyContent: 'space-between', 415 - }, 416 - agentName: { 417 - fontSize: darkTheme.typography.body.fontSize, 418 - fontWeight: '600', 419 - fontFamily: darkTheme.typography.body.fontFamily, 420 - color: darkTheme.colors.text.primary, 421 - }, 422 - starBtn: { 423 - paddingHorizontal: 6, 424 - paddingVertical: 4, 425 - }, 426 - selectedAgentName: { 427 - color: darkTheme.colors.text.primary, 428 - fontWeight: '600', 429 - }, 430 - agentMeta: { 431 - fontSize: darkTheme.typography.caption.fontSize, 432 - fontFamily: darkTheme.typography.caption.fontFamily, 433 - color: darkTheme.colors.text.secondary, 434 - marginTop: darkTheme.spacing[0.75] || darkTheme.spacing[0.5], 435 - }, 436 - bottomActions: { 437 - padding: darkTheme.spacing[2], 438 - borderTopWidth: 1, 439 - borderTopColor: darkTheme.colors.border.primary, 440 - backgroundColor: darkTheme.colors.background.tertiary, 441 - }, 442 - bottomProjectSelector: { 443 - flexDirection: 'row', 444 - alignItems: 'center', 445 - paddingVertical: darkTheme.spacing[1], 446 - }, 447 - bottomProjectText: { 448 - flex: 1, 449 - }, 450 - bottomProjectLabel: { 451 - fontSize: darkTheme.typography.label.fontSize, 452 - fontFamily: darkTheme.typography.label.fontFamily, 453 - fontWeight: darkTheme.typography.label.fontWeight, 454 - color: darkTheme.colors.text.secondary, 455 - textTransform: 'uppercase', 456 - letterSpacing: darkTheme.typography.label.letterSpacing, 457 - marginBottom: darkTheme.spacing[0.5], 458 - }, 459 - bottomProjectName: { 460 - fontSize: darkTheme.typography.bodySmall.fontSize, 461 - fontWeight: darkTheme.typography.bodySmall.fontWeight, 462 - fontFamily: darkTheme.typography.bodySmall.fontFamily, 463 - color: darkTheme.colors.text.primary, 464 - letterSpacing: darkTheme.typography.bodySmall.letterSpacing, 465 - }, 466 - createButton: { 467 - backgroundColor: darkTheme.colors.interactive.secondary, 468 - paddingVertical: darkTheme.spacing[1.5], 469 - paddingHorizontal: darkTheme.spacing[2], 470 - borderRadius: darkTheme.layout.borderRadius.medium, 471 - marginBottom: darkTheme.spacing[1], 472 - shadowColor: darkTheme.colors.interactive.secondary, 473 - shadowOffset: { width: 0, height: 2 }, 474 - shadowOpacity: 0.3, 475 - shadowRadius: 4, 476 - elevation: 4, 477 - }, 478 - createButtonText: { 479 - color: darkTheme.colors.text.inverse, 480 - fontSize: darkTheme.typography.buttonSmall.fontSize, 481 - fontWeight: darkTheme.typography.buttonSmall.fontWeight, 482 - fontFamily: darkTheme.typography.buttonSmall.fontFamily, 483 - textAlign: 'center', 484 - textTransform: darkTheme.typography.buttonSmall.textTransform, 485 - letterSpacing: darkTheme.typography.buttonSmall.letterSpacing, 486 - }, 487 - logoutButton: { 488 - paddingVertical: darkTheme.spacing[1], 489 - paddingHorizontal: darkTheme.spacing[2], 490 - }, 491 - logoutButtonText: { 492 - color: darkTheme.colors.text.secondary, 493 - fontSize: darkTheme.typography.caption.fontSize, 494 - fontFamily: darkTheme.typography.caption.fontFamily, 495 - textAlign: 'center', 496 - }, 497 - });
-80
src/navigation/AppNavigator.tsx
··· 1 - import React from 'react'; 2 - import { NavigationContainer } from '@react-navigation/native'; 3 - import { createDrawerNavigator } from '@react-navigation/drawer'; 4 - import { createStackNavigator } from '@react-navigation/stack'; 5 - import { Ionicons } from '@expo/vector-icons'; 6 - 7 - import ChatScreen from '../screens/ChatScreen'; 8 - import SettingsScreen from '../screens/SettingsScreen'; 9 - import AgentsDrawerContent from '../components/AgentsDrawerContent'; 10 - import useAppStore from '../store/appStore'; 11 - 12 - export type RootStackParamList = { 13 - Main: undefined; 14 - Settings: undefined; 15 - }; 16 - 17 - export type MainDrawerParamList = { 18 - Chat: undefined; 19 - Settings: undefined; 20 - }; 21 - 22 - const Stack = createStackNavigator<RootStackParamList>(); 23 - const Drawer = createDrawerNavigator<MainDrawerParamList>(); 24 - 25 - function MainDrawer() { 26 - return ( 27 - <Drawer.Navigator 28 - drawerContent={(props) => <AgentsDrawerContent {...props} />} 29 - screenOptions={{ 30 - headerShown: true, 31 - drawerPosition: 'left', 32 - drawerStyle: { 33 - width: '80%', 34 - maxWidth: 350, 35 - }, 36 - }} 37 - > 38 - <Drawer.Screen 39 - name="Chat" 40 - component={ChatScreen} 41 - options={{ 42 - headerTitle: 'Letta Chat', 43 - drawerLabel: 'Chat', 44 - drawerIcon: ({ color, size }) => ( 45 - <Ionicons name="chatbubbles-outline" size={size} color={color} /> 46 - ), 47 - }} 48 - /> 49 - <Drawer.Screen 50 - name="Settings" 51 - component={SettingsScreen} 52 - options={{ 53 - headerTitle: 'Settings', 54 - drawerLabel: 'Settings', 55 - drawerIcon: ({ color, size }) => ( 56 - <Ionicons name="settings-outline" size={size} color={color} /> 57 - ), 58 - }} 59 - /> 60 - </Drawer.Navigator> 61 - ); 62 - } 63 - 64 - function AppNavigator() { 65 - const { isAuthenticated } = useAppStore(); 66 - 67 - return ( 68 - <NavigationContainer> 69 - <Stack.Navigator screenOptions={{ headerShown: false }}> 70 - {isAuthenticated ? ( 71 - <Stack.Screen name="Main" component={MainDrawer} /> 72 - ) : ( 73 - <Stack.Screen name="Settings" component={SettingsScreen} /> 74 - )} 75 - </Stack.Navigator> 76 - </NavigationContainer> 77 - ); 78 - } 79 - 80 - export default AppNavigator;
-204
src/screens/ChatScreen.tsx
··· 1 - import React, { useEffect, useRef } from 'react'; 2 - import { 3 - View, 4 - FlatList, 5 - Text, 6 - StyleSheet, 7 - SafeAreaView, 8 - KeyboardAvoidingView, 9 - Platform, 10 - RefreshControl, 11 - } from 'react-native'; 12 - import { useFocusEffect } from '@react-navigation/native'; 13 - import MessageBubble from '../components/MessageBubble'; 14 - import ChatInput from '../components/ChatInput'; 15 - import useAppStore from '../store/appStore'; 16 - import { LettaMessage } from '../types/letta'; 17 - import { darkTheme } from '../theme'; 18 - 19 - const ChatScreen: React.FC = () => { 20 - const flatListRef = useRef<FlatList>(null); 21 - const { 22 - currentAgentId, 23 - messages, 24 - agents, 25 - isLoading, 26 - error, 27 - sendMessage, 28 - fetchMessages, 29 - } = useAppStore(); 30 - 31 - const currentAgent = agents.find(agent => agent.id === currentAgentId); 32 - const currentMessages = currentAgentId ? messages[currentAgentId] || [] : []; 33 - 34 - useFocusEffect( 35 - React.useCallback(() => { 36 - if (currentAgentId) { 37 - fetchMessages(currentAgentId); 38 - } 39 - }, [currentAgentId, fetchMessages]) 40 - ); 41 - 42 - useEffect(() => { 43 - // Jump to bottom when new messages arrive 44 - if (currentMessages.length > 0) { 45 - setTimeout(() => { 46 - flatListRef.current?.scrollToEnd({ animated: false }); 47 - }, 100); 48 - } 49 - }, [currentMessages.length]); 50 - 51 - const handleSendMessage = async (content: string) => { 52 - if (currentAgentId) { 53 - await sendMessage(currentAgentId, content); 54 - } 55 - }; 56 - 57 - const handleRefresh = () => { 58 - if (currentAgentId) { 59 - fetchMessages(currentAgentId); 60 - } 61 - }; 62 - 63 - const renderMessage = ({ item, index }: { item: LettaMessage; index: number }) => { 64 - return <MessageBubble key={item.id || index} message={item} />; 65 - }; 66 - 67 - const renderEmptyState = () => { 68 - if (!currentAgentId) { 69 - return ( 70 - <View style={styles.emptyContainer}> 71 - <Text style={styles.emptyTitle}>Select an Agent</Text> 72 - <Text style={styles.emptySubtitle}> 73 - Choose an agent from the drawer to start chatting 74 - </Text> 75 - </View> 76 - ); 77 - } 78 - 79 - if (currentMessages.length === 0 && !isLoading) { 80 - return ( 81 - <View style={styles.emptyContainer}> 82 - <Text style={styles.emptyTitle}>Start a conversation</Text> 83 - <Text style={styles.emptySubtitle}> 84 - Send a message to {currentAgent?.name || 'your agent'} to begin 85 - </Text> 86 - </View> 87 - ); 88 - } 89 - 90 - return null; 91 - }; 92 - 93 - const renderError = () => { 94 - if (error) { 95 - return ( 96 - <View style={styles.errorContainer}> 97 - <Text style={styles.errorText}>{error}</Text> 98 - </View> 99 - ); 100 - } 101 - return null; 102 - }; 103 - 104 - return ( 105 - <SafeAreaView style={styles.container}> 106 - <KeyboardAvoidingView 107 - style={styles.container} 108 - behavior={Platform.OS === 'ios' ? 'padding' : 'height'} 109 - keyboardVerticalOffset={Platform.OS === 'ios' ? 88 : 0} 110 - > 111 - <View style={styles.messagesContainer}> 112 - {renderError()} 113 - <FlatList 114 - ref={flatListRef} 115 - data={currentMessages} 116 - renderItem={renderMessage} 117 - keyExtractor={(item, index) => item.id || `message-${index}`} 118 - contentContainerStyle={[ 119 - styles.messagesList, 120 - currentMessages.length === 0 && styles.emptyMessagesList, 121 - ]} 122 - ListEmptyComponent={renderEmptyState} 123 - showsVerticalScrollIndicator={false} 124 - refreshControl={ 125 - <RefreshControl 126 - refreshing={isLoading} 127 - onRefresh={handleRefresh} 128 - tintColor={darkTheme.colors.interactive.primary} 129 - /> 130 - } 131 - onContentSizeChange={() => { 132 - if (currentMessages.length > 0) { 133 - flatListRef.current?.scrollToEnd({ animated: false }); 134 - } 135 - }} 136 - /> 137 - </View> 138 - 139 - <ChatInput 140 - onSendMessage={handleSendMessage} 141 - disabled={!currentAgentId || isLoading} 142 - placeholder={ 143 - !currentAgentId 144 - ? "Select an agent to start chatting..." 145 - : isLoading 146 - ? "Sending..." 147 - : "Type a message..." 148 - } 149 - /> 150 - </KeyboardAvoidingView> 151 - </SafeAreaView> 152 - ); 153 - }; 154 - 155 - const styles = StyleSheet.create({ 156 - container: { 157 - flex: 1, 158 - backgroundColor: darkTheme.colors.background.primary, 159 - }, 160 - messagesContainer: { 161 - flex: 1, 162 - }, 163 - messagesList: { 164 - paddingVertical: 8, 165 - }, 166 - emptyMessagesList: { 167 - flex: 1, 168 - justifyContent: 'center', 169 - }, 170 - emptyContainer: { 171 - flex: 1, 172 - alignItems: 'center', 173 - justifyContent: 'center', 174 - paddingHorizontal: 32, 175 - }, 176 - emptyTitle: { 177 - fontSize: 20, 178 - fontWeight: '600', 179 - color: darkTheme.colors.text.secondary, 180 - marginBottom: 8, 181 - textAlign: 'center', 182 - }, 183 - emptySubtitle: { 184 - fontSize: 16, 185 - color: darkTheme.colors.text.secondary, 186 - textAlign: 'center', 187 - lineHeight: 22, 188 - }, 189 - errorContainer: { 190 - margin: 16, 191 - padding: 12, 192 - backgroundColor: darkTheme.colors.status.error + '20', 193 - borderRadius: 8, 194 - borderLeftWidth: 4, 195 - borderLeftColor: darkTheme.colors.status.error, 196 - }, 197 - errorText: { 198 - fontSize: 14, 199 - color: darkTheme.colors.status.error, 200 - textAlign: 'center', 201 - }, 202 - }); 203 - 204 - export default ChatScreen;
-349
src/screens/SettingsScreen.tsx
··· 1 - import React, { useState } from 'react'; 2 - import { 3 - View, 4 - Text, 5 - TextInput, 6 - TouchableOpacity, 7 - StyleSheet, 8 - SafeAreaView, 9 - ScrollView, 10 - KeyboardAvoidingView, 11 - Platform, 12 - } from 'react-native'; 13 - import { Ionicons } from '@expo/vector-icons'; 14 - import { darkTheme } from '../theme'; 15 - import useAppStore from '../store/appStore'; 16 - import { showAlert, showConfirmAlert } from '../utils/prompts'; 17 - 18 - const SettingsScreen: React.FC = () => { 19 - const { 20 - apiToken, 21 - isAuthenticated, 22 - isLoading, 23 - error, 24 - setApiToken, 25 - clearApiToken, 26 - reset, 27 - } = useAppStore(); 28 - 29 - const [tokenInput, setTokenInput] = useState(apiToken || ''); 30 - const [showToken, setShowToken] = useState(false); 31 - 32 - const handleSaveToken = async () => { 33 - const trimmedToken = tokenInput.trim(); 34 - 35 - if (!trimmedToken) { 36 - showAlert('Error', 'Please enter a valid API token'); 37 - return; 38 - } 39 - 40 - try { 41 - await setApiToken(trimmedToken); 42 - } catch (error) { 43 - // Error is already handled in the store 44 - } 45 - }; 46 - 47 - const handleClearToken = () => { 48 - showConfirmAlert( 49 - 'Clear API Token', 50 - 'This will log you out and clear all data. Are you sure?', 51 - () => { 52 - clearApiToken(); 53 - setTokenInput(''); 54 - }, 55 - undefined, 56 - 'Clear' 57 - ); 58 - }; 59 - 60 - const handleResetApp = () => { 61 - showConfirmAlert( 62 - 'Reset App', 63 - 'This will clear all data and log you out. Are you sure?', 64 - () => { 65 - reset(); 66 - setTokenInput(''); 67 - }, 68 - undefined, 69 - 'Reset' 70 - ); 71 - }; 72 - 73 - return ( 74 - <SafeAreaView style={styles.container}> 75 - <KeyboardAvoidingView 76 - style={styles.container} 77 - behavior={Platform.OS === 'ios' ? 'padding' : 'height'} 78 - > 79 - <ScrollView style={styles.scrollView} keyboardShouldPersistTaps="handled"> 80 - <View style={styles.header}> 81 - <Text style={styles.title}>Settings</Text> 82 - <Text style={styles.subtitle}> 83 - Configure your Letta API connection 84 - </Text> 85 - </View> 86 - 87 - <View style={styles.section}> 88 - <Text style={styles.sectionTitle}>API Configuration</Text> 89 - 90 - <View style={styles.inputContainer}> 91 - <Text style={styles.inputLabel}>API Token</Text> 92 - <View style={styles.passwordInputContainer}> 93 - <TextInput 94 - style={styles.tokenInput} 95 - value={tokenInput} 96 - onChangeText={setTokenInput} 97 - placeholder="Enter your Letta API token" 98 - placeholderTextColor={darkTheme.colors.text.secondary} 99 - secureTextEntry={!showToken} 100 - editable={!isLoading} 101 - autoCapitalize="none" 102 - autoCorrect={false} 103 - /> 104 - <TouchableOpacity 105 - style={styles.eyeButton} 106 - onPress={() => setShowToken(!showToken)} 107 - > 108 - <Ionicons 109 - name={showToken ? 'eye-off-outline' : 'eye-outline'} 110 - size={20} 111 - color={darkTheme.colors.text.secondary} 112 - /> 113 - </TouchableOpacity> 114 - </View> 115 - 116 - {error && ( 117 - <View style={styles.errorContainer}> 118 - <Text style={styles.errorText}>{error}</Text> 119 - </View> 120 - )} 121 - 122 - {isAuthenticated && ( 123 - <View style={styles.successContainer}> 124 - <Ionicons name="checkmark-circle" size={16} color={darkTheme.colors.status.success} /> 125 - <Text style={styles.successText}>Connected successfully</Text> 126 - </View> 127 - )} 128 - 129 - <TouchableOpacity 130 - style={[ 131 - styles.saveButton, 132 - isLoading && styles.saveButtonDisabled, 133 - ]} 134 - onPress={handleSaveToken} 135 - disabled={isLoading} 136 - > 137 - <Text style={styles.saveButtonText}> 138 - {isLoading ? 'Connecting...' : 'Save & Connect'} 139 - </Text> 140 - </TouchableOpacity> 141 - </View> 142 - 143 - {isAuthenticated && ( 144 - <TouchableOpacity 145 - style={styles.clearButton} 146 - onPress={handleClearToken} 147 - > 148 - <Text style={styles.clearButtonText}>Clear Token</Text> 149 - </TouchableOpacity> 150 - )} 151 - </View> 152 - 153 - <View style={styles.section}> 154 - <Text style={styles.sectionTitle}>About</Text> 155 - 156 - <View style={styles.infoContainer}> 157 - <Text style={styles.infoTitle}>Letta Chat App</Text> 158 - <Text style={styles.infoText}> 159 - Connect to your Letta agents and have conversations with AI assistants. 160 - </Text> 161 - 162 - <Text style={styles.infoSubtitle}>Getting Started:</Text> 163 - <Text style={styles.infoText}> 164 - 1. Get your API token from the Letta dashboard{'\n'} 165 - 2. Enter it above and tap "Save & Connect"{'\n'} 166 - 3. Create or select an agent from the drawer{'\n'} 167 - 4. Start chatting! 168 - </Text> 169 - 170 - <Text style={styles.infoSubtitle}>Documentation:</Text> 171 - <Text style={styles.linkText}>docs.letta.com</Text> 172 - </View> 173 - </View> 174 - 175 - <View style={styles.section}> 176 - <Text style={styles.sectionTitle}>Data & Privacy</Text> 177 - 178 - <TouchableOpacity 179 - style={styles.dangerButton} 180 - onPress={handleResetApp} 181 - > 182 - <Ionicons name="trash-outline" size={20} color="#FF3B30" /> 183 - <Text style={styles.dangerButtonText}>Reset App Data</Text> 184 - </TouchableOpacity> 185 - </View> 186 - </ScrollView> 187 - </KeyboardAvoidingView> 188 - </SafeAreaView> 189 - ); 190 - }; 191 - 192 - const styles = StyleSheet.create({ 193 - container: { 194 - flex: 1, 195 - backgroundColor: darkTheme.colors.background.primary, 196 - }, 197 - scrollView: { 198 - flex: 1, 199 - }, 200 - header: { 201 - padding: 24, 202 - paddingBottom: 16, 203 - }, 204 - title: { 205 - fontSize: 32, 206 - fontWeight: '700', 207 - color: darkTheme.colors.text.primary, 208 - marginBottom: 8, 209 - }, 210 - subtitle: { 211 - fontSize: 16, 212 - color: darkTheme.colors.text.secondary, 213 - }, 214 - section: { 215 - marginBottom: 32, 216 - }, 217 - sectionTitle: { 218 - fontSize: 20, 219 - fontWeight: '600', 220 - color: darkTheme.colors.text.primary, 221 - marginBottom: 16, 222 - paddingHorizontal: 24, 223 - }, 224 - inputContainer: { 225 - paddingHorizontal: 24, 226 - }, 227 - inputLabel: { 228 - fontSize: 16, 229 - fontWeight: '500', 230 - color: darkTheme.colors.text.primary, 231 - marginBottom: 8, 232 - }, 233 - passwordInputContainer: { 234 - flexDirection: 'row', 235 - alignItems: 'center', 236 - backgroundColor: darkTheme.colors.background.surface, 237 - borderRadius: 12, 238 - borderWidth: 1, 239 - borderColor: darkTheme.colors.border.primary, 240 - }, 241 - tokenInput: { 242 - flex: 1, 243 - height: 48, 244 - paddingHorizontal: 16, 245 - fontSize: 16, 246 - color: darkTheme.colors.text.primary, 247 - }, 248 - eyeButton: { 249 - padding: 12, 250 - }, 251 - errorContainer: { 252 - marginTop: 8, 253 - padding: 12, 254 - backgroundColor: darkTheme.colors.status.error + '20', 255 - borderRadius: 8, 256 - borderLeftWidth: 4, 257 - borderLeftColor: darkTheme.colors.status.error, 258 - }, 259 - errorText: { 260 - fontSize: 14, 261 - color: darkTheme.colors.status.error, 262 - }, 263 - successContainer: { 264 - flexDirection: 'row', 265 - alignItems: 'center', 266 - marginTop: 8, 267 - padding: 12, 268 - backgroundColor: darkTheme.colors.status.success + '20', 269 - borderRadius: 8, 270 - borderLeftWidth: 4, 271 - borderLeftColor: darkTheme.colors.status.success, 272 - }, 273 - successText: { 274 - fontSize: 14, 275 - color: darkTheme.colors.status.success, 276 - marginLeft: 8, 277 - }, 278 - saveButton: { 279 - backgroundColor: darkTheme.colors.interactive.primary, 280 - borderRadius: 12, 281 - height: 48, 282 - alignItems: 'center', 283 - justifyContent: 'center', 284 - marginTop: 16, 285 - }, 286 - saveButtonDisabled: { 287 - opacity: 0.6, 288 - }, 289 - saveButtonText: { 290 - fontSize: 16, 291 - fontWeight: '600', 292 - color: darkTheme.colors.text.inverse, 293 - }, 294 - clearButton: { 295 - marginTop: 12, 296 - marginHorizontal: 24, 297 - paddingVertical: 12, 298 - alignItems: 'center', 299 - }, 300 - clearButtonText: { 301 - fontSize: 16, 302 - color: darkTheme.colors.status.error, 303 - fontWeight: '500', 304 - }, 305 - infoContainer: { 306 - paddingHorizontal: 24, 307 - }, 308 - infoTitle: { 309 - fontSize: 18, 310 - fontWeight: '600', 311 - color: darkTheme.colors.text.primary, 312 - marginBottom: 8, 313 - }, 314 - infoSubtitle: { 315 - fontSize: 16, 316 - fontWeight: '500', 317 - color: darkTheme.colors.text.primary, 318 - marginTop: 16, 319 - marginBottom: 8, 320 - }, 321 - infoText: { 322 - fontSize: 14, 323 - color: darkTheme.colors.text.secondary, 324 - lineHeight: 20, 325 - }, 326 - linkText: { 327 - fontSize: 14, 328 - color: darkTheme.colors.interactive.primary, 329 - textDecorationLine: 'underline', 330 - }, 331 - dangerButton: { 332 - flexDirection: 'row', 333 - alignItems: 'center', 334 - justifyContent: 'center', 335 - marginHorizontal: 24, 336 - paddingVertical: 12, 337 - borderWidth: 1, 338 - borderColor: darkTheme.colors.status.error, 339 - borderRadius: 12, 340 - }, 341 - dangerButtonText: { 342 - fontSize: 16, 343 - color: darkTheme.colors.status.error, 344 - fontWeight: '500', 345 - marginLeft: 8, 346 - }, 347 - }); 348 - 349 - export default SettingsScreen;
-316
src/store/appStore.ts
··· 1 - import { create } from 'zustand'; 2 - import { persist } from 'zustand/middleware'; 3 - import AsyncStorage from '@react-native-async-storage/async-storage'; 4 - import lettaApi from '../api/lettaApi'; 5 - import { LettaAgent, LettaMessage, SendMessageRequest, ApiError } from '../types/letta'; 6 - 7 - interface AppState { 8 - // Authentication 9 - apiToken: string | null; 10 - isAuthenticated: boolean; 11 - 12 - // Agents 13 - agents: LettaAgent[]; 14 - currentAgentId: string | null; 15 - favorites: string[]; // agent IDs favorited across projects 16 - 17 - // Messages 18 - messages: Record<string, LettaMessage[]>; 19 - 20 - // UI State 21 - isLoading: boolean; 22 - isDrawerOpen: boolean; 23 - error: string | null; 24 - 25 - // Actions 26 - setApiToken: (token: string) => Promise<void>; 27 - clearApiToken: () => void; 28 - 29 - // Agent Actions 30 - fetchAgents: () => Promise<void>; 31 - setCurrentAgent: (agentId: string) => void; 32 - createAgent: (name: string, description?: string) => Promise<LettaAgent>; 33 - toggleFavorite: (agentId: string) => void; 34 - isFavorite: (agentId: string) => boolean; 35 - 36 - // Message Actions 37 - fetchMessages: (agentId: string) => Promise<void>; 38 - sendMessage: (agentId: string, content: string) => Promise<void>; 39 - 40 - // UI Actions 41 - setLoading: (loading: boolean) => void; 42 - setError: (error: string | null) => void; 43 - setDrawerOpen: (open: boolean) => void; 44 - 45 - // Reset 46 - reset: () => void; 47 - } 48 - 49 - const useAppStore = create<AppState>()( 50 - persist( 51 - (set, get) => ({ 52 - // Initial State 53 - apiToken: null, 54 - isAuthenticated: false, 55 - agents: [], 56 - currentAgentId: null, 57 - favorites: [], 58 - messages: {}, 59 - isLoading: false, 60 - isDrawerOpen: false, 61 - error: null, 62 - 63 - // Authentication Actions 64 - setApiToken: async (token: string) => { 65 - set({ isLoading: true, error: null }); 66 - 67 - try { 68 - lettaApi.setAuthToken(token); 69 - const isValid = await lettaApi.testConnection(); 70 - 71 - if (isValid) { 72 - set({ 73 - apiToken: token, 74 - isAuthenticated: true, 75 - isLoading: false, 76 - }); 77 - 78 - // Fetch agents after successful authentication 79 - await get().fetchAgents(); 80 - } else { 81 - lettaApi.removeAuthToken(); 82 - set({ 83 - error: 'Invalid API token', 84 - isLoading: false, 85 - }); 86 - } 87 - } catch (error) { 88 - lettaApi.removeAuthToken(); 89 - set({ 90 - error: (error as ApiError).message || 'Authentication failed', 91 - isLoading: false, 92 - }); 93 - } 94 - }, 95 - 96 - clearApiToken: () => { 97 - lettaApi.removeAuthToken(); 98 - set({ 99 - apiToken: null, 100 - isAuthenticated: false, 101 - agents: [], 102 - currentAgentId: null, 103 - messages: {}, 104 - error: null, 105 - }); 106 - }, 107 - 108 - // Agent Actions 109 - fetchAgents: async () => { 110 - if (!get().isAuthenticated) return; 111 - 112 - set({ isLoading: true, error: null }); 113 - 114 - try { 115 - const agents = await lettaApi.listAgents({ limit: 50 }); 116 - set({ agents, isLoading: false }); 117 - } catch (error) { 118 - set({ 119 - error: (error as ApiError).message || 'Failed to fetch agents', 120 - isLoading: false, 121 - }); 122 - } 123 - }, 124 - 125 - setCurrentAgent: (agentId: string) => { 126 - set({ currentAgentId: agentId }); 127 - 128 - // Fetch messages for the selected agent 129 - get().fetchMessages(agentId); 130 - }, 131 - 132 - toggleFavorite: (agentId: string) => { 133 - const { favorites } = get(); 134 - const next = favorites.includes(agentId) 135 - ? favorites.filter((id) => id !== agentId) 136 - : [...favorites, agentId]; 137 - set({ favorites: next }); 138 - }, 139 - isFavorite: (agentId: string) => get().favorites.includes(agentId), 140 - 141 - createAgent: async (name: string, description?: string) => { 142 - if (!get().isAuthenticated) { 143 - throw new Error('Not authenticated'); 144 - } 145 - 146 - set({ isLoading: true, error: null }); 147 - 148 - try { 149 - const newAgent = await lettaApi.createAgent({ 150 - name, 151 - description, 152 - model: 'openai/gpt-4.1', 153 - embedding: 'openai/text-embedding-3-small', 154 - memory_blocks: [ 155 - { 156 - label: 'human', 157 - value: 'The user is using a mobile chat application.', 158 - }, 159 - { 160 - label: 'persona', 161 - value: 'I am a helpful AI assistant.', 162 - }, 163 - ], 164 - }); 165 - 166 - const agents = [...get().agents, newAgent]; 167 - set({ agents, isLoading: false }); 168 - 169 - return newAgent; 170 - } catch (error) { 171 - set({ 172 - error: (error as ApiError).message || 'Failed to create agent', 173 - isLoading: false, 174 - }); 175 - throw error; 176 - } 177 - }, 178 - 179 - // Message Actions 180 - fetchMessages: async (agentId: string) => { 181 - if (!get().isAuthenticated) return; 182 - 183 - set({ isLoading: true, error: null }); 184 - 185 - try { 186 - const messages = await lettaApi.listMessages(agentId, { 187 - limit: 50, 188 - use_assistant_message: true, 189 - }); 190 - 191 - set(state => ({ 192 - messages: { 193 - ...state.messages, 194 - [agentId]: messages.reverse(), // Reverse to show oldest first 195 - }, 196 - isLoading: false, 197 - })); 198 - } catch (error) { 199 - set({ 200 - error: (error as ApiError).message || 'Failed to fetch messages', 201 - isLoading: false, 202 - }); 203 - } 204 - }, 205 - 206 - sendMessage: async (agentId: string, content: string) => { 207 - if (!get().isAuthenticated) return; 208 - 209 - set({ isLoading: true, error: null }); 210 - 211 - // Add user message immediately to UI 212 - const userMessage: LettaMessage = { 213 - id: `temp-${Date.now()}`, 214 - role: 'user', 215 - content, 216 - created_at: new Date().toISOString(), 217 - }; 218 - 219 - set(state => ({ 220 - messages: { 221 - ...state.messages, 222 - [agentId]: [...(state.messages[agentId] || []), userMessage], 223 - }, 224 - })); 225 - 226 - try { 227 - const request: SendMessageRequest = { 228 - messages: [{ role: 'user', content }], 229 - use_assistant_message: true, 230 - }; 231 - 232 - const response = await lettaApi.sendMessage(agentId, request); 233 - 234 - // Replace temp user message and add assistant messages 235 - set(state => { 236 - const currentMessages = state.messages[agentId] || []; 237 - const messagesWithoutTemp = currentMessages.filter( 238 - m => m.id !== userMessage.id 239 - ); 240 - 241 - return { 242 - messages: { 243 - ...state.messages, 244 - [agentId]: [...messagesWithoutTemp, ...response.messages], 245 - }, 246 - isLoading: false, 247 - }; 248 - }); 249 - } catch (error) { 250 - // Remove the temporary user message on error 251 - set(state => ({ 252 - messages: { 253 - ...state.messages, 254 - [agentId]: (state.messages[agentId] || []).filter( 255 - m => m.id !== userMessage.id 256 - ), 257 - }, 258 - error: (error as ApiError).message || 'Failed to send message', 259 - isLoading: false, 260 - })); 261 - } 262 - }, 263 - 264 - // UI Actions 265 - setLoading: (loading: boolean) => set({ isLoading: loading }), 266 - setError: (error: string | null) => set({ error }), 267 - setDrawerOpen: (open: boolean) => set({ isDrawerOpen: open }), 268 - 269 - // Reset 270 - reset: () => { 271 - lettaApi.removeAuthToken(); 272 - set({ 273 - apiToken: null, 274 - isAuthenticated: false, 275 - agents: [], 276 - currentAgentId: null, 277 - favorites: [], 278 - messages: {}, 279 - isLoading: false, 280 - isDrawerOpen: false, 281 - error: null, 282 - }); 283 - }, 284 - }), 285 - { 286 - name: 'letta-app-storage', 287 - storage: { 288 - getItem: async (name: string) => { 289 - const value = await AsyncStorage.getItem(name); 290 - return value ? JSON.parse(value) : null; 291 - }, 292 - setItem: async (name: string, value: any) => { 293 - await AsyncStorage.setItem(name, JSON.stringify(value)); 294 - }, 295 - removeItem: async (name: string) => { 296 - await AsyncStorage.removeItem(name); 297 - }, 298 - }, 299 - partialize: (state) => ({ 300 - apiToken: state.apiToken, 301 - currentAgentId: state.currentAgentId, 302 - favorites: state.favorites, 303 - }), 304 - onRehydrateStorage: () => (state) => { 305 - if (state?.apiToken) { 306 - lettaApi.setAuthToken(state.apiToken); 307 - state.isAuthenticated = true; 308 - // Don't await this, let it run in background 309 - state.fetchAgents(); 310 - } 311 - }, 312 - } 313 - ) 314 - ); 315 - 316 - export default useAppStore;