feat(items): add server field and improve item management

refactor(ai): enhance item structure generation with schema validation
feat(ai): add token usage tracking and cost calculation
fix(ui): improve item card number formatting with locale support
docs(items): add documentation for item value separation
test(items): add tests for value field validation
This commit is contained in:
Evandro Fonseca Junior committed 2025-11-11 03:04:49 -03:00
1 parent dc169f9eba
commit d3704602fd
22 files changed
+1317 -95

No files matched your search

+44 -2
View File
@@ -1,6 +1,8 @@
import { NextRequest, NextResponse } from "next/server";
import { requireAdminUser } from "@/lib/auth/middleware";
import { generateItemStructure } from "@/lib/ai/generate-item-structure";
import { calculateTokenCost } from "@/lib/ai/token-pricing";
import { recordTokenUsage } from "@/lib/ai/token-usage";
export async function POST(request: NextRequest) {
const auth = await requireAdminUser(request);
@@ -26,12 +28,16 @@ export async function POST(request: NextRequest) {
const images: string[] = Array.isArray(body?.images)
? body.images.filter((item: unknown) => typeof item === "string")
: [];
const hasDescription = !!description;
const hasImages = images.length > 0;
if (!name || !description) {
// Nova regra: aceitar quando houver descrição OU ao menos uma imagem.
// Nome pode ser vazio; o assistente do front lida com sugestão/confirmar_nome.
if (!hasDescription && !hasImages) {
return NextResponse.json(
{
success: false,
error: "Informe nome e descrição para gerar o JSON estrutural.",
error: "Forneça descrição ou ao menos uma imagem para análise.",
},
{ status: 400 }
);
@@ -57,10 +63,46 @@ export async function POST(request: NextRequest) {
.filter((image) => image.base64Data),
});
// Cálculo de custo baseado em usageMetadata
const model = (result.model ?? "gemini-2.5-flash") as keyof typeof import("@/lib/ai/token-pricing").TOKEN_PRICING;
const breakdown = calculateTokenCost("gemini-2.5-flash", result.usage);
// Persistir histórico para relatórios (não bloqueia resposta; erros são logados no Redis util)
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
try {
await recordTokenUsage({
id: requestId,
timestamp: Date.now(),
model: result.model ?? "gemini-2.5-flash",
inputTokens: breakdown.inputTokens,
outputTokens: breakdown.outputTokens,
totalTokens: breakdown.totalTokens,
costUsd: breakdown.totalUsd,
context: {
route: "/api/admin/items/analyze",
scope,
server,
hasImages,
hasDescription,
categoriesCount: categories.length,
},
});
} catch {
// Falhas de Redis não devem impedir a análise
}
return NextResponse.json(
{
success: true,
structure: result.structure,
usage: {
promptTokens: breakdown.inputTokens,
outputTokens: breakdown.outputTokens,
totalTokens: breakdown.totalTokens,
},
costUsd: breakdown.totalUsd,
model: result.model ?? "gemini-2.5-flash",
requestId,
},
{ status: 200 }
);
+10 -1
View File
@@ -129,6 +129,7 @@ export async function POST(request: NextRequest) {
const body = await request.json();
const name = typeof body?.name === "string" ? body.name.trim() : "";
const category = typeof body?.category === "string" ? body.category.trim().toLowerCase() : "";
const originServer = typeof body?.server === "string" ? body.server.trim() || null : null;
if (!name || !category) {
return NextResponse.json(
@@ -150,11 +151,19 @@ export async function POST(request: NextRequest) {
...dynamicFields,
} as Record<string, unknown>;
// Se enviado servidor de origem, inclui na linha do SQLite e garante presença na lista de servidores do item
if (originServer) {
sqliteData.server = originServer;
}
const sqliteId = insertCategoryItem({ category, data: sqliteData });
const metadataBase = typeof body?.metadata === "object" && body.metadata !== null ? body.metadata : {};
const metadata = { ...metadataBase, ...dynamicFields } as Record<string, unknown>;
const itemServers = parseList(body?.servers);
const finalServers = originServer ? Array.from(new Set([originServer, ...itemServers])) : itemServers;
const item = await createAdminItem({
item_name: name,
category,
@@ -165,7 +174,7 @@ export async function POST(request: NextRequest) {
weight: typeof body?.weight === "number" ? body.weight : null,
slots: typeof body?.slots === "number" ? body.slots : null,
skill: typeof body?.skill === "string" ? body.skill.trim() || null : null,
servers: parseList(body?.servers),
servers: finalServers,
tags: parseList(body?.tags),
description:
typeof body?.description === "string" ? body.description.trim() || null : null,