From e2cd34f9cb3202373b99df78f51068c4c24ba361 Mon Sep 17 00:00:00 2001
From: StudentHub
Date: Sun, 8 Nov 2026 21:19:02 +0530
Subject: [PATCH 01/95] Fix chatbot active listing retrieval, nearby evidence
and request limits
---
supabase/functions/studenthubhelp-chat/index.ts | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/supabase/functions/studenthubhelp-chat/index.ts b/supabase/functions/studenthubhelp-chat/index.ts
index 97d93ce28..796fcedff 100644
--- a/supabase/functions/studenthubhelp-chat/index.ts
+++ b/supabase/functions/studenthubhelp-chat/index.ts
@@ -13,8 +13,8 @@ const H={"Access-Control-Allow-Origin":"*","Access-Control-Allow-Headers":"autho
const SRC=[["hostels","hostel"],["tiffins","tiffin"],["libraries","library"],["cafes","cafe"],["bookstores","bookstore"]] as const;
const n=(v:any)=>String(v??"").toLowerCase().normalize("NFKC").replace(/[^a-z0-9\u0900-\u097f]+/gi," ").replace(/\s+/g," ").trim();
const num=(v:any)=>{const x=Number(String(v??"").replace(/,/g,""));return Number.isFinite(x)?x:undefined};
-function rateKey(req:Request){const f=req.headers.get("x-forwarded-for")||req.headers.get("cf-connecting-ip")||"unknown";return f.split(",")[0].trim().slice(0,100)||"unknown"}
-function allowed(req:Request){const key=rateKey(req),now=Date.now(),old=rateMap.get(key);if(!old||now-old.start>=RATE_WINDOW_MS){rateMap.set(key,{start:now,count:1});return true}if(old.count>=RATE_MAX)return false;old.count++;return true}
+function rateKey(req:Request){const f=req.headers.get("cf-connecting-ip")||req.headers.get("x-real-ip")||"unknown";return f.trim().slice(0,100)||"unknown"}
+function pruneRateMap(map:Map,now:number){if(map.size<5000)return;for(const [key,value] of map){if(now-value.start>Math.max(RATE_WINDOW_MS,OWNER_LEAD_WINDOW_MS))map.delete(key);if(map.size<4000)break}}\nfunction allowed(req:Request){const key=rateKey(req),now=Date.now(),old=rateMap.get(key);pruneRateMap(rateMap,now);if(!old||now-old.start>=RATE_WINDOW_MS){rateMap.set(key,{start:now,count:1});return true}if(old.count>=RATE_MAX)return false;old.count++;return true}
async function db(path:string):Promise{if(!U||!K)throw Error("database configuration missing");const r=await fetch(U+path,{headers:{apikey:K,Authorization:`Bearer ${K}`,"Content-Type":"application/json"}});if(!r.ok)throw Error(`database ${r.status}`);return await r.json() as T}
const LANDMARKS=[{key:"allen",label:"Allen Career Institute Sikar",aliases:["allen","allen sikar","allen coaching","allen career","allen sanskar","allen piprali"],city:"Sikar",lat:27.62624,lng:75.17721},{key:"clc",label:"CLC Sikar",aliases:["clc","clc sikar"],city:"Sikar",lat:27.6206291,lng:75.1616672},{key:"vibrant",label:"Vibrant Academy Sikar",aliases:["vibrant","vibrant coaching","vibrant academy","vibrant academy ssc","vibrant ssc"],city:"Sikar"},{key:"matrix",label:"Matrix Coaching Sikar",aliases:["matrix","matrix academy","matrix coaching","matrix sikar"],city:"Sikar"},{key:"gurukripa",label:"Gurukripa Coaching Sikar",aliases:["gurukripa","guru kripa","gurukripa coaching","gurukripa sikar"],city:"Sikar"}];
function city(s:string){const x=n(s),m:[string,string][]=[["sikar","Sikar"],["piprali","Sikar"],["पिपराली","Sikar"],["सिकर","Sikar"],["kota","Kota"],["talwandi","Kota"],["jaipur","Jaipur"],["ajmer","Ajmer"],["bikaner","Bikaner"],["jodhpur","Jodhpur"],["udaipur","Udaipur"],["alwar","Alwar"],["delhi","Delhi"],["ahmedabad","Ahmedabad"],["lucknow","Lucknow"],["hyderabad","Hyderabad"],["pune","Pune"]];return m.find(([a])=>x.includes(n(a)))?.[1]||""}
@@ -26,7 +26,7 @@ function coords(p:any){const lat=Number(p.latitude??p.lat??p.location_latitude??
function distanceKm(a:any,b:any){const A=coords(a),B=coords(b);if(!A||!B)return undefined;const r=(v:number)=>v*Math.PI/180,dLat=r(B.lat-A.lat),dLon=r(B.lng-A.lng),h=Math.sin(dLat/2)**2+Math.cos(r(A.lat))*Math.cos(r(B.lat))*Math.sin(dLon/2)**2;return 6371*2*Math.atan2(Math.sqrt(h),Math.sqrt(1-h))}
function distanceToLandmark(p:any,l:any){const A=coords(p),B=l?{latitude:l.lat,longitude:l.lng}:null;if(!A||!B)return undefined;return distanceKm(A,B)}
function searchText(p:any){return n([p.name,p.title,p.business_name,p.service_name,p.city,p.area,p.service_area,p.address,p.landmark,p.description,p.facilities,p.amenities].filter(Boolean).join(" "))}
-async function direct(type:string,ct?:string){const table=SRC.find(x=>x[1]===type)?.[0];if(!table)return [];const rows=await db(`/rest/v1/${table}?select=*&limit=1000`);return (Array.isArray(rows)?rows:[]).map(p=>({...p,_type:type})).filter(p=>!ct||n(p.city||"").includes(n(ct)))}
+async function direct(type:string,ct?:string){const table=SRC.find(x=>x[1]===type)?.[0];if(!table)return [];const all:any[]=[];let offset=0;const pageSize=500;while(true){const params=new URLSearchParams({select:"*",status:"eq.active",limit:String(pageSize),offset:String(offset)});if(ct)params.set("city",`ilike.*${ct.replace(/[%,()]/g,"")}*`);const rows=await db(`/rest/v1/${table}?${params.toString()}`);const page=Array.isArray(rows)?rows:[];all.push(...page.map(p=>({...p,_type:type})));if(page.length=10000)break}return all.filter(p=>!ct||n(p.city||"").includes(n(ct)))}
async function multiDirect(types:string[],ct?:string){const unique=[...new Set(types.filter(Boolean))];const groups=await Promise.all(unique.map(type=>direct(type,ct)));return groups.flat()}
function genderWanted(s:string){const x=n(s);if(/\b(girls|girl|female|ladki|ladkiyo|ladkiyon|महिला|लड़क|लड़की|girls hostel)\b/i.test(x))return "girls";if(/\b(boys|boy|male|ladke|ladkon|लड़के|लड़का|boys hostel)\b/i.test(x))return "boys";return ""}
function areaWanted(s:string){const x=n(s);if(/nawalgarh road|nawalgarh rd|नवलगढ़ रोड/i.test(x))return "nawalgarh";if(/piprali road|piprali rd|पिपराली रोड/i.test(x))return "piprali";return ""}
@@ -251,10 +251,13 @@ Deno.serve(async(req:Request)=>{
try{
if(!allowed(req))return new Response(JSON.stringify({error:"Too many requests. Please try again shortly."}),{status:429,headers:{...H,"Retry-After":"60"}});
const contentLength=Number(req.headers.get("content-length")||0);if(contentLength>32768)return new Response(JSON.stringify({error:"Request is too large."}),{status:413,headers:H});
- const b=await req.json(),msg=String(b?.message||"").trim().slice(0,1500),history=Array.isArray(b?.history)?b.history.slice(-8).map((x:any)=>({role:String(x?.role||"").slice(0,20),text:String(x?.text||"").slice(0,1500)})):[];
+ const rawBody=await req.text();if(new TextEncoder().encode(rawBody).byteLength>32768)return new Response(JSON.stringify({error:"Request is too large."}),{status:413,headers:H});
+ let b:any;try{b=JSON.parse(rawBody)}catch{return new Response(JSON.stringify({error:"Invalid JSON request."}),{status:400,headers:H})}
+ const msg=String(b?.message||"").trim().slice(0,1500),history=Array.isArray(b?.history)?b.history.slice(-8).map((x:any)=>({role:String(x?.role||"").slice(0,20),text:String(x?.text||"").slice(0,1500)})):[];
if(!msg)return new Response(JSON.stringify({error:"Message is required"}),{status:400,headers:H});
const sessionId=String(b?.sessionId||crypto.randomUUID());
if(String(b?.action||"")==="submit_owner_lead"){
+ if(!ownerLeadAllowed(req))return new Response(JSON.stringify({error:"Too many owner requests. Please try again later."}),{status:429,headers:{...H,"Retry-After":"600"}});
const lead=b?.ownerLead||{};
const name=String(lead.name||"").trim().replace(/\s+/g," ").slice(0,80);
const phone=String(lead.phone||"").trim().slice(0,20);
@@ -389,7 +392,7 @@ const ownerDetailsIntent=/(?:owner|malik|maalik|proprietor|manager).{0,35}(?:det
if(u.budgetMax!==null||u.budgetMin!==null)rows=rows.filter(p=>{const v=budgetValue(p);if(v===undefined)return false;return (u.budgetMax===null||v<=u.budgetMax)&&(u.budgetMin===null||v>=u.budgetMin)});
if(u.facilities?.length)rows=rows.filter(p=>matchesFacility(p,u.facilities));
const landmark=u.landmark?findLandmark(u.landmark)||LANDMARKS.find(l=>l.key===u.landmark):findLandmark(near||"");
- if(landmark){for(const p of rows){const d=distanceToLandmark(p,landmark);if(d!==undefined)p._distanceKm=d}rows=rows.filter(p=>n(p.city||"")===n(landmark.city)&&(p._distanceKm===undefined||p._distanceKm<=10))}
+ if(landmark){for(const p of rows){const d=distanceToLandmark(p,landmark);if(d!==undefined)p._distanceKm=d}rows=rows.filter(p=>n(p.city||"")===n(landmark.city)&&((p._distanceKm!==undefined&&p._distanceKm<=10)||searchText(p).includes(n(landmark.key))))}
else if(near){const q=n(near);const exact=rows.filter(p=>searchText(p).includes(q));if(exact.length)rows=exact;else if(!wantedArea)rows=[]}
if(nearCats.length){const secondaryRows=await multiDirect(nearCats,ct);for(const p of rows){let best=Infinity,hasCoords=false;for(const q of secondaryRows){const d=distanceKm(p,q);if(d!==undefined){hasCoords=true;best=Math.min(best,d)}else if(p.area&&searchText(q).includes(n(p.area)))best=Math.min(best,1)}if(best<=3){p._nearbyDistanceKm=best;p._nearbyVerified=hasCoords;p._nearbyAreaMatch=!hasCoords}}rows=rows.filter(p=>p._nearbyDistanceKm!==undefined)}
rows=rows.filter(p=>!shownIds.has(String(p.id)));
From ea614ba262bcb7d252b668dd343c00a6d9d45c45 Mon Sep 17 00:00:00 2001
From: StudentHub
Date: Sun, 8 Nov 2026 21:19:08 +0530
Subject: [PATCH 02/95] Remove duplicate chatbot admin controls instance
---
admin/src/components/IntelligenceViews.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/admin/src/components/IntelligenceViews.tsx b/admin/src/components/IntelligenceViews.tsx
index 8da9732c7..6972dadca 100644
--- a/admin/src/components/IntelligenceViews.tsx
+++ b/admin/src/components/IntelligenceViews.tsx
@@ -1272,7 +1272,7 @@ export const IntelligenceViews: React.FC = ({
-
+
);
From d39861fd4e57d8471dc82e02a0ca3f2496cee249 Mon Sep 17 00:00:00 2001
From: StudentHub
Date: Sun, 8 Nov 2026 21:19:37 +0530
Subject: [PATCH 03/95] Connect chatbot admin controls to live Edge Function
---
.../functions/studenthubhelp-chat/index.ts | 33 +++++++++++--------
1 file changed, 20 insertions(+), 13 deletions(-)
diff --git a/supabase/functions/studenthubhelp-chat/index.ts b/supabase/functions/studenthubhelp-chat/index.ts
index 796fcedff..fede20852 100644
--- a/supabase/functions/studenthubhelp-chat/index.ts
+++ b/supabase/functions/studenthubhelp-chat/index.ts
@@ -16,6 +16,10 @@ const num=(v:any)=>{const x=Number(String(v??"").replace(/,/g,""));return Number
function rateKey(req:Request){const f=req.headers.get("cf-connecting-ip")||req.headers.get("x-real-ip")||"unknown";return f.trim().slice(0,100)||"unknown"}
function pruneRateMap(map:Map,now:number){if(map.size<5000)return;for(const [key,value] of map){if(now-value.start>Math.max(RATE_WINDOW_MS,OWNER_LEAD_WINDOW_MS))map.delete(key);if(map.size<4000)break}}\nfunction allowed(req:Request){const key=rateKey(req),now=Date.now(),old=rateMap.get(key);pruneRateMap(rateMap,now);if(!old||now-old.start>=RATE_WINDOW_MS){rateMap.set(key,{start:now,count:1});return true}if(old.count>=RATE_MAX)return false;old.count++;return true}
async function db(path:string):Promise{if(!U||!K)throw Error("database configuration missing");const r=await fetch(U+path,{headers:{apikey:K,Authorization:`Bearer ${K}`,"Content-Type":"application/json"}});if(!r.ok)throw Error(`database ${r.status}`);return await r.json() as T}
+type ChatbotRuntimeConfig={enabled:boolean;ai_rerank_enabled:boolean;lead_capture_enabled:boolean;comparison_enabled:boolean;budget_planner_enabled:boolean;max_candidates:number;max_results:number;max_output_tokens:number;temperature:number;maintenance_message:string};
+const DEFAULT_RUNTIME_CONFIG:ChatbotRuntimeConfig={enabled:true,ai_rerank_enabled:true,lead_capture_enabled:true,comparison_enabled:true,budget_planner_enabled:true,max_candidates:40,max_results:8,max_output_tokens:900,temperature:0.2,maintenance_message:""};
+let runtimeConfigCache:{value:ChatbotRuntimeConfig;expiresAt:number}|null=null;
+async function getRuntimeConfig():Promise{if(runtimeConfigCache&&runtimeConfigCache.expiresAt>Date.now())return runtimeConfigCache.value;try{const rows=await db("/rest/v1/chatbot_admin_config?select=*&id=eq.1&limit=1");const row=rows?.[0]||{};const value:ChatbotRuntimeConfig={enabled:row.enabled!==false,ai_rerank_enabled:row.ai_rerank_enabled!==false,lead_capture_enabled:row.lead_capture_enabled!==false,comparison_enabled:row.comparison_enabled!==false,budget_planner_enabled:row.budget_planner_enabled!==false,max_candidates:Math.max(10,Math.min(100,Number(row.max_candidates)||40)),max_results:Math.max(1,Math.min(20,Number(row.max_results)||8)),max_output_tokens:Math.max(200,Math.min(3000,Number(row.max_output_tokens)||900)),temperature:Math.max(0,Math.min(1,Number.isFinite(Number(row.temperature))?Number(row.temperature):0.2)),maintenance_message:String(row.maintenance_message||"").slice(0,500)};runtimeConfigCache={value,expiresAt:Date.now()+15000};return value}catch(e){console.error("chatbot admin config unavailable; using safe defaults",e);return DEFAULT_RUNTIME_CONFIG}}
const LANDMARKS=[{key:"allen",label:"Allen Career Institute Sikar",aliases:["allen","allen sikar","allen coaching","allen career","allen sanskar","allen piprali"],city:"Sikar",lat:27.62624,lng:75.17721},{key:"clc",label:"CLC Sikar",aliases:["clc","clc sikar"],city:"Sikar",lat:27.6206291,lng:75.1616672},{key:"vibrant",label:"Vibrant Academy Sikar",aliases:["vibrant","vibrant coaching","vibrant academy","vibrant academy ssc","vibrant ssc"],city:"Sikar"},{key:"matrix",label:"Matrix Coaching Sikar",aliases:["matrix","matrix academy","matrix coaching","matrix sikar"],city:"Sikar"},{key:"gurukripa",label:"Gurukripa Coaching Sikar",aliases:["gurukripa","guru kripa","gurukripa coaching","gurukripa sikar"],city:"Sikar"}];
function city(s:string){const x=n(s),m:[string,string][]=[["sikar","Sikar"],["piprali","Sikar"],["पिपराली","Sikar"],["सिकर","Sikar"],["kota","Kota"],["talwandi","Kota"],["jaipur","Jaipur"],["ajmer","Ajmer"],["bikaner","Bikaner"],["jodhpur","Jodhpur"],["udaipur","Udaipur"],["alwar","Alwar"],["delhi","Delhi"],["ahmedabad","Ahmedabad"],["lucknow","Lucknow"],["hyderabad","Hyderabad"],["pune","Pune"]];return m.find(([a])=>x.includes(n(a)))?.[1]||""}
function findLandmark(s:string){const x=n(s);for(const l of LANDMARKS)if(l.aliases.some(a=>x.includes(n(a))))return l;return undefined}
@@ -111,7 +115,7 @@ function strictLocation(s:string){
if(d.city||d.locality||d.area||d.landmark)return d;
const c=city(s);return c?{city:c}:{};
}
-async function aiUnderstand(msg:string,history:any[]){
+async function aiUnderstand(msg:string,history:any[],cfg:ChatbotRuntimeConfig=DEFAULT_RUNTIME_CONFIG){
const fallback=offlineIntent(msg,history);if(!GEMINI)return fallback;
try{
const h=history.slice(-8).map(x=>`${x.role==="user"?"USER":"ASSISTANT"}: ${String(x.text||"").slice(0,1200)}`).join("\n");
@@ -133,7 +137,7 @@ CURRENT MESSAGE:
${msg}
RECENT CONVERSATION:
${h}`;
- const r=await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent`,{method:"POST",headers:{"Content-Type":"application/json","x-goog-api-key":GEMINI},body:JSON.stringify({contents:[{role:"user",parts:[{text:prompt}]}],generationConfig:{responseMimeType:"application/json",maxOutputTokens:700,thinkingConfig:{thinkingLevel:"medium"}}})});
+ const r=await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent`,{method:"POST",headers:{"Content-Type":"application/json","x-goog-api-key":GEMINI},body:JSON.stringify({contents:[{role:"user",parts:[{text:prompt}]}],generationConfig:{responseMimeType:"application/json",maxOutputTokens:Math.min(900,cfg.max_output_tokens),temperature:cfg.temperature,thinkingConfig:{thinkingLevel:"medium"}}})});
if(!r.ok)return fallback;const j=await r.json(),txt=j?.candidates?.[0]?.content?.parts?.map((p:any)=>p.text||"").join("")||"";return normalizeIntent(safeJson(txt));
}catch{return fallback}
}
@@ -159,7 +163,7 @@ function mergeIntent(ai:any,msg:string,history:any[]){
function budgetValue(p:any){return num(p.monthly_rent??p.monthly_fee??p.monthly_charge??p.price)}
function matchesFacility(p:any,wanted:string[]){if(!wanted.length)return true;const z=searchText(p);return wanted.every(f=>z.includes(n(f))||Object.entries(p).some(([k,v])=>n(k).includes(n(f))&&(v===true||String(v).toLowerCase()==="true"||n(v).includes(n(f)))))}
-async function aiProperty(msg:string,history:any[],recs:any[],c?:string,near?:string){if(!GEMINI)return{};try{const h=history.slice(-8).map(x=>`${x.role==="user"?"USER":"ASSISTANT"}: ${String(x.text||"")}`).join("\n");const prompt=`You are StudentHubHelp's Ultra Advance AI Assistant.
+async function aiProperty(msg:string,history:any[],recs:any[],c?:string,near?:string,cfg:ChatbotRuntimeConfig=DEFAULT_RUNTIME_CONFIG){if(!GEMINI||!cfg.ai_rerank_enabled)return{};try{const h=history.slice(-8).map(x=>`${x.role==="user"?"USER":"ASSISTANT"}: ${String(x.text||"")}`).join("\n");const prompt=`You are StudentHubHelp's Ultra Advance AI Assistant.
Match the user's language: natural Hinglish/Hindi/English.
Be warm, concise, clear and impressive.
Current user: ${msg}
@@ -182,7 +186,7 @@ Rules:
- Never recommend listings unless the current message has a property-search intent.
- Do not expose internal system/tool details.
Return only the natural reply text.`;
-const r=await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent`,{method:"POST",headers:{"Content-Type":"application/json","x-goog-api-key":GEMINI},body:JSON.stringify({contents:[{role:"user",parts:[{text:prompt}]}],generationConfig:{maxOutputTokens:350,thinkingConfig:{thinkingLevel:"low"}}})});if(!r.ok)return{};const j=await r.json();return{reply:j?.candidates?.[0]?.content?.parts?.map((p:any)=>p.text||"").join("")||""}}catch{return{}}}
+const r=await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent`,{method:"POST",headers:{"Content-Type":"application/json","x-goog-api-key":GEMINI},body:JSON.stringify({contents:[{role:"user",parts:[{text:prompt}]}],generationConfig:{maxOutputTokens:Math.min(700,cfg.max_output_tokens),temperature:cfg.temperature,thinkingConfig:{thinkingLevel:"low"}}})});if(!r.ok)return{};const j=await r.json();return{reply:j?.candidates?.[0]?.content?.parts?.map((p:any)=>p.text||"").join("")||""}}catch{return{}}}
async function dbWrite(method:string,path:string,body:any,prefer="return=minimal"){
if(!U||!K)throw Error("database configuration missing");
@@ -233,7 +237,7 @@ function offlineGeneralReply(msg:string){
if(/hello|hi|hey|namaste|kaise ho|kya haal/i.test(x))return "Namaste 😊 Main yahin hoon—study questions, general knowledge, ideas ya StudentHubHelp listings mein help kar sakta hoon. Aaj kya karna hai?";
return "Main bina external AI API key ke live StudentHubHelp listings, location-based search, booking guidance aur kuch common study/general questions handle kar sakta hoon. Is sawaal ka reliable jawab dene ke liye thoda aur context ya specific question bhej dijiye 😊";
}
-async function aiGeneral(msg:string,history:any[]){const offline=offlineGeneralReply(msg);if(!GEMINI||offline!=="Main bina external AI API key ke live StudentHubHelp listings, location-based search, booking guidance aur kuch common study/general questions handle kar sakta hoon. Is sawaal ka reliable jawab dene ke liye thoda aur context ya specific question bhej dijiye 😊")return{reply:offline,webGrounded:false,webSources:[]};try{const h=history.slice(-8).map(x=>({role:x.role==="assistant"?"model":"user",parts:[{text:String(x.text||"").slice(0,1500)}]}));const system=`You are StudentHubHelp's Ultra Advance AI Assistant for students.
+async function aiGeneral(msg:string,history:any[],cfg:ChatbotRuntimeConfig=DEFAULT_RUNTIME_CONFIG){const offline=offlineGeneralReply(msg);if(!GEMINI||offline!=="Main bina external AI API key ke live StudentHubHelp listings, location-based search, booking guidance aur kuch common study/general questions handle kar sakta hoon. Is sawaal ka reliable jawab dene ke liye thoda aur context ya specific question bhej dijiye 😊")return{reply:offline,webGrounded:false,webSources:[]};try{const h=history.slice(-8).map(x=>({role:x.role==="assistant"?"model":"user",parts:[{text:String(x.text||"").slice(0,1500)}]}));const system=`You are StudentHubHelp's Ultra Advance AI Assistant for students.
You are a natural conversational, study and general knowledge assistant.
Match the user's language and tone: Hindi, Hinglish or English.
You can discuss normal life, boredom, tiredness, motivation, hobbies and casual topics in a friendly respectful way.
@@ -243,7 +247,7 @@ Do not claim StudentHubHelp property facts here. Property data belongs to the se
Do not pretend to know personal facts about the user.
Keep normal chat concise and human; for study questions be clear and useful.
Current message: ${msg}`;
-const body:any={contents:[...h,{role:"user",parts:[{text:system}]}],generationConfig:{maxOutputTokens:700,thinkingConfig:{thinkingLevel:"medium"}}};if(wantsCurrentWeb(msg))body.tools=[{google_search:{}}];const r=await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent`,{method:"POST",headers:{"Content-Type":"application/json","x-goog-api-key":GEMINI},body:JSON.stringify(body)});if(!r.ok)return{reply:offlineGeneralReply(msg),webGrounded:false,webSources:[]};const j=await r.json();const reply=j?.candidates?.[0]?.content?.parts?.map((p:any)=>p.text||"").join("")||"";const chunks=j?.candidates?.[0]?.groundingMetadata?.groundingChunks||[];const sources=chunks.map((x:any)=>x?.web?.uri&&{url:x.web.uri,title:x.web.title||x.web.uri}).filter(Boolean).slice(0,5);return{reply:reply||offlineGeneralReply(msg),webGrounded:sources.length>0,webSources:sources}}catch{return{reply:offlineGeneralReply(msg),webGrounded:false,webSources:[]}}}
+const body:any={contents:[...h,{role:"user",parts:[{text:system}]}],generationConfig:{maxOutputTokens:cfg.max_output_tokens,temperature:cfg.temperature,thinkingConfig:{thinkingLevel:"medium"}}};if(wantsCurrentWeb(msg))body.tools=[{google_search:{}}];const r=await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent`,{method:"POST",headers:{"Content-Type":"application/json","x-goog-api-key":GEMINI},body:JSON.stringify(body)});if(!r.ok)return{reply:offlineGeneralReply(msg),webGrounded:false,webSources:[]};const j=await r.json();const reply=j?.candidates?.[0]?.content?.parts?.map((p:any)=>p.text||"").join("")||"";const chunks=j?.candidates?.[0]?.groundingMetadata?.groundingChunks||[];const sources=chunks.map((x:any)=>x?.web?.uri&&{url:x.web.uri,title:x.web.title||x.web.uri}).filter(Boolean).slice(0,5);return{reply:reply||offlineGeneralReply(msg),webGrounded:sources.length>0,webSources:sources}}catch{return{reply:offlineGeneralReply(msg),webGrounded:false,webSources:[]}}}
Deno.serve(async(req:Request)=>{
if(req.method==="OPTIONS")return new Response("ok",{headers:H});
@@ -256,7 +260,10 @@ Deno.serve(async(req:Request)=>{
const msg=String(b?.message||"").trim().slice(0,1500),history=Array.isArray(b?.history)?b.history.slice(-8).map((x:any)=>({role:String(x?.role||"").slice(0,20),text:String(x?.text||"").slice(0,1500)})):[];
if(!msg)return new Response(JSON.stringify({error:"Message is required"}),{status:400,headers:H});
const sessionId=String(b?.sessionId||crypto.randomUUID());
+ const cfg=await getRuntimeConfig();
+ if(!cfg.enabled)return new Response(JSON.stringify({reply:cfg.maintenance_message||"StudentHubHelp AI assistant is temporarily unavailable. Please try again shortly.",grounded:false,maintenance:true}),{status:503,headers:H});
if(String(b?.action||"")==="submit_owner_lead"){
+ if(!cfg.lead_capture_enabled)return new Response(JSON.stringify({error:"Owner listing requests are temporarily disabled."}),{status:403,headers:H});
if(!ownerLeadAllowed(req))return new Response(JSON.stringify({error:"Too many owner requests. Please try again later."}),{status:429,headers:{...H,"Retry-After":"600"}});
const lead=b?.ownerLead||{};
const name=String(lead.name||"").trim().replace(/\s+/g," ").slice(0,80);
@@ -293,14 +300,14 @@ Deno.serve(async(req:Request)=>{
const reply=validPhone?"Ji 😊 **"+fp.name+"** ka listed phone number: **"+validPhone+"**":"Is listing ka valid phone number abhi StudentHubHelp data me available nahi hai.";
return await chatResponse({reply,intent:"Property Contact",primaryTopic:label(focusType)+" contact",recommendedProperties:[fp],recommendations:[fp],suggestedFollowUps:["Iske full details batao","Allen ke paas aur options dikhao"],grounded:true,sessionId},{headers:H},sessionId,msg);
}
- const ar=await aiProperty(msg,history,[fp],focusType,nearTerm(focusCombined));
+ const ar=await aiProperty(msg,history,[fp],focusType,nearTerm(focusCombined),cfg);
const reply=ar.reply||("Bilkul 😊 **"+fp.name+"** ki available details neeche di hain.");
return await chatResponse({reply,intent:"Property Details",primaryTopic:label(focusType)+" details",recommendedProperties:[fp],recommendations:[fp],suggestedFollowUps:["Iska phone number batao","Allen ke paas aur options dikhao"],grounded:true,sessionId},{headers:H},sessionId,msg);
}
}
if(isAccountSupport(msg))return await chatResponse({reply:accountSupportReply(msg),intent:"Account / Login Support",primaryTopic:"Account access",suggestedFollowUps:["I forgot my password","Google login is not working","Dashboard is not opening","Contact support"],recommendations:[],recommendedProperties:[],grounded:true,sessionId},{headers:H},sessionId,msg);
if(isContact(msg)||String(b?.action||"")==="contact_support")return await chatResponse({reply:"Ji bilkul 😊 StudentHubHelp team se contact ke liye:\n\n📧 Email: studenthubhelpp@gmail.com\n📞 Phone: +91 9929718264",intent:"Support / Contact",primaryTopic:"Contact StudentHubHelp",suggestedFollowUps:["Find a hostel","Find a library","Search by area","Back to search"],recommendations:[],recommendedProperties:[],grounded:true,sessionId},{headers:H},sessionId,msg);
- const previous=history.filter(x=>x.role==="user").map(x=>x.text||"").join(" "),combined=previous+" "+msg,aiRaw=await aiUnderstand(msg,history),u=mergeIntent(aiRaw,msg,history),explicitCat=cat(msg),nearCats=secondaryCats(msg),aiCats=u.categories||[],c=u.category||explicitCat||(u.followUp?cat(previous):""),propertyMode=u.intent.startsWith("property_")||isPropertyQuery(msg,u.followUp?previous:""),ct=u.city||(u.followUp?city(previous):""),near=u.landmark||u.area||u.locality||(u.nearRelation?nearTerm(msg):""),locationForFilter=strictLocation(msg),action=String(b?.action||u.action||""),shownList=(Array.isArray(b?.shownPropertyIds)?b.shownPropertyIds:[]).map((x:any)=>String(x)),shownIds=new Set(shownList);
+ const previous=history.filter(x=>x.role==="user").map(x=>x.text||"").join(" "),combined=previous+" "+msg,aiRaw=await aiUnderstand(msg,history,cfg),u=mergeIntent(aiRaw,msg,history),explicitCat=cat(msg),nearCats=secondaryCats(msg),aiCats=u.categories||[],c=u.category||explicitCat||(u.followUp?cat(previous):""),propertyMode=u.intent.startsWith("property_")||isPropertyQuery(msg,u.followUp?previous:""),ct=u.city||(u.followUp?city(previous):""),near=u.landmark||u.area||u.locality||(u.nearRelation?nearTerm(msg):""),locationForFilter=strictLocation(msg),action=String(b?.action||u.action||""),shownList=(Array.isArray(b?.shownPropertyIds)?b.shownPropertyIds:[]).map((x:any)=>String(x)),shownIds=new Set(shownList);
if(action==="search_area"&&!city(msg)&&!nearTerm(msg))return await chatResponse({reply:"Bilkul 😊 Aap kis **area / locality** mein search karna chahte hain? Area ka naam bhejiye, main current requirement ke saath search refine kar dunga.",intent:"Area Refinement",primaryTopic:c?label(c)+" discovery":"Student Services Discovery",recommendedProperties:[],recommendations:[],suggestedFollowUps:["Search by city","Show more options","Contact support"],grounded:true,sessionId},{headers:H},sessionId,msg);
if(u.needsClarification&&u.clarificationQuestion&&!u.category&&!u.categories.length&&!u.locality&&!u.area&&!u.landmark)return await chatResponse({reply:u.clarificationQuestion,intent:"Clarification",primaryTopic:"Requirement clarification",recommendedProperties:[],recommendations:[],suggestedFollowUps:["Find a hostel","Find a library","Find a cafe","Find a tiffin service"],grounded:true,sessionId},{headers:H},sessionId,msg);
if(u.intent==="property_reference"&&u.referenceIndex){
@@ -310,7 +317,7 @@ Deno.serve(async(req:Request)=>{
if(target){
const fp=card(target),wantPhone=u.action==="phone"||isPhoneIntent(msg);
if(wantPhone){const ph=String(fp.phone||"").trim();const reply=ph&&ph.replace(/\\D/g,"").length>=7?"Ji 😊 **"+fp.name+"** ka listed phone number: **"+ph+"**":"Is listing ka valid phone number abhi StudentHubHelp data me available nahi hai.";return await chatResponse({reply,intent:"Property Contact",primaryTopic:"Referenced property contact",recommendedProperties:[fp],recommendations:[fp],suggestedFollowUps:["Iske full details batao","Aur options dikhao"],grounded:true,sessionId},{headers:H},sessionId,msg)}
- const ar=await aiProperty(msg,history,[fp],fp._type,near),reply=ar.reply||("Bilkul 😊 **"+fp.name+"** ki available details neeche di hain.");return await chatResponse({reply,intent:"Property Reference",primaryTopic:"Referenced property",recommendedProperties:[fp],recommendations:[fp],suggestedFollowUps:["Iska phone number batao","Aur options dikhao"],grounded:true,sessionId},{headers:H},sessionId,msg)
+ const ar=await aiProperty(msg,history,[fp],fp._type,near,cfg),reply=ar.reply||("Bilkul 😊 **"+fp.name+"** ki available details neeche di hain.");return await chatResponse({reply,intent:"Property Reference",primaryTopic:"Referenced property",recommendedProperties:[fp],recommendations:[fp],suggestedFollowUps:["Iska phone number batao","Aur options dikhao"],grounded:true,sessionId},{headers:H},sessionId,msg)
}
}
if(/(?:konsa|kaunsa|kaunsi|which|should i|chahiye).{0,35}(book|hostel|pg)|(?:book|hostel|pg).{0,35}(konsa|kaunsa|kaunsi|which|should)/i.test(n(msg))&&!isBookingInfoQuestion(msg)){
@@ -320,7 +327,7 @@ Deno.serve(async(req:Request)=>{
let decision=decisionRows.filter(p=>!shownIds.has(String(p.id)));
if(decisionLandmark)decision=decision.filter(p=>n(p.city||"")===n(decisionLandmark.city)&&(p._distanceKm===undefined||p._distanceKm<=10));
const rankedDecision=decision.map(p=>({...p,_score:rank(p,combined,ct,near)})).sort((a,z)=>z._score-a._score).slice(0,8).map(card);
- const ar=await aiProperty(msg,history,rankedDecision,c||"hostel",near);
+ const ar=await aiProperty(msg,history,rankedDecision,c||"hostel",near,cfg);
const reply=ar.reply||"Aapki requirement ke hisaab se live options compare kar sakte hain.";
return await chatResponse({reply,intent:"Property Comparison",primaryTopic:label(c||"hostel")+" comparison",recommendedProperties:rankedDecision,recommendations:rankedDecision,suggestedFollowUps:["Boys hostel dikhao","Girls hostel dikhao","Budget ke according dikhao"],grounded:true,sessionId},{headers:H},sessionId,msg);
}
@@ -378,7 +385,7 @@ const ownerDetailsIntent=/(?:owner|malik|maalik|proprietor|manager).{0,35}(?:det
if(isPropertyAdviceQuestion(msg)){const adviceCat=cat(msg)||cat(previous)||"hostel";return await chatResponse({reply:adviceReply(adviceCat),intent:"Property Selection Guidance",primaryTopic:label(adviceCat)+" selection guidance",recommendedProperties:[],recommendations:[],suggestedFollowUps:["Find a property","Search by area","Show more options","Contact support"],grounded:true,sessionId},{headers:H},sessionId,msg);}
if(isBookingInfoQuestion(msg)){const ar=await aiGeneral(`The user asks how to book/reserve a hostel or PG. Explain a practical StudentHubHelp booking process: shortlist suitable live listings, open full details, check price/room type/availability, call owner, confirm terms and visit/verify before paying. Do not invent any property-specific availability or booking facility.`,history);const reply=ar.reply||"Hostel book karne ka simple process: pehle suitable listing shortlist karein, full details me price/room type check karein, owner ko Call karke availability aur terms confirm karein, aur payment se pehle property/owner verify karein.";return await chatResponse({reply,intent:"Booking Guidance",primaryTopic:"Hostel / PG Booking",recommendedProperties:[],recommendations:[],suggestedFollowUps:["Allen ke paas hostel dikhao","Budget ke according hostel dikhao","Boys hostel dikhao","Girls hostel dikhao"],grounded:true,sessionId},{headers:H},sessionId,msg);}
- if(!propertyMode){const ar=await aiGeneral(msg,history);const reply=ar.reply||"Bilkul 😊 Main yahin hoon. Aap jo poochna chahein, seedha poochiye—study, ideas, facts ya normal conversation, sab par baat kar sakte hain.";return await chatResponse({reply,intent:"General / Study / Conversation",primaryTopic:"Student AI Assistant",recommendedProperties:[],recommendations:[],suggestedFollowUps:["Ask a study question","Tell me something interesting","Find a property","Contact support"],grounded:true,webGrounded:ar.webGrounded||false,webSources:ar.webSources||[],sessionId},{headers:H},sessionId,msg);}
+ if(!propertyMode){const ar=await aiGeneral(msg,history,cfg);const reply=ar.reply||"Bilkul 😊 Main yahin hoon. Aap jo poochna chahein, seedha poochiye—study, ideas, facts ya normal conversation, sab par baat kar sakte hain.";return await chatResponse({reply,intent:"General / Study / Conversation",primaryTopic:"Student AI Assistant",recommendedProperties:[],recommendations:[],suggestedFollowUps:["Ask a study question","Tell me something interesting","Find a property","Contact support"],grounded:true,webGrounded:ar.webGrounded||false,webSources:ar.webSources||[],sessionId},{headers:H},sessionId,msg);}
let rows=await multiDirect(aiCats.length?aiCats:(c?[c]:(nearCats.length?nearCats:["hostel"])),ct);
const wantedGender=u.gender||genderWanted(msg),wantedArea=u.area?(/nawalgarh/i.test(u.area)?"nawalgarh":/piprali/i.test(u.area)?"piprali":u.area.toLowerCase()):areaWanted(msg);
const wantedLocality=n(u.locality||"");
@@ -397,9 +404,9 @@ const ownerDetailsIntent=/(?:owner|malik|maalik|proprietor|manager).{0,35}(?:det
if(nearCats.length){const secondaryRows=await multiDirect(nearCats,ct);for(const p of rows){let best=Infinity,hasCoords=false;for(const q of secondaryRows){const d=distanceKm(p,q);if(d!==undefined){hasCoords=true;best=Math.min(best,d)}else if(p.area&&searchText(q).includes(n(p.area)))best=Math.min(best,1)}if(best<=3){p._nearbyDistanceKm=best;p._nearbyVerified=hasCoords;p._nearbyAreaMatch=!hasCoords}}rows=rows.filter(p=>p._nearbyDistanceKm!==undefined)}
rows=rows.filter(p=>!shownIds.has(String(p.id)));
const ranked=rows.map(p=>({...p,_score:rank(p,combined,ct,near)})).sort((a,z)=>z._score-a._score);
- let recs=ranked.slice(0,8).map(card);
+ let recs=ranked.slice(0,cfg.max_results).map(card);
if(action==="view_details"){const focusId=String(b?.focusPropertyId||""),focusType=String(b?.focusPropertyType||c||"hostel"),focusRows=await multiDirect([focusType],ct),focus=focusRows.find(p=>String(p.id)===focusId);recs=focus?[card(focus)]:recs.slice(0,1)}
- const ar=await aiProperty(msg,history,recs,c,near),reply=ar.reply||fallbackReply(c,recs,near,ct),followups=["View full details","Search by area","Show more options","Contact support"];
+ const ar=await aiProperty(msg,history,recs,c,near,cfg),reply=ar.reply||fallbackReply(c,recs,near,ct),followups=["View full details","Search by area","Show more options","Contact support"];
return await chatResponse({reply,intent:"Live Property Search",primaryTopic:c?label(c)+" discovery":"Student Services Discovery",recommendedProperties:recs,recommendations:recs,suggestedFollowUps:followups,grounded:true,sessionId,searchMode:"ultra_advance_live_supabase_ai",targetCity:ct||undefined,targetCategory:c||undefined,nearbyTerm:near||undefined,aiIntent:u.intent,aiConfidence:u.confidence,aiEntities:{category:u.category,categories:u.categories,city:u.city,locality:u.locality,area:u.area,landmark:u.landmark,gender:u.gender,budgetMin:u.budgetMin,budgetMax:u.budgetMax,facilities:u.facilities,referenceIndex:u.referenceIndex}},{headers:H},sessionId,msg);
}catch(e){return new Response(JSON.stringify({reply:"Ji, live search me temporary issue aaya. Please same query dobara bhejiye.",grounded:false,error:"Temporary server error"}),{status:200,headers:H})}
});
\ No newline at end of file
From 8e3ad2551abc490014b75818caa2b7fe306e031f Mon Sep 17 00:00:00 2001
From: StudentHub
Date: Sun, 8 Nov 2026 21:19:59 +0530
Subject: [PATCH 04/95] Apply chatbot comparison and retrieval limits
---
supabase/functions/studenthubhelp-chat/index.ts | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/supabase/functions/studenthubhelp-chat/index.ts b/supabase/functions/studenthubhelp-chat/index.ts
index fede20852..c0493b72a 100644
--- a/supabase/functions/studenthubhelp-chat/index.ts
+++ b/supabase/functions/studenthubhelp-chat/index.ts
@@ -320,13 +320,13 @@ Deno.serve(async(req:Request)=>{
const ar=await aiProperty(msg,history,[fp],fp._type,near,cfg),reply=ar.reply||("Bilkul 😊 **"+fp.name+"** ki available details neeche di hain.");return await chatResponse({reply,intent:"Property Reference",primaryTopic:"Referenced property",recommendedProperties:[fp],recommendations:[fp],suggestedFollowUps:["Iska phone number batao","Aur options dikhao"],grounded:true,sessionId},{headers:H},sessionId,msg)
}
}
- if(/(?:konsa|kaunsa|kaunsi|which|should i|chahiye).{0,35}(book|hostel|pg)|(?:book|hostel|pg).{0,35}(konsa|kaunsa|kaunsi|which|should)/i.test(n(msg))&&!isBookingInfoQuestion(msg)){
+ if(cfg.comparison_enabled&&/(?:konsa|kaunsa|kaunsi|which|should i|chahiye).{0,35}(book|hostel|pg)|(?:book|hostel|pg).{0,35}(konsa|kaunsa|kaunsi|which|should)/i.test(n(msg))&&!isBookingInfoQuestion(msg)){
const decisionRows=await multiDirect([c||"hostel"],ct);
const decisionLandmark=findLandmark(near||"");
if(decisionLandmark){for(const p of decisionRows){const d=distanceToLandmark(p,decisionLandmark);if(d!==undefined)p._distanceKm=d}}
let decision=decisionRows.filter(p=>!shownIds.has(String(p.id)));
- if(decisionLandmark)decision=decision.filter(p=>n(p.city||"")===n(decisionLandmark.city)&&(p._distanceKm===undefined||p._distanceKm<=10));
- const rankedDecision=decision.map(p=>({...p,_score:rank(p,combined,ct,near)})).sort((a,z)=>z._score-a._score).slice(0,8).map(card);
+ if(decisionLandmark)decision=decision.filter(p=>n(p.city||"")===n(decisionLandmark.city)&&((p._distanceKm!==undefined&&p._distanceKm<=10)||searchText(p).includes(n(decisionLandmark.key))));
+ const rankedDecision=decision.map(p=>({...p,_score:rank(p,combined,ct,near)})).sort((a,z)=>z._score-a._score).slice(0,cfg.max_results).map(card);
const ar=await aiProperty(msg,history,rankedDecision,c||"hostel",near,cfg);
const reply=ar.reply||"Aapki requirement ke hisaab se live options compare kar sakte hain.";
return await chatResponse({reply,intent:"Property Comparison",primaryTopic:label(c||"hostel")+" comparison",recommendedProperties:rankedDecision,recommendations:rankedDecision,suggestedFollowUps:["Boys hostel dikhao","Girls hostel dikhao","Budget ke according dikhao"],grounded:true,sessionId},{headers:H},sessionId,msg);
@@ -403,7 +403,7 @@ const ownerDetailsIntent=/(?:owner|malik|maalik|proprietor|manager).{0,35}(?:det
else if(near){const q=n(near);const exact=rows.filter(p=>searchText(p).includes(q));if(exact.length)rows=exact;else if(!wantedArea)rows=[]}
if(nearCats.length){const secondaryRows=await multiDirect(nearCats,ct);for(const p of rows){let best=Infinity,hasCoords=false;for(const q of secondaryRows){const d=distanceKm(p,q);if(d!==undefined){hasCoords=true;best=Math.min(best,d)}else if(p.area&&searchText(q).includes(n(p.area)))best=Math.min(best,1)}if(best<=3){p._nearbyDistanceKm=best;p._nearbyVerified=hasCoords;p._nearbyAreaMatch=!hasCoords}}rows=rows.filter(p=>p._nearbyDistanceKm!==undefined)}
rows=rows.filter(p=>!shownIds.has(String(p.id)));
- const ranked=rows.map(p=>({...p,_score:rank(p,combined,ct,near)})).sort((a,z)=>z._score-a._score);
+ const ranked=rows.map(p=>({...p,_score:rank(p,combined,ct,near)})).sort((a,z)=>z._score-a._score).slice(0,cfg.max_candidates);
let recs=ranked.slice(0,cfg.max_results).map(card);
if(action==="view_details"){const focusId=String(b?.focusPropertyId||""),focusType=String(b?.focusPropertyType||c||"hostel"),focusRows=await multiDirect([focusType],ct),focus=focusRows.find(p=>String(p.id)===focusId);recs=focus?[card(focus)]:recs.slice(0,1)}
const ar=await aiProperty(msg,history,recs,c,near,cfg),reply=ar.reply||fallbackReply(c,recs,near,ct),followups=["View full details","Search by area","Show more options","Contact support"];
From 835c93062fef4db6d3b51dee947ad76a6c233356 Mon Sep 17 00:00:00 2001
From: StudentHub
Date: Sun, 8 Nov 2026 21:20:27 +0530
Subject: [PATCH 05/95] Harden chatbot admin feature gates and background
logging
---
supabase/functions/studenthubhelp-chat/index.ts | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/supabase/functions/studenthubhelp-chat/index.ts b/supabase/functions/studenthubhelp-chat/index.ts
index c0493b72a..667e009b3 100644
--- a/supabase/functions/studenthubhelp-chat/index.ts
+++ b/supabase/functions/studenthubhelp-chat/index.ts
@@ -224,7 +224,7 @@ async function logChatTurn(sessionId:string,userMessage:string,payload:any){
}
}
async function chatResponse(payload:any,init:any,sessionId:string,userMessage:string){
- void logChatTurn(sessionId,userMessage,payload);
+ EdgeRuntime.waitUntil(logChatTurn(sessionId,userMessage,payload));
return new Response(JSON.stringify(payload),init);
}
function offlineGeneralReply(msg:string){
@@ -320,7 +320,7 @@ Deno.serve(async(req:Request)=>{
const ar=await aiProperty(msg,history,[fp],fp._type,near,cfg),reply=ar.reply||("Bilkul 😊 **"+fp.name+"** ki available details neeche di hain.");return await chatResponse({reply,intent:"Property Reference",primaryTopic:"Referenced property",recommendedProperties:[fp],recommendations:[fp],suggestedFollowUps:["Iska phone number batao","Aur options dikhao"],grounded:true,sessionId},{headers:H},sessionId,msg)
}
}
- if(cfg.comparison_enabled&&/(?:konsa|kaunsa|kaunsi|which|should i|chahiye).{0,35}(book|hostel|pg)|(?:book|hostel|pg).{0,35}(konsa|kaunsa|kaunsi|which|should)/i.test(n(msg))&&!isBookingInfoQuestion(msg)){
+ if(cfg.comparison_enabled&&(/(?:konsa|kaunsa|kaunsi|which|should i|chahiye).{0,35}(book|hostel|pg)|(?:book|hostel|pg).{0,35}(konsa|kaunsa|kaunsi|which|should)/i.test(n(msg)))&&!isBookingInfoQuestion(msg)){
const decisionRows=await multiDirect([c||"hostel"],ct);
const decisionLandmark=findLandmark(near||"");
if(decisionLandmark){for(const p of decisionRows){const d=distanceToLandmark(p,decisionLandmark);if(d!==undefined)p._distanceKm=d}}
@@ -384,7 +384,7 @@ const ownerDetailsIntent=/(?:owner|malik|maalik|proprietor|manager).{0,35}(?:det
}
if(isPropertyAdviceQuestion(msg)){const adviceCat=cat(msg)||cat(previous)||"hostel";return await chatResponse({reply:adviceReply(adviceCat),intent:"Property Selection Guidance",primaryTopic:label(adviceCat)+" selection guidance",recommendedProperties:[],recommendations:[],suggestedFollowUps:["Find a property","Search by area","Show more options","Contact support"],grounded:true,sessionId},{headers:H},sessionId,msg);}
- if(isBookingInfoQuestion(msg)){const ar=await aiGeneral(`The user asks how to book/reserve a hostel or PG. Explain a practical StudentHubHelp booking process: shortlist suitable live listings, open full details, check price/room type/availability, call owner, confirm terms and visit/verify before paying. Do not invent any property-specific availability or booking facility.`,history);const reply=ar.reply||"Hostel book karne ka simple process: pehle suitable listing shortlist karein, full details me price/room type check karein, owner ko Call karke availability aur terms confirm karein, aur payment se pehle property/owner verify karein.";return await chatResponse({reply,intent:"Booking Guidance",primaryTopic:"Hostel / PG Booking",recommendedProperties:[],recommendations:[],suggestedFollowUps:["Allen ke paas hostel dikhao","Budget ke according hostel dikhao","Boys hostel dikhao","Girls hostel dikhao"],grounded:true,sessionId},{headers:H},sessionId,msg);}
+ if(isBookingInfoQuestion(msg)){const ar=await aiGeneral(`The user asks how to book/reserve a hostel or PG. Explain a practical StudentHubHelp booking process: shortlist suitable live listings, open full details, check price/room type/availability, call owner, confirm terms and visit/verify before paying. Do not invent any property-specific availability or booking facility.`,history,cfg);const reply=ar.reply||"Hostel book karne ka simple process: pehle suitable listing shortlist karein, full details me price/room type check karein, owner ko Call karke availability aur terms confirm karein, aur payment se pehle property/owner verify karein.";return await chatResponse({reply,intent:"Booking Guidance",primaryTopic:"Hostel / PG Booking",recommendedProperties:[],recommendations:[],suggestedFollowUps:["Allen ke paas hostel dikhao","Budget ke according hostel dikhao","Boys hostel dikhao","Girls hostel dikhao"],grounded:true,sessionId},{headers:H},sessionId,msg);}
if(!propertyMode){const ar=await aiGeneral(msg,history,cfg);const reply=ar.reply||"Bilkul 😊 Main yahin hoon. Aap jo poochna chahein, seedha poochiye—study, ideas, facts ya normal conversation, sab par baat kar sakte hain.";return await chatResponse({reply,intent:"General / Study / Conversation",primaryTopic:"Student AI Assistant",recommendedProperties:[],recommendations:[],suggestedFollowUps:["Ask a study question","Tell me something interesting","Find a property","Contact support"],grounded:true,webGrounded:ar.webGrounded||false,webSources:ar.webSources||[],sessionId},{headers:H},sessionId,msg);}
let rows=await multiDirect(aiCats.length?aiCats:(c?[c]:(nearCats.length?nearCats:["hostel"])),ct);
const wantedGender=u.gender||genderWanted(msg),wantedArea=u.area?(/nawalgarh/i.test(u.area)?"nawalgarh":/piprali/i.test(u.area)?"piprali":u.area.toLowerCase()):areaWanted(msg);
From 9238ca3c827fbd46d53ad3694a142f547fb052bc Mon Sep 17 00:00:00 2001
From: StudentHub
Date: Sun, 8 Nov 2026 21:21:03 +0530
Subject: [PATCH 06/95] Tighten chatbot rate-limit identity and config bounds
---
supabase/functions/studenthubhelp-chat/index.ts | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/supabase/functions/studenthubhelp-chat/index.ts b/supabase/functions/studenthubhelp-chat/index.ts
index 667e009b3..1b8da3275 100644
--- a/supabase/functions/studenthubhelp-chat/index.ts
+++ b/supabase/functions/studenthubhelp-chat/index.ts
@@ -8,12 +8,12 @@ const RATE_WINDOW_MS=60_000, RATE_MAX=30;
const rateMap=new Map();
const ownerLeadRateMap=new Map();
const OWNER_LEAD_WINDOW_MS=10*60_000, OWNER_LEAD_MAX=3;
-function ownerLeadAllowed(req:Request){const key=rateKey(req),now=Date.now(),old=ownerLeadRateMap.get(key);if(!old||now-old.start>=OWNER_LEAD_WINDOW_MS){ownerLeadRateMap.set(key,{start:now,count:1});return true}if(old.count>=OWNER_LEAD_MAX)return false;old.count++;return true}
+function ownerLeadAllowed(req:Request){const key=rateKey(req),now=Date.now(),old=ownerLeadRateMap.get(key);pruneRateMap(ownerLeadRateMap,now);if(!old||now-old.start>=OWNER_LEAD_WINDOW_MS){ownerLeadRateMap.set(key,{start:now,count:1});return true}if(old.count>=OWNER_LEAD_MAX)return false;old.count++;return true}
const H={"Access-Control-Allow-Origin":"*","Access-Control-Allow-Headers":"authorization,x-client-info,apikey,content-type","Access-Control-Allow-Methods":"POST,OPTIONS","Content-Type":"application/json"};
const SRC=[["hostels","hostel"],["tiffins","tiffin"],["libraries","library"],["cafes","cafe"],["bookstores","bookstore"]] as const;
const n=(v:any)=>String(v??"").toLowerCase().normalize("NFKC").replace(/[^a-z0-9\u0900-\u097f]+/gi," ").replace(/\s+/g," ").trim();
const num=(v:any)=>{const x=Number(String(v??"").replace(/,/g,""));return Number.isFinite(x)?x:undefined};
-function rateKey(req:Request){const f=req.headers.get("cf-connecting-ip")||req.headers.get("x-real-ip")||"unknown";return f.trim().slice(0,100)||"unknown"}
+function rateKey(req:Request){const f=req.headers.get("cf-connecting-ip")||"unknown";return f.trim().slice(0,100)||"unknown"}
function pruneRateMap(map:Map,now:number){if(map.size<5000)return;for(const [key,value] of map){if(now-value.start>Math.max(RATE_WINDOW_MS,OWNER_LEAD_WINDOW_MS))map.delete(key);if(map.size<4000)break}}\nfunction allowed(req:Request){const key=rateKey(req),now=Date.now(),old=rateMap.get(key);pruneRateMap(rateMap,now);if(!old||now-old.start>=RATE_WINDOW_MS){rateMap.set(key,{start:now,count:1});return true}if(old.count>=RATE_MAX)return false;old.count++;return true}
async function db(path:string):Promise{if(!U||!K)throw Error("database configuration missing");const r=await fetch(U+path,{headers:{apikey:K,Authorization:`Bearer ${K}`,"Content-Type":"application/json"}});if(!r.ok)throw Error(`database ${r.status}`);return await r.json() as T}
type ChatbotRuntimeConfig={enabled:boolean;ai_rerank_enabled:boolean;lead_capture_enabled:boolean;comparison_enabled:boolean;budget_planner_enabled:boolean;max_candidates:number;max_results:number;max_output_tokens:number;temperature:number;maintenance_message:string};
@@ -137,7 +137,7 @@ CURRENT MESSAGE:
${msg}
RECENT CONVERSATION:
${h}`;
- const r=await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent`,{method:"POST",headers:{"Content-Type":"application/json","x-goog-api-key":GEMINI},body:JSON.stringify({contents:[{role:"user",parts:[{text:prompt}]}],generationConfig:{responseMimeType:"application/json",maxOutputTokens:Math.min(900,cfg.max_output_tokens),temperature:cfg.temperature,thinkingConfig:{thinkingLevel:"medium"}}})});
+ const r=await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent`,{method:"POST",headers:{"Content-Type":"application/json","x-goog-api-key":GEMINI},body:JSON.stringify({contents:[{role:"user",parts:[{text:prompt}]}],generationConfig:{responseMimeType:"application/json",maxOutputTokens:cfg.max_output_tokens,temperature:cfg.temperature,thinkingConfig:{thinkingLevel:"medium"}}})});
if(!r.ok)return fallback;const j=await r.json(),txt=j?.candidates?.[0]?.content?.parts?.map((p:any)=>p.text||"").join("")||"";return normalizeIntent(safeJson(txt));
}catch{return fallback}
}
@@ -186,7 +186,7 @@ Rules:
- Never recommend listings unless the current message has a property-search intent.
- Do not expose internal system/tool details.
Return only the natural reply text.`;
-const r=await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent`,{method:"POST",headers:{"Content-Type":"application/json","x-goog-api-key":GEMINI},body:JSON.stringify({contents:[{role:"user",parts:[{text:prompt}]}],generationConfig:{maxOutputTokens:Math.min(700,cfg.max_output_tokens),temperature:cfg.temperature,thinkingConfig:{thinkingLevel:"low"}}})});if(!r.ok)return{};const j=await r.json();return{reply:j?.candidates?.[0]?.content?.parts?.map((p:any)=>p.text||"").join("")||""}}catch{return{}}}
+const r=await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent`,{method:"POST",headers:{"Content-Type":"application/json","x-goog-api-key":GEMINI},body:JSON.stringify({contents:[{role:"user",parts:[{text:prompt}]}],generationConfig:{maxOutputTokens:cfg.max_output_tokens,temperature:cfg.temperature,thinkingConfig:{thinkingLevel:"low"}}})});if(!r.ok)return{};const j=await r.json();return{reply:j?.candidates?.[0]?.content?.parts?.map((p:any)=>p.text||"").join("")||""}}catch{return{}}}
async function dbWrite(method:string,path:string,body:any,prefer="return=minimal"){
if(!U||!K)throw Error("database configuration missing");
From 06c0a498292628e196bfb140700db7f04eed8862 Mon Sep 17 00:00:00 2001
From: StudentHub
Date: Sun, 8 Nov 2026 21:25:54 +0530
Subject: [PATCH 07/95] Add atomic distributed chatbot rate limiting
---
...8190000_chatbot_distributed_rate_limit.sql | 36 +++++++++++++++++++
1 file changed, 36 insertions(+)
create mode 100644 supabase/migrations/20261108190000_chatbot_distributed_rate_limit.sql
diff --git a/supabase/migrations/20261108190000_chatbot_distributed_rate_limit.sql b/supabase/migrations/20261108190000_chatbot_distributed_rate_limit.sql
new file mode 100644
index 000000000..0abaf161a
--- /dev/null
+++ b/supabase/migrations/20261108190000_chatbot_distributed_rate_limit.sql
@@ -0,0 +1,36 @@
+-- Durable, atomic rate limiting for the StudentHubHelp chatbot.
+create table if not exists private.chatbot_rate_limits (
+ scope text not null,
+ key_hash text not null,
+ window_started_at timestamptz not null,
+ hit_count integer not null default 0,
+ primary key (scope, key_hash)
+);
+revoke all on table private.chatbot_rate_limits from public, anon, authenticated;
+grant all on table private.chatbot_rate_limits to service_role;
+
+create or replace function public.consume_chatbot_rate_limit(
+ p_scope text, p_key text, p_limit integer, p_window_seconds integer
+) returns boolean
+language plpgsql security definer set search_path = ''
+as $$
+declare v_count integer;
+begin
+ if p_scope not in ('chat', 'owner_lead') or p_key is null or length(p_key)=0
+ or p_limit < 1 or p_limit > 1000 or p_window_seconds < 1 or p_window_seconds > 86400 then
+ raise exception 'Invalid rate limit arguments';
+ end if;
+ insert into private.chatbot_rate_limits(scope,key_hash,window_started_at,hit_count)
+ values(p_scope,md5(p_key),now(),1)
+ on conflict(scope,key_hash) do update
+ set window_started_at=case when private.chatbot_rate_limits.window_started_at <= now()-make_interval(secs=>p_window_seconds) then now() else private.chatbot_rate_limits.window_started_at end,
+ hit_count=case when private.chatbot_rate_limits.window_started_at <= now()-make_interval(secs=>p_window_seconds) then 1 else private.chatbot_rate_limits.hit_count+1 end
+ returning hit_count into v_count;
+ if mod(extract(epoch from now())::bigint,100)=0 then
+ delete from private.chatbot_rate_limits where window_started_at < now()-interval '2 days';
+ end if;
+ return v_count <= p_limit;
+end;
+$$;
+revoke all on function public.consume_chatbot_rate_limit(text,text,integer,integer) from public,anon,authenticated;
+grant execute on function public.consume_chatbot_rate_limit(text,text,integer,integer) to service_role;
From cd0da17ab6de4f8c71c06815006f9bc82c9da108 Mon Sep 17 00:00:00 2001
From: StudentHub
Date: Sun, 8 Nov 2026 21:25:57 +0530
Subject: [PATCH 08/95] Fix chatbot syntax and use distributed rate limits
---
supabase/functions/studenthubhelp-chat/index.ts | 13 ++++---------
1 file changed, 4 insertions(+), 9 deletions(-)
diff --git a/supabase/functions/studenthubhelp-chat/index.ts b/supabase/functions/studenthubhelp-chat/index.ts
index 1b8da3275..6a6f9f485 100644
--- a/supabase/functions/studenthubhelp-chat/index.ts
+++ b/supabase/functions/studenthubhelp-chat/index.ts
@@ -4,17 +4,12 @@ const U=(Deno.env.get("SUPABASE_URL")||"").replace(/\/$/,"");
const K=Deno.env.get("SUPABASE_SECRET_KEY")||Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")||"";
const GEMINI=Deno.env.get("GEMINI_API_KEY")||"";
const MODEL="gemini-3.8-flash";
-const RATE_WINDOW_MS=60_000, RATE_MAX=30;
-const rateMap=new Map();
-const ownerLeadRateMap=new Map();
-const OWNER_LEAD_WINDOW_MS=10*60_000, OWNER_LEAD_MAX=3;
-function ownerLeadAllowed(req:Request){const key=rateKey(req),now=Date.now(),old=ownerLeadRateMap.get(key);pruneRateMap(ownerLeadRateMap,now);if(!old||now-old.start>=OWNER_LEAD_WINDOW_MS){ownerLeadRateMap.set(key,{start:now,count:1});return true}if(old.count>=OWNER_LEAD_MAX)return false;old.count++;return true}
const H={"Access-Control-Allow-Origin":"*","Access-Control-Allow-Headers":"authorization,x-client-info,apikey,content-type","Access-Control-Allow-Methods":"POST,OPTIONS","Content-Type":"application/json"};
const SRC=[["hostels","hostel"],["tiffins","tiffin"],["libraries","library"],["cafes","cafe"],["bookstores","bookstore"]] as const;
const n=(v:any)=>String(v??"").toLowerCase().normalize("NFKC").replace(/[^a-z0-9\u0900-\u097f]+/gi," ").replace(/\s+/g," ").trim();
const num=(v:any)=>{const x=Number(String(v??"").replace(/,/g,""));return Number.isFinite(x)?x:undefined};
-function rateKey(req:Request){const f=req.headers.get("cf-connecting-ip")||"unknown";return f.trim().slice(0,100)||"unknown"}
-function pruneRateMap(map:Map,now:number){if(map.size<5000)return;for(const [key,value] of map){if(now-value.start>Math.max(RATE_WINDOW_MS,OWNER_LEAD_WINDOW_MS))map.delete(key);if(map.size<4000)break}}\nfunction allowed(req:Request){const key=rateKey(req),now=Date.now(),old=rateMap.get(key);pruneRateMap(rateMap,now);if(!old||now-old.start>=RATE_WINDOW_MS){rateMap.set(key,{start:now,count:1});return true}if(old.count>=RATE_MAX)return false;old.count++;return true}
+function rateKey(req:Request,sessionId:string){const ip=req.headers.get("cf-connecting-ip")||req.headers.get("x-real-ip");if(ip?.trim())return ip.trim().slice(0,100);const ua=req.headers.get("user-agent")||"unknown";return ("session:"+sessionId+":"+ua).slice(0,220)}
+async function consumeRateLimit(scope:string,key:string,limit:number,windowSeconds:number){if(!U||!K)throw Error("rate limiter database configuration missing");const r=await fetch(U+"/rest/v1/rpc/consume_chatbot_rate_limit",{method:"POST",headers:{apikey:K,Authorization:"Bearer "+K,"Content-Type":"application/json"},body:JSON.stringify({p_scope:scope,p_key:key,p_limit:limit,p_window_seconds:windowSeconds})});if(!r.ok)throw Error("distributed rate limiter unavailable: "+r.status);return await r.json()===true}
async function db(path:string):Promise{if(!U||!K)throw Error("database configuration missing");const r=await fetch(U+path,{headers:{apikey:K,Authorization:`Bearer ${K}`,"Content-Type":"application/json"}});if(!r.ok)throw Error(`database ${r.status}`);return await r.json() as T}
type ChatbotRuntimeConfig={enabled:boolean;ai_rerank_enabled:boolean;lead_capture_enabled:boolean;comparison_enabled:boolean;budget_planner_enabled:boolean;max_candidates:number;max_results:number;max_output_tokens:number;temperature:number;maintenance_message:string};
const DEFAULT_RUNTIME_CONFIG:ChatbotRuntimeConfig={enabled:true,ai_rerank_enabled:true,lead_capture_enabled:true,comparison_enabled:true,budget_planner_enabled:true,max_candidates:40,max_results:8,max_output_tokens:900,temperature:0.2,maintenance_message:""};
@@ -253,18 +248,18 @@ Deno.serve(async(req:Request)=>{
if(req.method==="OPTIONS")return new Response("ok",{headers:H});
if(req.method!=="POST")return new Response(JSON.stringify({error:"POST required"}),{status:405,headers:H});
try{
- if(!allowed(req))return new Response(JSON.stringify({error:"Too many requests. Please try again shortly."}),{status:429,headers:{...H,"Retry-After":"60"}});
const contentLength=Number(req.headers.get("content-length")||0);if(contentLength>32768)return new Response(JSON.stringify({error:"Request is too large."}),{status:413,headers:H});
const rawBody=await req.text();if(new TextEncoder().encode(rawBody).byteLength>32768)return new Response(JSON.stringify({error:"Request is too large."}),{status:413,headers:H});
let b:any;try{b=JSON.parse(rawBody)}catch{return new Response(JSON.stringify({error:"Invalid JSON request."}),{status:400,headers:H})}
const msg=String(b?.message||"").trim().slice(0,1500),history=Array.isArray(b?.history)?b.history.slice(-8).map((x:any)=>({role:String(x?.role||"").slice(0,20),text:String(x?.text||"").slice(0,1500)})):[];
if(!msg)return new Response(JSON.stringify({error:"Message is required"}),{status:400,headers:H});
const sessionId=String(b?.sessionId||crypto.randomUUID());
+ try{if(!await consumeRateLimit("chat",rateKey(req,sessionId),30,60))return new Response(JSON.stringify({error:"Too many requests. Please try again shortly."}),{status:429,headers:{...H,"Retry-After":"60"}})}catch(e){console.error(e);return new Response(JSON.stringify({error:"Chat service temporarily unavailable."}),{status:503,headers:H})}
const cfg=await getRuntimeConfig();
if(!cfg.enabled)return new Response(JSON.stringify({reply:cfg.maintenance_message||"StudentHubHelp AI assistant is temporarily unavailable. Please try again shortly.",grounded:false,maintenance:true}),{status:503,headers:H});
if(String(b?.action||"")==="submit_owner_lead"){
if(!cfg.lead_capture_enabled)return new Response(JSON.stringify({error:"Owner listing requests are temporarily disabled."}),{status:403,headers:H});
- if(!ownerLeadAllowed(req))return new Response(JSON.stringify({error:"Too many owner requests. Please try again later."}),{status:429,headers:{...H,"Retry-After":"600"}});
+ try{if(!await consumeRateLimit("owner_lead",rateKey(req,sessionId),3,600))return new Response(JSON.stringify({error:"Too many owner requests. Please try again later."}),{status:429,headers:{...H,"Retry-After":"600"}})}catch(e){console.error(e);return new Response(JSON.stringify({error:"Owner request service temporarily unavailable."}),{status:503,headers:H})}
const lead=b?.ownerLead||{};
const name=String(lead.name||"").trim().replace(/\s+/g," ").slice(0,80);
const phone=String(lead.phone||"").trim().slice(0,20);
From d8b5c9d55f2cc5644395a5f82b464e405bedefe3 Mon Sep 17 00:00:00 2001
From: StudentHub
Date: Sun, 8 Nov 2026 21:26:24 +0530
Subject: [PATCH 09/95] Honor chatbot budget planner admin control
---
supabase/functions/studenthubhelp-chat/index.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/supabase/functions/studenthubhelp-chat/index.ts b/supabase/functions/studenthubhelp-chat/index.ts
index 6a6f9f485..dfa3be089 100644
--- a/supabase/functions/studenthubhelp-chat/index.ts
+++ b/supabase/functions/studenthubhelp-chat/index.ts
@@ -391,7 +391,7 @@ const ownerDetailsIntent=/(?:owner|malik|maalik|proprietor|manager).{0,35}(?:det
if(wantedLocality)rows=rows.filter(p=>searchText(p).includes(wantedLocality));
// An explicit road/locality is also a hard constraint when the user asked for it.
if(locationForFilter.area)rows=rows.filter(p=>searchText(p).includes(n(locationForFilter.area)));
- if(u.budgetMax!==null||u.budgetMin!==null)rows=rows.filter(p=>{const v=budgetValue(p);if(v===undefined)return false;return (u.budgetMax===null||v<=u.budgetMax)&&(u.budgetMin===null||v>=u.budgetMin)});
+ if(cfg.budget_planner_enabled&&(u.budgetMax!==null||u.budgetMin!==null))rows=rows.filter(p=>{const v=budgetValue(p);if(v===undefined)return false;return (u.budgetMax===null||v<=u.budgetMax)&&(u.budgetMin===null||v>=u.budgetMin)});
if(u.facilities?.length)rows=rows.filter(p=>matchesFacility(p,u.facilities));
const landmark=u.landmark?findLandmark(u.landmark)||LANDMARKS.find(l=>l.key===u.landmark):findLandmark(near||"");
if(landmark){for(const p of rows){const d=distanceToLandmark(p,landmark);if(d!==undefined)p._distanceKm=d}rows=rows.filter(p=>n(p.city||"")===n(landmark.city)&&((p._distanceKm!==undefined&&p._distanceKm<=10)||searchText(p).includes(n(landmark.key))))}
From e11e4c45bdf0ca637eb1870834b2f5b289968e68 Mon Sep 17 00:00:00 2001
From: StudentHub
Date: Sun, 8 Nov 2026 21:26:27 +0530
Subject: [PATCH 10/95] Clarify chatbot AI control label
---
admin/src/components/ChatbotAdminControls.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/admin/src/components/ChatbotAdminControls.tsx b/admin/src/components/ChatbotAdminControls.tsx
index 7d0756a38..d403aa83c 100644
--- a/admin/src/components/ChatbotAdminControls.tsx
+++ b/admin/src/components/ChatbotAdminControls.tsx
@@ -241,7 +241,7 @@ export const ChatbotAdminControls: React.FC = ({
{[
['enabled', 'AI assistant enabled', ShieldCheck],
- ['ai_rerank_enabled', 'Gemini AI reranking', Brain],
+ ['ai_rerank_enabled', 'Gemini AI response enhancement', Brain],
['lead_capture_enabled', 'Lead capture', SlidersHorizontal],
['comparison_enabled', 'Live comparison mode', SlidersHorizontal],
['budget_planner_enabled', 'Live budget planner', SlidersHorizontal],
From d86fab143c447b70f3c3ac729215390417a46f14 Mon Sep 17 00:00:00 2001
From: StudentHub
Date: Sun, 8 Nov 2026 21:35:48 +0530
Subject: [PATCH 11/95] Harden chatbot runtime config, rate limits and error
handling
---
supabase/functions/studenthubhelp-chat/index.ts | 15 ++++++++-------
1 file changed, 8 insertions(+), 7 deletions(-)
diff --git a/supabase/functions/studenthubhelp-chat/index.ts b/supabase/functions/studenthubhelp-chat/index.ts
index dfa3be089..53ff0034e 100644
--- a/supabase/functions/studenthubhelp-chat/index.ts
+++ b/supabase/functions/studenthubhelp-chat/index.ts
@@ -8,13 +8,14 @@ const H={"Access-Control-Allow-Origin":"*","Access-Control-Allow-Headers":"autho
const SRC=[["hostels","hostel"],["tiffins","tiffin"],["libraries","library"],["cafes","cafe"],["bookstores","bookstore"]] as const;
const n=(v:any)=>String(v??"").toLowerCase().normalize("NFKC").replace(/[^a-z0-9\u0900-\u097f]+/gi," ").replace(/\s+/g," ").trim();
const num=(v:any)=>{const x=Number(String(v??"").replace(/,/g,""));return Number.isFinite(x)?x:undefined};
-function rateKey(req:Request,sessionId:string){const ip=req.headers.get("cf-connecting-ip")||req.headers.get("x-real-ip");if(ip?.trim())return ip.trim().slice(0,100);const ua=req.headers.get("user-agent")||"unknown";return ("session:"+sessionId+":"+ua).slice(0,220)}
+function rateKeys(req:Request,sessionId:string){const cfIp=req.headers.get("cf-connecting-ip")?.trim();const ipKey=cfIp?("ip:"+cfIp.slice(0,100)):"ip:unavailable";const sessionKey=("session:"+String(sessionId||"missing").slice(0,160));return [{scopeKey:ipKey,limit:30,windowSeconds:60},{scopeKey:sessionKey,limit:15,windowSeconds:60}]};
+async function consumeDualRateLimit(scope:string,req:Request,sessionId:string,limit:number,windowSeconds:number){for(const item of rateKeys(req,sessionId)){const allowed=await consumeRateLimit(scope,item.scopeKey,Math.min(limit,item.limit),Math.max(windowSeconds,item.windowSeconds));if(!allowed)return false}return true}
async function consumeRateLimit(scope:string,key:string,limit:number,windowSeconds:number){if(!U||!K)throw Error("rate limiter database configuration missing");const r=await fetch(U+"/rest/v1/rpc/consume_chatbot_rate_limit",{method:"POST",headers:{apikey:K,Authorization:"Bearer "+K,"Content-Type":"application/json"},body:JSON.stringify({p_scope:scope,p_key:key,p_limit:limit,p_window_seconds:windowSeconds})});if(!r.ok)throw Error("distributed rate limiter unavailable: "+r.status);return await r.json()===true}
async function db(path:string):Promise{if(!U||!K)throw Error("database configuration missing");const r=await fetch(U+path,{headers:{apikey:K,Authorization:`Bearer ${K}`,"Content-Type":"application/json"}});if(!r.ok)throw Error(`database ${r.status}`);return await r.json() as T}
type ChatbotRuntimeConfig={enabled:boolean;ai_rerank_enabled:boolean;lead_capture_enabled:boolean;comparison_enabled:boolean;budget_planner_enabled:boolean;max_candidates:number;max_results:number;max_output_tokens:number;temperature:number;maintenance_message:string};
-const DEFAULT_RUNTIME_CONFIG:ChatbotRuntimeConfig={enabled:true,ai_rerank_enabled:true,lead_capture_enabled:true,comparison_enabled:true,budget_planner_enabled:true,max_candidates:40,max_results:8,max_output_tokens:900,temperature:0.2,maintenance_message:""};
+const SAFE_FALLBACK_CONFIG:ChatbotRuntimeConfig={enabled:false,ai_rerank_enabled:false,lead_capture_enabled:false,comparison_enabled:false,budget_planner_enabled:false,max_candidates:10,max_results:1,max_output_tokens:200,temperature:0,maintenance_message:"Chatbot system maintenance par hai. Kripya thodi der baad koshish karein ya seedhe support par sampark karein."};
let runtimeConfigCache:{value:ChatbotRuntimeConfig;expiresAt:number}|null=null;
-async function getRuntimeConfig():Promise{if(runtimeConfigCache&&runtimeConfigCache.expiresAt>Date.now())return runtimeConfigCache.value;try{const rows=await db("/rest/v1/chatbot_admin_config?select=*&id=eq.1&limit=1");const row=rows?.[0]||{};const value:ChatbotRuntimeConfig={enabled:row.enabled!==false,ai_rerank_enabled:row.ai_rerank_enabled!==false,lead_capture_enabled:row.lead_capture_enabled!==false,comparison_enabled:row.comparison_enabled!==false,budget_planner_enabled:row.budget_planner_enabled!==false,max_candidates:Math.max(10,Math.min(100,Number(row.max_candidates)||40)),max_results:Math.max(1,Math.min(20,Number(row.max_results)||8)),max_output_tokens:Math.max(200,Math.min(3000,Number(row.max_output_tokens)||900)),temperature:Math.max(0,Math.min(1,Number.isFinite(Number(row.temperature))?Number(row.temperature):0.2)),maintenance_message:String(row.maintenance_message||"").slice(0,500)};runtimeConfigCache={value,expiresAt:Date.now()+15000};return value}catch(e){console.error("chatbot admin config unavailable; using safe defaults",e);return DEFAULT_RUNTIME_CONFIG}}
+async function getRuntimeConfig():Promise{if(runtimeConfigCache&&runtimeConfigCache.expiresAt>Date.now())return runtimeConfigCache.value;try{const rows=await db("/rest/v1/chatbot_admin_config?select=*&id=eq.1&limit=1");const row=rows?.[0];if(!row){console.error("[CRITICAL] chatbot_admin_config row id=1 is missing; disabling chatbot");return SAFE_FALLBACK_CONFIG}const value:ChatbotRuntimeConfig={enabled:row.enabled===true,ai_rerank_enabled:row.ai_rerank_enabled===true,lead_capture_enabled:row.lead_capture_enabled===true,comparison_enabled:row.comparison_enabled===true,budget_planner_enabled:row.budget_planner_enabled===true,max_candidates:Math.max(10,Math.min(100,Number(row.max_candidates)||40)),max_results:Math.max(1,Math.min(20,Number(row.max_results)||8)),max_output_tokens:Math.max(200,Math.min(3000,Number(row.max_output_tokens)||900)),temperature:Math.max(0,Math.min(1,Number.isFinite(Number(row.temperature))?Number(row.temperature):0.2)),maintenance_message:String(row.maintenance_message||"").slice(0,500)};runtimeConfigCache={value,expiresAt:Date.now()+15000};return value}catch(e){console.error("[CRITICAL] chatbot admin config fetch failed; disabling chatbot",e);return SAFE_FALLBACK_CONFIG}}
const LANDMARKS=[{key:"allen",label:"Allen Career Institute Sikar",aliases:["allen","allen sikar","allen coaching","allen career","allen sanskar","allen piprali"],city:"Sikar",lat:27.62624,lng:75.17721},{key:"clc",label:"CLC Sikar",aliases:["clc","clc sikar"],city:"Sikar",lat:27.6206291,lng:75.1616672},{key:"vibrant",label:"Vibrant Academy Sikar",aliases:["vibrant","vibrant coaching","vibrant academy","vibrant academy ssc","vibrant ssc"],city:"Sikar"},{key:"matrix",label:"Matrix Coaching Sikar",aliases:["matrix","matrix academy","matrix coaching","matrix sikar"],city:"Sikar"},{key:"gurukripa",label:"Gurukripa Coaching Sikar",aliases:["gurukripa","guru kripa","gurukripa coaching","gurukripa sikar"],city:"Sikar"}];
function city(s:string){const x=n(s),m:[string,string][]=[["sikar","Sikar"],["piprali","Sikar"],["पिपराली","Sikar"],["सिकर","Sikar"],["kota","Kota"],["talwandi","Kota"],["jaipur","Jaipur"],["ajmer","Ajmer"],["bikaner","Bikaner"],["jodhpur","Jodhpur"],["udaipur","Udaipur"],["alwar","Alwar"],["delhi","Delhi"],["ahmedabad","Ahmedabad"],["lucknow","Lucknow"],["hyderabad","Hyderabad"],["pune","Pune"]];return m.find(([a])=>x.includes(n(a)))?.[1]||""}
function findLandmark(s:string){const x=n(s);for(const l of LANDMARKS)if(l.aliases.some(a=>x.includes(n(a))))return l;return undefined}
@@ -254,12 +255,12 @@ Deno.serve(async(req:Request)=>{
const msg=String(b?.message||"").trim().slice(0,1500),history=Array.isArray(b?.history)?b.history.slice(-8).map((x:any)=>({role:String(x?.role||"").slice(0,20),text:String(x?.text||"").slice(0,1500)})):[];
if(!msg)return new Response(JSON.stringify({error:"Message is required"}),{status:400,headers:H});
const sessionId=String(b?.sessionId||crypto.randomUUID());
- try{if(!await consumeRateLimit("chat",rateKey(req,sessionId),30,60))return new Response(JSON.stringify({error:"Too many requests. Please try again shortly."}),{status:429,headers:{...H,"Retry-After":"60"}})}catch(e){console.error(e);return new Response(JSON.stringify({error:"Chat service temporarily unavailable."}),{status:503,headers:H})}
+ try{if(!await consumeDualRateLimit("chat",req,sessionId,30,60))return new Response(JSON.stringify({error:"Too many requests. Please try again shortly."}),{status:429,headers:{...H,"Retry-After":"60"}})}catch(e){console.error(e);return new Response(JSON.stringify({error:"Chat service temporarily unavailable."}),{status:503,headers:H})}
const cfg=await getRuntimeConfig();
if(!cfg.enabled)return new Response(JSON.stringify({reply:cfg.maintenance_message||"StudentHubHelp AI assistant is temporarily unavailable. Please try again shortly.",grounded:false,maintenance:true}),{status:503,headers:H});
if(String(b?.action||"")==="submit_owner_lead"){
if(!cfg.lead_capture_enabled)return new Response(JSON.stringify({error:"Owner listing requests are temporarily disabled."}),{status:403,headers:H});
- try{if(!await consumeRateLimit("owner_lead",rateKey(req,sessionId),3,600))return new Response(JSON.stringify({error:"Too many owner requests. Please try again later."}),{status:429,headers:{...H,"Retry-After":"600"}})}catch(e){console.error(e);return new Response(JSON.stringify({error:"Owner request service temporarily unavailable."}),{status:503,headers:H})}
+ try{if(!await consumeDualRateLimit("owner_lead",req,sessionId,3,600))return new Response(JSON.stringify({error:"Too many owner requests. Please try again later."}),{status:429,headers:{...H,"Retry-After":"600"}})}catch(e){console.error(e);return new Response(JSON.stringify({error:"Owner request service temporarily unavailable."}),{status:503,headers:H})}
const lead=b?.ownerLead||{};
const name=String(lead.name||"").trim().replace(/\s+/g," ").slice(0,80);
const phone=String(lead.phone||"").trim().slice(0,20);
@@ -391,7 +392,7 @@ const ownerDetailsIntent=/(?:owner|malik|maalik|proprietor|manager).{0,35}(?:det
if(wantedLocality)rows=rows.filter(p=>searchText(p).includes(wantedLocality));
// An explicit road/locality is also a hard constraint when the user asked for it.
if(locationForFilter.area)rows=rows.filter(p=>searchText(p).includes(n(locationForFilter.area)));
- if(cfg.budget_planner_enabled&&(u.budgetMax!==null||u.budgetMin!==null))rows=rows.filter(p=>{const v=budgetValue(p);if(v===undefined)return false;return (u.budgetMax===null||v<=u.budgetMax)&&(u.budgetMin===null||v>=u.budgetMin)});
+ if(u.budgetMax!==null||u.budgetMin!==null)rows=rows.filter(p=>{const v=budgetValue(p);if(v===undefined)return false;return (u.budgetMax===null||v<=u.budgetMax)&&(u.budgetMin===null||v>=u.budgetMin)});
if(u.facilities?.length)rows=rows.filter(p=>matchesFacility(p,u.facilities));
const landmark=u.landmark?findLandmark(u.landmark)||LANDMARKS.find(l=>l.key===u.landmark):findLandmark(near||"");
if(landmark){for(const p of rows){const d=distanceToLandmark(p,landmark);if(d!==undefined)p._distanceKm=d}rows=rows.filter(p=>n(p.city||"")===n(landmark.city)&&((p._distanceKm!==undefined&&p._distanceKm<=10)||searchText(p).includes(n(landmark.key))))}
@@ -403,5 +404,5 @@ const ownerDetailsIntent=/(?:owner|malik|maalik|proprietor|manager).{0,35}(?:det
if(action==="view_details"){const focusId=String(b?.focusPropertyId||""),focusType=String(b?.focusPropertyType||c||"hostel"),focusRows=await multiDirect([focusType],ct),focus=focusRows.find(p=>String(p.id)===focusId);recs=focus?[card(focus)]:recs.slice(0,1)}
const ar=await aiProperty(msg,history,recs,c,near,cfg),reply=ar.reply||fallbackReply(c,recs,near,ct),followups=["View full details","Search by area","Show more options","Contact support"];
return await chatResponse({reply,intent:"Live Property Search",primaryTopic:c?label(c)+" discovery":"Student Services Discovery",recommendedProperties:recs,recommendations:recs,suggestedFollowUps:followups,grounded:true,sessionId,searchMode:"ultra_advance_live_supabase_ai",targetCity:ct||undefined,targetCategory:c||undefined,nearbyTerm:near||undefined,aiIntent:u.intent,aiConfidence:u.confidence,aiEntities:{category:u.category,categories:u.categories,city:u.city,locality:u.locality,area:u.area,landmark:u.landmark,gender:u.gender,budgetMin:u.budgetMin,budgetMax:u.budgetMax,facilities:u.facilities,referenceIndex:u.referenceIndex}},{headers:H},sessionId,msg);
- }catch(e){return new Response(JSON.stringify({reply:"Ji, live search me temporary issue aaya. Please same query dobara bhejiye.",grounded:false,error:"Temporary server error"}),{status:200,headers:H})}
+ }catch(e){const errorId=crypto.randomUUID();console.error(`[ERROR_REF:${errorId}] Internal chatbot failure`,{message:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:undefined,timestamp:new Date().toISOString()});return new Response(JSON.stringify({success:false,reply:"Server par temporary connectivity issue aaya hai. Kripya 1 minute baad punah prayas karein.",error:"Temporary server error",error_id:errorId,can_retry:true,grounded:false}),{status:500,headers:{...H,"Retry-After":"60"}})}
});
\ No newline at end of file
From a8cc46bfe59cb82a688834c498c46c23b850851d Mon Sep 17 00:00:00 2001
From: StudentHub
Date: Sun, 8 Nov 2026 21:35:50 +0530
Subject: [PATCH 12/95] Verify chatbot admin settings upsert
---
admin/src/components/ChatbotAdminControls.tsx | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/admin/src/components/ChatbotAdminControls.tsx b/admin/src/components/ChatbotAdminControls.tsx
index d403aa83c..c84a4f9f2 100644
--- a/admin/src/components/ChatbotAdminControls.tsx
+++ b/admin/src/components/ChatbotAdminControls.tsx
@@ -111,10 +111,11 @@ export const ChatbotAdminControls: React.FC = ({
const { data: userData } = await supabase.auth.getUser();
const updatedBy = userData.user?.email || 'admin';
const payload = { ...config, updated_by: updatedBy, updated_at: new Date().toISOString() };
- const { error } = await supabase.from('chatbot_admin_config').update(payload).eq('id', 1);
+ const { data, error } = await supabase.from('chatbot_admin_config').upsert(payload, { onConflict: 'id' }).select('id, updated_at').single();
if (error) throw error;
- setConfig(payload);
- setMessage('AI control settings saved to Supabase.');
+ if (!data || Number(data.id) !== 1) throw new Error('Settings save verify nahi ho payi.');
+ setConfig({ ...payload, updated_at: data.updated_at || payload.updated_at });
+ setMessage('AI control settings saved and verified in Supabase.');
} catch (e: any) {
setMessage(e?.message || 'Unable to save AI controls.');
} finally {
From eee370b0275a25734a20d3dfd841f22939789825 Mon Sep 17 00:00:00 2001
From: StudentHub
Date: Sun, 8 Nov 2026 21:36:18 +0530
Subject: [PATCH 13/95] Add chatbot retry action for transient backend errors
---
widget.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/widget.js b/widget.js
index 3a2922fad..b0873593f 100644
--- a/widget.js
+++ b/widget.js
@@ -34,7 +34,7 @@ const SHH_WIDGET=(()=>{
};
messages.appendChild(wrap);messages.scrollTop=messages.scrollHeight;
}
- async function sendText(value,meta={}){const text=String(value??'').trim()||field.value.trim();if(!text||busy)return;if(/\bappointment\b|\bmeeting\b|\bconsultation\b/i.test(text)&&/(book|schedule|chahiye|chahta|chahti|karna|karo|fix|set|request|meet)/i.test(text)){addMessage(text,false);showAppointmentForm();return}field.value='';addMessage(text,false);busy=true;document.getElementById('shh-status').textContent='Thinking · checking live listings…';try{const ctl=new AbortController(),timer=setTimeout(()=>ctl.abort(),18000);let r;try{r=await fetch(API,{method:'POST',headers:{'Content-Type':'application/json',apikey:KEY,Authorization:'Bearer '+KEY},body:JSON.stringify({message:text,history:history.slice(-8),sessionId,action:meta.action||'',focusPropertyId:meta.focusPropertyId||'',focusPropertyType:meta.focusPropertyType||'',shownPropertyIds:meta.shownPropertyIds||[]}),signal:ctl.signal})}finally{clearTimeout(timer)}if(!r.ok)throw Error('Chat request failed ('+r.status+')');const d=await r.json();lastData=d;const reply=d?.reply||'Ji, main StudentHubHelp par aapki help karta hoon.';addMessage(reply,true,d);history.push({role:'user',text},{role:'assistant',text:reply});if(history.length>16)history=history.slice(-16);const topic=document.getElementById('shh-topic');if(d?.primaryTopic){topic.textContent='Focus · '+strip(d.primaryTopic);topic.style.display='block'}document.getElementById('shh-status').textContent='Ready · Live listing support'}catch(e){addMessage(e?.name==='AbortError'?'Response thoda slow ho gaya. Please try again.':'Ji, live AI service me temporary issue aaya. Please try again.',true);document.getElementById('shh-status').textContent='Ready · Please try again'}finally{busy=false}}
+ async function sendText(value,meta={}){const text=String(value??'').trim()||field.value.trim();if(!text||busy)return;if(/\bappointment\b|\bmeeting\b|\bconsultation\b/i.test(text)&&/(book|schedule|chahiye|chahta|chahti|karna|karo|fix|set|request|meet)/i.test(text)){addMessage(text,false);showAppointmentForm();return}field.value='';addMessage(text,false);busy=true;document.getElementById('shh-status').textContent='Thinking · checking live listings…';try{const ctl=new AbortController(),timer=setTimeout(()=>ctl.abort(),18000);let r;try{r=await fetch(API,{method:'POST',headers:{'Content-Type':'application/json',apikey:KEY,Authorization:'Bearer '+KEY},body:JSON.stringify({message:text,history:history.slice(-8),sessionId,action:meta.action||'',focusPropertyId:meta.focusPropertyId||'',focusPropertyType:meta.focusPropertyType||'',shownPropertyIds:meta.shownPropertyIds||[]}),signal:ctl.signal})}finally{clearTimeout(timer)}const d=await r.json().catch(()=>({}));if(!r.ok){const err=new Error('Chat request failed ('+r.status+')');err.payload=d;err.status=r.status;throw err}lastData=d;const reply=d?.reply||'Ji, main StudentHubHelp par aapki help karta hoon.';addMessage(reply,true,d);history.push({role:'user',text},{role:'assistant',text:reply});if(history.length>16)history=history.slice(-16);const topic=document.getElementById('shh-topic');if(d?.primaryTopic){topic.textContent='Focus · '+strip(d.primaryTopic);topic.style.display='block'}document.getElementById('shh-status').textContent='Ready · Live listing support'}catch(e){const payload=e?.payload||{};if(payload.error_id)console.error('[StudentHubHelp chatbot error]',payload.error_id);addMessage(e?.name==='AbortError'?'Response thoda slow ho gaya. Please try again.':(payload.reply||'Ji, live AI service me temporary issue aaya. Please try again.'),true);if(payload.can_retry||e?.name==='AbortError'||Number(e?.status)>=500){const retryWrap=document.createElement('div');retryWrap.style.cssText='display:flex;justify-content:flex-start;margin:4px 0 8px';const retry=document.createElement('button');retry.type='button';retry.textContent='↻ Retry';retry.style.cssText='border:1px solid #cbd5e1;border-radius:999px;padding:7px 13px;background:#fff;color:#334155;font-size:12px;font-weight:800;cursor:pointer';retry.onclick=()=>{retry.disabled=true;retry.textContent='Retrying…';sendText(text)};retryWrap.appendChild(retry);messages.appendChild(retryWrap);messages.scrollTop=messages.scrollHeight}document.getElementById('shh-status').textContent='Ready · Please try again'}finally{busy=false}}
const tts=header.querySelector('#shh-tts');tts.onclick=()=>{speaking=!speaking;tts.textContent=speaking?'🔊':'🔇';if(!speaking&&window.speechSynthesis)window.speechSynthesis.cancel()};header.querySelector('#shh-max').onclick=()=>{expanded=!expanded;root.classList.toggle('shh-expanded',expanded);header.querySelector('#shh-max').textContent=expanded?'↙':'↗'};header.querySelector('#shh-close-btn').onclick=()=>{win.style.display='none';launcher.style.display='flex'};launcher.onclick=()=>{win.style.display='flex';launcher.style.display='none';messages.scrollTop=messages.scrollHeight};input.querySelector('#shh-send-btn').onclick=()=>sendText();
const field=input.querySelector('#shh-input');field.addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();sendText()}});input.querySelector('#shh-clear').onclick=()=>{history=[];lastData=null;messages.innerHTML='';document.getElementById('shh-topic').style.display='none';addMessage(getWelcome(),true)};
function getWelcome(){return'Namaste! 🙏 Main StudentHubHelp ka **Ultra Advance AI Assistant** hoon.\n\nAap jo bhi requirement batayenge—Hostel, Tiffin, Library, Cafe, Bookstore, area, budget ya nearby location—main aapki requirement samajhkar live StudentHubHelp listings mein relevant options dhoondunga.\n\n**Aapko kis cheez ki jankari chahiye?**'}
From 20de096ed7cee10eac98559aed6e5f6861fd2d69 Mon Sep 17 00:00:00 2001
From: StudentHub
Date: Sun, 8 Nov 2026 21:36:37 +0530
Subject: [PATCH 14/95] Reduce chatbot admin config cache staleness
---
supabase/functions/studenthubhelp-chat/index.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/supabase/functions/studenthubhelp-chat/index.ts b/supabase/functions/studenthubhelp-chat/index.ts
index 53ff0034e..321f71654 100644
--- a/supabase/functions/studenthubhelp-chat/index.ts
+++ b/supabase/functions/studenthubhelp-chat/index.ts
@@ -15,7 +15,7 @@ async function db(path:string):Promise{if(!U||!K)throw Error("database con
type ChatbotRuntimeConfig={enabled:boolean;ai_rerank_enabled:boolean;lead_capture_enabled:boolean;comparison_enabled:boolean;budget_planner_enabled:boolean;max_candidates:number;max_results:number;max_output_tokens:number;temperature:number;maintenance_message:string};
const SAFE_FALLBACK_CONFIG:ChatbotRuntimeConfig={enabled:false,ai_rerank_enabled:false,lead_capture_enabled:false,comparison_enabled:false,budget_planner_enabled:false,max_candidates:10,max_results:1,max_output_tokens:200,temperature:0,maintenance_message:"Chatbot system maintenance par hai. Kripya thodi der baad koshish karein ya seedhe support par sampark karein."};
let runtimeConfigCache:{value:ChatbotRuntimeConfig;expiresAt:number}|null=null;
-async function getRuntimeConfig():Promise{if(runtimeConfigCache&&runtimeConfigCache.expiresAt>Date.now())return runtimeConfigCache.value;try{const rows=await db("/rest/v1/chatbot_admin_config?select=*&id=eq.1&limit=1");const row=rows?.[0];if(!row){console.error("[CRITICAL] chatbot_admin_config row id=1 is missing; disabling chatbot");return SAFE_FALLBACK_CONFIG}const value:ChatbotRuntimeConfig={enabled:row.enabled===true,ai_rerank_enabled:row.ai_rerank_enabled===true,lead_capture_enabled:row.lead_capture_enabled===true,comparison_enabled:row.comparison_enabled===true,budget_planner_enabled:row.budget_planner_enabled===true,max_candidates:Math.max(10,Math.min(100,Number(row.max_candidates)||40)),max_results:Math.max(1,Math.min(20,Number(row.max_results)||8)),max_output_tokens:Math.max(200,Math.min(3000,Number(row.max_output_tokens)||900)),temperature:Math.max(0,Math.min(1,Number.isFinite(Number(row.temperature))?Number(row.temperature):0.2)),maintenance_message:String(row.maintenance_message||"").slice(0,500)};runtimeConfigCache={value,expiresAt:Date.now()+15000};return value}catch(e){console.error("[CRITICAL] chatbot admin config fetch failed; disabling chatbot",e);return SAFE_FALLBACK_CONFIG}}
+async function getRuntimeConfig():Promise{if(runtimeConfigCache&&runtimeConfigCache.expiresAt>Date.now())return runtimeConfigCache.value;try{const rows=await db("/rest/v1/chatbot_admin_config?select=*&id=eq.1&limit=1");const row=rows?.[0];if(!row){console.error("[CRITICAL] chatbot_admin_config row id=1 is missing; disabling chatbot");return SAFE_FALLBACK_CONFIG}const value:ChatbotRuntimeConfig={enabled:row.enabled===true,ai_rerank_enabled:row.ai_rerank_enabled===true,lead_capture_enabled:row.lead_capture_enabled===true,comparison_enabled:row.comparison_enabled===true,budget_planner_enabled:row.budget_planner_enabled===true,max_candidates:Math.max(10,Math.min(100,Number(row.max_candidates)||40)),max_results:Math.max(1,Math.min(20,Number(row.max_results)||8)),max_output_tokens:Math.max(200,Math.min(3000,Number(row.max_output_tokens)||900)),temperature:Math.max(0,Math.min(1,Number.isFinite(Number(row.temperature))?Number(row.temperature):0.2)),maintenance_message:String(row.maintenance_message||"").slice(0,500)};runtimeConfigCache={value,expiresAt:Date.now()+2000};return value}catch(e){console.error("[CRITICAL] chatbot admin config fetch failed; disabling chatbot",e);return SAFE_FALLBACK_CONFIG}}
const LANDMARKS=[{key:"allen",label:"Allen Career Institute Sikar",aliases:["allen","allen sikar","allen coaching","allen career","allen sanskar","allen piprali"],city:"Sikar",lat:27.62624,lng:75.17721},{key:"clc",label:"CLC Sikar",aliases:["clc","clc sikar"],city:"Sikar",lat:27.6206291,lng:75.1616672},{key:"vibrant",label:"Vibrant Academy Sikar",aliases:["vibrant","vibrant coaching","vibrant academy","vibrant academy ssc","vibrant ssc"],city:"Sikar"},{key:"matrix",label:"Matrix Coaching Sikar",aliases:["matrix","matrix academy","matrix coaching","matrix sikar"],city:"Sikar"},{key:"gurukripa",label:"Gurukripa Coaching Sikar",aliases:["gurukripa","guru kripa","gurukripa coaching","gurukripa sikar"],city:"Sikar"}];
function city(s:string){const x=n(s),m:[string,string][]=[["sikar","Sikar"],["piprali","Sikar"],["पिपराली","Sikar"],["सिकर","Sikar"],["kota","Kota"],["talwandi","Kota"],["jaipur","Jaipur"],["ajmer","Ajmer"],["bikaner","Bikaner"],["jodhpur","Jodhpur"],["udaipur","Udaipur"],["alwar","Alwar"],["delhi","Delhi"],["ahmedabad","Ahmedabad"],["lucknow","Lucknow"],["hyderabad","Hyderabad"],["pune","Pune"]];return m.find(([a])=>x.includes(n(a)))?.[1]||""}
function findLandmark(s:string){const x=n(s);for(const l of LANDMARKS)if(l.aliases.some(a=>x.includes(n(a))))return l;return undefined}
From 7e96d582359c35a8ff9145e106c1e27049c45275 Mon Sep 17 00:00:00 2001
From: StudentHub
Date: Sun, 8 Nov 2026 21:41:48 +0530
Subject: [PATCH 15/95] Improve chatbot search pushdown, location labels and
Gemini fallback
---
.../functions/studenthubhelp-chat/index.ts | 21 ++++++++++---------
1 file changed, 11 insertions(+), 10 deletions(-)
diff --git a/supabase/functions/studenthubhelp-chat/index.ts b/supabase/functions/studenthubhelp-chat/index.ts
index 321f71654..d9f3b12b4 100644
--- a/supabase/functions/studenthubhelp-chat/index.ts
+++ b/supabase/functions/studenthubhelp-chat/index.ts
@@ -8,7 +8,7 @@ const H={"Access-Control-Allow-Origin":"*","Access-Control-Allow-Headers":"autho
const SRC=[["hostels","hostel"],["tiffins","tiffin"],["libraries","library"],["cafes","cafe"],["bookstores","bookstore"]] as const;
const n=(v:any)=>String(v??"").toLowerCase().normalize("NFKC").replace(/[^a-z0-9\u0900-\u097f]+/gi," ").replace(/\s+/g," ").trim();
const num=(v:any)=>{const x=Number(String(v??"").replace(/,/g,""));return Number.isFinite(x)?x:undefined};
-function rateKeys(req:Request,sessionId:string){const cfIp=req.headers.get("cf-connecting-ip")?.trim();const ipKey=cfIp?("ip:"+cfIp.slice(0,100)):"ip:unavailable";const sessionKey=("session:"+String(sessionId||"missing").slice(0,160));return [{scopeKey:ipKey,limit:30,windowSeconds:60},{scopeKey:sessionKey,limit:15,windowSeconds:60}]};
+function rateKeys(req:Request,sessionId:string){const cfIp=req.headers.get("cf-connecting-ip")?.trim();const keys:{scopeKey:string;limit:number;windowSeconds:number}[]=[];if(cfIp&&cfIp.length<=100)keys.push({scopeKey:"ip:"+cfIp,limit:30,windowSeconds:60});keys.push({scopeKey:"session:"+String(sessionId||"missing").slice(0,160),limit:15,windowSeconds:60)}
async function consumeDualRateLimit(scope:string,req:Request,sessionId:string,limit:number,windowSeconds:number){for(const item of rateKeys(req,sessionId)){const allowed=await consumeRateLimit(scope,item.scopeKey,Math.min(limit,item.limit),Math.max(windowSeconds,item.windowSeconds));if(!allowed)return false}return true}
async function consumeRateLimit(scope:string,key:string,limit:number,windowSeconds:number){if(!U||!K)throw Error("rate limiter database configuration missing");const r=await fetch(U+"/rest/v1/rpc/consume_chatbot_rate_limit",{method:"POST",headers:{apikey:K,Authorization:"Bearer "+K,"Content-Type":"application/json"},body:JSON.stringify({p_scope:scope,p_key:key,p_limit:limit,p_window_seconds:windowSeconds})});if(!r.ok)throw Error("distributed rate limiter unavailable: "+r.status);return await r.json()===true}
async function db(path:string):Promise{if(!U||!K)throw Error("database configuration missing");const r=await fetch(U+path,{headers:{apikey:K,Authorization:`Bearer ${K}`,"Content-Type":"application/json"}});if(!r.ok)throw Error(`database ${r.status}`);return await r.json() as T}
@@ -26,12 +26,12 @@ function coords(p:any){const lat=Number(p.latitude??p.lat??p.location_latitude??
function distanceKm(a:any,b:any){const A=coords(a),B=coords(b);if(!A||!B)return undefined;const r=(v:number)=>v*Math.PI/180,dLat=r(B.lat-A.lat),dLon=r(B.lng-A.lng),h=Math.sin(dLat/2)**2+Math.cos(r(A.lat))*Math.cos(r(B.lat))*Math.sin(dLon/2)**2;return 6371*2*Math.atan2(Math.sqrt(h),Math.sqrt(1-h))}
function distanceToLandmark(p:any,l:any){const A=coords(p),B=l?{latitude:l.lat,longitude:l.lng}:null;if(!A||!B)return undefined;return distanceKm(A,B)}
function searchText(p:any){return n([p.name,p.title,p.business_name,p.service_name,p.city,p.area,p.service_area,p.address,p.landmark,p.description,p.facilities,p.amenities].filter(Boolean).join(" "))}
-async function direct(type:string,ct?:string){const table=SRC.find(x=>x[1]===type)?.[0];if(!table)return [];const all:any[]=[];let offset=0;const pageSize=500;while(true){const params=new URLSearchParams({select:"*",status:"eq.active",limit:String(pageSize),offset:String(offset)});if(ct)params.set("city",`ilike.*${ct.replace(/[%,()]/g,"")}*`);const rows=await db(`/rest/v1/${table}?${params.toString()}`);const page=Array.isArray(rows)?rows:[];all.push(...page.map(p=>({...p,_type:type})));if(page.length=10000)break}return all.filter(p=>!ct||n(p.city||"").includes(n(ct)))}
-async function multiDirect(types:string[],ct?:string){const unique=[...new Set(types.filter(Boolean))];const groups=await Promise.all(unique.map(type=>direct(type,ct)));return groups.flat()}
+async function direct(type:string,ct?:string,maxBudget?:number){const table=SRC.find(x=>x[1]===type)?.[0];if(!table)return [];const all:any[]=[];let offset=0;const pageSize=500;while(true){const params=new URLSearchParams({select:"*",status:"eq.active",limit:String(pageSize),offset:String(offset)});if(ct)params.set("city",`ilike.*${ct.replace(/[%,()]/g,"")}*`);if(maxBudget!==undefined&&Number.isFinite(maxBudget)&&maxBudget>0){const budget=String(Math.floor(maxBudget));const priceFilter=type==="hostel"?`(monthly_rent.lte.${budget},and(monthly_rent.is.null,monthly_fee.lte.${budget}),and(monthly_rent.is.null,monthly_fee.is.null,price.lte.${budget}))`:`(monthly_fee.lte.${budget},and(monthly_fee.is.null,price.lte.${budget}))`;params.set("or",priceFilter)}const rows=await db(`/rest/v1/${table}?${params.toString()}`);const page=Array.isArray(rows)?rows:[];all.push(...page.map(p=>({...p,_type:type})));if(page.length=10000)break}return all.filter(p=>!ct||n(p.city||"").includes(n(ct)))}
+async function multiDirect(types:string[],ct?:string,maxBudget?:number){const unique=[...new Set(types.filter(Boolean))];const groups=await Promise.all(unique.map(type=>direct(type,ct,maxBudget)));return groups.flat()}
function genderWanted(s:string){const x=n(s);if(/\b(girls|girl|female|ladki|ladkiyo|ladkiyon|महिला|लड़क|लड़की|girls hostel)\b/i.test(x))return "girls";if(/\b(boys|boy|male|ladke|ladkon|लड़के|लड़का|boys hostel)\b/i.test(x))return "boys";return ""}
function areaWanted(s:string){const x=n(s);if(/nawalgarh road|nawalgarh rd|नवलगढ़ रोड/i.test(x))return "nawalgarh";if(/piprali road|piprali rd|पिपराली रोड/i.test(x))return "piprali";return ""}
function priceLabel(p:any,c:string){const v=Number(p.monthly_rent??p.monthly_fee??p.monthly_charge??p.price);if(!Number.isFinite(v)||v<=0)return "";if(c==="hostel"||c==="tiffin"||c==="library")return "₹"+v.toLocaleString("en-IN")+"/mo";return "₹"+v.toLocaleString("en-IN")}
-function card(p:any){const c=p._type,price=priceLabel(p,c),d=p._distanceKm;return{id:String(p.id),name:p.name||p.title||p.service_name||p.business_name||"Active listing",type:c,city:p.city||"",area:p.area||p.service_area||"",address:p.address||"",phone:([p.phone,p.contact_phone,p.whatsapp,p.whatsapp_number].find(v=>{const s=String(v??"").trim();return s&&s!=="0"&&s.replace(/\D/g,"").length>=7})||""),rating:num(p.rating),verified:p.verified===true,price,distanceKm:d===undefined?undefined:Number(d.toFixed(1)),distanceLabel:d===undefined?"":`~${d.toFixed(1)} km`,nearbyMatch:p._nearbyVerified===true||p._nearbyAreaMatch===true,highlights:["wifi","ac_available","ac","cctv","food_available","mess_available","available_beds","room_types","room_sharing","gender_type","timing","timings","delivery_available","nearby_coaching"].map(k=>[k,p[k]]).filter(([,v])=>v!==null&&v!==undefined&&String(v).trim()!==""&&String(v).toLowerCase()!=="false").slice(0,4).map(([k,v])=>k+": "+String(v)),description:p.description&&String(p.description).trim().length<=280?String(p.description).trim():"",mapsUrl:p.google_maps_url||"",detailsUrl:p.slug?`property-details.html?type=${c}&slug=${encodeURIComponent(p.slug)}`:undefined}}
+function card(p:any){const c=p._type,price=priceLabel(p,c),d=p._distanceKm;return{id:String(p.id),name:p.name||p.title||p.service_name||p.business_name||"Active listing",type:c,city:p.city||"",area:p.area||p.service_area||"",address:p.address||"",phone:([p.phone,p.contact_phone,p.whatsapp,p.whatsapp_number].find(v=>{const s=String(v??"").trim();return s&&s!=="0"&&s.replace(/\D/g,"").length>=7})||""),rating:num(p.rating),verified:p.verified===true,price,distanceKm:d===undefined?null:Number(d.toFixed(1)),distanceVerified:d!==undefined,distanceLabel:d===undefined?(p._nearbyAreaMatch?`${p.area||p.service_area||p.city||"Area"} area match · exact distance unavailable`:""):`~${d.toFixed(1)} km (GPS straight-line)`,nearbyMatch:p._nearbyVerified===true||p._nearbyAreaMatch===true,highlights:["wifi","ac_available","ac","cctv","food_available","mess_available","available_beds","room_types","room_sharing","gender_type","timing","timings","delivery_available","nearby_coaching"].map(k=>[k,p[k]]).filter(([,v])=>v!==null&&v!==undefined&&String(v).trim()!==""&&String(v).toLowerCase()!=="false").slice(0,4).map(([k,v])=>k+": "+String(v)),description:p.description&&String(p.description).trim().length<=280?String(p.description).trim():"",mapsUrl:p.google_maps_url||"",detailsUrl:p.slug?`property-details.html?type=${c}&slug=${encodeURIComponent(p.slug)}`:undefined}}
function rank(p:any,q:string,ct:string,near:string){const x=searchText(p),t=n(q);let s=0;const a=areaWanted(q);if(a&&x.includes(a))s+=25;if(ct&&n(p.city||"").includes(n(ct)))s+=30;for(const w of t.split(" ").filter(z=>z.length>2).slice(0,20))if(x.includes(w))s+=2;if(near){if(near==="allen"||near==="clc"){if(p._distanceKm!==undefined)s+=Math.max(0,20-p._distanceKm*5);if(x.includes(near))s+=10}else if(near==="vibrant"){if(x.includes("vibrant"))s+=20;if(x.includes("nawalgarh"))s+=8}else if(near==="matrix"||near==="gurukripa"){if(x.includes(near))s+=30;else s-=3}else if(x.includes(n(near)))s+=20}if(p.verified===true)s+=4;if(Number.isFinite(Number(p.rating)))s+=Math.min(5,Number(p.rating));return s}
function isGreeting(s:string){return /^(hi|hello|hey|namaste|good morning|good afternoon|good evening|good night|gm|gn|hii|hlo|नमस्ते|सुप्रभात|शुभ प्रभात|शुभ संध्या)[!.\s]*$/i.test(s.trim())}
function isAccountSupport(s:string){
@@ -65,9 +65,11 @@ function greeting(){return "Namaste! 🙏 Main StudentHubHelp ka **Ultra Advance
function label(c:string){return c==="hostel"?"hostel / PG":c==="tiffin"?"tiffin service":c==="library"?"library":c==="cafe"?"cafe":c==="bookstore"?"bookstore":"student service"}
function fallbackReply(c:string|undefined,recs:any[],near:string,cityName:string=""){const l=label(c||""),place=near?(near==="allen"?"Allen ke paas":near==="clc"?"CLC ke paas":`${near} area mein`):"";if(recs.length)return `Bilkul 😊 ${place?place+" ":""}${recs.length} active ${l} listing${recs.length>1?"s":""} mili ${recs.length>1?"hain":"hai"}. Neeche live StudentHubHelp options diye hain.`;if(cityName)return `Maine live StudentHubHelp listings check ki hain, lekin abhi **${cityName}** mein koi active matching ${l} listing nahi mili. Main fake options nahi dikhaunga. Aap ${cityName} ka area/landmark bata sakte hain, ya kisi doosri category ki listing search kar sakte hain.`;return `Abhi ${place?place+" ":""}koi matching **${l}** listing nahi mili. Aap city aur area/landmark bataiye; main live listings ko uske hisaab se check karunga.`}
function safeJson(s:string){try{return JSON.parse(s)}catch{const m=s.match(/\{[\s\S]*\}/);if(m)try{return JSON.parse(m[0])}catch{}return null}}
+const SEARCH_ALIASES:[[RegExp,string]]=[[/\b(?:piparli\s+rd|piprali\s+rd|pipli\s+road|piprali)\b/gi,"piprali road"],[/\b(?:allun|allan|allen\s+institute|allen\s+career)\b/gi,"allen"],[/\b(?:sasta|saste|kam\s+budget)\b/gi,"budget"],[/\b(?:ladko\s+ka|ladkon\s+ka)\b/gi,"boys"],[/\b(?:ladkiyo\s+ka|ladkiyon\s+ka)\b/gi,"girls"]];
+function normalizeSearchTerms(input:string){let value=String(input||"").normalize("NFKC");for(const [pattern,replacement] of SEARCH_ALIASES)value=value.replace(pattern,replacement);return value}
function aiIntentDefaults(){return{intent:"general",confidence:0,category:"",categories:[],city:"",locality:"",area:"",landmark:"",nearRelation:"",gender:"",budgetMin:null,budgetMax:null,facilities:[],referenceIndex:null,action:"",followUp:false,correction:false,needsClarification:false,clarificationQuestion:"",searchScope:""}}
function offlineIntent(msg:string,history:any[]){
- const o=aiIntentDefaults(),x=n(msg),defs:[string,RegExp][]=[["hostel",/hostel|\bpg\b|room|stay|accommodation|girls hostel|boys hostel|हॉस्टल|पीजी/],["tiffin",/tiffin|mess|khana|food service|टिफिन|मेस/],["library",/library|reading room|study room|लाइब्रेरी/],["cafe",/cafe|coffee shop|कैफे/],["bookstore",/bookstore|book shop|bookshop|stationery|बुकस्टोर/]];
+ const o=aiIntentDefaults(),x=n(normalizeSearchTerms(msg)),defs:[string,RegExp][]=[["hostel",/hostel|\bpg\b|room|stay|accommodation|girls hostel|boys hostel|हॉस्टल|पीजी/],["tiffin",/tiffin|mess|khana|food service|टिफिन|मेस/],["library",/library|reading room|study room|लाइब्रेरी/],["cafe",/cafe|coffee shop|कैफे/],["bookstore",/bookstore|book shop|bookshop|stationery|बुकस्टोर/]];
const cats=defs.filter(([,re])=>re.test(x)).map(([c])=>c);
o.categories=[...new Set(cats)];o.category=o.categories[0]||cat(msg)||"";
const loc=strictLocation(msg);o.city=loc.city||city(msg)||"";o.locality=loc.locality||"";o.area=loc.area||"";o.landmark=loc.landmark||"";
@@ -133,8 +135,7 @@ CURRENT MESSAGE:
${msg}
RECENT CONVERSATION:
${h}`;
- const r=await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent`,{method:"POST",headers:{"Content-Type":"application/json","x-goog-api-key":GEMINI},body:JSON.stringify({contents:[{role:"user",parts:[{text:prompt}]}],generationConfig:{responseMimeType:"application/json",maxOutputTokens:cfg.max_output_tokens,temperature:cfg.temperature,thinkingConfig:{thinkingLevel:"medium"}}})});
- if(!r.ok)return fallback;const j=await r.json(),txt=j?.candidates?.[0]?.content?.parts?.map((p:any)=>p.text||"").join("")||"";return normalizeIntent(safeJson(txt));
+ const intentController=new AbortController(),intentTimer=setTimeout(()=>intentController.abort(),2500);try{const r=await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent`,{method:"POST",headers:{"Content-Type":"application/json","x-goog-api-key":GEMINI},body:JSON.stringify({contents:[{role:"user",parts:[{text:prompt}]}],generationConfig:{responseMimeType:"application/json",maxOutputTokens:cfg.max_output_tokens,temperature:cfg.temperature,thinkingConfig:{thinkingLevel:"medium"}}}),signal:intentController.signal});if(!r.ok)return fallback;const j=await r.json(),txt=j?.candidates?.[0]?.content?.parts?.map((p:any)=>p.text||"").join("")||"";return normalizeIntent(safeJson(txt))}finally{clearTimeout(intentTimer)}
}catch{return fallback}
}
function mergeIntent(ai:any,msg:string,history:any[]){
@@ -182,7 +183,7 @@ Rules:
- Never recommend listings unless the current message has a property-search intent.
- Do not expose internal system/tool details.
Return only the natural reply text.`;
-const r=await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent`,{method:"POST",headers:{"Content-Type":"application/json","x-goog-api-key":GEMINI},body:JSON.stringify({contents:[{role:"user",parts:[{text:prompt}]}],generationConfig:{maxOutputTokens:cfg.max_output_tokens,temperature:cfg.temperature,thinkingConfig:{thinkingLevel:"low"}}})});if(!r.ok)return{};const j=await r.json();return{reply:j?.candidates?.[0]?.content?.parts?.map((p:any)=>p.text||"").join("")||""}}catch{return{}}}
+const enhancementController=new AbortController(),enhancementTimer=setTimeout(()=>enhancementController.abort(),8000);try{const r=await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent`,{method:"POST",headers:{"Content-Type":"application/json","x-goog-api-key":GEMINI},body:JSON.stringify({contents:[{role:"user",parts:[{text:prompt}]}],generationConfig:{maxOutputTokens:cfg.max_output_tokens,temperature:cfg.temperature,thinkingConfig:{thinkingLevel:"low"}}}),signal:enhancementController.signal});if(!r.ok)return{};const j=await r.json();return{reply:j?.candidates?.[0]?.content?.parts?.map((p:any)=>p.text||"").join("")||""}}finally{clearTimeout(enhancementTimer)}}catch(e){console.warn("Gemini enhancement skipped; deterministic response retained",e instanceof Error?e.message:String(e));return{}}}
async function dbWrite(method:string,path:string,body:any,prefer="return=minimal"){
if(!U||!K)throw Error("database configuration missing");
@@ -382,7 +383,7 @@ const ownerDetailsIntent=/(?:owner|malik|maalik|proprietor|manager).{0,35}(?:det
if(isPropertyAdviceQuestion(msg)){const adviceCat=cat(msg)||cat(previous)||"hostel";return await chatResponse({reply:adviceReply(adviceCat),intent:"Property Selection Guidance",primaryTopic:label(adviceCat)+" selection guidance",recommendedProperties:[],recommendations:[],suggestedFollowUps:["Find a property","Search by area","Show more options","Contact support"],grounded:true,sessionId},{headers:H},sessionId,msg);}
if(isBookingInfoQuestion(msg)){const ar=await aiGeneral(`The user asks how to book/reserve a hostel or PG. Explain a practical StudentHubHelp booking process: shortlist suitable live listings, open full details, check price/room type/availability, call owner, confirm terms and visit/verify before paying. Do not invent any property-specific availability or booking facility.`,history,cfg);const reply=ar.reply||"Hostel book karne ka simple process: pehle suitable listing shortlist karein, full details me price/room type check karein, owner ko Call karke availability aur terms confirm karein, aur payment se pehle property/owner verify karein.";return await chatResponse({reply,intent:"Booking Guidance",primaryTopic:"Hostel / PG Booking",recommendedProperties:[],recommendations:[],suggestedFollowUps:["Allen ke paas hostel dikhao","Budget ke according hostel dikhao","Boys hostel dikhao","Girls hostel dikhao"],grounded:true,sessionId},{headers:H},sessionId,msg);}
if(!propertyMode){const ar=await aiGeneral(msg,history,cfg);const reply=ar.reply||"Bilkul 😊 Main yahin hoon. Aap jo poochna chahein, seedha poochiye—study, ideas, facts ya normal conversation, sab par baat kar sakte hain.";return await chatResponse({reply,intent:"General / Study / Conversation",primaryTopic:"Student AI Assistant",recommendedProperties:[],recommendations:[],suggestedFollowUps:["Ask a study question","Tell me something interesting","Find a property","Contact support"],grounded:true,webGrounded:ar.webGrounded||false,webSources:ar.webSources||[],sessionId},{headers:H},sessionId,msg);}
- let rows=await multiDirect(aiCats.length?aiCats:(c?[c]:(nearCats.length?nearCats:["hostel"])),ct);
+ let rows=await multiDirect(aiCats.length?aiCats:(c?[c]:(nearCats.length?nearCats:["hostel"])),ct,u.budgetMax??undefined);
const wantedGender=u.gender||genderWanted(msg),wantedArea=u.area?(/nawalgarh/i.test(u.area)?"nawalgarh":/piprali/i.test(u.area)?"piprali":u.area.toLowerCase()):areaWanted(msg);
const wantedLocality=n(u.locality||"");
if(wantedGender)rows=rows.filter(p=>{const z=searchText(p);const g=n([p.gender_type,p.gender,p.hostel_type,p.name,p.title,p.description].filter(Boolean).join(" "));return wantedGender==="girls"?/girls|girl|female|women|ladki|ladkiyon|महिला|लड़क/i.test(g):/boys|boy|male|men|ladke|लड़के/i.test(g)});
@@ -395,7 +396,7 @@ const ownerDetailsIntent=/(?:owner|malik|maalik|proprietor|manager).{0,35}(?:det
if(u.budgetMax!==null||u.budgetMin!==null)rows=rows.filter(p=>{const v=budgetValue(p);if(v===undefined)return false;return (u.budgetMax===null||v<=u.budgetMax)&&(u.budgetMin===null||v>=u.budgetMin)});
if(u.facilities?.length)rows=rows.filter(p=>matchesFacility(p,u.facilities));
const landmark=u.landmark?findLandmark(u.landmark)||LANDMARKS.find(l=>l.key===u.landmark):findLandmark(near||"");
- if(landmark){for(const p of rows){const d=distanceToLandmark(p,landmark);if(d!==undefined)p._distanceKm=d}rows=rows.filter(p=>n(p.city||"")===n(landmark.city)&&((p._distanceKm!==undefined&&p._distanceKm<=10)||searchText(p).includes(n(landmark.key))))}
+ if(landmark){for(const p of rows){const d=distanceToLandmark(p,landmark);if(d!==undefined)p._distanceKm=d;else if(searchText(p).includes(n(landmark.key)))p._nearbyAreaMatch=true}rows=rows.filter(p=>n(p.city||"")===n(landmark.city)&&((p._distanceKm!==undefined&&p._distanceKm<=10)||p._nearbyAreaMatch===true))}
else if(near){const q=n(near);const exact=rows.filter(p=>searchText(p).includes(q));if(exact.length)rows=exact;else if(!wantedArea)rows=[]}
if(nearCats.length){const secondaryRows=await multiDirect(nearCats,ct);for(const p of rows){let best=Infinity,hasCoords=false;for(const q of secondaryRows){const d=distanceKm(p,q);if(d!==undefined){hasCoords=true;best=Math.min(best,d)}else if(p.area&&searchText(q).includes(n(p.area)))best=Math.min(best,1)}if(best<=3){p._nearbyDistanceKm=best;p._nearbyVerified=hasCoords;p._nearbyAreaMatch=!hasCoords}}rows=rows.filter(p=>p._nearbyDistanceKm!==undefined)}
rows=rows.filter(p=>!shownIds.has(String(p.id)));
From 9acec1ca1f6c463e3dcbf890723404de073a1879 Mon Sep 17 00:00:00 2001
From: StudentHub
Date: Sun, 8 Nov 2026 21:41:58 +0530
Subject: [PATCH 16/95] Align embedded chatbot request timeout to 15 seconds
---
widget.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/widget.js b/widget.js
index b0873593f..050e20dad 100644
--- a/widget.js
+++ b/widget.js
@@ -34,7 +34,7 @@ const SHH_WIDGET=(()=>{
};
messages.appendChild(wrap);messages.scrollTop=messages.scrollHeight;
}
- async function sendText(value,meta={}){const text=String(value??'').trim()||field.value.trim();if(!text||busy)return;if(/\bappointment\b|\bmeeting\b|\bconsultation\b/i.test(text)&&/(book|schedule|chahiye|chahta|chahti|karna|karo|fix|set|request|meet)/i.test(text)){addMessage(text,false);showAppointmentForm();return}field.value='';addMessage(text,false);busy=true;document.getElementById('shh-status').textContent='Thinking · checking live listings…';try{const ctl=new AbortController(),timer=setTimeout(()=>ctl.abort(),18000);let r;try{r=await fetch(API,{method:'POST',headers:{'Content-Type':'application/json',apikey:KEY,Authorization:'Bearer '+KEY},body:JSON.stringify({message:text,history:history.slice(-8),sessionId,action:meta.action||'',focusPropertyId:meta.focusPropertyId||'',focusPropertyType:meta.focusPropertyType||'',shownPropertyIds:meta.shownPropertyIds||[]}),signal:ctl.signal})}finally{clearTimeout(timer)}const d=await r.json().catch(()=>({}));if(!r.ok){const err=new Error('Chat request failed ('+r.status+')');err.payload=d;err.status=r.status;throw err}lastData=d;const reply=d?.reply||'Ji, main StudentHubHelp par aapki help karta hoon.';addMessage(reply,true,d);history.push({role:'user',text},{role:'assistant',text:reply});if(history.length>16)history=history.slice(-16);const topic=document.getElementById('shh-topic');if(d?.primaryTopic){topic.textContent='Focus · '+strip(d.primaryTopic);topic.style.display='block'}document.getElementById('shh-status').textContent='Ready · Live listing support'}catch(e){const payload=e?.payload||{};if(payload.error_id)console.error('[StudentHubHelp chatbot error]',payload.error_id);addMessage(e?.name==='AbortError'?'Response thoda slow ho gaya. Please try again.':(payload.reply||'Ji, live AI service me temporary issue aaya. Please try again.'),true);if(payload.can_retry||e?.name==='AbortError'||Number(e?.status)>=500){const retryWrap=document.createElement('div');retryWrap.style.cssText='display:flex;justify-content:flex-start;margin:4px 0 8px';const retry=document.createElement('button');retry.type='button';retry.textContent='↻ Retry';retry.style.cssText='border:1px solid #cbd5e1;border-radius:999px;padding:7px 13px;background:#fff;color:#334155;font-size:12px;font-weight:800;cursor:pointer';retry.onclick=()=>{retry.disabled=true;retry.textContent='Retrying…';sendText(text)};retryWrap.appendChild(retry);messages.appendChild(retryWrap);messages.scrollTop=messages.scrollHeight}document.getElementById('shh-status').textContent='Ready · Please try again'}finally{busy=false}}
+ async function sendText(value,meta={}){const text=String(value??'').trim()||field.value.trim();if(!text||busy)return;if(/\bappointment\b|\bmeeting\b|\bconsultation\b/i.test(text)&&/(book|schedule|chahiye|chahta|chahti|karna|karo|fix|set|request|meet)/i.test(text)){addMessage(text,false);showAppointmentForm();return}field.value='';addMessage(text,false);busy=true;document.getElementById('shh-status').textContent='Thinking · checking live listings…';try{const ctl=new AbortController(),timer=setTimeout(()=>ctl.abort(),15000);let r;try{r=await fetch(API,{method:'POST',headers:{'Content-Type':'application/json',apikey:KEY,Authorization:'Bearer '+KEY},body:JSON.stringify({message:text,history:history.slice(-8),sessionId,action:meta.action||'',focusPropertyId:meta.focusPropertyId||'',focusPropertyType:meta.focusPropertyType||'',shownPropertyIds:meta.shownPropertyIds||[]}),signal:ctl.signal})}finally{clearTimeout(timer)}const d=await r.json().catch(()=>({}));if(!r.ok){const err=new Error('Chat request failed ('+r.status+')');err.payload=d;err.status=r.status;throw err}lastData=d;const reply=d?.reply||'Ji, main StudentHubHelp par aapki help karta hoon.';addMessage(reply,true,d);history.push({role:'user',text},{role:'assistant',text:reply});if(history.length>16)history=history.slice(-16);const topic=document.getElementById('shh-topic');if(d?.primaryTopic){topic.textContent='Focus · '+strip(d.primaryTopic);topic.style.display='block'}document.getElementById('shh-status').textContent='Ready · Live listing support'}catch(e){const payload=e?.payload||{};if(payload.error_id)console.error('[StudentHubHelp chatbot error]',payload.error_id);addMessage(e?.name==='AbortError'?'Response thoda slow ho gaya. Please try again.':(payload.reply||'Ji, live AI service me temporary issue aaya. Please try again.'),true);if(payload.can_retry||e?.name==='AbortError'||Number(e?.status)>=500){const retryWrap=document.createElement('div');retryWrap.style.cssText='display:flex;justify-content:flex-start;margin:4px 0 8px';const retry=document.createElement('button');retry.type='button';retry.textContent='↻ Retry';retry.style.cssText='border:1px solid #cbd5e1;border-radius:999px;padding:7px 13px;background:#fff;color:#334155;font-size:12px;font-weight:800;cursor:pointer';retry.onclick=()=>{retry.disabled=true;retry.textContent='Retrying…';sendText(text)};retryWrap.appendChild(retry);messages.appendChild(retryWrap);messages.scrollTop=messages.scrollHeight}document.getElementById('shh-status').textContent='Ready · Please try again'}finally{busy=false}}
const tts=header.querySelector('#shh-tts');tts.onclick=()=>{speaking=!speaking;tts.textContent=speaking?'🔊':'🔇';if(!speaking&&window.speechSynthesis)window.speechSynthesis.cancel()};header.querySelector('#shh-max').onclick=()=>{expanded=!expanded;root.classList.toggle('shh-expanded',expanded);header.querySelector('#shh-max').textContent=expanded?'↙':'↗'};header.querySelector('#shh-close-btn').onclick=()=>{win.style.display='none';launcher.style.display='flex'};launcher.onclick=()=>{win.style.display='flex';launcher.style.display='none';messages.scrollTop=messages.scrollHeight};input.querySelector('#shh-send-btn').onclick=()=>sendText();
const field=input.querySelector('#shh-input');field.addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();sendText()}});input.querySelector('#shh-clear').onclick=()=>{history=[];lastData=null;messages.innerHTML='';document.getElementById('shh-topic').style.display='none';addMessage(getWelcome(),true)};
function getWelcome(){return'Namaste! 🙏 Main StudentHubHelp ka **Ultra Advance AI Assistant** hoon.\n\nAap jo bhi requirement batayenge—Hostel, Tiffin, Library, Cafe, Bookstore, area, budget ya nearby location—main aapki requirement samajhkar live StudentHubHelp listings mein relevant options dhoondunga.\n\n**Aapko kis cheez ki jankari chahiye?**'}
From 2a00fa8f91f13b5949f022422dc1657ad3532dd3 Mon Sep 17 00:00:00 2001
From: StudentHub
Date: Sun, 8 Nov 2026 21:42:30 +0530
Subject: [PATCH 17/95] Correct rate key return and normalize location aliases
---
supabase/functions/studenthubhelp-chat/index.ts | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/supabase/functions/studenthubhelp-chat/index.ts b/supabase/functions/studenthubhelp-chat/index.ts
index d9f3b12b4..72c261604 100644
--- a/supabase/functions/studenthubhelp-chat/index.ts
+++ b/supabase/functions/studenthubhelp-chat/index.ts
@@ -8,7 +8,7 @@ const H={"Access-Control-Allow-Origin":"*","Access-Control-Allow-Headers":"autho
const SRC=[["hostels","hostel"],["tiffins","tiffin"],["libraries","library"],["cafes","cafe"],["bookstores","bookstore"]] as const;
const n=(v:any)=>String(v??"").toLowerCase().normalize("NFKC").replace(/[^a-z0-9\u0900-\u097f]+/gi," ").replace(/\s+/g," ").trim();
const num=(v:any)=>{const x=Number(String(v??"").replace(/,/g,""));return Number.isFinite(x)?x:undefined};
-function rateKeys(req:Request,sessionId:string){const cfIp=req.headers.get("cf-connecting-ip")?.trim();const keys:{scopeKey:string;limit:number;windowSeconds:number}[]=[];if(cfIp&&cfIp.length<=100)keys.push({scopeKey:"ip:"+cfIp,limit:30,windowSeconds:60});keys.push({scopeKey:"session:"+String(sessionId||"missing").slice(0,160),limit:15,windowSeconds:60)}
+function rateKeys(req:Request,sessionId:string){const cfIp=req.headers.get("cf-connecting-ip")?.trim();const keys:{scopeKey:string;limit:number;windowSeconds:number}[]=[];if(cfIp&&cfIp.length<=100)keys.push({scopeKey:"ip:"+cfIp,limit:30,windowSeconds:60});keys.push({scopeKey:"session:"+String(sessionId||"missing").slice(0,160),limit:15,windowSeconds:60)};return keys}
async function consumeDualRateLimit(scope:string,req:Request,sessionId:string,limit:number,windowSeconds:number){for(const item of rateKeys(req,sessionId)){const allowed=await consumeRateLimit(scope,item.scopeKey,Math.min(limit,item.limit),Math.max(windowSeconds,item.windowSeconds));if(!allowed)return false}return true}
async function consumeRateLimit(scope:string,key:string,limit:number,windowSeconds:number){if(!U||!K)throw Error("rate limiter database configuration missing");const r=await fetch(U+"/rest/v1/rpc/consume_chatbot_rate_limit",{method:"POST",headers:{apikey:K,Authorization:"Bearer "+K,"Content-Type":"application/json"},body:JSON.stringify({p_scope:scope,p_key:key,p_limit:limit,p_window_seconds:windowSeconds})});if(!r.ok)throw Error("distributed rate limiter unavailable: "+r.status);return await r.json()===true}
async function db(path:string):Promise{if(!U||!K)throw Error("database configuration missing");const r=await fetch(U+path,{headers:{apikey:K,Authorization:`Bearer ${K}`,"Content-Type":"application/json"}});if(!r.ok)throw Error(`database ${r.status}`);return await r.json() as T}
@@ -18,8 +18,8 @@ let runtimeConfigCache:{value:ChatbotRuntimeConfig;expiresAt:number}|null=null;
async function getRuntimeConfig():Promise{if(runtimeConfigCache&&runtimeConfigCache.expiresAt>Date.now())return runtimeConfigCache.value;try{const rows=await db("/rest/v1/chatbot_admin_config?select=*&id=eq.1&limit=1");const row=rows?.[0];if(!row){console.error("[CRITICAL] chatbot_admin_config row id=1 is missing; disabling chatbot");return SAFE_FALLBACK_CONFIG}const value:ChatbotRuntimeConfig={enabled:row.enabled===true,ai_rerank_enabled:row.ai_rerank_enabled===true,lead_capture_enabled:row.lead_capture_enabled===true,comparison_enabled:row.comparison_enabled===true,budget_planner_enabled:row.budget_planner_enabled===true,max_candidates:Math.max(10,Math.min(100,Number(row.max_candidates)||40)),max_results:Math.max(1,Math.min(20,Number(row.max_results)||8)),max_output_tokens:Math.max(200,Math.min(3000,Number(row.max_output_tokens)||900)),temperature:Math.max(0,Math.min(1,Number.isFinite(Number(row.temperature))?Number(row.temperature):0.2)),maintenance_message:String(row.maintenance_message||"").slice(0,500)};runtimeConfigCache={value,expiresAt:Date.now()+2000};return value}catch(e){console.error("[CRITICAL] chatbot admin config fetch failed; disabling chatbot",e);return SAFE_FALLBACK_CONFIG}}
const LANDMARKS=[{key:"allen",label:"Allen Career Institute Sikar",aliases:["allen","allen sikar","allen coaching","allen career","allen sanskar","allen piprali"],city:"Sikar",lat:27.62624,lng:75.17721},{key:"clc",label:"CLC Sikar",aliases:["clc","clc sikar"],city:"Sikar",lat:27.6206291,lng:75.1616672},{key:"vibrant",label:"Vibrant Academy Sikar",aliases:["vibrant","vibrant coaching","vibrant academy","vibrant academy ssc","vibrant ssc"],city:"Sikar"},{key:"matrix",label:"Matrix Coaching Sikar",aliases:["matrix","matrix academy","matrix coaching","matrix sikar"],city:"Sikar"},{key:"gurukripa",label:"Gurukripa Coaching Sikar",aliases:["gurukripa","guru kripa","gurukripa coaching","gurukripa sikar"],city:"Sikar"}];
function city(s:string){const x=n(s),m:[string,string][]=[["sikar","Sikar"],["piprali","Sikar"],["पिपराली","Sikar"],["सिकर","Sikar"],["kota","Kota"],["talwandi","Kota"],["jaipur","Jaipur"],["ajmer","Ajmer"],["bikaner","Bikaner"],["jodhpur","Jodhpur"],["udaipur","Udaipur"],["alwar","Alwar"],["delhi","Delhi"],["ahmedabad","Ahmedabad"],["lucknow","Lucknow"],["hyderabad","Hyderabad"],["pune","Pune"]];return m.find(([a])=>x.includes(n(a)))?.[1]||""}
-function findLandmark(s:string){const x=n(s);for(const l of LANDMARKS)if(l.aliases.some(a=>x.includes(n(a))))return l;return undefined}
-function nearTerm(s:string){const x=n(s),lm=findLandmark(x);if(lm)return lm.key;const known=["vibrant coaching","vibrant career institute","nawalgarh road","nawalgarh rd","nawalgarh","clc","allen","gurukripa","matrix","matrix academy","prayas eduhub","banco career academy","inspector ssc academy"];const hit=known.find(k=>x.includes(n(k)));if(hit)return hit;const m=x.match(/(?:near|nearby|around|paas|pass|ke paas|k pass|aas paas|bagal|nazdeek|पास|आस पास|बगल)(?:\s+me)?\s+([a-z0-9\u0900-\u097f][a-z0-9\u0900-\u097f ]{1,50}?)(?=\s+(?:chahiye|dikhao|dikhado|hai|ho|rehna|rahna|book|konsa|kaunsa|aur|bhi)\b|$)/i);if(m)return m[1].trim();if(/(?:piprali road|piprali rd|पिपराली रोड)/i.test(x))return "piprali road";return ""}
+function findLandmark(s:string){const x=n(normalizeSearchTerms(s));for(const l of LANDMARKS)if(l.aliases.some(a=>x.includes(n(a))))return l;return undefined}
+function nearTerm(s:string){const x=n(normalizeSearchTerms(s)),lm=findLandmark(x);if(lm)return lm.key;const known=["vibrant coaching","vibrant career institute","nawalgarh road","nawalgarh rd","nawalgarh","clc","allen","gurukripa","matrix","matrix academy","prayas eduhub","banco career academy","inspector ssc academy"];const hit=known.find(k=>x.includes(n(k)));if(hit)return hit;const m=x.match(/(?:near|nearby|around|paas|pass|ke paas|k pass|aas paas|bagal|nazdeek|पास|आस पास|बगल)(?:\s+me)?\s+([a-z0-9\u0900-\u097f][a-z0-9\u0900-\u097f ]{1,50}?)(?=\s+(?:chahiye|dikhao|dikhado|hai|ho|rehna|rahna|book|konsa|kaunsa|aur|bhi)\b|$)/i);if(m)return m[1].trim();if(/(?:piprali road|piprali rd|पिपराली रोड)/i.test(x))return "piprali road";return ""}
function cat(s:string){const x=n(s),hits:[string,number][]=[];const defs:[string,RegExp][]=[["hostel",/hostel|pg|room|stay|accommodation|girls|boys|female|male|rehne|rehna|रहने|हॉस्टल|पीजी/],["tiffin",/tiffin|mess|khana|food|meal|jain food|टिफिन|मेस|खाना/],["library",/library|reading|seat|लाइब्रेरी/],["cafe",/cafe|coffee|restaurant|hangout|कैफे/],["bookstore",/bookstore|books|stationery|bookshop|किताब|बुकस्टोर/]];for(const [type,re] of defs)for(const m of x.matchAll(new RegExp(re.source,"gi")))hits.push([type,m.index??-1]);const near=new Set(hits.filter(([,pos])=>/(near|nearby|around|paas|pass|ke paas|k pass|aas paas|bagal|nazdeek|पास|आस पास|बगल)/i.test(x.slice(Math.max(0,pos-50),pos))).map(([type])=>type));return hits.find(([type])=>!near.has(type))?.[0]||hits[0]?.[0]}
function secondaryCats(s:string){const x=n(s),out:string[]=[];const defs:[string,RegExp][]=[["hostel",/hostel|pg|room|stay|accommodation|girls|boys|female|male|rehne|rehna|रहने|हॉस्टल|पीजी/],["tiffin",/tiffin|mess|khana|food|meal|टिफिन|मेस|खाना/],["library",/library|reading|seat|लाइब्रेरी/],["cafe",/cafe|coffee|restaurant|hangout|कैफे/],["bookstore",/bookstore|books|stationery|bookshop|किताब|बुकस्टोर/]];for(const [type,re] of defs)for(const m of x.matchAll(new RegExp(re.source,"gi"))){const pos=m.index??-1;if(/(near|nearby|around|paas|pass|ke paas|k pass|aas paas|bagal|nazdeek|पास|आस पास|बगल)/i.test(x.slice(Math.max(0,pos-50),pos)))out.push(type)}return [...new Set(out)]}
function coords(p:any){const lat=Number(p.latitude??p.lat??p.location_latitude??p.latitude_deg),lng=Number(p.longitude??p.lng??p.lon??p.location_longitude??p.longitude_deg);return Number.isFinite(lat)&&Number.isFinite(lng)?{lat,lng}:null}
@@ -28,8 +28,8 @@ function distanceToLandmark(p:any,l:any){const A=coords(p),B=l?{latitude:l.lat,l
function searchText(p:any){return n([p.name,p.title,p.business_name,p.service_name,p.city,p.area,p.service_area,p.address,p.landmark,p.description,p.facilities,p.amenities].filter(Boolean).join(" "))}
async function direct(type:string,ct?:string,maxBudget?:number){const table=SRC.find(x=>x[1]===type)?.[0];if(!table)return [];const all:any[]=[];let offset=0;const pageSize=500;while(true){const params=new URLSearchParams({select:"*",status:"eq.active",limit:String(pageSize),offset:String(offset)});if(ct)params.set("city",`ilike.*${ct.replace(/[%,()]/g,"")}*`);if(maxBudget!==undefined&&Number.isFinite(maxBudget)&&maxBudget>0){const budget=String(Math.floor(maxBudget));const priceFilter=type==="hostel"?`(monthly_rent.lte.${budget},and(monthly_rent.is.null,monthly_fee.lte.${budget}),and(monthly_rent.is.null,monthly_fee.is.null,price.lte.${budget}))`:`(monthly_fee.lte.${budget},and(monthly_fee.is.null,price.lte.${budget}))`;params.set("or",priceFilter)}const rows=await db(`/rest/v1/${table}?${params.toString()}`);const page=Array.isArray(rows)?rows:[];all.push(...page.map(p=>({...p,_type:type})));if(page.length=10000)break}return all.filter(p=>!ct||n(p.city||"").includes(n(ct)))}
async function multiDirect(types:string[],ct?:string,maxBudget?:number){const unique=[...new Set(types.filter(Boolean))];const groups=await Promise.all(unique.map(type=>direct(type,ct,maxBudget)));return groups.flat()}
-function genderWanted(s:string){const x=n(s);if(/\b(girls|girl|female|ladki|ladkiyo|ladkiyon|महिला|लड़क|लड़की|girls hostel)\b/i.test(x))return "girls";if(/\b(boys|boy|male|ladke|ladkon|लड़के|लड़का|boys hostel)\b/i.test(x))return "boys";return ""}
-function areaWanted(s:string){const x=n(s);if(/nawalgarh road|nawalgarh rd|नवलगढ़ रोड/i.test(x))return "nawalgarh";if(/piprali road|piprali rd|पिपराली रोड/i.test(x))return "piprali";return ""}
+function genderWanted(s:string){const x=n(normalizeSearchTerms(s));if(/\b(girls|girl|female|ladki|ladkiyo|ladkiyon|महिला|लड़क|लड़की|girls hostel)\b/i.test(x))return "girls";if(/\b(boys|boy|male|ladke|ladkon|लड़के|लड़का|boys hostel)\b/i.test(x))return "boys";return ""}
+function areaWanted(s:string){const x=n(normalizeSearchTerms(s));if(/nawalgarh road|nawalgarh rd|नवलगढ़ रोड/i.test(x))return "nawalgarh";if(/piprali road|piprali rd|पिपराली रोड/i.test(x))return "piprali";return ""}
function priceLabel(p:any,c:string){const v=Number(p.monthly_rent??p.monthly_fee??p.monthly_charge??p.price);if(!Number.isFinite(v)||v<=0)return "";if(c==="hostel"||c==="tiffin"||c==="library")return "₹"+v.toLocaleString("en-IN")+"/mo";return "₹"+v.toLocaleString("en-IN")}
function card(p:any){const c=p._type,price=priceLabel(p,c),d=p._distanceKm;return{id:String(p.id),name:p.name||p.title||p.service_name||p.business_name||"Active listing",type:c,city:p.city||"",area:p.area||p.service_area||"",address:p.address||"",phone:([p.phone,p.contact_phone,p.whatsapp,p.whatsapp_number].find(v=>{const s=String(v??"").trim();return s&&s!=="0"&&s.replace(/\D/g,"").length>=7})||""),rating:num(p.rating),verified:p.verified===true,price,distanceKm:d===undefined?null:Number(d.toFixed(1)),distanceVerified:d!==undefined,distanceLabel:d===undefined?(p._nearbyAreaMatch?`${p.area||p.service_area||p.city||"Area"} area match · exact distance unavailable`:""):`~${d.toFixed(1)} km (GPS straight-line)`,nearbyMatch:p._nearbyVerified===true||p._nearbyAreaMatch===true,highlights:["wifi","ac_available","ac","cctv","food_available","mess_available","available_beds","room_types","room_sharing","gender_type","timing","timings","delivery_available","nearby_coaching"].map(k=>[k,p[k]]).filter(([,v])=>v!==null&&v!==undefined&&String(v).trim()!==""&&String(v).toLowerCase()!=="false").slice(0,4).map(([k,v])=>k+": "+String(v)),description:p.description&&String(p.description).trim().length<=280?String(p.description).trim():"",mapsUrl:p.google_maps_url||"",detailsUrl:p.slug?`property-details.html?type=${c}&slug=${encodeURIComponent(p.slug)}`:undefined}}
function rank(p:any,q:string,ct:string,near:string){const x=searchText(p),t=n(q);let s=0;const a=areaWanted(q);if(a&&x.includes(a))s+=25;if(ct&&n(p.city||"").includes(n(ct)))s+=30;for(const w of t.split(" ").filter(z=>z.length>2).slice(0,20))if(x.includes(w))s+=2;if(near){if(near==="allen"||near==="clc"){if(p._distanceKm!==undefined)s+=Math.max(0,20-p._distanceKm*5);if(x.includes(near))s+=10}else if(near==="vibrant"){if(x.includes("vibrant"))s+=20;if(x.includes("nawalgarh"))s+=8}else if(near==="matrix"||near==="gurukripa"){if(x.includes(near))s+=30;else s-=3}else if(x.includes(n(near)))s+=20}if(p.verified===true)s+=4;if(Number.isFinite(Number(p.rating)))s+=Math.min(5,Number(p.rating));return s}
@@ -109,9 +109,9 @@ function deterministicLocation(s:string){
const lm=findLandmark(x);if(lm)return{city:lm.city,landmark:lm.key};return{};
}
function strictLocation(s:string){
- const d=deterministicLocation(s),x=n(s);
+ const normalized=normalizeSearchTerms(s),d=deterministicLocation(normalized),x=n(normalized);
if(d.city||d.locality||d.area||d.landmark)return d;
- const c=city(s);return c?{city:c}:{};
+ const c=city(normalized);return c?{city:c}:{};
}
async function aiUnderstand(msg:string,history:any[],cfg:ChatbotRuntimeConfig=DEFAULT_RUNTIME_CONFIG){
const fallback=offlineIntent(msg,history);if(!GEMINI)return fallback;
From 54827588d6391be12ff006fc353344785ec0871c Mon Sep 17 00:00:00 2001
From: StudentHub
Date: Sun, 8 Nov 2026 21:43:12 +0530
Subject: [PATCH 18/95] Bound Gemini calls to shared 12-second request deadline
---
.../functions/studenthubhelp-chat/index.ts | 29 ++++++++++---------
1 file changed, 15 insertions(+), 14 deletions(-)
diff --git a/supabase/functions/studenthubhelp-chat/index.ts b/supabase/functions/studenthubhelp-chat/index.ts
index 72c261604..e944c4da0 100644
--- a/supabase/functions/studenthubhelp-chat/index.ts
+++ b/supabase/functions/studenthubhelp-chat/index.ts
@@ -8,7 +8,7 @@ const H={"Access-Control-Allow-Origin":"*","Access-Control-Allow-Headers":"autho
const SRC=[["hostels","hostel"],["tiffins","tiffin"],["libraries","library"],["cafes","cafe"],["bookstores","bookstore"]] as const;
const n=(v:any)=>String(v??"").toLowerCase().normalize("NFKC").replace(/[^a-z0-9\u0900-\u097f]+/gi," ").replace(/\s+/g," ").trim();
const num=(v:any)=>{const x=Number(String(v??"").replace(/,/g,""));return Number.isFinite(x)?x:undefined};
-function rateKeys(req:Request,sessionId:string){const cfIp=req.headers.get("cf-connecting-ip")?.trim();const keys:{scopeKey:string;limit:number;windowSeconds:number}[]=[];if(cfIp&&cfIp.length<=100)keys.push({scopeKey:"ip:"+cfIp,limit:30,windowSeconds:60});keys.push({scopeKey:"session:"+String(sessionId||"missing").slice(0,160),limit:15,windowSeconds:60)};return keys}
+function rateKeys(req:Request,sessionId:string){const cfIp=req.headers.get("cf-connecting-ip")?.trim();const keys:{scopeKey:string;limit:number;windowSeconds:number}[]=[];if(cfIp&&cfIp.length<=100)keys.push({scopeKey:"ip:"+cfIp,limit:30,windowSeconds:60});keys.push({scopeKey:"session:"+String(sessionId||"missing").slice(0,160),limit:15,windowSeconds:60});return keys};return keys}
async function consumeDualRateLimit(scope:string,req:Request,sessionId:string,limit:number,windowSeconds:number){for(const item of rateKeys(req,sessionId)){const allowed=await consumeRateLimit(scope,item.scopeKey,Math.min(limit,item.limit),Math.max(windowSeconds,item.windowSeconds));if(!allowed)return false}return true}
async function consumeRateLimit(scope:string,key:string,limit:number,windowSeconds:number){if(!U||!K)throw Error("rate limiter database configuration missing");const r=await fetch(U+"/rest/v1/rpc/consume_chatbot_rate_limit",{method:"POST",headers:{apikey:K,Authorization:"Bearer "+K,"Content-Type":"application/json"},body:JSON.stringify({p_scope:scope,p_key:key,p_limit:limit,p_window_seconds:windowSeconds})});if(!r.ok)throw Error("distributed rate limiter unavailable: "+r.status);return await r.json()===true}
async function db(path:string):Promise{if(!U||!K)throw Error("database configuration missing");const r=await fetch(U+path,{headers:{apikey:K,Authorization:`Bearer ${K}`,"Content-Type":"application/json"}});if(!r.ok)throw Error(`database ${r.status}`);return await r.json() as T}
@@ -113,7 +113,7 @@ function strictLocation(s:string){
if(d.city||d.locality||d.area||d.landmark)return d;
const c=city(normalized);return c?{city:c}:{};
}
-async function aiUnderstand(msg:string,history:any[],cfg:ChatbotRuntimeConfig=DEFAULT_RUNTIME_CONFIG){
+async function aiUnderstand(msg:string,history:any[],cfg:ChatbotRuntimeConfig=DEFAULT_RUNTIME_CONFIG,deadlineAt?:number){
const fallback=offlineIntent(msg,history);if(!GEMINI)return fallback;
try{
const h=history.slice(-8).map(x=>`${x.role==="user"?"USER":"ASSISTANT"}: ${String(x.text||"").slice(0,1200)}`).join("\n");
@@ -135,7 +135,7 @@ CURRENT MESSAGE:
${msg}
RECENT CONVERSATION:
${h}`;
- const intentController=new AbortController(),intentTimer=setTimeout(()=>intentController.abort(),2500);try{const r=await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent`,{method:"POST",headers:{"Content-Type":"application/json","x-goog-api-key":GEMINI},body:JSON.stringify({contents:[{role:"user",parts:[{text:prompt}]}],generationConfig:{responseMimeType:"application/json",maxOutputTokens:cfg.max_output_tokens,temperature:cfg.temperature,thinkingConfig:{thinkingLevel:"medium"}}}),signal:intentController.signal});if(!r.ok)return fallback;const j=await r.json(),txt=j?.candidates?.[0]?.content?.parts?.map((p:any)=>p.text||"").join("")||"";return normalizeIntent(safeJson(txt))}finally{clearTimeout(intentTimer)}
+ const intentTimeout=Math.min(2500,Math.max(0,deadlineAt===undefined?2500:deadlineAt-Date.now()));if(intentTimeout<100)return fallback;const intentController=new AbortController(),intentTimer=setTimeout(()=>intentController.abort(),intentTimeout);try{const r=await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent`,{method:"POST",headers:{"Content-Type":"application/json","x-goog-api-key":GEMINI},body:JSON.stringify({contents:[{role:"user",parts:[{text:prompt}]}],generationConfig:{responseMimeType:"application/json",maxOutputTokens:cfg.max_output_tokens,temperature:cfg.temperature,thinkingConfig:{thinkingLevel:"medium"}}}),signal:intentController.signal});if(!r.ok)return fallback;const j=await r.json(),txt=j?.candidates?.[0]?.content?.parts?.map((p:any)=>p.text||"").join("")||"";return normalizeIntent(safeJson(txt))}finally{clearTimeout(intentTimer)}
}catch{return fallback}
}
function mergeIntent(ai:any,msg:string,history:any[]){
@@ -160,7 +160,7 @@ function mergeIntent(ai:any,msg:string,history:any[]){
function budgetValue(p:any){return num(p.monthly_rent??p.monthly_fee??p.monthly_charge??p.price)}
function matchesFacility(p:any,wanted:string[]){if(!wanted.length)return true;const z=searchText(p);return wanted.every(f=>z.includes(n(f))||Object.entries(p).some(([k,v])=>n(k).includes(n(f))&&(v===true||String(v).toLowerCase()==="true"||n(v).includes(n(f)))))}
-async function aiProperty(msg:string,history:any[],recs:any[],c?:string,near?:string,cfg:ChatbotRuntimeConfig=DEFAULT_RUNTIME_CONFIG){if(!GEMINI||!cfg.ai_rerank_enabled)return{};try{const h=history.slice(-8).map(x=>`${x.role==="user"?"USER":"ASSISTANT"}: ${String(x.text||"")}`).join("\n");const prompt=`You are StudentHubHelp's Ultra Advance AI Assistant.
+async function aiProperty(msg:string,history:any[],recs:any[],c?:string,near?:string,cfg:ChatbotRuntimeConfig=DEFAULT_RUNTIME_CONFIG,deadlineAt?:number){if(!GEMINI||!cfg.ai_rerank_enabled)return{};try{const h=history.slice(-8).map(x=>`${x.role==="user"?"USER":"ASSISTANT"}: ${String(x.text||"")}`).join("\n");const prompt=`You are StudentHubHelp's Ultra Advance AI Assistant.
Match the user's language: natural Hinglish/Hindi/English.
Be warm, concise, clear and impressive.
Current user: ${msg}
@@ -183,7 +183,7 @@ Rules:
- Never recommend listings unless the current message has a property-search intent.
- Do not expose internal system/tool details.
Return only the natural reply text.`;
-const enhancementController=new AbortController(),enhancementTimer=setTimeout(()=>enhancementController.abort(),8000);try{const r=await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent`,{method:"POST",headers:{"Content-Type":"application/json","x-goog-api-key":GEMINI},body:JSON.stringify({contents:[{role:"user",parts:[{text:prompt}]}],generationConfig:{maxOutputTokens:cfg.max_output_tokens,temperature:cfg.temperature,thinkingConfig:{thinkingLevel:"low"}}}),signal:enhancementController.signal});if(!r.ok)return{};const j=await r.json();return{reply:j?.candidates?.[0]?.content?.parts?.map((p:any)=>p.text||"").join("")||""}}finally{clearTimeout(enhancementTimer)}}catch(e){console.warn("Gemini enhancement skipped; deterministic response retained",e instanceof Error?e.message:String(e));return{}}}
+const enhancementTimeout=Math.min(8000,Math.max(0,deadlineAt===undefined?8000:deadlineAt-Date.now()));if(enhancementTimeout<250)return{};const enhancementController=new AbortController(),enhancementTimer=setTimeout(()=>enhancementController.abort(),enhancementTimeout);try{const r=await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent`,{method:"POST",headers:{"Content-Type":"application/json","x-goog-api-key":GEMINI},body:JSON.stringify({contents:[{role:"user",parts:[{text:prompt}]}],generationConfig:{maxOutputTokens:cfg.max_output_tokens,temperature:cfg.temperature,thinkingConfig:{thinkingLevel:"low"}}}),signal:enhancementController.signal});if(!r.ok)return{};const j=await r.json();return{reply:j?.candidates?.[0]?.content?.parts?.map((p:any)=>p.text||"").join("")||""}}finally{clearTimeout(enhancementTimer)}}catch(e){console.warn("Gemini enhancement skipped; deterministic response retained",e instanceof Error?e.message:String(e));return{}}}
async function dbWrite(method:string,path:string,body:any,prefer="return=minimal"){
if(!U||!K)throw Error("database configuration missing");
@@ -234,7 +234,7 @@ function offlineGeneralReply(msg:string){
if(/hello|hi|hey|namaste|kaise ho|kya haal/i.test(x))return "Namaste 😊 Main yahin hoon—study questions, general knowledge, ideas ya StudentHubHelp listings mein help kar sakta hoon. Aaj kya karna hai?";
return "Main bina external AI API key ke live StudentHubHelp listings, location-based search, booking guidance aur kuch common study/general questions handle kar sakta hoon. Is sawaal ka reliable jawab dene ke liye thoda aur context ya specific question bhej dijiye 😊";
}
-async function aiGeneral(msg:string,history:any[],cfg:ChatbotRuntimeConfig=DEFAULT_RUNTIME_CONFIG){const offline=offlineGeneralReply(msg);if(!GEMINI||offline!=="Main bina external AI API key ke live StudentHubHelp listings, location-based search, booking guidance aur kuch common study/general questions handle kar sakta hoon. Is sawaal ka reliable jawab dene ke liye thoda aur context ya specific question bhej dijiye 😊")return{reply:offline,webGrounded:false,webSources:[]};try{const h=history.slice(-8).map(x=>({role:x.role==="assistant"?"model":"user",parts:[{text:String(x.text||"").slice(0,1500)}]}));const system=`You are StudentHubHelp's Ultra Advance AI Assistant for students.
+async function aiGeneral(msg:string,history:any[],cfg:ChatbotRuntimeConfig=DEFAULT_RUNTIME_CONFIG,deadlineAt?:number){const offline=offlineGeneralReply(msg);if(!GEMINI||offline!=="Main bina external AI API key ke live StudentHubHelp listings, location-based search, booking guidance aur kuch common study/general questions handle kar sakta hoon. Is sawaal ka reliable jawab dene ke liye thoda aur context ya specific question bhej dijiye 😊")return{reply:offline,webGrounded:false,webSources:[]};try{const h=history.slice(-8).map(x=>({role:x.role==="assistant"?"model":"user",parts:[{text:String(x.text||"").slice(0,1500)}]}));const system=`You are StudentHubHelp's Ultra Advance AI Assistant for students.
You are a natural conversational, study and general knowledge assistant.
Match the user's language and tone: Hindi, Hinglish or English.
You can discuss normal life, boredom, tiredness, motivation, hobbies and casual topics in a friendly respectful way.
@@ -244,9 +244,10 @@ Do not claim StudentHubHelp property facts here. Property data belongs to the se
Do not pretend to know personal facts about the user.
Keep normal chat concise and human; for study questions be clear and useful.
Current message: ${msg}`;
-const body:any={contents:[...h,{role:"user",parts:[{text:system}]}],generationConfig:{maxOutputTokens:cfg.max_output_tokens,temperature:cfg.temperature,thinkingConfig:{thinkingLevel:"medium"}}};if(wantsCurrentWeb(msg))body.tools=[{google_search:{}}];const r=await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent`,{method:"POST",headers:{"Content-Type":"application/json","x-goog-api-key":GEMINI},body:JSON.stringify(body)});if(!r.ok)return{reply:offlineGeneralReply(msg),webGrounded:false,webSources:[]};const j=await r.json();const reply=j?.candidates?.[0]?.content?.parts?.map((p:any)=>p.text||"").join("")||"";const chunks=j?.candidates?.[0]?.groundingMetadata?.groundingChunks||[];const sources=chunks.map((x:any)=>x?.web?.uri&&{url:x.web.uri,title:x.web.title||x.web.uri}).filter(Boolean).slice(0,5);return{reply:reply||offlineGeneralReply(msg),webGrounded:sources.length>0,webSources:sources}}catch{return{reply:offlineGeneralReply(msg),webGrounded:false,webSources:[]}}}
+const body:any={contents:[...h,{role:"user",parts:[{text:system}]}],generationConfig:{maxOutputTokens:cfg.max_output_tokens,temperature:cfg.temperature,thinkingConfig:{thinkingLevel:"medium"}}};if(wantsCurrentWeb(msg))body.tools=[{google_search:{}}];const aiTimeout=Math.min(8000,Math.max(0,deadlineAt===undefined?8000:deadlineAt-Date.now()));if(aiTimeout<250)return{reply:offlineGeneralReply(msg),webGrounded:false,webSources:[]};const aiController=new AbortController(),aiTimer=setTimeout(()=>aiController.abort(),aiTimeout);let r:Response;try{r=await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent`,{method:"POST",headers:{"Content-Type":"application/json","x-goog-api-key":GEMINI},body:JSON.stringify(body),signal:aiController.signal})}finally{clearTimeout(aiTimer)};if(!r.ok)return{reply:offlineGeneralReply(msg),webGrounded:false,webSources:[]};const j=await r.json();const reply=j?.candidates?.[0]?.content?.parts?.map((p:any)=>p.text||"").join("")||"";const chunks=j?.candidates?.[0]?.groundingMetadata?.groundingChunks||[];const sources=chunks.map((x:any)=>x?.web?.uri&&{url:x.web.uri,title:x.web.title||x.web.uri}).filter(Boolean).slice(0,5);return{reply:reply||offlineGeneralReply(msg),webGrounded:sources.length>0,webSources:sources}}catch{return{reply:offlineGeneralReply(msg),webGrounded:false,webSources:[]}}}
Deno.serve(async(req:Request)=>{
+ const requestDeadlineAt=Date.now()+12000;
if(req.method==="OPTIONS")return new Response("ok",{headers:H});
if(req.method!=="POST")return new Response(JSON.stringify({error:"POST required"}),{status:405,headers:H});
try{
@@ -297,14 +298,14 @@ Deno.serve(async(req:Request)=>{
const reply=validPhone?"Ji 😊 **"+fp.name+"** ka listed phone number: **"+validPhone+"**":"Is listing ka valid phone number abhi StudentHubHelp data me available nahi hai.";
return await chatResponse({reply,intent:"Property Contact",primaryTopic:label(focusType)+" contact",recommendedProperties:[fp],recommendations:[fp],suggestedFollowUps:["Iske full details batao","Allen ke paas aur options dikhao"],grounded:true,sessionId},{headers:H},sessionId,msg);
}
- const ar=await aiProperty(msg,history,[fp],focusType,nearTerm(focusCombined),cfg);
+ const ar=await aiProperty(msg,history,[fp],focusType,nearTerm(focusCombined),cfg,requestDeadlineAt);
const reply=ar.reply||("Bilkul 😊 **"+fp.name+"** ki available details neeche di hain.");
return await chatResponse({reply,intent:"Property Details",primaryTopic:label(focusType)+" details",recommendedProperties:[fp],recommendations:[fp],suggestedFollowUps:["Iska phone number batao","Allen ke paas aur options dikhao"],grounded:true,sessionId},{headers:H},sessionId,msg);
}
}
if(isAccountSupport(msg))return await chatResponse({reply:accountSupportReply(msg),intent:"Account / Login Support",primaryTopic:"Account access",suggestedFollowUps:["I forgot my password","Google login is not working","Dashboard is not opening","Contact support"],recommendations:[],recommendedProperties:[],grounded:true,sessionId},{headers:H},sessionId,msg);
if(isContact(msg)||String(b?.action||"")==="contact_support")return await chatResponse({reply:"Ji bilkul 😊 StudentHubHelp team se contact ke liye:\n\n📧 Email: studenthubhelpp@gmail.com\n📞 Phone: +91 9929718264",intent:"Support / Contact",primaryTopic:"Contact StudentHubHelp",suggestedFollowUps:["Find a hostel","Find a library","Search by area","Back to search"],recommendations:[],recommendedProperties:[],grounded:true,sessionId},{headers:H},sessionId,msg);
- const previous=history.filter(x=>x.role==="user").map(x=>x.text||"").join(" "),combined=previous+" "+msg,aiRaw=await aiUnderstand(msg,history,cfg),u=mergeIntent(aiRaw,msg,history),explicitCat=cat(msg),nearCats=secondaryCats(msg),aiCats=u.categories||[],c=u.category||explicitCat||(u.followUp?cat(previous):""),propertyMode=u.intent.startsWith("property_")||isPropertyQuery(msg,u.followUp?previous:""),ct=u.city||(u.followUp?city(previous):""),near=u.landmark||u.area||u.locality||(u.nearRelation?nearTerm(msg):""),locationForFilter=strictLocation(msg),action=String(b?.action||u.action||""),shownList=(Array.isArray(b?.shownPropertyIds)?b.shownPropertyIds:[]).map((x:any)=>String(x)),shownIds=new Set(shownList);
+ const previous=history.filter(x=>x.role==="user").map(x=>x.text||"").join(" "),combined=previous+" "+msg,aiRaw=await aiUnderstand(msg,history,cfg,requestDeadlineAt),u=mergeIntent(aiRaw,msg,history),explicitCat=cat(msg),nearCats=secondaryCats(msg),aiCats=u.categories||[],c=u.category||explicitCat||(u.followUp?cat(previous):""),propertyMode=u.intent.startsWith("property_")||isPropertyQuery(msg,u.followUp?previous:""),ct=u.city||(u.followUp?city(previous):""),near=u.landmark||u.area||u.locality||(u.nearRelation?nearTerm(msg):""),locationForFilter=strictLocation(msg),action=String(b?.action||u.action||""),shownList=(Array.isArray(b?.shownPropertyIds)?b.shownPropertyIds:[]).map((x:any)=>String(x)),shownIds=new Set(shownList);
if(action==="search_area"&&!city(msg)&&!nearTerm(msg))return await chatResponse({reply:"Bilkul 😊 Aap kis **area / locality** mein search karna chahte hain? Area ka naam bhejiye, main current requirement ke saath search refine kar dunga.",intent:"Area Refinement",primaryTopic:c?label(c)+" discovery":"Student Services Discovery",recommendedProperties:[],recommendations:[],suggestedFollowUps:["Search by city","Show more options","Contact support"],grounded:true,sessionId},{headers:H},sessionId,msg);
if(u.needsClarification&&u.clarificationQuestion&&!u.category&&!u.categories.length&&!u.locality&&!u.area&&!u.landmark)return await chatResponse({reply:u.clarificationQuestion,intent:"Clarification",primaryTopic:"Requirement clarification",recommendedProperties:[],recommendations:[],suggestedFollowUps:["Find a hostel","Find a library","Find a cafe","Find a tiffin service"],grounded:true,sessionId},{headers:H},sessionId,msg);
if(u.intent==="property_reference"&&u.referenceIndex){
@@ -314,7 +315,7 @@ Deno.serve(async(req:Request)=>{
if(target){
const fp=card(target),wantPhone=u.action==="phone"||isPhoneIntent(msg);
if(wantPhone){const ph=String(fp.phone||"").trim();const reply=ph&&ph.replace(/\\D/g,"").length>=7?"Ji 😊 **"+fp.name+"** ka listed phone number: **"+ph+"**":"Is listing ka valid phone number abhi StudentHubHelp data me available nahi hai.";return await chatResponse({reply,intent:"Property Contact",primaryTopic:"Referenced property contact",recommendedProperties:[fp],recommendations:[fp],suggestedFollowUps:["Iske full details batao","Aur options dikhao"],grounded:true,sessionId},{headers:H},sessionId,msg)}
- const ar=await aiProperty(msg,history,[fp],fp._type,near,cfg),reply=ar.reply||("Bilkul 😊 **"+fp.name+"** ki available details neeche di hain.");return await chatResponse({reply,intent:"Property Reference",primaryTopic:"Referenced property",recommendedProperties:[fp],recommendations:[fp],suggestedFollowUps:["Iska phone number batao","Aur options dikhao"],grounded:true,sessionId},{headers:H},sessionId,msg)
+ const ar=await aiProperty(msg,history,[fp],fp._type,near,cfg,requestDeadlineAt),reply=ar.reply||("Bilkul 😊 **"+fp.name+"** ki available details neeche di hain.");return await chatResponse({reply,intent:"Property Reference",primaryTopic:"Referenced property",recommendedProperties:[fp],recommendations:[fp],suggestedFollowUps:["Iska phone number batao","Aur options dikhao"],grounded:true,sessionId},{headers:H},sessionId,msg)
}
}
if(cfg.comparison_enabled&&(/(?:konsa|kaunsa|kaunsi|which|should i|chahiye).{0,35}(book|hostel|pg)|(?:book|hostel|pg).{0,35}(konsa|kaunsa|kaunsi|which|should)/i.test(n(msg)))&&!isBookingInfoQuestion(msg)){
@@ -324,7 +325,7 @@ Deno.serve(async(req:Request)=>{
let decision=decisionRows.filter(p=>!shownIds.has(String(p.id)));
if(decisionLandmark)decision=decision.filter(p=>n(p.city||"")===n(decisionLandmark.city)&&((p._distanceKm!==undefined&&p._distanceKm<=10)||searchText(p).includes(n(decisionLandmark.key))));
const rankedDecision=decision.map(p=>({...p,_score:rank(p,combined,ct,near)})).sort((a,z)=>z._score-a._score).slice(0,cfg.max_results).map(card);
- const ar=await aiProperty(msg,history,rankedDecision,c||"hostel",near,cfg);
+ const ar=await aiProperty(msg,history,rankedDecision,c||"hostel",near,cfg,requestDeadlineAt);
const reply=ar.reply||"Aapki requirement ke hisaab se live options compare kar sakte hain.";
return await chatResponse({reply,intent:"Property Comparison",primaryTopic:label(c||"hostel")+" comparison",recommendedProperties:rankedDecision,recommendations:rankedDecision,suggestedFollowUps:["Boys hostel dikhao","Girls hostel dikhao","Budget ke according dikhao"],grounded:true,sessionId},{headers:H},sessionId,msg);
}
@@ -381,8 +382,8 @@ const ownerDetailsIntent=/(?:owner|malik|maalik|proprietor|manager).{0,35}(?:det
}
if(isPropertyAdviceQuestion(msg)){const adviceCat=cat(msg)||cat(previous)||"hostel";return await chatResponse({reply:adviceReply(adviceCat),intent:"Property Selection Guidance",primaryTopic:label(adviceCat)+" selection guidance",recommendedProperties:[],recommendations:[],suggestedFollowUps:["Find a property","Search by area","Show more options","Contact support"],grounded:true,sessionId},{headers:H},sessionId,msg);}
- if(isBookingInfoQuestion(msg)){const ar=await aiGeneral(`The user asks how to book/reserve a hostel or PG. Explain a practical StudentHubHelp booking process: shortlist suitable live listings, open full details, check price/room type/availability, call owner, confirm terms and visit/verify before paying. Do not invent any property-specific availability or booking facility.`,history,cfg);const reply=ar.reply||"Hostel book karne ka simple process: pehle suitable listing shortlist karein, full details me price/room type check karein, owner ko Call karke availability aur terms confirm karein, aur payment se pehle property/owner verify karein.";return await chatResponse({reply,intent:"Booking Guidance",primaryTopic:"Hostel / PG Booking",recommendedProperties:[],recommendations:[],suggestedFollowUps:["Allen ke paas hostel dikhao","Budget ke according hostel dikhao","Boys hostel dikhao","Girls hostel dikhao"],grounded:true,sessionId},{headers:H},sessionId,msg);}
- if(!propertyMode){const ar=await aiGeneral(msg,history,cfg);const reply=ar.reply||"Bilkul 😊 Main yahin hoon. Aap jo poochna chahein, seedha poochiye—study, ideas, facts ya normal conversation, sab par baat kar sakte hain.";return await chatResponse({reply,intent:"General / Study / Conversation",primaryTopic:"Student AI Assistant",recommendedProperties:[],recommendations:[],suggestedFollowUps:["Ask a study question","Tell me something interesting","Find a property","Contact support"],grounded:true,webGrounded:ar.webGrounded||false,webSources:ar.webSources||[],sessionId},{headers:H},sessionId,msg);}
+ if(isBookingInfoQuestion(msg)){const ar=await aiGeneral(`The user asks how to book/reserve a hostel or PG. Explain a practical StudentHubHelp booking process: shortlist suitable live listings, open full details, check price/room type/availability, call owner, confirm terms and visit/verify before paying. Do not invent any property-specific availability or booking facility.`,history,cfg,requestDeadlineAt);const reply=ar.reply||"Hostel book karne ka simple process: pehle suitable listing shortlist karein, full details me price/room type check karein, owner ko Call karke availability aur terms confirm karein, aur payment se pehle property/owner verify karein.";return await chatResponse({reply,intent:"Booking Guidance",primaryTopic:"Hostel / PG Booking",recommendedProperties:[],recommendations:[],suggestedFollowUps:["Allen ke paas hostel dikhao","Budget ke according hostel dikhao","Boys hostel dikhao","Girls hostel dikhao"],grounded:true,sessionId},{headers:H},sessionId,msg);}
+ if(!propertyMode){const ar=await aiGeneral(msg,history,cfg,requestDeadlineAt);const reply=ar.reply||"Bilkul 😊 Main yahin hoon. Aap jo poochna chahein, seedha poochiye—study, ideas, facts ya normal conversation, sab par baat kar sakte hain.";return await chatResponse({reply,intent:"General / Study / Conversation",primaryTopic:"Student AI Assistant",recommendedProperties:[],recommendations:[],suggestedFollowUps:["Ask a study question","Tell me something interesting","Find a property","Contact support"],grounded:true,webGrounded:ar.webGrounded||false,webSources:ar.webSources||[],sessionId},{headers:H},sessionId,msg);}
let rows=await multiDirect(aiCats.length?aiCats:(c?[c]:(nearCats.length?nearCats:["hostel"])),ct,u.budgetMax??undefined);
const wantedGender=u.gender||genderWanted(msg),wantedArea=u.area?(/nawalgarh/i.test(u.area)?"nawalgarh":/piprali/i.test(u.area)?"piprali":u.area.toLowerCase()):areaWanted(msg);
const wantedLocality=n(u.locality||"");
@@ -403,7 +404,7 @@ const ownerDetailsIntent=/(?:owner|malik|maalik|proprietor|manager).{0,35}(?:det
const ranked=rows.map(p=>({...p,_score:rank(p,combined,ct,near)})).sort((a,z)=>z._score-a._score).slice(0,cfg.max_candidates);
let recs=ranked.slice(0,cfg.max_results).map(card);
if(action==="view_details"){const focusId=String(b?.focusPropertyId||""),focusType=String(b?.focusPropertyType||c||"hostel"),focusRows=await multiDirect([focusType],ct),focus=focusRows.find(p=>String(p.id)===focusId);recs=focus?[card(focus)]:recs.slice(0,1)}
- const ar=await aiProperty(msg,history,recs,c,near,cfg),reply=ar.reply||fallbackReply(c,recs,near,ct),followups=["View full details","Search by area","Show more options","Contact support"];
+ const ar=await aiProperty(msg,history,recs,c,near,cfg,requestDeadlineAt),reply=ar.reply||fallbackReply(c,recs,near,ct),followups=["View full details","Search by area","Show more options","Contact support"];
return await chatResponse({reply,intent:"Live Property Search",primaryTopic:c?label(c)+" discovery":"Student Services Discovery",recommendedProperties:recs,recommendations:recs,suggestedFollowUps:followups,grounded:true,sessionId,searchMode:"ultra_advance_live_supabase_ai",targetCity:ct||undefined,targetCategory:c||undefined,nearbyTerm:near||undefined,aiIntent:u.intent,aiConfidence:u.confidence,aiEntities:{category:u.category,categories:u.categories,city:u.city,locality:u.locality,area:u.area,landmark:u.landmark,gender:u.gender,budgetMin:u.budgetMin,budgetMax:u.budgetMax,facilities:u.facilities,referenceIndex:u.referenceIndex}},{headers:H},sessionId,msg);
}catch(e){const errorId=crypto.randomUUID();console.error(`[ERROR_REF:${errorId}] Internal chatbot failure`,{message:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:undefined,timestamp:new Date().toISOString()});return new Response(JSON.stringify({success:false,reply:"Server par temporary connectivity issue aaya hai. Kripya 1 minute baad punah prayas karein.",error:"Temporary server error",error_id:errorId,can_retry:true,grounded:false}),{status:500,headers:{...H,"Retry-After":"60"}})}
});
\ No newline at end of file
From 4fef81b714f7fc47583cab298989e4a4d445a973 Mon Sep 17 00:00:00 2001
From: StudentHub
Date: Sun, 8 Nov 2026 21:43:31 +0530
Subject: [PATCH 19/95] Fix rate key function and search alias typing
---
supabase/functions/studenthubhelp-chat/index.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/supabase/functions/studenthubhelp-chat/index.ts b/supabase/functions/studenthubhelp-chat/index.ts
index e944c4da0..6578a6656 100644
--- a/supabase/functions/studenthubhelp-chat/index.ts
+++ b/supabase/functions/studenthubhelp-chat/index.ts
@@ -8,7 +8,7 @@ const H={"Access-Control-Allow-Origin":"*","Access-Control-Allow-Headers":"autho
const SRC=[["hostels","hostel"],["tiffins","tiffin"],["libraries","library"],["cafes","cafe"],["bookstores","bookstore"]] as const;
const n=(v:any)=>String(v??"").toLowerCase().normalize("NFKC").replace(/[^a-z0-9\u0900-\u097f]+/gi," ").replace(/\s+/g," ").trim();
const num=(v:any)=>{const x=Number(String(v??"").replace(/,/g,""));return Number.isFinite(x)?x:undefined};
-function rateKeys(req:Request,sessionId:string){const cfIp=req.headers.get("cf-connecting-ip")?.trim();const keys:{scopeKey:string;limit:number;windowSeconds:number}[]=[];if(cfIp&&cfIp.length<=100)keys.push({scopeKey:"ip:"+cfIp,limit:30,windowSeconds:60});keys.push({scopeKey:"session:"+String(sessionId||"missing").slice(0,160),limit:15,windowSeconds:60});return keys};return keys}
+function rateKeys(req:Request,sessionId:string){const cfIp=req.headers.get("cf-connecting-ip")?.trim();const keys:{scopeKey:string;limit:number;windowSeconds:number}[]=[];if(cfIp&&cfIp.length<=100)keys.push({scopeKey:"ip:"+cfIp,limit:30,windowSeconds:60});keys.push({scopeKey:"session:"+String(sessionId||"missing").slice(0,160),limit:15,windowSeconds:60});return keys}
async function consumeDualRateLimit(scope:string,req:Request,sessionId:string,limit:number,windowSeconds:number){for(const item of rateKeys(req,sessionId)){const allowed=await consumeRateLimit(scope,item.scopeKey,Math.min(limit,item.limit),Math.max(windowSeconds,item.windowSeconds));if(!allowed)return false}return true}
async function consumeRateLimit(scope:string,key:string,limit:number,windowSeconds:number){if(!U||!K)throw Error("rate limiter database configuration missing");const r=await fetch(U+"/rest/v1/rpc/consume_chatbot_rate_limit",{method:"POST",headers:{apikey:K,Authorization:"Bearer "+K,"Content-Type":"application/json"},body:JSON.stringify({p_scope:scope,p_key:key,p_limit:limit,p_window_seconds:windowSeconds})});if(!r.ok)throw Error("distributed rate limiter unavailable: "+r.status);return await r.json()===true}
async function db(path:string):Promise{if(!U||!K)throw Error("database configuration missing");const r=await fetch(U+path,{headers:{apikey:K,Authorization:`Bearer ${K}`,"Content-Type":"application/json"}});if(!r.ok)throw Error(`database ${r.status}`);return await r.json() as T}
@@ -65,7 +65,7 @@ function greeting(){return "Namaste! 🙏 Main StudentHubHelp ka **Ultra Advance
function label(c:string){return c==="hostel"?"hostel / PG":c==="tiffin"?"tiffin service":c==="library"?"library":c==="cafe"?"cafe":c==="bookstore"?"bookstore":"student service"}
function fallbackReply(c:string|undefined,recs:any[],near:string,cityName:string=""){const l=label(c||""),place=near?(near==="allen"?"Allen ke paas":near==="clc"?"CLC ke paas":`${near} area mein`):"";if(recs.length)return `Bilkul 😊 ${place?place+" ":""}${recs.length} active ${l} listing${recs.length>1?"s":""} mili ${recs.length>1?"hain":"hai"}. Neeche live StudentHubHelp options diye hain.`;if(cityName)return `Maine live StudentHubHelp listings check ki hain, lekin abhi **${cityName}** mein koi active matching ${l} listing nahi mili. Main fake options nahi dikhaunga. Aap ${cityName} ka area/landmark bata sakte hain, ya kisi doosri category ki listing search kar sakte hain.`;return `Abhi ${place?place+" ":""}koi matching **${l}** listing nahi mili. Aap city aur area/landmark bataiye; main live listings ko uske hisaab se check karunga.`}
function safeJson(s:string){try{return JSON.parse(s)}catch{const m=s.match(/\{[\s\S]*\}/);if(m)try{return JSON.parse(m[0])}catch{}return null}}
-const SEARCH_ALIASES:[[RegExp,string]]=[[/\b(?:piparli\s+rd|piprali\s+rd|pipli\s+road|piprali)\b/gi,"piprali road"],[/\b(?:allun|allan|allen\s+institute|allen\s+career)\b/gi,"allen"],[/\b(?:sasta|saste|kam\s+budget)\b/gi,"budget"],[/\b(?:ladko\s+ka|ladkon\s+ka)\b/gi,"boys"],[/\b(?:ladkiyo\s+ka|ladkiyon\s+ka)\b/gi,"girls"]];
+const SEARCH_ALIASES:[RegExp,string][]=[[/\b(?:piparli\s+rd|piprali\s+rd|pipli\s+road|piprali)\b/gi,"piprali road"],[/\b(?:allun|allan|allen\s+institute|allen\s+career)\b/gi,"allen"],[/\b(?:sasta|saste|kam\s+budget)\b/gi,"budget"],[/\b(?:ladko\s+ka|ladkon\s+ka)\b/gi,"boys"],[/\b(?:ladkiyo\s+ka|ladkiyon\s+ka)\b/gi,"girls"]];
function normalizeSearchTerms(input:string){let value=String(input||"").normalize("NFKC");for(const [pattern,replacement] of SEARCH_ALIASES)value=value.replace(pattern,replacement);return value}
function aiIntentDefaults(){return{intent:"general",confidence:0,category:"",categories:[],city:"",locality:"",area:"",landmark:"",nearRelation:"",gender:"",budgetMin:null,budgetMax:null,facilities:[],referenceIndex:null,action:"",followUp:false,correction:false,needsClarification:false,clarificationQuestion:"",searchScope:""}}
function offlineIntent(msg:string,history:any[]){
From 6f97cded4d3caf26632eb67b549a4f640d768b78 Mon Sep 17 00:00:00 2001
From: StudentHub
Date: Sun, 8 Nov 2026 21:44:23 +0530
Subject: [PATCH 20/95] Fail closed when trusted client IP header is
unavailable
---
supabase/functions/studenthubhelp-chat/index.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/supabase/functions/studenthubhelp-chat/index.ts b/supabase/functions/studenthubhelp-chat/index.ts
index 6578a6656..5dafff7cc 100644
--- a/supabase/functions/studenthubhelp-chat/index.ts
+++ b/supabase/functions/studenthubhelp-chat/index.ts
@@ -9,7 +9,7 @@ const SRC=[["hostels","hostel"],["tiffins","tiffin"],["libraries","library"],["c
const n=(v:any)=>String(v??"").toLowerCase().normalize("NFKC").replace(/[^a-z0-9\u0900-\u097f]+/gi," ").replace(/\s+/g," ").trim();
const num=(v:any)=>{const x=Number(String(v??"").replace(/,/g,""));return Number.isFinite(x)?x:undefined};
function rateKeys(req:Request,sessionId:string){const cfIp=req.headers.get("cf-connecting-ip")?.trim();const keys:{scopeKey:string;limit:number;windowSeconds:number}[]=[];if(cfIp&&cfIp.length<=100)keys.push({scopeKey:"ip:"+cfIp,limit:30,windowSeconds:60});keys.push({scopeKey:"session:"+String(sessionId||"missing").slice(0,160),limit:15,windowSeconds:60});return keys}
-async function consumeDualRateLimit(scope:string,req:Request,sessionId:string,limit:number,windowSeconds:number){for(const item of rateKeys(req,sessionId)){const allowed=await consumeRateLimit(scope,item.scopeKey,Math.min(limit,item.limit),Math.max(windowSeconds,item.windowSeconds));if(!allowed)return false}return true}
+async function consumeDualRateLimit(scope:string,req:Request,sessionId:string,limit:number,windowSeconds:number){if(!req.headers.get("cf-connecting-ip")?.trim())throw Error("trusted Cloudflare client IP header unavailable");for(const item of rateKeys(req,sessionId)){const allowed=await consumeRateLimit(scope,item.scopeKey,Math.min(limit,item.limit),Math.max(windowSeconds,item.windowSeconds));if(!allowed)return false}return true}
async function consumeRateLimit(scope:string,key:string,limit:number,windowSeconds:number){if(!U||!K)throw Error("rate limiter database configuration missing");const r=await fetch(U+"/rest/v1/rpc/consume_chatbot_rate_limit",{method:"POST",headers:{apikey:K,Authorization:"Bearer "+K,"Content-Type":"application/json"},body:JSON.stringify({p_scope:scope,p_key:key,p_limit:limit,p_window_seconds:windowSeconds})});if(!r.ok)throw Error("distributed rate limiter unavailable: "+r.status);return await r.json()===true}
async function db(path:string):Promise{if(!U||!K)throw Error("database configuration missing");const r=await fetch(U+path,{headers:{apikey:K,Authorization:`Bearer ${K}`,"Content-Type":"application/json"}});if(!r.ok)throw Error(`database ${r.status}`);return await r.json() as T}
type ChatbotRuntimeConfig={enabled:boolean;ai_rerank_enabled:boolean;lead_capture_enabled:boolean;comparison_enabled:boolean;budget_planner_enabled:boolean;max_candidates:number;max_results:number;max_output_tokens:number;temperature:number;maintenance_message:string};
From 46ee0c2da796d28518bbaec54f9bfef7e9e83dca Mon Sep 17 00:00:00 2001
From: StudentHub
Date: Sun, 8 Nov 2026 21:44:27 +0530
Subject: [PATCH 21/95] Display verified GPS or approximate area match in
chatbot cards
---
widget.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/widget.js b/widget.js
index 050e20dad..ede0ac7b9 100644
--- a/widget.js
+++ b/widget.js
@@ -20,7 +20,7 @@ const SHH_WIDGET=(()=>{
const strip=t=>String(t??'').replace(/\s{2,}/g,' ').trim();function formatText(t){let x=esc(strip(t));x=x.replace(/\*\*(.+?)\*\*/g,'$1');return x.replace(/\n/g,' ')}
function speak(text){if(!speaking||!window.speechSynthesis)return;window.speechSynthesis.cancel();const u=new SpeechSynthesisUtterance(String(text).replace(/[*_#]/g,' ').replace(/\n+/g,'. '));u.lang='hi-IN';const vs=window.speechSynthesis.getVoices();const v=vs.find(x=>/^hi(-|_)/i.test(x.lang))||vs.find(x=>/^en(-|_)/i.test(x.lang));if(v)u.voice=v;window.speechSynthesis.speak(u)}
function addMessage(text,isBot,data){const wrap=document.createElement('div');wrap.style.cssText='align-self:'+(isBot?'stretch':'flex-end')+';max-width:'+(isBot?'100%':'88%')+';width:'+(isBot?'100%':'auto')+';';const bubble=document.createElement('div');bubble.style.cssText='background:'+(isBot?'#fff':'#071a33')+';color:'+(isBot?'#172033':'#fff')+';border:'+(isBot?'1px solid #e1e8f0':'0')+';border-radius:18px;padding:11px 13px;font-size:13px;line-height:1.58;box-shadow:0 3px 12px rgba(15,23,42,.06);word-break:break-word;';if(isBot){const meta=document.createElement('div');meta.style.cssText='display:flex;align-items:center;gap:6px;font-size:9px;font-weight:800;color:#64748b;margin-bottom:5px';meta.innerHTML='● StudentHubHelp AI';bubble.appendChild(meta)}const body=document.createElement('div');body.innerHTML=formatText(text);bubble.appendChild(body);wrap.appendChild(bubble);if(isBot&&data)renderData(wrap,data);messages.appendChild(wrap);messages.scrollTop=messages.scrollHeight;if(isBot)speak(strip(text))}
- function renderPropertyCard(p){const card=document.createElement('div');card.className='shh-card';const loc=[p.area,p.city].filter(Boolean).join(', '),phone=p.phone||p.whatsapp,maps=safeUrl(p.mapsUrl),details=safeUrl(p.detailsUrl);const top=document.createElement('div');top.style.cssText='display:flex;justify-content:space-between;gap:8px;align-items:flex-start';top.innerHTML='