import Link from "next/link"; import { and, desc, eq, isNull, inArray, sql } from "drizzle-orm"; import { auth } from "@/auth"; import { db } from "@/lib/db/client"; import { memories, projects } from "@/lib/db/schema"; import { searchMemories } from "@/lib/memories"; import { Container, PageHeader } from "@/app/_components/ui/container"; import { Card, CardBody } from "@/app/_components/ui/card"; import { Badge } from "@/app/_components/ui/badge"; import { Button } from "@/app/_components/ui/button"; import { Input } from "@/app/_components/ui/input"; import { EmptyState } from "@/app/_components/ui/empty-state"; export const dynamic = "force-dynamic"; type Scope = "project" | "user"; interface MemoryRow { id: string; scope: "project" | "user"; projectKey: string | null; content: string; tags: string[]; createdAt: Date; rank?: { rrfScore: number; vectorRank: number | null; ftsRank: number | null; tagRank: number | null }; } async function fetchMemoriesByIds( userId: string, ids: string[], ): Promise> { if (ids.length === 0) return new Map(); const rows = await db .select({ id: memories.id, scope: memories.scope, content: memories.content, tags: memories.tags, createdAt: memories.createdAt, projectKey: projects.key, }) .from(memories) .leftJoin(projects, eq(memories.projectId, projects.id)) .where(and(eq(memories.userId, userId), inArray(memories.id, ids), isNull(memories.deletedAt))); return new Map(rows.map((r) => [r.id, r as MemoryRow])); } async function listRecent(userId: string, scope?: Scope, project?: string): Promise { const filters = [eq(memories.userId, userId), isNull(memories.deletedAt)]; if (scope) filters.push(eq(memories.scope, scope)); if (project) { filters.push( sql`${memories.projectId} = ( SELECT id FROM ${projects} WHERE ${projects.userId} = ${userId} AND ${projects.key} = ${project} )`, ); } const rows = await db .select({ id: memories.id, scope: memories.scope, content: memories.content, tags: memories.tags, createdAt: memories.createdAt, projectKey: projects.key, }) .from(memories) .leftJoin(projects, eq(memories.projectId, projects.id)) .where(and(...filters)) .orderBy(desc(memories.createdAt)) .limit(50); return rows; } export default async function MemoriesPage({ searchParams, }: { searchParams: Promise<{ q?: string; scope?: string; project?: string }>; }) { const session = await auth(); const userId = session!.user.id; const params = await searchParams; const q = params.q?.trim() || undefined; const scope = params.scope === "user" || params.scope === "project" ? params.scope : undefined; const project = params.project?.trim() || undefined; let rows: MemoryRow[] = []; let debug: { vec: number; fts: number; tag: number } | null = null; if (q) { const result = await searchMemories(userId, q, { scope, projectKey: project }, 30); const ids = result.hits.map((h) => h.id); const byId = await fetchMemoriesByIds(userId, ids); rows = result.hits.flatMap((h) => { const r = byId.get(h.id); return r ? [{ ...r, rank: h.rank }] : []; }); debug = result.debug; } else { rows = await listRecent(userId, scope, project); } return ( } />
{debug ? (

candidates · vector: {debug.vec} · fts: {debug.fts} · tag: {debug.tag}

) : null} {rows.length === 0 ? ( ) : null } /> ) : (
    {rows.map((m) => (
  • {m.scope} {m.projectKey ? ( · {m.projectKey} ) : null} · {new Date(m.createdAt).toLocaleString()} {m.rank ? ( rrf {m.rank.rrfScore.toFixed(4)} · v {m.rank.vectorRank ?? "−"} · f {m.rank.ftsRank ?? "−"} · t {m.rank.tagRank ?? "−"} ) : null}

    {m.content}

    {m.tags.length ? (
    {m.tags.map((t) => ( {t} ))}
    ) : null}
  • ))}
)}
); } function FilterSelect({ name, value, options, placeholder, }: { name: string; value: string | undefined; options: string[]; placeholder: string; }) { return ( ); }