import { promises as fs } from "node:fs"; import path from "node:path"; interface PromptContext { name: string; description: string; scope: "global" | "server"; server?: string | null; categories: string[]; servers: string[]; } let schemaSummaryCache: string | null = null; async function loadSchemaSummary(): Promise { if (schemaSummaryCache) { return schemaSummaryCache; } const schemaPath = path.join(process.cwd(), "prisma", "schema.prisma"); let file: string; try { file = await fs.readFile(schemaPath, "utf-8"); } catch (error) { console.warn("[item-prompt] Não foi possível ler schema.prisma:", error); schemaSummaryCache = "Schema indisponível."; return schemaSummaryCache; } const modelRegex = /model\s+(\w+)\s*{([^}]*)}/g; const blocks: string[] = []; let match: RegExpExecArray | null; while ((match = modelRegex.exec(file)) !== null) { const modelName = match[1]; const body = match[2] .split("\n") .map((line) => line.trim()) .filter( (line) => line && !line.startsWith("//") && !line.startsWith("@@"), ) .join("\n"); blocks.push(`Model ${modelName} {\n${body}\n}`); } schemaSummaryCache = blocks.join("\n\n"); return schemaSummaryCache; } const FORM_FIELD_SUMMARY = ` Front-end fields (AdminItemForm): - name: string - category: string (utiliza categorias pré-carregadas) - rarity: string opcional - imageUrl / imageLocalPath: caminhos de imagem - level, weight, slots: numéricos opcionais - skill: string opcional - servers: lista de servidores (separados por vírgula) - tags: lista de tags (texto) - description: texto (máximo 100 caracteres) - dynamic metadata: campos variáveis definidos por categoria (servidores, atributos, etc) `.trim(); export async function buildItemPrompt( context: PromptContext, extraInstructions?: string, ) { const schemaSummary = await loadSchemaSummary(); const categoriesText = context.categories.length > 0 ? context.categories.map((category) => `- ${category}`).join("\n") : "- Nenhuma categoria cadastrada"; const serversText = context.servers.length > 0 ? context.servers.map((server) => `- ${server}`).join("\n") : "- Nenhum servidor cadastrado"; return ` Você é um assistente que prepara payloads JSON para cadastro de itens no banco de dados do Tibia Trade. INSTRUÇÕES IMPORTANTES: - Responda SOMENTE com JSON válido. Nenhum texto fora do JSON. - Se não tiver certeza, retorne null nos campos desconhecidos. - Utilize ao máximo a estrutura de tabelas e campos informada abaixo. - Você receberá somente a estrutura das tabelas SQLite (nomes, colunas e tipos). Use esse esquema para escolher uma tabela existente adequada ou sugerir uma nova. Nunca inclua dados reais. - Campo "description": limite máximo de 100 caracteres. Se a descrição original exceder 100 caracteres, retorne apenas os primeiros 100 caracteres sem acrescentar sufixos (sem "..."), e nunca gere texto além desse limite. Contexto fornecido pelo usuário: - Nome: ${context.name} - Escopo: ${context.scope === "server" ? `servidor específico (${context.server ?? "não informado"})` : "global"} - Descrição original: """ ${context.description} """ - Categorias disponíveis no painel: ${categoriesText} - Servidores disponíveis no painel: ${serversText} Schema do banco (Prisma): ${schemaSummary} Campos principais do formulário no front: ${FORM_FIELD_SUMMARY} Formato obrigatório da resposta: { "table": "nome_da_tabela", "columns": { "name": string | null, "description": string | null, "category": string | null, "rarity": string | null, "image_url": string | null, "image_local_path": string | null, "level": number | null, "weight": number | null, "slots": number | null, "skill": string | null, "servers": string[] | null, "tags": string[] | null, "metadata": object | null }, "schemaSuggestion": { "table": "nome_da_tabela_existente_ou_nova", "columns": [ { "name": "coluna_1", "type": "TEXT|INTEGER|REAL|BOOLEAN" }, { "name": "coluna_2", "type": "TEXT|INTEGER|REAL|BOOLEAN" } ] } } Consistência: - Escolha a tabela coerente com o escopo. Para global, use "items_all". - Para escopo de servidor, use "items_", onde é o nome do servidor em minúsculas, com espaços transformados em "_" e removendo caracteres não alfanuméricos. - Inclua sempre os campos básicos: name, description. - Aplique outros campos conforme a descrição (ex.: atributos, requisitos, bônus). - Se houver informações que encaixem em metadados dinâmicos, use um objeto "metadata". - Nunca produza "description" com mais de 100 caracteres. Regras adicionais: - Servidores ("servers"): utilize SOMENTE nomes presentes na lista fornecida no contexto. Prefira o formato array de strings (ex.: ["Belobra", "Antica"]). Se não aplicável, use null. - Metadata: inclua apenas atributos descritivos (ex.: "attributes", "requirements", "protections", "flags"). É PROIBIDO incluir qualquer campo de valor/preço/mercado. Não utilize chaves: "price", "value", "buy", "sell", "currency", "npc_price", "marketPrice", "minPrice", "maxPrice", "avgPrice". - Tipos: números devem ser números (sem aspas). Se o valor for desconhecido, retorne null. Não invente dados. ${extraInstructions ?? ""} `.trim(); } export interface InlineImageData { mimeType: string; base64Data: string; } export async function buildGeminiParts( prompt: string, images: InlineImageData[] = [], ) { const parts: Array<{ text?: string; inline_data?: { mimeType: string; data: string }; }> = [{ text: prompt }]; images.slice(0, 2).forEach((image) => { parts.push({ inline_data: { mimeType: image.mimeType, data: image.base64Data, }, }); }); return parts; }