A React Native app for the ultimate thinking partner.
at bf2a37bf805fa5ccc8b5badeeb098f842186e615 781 lines 23 kB view raw
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;