Files
2026-04-13 13:45:18 +07:00

91 lines
3.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Supabase Edge Function: writing-check
// Uses GLM API (OpenAI-compatible) to analyze English writing submissions.
// Deploy: supabase functions deploy writing-check --no-verify-jwt
// Secrets: supabase secrets set GLM_API_KEY=<your_key>
import OpenAI from "npm:openai@^4"
const glm = new OpenAI({
apiKey: Deno.env.get("GLM_API_KEY") ?? "",
baseURL: "https://open.bigmodel.cn/api/paas/v4/",
})
const CORS_HEADERS = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
}
const SYSTEM_PROMPT = `You are an expert English writing teacher specialising in TOEIC and IELTS assessment.
Analyse the student's writing and respond ONLY with valid JSON — no markdown, no extra text:
{
"score": "<estimated band score, e.g. 6.5>",
"grammar": ["<issue 1 with correction, mix English example + Vietnamese explanation>", ...],
"vocabulary": ["<vocabulary observation in Vietnamese>", ...],
"structure": "<23 sentence structure assessment in Vietnamese>",
"improved_version": "<the full improved text in English>",
"summary": "<23 sentence overall assessment in Vietnamese>"
}`
Deno.serve(async (req: Request) => {
if (req.method === "OPTIONS") {
return new Response("ok", { headers: CORS_HEADERS })
}
try {
const { content } = await req.json() as { content: string }
if (!content || content.trim().length < 10) {
return new Response(
JSON.stringify({ error: "Bài viết quá ngắn. Vui lòng nhập ít nhất 10 ký tự." }),
{ status: 400, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } },
)
}
const stream = await glm.chat.completions.create({
model: Deno.env.get("GLM_MODEL") ?? "GLM-4.5-Flash",
messages: [
{ role: "system", content: SYSTEM_PROMPT },
{ role: "user", content: `Analyse this writing:\n\n${content.slice(0, 2000)}` },
],
temperature: 0.3,
max_tokens: 1500,
stream: true,
})
const encoder = new TextEncoder()
const body = new ReadableStream({
async start(controller) {
try {
for await (const chunk of stream) {
const text = chunk.choices[0]?.delta?.content ?? ""
if (text) {
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ text })}\n\n`))
}
}
} catch (err) {
console.error("writing-check stream error:", err)
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ error: "Đã có lỗi khi chấm bài. Vui lòng thử lại." })}\n\n`),
)
}
controller.enqueue(encoder.encode("data: [DONE]\n\n"))
controller.close()
},
})
return new Response(body, {
headers: {
...CORS_HEADERS,
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
},
})
} catch (err) {
console.error("writing-check error:", err)
return new Response(
JSON.stringify({ error: "Đã có lỗi khi chấm bài. Vui lòng thử lại." }),
{ status: 500, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } },
)
}
})