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