/* AgroSense SPA — talks to the FastAPI backend (same origin). */ const $ = (id) => document.getElementById(id); const el = (html) => { const d = document.createElement('div'); d.innerHTML = html.trim(); return d.firstChild; }; const esc = (s) => (s ?? '').toString().replace(/[&<>]/g, c => ({ '&': '&', '<': '<', '>': '>' }[c])); const md = (s) => esc(s).replace(/\*\*(.+?)\*\*/g, '$1').replace(/\n/g, '
'); const num = (v, suf = '') => (v === null || v === undefined ? '—' : v + suf); async function getJSON(path) { const r = await fetch(path); if (!r.ok) throw new Error(r.status); return r.json(); } async function postForm(path, obj, headers = {}) { const fd = new FormData(); for (const [k, v] of Object.entries(obj)) if (v !== null && v !== undefined && v !== '') fd.append(k, v); const r = await fetch(path, { method: 'POST', body: fd, headers }); const data = await r.json().catch(() => ({})); if (!r.ok) throw new Error(data.detail || r.status); return data; } async function postJSON(path, obj) { const r = await fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(obj) }); if (!r.ok) throw new Error(r.status); return r.json(); } const state = () => ({ location: $('loc').value.trim(), language: $('lang').value, crop: $('crop').value.trim(), stage: $('stage').value, }); const spin = (target) => { target.innerHTML = '
loading…'; }; const errBox = (e) => `
Request failed (${esc(e.message)}). Is the API reachable?
`; const qs = (o) => Object.entries(o).filter(([, v]) => v !== '' && v != null) .map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&'); /* ---------- header: datetime, languages, news, commodities ---------- */ async function loadDateTime() { try { const d = await getJSON('/datetime'); $('dt-greg').textContent = '📅 ' + d.gregorian; const pg = d.panchang; let saka = '🪔 ' + d.indian_national + (d.lunar_day ? ' · 🌙 ' + d.lunar_day : ''); if (pg && pg.nakshatra) saka += ' · ⭐ ' + pg.nakshatra; const sakaEl = $('dt-saka'); sakaEl.textContent = saka; if (pg) sakaEl.title = `Panchang — Vaara: ${pg.vaara} | Tithi: ${pg.tithi} | ` + `Nakshatra: ${pg.nakshatra} | Yoga: ${pg.yoga} | Karana: ${pg.karana}`; $('dt-ist').textContent = '🕐 ' + d.ist_time + ' IST'; } catch (e) { /* ignore */ } } async function loadLanguages() { try { const d = await getJSON('/languages'); $('lang').innerHTML = Object.entries(d.languages).map(([c, n]) => ``).join(''); } catch (e) { $('lang').innerHTML = ''; } } async function loadNews() { const region = $('news-region').value || 'IN'; const topic = $('news-topic').value || 'Top stories'; try { const d = await getJSON('/news?' + qs({ region, topic, limit: 18 })); if (!$('news-region').dataset.init) { $('news-region').innerHTML = Object.entries(d.available_regions || { IN: 'India' }) .map(([c, n]) => ``).join(''); $('news-topic').innerHTML = (d.available_topics || ['Top stories']) .map(t => ``).join(''); $('news-region').dataset.init = '1'; } const items = d.items || []; $('news-ticker').innerHTML = '📰 ' + (items.length ? items.map(i => `${esc(i.title)}`).join(' • ') : 'No headlines.'); } catch (e) { $('news-ticker').textContent = '📰 News unavailable.'; } } async function loadCommodities() { try { const d = await getJSON('/commodities'); const chips = (d.items || []).map(c => { if (c.price === null) return `${esc(c.name)}: n/a`; let delta = ''; if (c.change_pct != null) { const cls = c.change_pct >= 0 ? 'up' : 'down'; delta = ` ${c.change_pct >= 0 ? '▲' : '▼'}${Math.abs(c.change_pct)}%`; } return `${esc(c.name)}: ${esc(c.currency)}${c.price}/${esc(c.unit)}${delta}`; }); $('cmd-ticker').innerHTML = '💹 ' + chips.join('  '); } catch (e) { $('cmd-ticker').textContent = '💹 Commodities unavailable.'; } } /* ---------- tabs ---------- */ function setupTabs() { const panels = [...document.querySelectorAll('.panel')]; const names = panels.map(p => p.dataset.tab); $('tabs').innerHTML = names.map((n, i) => ``).join(''); $('tabs').addEventListener('click', (e) => { const b = e.target.closest('.tab'); if (!b) return; document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t === b)); panels.forEach(p => p.classList.toggle('active', p.dataset.tab === b.dataset.t)); if (b.dataset.t === 'Location intel') maybeAutoIntel(); // auto-load on open }); } /* ---------- speaking avatar (Web Speech API) ---------- */ let LAST_ANSWER_TEXT = ''; const LANG_BCP = { en: 'en-IN', hi: 'hi-IN', kn: 'kn-IN', te: 'te-IN', ta: 'ta-IN', mr: 'mr-IN', bn: 'bn-IN', ml: 'ml-IN', gu: 'gu-IN', pa: 'pa-IN' }; const ttsOK = () => typeof window !== 'undefined' && 'speechSynthesis' in window; function setTts(s) { const e = $('tts-status'); if (e) e.textContent = s; } function avatarTalk(on) { const a = $('avatar'); if (a) a.classList.toggle('talking', on); } function speakableText(answer) { // Speak the recommendation prose only: drop the Sources/URLs and markdown. let t = (answer || '').split(/\*\*Sources\*\*/)[0].split('**Local weather')[0]; t = t.replace(/https?:\S+/g, '').replace(/\*\*/g, '').replace(/\[\d+\]/g, '') .replace(/[#`>_*]/g, '').replace(/\s+/g, ' ').trim(); return t.slice(0, 650); } function pickVoice(langCode) { const voices = window.speechSynthesis.getVoices() || []; const bcp = LANG_BCP[langCode] || 'en-IN'; const base = bcp.split('-')[0]; return voices.find(v => v.lang === bcp) || voices.find(v => v.lang && v.lang.toLowerCase().startsWith(base)) || voices.find(v => v.lang && v.lang.startsWith('en')) || null; } function speak(text, langCode) { if (!ttsOK()) { setTts('voice not supported on this browser'); return; } if (!text) { setTts('ask something first'); return; } window.speechSynthesis.cancel(); const u = new SpeechSynthesisUtterance(text); u.lang = LANG_BCP[langCode] || 'en-IN'; const v = pickVoice(langCode); if (v) u.voice = v; u.rate = 1; u.pitch = 1; u.onstart = () => { avatarTalk(true); setTts('speaking…'); }; u.onend = () => { avatarTalk(false); setTts('ready'); }; u.onerror = () => { avatarTalk(false); setTts('voice error'); }; window.speechSynthesis.speak(u); } function stopSpeak() { if (ttsOK()) window.speechSynthesis.cancel(); avatarTalk(false); setTts('stopped'); } /* ---------- advisor ---------- */ async function ask() { const s = state(); const out = $('answer'); if (!$('q').value.trim()) { out.innerHTML = '
Type a question first.
'; return; } spin(out); try { const r = await postJSON('/query', { query: $('q').value.trim(), language: s.language, location: s.location || null, include_satellite: $('t-sat').checked, include_advisories: $('t-adv').checked, include_prices: $('t-prices').checked, commodity: s.crop || null, stage: s.stage || null, }); let html = `
${md(r.answer)}
`; if (r.citations && r.citations.length) html += `
Citations: ${r.citations.map(c => `[${c.n}] ${esc(c.source)} (${esc(c.crop)})`).join(' · ')}
`; html += `
lang ${esc(r.language)} · ${r.latency_ms} ms · ${esc(r.backends?.embedding || '')}
`; if (r.prices) html += priceCard(r.prices); out.innerHTML = html; LAST_ANSWER_TEXT = speakableText(r.answer); if ($('autospeak') && $('autospeak').checked) speak(LAST_ANSWER_TEXT, s.language); } catch (e) { out.innerHTML = errBox(e); } } /* ---------- location intel ---------- */ function card(title, body) { return `

${title}

${body}
`; } function kv(k, v) { return `
${esc(k)}${v}
`; } let intelLoadedFor = null; function maybeAutoIntel() { const s = state(); if (s.location && s.location !== intelLoadedFor) loadIntel(); } function onLocationChange() { intelLoadedFor = null; // force a refresh for the new location const p = document.querySelector('.panel[data-tab="Location intel"]'); if (p && p.classList.contains('active')) maybeAutoIntel(); } async function loadIntel() { const s = state(); const root = $('intel'); if (!s.location) { root.innerHTML = '
Enter a location above.
'; return; } intelLoadedFor = s.location; root.innerHTML = '
loading the dashboard…'; const loc = encodeURIComponent(s.location); const cards = {}; const set = () => root.innerHTML = Object.values(cards).join(''); const add = (key, html) => { cards[key] = html; set(); }; // fire all in parallel; render as each resolves getJSON('/weather?location=' + loc).then(w => add('w', weatherCard(w))).catch(() => {}); getJSON('/environment?location=' + loc).then(e => add('e', e.available ? envCard(e) : '')).catch(() => {}); getJSON('/satellite?location=' + loc).then(x => add('s', x.available ? satCard(x) : '')).catch(() => {}); getJSON('/planetary?location=' + loc).then(p => add('p', p.available ? planetCard(p) : '')).catch(() => {}); getJSON('/hazards?location=' + loc).then(h => add('h', h.available ? hazardCard(h) : '')).catch(() => {}); getJSON('/advisories?' + qs({ location: s.location, crop: s.crop, stage: s.stage })) .then(a => add('a', a.available ? advisoryCard(a) : '')).catch(() => {}); } function weatherCard(w) { if (!w.available) return ''; let b = kv('Now', num(w.current_temp_c, '°C')) + kv('Humidity', num(w.current_humidity, '%')); if (w.current_precip_mm != null) b += kv('Precipitation now', num(w.current_precip_mm, ' mm')); // Full daily forecast list. b += '
Forecast
'; (w.daily || []).forEach(d => { b += `
${esc(d.date)} ${num(d.tmin_c)}–${num(d.tmax_c)}°C · rain ${num(d.precip_mm, ' mm')}` + (d.precip_prob != null ? ` (${d.precip_prob}%)` : '') + `
`; }); (w.advisories || []).forEach(a => b += `
${esc(a)}
`); b += `
Source: ${esc(w.source || 'Open-Meteo')}
`; return card('🌤️ Weather — ' + esc(w.location_name), b); } function envCard(e) { const s = e.sunlight || {}, wd = e.wind || {}, aq = e.air_quality || {}, gw = e.groundwater || {}; const b = `
${num(e.elevation_m, 'm')}
Altitude
${num(e.population)}
Population
${num(e.humidity_pct, '%')}
Humidity
${num(wd.speed_kmh)} ${esc(wd.direction_compass || '')}
Wind km/h
${num(s.sunshine_hours, 'h')}
Sunshine
${num(aq.us_aqi)}
US AQI ${esc(aq.category || '')}
${num(aq.pm2_5)}
PM2.5
${gw.level_m != null ? gw.level_m + 'm' : num(gw.soil_moisture_m3m3)}
Groundwater
${esc(gw.note || '')}
`; return card('🌍 Environment — ' + esc(e.location_name), b); } function satCard(x) { const ac = x.agroclimate || {}, nd = x.numeric_ndvi; let b = `NDVI`; b += kv('Agroclimate solar', num(ac.avg_solar_mj, ' MJ/m²/d')); b += kv('Recent rain', num(ac.total_precip_mm, ' mm')); (ac.notes || []).forEach(n => b += `
${esc(n)}
`); b += nd && nd.latest != null ? kv('Field NDVI', `${nd.latest} (${esc(nd.status)})`) : `
Field NDVI: configure Earth Engine to enable.
`; b += `
Open in NASA Worldview
`; return card('🛰️ Satellite — ' + esc(x.location_name), b); } function planetCard(p) { const mp = p.moon_phase || {}; let b = `
🌙 ${esc(mp.name)} — ${Math.round((mp.illumination || 0) * 100)}% illuminated
`; b += '' + (p.bodies || []).map(x => ``).join('') + '
BodyAltAz
${esc(x.name)}${x.altitude_deg}° ${x.azimuth_deg}° ${esc(x.azimuth_compass)}${x.above_horizon ? '✅' : '—'}
'; return card('🪐 Sky — ' + esc(p.location_name), b); } function hazardCard(h) { let b = ''; const ev = h.events || []; b += ev.length ? ev.slice(0, 6).map(e => `
${esc(e.category)} — ${esc(e.title)}${e.distance_km != null ? ' · ' + e.distance_km + ' km' : ''}
`).join('') : '
No hazard events within range.
'; if (h.fires === null) b += '
🔥 Active fires: set a NASA FIRMS key to enable.
'; else if (h.fires.length) b += `
🔥 ${h.fires.length} active fire(s) nearby; nearest ${h.fires[0].distance_km} km.
`; else b += '
🔥 No active fires nearby.
'; return card('⚠️ Hazards — ' + esc(h.location_name), b); } function advisoryCard(a) { const items = a.advisories || []; const b = (items.length ? items : []).map(x => `
${esc(x.urgency)} ${esc(x.title)}: ${esc(x.action)}
${esc(x.rationale)}
`).join('') || '
No urgent advisories.
'; return card('🧭 Advisories' + (a.crop ? ' — ' + esc(a.crop) + (a.stage ? ' / ' + esc(a.stage) : '') : ''), b); } /* ---------- market ---------- */ function priceCard(p) { if (!p) return ''; const s = p.summary || {}; let b = ''; if (s.modal_min != null) b += `
₹${s.modal_min}
min
₹${s.modal_avg}
avg
₹${s.modal_max}
max
`; (p.records || []).slice(0, 6).forEach(r => b += kv(`${esc(r.market)} (${esc(r.state)})`, '₹' + num(r.modal_price))); (p.notes || []).forEach(n => b += `
${esc(n)}
`); return `
${b || 'No price records.'}
`; } async function loadPrices() { const out = $('prices'); spin(out); try { const d = await getJSON('/prices?' + qs({ commodity: $('price-commodity').value.trim(), state: $('price-state').value.trim() })); out.innerHTML = d.available ? priceCard(d) : '
Prices unavailable — set a data.gov.in key (AGROSENSE_DATAGOV_API_KEY).
'; } catch (e) { out.innerHTML = errBox(e); } } async function fillCommoditiesTable() { try { const d = await getJSON('/commodities'); $('commodities').innerHTML = '' + d.items.map(c => ``).join('') + '
CommodityPriceΔ
${esc(c.name)}${c.price == null ? 'n/a' : esc(c.currency) + c.price + '/' + esc(c.unit)} ${c.change_pct == null ? '' : (c.change_pct >= 0 ? '▲' : '▼') + Math.abs(c.change_pct) + '%'}
'; } catch (e) { $('commodities').innerHTML = errBox(e); } } /* ---------- farmer trading platform ---------- */ let TRADE_USER = ''; function trBadge(l) { return l.type === 'sell' ? 'Selling' : 'Buying'; } function trPrice(l) { const cur = l.currency === 'INR' ? '₹' : esc(l.currency) + ' '; return `${cur}${l.price}/${esc(l.unit)} · ${l.quantity} ${esc(l.unit)}`; } async function loadListings() { const out = $('tr-list'); spin(out); try { const d = await getJSON('/market/listings?' + qs({ type: $('tr-type').value, commodity: $('tr-commodity').value.trim(), state: $('tr-state').value.trim() })); out.innerHTML = (d.listings || []).map(l => `
${trBadge(l)} ${esc(l.commodity)} ${trPrice(l)} ${l.grade ? `· ${esc(l.grade)}` : ''} ${l.location || l.state ? `· 📍 ${esc(l.location || l.state)}` : ''} ${l.inquiry_count ? `· 💬 ${l.inquiry_count}` : ''}
${esc(l.description || '')}
`).join('') || 'No matching listings. Post one below!'; out.innerHTML += `
🤝 ${esc(d.disclaimer || '')}
`; out.querySelectorAll('.tr-open').forEach(b => b.onclick = () => openListing(b.dataset.id)); } catch (e) { out.innerHTML = errBox(e); } } async function openListing(id) { const out = $('tr-detail'); spin(out); try { renderListing(await getJSON('/market/listings/' + encodeURIComponent(id))); } catch (e) { out.innerHTML = errBox(e); } } function renderListing(l) { const closed = l.status !== 'open'; const inqs = (l.inquiries || []).map(q => `
${esc(q.name)} ${q.contact ? `· ☎ ${esc(q.contact)}` : ''} ${q.quantity ? `· wants ${q.quantity}` : ''} ${q.at ? `· ${esc(q.at)}` : ''}
${esc(q.message || '')}
`).join(''); $('tr-detail').innerHTML = `

${trBadge(l)} ${esc(l.commodity)} ${closed ? 'Closed' : ''}

${trPrice(l)} ${l.grade ? '· ' + esc(l.grade) : ''} ${l.location || l.state ? '· 📍 ' + esc(l.location || '') + ' ' + esc(l.state || '') : ''}

${esc(l.description || '')}

Posted by ${esc(l.seller || 'Anonymous')}${l.harvest_date ? ' · ' + esc(l.harvest_date) : ''} ${l.contact ? ' · ' + esc(l.contact) : ''}
${closed ? '' : ``}

💬 Inquiries

${inqs || 'No inquiries yet.'}
${closed ? '
This listing is closed.
' : `
`}
`; const closeBtn = $('tr-close'); if (closeBtn) closeBtn.onclick = async () => renderListing( (await postForm(`/market/listings/${l.id}/close`, { status: 'closed' })).listing); const sendBtn = $('inq-send'); if (sendBtn) sendBtn.onclick = async () => { const msg = $('inq-msg').value.trim(), contact = $('inq-contact').value.trim(); if (!msg && !contact) { return; } TRADE_USER = $('inq-name').value.trim() || TRADE_USER; try { renderListing(await postForm(`/market/listings/${l.id}/inquire`, { name: TRADE_USER, contact, message: msg, quantity: $('inq-qty').value.trim() })); } catch (e) { $('tr-detail').insertAdjacentHTML('beforeend', errBox(e)); } // keep form handlers }; } async function createListing() { const msg = $('nt-msg'); const f = (id) => $(id).value.trim(); try { const d = await postForm('/market/listings', { type: $('nt-type').value, commodity: f('nt-commodity'), quantity: f('nt-quantity'), unit: $('nt-unit').value, price: f('nt-price'), grade: f('nt-grade'), location: f('nt-location'), state: f('nt-state'), seller: f('nt-seller'), contact: f('nt-contact'), description: f('nt-description') }); msg.innerHTML = `Posted your ${esc(d.listing.type)} listing for ${esc(d.listing.commodity)}.`; ['nt-commodity', 'nt-quantity', 'nt-price', 'nt-grade', 'nt-location', 'nt-state', 'nt-seller', 'nt-contact', 'nt-description'].forEach(i => $(i).value = ''); await loadListings(); openListing(d.listing.id); } catch (e) { msg.innerHTML = `${esc(e.message)}`; } } /* ---------- plant clinic ---------- */ async function classifyImage() { const out = $('vision'); const f = $('img').files[0]; if (!f) { out.innerHTML = '
Choose an image first.
'; return; } spin(out); try { const r = await postForm('/vision/classify?task=all', { file: f }); const line = (t, o) => `
${t}${esc(o.label)} (${Math.round((o.confidence || 0) * 100)}%)
${esc(o.note || '')}
`; out.innerHTML = line('Disease/health', r.disease || {}) + line('Plant ID', r.plant || {}) + line('Pest ID', r.pest || {}); } catch (e) { out.innerHTML = errBox(e); } } async function telemedicine() { const s = state(); const out = $('tele'); spin(out); try { const r = await postForm('/telemedicine', { crop: s.crop, symptoms: $('symptoms').value.trim(), location: s.location, file: $('img').files[0] || null, }); let b = `
${esc(r.diagnosis)} · ${esc(r.health_status)} · severity ${esc(r.severity)} · ${Math.round((r.confidence || 0) * 100)}%
`; if (r.weather_note) b += `
🌦️ ${esc(r.weather_note)}
`; b += '📋 Prescription'; (r.prescription || []).forEach(p => b += `
${esc(p.category)}: ${esc(p.instruction)}
`); if (r.citations?.length) b += `
Sources: ${esc(r.citations.join(', '))}
`; b += `
⚕️ ${esc(r.disclaimer)}
`; out.innerHTML = b; } catch (e) { out.innerHTML = errBox(e); } } /* ---------- live doctor ---------- */ const stars = (avg, count) => avg == null ? 'no ratings yet' : `${'★'.repeat(Math.round(avg))}${'☆'.repeat(5 - Math.round(avg))} ${avg} (${count})`; async function loadExperts() { try { const d = await getJSON('/experts'); $('experts').innerHTML = d.experts.map(e => `
${esc(e.name)} — ${esc(e.specialization)}   ${stars(e.rating_avg, e.rating_count)}
${esc(e.region)} · ${esc(e.languages.join(', '))} ${e.registration_no ? '· Reg ' + esc(e.registration_no) : ''}
`).join('') || 'No verified doctors yet.'; $('experts').querySelectorAll('.exp-profile').forEach(b => b.onclick = () => showProfile(b.dataset.id)); } catch (e) { $('experts').innerHTML = errBox(e); } } async function showProfile(id) { const out = $('doctor-profile'); spin(out); try { const p = await getJSON('/doctors/' + encodeURIComponent(id)); let b = `

👨‍⚕️ ${esc(p.name)}

${esc(p.specialization)}   ${stars(p.rating_avg, p.rating_count)}
Region${esc(p.region)}
Languages${esc(p.languages.join(', '))}
ICAR / Reg. no.${esc(p.registration_no || '—')}
Credentials${esc(p.credentials || '—')}
Contact${esc(p.contact || '—')}
`; if (p.ratings.length) b += '

Recent reviews

' + p.ratings.slice().reverse().map(r => `
${'★'.repeat(r.stars)} ${esc(r.comment || '')} ${esc(r.at || '')}
`).join(''); b += `
`; out.innerHTML = b; $('rate-go').onclick = () => rateDoctor(p.id); } catch (e) { out.innerHTML = errBox(e); } } async function rateDoctor(id) { try { await postForm('/doctors/' + encodeURIComponent(id) + '/rate', { stars: $('rate-stars').value, comment: $('rate-comment').value.trim() }); await showProfile(id); await loadExperts(); } catch (e) { $('rate-msg').innerHTML = `${esc(e.message)}`; } } function renderConsult(c) { const out = $('consult-session'); let b = `
Consultation ${esc(c.id)}${esc(c.status)}
`; if (c.expert) b += `
👨‍⚕️ ${esc(c.expert.name)} — ${esc(c.expert.specialization)}
${esc(c.expert.region)} · ${esc(c.expert.languages.join(', '))}
`; if (c.summary) b += `
Shared: ${esc(c.summary)}
`; const okch = (c.notifications || []).filter(n => n.ok).map(n => n.channel).join(', '); if (okch) b += `
🔔 Notified via: ${esc(okch)}
`; if (c.room_url) b += ``; b += '
' + (c.messages || []).map(m => { const who = { farmer: '🧑‍🌾 You', expert: '👨‍⚕️ Expert', system: 'ℹ️ System' }[m.sender] || m.sender; return `
${who}: ${esc(m.text)} ${m.at ? `${esc(m.at)}` : ''}
`; }).join('') + '
'; b += `
`; out.innerHTML = b; $('csend').onclick = async () => { const txt = $('cmsg').value.trim(); if (!txt) return; const upd = await postForm(`/consult/${c.id}/message`, { sender: 'farmer', text: txt }); renderConsult(upd); }; } async function applyDoctor() { const msg = $('doc-apply-msg'); try { const d = await postForm('/doctors/apply', { name: $('doc-name').value.trim(), specialization: $('doc-spec').value.trim(), region: $('doc-region').value.trim(), languages: $('doc-langs').value.trim(), contact: $('doc-contact').value.trim(), credentials: $('doc-cred').value.trim(), registration_no: $('doc-reg').value.trim(), }); msg.innerHTML = `Submitted as ${esc(d.doctor.id)} — ${esc(d.message)}`; ['doc-name', 'doc-spec', 'doc-region', 'doc-langs', 'doc-contact', 'doc-cred', 'doc-reg'].forEach(i => $(i).value = ''); } catch (e) { msg.innerHTML = `${esc(e.message)}`; } } async function requestConsult() { const s = state(); const out = $('consult-session'); spin(out); try { const c = await postForm('/consult/request', { farmer_name: $('farmer').value.trim() || 'Farmer', crop: s.crop, symptoms: $('symptoms').value.trim(), channel: $('channel').value, language: $('consult-lang').value, }); renderConsult(c); } catch (e) { out.innerHTML = errBox(e); } } /* ---------- farmers' digital clubs ---------- */ let CLUB_USER = ''; async function loadClubs() { const out = $('clubs-list'); spin(out); try { const d = await getJSON('/clubs?' + qs({ type: $('club-type').value, key: $('club-key').value.trim() })); out.innerHTML = (d.clubs || []).map(c => `
${esc(c.name)} ${esc(c.type)} ${esc(c.key)} · ${c.member_count} member(s)
`).join('') || 'No clubs found — create one!'; out.querySelectorAll('.club-open').forEach(b => b.onclick = () => openClub(b.dataset.id)); } catch (e) { out.innerHTML = errBox(e); } } async function createClub() { const msg = $('nc-msg'); try { const d = await postForm('/clubs', { name: $('nc-name').value.trim(), type: $('nc-type').value, key: $('nc-key').value.trim(), description: $('nc-desc').value.trim(), creator: $('nc-creator').value.trim() }); msg.innerHTML = `Created "${esc(d.club.name)}".`; ['nc-name', 'nc-key', 'nc-desc', 'nc-creator'].forEach(i => $(i).value = ''); await loadClubs(); openClub(d.club.id); } catch (e) { msg.innerHTML = `${esc(e.message)}`; } } async function openClub(id) { const out = $('club-detail'); spin(out); try { renderClub(await getJSON('/clubs/' + encodeURIComponent(id))); } catch (e) { out.innerHTML = errBox(e); } } function renderClub(c) { const posts = (c.posts || []).map(p => `
${esc(p.author)}: ${esc(p.text)} ${p.link ? `link` : ''} ${p.at ? `${esc(p.at)}` : ''}
`).join(''); $('club-detail').innerHTML = `

👥 ${esc(c.name)}

${esc(c.type)} · ${esc(c.key)} · ${c.member_count} member(s)

${esc(c.description || '')}

Members: ${esc((c.members || []).join(', ') || '—')}

💬 Discussion & sharing

${posts || 'No posts yet.'}
`; $('club-join').onclick = async () => { CLUB_USER = $('club-member').value.trim(); renderClub(await postForm(`/clubs/${c.id}/join`, { member: CLUB_USER })); }; $('club-send').onclick = async () => { const t = $('club-post-text').value.trim(); if (!t) return; CLUB_USER = $('club-post-author').value.trim() || CLUB_USER; renderClub(await postForm(`/clubs/${c.id}/post`, { author: CLUB_USER, text: t, link: $('club-post-link').value.trim() })); }; } /* ---------- traditional advisor ---------- */ async function loadTraditional() { const out = $('trad-out'); spin(out); const s = state(); try { const d = await getJSON('/traditional?' + qs({ activity: $('trad-activity').value, location: s.location })); const a = d.astrology || {}, p = d.panchang || {}; const v = (a.verdict || '').toLowerCase(); const cls = v.includes('favourable') ? 'low' : (v.includes('postpone') || v.includes('not ideal') || v.includes('caution')) ? 'high' : 'medium'; const box = cls === 'low' ? 'ok' : cls === 'high' ? 'danger' : 'warn'; let b = `
${esc(a.verdict)} for ${esc(d.activity)} ${d.location_name ? '· ' + esc(d.location_name) : ''}
`; b += `
🪔 Panchang — Vaara ${esc(p.vaara)} · ${esc(p.tithi)} · ` + `Nakshatra ${esc(p.nakshatra)} · Yoga ${esc(p.yoga)} · Karana ${esc(p.karana)}
`; b += '🔮 Astrological note'; (a.reasons || []).forEach(r => b += `
• ${esc(r)}
`); b += '🌾 Traditional practices'; (d.practices || []).forEach(pr => b += `
${esc(pr.title)} ${esc(pr.region)} · ${esc(pr.source)}
${esc(pr.practice)}
`); b += `
🪔 ${esc(d.disclaimer)}
`; out.innerHTML = b; } catch (e) { out.innerHTML = errBox(e); } } /* ---------- agricultural finance (bank assistance / loans) ---------- */ let FIN_CATS_LOADED = false; async function loadFinance() { const out = $('fins-list'); spin(out); try { const d = await getJSON('/finance?' + qs({ category: $('fin-category').value, search: $('fin-search').value.trim() })); if (!FIN_CATS_LOADED && (d.categories || []).length) { const cur = $('fin-category').value; $('fin-category').innerHTML = '' + d.categories.map(c => ``).join(''); $('fin-category').value = cur; FIN_CATS_LOADED = true; } const rows = (d.products || []).map(p => `
${esc(p.name)} ${esc(p.category)} · ${esc(p.provider)}
${esc(p.summary)}
💸 ${esc(p.interest || '')} ${p.loan_amount ? '· ' + esc(p.loan_amount) : ''}
`).join('') || 'No matching products.'; out.innerHTML = rows + (d.disclaimer ? `
🏦 ${esc(d.disclaimer)}
` : ''); out.querySelectorAll('.fin-open').forEach(b => b.onclick = () => openFinance(b.dataset.id)); } catch (e) { out.innerHTML = errBox(e); } } async function openFinance(id) { const out = $('fin-detail'); spin(out); try { renderFinance(await getJSON('/finance/' + encodeURIComponent(id))); } catch (e) { out.innerHTML = errBox(e); } } function renderFinance(p) { const li = (arr) => (arr || []).map(i => `
  • ${esc(i)}
  • `).join(''); const steps = (p.application_process || []).map(s => `
  • ${esc(s)}
  • `).join(''); const s = state(); $('fin-detail').innerHTML = `

    🏦 ${esc(p.name)}

    ${esc(p.category)} ${esc(p.provider)}

    ${esc(p.summary)}

    ${p.interest ? `
    Interest
    ${esc(p.interest)}
    ` : ''} ${p.loan_amount ? `
    Loan amount
    ${esc(p.loan_amount)}
    ` : ''} ${p.tenure ? `
    Tenure
    ${esc(p.tenure)}
    ` : ''}
    ${p.benefits ? `

    💰 Benefits

    ${esc(p.benefits)}

    ` : ''} ${p.eligibility ? `

    ✅ Eligibility

    ${esc(p.eligibility)}

    ` : ''} ${steps ? `

    📝 How to apply

      ${steps}
    ` : ''} ${(p.documents || []).length ? `

    📄 Documents needed

    ` : ''}
    ${p.portal ? `` : ''} ${p.helpline ? `☎ ${esc(p.helpline)}` : ''}

    🧾 Lodge a loan enquiry

    ${p.source ? `
    Source: ${esc(p.source)}
    ` : ''} ${p.disclaimer ? `
    🏦 ${esc(p.disclaimer)}
    ` : ''}
    `; $('fa-send').onclick = applyFinance; try { $('fin-detail').scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } catch (e) { /* jsdom */ } } async function applyFinance() { const id = $('fa-send').dataset.id; const msg = $('fa-msg'); try { const d = await postForm(`/finance/${encodeURIComponent(id)}/apply`, { name: $('fa-name').value.trim(), contact: $('fa-contact').value.trim(), amount: $('fa-amount').value.trim(), location: $('fa-location').value.trim(), message: $('fa-message').value.trim() }); msg.innerHTML = `Enquiry ${esc(d.application.id)} lodged. ${esc(d.message)}`; ['fa-name', 'fa-contact', 'fa-amount', 'fa-message'].forEach(i => $(i).value = ''); } catch (e) { msg.innerHTML = `${esc(e.message)}`; } } /* ---------- government subsidies & schemes ---------- */ async function loadSubsidies() { const out = $('subs-list'); spin(out); const s = state(); try { const d = await getJSON('/subsidies?' + qs({ level: $('sub-level').value, state: s.location, search: $('sub-search').value.trim() })); const rows = (d.schemes || []).map(x => `
    ${esc(x.name)} ${esc(x.level === 'central' ? 'Central' : 'State')} ${x.state ? `${esc(x.state)}` : ''} · ${esc(x.category)}${x.update_count ? ' · 📢 ' + x.update_count : ''}
    ${esc(x.summary)}
    `).join('') || 'No schemes match. Try clearing filters.'; out.innerHTML = rows + (d.disclaimer ? `
    🏛️ ${esc(d.disclaimer)}
    ` : ''); out.querySelectorAll('.sub-open').forEach(b => b.onclick = () => openSubsidy(b.dataset.id)); } catch (e) { out.innerHTML = errBox(e); } } async function openSubsidy(id) { const out = $('sub-detail'); spin(out); try { renderSubsidy(await getJSON('/subsidies/' + encodeURIComponent(id))); } catch (e) { out.innerHTML = errBox(e); } } function renderSubsidy(x) { const li = (arr) => (arr || []).map(i => `
  • ${esc(i)}
  • `).join(''); const steps = (x.application_process || []).map(s => `
  • ${esc(s)}
  • `).join(''); const ups = (x.updates || []).slice().reverse().map(u => `
    ${esc(u.date)}
    ${esc(u.text)}
    `).join(''); $('sub-detail').innerHTML = `

    🏛️ ${esc(x.name)}

    ${esc(x.level === 'central' ? 'Central' : 'State')} ${x.state ? esc(x.state) + ' · ' : ''}${esc(x.category)}

    ${esc(x.summary)}

    ${x.benefits ? `

    💰 Benefits

    ${esc(x.benefits)}

    ` : ''} ${x.eligibility ? `

    ✅ Eligibility

    ${esc(x.eligibility)}

    ` : ''} ${steps ? `

    📝 How to apply

      ${steps}
    ` : ''} ${(x.documents || []).length ? `

    📄 Documents needed

    ` : ''}
    ${x.portal ? `` : ''} ${x.helpline ? `☎ ${esc(x.helpline)}` : ''}
    ${ups ? `

    📢 Announcements & updates

    ${ups}` : ''} ${x.source ? `
    Source: ${esc(x.source)}
    ` : ''} ${x.disclaimer ? `
    🏛️ ${esc(x.disclaimer)}
    ` : ''}
    `; try { $('sub-detail').scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } catch (e) { /* jsdom */ } } async function loadSubsidyUpdates() { const out = $('sub-updates'); if (!out) return; try { const d = await getJSON('/subsidies/updates?limit=10'); out.innerHTML = (d.updates || []).map(u => `
    ${esc(u.date)} · ${esc(u.scheme)}
    ${esc(u.text)}
    `).join('') || 'No announcements.'; } catch (e) { out.innerHTML = errBox(e); } } /* ---------- land records ---------- */ let LR_STATES_LOADED = false; async function loadLandRecords() { const out = $('lr-out'); spin(out); const s = state(); const chosen = $('lr-state').value; try { const d = await getJSON('/land-records?' + qs(chosen ? { state: chosen } : { location: s.location })); if (!LR_STATES_LOADED && (d.covered_states || []).length) { $('lr-state').innerHTML = '' + d.covered_states.map(x => ``).join(''); $('lr-state').value = chosen; LR_STATES_LOADED = true; } const li = (arr) => (arr || []).map(i => `
  • ${esc(i)}
  • `).join(''); const ol = (arr) => (arr || []).map(i => `
  • ${esc(i)}
  • `).join(''); const stateName = d.state || 'your State'; const dlq = qs(chosen ? { state: chosen } : { location: s.location }); const uncovered = d.covered === false; out.innerHTML = `

    🗺️ ${esc(stateName)} — ${esc(d.system || 'land records')}

    ${uncovered ? `
    No state-specific guide for "${esc(stateName)}" yet — showing the general all-India steps. Use the official portal below.
    ` : ''}
    Record: ${esc(d.record_name || '')}

    ${esc(d.summary || '')}

    ${d.portal ? `` : ''} ${d.map_portal ? `` : ''} ${d.helpline ? `☎ ${esc(d.helpline)}` : ''}

    🔎 Search by

    📝 How to search & download

      ${ol(d.steps)}

    📄 The record contains

    ${d.notes ? `
    ${esc(d.notes)}
    ` : ''}

    ⬇️ Download a printable guide

    🗺️ ${esc(d.disclaimer || '')}
    `; } catch (e) { out.innerHTML = errBox(e); } } /* ---------- internet radio ---------- */ let RADIO_STATIONS = []; async function loadRadio() { const out = $('radio-list'); spin(out); try { const d = await getJSON('/radio?' + qs({ country: 'IN', search: $('radio-search').value.trim(), limit: 80 })); RADIO_STATIONS = d.stations || []; out.innerHTML = RADIO_STATIONS.map((s, i) => `
    ${esc(s.name)} ${esc(s.state || '')} ${s.codec ? '· ' + esc(s.codec) : ''} ${s.bitrate ? s.bitrate + 'kbps' : ''}
    `).join('') || 'No stations found. Try a different filter.'; out.querySelectorAll('.radio-play').forEach(b => b.onclick = () => playStation(+b.dataset.i)); } catch (e) { out.innerHTML = errBox(e); } } function playStation(i) { const s = RADIO_STATIONS[i]; if (!s) return; const a = $('radio-audio'); a.src = s.url; $('radio-now').innerHTML = `▶ Now playing: ${esc(s.name)}` + (s.homepage ? ` — station site` : ''); try { const p = a.play(); if (p && p.catch) p.catch(() => { $('radio-now').innerHTML += ' (stream unavailable - try another)'; }); } catch (e) { /* jsdom / autoplay */ } } /* ---------- admin: configure knowledge base ---------- */ let ADMIN_TOKEN = ''; const ADM_FIELDS = ['crop', 'category', 'soil_type', 'rainfall_mm', 'region', 'season', 'source', 'recommended_fertilizer', 'disease_prevention', 'pest_management']; async function adminReq(method, path, body) { const opts = { method, headers: { 'X-Admin-Token': ADMIN_TOKEN } }; if (body) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body); } const r = await fetch(path, opts); const data = await r.json().catch(() => ({})); if (!r.ok) throw new Error(data.detail || r.status); return data; } async function adminUnlock() { ADMIN_TOKEN = $('admin-token').value; const st = $('admin-status'); try { await adminLoad(); $('admin-area').style.display = ''; st.innerHTML = 'Unlocked. Changes rebuild the live index.'; } catch (e) { $('admin-area').style.display = 'none'; st.innerHTML = `${esc(e.message)}`; } } async function adminLoadDoctors() { const d = await adminReq('GET', '/admin/doctors'); const badge = (s) => `${esc(s)}`; $('admin-doctors').innerHTML = (d.doctors || []).map(x => `
    ${esc(x.name)} ${badge(x.status)}
    ${esc(x.specialization)} · ${esc(x.region)} · ${esc((x.languages || []).join(', '))} · ${esc(x.contact || '')} · ${esc(x.credentials || '')}
    ${x.status !== 'verified' ? `` : ''} ${x.status !== 'rejected' ? `` : ''}
    `).join('') || 'No doctors.'; $('admin-doctors').querySelectorAll('.doc-ok').forEach(b => b.onclick = () => adminVerify(b.dataset.id, true)); $('admin-doctors').querySelectorAll('.doc-no').forEach(b => b.onclick = () => adminVerify(b.dataset.id, false)); } async function adminVerify(id, approve) { try { await postForm(`/admin/doctors/${encodeURIComponent(id)}/verify`, { approve }, { 'X-Admin-Token': ADMIN_TOKEN }); await adminLoadDoctors(); await loadExperts(); } catch (e) { $('admin-doctors').innerHTML = errBox(e); } } async function adminLoadSubsidies() { const sel = $('asub-id'); if (!sel) return; const d = await getJSON('/subsidies'); // public list (summaries) sel.innerHTML = (d.schemes || []).map(s => ``).join(''); } async function adminPostSubsidyUpdate() { const msg = $('asub-msg'); const text = $('asub-text').value.trim(); if (!text) { msg.innerHTML = 'Announcement text is required.'; return; } try { const d = await postForm(`/admin/subsidies/${encodeURIComponent($('asub-id').value)}/update`, { text, date: $('asub-date').value.trim() }, { 'X-Admin-Token': ADMIN_TOKEN }); msg.innerHTML = `Posted to "${esc(d.scheme.name)}".`; $('asub-text').value = ''; $('asub-date').value = ''; loadSubsidyUpdates(); // refresh the public feed } catch (e) { msg.innerHTML = `${esc(e.message)}`; } } async function adminLoadFinance() { const out = $('admin-finance'); if (!out) return; const d = await adminReq('GET', '/admin/finance/applications'); $('admin-fin-count').textContent = d.count; out.innerHTML = (d.applications || []).map(a => `
    ${esc(a.name)} · ${esc(a.product)} ${a.amount ? `· ₹${a.amount}` : ''}
    ☎ ${esc(a.contact)} ${a.location ? '· 📍 ' + esc(a.location) : ''} ${a.at ? '· ' + esc(a.at) : ''}
    ${a.message ? `
    ${esc(a.message)}
    ` : ''}
    `).join('') || 'No loan enquiries yet.'; } async function adminLoad() { await adminLoadDoctors(); await adminLoadSubsidies(); await adminLoadFinance(); const d = await adminReq('GET', '/admin/kb'); $('admin-count').textContent = d.count; $('admin-list').innerHTML = (d.entries || []).map(e => `
    ${esc(e.crop)} ${esc(e.id)} · ${esc(e.soil_type || '')} · ${esc(e.source || '')}
    `).join('') || 'No entries.'; $('admin-list').querySelectorAll('.adm-edit').forEach(b => b.onclick = () => adminEdit(d.entries.find(x => x.id === b.dataset.id))); $('admin-list').querySelectorAll('.adm-del').forEach(b => b.onclick = () => adminDelete(b.dataset.id)); } function adminCollect() { const e = {}; ADM_FIELDS.forEach(f => { const v = $('adm-' + f).value.trim(); if (v) e[f] = v; }); const id = $('adm-id').value; if (id) e.id = id; return e; } function adminEdit(entry) { $('adm-id').value = entry.id || ''; ADM_FIELDS.forEach(f => $('adm-' + f).value = entry[f] ?? ''); $('admin-form-title').textContent = '✏️ Edit ' + entry.id; } function adminClear() { $('adm-id').value = ''; ADM_FIELDS.forEach(f => $('adm-' + f).value = ''); $('admin-form-title').textContent = '➕ Add entry'; $('admin-form-msg').textContent = ''; } async function adminSave() { const entry = adminCollect(); const msg = $('admin-form-msg'); try { const id = $('adm-id').value; const d = id ? await adminReq('PUT', '/admin/kb/' + encodeURIComponent(id), entry) : await adminReq('POST', '/admin/kb', entry); msg.innerHTML = `Saved ${esc(d.entry.id)} · KB now ${d.count} entries (index rebuilt).`; adminClear(); await adminLoad(); } catch (e) { msg.innerHTML = `${esc(e.message)}`; } } async function adminDelete(id) { try { const d = await adminReq('DELETE', '/admin/kb/' + encodeURIComponent(id)); $('admin-form-msg').innerHTML = `Deleted ${esc(id)} · KB now ${d.count} entries.`; await adminLoad(); } catch (e) { $('admin-form-msg').innerHTML = `${esc(e.message)}`; } } /* ---------- wire up ---------- */ function init() { setupTabs(); loadDateTime(); loadLanguages(); loadNews(); loadCommodities(); loadExperts(); fillCommoditiesTable(); setInterval(loadDateTime, 60000); $('news-region').onchange = loadNews; $('news-topic').onchange = loadNews; $('ask').onclick = ask; $('load-intel').onclick = loadIntel; $('loc').addEventListener('change', onLocationChange); // auto-refresh intel on location change $('load-prices').onclick = loadPrices; $('tr-find').onclick = loadListings; $('tr-create').onclick = createListing; loadListings(); $('classify').onclick = classifyImage; $('consult').onclick = telemedicine; $('request-consult').onclick = requestConsult; $('admin-unlock').onclick = adminUnlock; $('admin-save').onclick = adminSave; $('admin-clear').onclick = adminClear; $('doc-apply').onclick = applyDoctor; $('radio-load').onclick = loadRadio; $('trad-go').onclick = loadTraditional; $('club-find').onclick = loadClubs; $('club-create').onclick = createClub; $('sub-find').onclick = loadSubsidies; $('asub-post').onclick = adminPostSubsidyUpdate; loadSubsidyUpdates(); $('fin-find').onclick = loadFinance; loadFinance(); $('lr-find').onclick = loadLandRecords; $('lr-state').onchange = loadLandRecords; // Speaking avatar (Web Speech API). $('speak-btn').onclick = () => speak(LAST_ANSWER_TEXT, state().language); $('stop-btn').onclick = stopSpeak; if (ttsOK()) { try { window.speechSynthesis.getVoices(); window.speechSynthesis.onvoiceschanged = () => {}; } catch (e) { /* ignore */ } } else { setTts('voice not supported'); $('speak-btn').disabled = true; } // Docs dropdown menu (toggle; close on outside click / Escape). const dt = $('docs-toggle'), dm = $('docs-menu'); if (dt && dm) { dt.onclick = (e) => { e.stopPropagation(); dm.classList.toggle('open'); }; document.addEventListener('click', (e) => { if (!dm.contains(e.target) && e.target !== dt) dm.classList.remove('open'); }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') dm.classList.remove('open'); }); } } document.addEventListener('DOMContentLoaded', init);