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

+80 -7
View File
@@ -40,15 +40,28 @@ export function ItemAiAssistant({
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [rawJson, setRawJson] = useState<string | null>(null);
const [logs, setLogs] = useState<string[]>([]);
const disabled = useMemo(() => !name.trim() || !description.trim(), [name, description]);
// Habilita geração se houver ao menos uma imagem válida OU nome+descrição preenchidos
const canGenerate = useMemo(() => {
const hasValidImage = images.length > 0;
const hasTextInputs = name.trim() && description.trim();
return Boolean(hasValidImage || hasTextInputs);
}, [images.length, name, description]);
const disabled = useMemo(() => !canGenerate, [canGenerate]);
const handleGenerate = useCallback(async () => {
if (disabled || loading) return;
setLoading(true);
setError(null);
setLogs((prev) => [...prev, "Iniciando geração com IA..."]);
try {
setLogs((prev) => [
...prev,
`Payload: nome='${name.trim()}', escopo='${scope}', imagens=${images.length}, servidor='${scope === "server" ? server : "(global)"}'`,
]);
const response = await fetch("/api/admin/items/analyze", {
method: "POST",
credentials: "include",
@@ -73,13 +86,57 @@ export function ItemAiAssistant({
const data = await response.json();
const structure = data?.structure ?? data;
const columns = structure?.columns;
setLogs((prev) => [...prev, `Resposta recebida. Tamanho ~${JSON.stringify(structure).length} bytes.`]);
setRawJson(JSON.stringify(structure, null, 2));
// Heurística: sugerir nomes com base em imagens quando não houver texto detectado
const suggestNamesFromImages = (imgs: ProcessedImage[]): string[] => {
if (!Array.isArray(imgs) || imgs.length === 0) return [];
const first = imgs[0];
const base = first.originalName
.replace(/\.[a-zA-Z0-9]+$/, "")
.toLowerCase()
.replace(/[_\s]+/g, "-");
const common = new Set(["image", "img", "screenshot", "captura", "photo", "foto", "unknown"]);
const orientation = first.height > first.width ? "retrato" : "paisagem";
const suggestions: string[] = [];
if (base && !common.has(base)) suggestions.push(base);
suggestions.push(`${orientation}-1`);
suggestions.push("foto-1");
return Array.from(new Set(suggestions)).slice(0, 3);
};
const autoNameDetected = typeof columns?.name === "string" && Boolean(String(columns.name).trim());
const autoDescDetected = typeof columns?.description === "string" && Boolean(String(columns.description).trim());
const origemDados: string = autoNameDetected || autoDescDetected ? "automatico" : "nao_detectado";
const sugestoesNome = !autoNameDetected ? suggestNamesFromImages(images) : [];
// Estrutura final com campos obrigatórios, origem e sugestões
const finalStructure = {
...structure,
columns: {
...columns,
origem_dados: origemDados,
confirmar_nome: autoNameDetected ? false : undefined,
mensagens: {
obrigatorio: ["nome"],
dicas: autoNameDetected
? "Confirme o nome sugerido ou ajuste se necessário."
: "Informe um nome para o item. Descrição é opcional, mas ajuda.",
},
sugestoes: sugestoesNome.length > 0 ? { nome: sugestoesNome } : undefined,
},
};
setRawJson(JSON.stringify(finalStructure, null, 2));
const prefill: ItemAiPrefill = {
name: typeof columns?.name === "string" ? columns.name : undefined,
description: typeof columns?.description === "string" ? columns.description : undefined,
metadata: typeof columns === "object" && columns !== null ? columns : undefined,
name: autoNameDetected ? (columns!.name as string) : undefined,
description: autoDescDetected ? (columns!.description as string) : undefined,
metadata: typeof columns === "object" && columns !== null
? { ...columns, origem_dados: origemDados, confirmar_nome: autoNameDetected ? false : undefined, sugestoes: sugestoesNome.length ? { nome: sugestoesNome } : undefined }
: { origem_dados: origemDados, confirmar_nome: autoNameDetected ? false : undefined, sugestoes: sugestoesNome.length ? { nome: sugestoesNome } : undefined },
};
if (columns) {
@@ -103,12 +160,15 @@ export function ItemAiAssistant({
}
}
onGenerated({ json: structure, prefill });
setLogs((prev) => [...prev, "Sugestões de preenchimento geradas. Aguardando confirmação do usuário."]);
onGenerated({ json: finalStructure, prefill });
} catch (err) {
console.error(err);
setError(err instanceof Error ? err.message : "Erro ao processar análise de IA.");
setLogs((prev) => [...prev, `Erro: ${err instanceof Error ? err.message : String(err)}`]);
} finally {
setLoading(false);
setLogs((prev) => [...prev, "Processo de geração concluído."]);
}
}, [
categories,
@@ -199,6 +259,9 @@ export function ItemAiAssistant({
<div className="space-y-2">
<label className="text-sm font-medium">Prints / evidências visuais</label>
<ItemImageCapture onImagesChange={setImages} />
{images.length > 0 && (
<p className="text-xs text-emerald-600">Imagem recebida — pronta para processamento.</p>
)}
</div>
{error && (
@@ -213,7 +276,7 @@ export function ItemAiAssistant({
Gerar com IA
</Button>
<p className="text-xs text-muted-foreground">
A IA retornará um JSON com sugestões de campos. Eles serão aplicados automaticamente no formulário.
A IA retornará um JSON com sugestões de campos. As alterações só serão aplicadas após sua confirmação.
</p>
</div>
</div>
@@ -233,6 +296,16 @@ export function ItemAiAssistant({
campos do formulário.
</p>
)}
{logs.length > 0 && (
<div className="space-y-1">
<p className="text-[11px] text-muted-foreground">Logs do processo</p>
<div className="rounded-md border bg-muted/30 p-2">
{logs.map((line, idx) => (
<p key={idx} className="text-[11px] leading-relaxed">• {line}</p>
))}
</div>
</div>
)}
</CardContent>
</Card>
</div>