A React Native app for the ultimate thinking partner.
1/**
2 * KnowledgeView Component
3 *
4 * Knowledge management interface with three tabs:
5 * - Core Memory: View and search memory blocks
6 * - Archival Memory: Search, create, edit, delete passages
7 * - Files: Upload, list, delete files
8 *
9 * Features responsive layouts (2-column grid on desktop, single column on mobile).
10 */
11
12import React from 'react';
13import {
14 View,
15 Text,
16 TouchableOpacity,
17 TextInput,
18 FlatList,
19 ActivityIndicator,
20 StyleSheet,
21} from 'react-native';
22import { Ionicons } from '@expo/vector-icons';
23import type { Theme } from '../theme';
24import type { MemoryBlock, Passage } from '../types/letta';
25
26type KnowledgeTab = 'core' | 'archival' | 'files';
27
28interface FileItem {
29 id: string;
30 fileName?: string;
31 name?: string;
32 createdAt?: string;
33 created_at?: string;
34}
35
36interface KnowledgeViewProps {
37 theme: Theme;
38
39 // Tab state
40 knowledgeTab: KnowledgeTab;
41 onTabChange: (tab: KnowledgeTab) => void;
42
43 // Core Memory
44 memoryBlocks: MemoryBlock[];
45 memorySearchQuery: string;
46 onMemorySearchChange: (query: string) => void;
47 isLoadingBlocks: boolean;
48 blocksError: string | null;
49 onSelectBlock: (block: MemoryBlock) => void;
50
51 // Archival Memory
52 passages: Passage[];
53 passageSearchQuery: string;
54 onPassageSearchChange: (query: string) => void;
55 onPassageSearchSubmit: () => void;
56 isLoadingPassages: boolean;
57 passagesError: string | null;
58 hasMorePassages: boolean;
59 onLoadMorePassages: () => void;
60 onPassageCreate: () => void;
61 onPassageEdit: (passage: Passage) => void;
62 onPassageDelete: (id: string) => void;
63
64 // Files
65 folderFiles: FileItem[];
66 isLoadingFiles: boolean;
67 filesError: string | null;
68 isUploadingFile: boolean;
69 uploadProgress: string | null;
70 onFileUpload: () => void;
71 onFileDelete: (id: string, name: string) => void;
72
73 // Layout
74 isDesktop: boolean;
75}
76
77export function KnowledgeView(props: KnowledgeViewProps) {
78 const {
79 theme,
80 knowledgeTab,
81 onTabChange,
82 memoryBlocks,
83 memorySearchQuery,
84 onMemorySearchChange,
85 isLoadingBlocks,
86 blocksError,
87 onSelectBlock,
88 passages,
89 passageSearchQuery,
90 onPassageSearchChange,
91 onPassageSearchSubmit,
92 isLoadingPassages,
93 passagesError,
94 hasMorePassages,
95 onLoadMorePassages,
96 onPassageCreate,
97 onPassageEdit,
98 onPassageDelete,
99 folderFiles,
100 isLoadingFiles,
101 filesError,
102 isUploadingFile,
103 uploadProgress,
104 onFileUpload,
105 onFileDelete,
106 isDesktop,
107 } = props;
108
109 return (
110 <View style={[styles.container, { backgroundColor: theme.colors.background.primary }]}>
111 {/* Tab Switcher */}
112 <View
113 style={[
114 styles.tabsContainer,
115 {
116 backgroundColor: theme.colors.background.secondary,
117 borderBottomColor: theme.colors.border.primary,
118 },
119 ]}
120 >
121 <TouchableOpacity
122 style={[
123 styles.tab,
124 knowledgeTab === 'core' && {
125 borderBottomColor: theme.colors.text.primary,
126 borderBottomWidth: 2,
127 },
128 ]}
129 onPress={() => onTabChange('core')}
130 >
131 <Text
132 style={[
133 styles.tabText,
134 {
135 color:
136 knowledgeTab === 'core'
137 ? theme.colors.text.primary
138 : theme.colors.text.tertiary,
139 },
140 ]}
141 >
142 Core Memory
143 </Text>
144 </TouchableOpacity>
145
146 <TouchableOpacity
147 style={[
148 styles.tab,
149 knowledgeTab === 'archival' && {
150 borderBottomColor: theme.colors.text.primary,
151 borderBottomWidth: 2,
152 },
153 ]}
154 onPress={() => onTabChange('archival')}
155 >
156 <Text
157 style={[
158 styles.tabText,
159 {
160 color:
161 knowledgeTab === 'archival'
162 ? theme.colors.text.primary
163 : theme.colors.text.tertiary,
164 },
165 ]}
166 >
167 Archival Memory
168 </Text>
169 </TouchableOpacity>
170
171 <TouchableOpacity
172 style={[
173 styles.tab,
174 knowledgeTab === 'files' && {
175 borderBottomColor: theme.colors.text.primary,
176 borderBottomWidth: 2,
177 },
178 ]}
179 onPress={() => onTabChange('files')}
180 >
181 <Text
182 style={[
183 styles.tabText,
184 {
185 color:
186 knowledgeTab === 'files'
187 ? theme.colors.text.primary
188 : theme.colors.text.tertiary,
189 },
190 ]}
191 >
192 Files
193 </Text>
194 </TouchableOpacity>
195 </View>
196
197 {/* Search Bar for Core Memory */}
198 {knowledgeTab === 'core' && (
199 <View style={styles.searchContainer}>
200 <Ionicons
201 name="search"
202 size={20}
203 color={theme.colors.text.tertiary}
204 style={styles.searchIcon}
205 />
206 <TextInput
207 style={[
208 styles.searchInput,
209 {
210 color: theme.colors.text.primary,
211 backgroundColor: theme.colors.background.tertiary,
212 borderColor: theme.colors.border.primary,
213 },
214 ]}
215 placeholder="Search memory blocks..."
216 placeholderTextColor={theme.colors.text.tertiary}
217 value={memorySearchQuery}
218 onChangeText={onMemorySearchChange}
219 />
220 </View>
221 )}
222
223 {/* Search Bar for Archival Memory */}
224 {knowledgeTab === 'archival' && (
225 <View style={styles.searchContainer}>
226 <Ionicons
227 name="search"
228 size={20}
229 color={theme.colors.text.tertiary}
230 style={styles.searchIcon}
231 />
232 <TextInput
233 style={[
234 styles.searchInput,
235 {
236 color: theme.colors.text.primary,
237 backgroundColor: theme.colors.background.tertiary,
238 borderColor: theme.colors.border.primary,
239 paddingRight: passageSearchQuery ? 96 : 60,
240 },
241 ]}
242 placeholder="Search archival memory..."
243 placeholderTextColor={theme.colors.text.tertiary}
244 value={passageSearchQuery}
245 onChangeText={onPassageSearchChange}
246 onSubmitEditing={onPassageSearchSubmit}
247 />
248 {passageSearchQuery && (
249 <TouchableOpacity
250 style={styles.clearSearchButton}
251 onPress={() => {
252 onPassageSearchChange('');
253 onPassageSearchSubmit();
254 }}
255 >
256 <Ionicons name="close-circle" size={20} color={theme.colors.text.tertiary} />
257 </TouchableOpacity>
258 )}
259 <TouchableOpacity style={styles.createButton} onPress={onPassageCreate}>
260 <Ionicons name="add-circle-outline" size={24} color={theme.colors.text.primary} />
261 </TouchableOpacity>
262 </View>
263 )}
264
265 {/* Content Grid */}
266 <View style={styles.contentGrid}>
267 {knowledgeTab === 'files' ? (
268 // FILES TAB
269 <>
270 <View style={styles.filesHeader}>
271 <Text
272 style={[
273 styles.sectionTitle,
274 { color: theme.colors.text.secondary, marginBottom: 0 },
275 ]}
276 >
277 Uploaded Files
278 </Text>
279 <TouchableOpacity
280 onPress={onFileUpload}
281 disabled={isUploadingFile}
282 style={styles.fileUploadButton}
283 >
284 {isUploadingFile ? (
285 <ActivityIndicator size="small" color={theme.colors.text.secondary} />
286 ) : (
287 <Ionicons name="add-circle-outline" size={24} color={theme.colors.text.primary} />
288 )}
289 </TouchableOpacity>
290 </View>
291
292 {uploadProgress && (
293 <View
294 style={[
295 styles.uploadProgress,
296 { backgroundColor: theme.colors.background.tertiary },
297 ]}
298 >
299 <Text style={{ color: theme.colors.text.secondary, fontSize: 14 }}>
300 {uploadProgress}
301 </Text>
302 </View>
303 )}
304
305 {isLoadingFiles ? (
306 <View style={styles.loadingContainer}>
307 <ActivityIndicator size="large" color={theme.colors.text.secondary} />
308 </View>
309 ) : filesError ? (
310 <Text style={[styles.errorText, { textAlign: 'center', marginTop: 40 }]}>
311 {filesError}
312 </Text>
313 ) : folderFiles.length === 0 ? (
314 <View style={styles.emptyState}>
315 <Ionicons
316 name="folder-outline"
317 size={64}
318 color={theme.colors.text.tertiary}
319 style={{ opacity: 0.3 }}
320 />
321 <Text style={[styles.emptyText, { color: theme.colors.text.tertiary }]}>
322 No files uploaded yet
323 </Text>
324 </View>
325 ) : (
326 <FlatList
327 data={folderFiles}
328 keyExtractor={(item) => item.id}
329 contentContainerStyle={styles.listContent}
330 renderItem={({ item }) => (
331 <View
332 style={[
333 styles.fileCard,
334 {
335 backgroundColor: theme.colors.background.secondary,
336 borderColor: theme.colors.border.primary,
337 },
338 ]}
339 >
340 <View style={{ flex: 1 }}>
341 <Text
342 style={[styles.fileCardLabel, { color: theme.colors.text.primary }]}
343 numberOfLines={1}
344 >
345 {item.fileName || item.name || 'Untitled'}
346 </Text>
347 <Text
348 style={[
349 styles.fileCardPreview,
350 { color: theme.colors.text.secondary, fontSize: 12 },
351 ]}
352 >
353 {new Date(item.createdAt || item.created_at || '').toLocaleDateString()}
354 </Text>
355 </View>
356 <TouchableOpacity
357 onPress={() => onFileDelete(item.id, item.fileName || item.name || '')}
358 style={{ padding: 8 }}
359 >
360 <Ionicons name="trash-outline" size={20} color={theme.colors.status.error} />
361 </TouchableOpacity>
362 </View>
363 )}
364 />
365 )}
366 </>
367 ) : knowledgeTab === 'archival' ? (
368 // ARCHIVAL MEMORY TAB
369 isLoadingPassages ? (
370 <View style={styles.loadingContainer}>
371 <ActivityIndicator size="large" color={theme.colors.text.secondary} />
372 </View>
373 ) : passagesError ? (
374 <Text style={[styles.errorText, { textAlign: 'center', marginTop: 40 }]}>
375 {passagesError}
376 </Text>
377 ) : passages.length === 0 ? (
378 <View style={styles.emptyState}>
379 <Ionicons
380 name="archive-outline"
381 size={64}
382 color={theme.colors.text.tertiary}
383 style={{ opacity: 0.3 }}
384 />
385 <Text style={[styles.emptyText, { color: theme.colors.text.tertiary }]}>
386 No archival memories yet
387 </Text>
388 </View>
389 ) : (
390 <FlatList
391 data={passages}
392 keyExtractor={(item) => item.id}
393 contentContainerStyle={styles.listContent}
394 renderItem={({ item }) => (
395 <View
396 style={[
397 styles.passageCard,
398 {
399 backgroundColor: theme.colors.background.secondary,
400 borderColor: theme.colors.border.primary,
401 },
402 ]}
403 >
404 <View style={styles.passageHeader}>
405 <Text
406 style={[
407 styles.passageDate,
408 { color: theme.colors.text.tertiary, fontSize: 11, flex: 1 },
409 ]}
410 >
411 {new Date(item.created_at).toLocaleString()}
412 </Text>
413 <View style={styles.passageActions}>
414 <TouchableOpacity onPress={() => onPassageEdit(item)} style={{ padding: 4 }}>
415 <Ionicons name="create-outline" size={18} color={theme.colors.text.secondary} />
416 </TouchableOpacity>
417 <TouchableOpacity onPress={() => onPassageDelete(item.id)} style={{ padding: 4 }}>
418 <Ionicons name="trash-outline" size={18} color={theme.colors.status.error} />
419 </TouchableOpacity>
420 </View>
421 </View>
422 <Text
423 style={[styles.passageText, { color: theme.colors.text.primary }]}
424 numberOfLines={6}
425 >
426 {item.text}
427 </Text>
428 {item.tags && item.tags.length > 0 && (
429 <View style={styles.tagsContainer}>
430 {item.tags.map((tag, idx) => (
431 <View
432 key={idx}
433 style={[
434 styles.tag,
435 { backgroundColor: theme.colors.background.tertiary },
436 ]}
437 >
438 <Text style={{ color: theme.colors.text.secondary, fontSize: 11 }}>
439 {tag}
440 </Text>
441 </View>
442 ))}
443 </View>
444 )}
445 </View>
446 )}
447 ListFooterComponent={
448 hasMorePassages ? (
449 <TouchableOpacity onPress={onLoadMorePassages} style={styles.loadMoreButton}>
450 <Text style={{ color: theme.colors.text.secondary }}>Load more...</Text>
451 </TouchableOpacity>
452 ) : null
453 }
454 />
455 )
456 ) : (
457 // CORE MEMORY TAB
458 isLoadingBlocks ? (
459 <View style={styles.loadingContainer}>
460 <ActivityIndicator size="large" color={theme.colors.text.secondary} />
461 </View>
462 ) : blocksError ? (
463 <Text style={[styles.errorText, { textAlign: 'center', marginTop: 40 }]}>
464 {blocksError}
465 </Text>
466 ) : (
467 <FlatList
468 data={memoryBlocks.filter((block) => {
469 if (memorySearchQuery) {
470 return (
471 block.label.toLowerCase().includes(memorySearchQuery.toLowerCase()) ||
472 block.value.toLowerCase().includes(memorySearchQuery.toLowerCase())
473 );
474 }
475 return true;
476 })}
477 numColumns={isDesktop ? 2 : 1}
478 key={isDesktop ? 'desktop' : 'mobile'}
479 keyExtractor={(item) => item.id || item.label}
480 contentContainerStyle={styles.listContent}
481 renderItem={({ item }) => (
482 <TouchableOpacity
483 style={[
484 styles.memoryCard,
485 {
486 backgroundColor: theme.colors.background.secondary,
487 borderColor: theme.colors.border.primary,
488 },
489 ]}
490 onPress={() => onSelectBlock(item)}
491 >
492 <View style={styles.memoryCardHeader}>
493 <Text style={[styles.memoryCardLabel, { color: theme.colors.text.primary }]}>
494 {item.label}
495 </Text>
496 <Text style={[styles.memoryCardCount, { color: theme.colors.text.tertiary }]}>
497 {item.value.length} chars
498 </Text>
499 </View>
500 <Text
501 style={[styles.memoryCardPreview, { color: theme.colors.text.secondary }]}
502 numberOfLines={4}
503 >
504 {item.value || 'Empty'}
505 </Text>
506 </TouchableOpacity>
507 )}
508 ListEmptyComponent={
509 <View style={styles.emptyState}>
510 <Ionicons
511 name="library-outline"
512 size={64}
513 color={theme.colors.text.tertiary}
514 style={{ opacity: 0.3 }}
515 />
516 <Text style={[styles.emptyText, { color: theme.colors.text.secondary }]}>
517 {memorySearchQuery ? 'No memory blocks found' : 'No memory blocks yet'}
518 </Text>
519 </View>
520 }
521 />
522 )
523 )}
524 </View>
525 </View>
526 );
527}
528
529const styles = StyleSheet.create({
530 container: {
531 flex: 1,
532 },
533 tabsContainer: {
534 flexDirection: 'row',
535 justifyContent: 'space-between',
536 alignItems: 'center',
537 paddingHorizontal: 16,
538 paddingVertical: 4,
539 borderBottomWidth: 1,
540 },
541 tab: {
542 paddingVertical: 12,
543 paddingHorizontal: 16,
544 flex: 1,
545 alignItems: 'center',
546 },
547 tabText: {
548 fontSize: 14,
549 fontFamily: 'Lexend_500Medium',
550 },
551 searchContainer: {
552 flexDirection: 'row',
553 alignItems: 'center',
554 paddingHorizontal: 16,
555 paddingVertical: 12,
556 },
557 searchIcon: {
558 position: 'absolute',
559 left: 24,
560 zIndex: 1,
561 },
562 searchInput: {
563 flex: 1,
564 height: 40,
565 borderRadius: 20,
566 paddingLeft: 40,
567 paddingRight: 16,
568 fontSize: 14,
569 borderWidth: 1,
570 fontFamily: 'Lexend_400Regular',
571 },
572 clearSearchButton: {
573 position: 'absolute',
574 right: 64,
575 padding: 8,
576 },
577 createButton: {
578 position: 'absolute',
579 right: 28,
580 padding: 8,
581 },
582 contentGrid: {
583 flex: 1,
584 },
585 filesHeader: {
586 flexDirection: 'row',
587 justifyContent: 'space-between',
588 alignItems: 'center',
589 paddingHorizontal: 8,
590 paddingVertical: 12,
591 },
592 sectionTitle: {
593 fontSize: 14,
594 fontFamily: 'Lexend_600SemiBold',
595 textTransform: 'uppercase',
596 letterSpacing: 0.5,
597 },
598 fileUploadButton: {
599 padding: 4,
600 },
601 uploadProgress: {
602 marginHorizontal: 8,
603 marginBottom: 12,
604 paddingVertical: 8,
605 paddingHorizontal: 12,
606 borderRadius: 8,
607 },
608 loadingContainer: {
609 flex: 1,
610 justifyContent: 'center',
611 alignItems: 'center',
612 },
613 emptyState: {
614 flex: 1,
615 justifyContent: 'center',
616 alignItems: 'center',
617 paddingTop: 80,
618 },
619 emptyText: {
620 fontSize: 16,
621 fontFamily: 'Lexend_400Regular',
622 marginTop: 16,
623 },
624 errorText: {
625 fontSize: 14,
626 fontFamily: 'Lexend_400Regular',
627 color: '#E07042',
628 },
629 listContent: {
630 padding: 8,
631 },
632 fileCard: {
633 flexDirection: 'row',
634 justifyContent: 'space-between',
635 alignItems: 'center',
636 padding: 16,
637 borderRadius: 12,
638 borderWidth: 1,
639 marginBottom: 8,
640 },
641 fileCardLabel: {
642 fontSize: 16,
643 fontFamily: 'Lexend_500Medium',
644 },
645 fileCardPreview: {
646 fontSize: 12,
647 fontFamily: 'Lexend_400Regular',
648 marginTop: 4,
649 },
650 passageCard: {
651 padding: 16,
652 borderRadius: 12,
653 borderWidth: 1,
654 marginBottom: 12,
655 },
656 passageHeader: {
657 flexDirection: 'row',
658 justifyContent: 'space-between',
659 alignItems: 'flex-start',
660 marginBottom: 8,
661 },
662 passageDate: {
663 fontFamily: 'Lexend_400Regular',
664 },
665 passageActions: {
666 flexDirection: 'row',
667 gap: 8,
668 },
669 passageText: {
670 fontSize: 14,
671 fontFamily: 'Lexend_400Regular',
672 lineHeight: 20,
673 },
674 tagsContainer: {
675 flexDirection: 'row',
676 flexWrap: 'wrap',
677 gap: 6,
678 marginTop: 8,
679 },
680 tag: {
681 paddingHorizontal: 8,
682 paddingVertical: 4,
683 borderRadius: 4,
684 },
685 memoryCard: {
686 flex: 1,
687 padding: 16,
688 borderRadius: 12,
689 borderWidth: 1,
690 margin: 4,
691 minHeight: 120,
692 },
693 memoryCardHeader: {
694 flexDirection: 'row',
695 justifyContent: 'space-between',
696 alignItems: 'center',
697 marginBottom: 8,
698 },
699 memoryCardLabel: {
700 fontSize: 16,
701 fontFamily: 'Lexend_600SemiBold',
702 flex: 1,
703 },
704 memoryCardCount: {
705 fontSize: 11,
706 fontFamily: 'Lexend_400Regular',
707 marginLeft: 8,
708 },
709 memoryCardPreview: {
710 fontSize: 13,
711 fontFamily: 'Lexend_400Regular',
712 lineHeight: 18,
713 },
714 loadMoreButton: {
715 padding: 16,
716 alignItems: 'center',
717 },
718});
719
720export default KnowledgeView;