/* 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]) =>
`${esc(n)} `).join('');
} catch (e) { $('lang').innerHTML = 'English '; }
}
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]) => `${esc(n)} `).join('');
$('news-topic').innerHTML = (d.available_topics || ['Top stories'])
.map(t => `${esc(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) =>
`${esc(n)} `).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 || '')}
${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 = ` `;
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 += ``;
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 += 'Body Alt Az ' +
(p.bodies || []).map(x => `${esc(x.name)} ${x.altitude_deg}°
${x.azimuth_deg}° ${esc(x.azimuth_compass)} ${x.above_horizon ? '✅' : '—'} `).join('') + '
';
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 += ``;
(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 = 'Commodity Price Δ ' +
d.items.map(c => `${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) + '%'} `).join('') + '
';
} 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 || '')}
View & contact
`).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 = ``;
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) : ''}
View profile
`).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 += `
Stars
${[5, 4, 3, 2, 1].map(s => `${s} ★ `).join('')}
Rate
`;
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 += `🎥 Join live video room `;
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 += `
Send
`;
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)
Open
`).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 = ``;
$('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 = 'All categories ' +
d.categories.map(c => `${esc(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) : ''}
View & apply
`).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 ` : ''}
🧾 Lodge a loan enquiry
Submit 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)}
View details
`).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 ` : ''}
${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 = 'Use my location ' +
d.covered_states.map(x => `${esc(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 || '')}
🔎 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) => `
▶ Play
${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' ? `
Verify ` : ''}
${x.status !== 'rejected' ? `
Reject ` : ''}
`).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 =>
`${esc(s.name)} (${esc(s.level)}) `).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 || '')}
Edit
Delete
`).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);