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>
67 lines
2.7 KiB
TypeScript
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>
|
|
);
|
|
}
|