// 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, })); const finance = state.finance || {}; const txs = finance.transactions || []; let income = 0; let expense = 0; const catTotals = {}; const thisMonth = today.slice(0, 7); txs.forEach((tx) => { if (typeof tx.date === "string" && tx.date.slice(0, 7) !== thisMonth) return; const v = Number(tx.amount) || 0; if (tx.type === "income") income += v; else expense += Math.abs(v); if (tx.category) catTotals[tx.category] = (catTotals[tx.category] || 0) + Math.abs(v); }); const topCategory = Object.entries(catTotals).sort((a, b) => b[1] - a[1])[0]?.[0]; // Cofre — dados sensíveis (o usuário pediu pra Sofia poder consultar) // Incluímos tudo do cofre, mas o system prompt orienta a Sofia a tratar // como informação sensível: só revelar quando explicitamente perguntado, // nunca repetir sem necessidade. const vault = (state.vault || []).map(v => ({ type: v.type, // 'senha' | 'conta' | 'email' | 'nota' title: v.title, site: v.site, user: v.user, password: v.password, banco: v.banco, agencia: v.agencia, conta: v.conta, tipoConta: v.tipoConta, provedor: v.provedor, email: v.email, notes: v.notes, text: v.text, })); return { userName, tasksOpen, tasksToday, habitsActive: habits, goalsActive: goals, financeMonth: { income: Math.round(income), expense: Math.round(expense), topCategory, }, 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, }; })();