Adeen
Update auth logic with debug info in all edge functions
8f989d7
// Streams structured study notes (Markdown) from Gemini 2.5 Pro for a given document.
// Frontend reads SSE deltas; this function also persists the final markdown to `notes`.
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
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",
};
const SYSTEM_PROMPT = `You are an Expert Academic Data Extraction Agent. Your directive is to perform a **lossless, exhaustive extraction** of the provided educational document into a professional, clean Markdown format.
**STYLE CONSTRAINTS:**
- **NO EMOJIS:** You are strictly forbidden from using any emojis in the output.
- **PROFESSIONAL TONE:** Use clear, academic language.
- **EXHAUSTIVE DETAIL:** Do not summarize. Capture every definition, example, and scenario in full.
**REQUIRED OUTPUT STRUCTURE:**
# [Document Title]
**Overview:** This note covering [Topic] was created from a [Page Count] page document. It provides a structured walk-through of [Primary Goal of Document].
## Key Points
- [High-level topic 1]
- [High-level topic 2]
- [High-level topic 3]
- [High-level topic 4]
## [Section Heading]
[Detailed introduction to the section]
### [Sub-topic or Framework Item]
**Full Definition:** [Extract exhaustive definition word-for-word]
**Detailed Example/Context:** [Extract full context and examples]
**Question:** [If a scenario is presented, format the question here]
**Answer:** [Format the solution/result here]
*(Use Tables for Frameworks like SHAPES or Ancillary Skills lists)*
| Item | Meaning/Why Important | Example |
|------|----------------------|---------|
| [X] | [Y] | [Z] |
## Key Scenarios & Practical Applications
**Scenario:** [Extract general scenarios]
**Solution/Takeaway:** [Extract solution]
## Final Summary & Synthesis
[A comprehensive wrap-up of the document's value]
**FINAL DIRECTIVE:** Maintain 100% fidelity to the source text. Every sub-topic from the document must have its own detailed heading. Never group distinct items into a single paragraph.`;
Deno.serve(async (req) => {
if (req.method === "OPTIONS") return new Response("ok", { headers: corsHeaders });
const authHeader = req.headers.get("Authorization");
if (!authHeader?.startsWith("Bearer ")) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;
const ANON = Deno.env.get("SUPABASE_ANON_KEY")!;
const SERVICE = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const GROQ_API_KEY = Deno.env.get("GROQ_API_KEY");
if (!GROQ_API_KEY) {
return new Response(JSON.stringify({ error: "GROQ_API_KEY not configured" }), {
status: 500,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
const userClient = createClient(SUPABASE_URL, ANON, {
global: { headers: { Authorization: authHeader } },
});
const admin = createClient(SUPABASE_URL, SERVICE);
const token = authHeader.replace("Bearer ", "");
const { data: { user }, error: userErr } = await userClient.auth.getUser(token);
if (userErr || !user) {
const msg = userErr ? userErr.message : "User not found";
return new Response(JSON.stringify({ error: "Unauthorized", details: msg }), {
status: 401,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
const userId = user.id;
let body: { document_id?: string };
try {
body = await req.json();
} catch {
return new Response(JSON.stringify({ error: "Invalid JSON" }), {
status: 400,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
const documentId = body.document_id;
if (!documentId) {
return new Response(JSON.stringify({ error: "document_id required" }), {
status: 400,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
const { data: doc } = await admin
.from("documents")
.select("id,user_id,title,raw_text,status")
.eq("id", documentId)
.maybeSingle();
if (!doc || doc.user_id !== userId) {
return new Response(JSON.stringify({ error: "Document not found" }), {
status: 404,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
if (!doc.raw_text || doc.raw_text.trim().length < 20) {
return new Response(JSON.stringify({ error: "Document not ready" }), {
status: 400,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
// Groq llama-3.3-70b-versatile context: ~32k tokens; keep source under ~80k chars to be safe
const MAX_CHARS = 80_000;
const source = doc.raw_text.length > MAX_CHARS ? doc.raw_text.slice(0, MAX_CHARS) : doc.raw_text;
const userPrompt =
`Source title: ${doc.title}\n\n--- SOURCE START ---\n${source}\n--- SOURCE END ---\n\n` +
`Generate the study notes now, following the required structure exactly.`;
const aiResp = await fetch("https://api.groq.com/openai/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${GROQ_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "llama-3.3-70b-versatile",
stream: true,
temperature: 0.4,
messages: [
{ role: "system", content: SYSTEM_PROMPT },
{ role: "user", content: userPrompt },
],
}),
});
if (!aiResp.ok || !aiResp.body) {
if (aiResp.status === 429) {
return new Response(
JSON.stringify({ error: "Rate limit reached on Groq's free tier — please wait a moment and try again." }),
{ status: 429, headers: { ...corsHeaders, "Content-Type": "application/json" } },
);
}
const t = await aiResp.text();
console.error("Groq error", aiResp.status, t);
return new Response(JSON.stringify({ error: "Groq API error" }), {
status: 500,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
let fullMarkdown = "";
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const stream = new ReadableStream({
async start(controller) {
const reader = aiResp.body!.getReader();
let textBuffer = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
controller.enqueue(value);
textBuffer += decoder.decode(value, { stream: true });
let idx: number;
while ((idx = textBuffer.indexOf("\n")) !== -1) {
let line = textBuffer.slice(0, idx);
textBuffer = textBuffer.slice(idx + 1);
if (line.endsWith("\r")) line = line.slice(0, -1);
if (!line.startsWith("data: ")) continue;
const json = line.slice(6).trim();
if (json === "[DONE]") continue;
try {
const parsed = JSON.parse(json);
const delta = parsed.choices?.[0]?.delta?.content;
if (delta) fullMarkdown += delta;
} catch {
textBuffer = line + "\n" + textBuffer;
break;
}
}
}
} catch (e) {
console.error("stream error", e);
} finally {
controller.close();
try {
if (fullMarkdown.trim().length > 0) {
const { data: existing } = await admin
.from("notes")
.select("id")
.eq("document_id", documentId)
.maybeSingle();
if (existing) {
await admin
.from("notes")
.update({ markdown: fullMarkdown })
.eq("id", existing.id);
} else {
await admin
.from("notes")
.insert({ document_id: documentId, user_id: userId, markdown: fullMarkdown });
}
}
} catch (e) {
console.error("note persist error", e);
}
}
},
});
return new Response(stream, {
headers: {
...corsHeaders,
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
},
});
});