mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
183fe0a93c
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
200 lines
7.2 KiB
TypeScript
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" } }
|
|
);
|
|
}
|
|
});
|