| |
| |
| |
|
|
| import { useState } from 'react'; |
| import { searchMemories, deleteMemory } from '../api/client'; |
| import type { ScoredMemory } from '../api/client'; |
| import MemoryCard from '../components/MemoryCard'; |
|
|
| export default function MemoryExplorer() { |
| const [userId, setUserId] = useState(''); |
| const [query, setQuery] = useState(''); |
| const [typeFilter, setTypeFilter] = useState<string>(''); |
| const [sortBy, setSortBy] = useState<'score' | 'importance' | 'recency'>('score'); |
| const [results, setResults] = useState<ScoredMemory[]>([]); |
| const [loading, setLoading] = useState(false); |
| const [searched, setSearched] = useState(false); |
|
|
| const handleSearch = async () => { |
| if (!userId || !query) return; |
| setLoading(true); |
| try { |
| const data = await searchMemories(userId, query, typeFilter || undefined, 20); |
| let sorted = [...data]; |
| if (sortBy === 'importance') { |
| sorted.sort((a, b) => b.memory.importance - a.memory.importance); |
| } else if (sortBy === 'recency') { |
| sorted.sort((a, b) => b.memory.recency_score - a.memory.recency_score); |
| } |
| setResults(sorted); |
| setSearched(true); |
| } catch { |
| setResults([]); |
| } finally { |
| setLoading(false); |
| } |
| }; |
|
|
| const handleForget = async (memoryId: string) => { |
| try { |
| await deleteMemory(memoryId); |
| setResults((prev) => prev.filter((r) => r.memory.id !== memoryId)); |
| } catch (e) { |
| console.error('Failed to forget memory:', e); |
| } |
| }; |
|
|
| return ( |
| <div style={{ padding: '32px', maxWidth: '900px', margin: '0 auto' }}> |
| <h1 style={{ fontSize: '28px', fontWeight: 700, marginBottom: '8px', color: 'var(--color-text-primary)' }}> |
| Memory Explorer |
| </h1> |
| <p style={{ color: 'var(--color-text-muted)', marginBottom: '32px', fontSize: '14px' }}> |
| Search, filter, and manage memories |
| </p> |
| |
| {/* Search Controls */} |
| <div className="glass-card" style={{ padding: '20px', marginBottom: '24px' }}> |
| <div style={{ display: 'flex', gap: '12px', marginBottom: '12px' }}> |
| <input |
| type="text" |
| placeholder="User ID (UUID)" |
| value={userId} |
| onChange={(e) => setUserId(e.target.value)} |
| style={{ |
| flex: 1, |
| padding: '10px 16px', |
| borderRadius: '10px', |
| border: '1px solid var(--color-border)', |
| background: 'var(--color-surface)', |
| color: 'var(--color-text-primary)', |
| fontSize: '14px', |
| outline: 'none', |
| }} |
| /> |
| </div> |
| <div style={{ display: 'flex', gap: '12px' }}> |
| <input |
| type="text" |
| placeholder="Search query..." |
| value={query} |
| onChange={(e) => setQuery(e.target.value)} |
| onKeyDown={(e) => e.key === 'Enter' && handleSearch()} |
| style={{ |
| flex: 1, |
| padding: '10px 16px', |
| borderRadius: '10px', |
| border: '1px solid var(--color-border)', |
| background: 'var(--color-surface)', |
| color: 'var(--color-text-primary)', |
| fontSize: '14px', |
| outline: 'none', |
| }} |
| /> |
| <select |
| value={typeFilter} |
| onChange={(e) => setTypeFilter(e.target.value)} |
| style={{ |
| padding: '10px 16px', |
| borderRadius: '10px', |
| border: '1px solid var(--color-border)', |
| background: 'var(--color-surface)', |
| color: 'var(--color-text-primary)', |
| fontSize: '14px', |
| outline: 'none', |
| cursor: 'pointer', |
| }} |
| > |
| <option value="">All Types</option> |
| <option value="episodic">Episodic</option> |
| <option value="semantic">Semantic</option> |
| <option value="procedural">Procedural</option> |
| </select> |
| <select |
| value={sortBy} |
| onChange={(e) => setSortBy(e.target.value as typeof sortBy)} |
| style={{ |
| padding: '10px 16px', |
| borderRadius: '10px', |
| border: '1px solid var(--color-border)', |
| background: 'var(--color-surface)', |
| color: 'var(--color-text-primary)', |
| fontSize: '14px', |
| outline: 'none', |
| cursor: 'pointer', |
| }} |
| > |
| <option value="score">Sort: Score</option> |
| <option value="importance">Sort: Importance</option> |
| <option value="recency">Sort: Recency</option> |
| </select> |
| <button |
| onClick={handleSearch} |
| disabled={loading} |
| style={{ |
| padding: '10px 24px', |
| borderRadius: '10px', |
| border: 'none', |
| background: 'var(--color-accent)', |
| color: '#fff', |
| fontSize: '14px', |
| fontWeight: 600, |
| cursor: loading ? 'not-allowed' : 'pointer', |
| opacity: loading ? 0.7 : 1, |
| transition: 'all 0.2s', |
| }} |
| > |
| {loading ? 'Searching...' : 'Search'} |
| </button> |
| </div> |
| </div> |
| |
| {/* Results */} |
| {searched && results.length === 0 && ( |
| <div style={{ textAlign: 'center', padding: '40px', color: 'var(--color-text-muted)' }}> |
| No memories found. Try a different query. |
| </div> |
| )} |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}> |
| {results.map((scored) => ( |
| <MemoryCard key={scored.memory.id} scored={scored} onForget={handleForget} /> |
| ))} |
| </div> |
| </div> |
| ); |
| } |
|
|