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:
1 parent
dc169f9eba
commit
d3704602fd
22 files changed
+1317
-95
No files matched your search
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user