A React Native app for the ultimate thinking partner.
at sdk-v1-upgrade 720 lines 21 kB view raw
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;