| |
| |
| |
|
|
| import { useState } from 'react'; |
| import type { ScoredMemory } from '../api/client'; |
| import TypeBadge from './TypeBadge'; |
| import ScoreBar from './ScoreBar'; |
|
|
| interface MemoryCardProps { |
| scored: ScoredMemory; |
| onForget?: (memoryId: string) => void; |
| } |
|
|
| export default function MemoryCard({ scored, onForget }: MemoryCardProps) { |
| const [expanded, setExpanded] = useState(false); |
| const { memory, score, cosine_similarity } = scored; |
|
|
| const timeAgo = formatTimeAgo(memory.created_at); |
|
|
| return ( |
| <div |
| className="glass-card animate-fade-in-up" |
| style={{ |
| padding: '16px', |
| cursor: 'pointer', |
| transition: 'all 0.3s ease', |
| }} |
| onClick={() => setExpanded(!expanded)} |
| > |
| {/* Header */} |
| <div |
| style={{ |
| display: 'flex', |
| alignItems: 'center', |
| justifyContent: 'space-between', |
| marginBottom: '8px', |
| }} |
| > |
| <TypeBadge type={memory.memory_type} size="sm" /> |
| <span style={{ fontSize: '12px', color: 'var(--color-text-muted)' }}> |
| {timeAgo} |
| </span> |
| </div> |
| |
| {/* Content */} |
| <p |
| style={{ |
| fontSize: '14px', |
| color: 'var(--color-text-primary)', |
| lineHeight: 1.6, |
| marginBottom: '12px', |
| overflow: 'hidden', |
| textOverflow: 'ellipsis', |
| display: '-webkit-box', |
| WebkitLineClamp: expanded ? 999 : 3, |
| WebkitBoxOrient: 'vertical', |
| }} |
| > |
| {memory.content} |
| </p> |
| |
| {/* Score bars */} |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}> |
| <ScoreBar score={score} label="Score" color="#6366f1" /> |
| <ScoreBar score={cosine_similarity} label="Cosine" color="#22c55e" /> |
| <ScoreBar score={memory.importance} label="Import." color="#f59e0b" /> |
| </div> |
| |
| {/* Expanded metadata */} |
| {expanded && ( |
| <div |
| style={{ |
| marginTop: '12px', |
| padding: '12px', |
| borderRadius: '8px', |
| background: 'var(--color-surface)', |
| border: '1px solid var(--color-border)', |
| fontSize: '12px', |
| fontFamily: 'monospace', |
| }} |
| > |
| <pre |
| style={{ |
| color: 'var(--color-text-secondary)', |
| whiteSpace: 'pre-wrap', |
| wordBreak: 'break-all', |
| }} |
| > |
| {JSON.stringify(memory.metadata, null, 2)} |
| </pre> |
| <div |
| style={{ |
| marginTop: '8px', |
| display: 'flex', |
| gap: '12px', |
| color: 'var(--color-text-muted)', |
| }} |
| > |
| <span>Accesses: {memory.access_count}</span> |
| <span>Recency: {memory.recency_score.toFixed(3)}</span> |
| <span>ID: {memory.id.slice(0, 8)}…</span> |
| </div> |
| {onForget && ( |
| <button |
| onClick={(e) => { |
| e.stopPropagation(); |
| onForget(memory.id); |
| }} |
| style={{ |
| marginTop: '8px', |
| padding: '6px 16px', |
| borderRadius: '8px', |
| border: '1px solid var(--color-danger)', |
| background: 'transparent', |
| color: 'var(--color-danger)', |
| fontSize: '12px', |
| fontWeight: 600, |
| cursor: 'pointer', |
| transition: 'all 0.2s', |
| }} |
| onMouseEnter={(e) => { |
| e.currentTarget.style.background = 'rgba(239, 68, 68, 0.1)'; |
| }} |
| onMouseLeave={(e) => { |
| e.currentTarget.style.background = 'transparent'; |
| }} |
| > |
| 🗑 Forget Memory |
| </button> |
| )} |
| </div> |
| )} |
| </div> |
| ); |
| } |
|
|
| function formatTimeAgo(dateStr: string): string { |
| const date = new Date(dateStr); |
| const now = new Date(); |
| const diffMs = now.getTime() - date.getTime(); |
| const diffMin = Math.floor(diffMs / 60000); |
| if (diffMin < 1) return 'just now'; |
| if (diffMin < 60) return `${diffMin}m ago`; |
| const diffHr = Math.floor(diffMin / 60); |
| if (diffHr < 24) return `${diffHr}h ago`; |
| const diffDays = Math.floor(diffHr / 24); |
| return `${diffDays}d ago`; |
| } |
|
|