-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.js
More file actions
76 lines (63 loc) · 3.35 KB
/
server.js
File metadata and controls
76 lines (63 loc) · 3.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
const express = require("express");
const path = require("path");
const app = express();
const PORT = process.env.PORT || 3000;
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
// ── Rate limiting ──
const rateLimits = new Map();
const WINDOW = 60 * 60 * 1000;
const MAX_REQ = 30;
function rateOk(ip) {
const now = Date.now();
const r = rateLimits.get(ip);
if (!r || now - r.start > WINDOW) { rateLimits.set(ip, { start: now, n: 1 }); return true; }
if (r.n >= MAX_REQ) return false;
r.n++;
return true;
}
// Clean up stale entries every hour
setInterval(() => {
const now = Date.now();
for (const [ip, r] of rateLimits) { if (now - r.start > WINDOW) rateLimits.delete(ip); }
}, WINDOW);
// ── Middleware ──
app.use(express.json({ limit: "10kb" }));
app.use(express.static(path.join(__dirname, "public")));
// ── Health check ──
app.get("/api/health", (req, res) => {
res.json({ status: "ok", ai: !!ANTHROPIC_API_KEY });
});
// ── AI proxy ──
app.post("/api/chat", async (req, res) => {
if (!ANTHROPIC_API_KEY) return res.status(500).json({ error: "API key not configured" });
const ip = req.headers["x-forwarded-for"] || req.ip;
if (!rateOk(ip)) return res.status(429).json({ error: "Rate limit exceeded. Try again in an hour." });
const { messages } = req.body;
if (!Array.isArray(messages) || !messages.length || messages.length > 20)
return res.status(400).json({ error: "Invalid messages" });
const system = `You are a WGU D308 (ITSW 3034) Mobile Application Development study tutor. You help students pass the Objective Assessment. Your knowledge covers: Android Activity lifecycle, Intents (explicit/implicit), UI layouts (LinearLayout, ConstraintLayout, RecyclerView), Room database (Entity, DAO, Database, Repository), SharedPreferences, Content Providers, BroadcastReceivers, Notifications (channels, AlarmManager), Fragments, Services, APK signing/deployment, and debugging with Logcat/ADB. When answering: be concise, use examples, and relate to the OA exam format. If asked to quiz, generate scenario-based multiple choice questions with explanations. Format code snippets clearly. Stay focused on D308 topics — politely redirect off-topic questions.`;
try {
const r = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: { "Content-Type": "application/json", "x-api-key": ANTHROPIC_API_KEY, "anthropic-version": "2023-06-01" },
body: JSON.stringify({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
system,
messages: messages.map(m => ({ role: m.role === "user" ? "user" : "assistant", content: String(m.content).slice(0, 2000) }))
})
});
if (!r.ok) { console.error("Anthropic:", r.status); return res.status(502).json({ error: "AI service unavailable" }); }
const data = await r.json();
res.json({ reply: data.content?.map(i => i.text || "").filter(Boolean).join("\n") || "No response." });
} catch (e) {
console.error("Proxy error:", e.message);
res.status(500).json({ error: "Internal error" });
}
});
// ── SPA fallback ──
app.get("*", (req, res) => res.sendFile(path.join(__dirname, "public", "index.html")));
app.listen(PORT, () => {
console.log(`D308 Study Lab running on :${PORT}`);
console.log(`AI Tutor: ${ANTHROPIC_API_KEY ? "enabled" : "DISABLED — set ANTHROPIC_API_KEY"}`);
});