A React Native app for the ultimate thinking partner.
1import React, { useRef, useEffect } from 'react';
2import {
3 View,
4 Text,
5 StyleSheet,
6 TouchableOpacity,
7 Animated,
8 ScrollView,
9 Dimensions,
10} from 'react-native';
11import { Ionicons } from '@expo/vector-icons';
12import MessageContent from './MessageContent';
13import { darkTheme, lightTheme } from '../theme';
14import type { MemoryBlock } from '../types/letta';
15
16interface MemoryBlockViewerProps {
17 block: MemoryBlock | null;
18 onClose: () => void;
19 isDark?: boolean;
20 isDesktop: boolean;
21}
22
23const MemoryBlockViewer: React.FC<MemoryBlockViewerProps> = ({
24 block,
25 onClose,
26 isDark = true,
27 isDesktop,
28}) => {
29 const theme = isDark ? darkTheme : lightTheme;
30 const slideAnim = useRef(new Animated.Value(0)).current;
31 const fadeAnim = useRef(new Animated.Value(0)).current;
32
33 useEffect(() => {
34 if (block) {
35 Animated.parallel([
36 Animated.timing(slideAnim, {
37 toValue: 1,
38 duration: 300,
39 useNativeDriver: false,
40 }),
41 Animated.timing(fadeAnim, {
42 toValue: 1,
43 duration: 250,
44 useNativeDriver: true,
45 }),
46 ]).start();
47 } else {
48 Animated.parallel([
49 Animated.timing(slideAnim, {
50 toValue: 0,
51 duration: 250,
52 useNativeDriver: false,
53 }),
54 Animated.timing(fadeAnim, {
55 toValue: 0,
56 duration: 200,
57 useNativeDriver: true,
58 }),
59 ]).start();
60 }
61 }, [block]);
62
63 if (!block) return null;
64
65 if (isDesktop) {
66 // Desktop: Right pane
67 const panelWidth = slideAnim.interpolate({
68 inputRange: [0, 1],
69 outputRange: [0, 440],
70 });
71
72 return (
73 <Animated.View
74 style={[
75 styles.desktopPane,
76 {
77 width: panelWidth,
78 backgroundColor: theme.colors.background.primary,
79 borderLeftColor: theme.colors.border.primary,
80 },
81 ]}
82 >
83 <View style={[styles.desktopHeader, { borderBottomColor: theme.colors.border.primary }]}>
84 <View style={styles.headerLeft}>
85 <Ionicons name="cube-outline" size={20} color={theme.colors.text.tertiary} />
86 <Text style={[styles.headerLabel, { color: theme.colors.text.tertiary }]}>
87 KNOWLEDGE
88 </Text>
89 </View>
90 <TouchableOpacity onPress={onClose} style={styles.closeButton}>
91 <Ionicons name="close" size={24} color={theme.colors.text.primary} />
92 </TouchableOpacity>
93 </View>
94
95 <ScrollView
96 style={styles.scrollContent}
97 contentContainerStyle={styles.scrollContentContainer}
98 showsVerticalScrollIndicator={true}
99 >
100 <View style={styles.blockContent}>
101 <Text style={[styles.blockTitle, { color: theme.colors.text.primary }]}>
102 {block.label}
103 </Text>
104 {block.description && (
105 <Text style={[styles.blockDescription, { color: theme.colors.text.secondary }]}>
106 {block.description}
107 </Text>
108 )}
109 <View style={[styles.divider, { backgroundColor: theme.colors.border.primary }]} />
110 <MessageContent content={block.value} isUser={false} isDark={isDark} />
111 </View>
112 </ScrollView>
113 </Animated.View>
114 );
115 } else {
116 // Mobile: Full screen overlay
117 return (
118 <Animated.View
119 style={[
120 styles.mobileOverlay,
121 {
122 opacity: fadeAnim,
123 },
124 ]}
125 >
126 <TouchableOpacity
127 style={styles.backdrop}
128 activeOpacity={1}
129 onPress={onClose}
130 />
131 <Animated.View
132 style={[
133 styles.mobilePanel,
134 {
135 backgroundColor: theme.colors.background.primary,
136 transform: [
137 {
138 translateY: slideAnim.interpolate({
139 inputRange: [0, 1],
140 outputRange: [Dimensions.get('window').height, 0],
141 }),
142 },
143 ],
144 },
145 ]}
146 >
147 <View style={[styles.mobileHeader, { borderBottomColor: theme.colors.border.primary }]}>
148 <View style={styles.headerLeft}>
149 <Ionicons name="cube-outline" size={20} color={theme.colors.text.tertiary} />
150 <Text style={[styles.headerLabel, { color: theme.colors.text.tertiary }]}>
151 KNOWLEDGE
152 </Text>
153 </View>
154 <TouchableOpacity onPress={onClose} style={styles.closeButton}>
155 <Ionicons name="close" size={24} color={theme.colors.text.primary} />
156 </TouchableOpacity>
157 </View>
158
159 <ScrollView
160 style={styles.scrollContent}
161 contentContainerStyle={styles.scrollContentContainer}
162 showsVerticalScrollIndicator={true}
163 >
164 <View style={styles.blockContent}>
165 <Text style={[styles.blockTitle, { color: theme.colors.text.primary }]}>
166 {block.label}
167 </Text>
168 {block.description && (
169 <Text style={[styles.blockDescription, { color: theme.colors.text.secondary }]}>
170 {block.description}
171 </Text>
172 )}
173 <View style={[styles.divider, { backgroundColor: theme.colors.border.primary }]} />
174 <MessageContent content={block.value} isUser={false} isDark={isDark} />
175 </View>
176 </ScrollView>
177 </Animated.View>
178 </Animated.View>
179 );
180 }
181};
182
183const styles = StyleSheet.create({
184 // Desktop styles
185 desktopPane: {
186 borderLeftWidth: 1,
187 overflow: 'hidden',
188 },
189 desktopHeader: {
190 flexDirection: 'row',
191 justifyContent: 'space-between',
192 alignItems: 'center',
193 paddingHorizontal: 20,
194 paddingVertical: 16,
195 borderBottomWidth: 1,
196 },
197
198 // Mobile styles
199 mobileOverlay: {
200 position: 'absolute',
201 top: 0,
202 left: 0,
203 right: 0,
204 bottom: 0,
205 zIndex: 1000,
206 },
207 backdrop: {
208 position: 'absolute',
209 top: 0,
210 left: 0,
211 right: 0,
212 bottom: 0,
213 backgroundColor: 'rgba(0, 0, 0, 0.6)',
214 },
215 mobilePanel: {
216 position: 'absolute',
217 top: 60,
218 left: 0,
219 right: 0,
220 bottom: 0,
221 borderTopLeftRadius: 20,
222 borderTopRightRadius: 20,
223 boxShadow: '0 -2px 10px rgba(0, 0, 0, 0.25)',
224 elevation: 10,
225 },
226 mobileHeader: {
227 flexDirection: 'row',
228 justifyContent: 'space-between',
229 alignItems: 'center',
230 paddingHorizontal: 20,
231 paddingTop: 20,
232 paddingBottom: 16,
233 borderBottomWidth: 1,
234 },
235
236 // Shared styles
237 headerLeft: {
238 flexDirection: 'row',
239 alignItems: 'center',
240 gap: 8,
241 },
242 headerLabel: {
243 fontSize: 12,
244 fontFamily: 'Lexend_600SemiBold',
245 letterSpacing: 1.2,
246 },
247 closeButton: {
248 padding: 4,
249 },
250 scrollContent: {
251 flex: 1,
252 },
253 scrollContentContainer: {
254 padding: 24,
255 },
256 blockContent: {
257 flex: 1,
258 },
259 blockTitle: {
260 fontSize: 24,
261 fontFamily: 'Lexend_700Bold',
262 marginBottom: 8,
263 },
264 blockDescription: {
265 fontSize: 14,
266 fontFamily: 'Lexend_400Regular',
267 marginBottom: 16,
268 lineHeight: 20,
269 },
270 divider: {
271 height: 1,
272 backgroundColor: 'rgba(255, 255, 255, 0.1)',
273 marginVertical: 20,
274 },
275});
276
277export default MemoryBlockViewer;