const express = require("express"); const path = require("path"); const app = express(); app.use(express.json({ limit: "10mb" })); app.use(express.static(path.join(__dirname, "public"))); const PORT = process.env.PORT || 7860; const ADMIN_SECRET = process.env.ADMIN_SECRET || "admin123"; const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "sacred2024"; // ── SAFE HELPERS — no more .includes() crashes ──────────── const S = v => (v == null ? "" : String(v)); const A = v => (Array.isArray(v) ? v : []); const N = v => (isFinite(v) ? parseInt(v) : 0); // ── Upstash Redis ───────────────────────────────────────── async function rGet(key) { const {UPSTASH_REDIS_REST_URL:url, UPSTASH_REDIS_REST_TOKEN:tok} = process.env; if (!url || !tok) throw new Error("UPSTASH not configured"); const r = await fetch(`${url}/get/${encodeURIComponent(S(key))}`, { headers:{Authorization:`Bearer ${tok}`} }); const d = await r.json(); if (d.result == null) return null; try { return JSON.parse(d.result); } catch { return d.result; } } async function rSet(key, val) { const {UPSTASH_REDIS_REST_URL:url, UPSTASH_REDIS_REST_TOKEN:tok} = process.env; if (!url || !tok) throw new Error("UPSTASH not configured"); await fetch(`${url}/set/${encodeURIComponent(S(key))}`, { method:"POST", headers:{Authorization:`Bearer ${tok}`,"Content-Type":"application/json"}, body: JSON.stringify(typeof val === "string" ? val : JSON.stringify(val)), }); } async function rDel(key) { const {UPSTASH_REDIS_REST_URL:url, UPSTASH_REDIS_REST_TOKEN:tok} = process.env; if (!url || !tok) return; await fetch(`${url}/del/${encodeURIComponent(S(key))}`, { method:"POST", headers:{Authorization:`Bearer ${tok}`} }); } // ── Auth ────────────────────────────────────────────────── function auth(req, res, next) { if (S(req.headers["x-admin-token"]) !== ADMIN_PASSWORD) return res.status(401).json({error:"Unauthorized"}); next(); } // ── Tradition detection ─────────────────────────────────── const TMAP = { quran:"Islam",hadith:"Islam",islam:"Islam",muhammad:"Islam", bible:"Christianity",testament:"Christianity",christian:"Christianity",jesus:"Christianity",gospel:"Christianity",psalm:"Christianity", gita:"Hinduism",geeta:"Hinduism",veda:"Hinduism",hindu:"Hinduism",upanishad:"Hinduism",ramayan:"Hinduism",mahabharata:"Hinduism",krishna:"Hinduism",shiva:"Hinduism", tripitaka:"Buddhism",dhammapada:"Buddhism",buddha:"Buddhism",pali:"Buddhism", torah:"Judaism",talmud:"Judaism",jewish:"Judaism",hebrew:"Judaism", granth:"Sikhism",sikh:"Sikhism",waheguru:"Sikhism",guru:"Sikhism", }; function tradition(name) { const fl = S(name).toLowerCase().replace(/[_\-\.]+/g," "); for (const [k,v] of Object.entries(TMAP)) if (fl.indexOf(k) !== -1) return v; return "Scripture"; } // ── AI providers ────────────────────────────────────────── async function ai_claude(k,m,sys,msgs,mx) { const r = await fetch("https://api.anthropic.com/v1/messages",{method:"POST",headers:{"Content-Type":"application/json","x-api-key":k,"anthropic-version":"2023-06-01"},body:JSON.stringify({model:m||"claude-sonnet-4-20250514",max_tokens:mx,system:sys,messages:msgs})}); const d = await r.json(); if(d.error) throw new Error(d.error.message); return S(d.content&&d.content[0]&&d.content[0].text); } async function ai_openai(k,m,sys,msgs,mx) { const r = await fetch("https://api.openai.com/v1/chat/completions",{method:"POST",headers:{"Content-Type":"application/json","Authorization":`Bearer ${k}`},body:JSON.stringify({model:m||"gpt-4o",max_tokens:mx,messages:[{role:"system",content:sys},...msgs]})}); const d = await r.json(); if(d.error) throw new Error(d.error.message); return S(d.choices&&d.choices[0]&&d.choices[0].message&&d.choices[0].message.content); } async function ai_gemini(k,m,sys,msgs,mx) { const r = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${m||"gemini-2.0-flash"}:generateContent?key=${k}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({systemInstruction:{parts:[{text:sys}]},contents:msgs.map(x=>({role:x.role==="assistant"?"model":"user",parts:[{text:S(x.content)}]})),generationConfig:{maxOutputTokens:mx}})}); const d = await r.json(); if(d.error) throw new Error(d.error.message); return S(d.candidates&&d.candidates[0]&&d.candidates[0].content&&d.candidates[0].content.parts&&d.candidates[0].content.parts[0]&&d.candidates[0].content.parts[0].text); } async function ai_groq(k,m,sys,msgs,mx) { const r = await fetch("https://api.groq.com/openai/v1/chat/completions",{method:"POST",headers:{"Content-Type":"application/json","Authorization":`Bearer ${k}`},body:JSON.stringify({model:m||"llama-3.3-70b-versatile",max_tokens:mx,messages:[{role:"system",content:sys},...msgs]})}); const d = await r.json(); if(d.error) throw new Error(d.error.message); return S(d.choices&&d.choices[0]&&d.choices[0].message&&d.choices[0].message.content); } async function ai_deepseek(k,m,sys,msgs,mx) { const r = await fetch("https://api.deepseek.com/chat/completions",{method:"POST",headers:{"Content-Type":"application/json","Authorization":`Bearer ${k}`},body:JSON.stringify({model:m||"deepseek-chat",max_tokens:mx,messages:[{role:"system",content:sys},...msgs]})}); const d = await r.json(); if(d.error) throw new Error(d.error.message); return S(d.choices&&d.choices[0]&&d.choices[0].message&&d.choices[0].message.content); } async function callAI(sys, msgs, mx=2000) { const providers = [ {name:"Claude", key:process.env.ANTHROPIC_API_KEY, model:process.env.CLAUDE_MODEL, fn:ai_claude}, {name:"OpenAI", key:process.env.OPENAI_API_KEY, model:process.env.OPENAI_MODEL, fn:ai_openai}, {name:"Gemini", key:process.env.GEMINI_API_KEY, model:process.env.GEMINI_MODEL, fn:ai_gemini}, {name:"Groq", key:process.env.GROQ_API_KEY, model:process.env.GROQ_MODEL, fn:ai_groq}, {name:"DeepSeek",key:process.env.DEEPSEEK_API_KEY, model:process.env.DEEPSEEK_MODEL, fn:ai_deepseek}, ].filter(p => S(p.key).trim()); for (const p of providers) { try { const raw=await p.fn(p.key,p.model,sys,msgs,mx); if(raw) return {raw,provider:p.name}; } catch(e) { console.error(p.name+" failed:",e.message); } } return null; } // ── Smart search across ALL books ───────────────────────── function searchChunks(question, books, topN=12) { const q = S(question).toLowerCase(); if (!q) return []; const words = q.split(/\s+/).filter(w=>w.length>2); const long = words.filter(w=>w.length>4); const results = []; for (const bk of Object.values(books||{})) { const txt = S(bk&&bk.text); if (!txt) continue; const bName = S(bk.name)||"Unknown"; const bTrad = S(bk.tradition)||"Scripture"; for (const chunk of txt.split(/\n{2,}/).filter(c=>c.trim().length>40)) { const cl = S(chunk).toLowerCase(); const pm = chunk.match(/\[PAGE (\d+)\]/); const t = chunk.replace(/\[PAGE \d+\]\n?/,"").trim(); if (t.length < 30) continue; let score = 0; for (const w of long) if (cl.indexOf(w) !== -1) score += 3; for (const w of words) if (cl.indexOf(w) !== -1) score += 1; if (q.length>10 && cl.indexOf(q.substring(0,15)) !== -1) score += 10; if (score > 0) results.push({ book: bName, tradition: bTrad, text: t.substring(0,600), fullChunk: chunk, page: S(pm&&pm[1])||"—", score, }); } } results.sort((a,b)=>b.score-a.score); const seen={}, top=[]; for (const r of results) { seen[r.book] = (seen[r.book]||0); if (seen[r.book]<3) { top.push(r); seen[r.book]++; } if (top.length>=topN) break; } return top; } function textFallback(question, books) { const top = searchChunks(question, books, 5); if (!top.length) return {answer:"📚 No relevant passages found. Admin should upload related books.",source:"—",fallback:true}; return { answer: "📚 Relevant passages:\n\n"+top.map(r=>`**📖 ${r.book}** (Page ${r.page})\n"${r.text.substring(0,350)}..."`).join("\n\n---\n\n"), source: top.map(r=>`${r.book} p.${r.page}`).join(", "), fallback:true, passages:top, }; } // ── Load books from Redis ───────────────────────────────── async function loadBooks() { try { const meta = await rGet("books-meta"); if (!meta) return {}; const books = {}; for (const name of A(meta.books)) { try { const d = await rGet("book-"+S(name)); if (d && S(d.text)) books[S(name)] = d; } catch {} } return books; } catch(e) { console.error("loadBooks:",e.message); return {}; } } // ── Store book helper ───────────────────────────────────── async function storeBook({bookName, text, totalPages, wordCount, fileId, modifiedTime}) { const name = S(bookName).trim().replace(/[_\-]+/g," ")||"Unknown Book"; const txt = S(text).trim(); if (!txt || txt.length < 30) throw new Error(`No text extracted from "${name}"`); const trad = tradition(name); const safe = txt.length>4000000 ? txt.substring(0,4000000) : txt; await rSet("book-"+name, {name,tradition:trad,text:safe,totalPages:N(totalPages),wordCount:N(wordCount),syncedAt:new Date().toISOString()}); let meta = {books:[],synced:{},totalBooks:0}; try { meta = (await rGet("books-meta"))||meta; } catch {} meta.books = A(meta.books); meta.synced = meta.synced||{}; if (meta.books.indexOf(name)===-1) meta.books.push(name); if (fileId) meta.synced[S(fileId)] = S(modifiedTime); meta.totalBooks = meta.books.length; meta.lastSync = new Date().toISOString(); await rSet("books-meta", meta); return {name, tradition:trad, totalPages:N(totalPages), wordCount:N(wordCount)}; } // ── Admin page ──────────────────────────────────────────── app.get("/admin-"+ADMIN_SECRET, (req,res) => res.sendFile(path.join(__dirname,"public","admin.html"))); // ── Chat ────────────────────────────────────────────────── app.post("/api/chat", async (req,res) => { try { const question = S(req.body&&req.body.question).trim(); const messages = A(req.body&&req.body.messages); const lang = S(req.body&&req.body.lang)||"en"; if (!question) return res.status(400).json({error:"Question required"}); const langInstr = {hi:"Answer in Hindi.",ur:"Answer in Urdu.",ar:"Answer in Arabic.",en:"Answer in English."}[lang]||"Answer in English."; const books = await loadBooks(); const bookCount = Object.keys(books).length; const chunks = bookCount>0 ? searchChunks(question,books,12) : []; let ctx = ""; if (chunks.length>0) { const byBook={}; for (const c of chunks) { if(!byBook[c.book]) byBook[c.book]=[]; byBook[c.book].push(c); } ctx = Object.entries(byBook).map(([n,cs])=>`=== ${n} ===\n`+cs.map(c=>S(c.fullChunk)).join("\n\n")).join("\n\n"); } const bookList = bookCount>0 ? Object.values(books).map(b=>`"${S(b.name)}" (${S(b.tradition)||"Scripture"})`).join(", ") : "none"; const system = `You are a scholar with access to a sacred text library. ${langInstr} Library (${bookCount} books): ${bookList} ${chunks.length>0 ? "RELEVANT PASSAGES:\n"+ctx+"\nQuote exact text with page numbers." : bookCount>0 ? "No keyword match. Answer from knowledge." : "No books uploaded. Answer from general knowledge."} Rules: cite book+page always. Translate if needed. Add commentary. Return ONLY valid JSON: {"answer":"text","quotes":[{"book":"name","tradition":"religion","original":"text","page":"Page X","translation":"translation","commentary":"notes"}],"source":"Book p.X"}`; const ai = await callAI(system, [...messages.slice(-4),{role:"user",content:question}], 2000); if (ai) { let parsed; try { parsed = JSON.parse(S(ai.raw).replace(/^```json\s*/i,"").replace(/^```\s*/i,"").replace(/\s*```$/i,"").trim()); } catch { parsed = {answer:S(ai.raw),quotes:[],source:"AI"}; } return res.json({...parsed,provider:ai.provider,fallback:false,bookCount}); } return res.json({...textFallback(question,books),bookCount}); } catch(e) { console.error("chat:",e); res.status(500).json({error:e.message}); } }); // ── Admin login ─────────────────────────────────────────── app.post("/api/admin/login", (req,res) => { if (S(req.body&&req.body.password)===ADMIN_PASSWORD) return res.json({success:true,token:ADMIN_PASSWORD}); res.status(401).json({error:"Wrong password"}); }); // ── Admin status ────────────────────────────────────────── app.get("/api/admin/status", auth, async (req,res) => { try { let meta={books:[],lastSync:null,totalBooks:0}; try { meta=(await rGet("books-meta"))||meta; } catch {} meta.books = A(meta.books); const bookDetails=[]; for (const name of meta.books) { try { const bk=await rGet("book-"+S(name)); if(bk) bookDetails.push({name:S(bk.name),tradition:S(bk.tradition)||"Scripture",totalPages:N(bk.totalPages),wordCount:N(bk.wordCount),uploadedAt:S(bk.syncedAt)}); } catch {} } res.json({meta,bookDetails,config:{ hasClaude: !!process.env.ANTHROPIC_API_KEY, hasOpenAI: !!process.env.OPENAI_API_KEY, hasGemini: !!process.env.GEMINI_API_KEY, hasGroq: !!process.env.GROQ_API_KEY, hasDeepSeek:!!process.env.DEEPSEEK_API_KEY, hasUpstash: !!(process.env.UPSTASH_REDIS_REST_URL&&process.env.UPSTASH_REDIS_REST_TOKEN), hasDrive: !!(process.env.GOOGLE_DRIVE_API_KEY&&process.env.GOOGLE_DRIVE_FOLDER_ID), }}); } catch(e) { res.status(500).json({error:e.message}); } }); // ── Upload (browser-parsed) ─────────────────────────────── app.post("/api/admin/upload-book", auth, async (req,res) => { try { const result = await storeBook(req.body||{}); res.json({success:true,...result}); } catch(e) { res.status(400).json({error:e.message}); } }); // ── Google Drive: list files ────────────────────────────── app.post("/api/admin/drive-list", auth, async (req,res) => { const KEY=process.env.GOOGLE_DRIVE_API_KEY, FOLDER=process.env.GOOGLE_DRIVE_FOLDER_ID; if (!KEY||!FOLDER) return res.status(400).json({error:"Set GOOGLE_DRIVE_API_KEY and GOOGLE_DRIVE_FOLDER_ID in HF Secrets"}); try { const url=`https://www.googleapis.com/drive/v3/files?q='${FOLDER}'+in+parents+and+mimeType='application/pdf'+and+trashed=false&fields=files(id,name,modifiedTime,size)&key=${KEY}&pageSize=100`; const r=await fetch(url); if (!r.ok) throw new Error("Drive API HTTP "+r.status); const d=await r.json(); if (d.error) throw new Error("Drive API: "+S(d.error.message||d.error)); // Load synced map safely — triple-guard against any null/undefined let synced = {}; try { const meta = await rGet("books-meta"); if (meta && typeof meta === "object" && meta.synced && typeof meta.synced === "object") { synced = meta.synced; } } catch {} // Filter out any null/undefined entries from Drive response const rawFiles = Array.isArray(d.files) ? d.files.filter(f => f && S(f.id)) : []; const files = rawFiles.map(f => { const id = S(f.id); const name = S(f.name); const modifiedTime = S(f.modifiedTime); const bookName = name.replace(/\.pdf$/i,"").trim().replace(/[_\-]+/g," ")||"Unknown"; // Safe synced check — never access property on undefined let alreadySynced = false; try { alreadySynced = (synced[id] === modifiedTime && modifiedTime !== ""); } catch {} return { id, name, bookName, modifiedTime, size:N(f.size), alreadySynced }; }); res.json({success:true, files, driveApiKey:KEY}); } catch(e) { console.error("drive-list error:", e.message); res.status(500).json({error:e.message}); } }); // ── Google Drive: store parsed text ────────────────────── app.post("/api/admin/drive-store", auth, async (req,res) => { try { const result = await storeBook(req.body||{}); res.json({success:true,...result}); } catch(e) { res.status(400).json({error:e.message}); } }); // ── Delete book ─────────────────────────────────────────── app.delete("/api/admin/delete-book", auth, async (req,res) => { try { const name=S(req.body&&req.body.bookName).trim(); if(!name) return res.status(400).json({error:"bookName required"}); await rDel("book-"+name); let meta={books:[]}; try { meta=(await rGet("books-meta"))||meta; } catch {} meta.books=A(meta.books).filter(b=>b!==name); meta.totalBooks=meta.books.length; await rSet("books-meta",meta); res.json({success:true}); } catch(e) { res.status(500).json({error:e.message}); } }); // ── Clear all ───────────────────────────────────────────── app.delete("/api/admin/clear-all", auth, async (req,res) => { try { let meta={books:[]}; try { meta=(await rGet("books-meta"))||meta; } catch {} for (const n of A(meta.books)) { try { await rDel("book-"+S(n)); } catch {} } await rSet("books-meta",{books:[],totalBooks:0,lastSync:null,synced:{}}); res.json({success:true}); } catch(e) { res.status(500).json({error:e.message}); } }); app.get("*",(req,res)=>res.sendFile(path.join(__dirname,"public","index.html"))); app.listen(PORT,"0.0.0.0",()=>{ console.log("Divine Wisdom running on port "+PORT); console.log("Admin: /admin-"+ADMIN_SECRET); });