// Sofia Client — SDK frontend pra chamar a Edge Function `sofia-chat`. // // Expõe window.SofiaAPI com: // chat(messages, userState, newMessage) → Promise<{ reply, mode, usage }> // buildUserState(state, userName) → resumo enxuto pro prompt // getMonthlyUsage() → para o admin // // Comportamento: // - Se Supabase não configurado OU Edge Function não disponível OU // resposta veio com mode='mock' → cai pro fallback local (pickReply). // - Rate-limit excedido (HTTP 429) → devolve a mensagem amiga da Sofia // ao invés de erro técnico. (function () { const FUNCTION_NAME = "sofia-chat"; // ── Fallback local (mock) ────────────────────────────────── function pickMockReply(q) { const lower = (q || "").toLowerCase(); if (lower.includes("semana")) return "Sua semana foi sólida! ✨ Quando a Sofia estiver 100% online consigo te trazer números reais. Por enquanto: continue assim que tá bom."; if (lower.includes("gast") || lower.includes("dinheiro")) return "Quando a IA tiver a chave configurada consigo analisar seu padrão de gastos em detalhe. Por agora: cuidado com a categoria que mais cresceu nos últimos 30 dias."; if (lower.includes("estudo") || lower.includes("inglês")) return "Posso montar um plano completo de estudos quando estiver totalmente ativa. Comece com 30 min/dia que já é um ótimo ritmo."; if (lower.includes("hábito") || lower.includes("habito")) return "Hábitos consistentes valem mais do que hábitos intensos. Foque em 1 ou 2 e mantenha a corrente acesa 🔥"; if (lower.includes("meta") || lower.includes("objetivo")) return "Metas grandes ficam mais leves quando você quebra em passos semanais. Que tal escolher 1 ação concreta pra essa semana?"; return "Anotado! Quando a Sofia estiver totalmente conectada, vou te dar respostas com dados reais do seu app. Algo mais? ✨"; } // ── Helpers ──────────────────────────────────────────────── function client() { if (window.Auth && typeof window.Auth._client === "function") { return window.Auth._client(); } return null; } function isConfigured() { return !!client(); } // Reduz o state do app a um resumo de baixo custo pra Sofia function buildUserState(state, userName) { if (!state) return { userName }; const today = new Date().toISOString().slice(0, 10); const tasks = state.tasks || []; const tasksOpen = tasks.filter((t) => !t.done).length; const tasksToday = tasks .filter((t) => !t.done && (t.date === today || t.dueToday)) .slice(0, 5) .map((t) => t.title); const habits = (state.habits || []) .filter((h) => !h.archived) .slice(0, 5) .map((h) => ({ name: h.name, streak: h.streak || 0 })); const goals = (state.goals || []) .filter((g) => !g.completed) .slice(0, 4) .map((g) => ({ title: g.title, progress: typeof g.progress === "number" ? Math.round(g.progress) : 0, })); // Calcula entradas/saídas direto das transações. // Estrutura: tx.value (negativo = saída, positivo = entrada), tx.cat (categoria), tx.time const finance = state.finance || {}; const txs = finance.transactions || []; let income = 0; let expense = 0; const catTotals = {}; txs.forEach((tx) => { const v = Number(tx.value !== undefined ? tx.value : tx.amount) || 0; const isIncome = v > 0 || tx.type === 'income'; if (isIncome) income += Math.abs(v); else expense += Math.abs(v); const cat = tx.cat || tx.category; if (cat && !isIncome) catTotals[cat] = (catTotals[cat] || 0) + Math.abs(v); }); const topCategory = Object.entries(catTotals).sort((a, b) => b[1] - a[1])[0]?.[0]; // Saldo: usa o campo direto do state se existir, senão calcula (entradas - saídas) const saldo = typeof finance.saldo === 'number' ? finance.saldo : (income - expense); // Cofre — dados sensíveis (o usuário pediu pra Sofia poder consultar) // Estrutura salva pelo Cofre: { type, title, data: { site, user, password, ... } } // Aqui achatamos pra Sofia não ter que navegar no aninhamento. const vault = (state.vault || []).map(v => { const d = (v && v.data) || {}; return { type: v.type, // 'senha' | 'conta' | 'email' | 'nota' title: v.title, site: d.site, user: d.user, password: d.password, banco: d.banco, agencia: d.agencia, conta: d.conta, tipoConta: d.tipoConta, provedor: d.provedor, email: d.email, notes: d.notes, text: d.text, }; }); return { userName, tasksOpen, tasksToday, habitsActive: habits, goalsActive: goals, financeMonth: { saldo: Math.round(saldo * 100) / 100, income: Math.round(income * 100) / 100, expense: Math.round(expense * 100) / 100, topCategory, transactionCount: txs.length, }, streak: state.streak || 0, level: state.level || 1, vault, }; } // ── Chat ─────────────────────────────────────────────────── async function chat({ messages = [], userState, newMessage }) { const c = client(); // Sem Supabase → mock direto if (!c) { return { reply: pickMockReply(newMessage), mode: "mock", usage: null }; } try { const { data, error } = await c.functions.invoke(FUNCTION_NAME, { body: { messages, userState, newMessage }, }); // Erro de rede / função não deployed if (error) { console.warn("[Sofia] function error:", error.message); return { reply: pickMockReply(newMessage), mode: "mock", usage: null }; } // Rate-limit 429 (Edge Function devolve mensagem amiga) if (data && data.error === "rate_limit") { return { reply: data.message || "Você atingiu o limite de hoje. Volto amanhã ✨", mode: "rate_limit", usage: { dailyLimit: data.limit, used: data.used }, }; } // Função respondeu mas sem chave OpenAI configurada if (data && data.mode === "mock") { return { reply: pickMockReply(newMessage), mode: "mock", usage: null }; } // Erro na chamada OpenAI if (data && data.error) { console.warn("[Sofia] api error:", data.detail); return { reply: pickMockReply(newMessage), mode: "mock", usage: null }; } // Sucesso return { reply: data.reply, mode: data.mode || "live", usage: data.usage || null, }; } catch (e) { console.warn("[Sofia] crash:", e.message); return { reply: pickMockReply(newMessage), mode: "mock", usage: null }; } } // ── Métricas de uso (pro painel admin) ───────────────────── async function getMonthlyUsage() { const c = client(); if (!c) return null; const { data, error } = await c .from("ai_usage_monthly") .select("*") .limit(12); if (error) { console.warn("[Sofia] usage fetch:", error.message); return null; } return data; } async function getCurrentMonthCost() { const c = client(); if (!c) return { cost: 0, calls: 0, users: 0 }; const first = new Date(); first.setDate(1); first.setHours(0, 0, 0, 0); const { data, error } = await c .from("ai_usage") .select("cost_usd, user_id") .gte("created_at", first.toISOString()); if (error) { console.warn("[Sofia] month cost:", error.message); return { cost: 0, calls: 0, users: 0 }; } const cost = (data || []).reduce((s, r) => s + Number(r.cost_usd || 0), 0); const users = new Set((data || []).map((r) => r.user_id)).size; return { cost, calls: (data || []).length, users }; } window.SofiaAPI = { chat, buildUserState, getMonthlyUsage, getCurrentMonthCost, isConfigured, pickMockReply, }; })();