'use client' import React, { useState, useEffect, useCallback } from 'react' import { useMissionControl } from '@/store' import { createClientLogger } from '@/lib/client-logger' const log = createClientLogger('MemoryBrowser') interface MemoryFile { path: string name: string type: 'file' | 'directory' size?: number modified?: number children?: MemoryFile[] } export function MemoryBrowserPanel() { const { memoryFiles, selectedMemoryFile, memoryContent, dashboardMode, setMemoryFiles, setSelectedMemoryFile, setMemoryContent } = useMissionControl() const isLocal = dashboardMode === 'local' const [isLoading, setIsLoading] = useState(false) const [expandedFolders, setExpandedFolders] = useState>(new Set()) const [searchResults, setSearchResults] = useState([]) const [searchQuery, setSearchQuery] = useState('') const [isSearching, setIsSearching] = useState(false) const [isEditing, setIsEditing] = useState(false) const [editedContent, setEditedContent] = useState('') const [showCreateModal, setShowCreateModal] = useState(false) const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) const [isSaving, setIsSaving] = useState(false) const [activeTab, setActiveTab] = useState<'daily' | 'knowledge' | 'all'>('all') const loadFileTree = useCallback(async () => { setIsLoading(true) try { const response = await fetch('/api/memory?action=tree') const data = await response.json() setMemoryFiles(data.tree || []) // Auto-expand some common directories setExpandedFolders(new Set(['daily', 'knowledge'])) } catch (error) { log.error('Failed to load file tree:', error) } finally { setIsLoading(false) } }, [setMemoryFiles]) useEffect(() => { loadFileTree() }, [loadFileTree]) const getFilteredFiles = () => { if (activeTab === 'all') return memoryFiles return memoryFiles.filter(file => { if (activeTab === 'daily') { return file.name === 'daily' || file.path.includes('daily/') } if (activeTab === 'knowledge') { return file.name === 'knowledge' || file.path.includes('knowledge/') } return true }) } const loadFileContent = async (filePath: string) => { setIsLoading(true) try { const response = await fetch(`/api/memory?action=content&path=${encodeURIComponent(filePath)}`) const data = await response.json() if (data.content !== undefined) { setSelectedMemoryFile(filePath) setMemoryContent(data.content) } else { alert(data.error || 'Failed to load file content') } } catch (error) { log.error('Failed to load file content:', error) alert('Network error occurred') } finally { setIsLoading(false) } } const searchFiles = async () => { if (!searchQuery.trim()) return setIsSearching(true) try { const response = await fetch(`/api/memory?action=search&query=${encodeURIComponent(searchQuery)}`) const data = await response.json() setSearchResults(data.results || []) } catch (error) { log.error('Search failed:', error) setSearchResults([]) } finally { setIsSearching(false) } } const toggleFolder = (folderPath: string) => { const newExpanded = new Set(expandedFolders) if (newExpanded.has(folderPath)) { newExpanded.delete(folderPath) } else { newExpanded.add(folderPath) } setExpandedFolders(newExpanded) } const formatFileSize = (bytes: number) => { if (bytes === 0) return '0 B' const k = 1024 const sizes = ['B', 'KB', 'MB', 'GB'] const i = Math.floor(Math.log(bytes) / Math.log(k)) return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i] } const formatDate = (timestamp: number) => { return new Date(timestamp).toLocaleString() } // Enhanced editing functionality const startEditing = () => { setIsEditing(true) setEditedContent(memoryContent ?? '') } const cancelEditing = () => { setIsEditing(false) setEditedContent('') } const saveFile = async () => { if (!selectedMemoryFile) return setIsSaving(true) try { const response = await fetch(`/api/memory`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'save', path: selectedMemoryFile, content: editedContent }) }) const data = await response.json() if (data.success) { setMemoryContent(editedContent) setIsEditing(false) setEditedContent('') // Refresh file tree to update file sizes loadFileTree() } else { alert(data.error || 'Failed to save file') } } catch (error) { log.error('Failed to save file:', error) alert('Network error occurred') } finally { setIsSaving(false) } } const createNewFile = async (filePath: string, content: string = '') => { try { const response = await fetch(`/api/memory`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'create', path: filePath, content }) }) const data = await response.json() if (data.success) { loadFileTree() loadFileContent(filePath) } else { alert(data.error || 'Failed to create file') } } catch (error) { log.error('Failed to create file:', error) alert('Network error occurred') } } const deleteFile = async () => { if (!selectedMemoryFile) return try { const response = await fetch(`/api/memory`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'delete', path: selectedMemoryFile }) }) const data = await response.json() if (data.success) { setSelectedMemoryFile('') setMemoryContent('') setShowDeleteConfirm(false) loadFileTree() } else { alert(data.error || 'Failed to delete file') } } catch (error) { log.error('Failed to delete file:', error) alert('Network error occurred') } } const renderFileTree = (files: MemoryFile[], level = 0): React.ReactElement[] => { return files.map((file) => (
{file.type === 'directory' ? (
toggleFolder(file.path)} > {expandedFolders.has(file.path) ? '📂' : '📁'} {file.name} ({file.children?.length || 0} items)
{expandedFolders.has(file.path) && file.children && (
{renderFileTree(file.children, level + 1)}
)}
) : (
loadFileContent(file.path)} > {file.name.endsWith('.md') ? '📄' : file.name.endsWith('.txt') ? '📝' : file.name.endsWith('.json') ? '📋' : '📄'} {file.name}
{file.size && {formatFileSize(file.size)}} {file.modified && {new Date(file.modified).toLocaleDateString()}}
)}
)) } const renderMarkdown = (content: string) => { // Improved markdown rendering with proper line handling const lines = content.split('\n') const elements: React.ReactElement[] = [] let inList = false let seenHeaders = new Set() for (let i = 0; i < lines.length; i++) { const line = lines[i] const trimmedLine = line.trim() if (trimmedLine.startsWith('# ')) { const headerText = trimmedLine.slice(2) const headerId = `h1-${headerText.toLowerCase().replace(/\s+/g, '-')}` // Skip duplicate headers if (seenHeaders.has(headerId)) continue seenHeaders.add(headerId) if (inList) inList = false elements.push(

{headerText}

) } else if (trimmedLine.startsWith('## ')) { const headerText = trimmedLine.slice(3) const headerId = `h2-${headerText.toLowerCase().replace(/\s+/g, '-')}` // Skip duplicate headers if (seenHeaders.has(headerId)) continue seenHeaders.add(headerId) if (inList) inList = false elements.push(

{headerText}

) } else if (trimmedLine.startsWith('### ')) { const headerText = trimmedLine.slice(4) const headerId = `h3-${headerText.toLowerCase().replace(/\s+/g, '-')}` // Skip duplicate headers if (seenHeaders.has(headerId)) continue seenHeaders.add(headerId) if (inList) inList = false elements.push(

{headerText}

) } else if (trimmedLine.startsWith('- ')) { if (inList) inList = false elements.push(
  • {trimmedLine.slice(2)}
  • ) } else if (trimmedLine.startsWith('**') && trimmedLine.endsWith('**') && trimmedLine.length > 4) { if (inList) inList = false elements.push(

    {trimmedLine.slice(2, -2)}

    ) } else if (trimmedLine === '') { if (inList) inList = false elements.push(
    ) } else if (trimmedLine.length > 0) { if (inList) inList = false // Handle inline formatting — escape HTML entities first to prevent XSS let content = trimmedLine .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') // Simple bold formatting content = content.replace(/\*\*(.*?)\*\*/g, '$1') // Simple italic formatting content = content.replace(/\*(.*?)\*/g, '$1') elements.push(

    ) } } return elements } return (

    Memory Browser

    {isLocal ? 'Browse and manage local knowledge files and memory' : 'Explore knowledge files and memory structure'}

    This page shows all workspace memory files. The agent profile Memory tab only edits that single agent's working memory.

    {/* Tab Navigation */}
    {/* Search Bar */}
    setSearchQuery(e.target.value)} onKeyPress={(e) => e.key === 'Enter' && searchFiles()} placeholder="Search in memory files..." className="w-full px-3 py-2 border border-border rounded-md bg-background text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50" />
    {/* Search Results */} {searchResults.length > 0 && (

    Search Results ({searchResults.length})

    {searchResults.map((result, index) => (
    loadFileContent(result.path)} >
    {result.name} ({result.path})
    {result.matches} matches
    ))}
    )}
    {/* File Tree */}

    Memory Structure

    {isLoading ? (
    Loading...
    ) : (
    {getFilteredFiles().length === 0 ? (
    {activeTab === 'all' ? 'No memory files found' : activeTab === 'daily' ? 'No daily logs found' : 'No knowledge files found'}
    ) : ( renderFileTree(getFilteredFiles()) )}
    )}
    {/* File Content */}

    {selectedMemoryFile || 'File Content'}

    {selectedMemoryFile && ( <> {!isEditing ? ( <> ) : ( <> )} )}
    {isLoading ? (
    Loading file...
    ) : memoryContent !== null ? (
    {isEditing ? (