feat: Decisions page — aggregated view of all decision points across traces
Adds /dashboard/decisions page with colored type badges, search, filters (by type), sort (newest/oldest/costliest), pagination, and links to parent traces. New /api/decisions endpoint with Prisma queries. Removes 'Soon' badge from sidebar nav.
This commit is contained in:
127
apps/web/src/app/api/decisions/route.ts
Normal file
127
apps/web/src/app/api/decisions/route.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { Prisma } from "@agentlens/database";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const page = parseInt(searchParams.get("page") ?? "1", 10);
|
||||
const limit = parseInt(searchParams.get("limit") ?? "20", 10);
|
||||
const type = searchParams.get("type");
|
||||
const search = searchParams.get("search");
|
||||
const sort = searchParams.get("sort") ?? "newest";
|
||||
|
||||
// Validate pagination
|
||||
if (isNaN(page) || page < 1) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid page parameter. Must be a positive integer." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (isNaN(limit) || limit < 1 || limit > 100) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid limit parameter. Must be between 1 and 100." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate type
|
||||
const validTypes = [
|
||||
"TOOL_SELECTION",
|
||||
"ROUTING",
|
||||
"RETRY",
|
||||
"ESCALATION",
|
||||
"MEMORY_RETRIEVAL",
|
||||
"PLANNING",
|
||||
"CUSTOM",
|
||||
];
|
||||
if (type && !validTypes.includes(type)) {
|
||||
return NextResponse.json(
|
||||
{ error: `Invalid type. Must be one of: ${validTypes.join(", ")}` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate sort
|
||||
const validSorts = ["newest", "oldest", "costliest"];
|
||||
if (!validSorts.includes(sort)) {
|
||||
return NextResponse.json(
|
||||
{ error: `Invalid sort. Must be one of: ${validSorts.join(", ")}` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Build where clause
|
||||
const where: Prisma.DecisionPointWhereInput = {};
|
||||
if (type) {
|
||||
where.type = type as Prisma.EnumDecisionTypeFilter["equals"];
|
||||
}
|
||||
if (search) {
|
||||
where.reasoning = {
|
||||
contains: search,
|
||||
mode: "insensitive",
|
||||
};
|
||||
}
|
||||
|
||||
// Build order by
|
||||
let orderBy: Prisma.DecisionPointOrderByWithRelationInput;
|
||||
switch (sort) {
|
||||
case "oldest":
|
||||
orderBy = { timestamp: "asc" };
|
||||
break;
|
||||
case "costliest":
|
||||
orderBy = { costUsd: "desc" };
|
||||
break;
|
||||
case "newest":
|
||||
default:
|
||||
orderBy = { timestamp: "desc" };
|
||||
break;
|
||||
}
|
||||
|
||||
// Count total
|
||||
const total = await prisma.decisionPoint.count({ where });
|
||||
|
||||
// Pagination
|
||||
const skip = (page - 1) * limit;
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
// Fetch decisions with parent trace and span
|
||||
const decisions = await prisma.decisionPoint.findMany({
|
||||
where,
|
||||
include: {
|
||||
trace: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
span: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy,
|
||||
skip,
|
||||
take: limit,
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
decisions,
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages,
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error listing decisions:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user