Files
2026-06-01 20:19:26 -04:00

200 lines
7.2 KiB
TypeScript

import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers":
"authorization, x-client-info, apikey, content-type, x-supabase-client-platform, x-supabase-client-platform-version, x-supabase-client-runtime, x-supabase-client-runtime-version",
};
serve(async (req) => {
if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });
try {
const LOVABLE_API_KEY = Deno.env.get("LOVABLE_API_KEY");
if (!LOVABLE_API_KEY) throw new Error("LOVABLE_API_KEY is not configured");
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
const supabaseServiceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const supabase = createClient(supabaseUrl, supabaseServiceKey);
// Validate user from auth header
const authHeader = req.headers.get("authorization") ?? "";
const anonKey = Deno.env.get("SUPABASE_ANON_KEY")!;
const userClient = createClient(supabaseUrl, anonKey, {
global: { headers: { Authorization: authHeader } },
});
const { data: { user }, error: authErr } = await userClient.auth.getUser();
if (authErr || !user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
const { question, association_id, chat_id } = await req.json();
if (!question || !association_id) {
return new Response(JSON.stringify({ error: "question and association_id are required" }), {
status: 400,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
// Fetch FAQs for the association
const { data: faqs } = await supabase
.from("association_faqs")
.select("question, answer")
.eq("association_id", association_id)
.order("sort_order");
const faqList = (faqs || [])
.filter((f: any) => f.answer)
.map((f: any, i: number) => `${i + 1}. Q: ${f.question}\n A: ${f.answer}`)
.join("\n\n");
const systemPrompt = `You are a friendly and helpful customer support assistant for a homeowners association community.
You have a knowledge base of pre-determined questions and answers. Try to match the homeowner's question to one of these FAQs and provide the answer. You can rephrase answers naturally but keep the same information.
If the question is clearly covered by one or more FAQs, answer it using that information.
If the question is NOT covered by any FAQ, or you're unsure, respond with EXACTLY this JSON format:
{"escalate": true, "reason": "Brief summary of what the homeowner is asking about"}
Do NOT make up answers. Only answer from the provided FAQ knowledge base.
Here are the available FAQs:
${faqList || "No FAQs have been configured yet."}`;
// Call Lovable AI
const aiResponse = await fetch("https://ai.gateway.lovable.dev/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${LOVABLE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "google/gemini-3-flash-preview",
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: question },
],
}),
});
if (!aiResponse.ok) {
if (aiResponse.status === 429) {
return new Response(JSON.stringify({ error: "Rate limited, please try again later." }), {
status: 429,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
if (aiResponse.status === 402) {
return new Response(JSON.stringify({ error: "Service temporarily unavailable." }), {
status: 402,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
const errText = await aiResponse.text();
console.error("AI gateway error:", aiResponse.status, errText);
throw new Error("AI gateway error");
}
const aiData = await aiResponse.json();
const aiMessage = aiData.choices?.[0]?.message?.content || "";
// Check if the AI wants to escalate
let escalated = false;
let answer = aiMessage;
let escalateReason = "";
try {
// Try parsing as JSON escalation response
const cleaned = aiMessage.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
const parsed = JSON.parse(cleaned);
if (parsed.escalate === true) {
escalated = true;
escalateReason = parsed.reason || question;
answer = "I wasn't able to find an answer to your question in our knowledge base. I've forwarded your question to our management team, and someone will get back to you shortly.";
}
} catch {
// Not JSON — it's a normal answer
}
// Ensure/create a chat record
let activeChatId = chat_id;
if (!activeChatId) {
const { data: newChat } = await supabase
.from("support_chats")
.insert({ user_id: user.id, association_id, status: "open" })
.select("id")
.single();
activeChatId = newChat?.id;
}
if (activeChatId) {
// Save user message
await supabase.from("support_chat_messages").insert({
chat_id: activeChatId,
role: "user",
content: question,
});
// Save AI response
await supabase.from("support_chat_messages").insert({
chat_id: activeChatId,
role: "assistant",
content: answer,
escalated,
});
}
// If escalated, notify all admins
if (escalated) {
// Get user's name
const { data: profile } = await supabase
.from("profiles")
.select("full_name")
.eq("user_id", user.id)
.single();
const userName = profile?.full_name || user.email || "A homeowner";
// Get all admin user IDs
const { data: adminRoles } = await supabase
.from("user_roles")
.select("user_id")
.eq("role", "admin");
const adminIds = (adminRoles || []).map((r: any) => r.user_id);
// Send notification to each admin
for (const adminId of adminIds) {
await supabase.rpc("insert_notification", {
p_user_id: adminId,
p_type: "support",
p_title: "Support Request",
p_message: `${userName} asked: "${question.substring(0, 100)}${question.length > 100 ? "..." : ""}"`,
});
}
// Also send a direct message to each admin
for (const adminId of adminIds) {
await supabase.from("direct_messages").insert({
sender_id: user.id,
recipient_id: adminId,
message: `[Support Request] ${userName} asked a question that couldn't be answered by the AI assistant:\n\n"${question}"`,
});
}
}
return new Response(
JSON.stringify({ answer, escalated, chat_id: activeChatId }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
} catch (e) {
console.error("ai-support-chat error:", e);
return new Response(
JSON.stringify({ error: e instanceof Error ? e.message : "Unknown error" }),
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
});