// SGL · Cliente OpenAI (frontend) // Chama a Edge Function `openai-proxy` via supa.functions.invoke, que anexa // automaticamente o token da sessão do usuário logado. A chave da OpenAI NUNCA // trafega pelo navegador — fica só nos Secrets do Supabase (servidor). // Mesma chave usada pelo M13 (m_config.jsx). Apenas tunings NÃO-sensíveis ficam // no localStorage; a OPENAI_API_KEY nunca é salva aqui. const OPENAI_CFG_STORAGE_KEY = 'sgl_config_v1'; /** * Lê as configurações não-sensíveis da OpenAI salvas no M13. * @returns {{ modelo: string, temperatura?: number, maxTokens?: number, org?: string }} */ function loadOpenAICfg() { try { const all = JSON.parse(localStorage.getItem(OPENAI_CFG_STORAGE_KEY) || '{}'); const o = (all.integracoes && all.integracoes.openai) || {}; return { modelo: o.modelo || 'gpt-4o', temperatura: o.temperatura, maxTokens: o.maxTokens, org: o.org || undefined, }; } catch { return { modelo: 'gpt-4o' }; } } /** Mensagem amigável para erros de transporte do functions.invoke. */ function friendlyInvokeError(error) { const name = error && error.name; if (name === 'FunctionsFetchError') { return 'Não foi possível contatar o servidor. A Edge Function "openai-proxy" já foi publicada no Supabase?'; } return (error && error.message) || 'Falha ao chamar a função openai-proxy.'; } /** * Chama a Edge Function openai-proxy. * @param {string} action - ex.: 'test' * @param {object} [payload] - dados adicionais para a ação * @returns {Promise<{ ok: boolean, error?: string, latencia?: number, modelos?: number }>} */ async function callOpenAI(action, payload = {}) { if (!window.supa) { return { ok: false, error: 'Supabase não inicializado.' }; } // Exige usuário logado — a função do servidor também valida, isto é só UX. const { data: { session } } = await window.supa.auth.getSession(); if (!session) { return { ok: false, error: 'Você precisa estar logado para usar a IA.' }; } const cfg = loadOpenAICfg(); try { const { data, error } = await window.supa.functions.invoke('openai-proxy', { body: { action, ...cfg, ...payload }, }); if (error) { // FunctionsHttpError carrega a Response em .context — extraímos a mensagem PT do corpo. let serverMsg = null; try { if (error.context && typeof error.context.json === 'function') { const body = await error.context.json(); serverMsg = body && body.error; } } catch { /* corpo não-JSON: ignora e usa fallback */ } return { ok: false, error: serverMsg || friendlyInvokeError(error) }; } return data || { ok: false, error: 'Resposta vazia do servidor.' }; } catch (e) { return { ok: false, error: (e && e.message) || 'Erro inesperado ao chamar a OpenAI.' }; } } /** * Testa a conexão com a OpenAI (custo zero — lista modelos no servidor). * @returns {Promise<{ ok: boolean, error?: string, latencia?: number, modelos?: number }>} */ async function testOpenAI() { return callOpenAI('test'); } // ─── Análise de edital (M02) ────────────────────────────────── const PDFJS_WORKER_SRC = 'https://unpkg.com/pdfjs-dist@3.11.174/build/pdf.worker.min.js'; const ANALISE_MAX_CHARS = 24000; // limita contexto/custo enviado à OpenAI /** * Extrai a camada de texto de um PDF no próprio navegador (pdf.js). * Retorna '' se o PDF não tiver texto (ex.: escaneado/imagem). * @param {File|Blob} file * @returns {Promise} */ async function extrairTextoPdf(file) { const lib = window.pdfjsLib; if (!lib) throw new Error('Leitor de PDF (pdf.js) não carregado.'); lib.GlobalWorkerOptions.workerSrc = PDFJS_WORKER_SRC; const buf = await file.arrayBuffer(); const pdf = await lib.getDocument({ data: buf }).promise; let texto = ''; for (let i = 1; i <= pdf.numPages; i++) { const page = await pdf.getPage(i); const content = await page.getTextContent(); texto += content.items.map(it => it.str).join(' ') + '\n'; } return texto.trim(); } /** Monta as mensagens do prompt de extração estruturada do edital. */ function buildEditalMessages(texto) { const sys = [ 'Você é um analista de licitações públicas brasileiro.', 'Extraia do EDITAL as informações estruturadas e responda SOMENTE em JSON válido,', 'sem nenhum texto fora do JSON. NÃO invente: se um campo não constar no edital, use null', '(ou lista vazia). Use exatamente este schema:', '{', ' "objeto": string|null,', ' "orgao": string|null, "uf": string|null, "cidade": string|null, "plataforma": string|null,', ' "valor_estimado": number|null,', ' "data_abertura": string|null,', ' "prazo_entrega": string|null, "prazo_pagamento": string|null,', ' "validade_proposta": string|null, "vigencia_arp": string|null,', ' "intervalo_lances": string|null, "local_entrega": string|null,', ' "habilitacao": [{"documento": string, "obrigatorio": boolean}],', ' "alertas": [{"tom": "amber"|"red"|"cyan", "titulo": string, "descricao": string}],', ' "confianca": number', '}', 'Regras: valor_estimado como número em reais (sem "R$" nem separador de milhar).', 'alertas = prazos curtos, exigência de amostras, condições incomuns ou riscos relevantes', '(tom: red=crítico, amber=atenção, cyan=informativo). confianca = 0 a 1.', ].join('\n'); const user = 'EDITAL:\n"""\n' + texto.slice(0, ANALISE_MAX_CHARS) + '\n"""'; return [ { role: 'system', content: sys }, { role: 'user', content: user }, ]; } /** * Analisa o texto de um edital via OpenAI e retorna campos estruturados. * @param {string} texto * @returns {Promise<{ ok: boolean, error?: string, analise?: object, usage?: object }>} */ async function analisarEdital(texto) { const limpo = (texto || '').trim(); if (limpo.length < 50) { return { ok: false, error: 'Texto do edital muito curto para analisar (mínimo ~50 caracteres).' }; } const cfg = loadOpenAICfg(); const r = await callOpenAI('chat', { messages: buildEditalMessages(limpo), model: cfg.modelo, temperature: typeof cfg.temperatura === 'number' ? cfg.temperatura : 0.2, max_tokens: cfg.maxTokens || 2000, response_format: { type: 'json_object' }, }); if (!r.ok) return r; let analise; try { analise = JSON.parse(r.content); } catch { return { ok: false, error: 'A IA retornou um formato inesperado. Tente novamente.' }; } analise.modelo = r.model || cfg.modelo || 'gpt-4o'; analise.analisado_em = new Date().toISOString(); return { ok: true, analise, usage: r.usage }; } window.callOpenAI = callOpenAI; window.testOpenAI = testOpenAI; window.extrairTextoPdf = extrairTextoPdf; window.analisarEdital = analisarEdital;