Files
personal-ai-assistant/frontend/src/components/memory/memory-list.tsx
dolgolyov.alexei 8b8fe916f0 Phase 4: Documents & Memory — upload, FTS, AI tools, context injection
Backend:
- Document + MemoryEntry models with Alembic migration (GIN FTS index)
- File upload endpoint with path traversal protection (sanitized filenames)
- Background document text extraction (PyMuPDF)
- Full-text search on extracted_text via PostgreSQL tsvector/tsquery
- Memory CRUD with enum-validated categories/importance, field allow-list
- AI tools: save_memory, search_documents, get_memory (Claude function calling)
- Tool execution loop in stream_ai_response (multi-turn tool use)
- Context assembly: injects critical memory + relevant doc excerpts
- File storage abstraction (local filesystem, S3-swappable)
- Secure file deletion (DB flush before disk delete)

Frontend:
- Document upload dialog (drag-and-drop + file picker)
- Document list with status badges, search, download (via authenticated blob)
- Document viewer with extracted text preview
- Memory list grouped by category with importance color coding
- Memory editor with category/importance dropdowns
- Documents + Memory pages with full CRUD
- Enabled sidebar navigation for both sections

Review fixes applied:
- Sanitized upload filenames (path traversal prevention)
- Download via axios blob (not bare <a href>, preserves auth)
- Route ordering: /search before /{id}/reindex
- Memory update allows is_active=False + field allow-list
- MemoryEditor form resets on mode switch
- Literal enum validation on category/importance schemas
- DB flush before file deletion for data integrity

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 13:46:59 +03:00

67 lines
2.7 KiB
TypeScript

import { useTranslation } from "react-i18next";
import { Pencil, Trash2 } from "lucide-react";
import type { MemoryEntry } from "@/api/memory";
import { cn } from "@/lib/utils";
interface MemoryListProps {
entries: MemoryEntry[];
onEdit: (entry: MemoryEntry) => void;
onDelete: (entryId: string) => void;
}
const importanceColors: Record<string, string> = {
critical: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400",
high: "bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-400",
medium: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400",
low: "bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400",
};
export function MemoryList({ entries, onEdit, onDelete }: MemoryListProps) {
const { t } = useTranslation();
if (entries.length === 0) {
return <p className="text-center text-muted-foreground py-8">{t("memory.no_entries")}</p>;
}
// Group by category
const grouped = entries.reduce<Record<string, MemoryEntry[]>>((acc, entry) => {
(acc[entry.category] ??= []).push(entry);
return acc;
}, {});
return (
<div className="space-y-6">
{Object.entries(grouped).map(([category, items]) => (
<div key={category}>
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-2">
{t(`memory.categories.${category}`)}
</h3>
<div className="space-y-2">
{items.map((entry) => (
<div key={entry.id} className={cn("flex items-start gap-3 rounded-lg border bg-card p-4", !entry.is_active && "opacity-50")}>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<p className="font-medium">{entry.title}</p>
<span className={cn("rounded-full px-2 py-0.5 text-xs font-medium", importanceColors[entry.importance])}>
{t(`memory.importance_levels.${entry.importance}`)}
</span>
</div>
<p className="text-sm text-muted-foreground">{entry.content}</p>
</div>
<div className="flex gap-1 shrink-0">
<button onClick={() => onEdit(entry)} className="rounded p-2 hover:bg-accent transition-colors">
<Pencil className="h-4 w-4" />
</button>
<button onClick={() => onDelete(entry.id)} className="rounded p-2 hover:bg-destructive/10 hover:text-destructive transition-colors">
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
))}
</div>
</div>
))}
</div>
);
}