Spaces:
Sleeping
Sleeping
| /* 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, '<strong>$1</strong>').replace(/\n/g, '<br>'); | |
| 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 = '<div class="spinner"></div> loading…'; }; | |
| const errBox = (e) => `<div class="alert danger">Request failed (${esc(e.message)}). Is the API reachable?</div>`; | |
| 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]) => | |
| `<option value="${c}">${esc(n)}</option>`).join(''); | |
| } catch (e) { $('lang').innerHTML = '<option value="en">English</option>'; } | |
| } | |
| 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]) => `<option value="${c}">${esc(n)}</option>`).join(''); | |
| $('news-topic').innerHTML = (d.available_topics || ['Top stories']) | |
| .map(t => `<option>${esc(t)}</option>`).join(''); | |
| $('news-region').dataset.init = '1'; | |
| } | |
| const items = d.items || []; | |
| $('news-ticker').innerHTML = '📰 ' + (items.length | |
| ? items.map(i => `<a href="${esc(i.link)}" target="_blank">${esc(i.title)}</a>`).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 `<span>${esc(c.name)}: <i>n/a</i></span>`; | |
| let delta = ''; | |
| if (c.change_pct != null) { | |
| const cls = c.change_pct >= 0 ? 'up' : 'down'; | |
| delta = ` <span class="${cls}">${c.change_pct >= 0 ? '▲' : '▼'}${Math.abs(c.change_pct)}%</span>`; | |
| } | |
| return `<span>${esc(c.name)}: ${esc(c.currency)}${c.price}/${esc(c.unit)}${delta}</span>`; | |
| }); | |
| $('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) => | |
| `<button class="tab${i === 0 ? ' active' : ''}" data-t="${esc(n)}">${esc(n)}</button>`).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 = '<div class="alert info">Type a question first.</div>'; 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 = `<div class="answer">${md(r.answer)}</div>`; | |
| if (r.citations && r.citations.length) | |
| html += `<div class="cite">Citations: ${r.citations.map(c => `[${c.n}] ${esc(c.source)} (${esc(c.crop)})`).join(' · ')}</div>`; | |
| html += `<div class="cite">lang ${esc(r.language)} · ${r.latency_ms} ms · ${esc(r.backends?.embedding || '')}</div>`; | |
| 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 `<div class="card"><h3>${title}</h3>${body}</div>`; } | |
| function kv(k, v) { return `<div class="kv"><span class="muted">${esc(k)}</span><span class="v">${v}</span></div>`; } | |
| 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 = '<div class="alert info">Enter a location above.</div>'; return; } | |
| intelLoadedFor = s.location; | |
| root.innerHTML = '<div class="spinner"></div> 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 += '<div style="margin:8px 0 2px"><b>Forecast</b></div>'; | |
| (w.daily || []).forEach(d => { | |
| b += `<div class="kv"><span class="muted">${esc(d.date)}</span> | |
| <span class="v">${num(d.tmin_c)}–${num(d.tmax_c)}°C · rain ${num(d.precip_mm, ' mm')}` | |
| + (d.precip_prob != null ? ` (${d.precip_prob}%)` : '') + `</span></div>`; | |
| }); | |
| (w.advisories || []).forEach(a => b += `<div class="alert warn">${esc(a)}</div>`); | |
| b += `<div class="cite">Source: ${esc(w.source || 'Open-Meteo')}</div>`; | |
| 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 = `<div class="metrics"> | |
| <div class="metric"><div class="n">${num(e.elevation_m, 'm')}</div><div class="l">Altitude</div></div> | |
| <div class="metric"><div class="n">${num(e.population)}</div><div class="l">Population</div></div> | |
| <div class="metric"><div class="n">${num(e.humidity_pct, '%')}</div><div class="l">Humidity</div></div> | |
| <div class="metric"><div class="n">${num(wd.speed_kmh)} ${esc(wd.direction_compass || '')}</div><div class="l">Wind km/h</div></div> | |
| <div class="metric"><div class="n">${num(s.sunshine_hours, 'h')}</div><div class="l">Sunshine</div></div> | |
| <div class="metric"><div class="n">${num(aq.us_aqi)}</div><div class="l">US AQI ${esc(aq.category || '')}</div></div> | |
| <div class="metric"><div class="n">${num(aq.pm2_5)}</div><div class="l">PM2.5</div></div> | |
| <div class="metric"><div class="n">${gw.level_m != null ? gw.level_m + 'm' : num(gw.soil_moisture_m3m3)}</div><div class="l">Groundwater</div></div> | |
| </div><div class="cite">${esc(gw.note || '')}</div>`; | |
| return card('🌍 Environment — ' + esc(e.location_name), b); | |
| } | |
| function satCard(x) { | |
| const ac = x.agroclimate || {}, nd = x.numeric_ndvi; | |
| let b = `<img class="sat" src="${esc(x.imagery.ndvi)}" alt="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 += `<div class="alert ok">${esc(n)}</div>`); | |
| b += nd && nd.latest != null ? kv('Field NDVI', `${nd.latest} (${esc(nd.status)})`) | |
| : `<div class="cite">Field NDVI: configure Earth Engine to enable.</div>`; | |
| b += `<div class="cite"><a href="${esc(x.imagery.worldview)}" target="_blank">Open in NASA Worldview</a></div>`; | |
| return card('🛰️ Satellite — ' + esc(x.location_name), b); | |
| } | |
| function planetCard(p) { | |
| const mp = p.moon_phase || {}; | |
| let b = `<div class="alert info">🌙 ${esc(mp.name)} — ${Math.round((mp.illumination || 0) * 100)}% illuminated</div>`; | |
| b += '<table class="tbl"><tr><th>Body</th><th>Alt</th><th>Az</th><th></th></tr>' + | |
| (p.bodies || []).map(x => `<tr><td>${esc(x.name)}</td><td>${x.altitude_deg}°</td> | |
| <td>${x.azimuth_deg}° ${esc(x.azimuth_compass)}</td><td>${x.above_horizon ? '✅' : '—'}</td></tr>`).join('') + '</table>'; | |
| 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 => `<div class="alert warn"><b>${esc(e.category)}</b> — ${esc(e.title)}${e.distance_km != null ? ' · ' + e.distance_km + ' km' : ''}</div>`).join('') | |
| : '<div class="alert ok">No hazard events within range.</div>'; | |
| if (h.fires === null) b += '<div class="cite">🔥 Active fires: set a NASA FIRMS key to enable.</div>'; | |
| else if (h.fires.length) b += `<div class="alert danger">🔥 ${h.fires.length} active fire(s) nearby; nearest ${h.fires[0].distance_km} km.</div>`; | |
| else b += '<div class="cite">🔥 No active fires nearby.</div>'; | |
| return card('⚠️ Hazards — ' + esc(h.location_name), b); | |
| } | |
| function advisoryCard(a) { | |
| const items = a.advisories || []; | |
| const b = (items.length ? items : []).map(x => | |
| `<div class="alert ${x.urgency === 'high' ? 'danger' : x.urgency === 'medium' ? 'warn' : 'ok'}"> | |
| <span class="badge ${x.urgency}">${esc(x.urgency)}</span> <b>${esc(x.title)}</b>: ${esc(x.action)} | |
| <div class="cite">${esc(x.rationale)}</div></div>`).join('') || '<div class="alert ok">No urgent advisories.</div>'; | |
| 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 += `<div class="metrics"> | |
| <div class="metric"><div class="n">₹${s.modal_min}</div><div class="l">min</div></div> | |
| <div class="metric"><div class="n">₹${s.modal_avg}</div><div class="l">avg</div></div> | |
| <div class="metric"><div class="n">₹${s.modal_max}</div><div class="l">max</div></div></div>`; | |
| (p.records || []).slice(0, 6).forEach(r => b += kv(`${esc(r.market)} (${esc(r.state)})`, '₹' + num(r.modal_price))); | |
| (p.notes || []).forEach(n => b += `<div class="cite">${esc(n)}</div>`); | |
| return `<div style="margin-top:8px">${b || '<span class="muted">No price records.</span>'}</div>`; | |
| } | |
| 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) : '<div class="alert info">Prices unavailable — set a data.gov.in key (AGROSENSE_DATAGOV_API_KEY).</div>'; | |
| } catch (e) { out.innerHTML = errBox(e); } | |
| } | |
| async function fillCommoditiesTable() { | |
| try { | |
| const d = await getJSON('/commodities'); | |
| $('commodities').innerHTML = '<table class="tbl"><tr><th>Commodity</th><th>Price</th><th>Δ</th></tr>' + | |
| d.items.map(c => `<tr><td>${esc(c.name)}</td><td>${c.price == null ? 'n/a' : esc(c.currency) + c.price + '/' + esc(c.unit)}</td> | |
| <td>${c.change_pct == null ? '' : (c.change_pct >= 0 ? '▲' : '▼') + Math.abs(c.change_pct) + '%'}</td></tr>`).join('') + '</table>'; | |
| } catch (e) { $('commodities').innerHTML = errBox(e); } | |
| } | |
| /* ---------- farmer trading platform ---------- */ | |
| let TRADE_USER = ''; | |
| function trBadge(l) { | |
| return l.type === 'sell' | |
| ? '<span class="badge low">Selling</span>' : '<span class="badge medium">Buying</span>'; | |
| } | |
| 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 => `<div class="msg"> | |
| ${trBadge(l)} <b>${esc(l.commodity)}</b> | |
| <span class="cite">${trPrice(l)}</span> | |
| ${l.grade ? `<span class="cite">· ${esc(l.grade)}</span>` : ''} | |
| ${l.location || l.state ? `<span class="cite">· 📍 ${esc(l.location || l.state)}</span>` : ''} | |
| ${l.inquiry_count ? `<span class="cite">· 💬 ${l.inquiry_count}</span>` : ''} | |
| <div class="small" style="margin:4px 0">${esc(l.description || '')}</div> | |
| <button class="ghost tr-open" data-id="${esc(l.id)}">View & contact</button> | |
| </div>`).join('') || '<span class="muted">No matching listings. Post one below!</span>'; | |
| out.innerHTML += `<div class="disclaimer">🤝 ${esc(d.disclaimer || '')}</div>`; | |
| 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 => `<div class="msg"><b>${esc(q.name)}</b> | |
| ${q.contact ? `<span class="cite">· ☎ ${esc(q.contact)}</span>` : ''} | |
| ${q.quantity ? `<span class="cite">· wants ${q.quantity}</span>` : ''} | |
| ${q.at ? `<span class="cite">· ${esc(q.at)}</span>` : ''} | |
| <br>${esc(q.message || '')}</div>`).join(''); | |
| $('tr-detail').innerHTML = `<div class="card"> | |
| <h3>${trBadge(l)} ${esc(l.commodity)} ${closed ? '<span class="badge high">Closed</span>' : ''}</h3> | |
| <div class="cite">${trPrice(l)} ${l.grade ? '· ' + esc(l.grade) : ''} | |
| ${l.location || l.state ? '· 📍 ' + esc(l.location || '') + ' ' + esc(l.state || '') : ''}</div> | |
| <p>${esc(l.description || '')}</p> | |
| <div class="cite">Posted by ${esc(l.seller || 'Anonymous')}${l.harvest_date ? ' · ' + esc(l.harvest_date) : ''} | |
| ${l.contact ? ' · ' + esc(l.contact) : ''}</div> | |
| <div class="row" style="margin-top:6px"> | |
| <a href="${esc(l.room_url)}" target="_blank"><button>🎥 Negotiate (video room)</button></a> | |
| ${closed ? '' : `<button class="ghost" id="tr-close" data-id="${esc(l.id)}">Mark sold/closed</button>`} | |
| </div> | |
| <h3 style="margin-top:10px">💬 Inquiries</h3> | |
| <div class="chat">${inqs || '<span class="muted">No inquiries yet.</span>'}</div> | |
| ${closed ? '<div class="cite">This listing is closed.</div>' : `<div class="row" style="margin-top:8px"> | |
| <input id="inq-name" placeholder="Your name" value="${esc(TRADE_USER)}" style="width:130px" /> | |
| <input id="inq-contact" placeholder="Your contact (phone)" style="width:160px" /> | |
| <input id="inq-qty" placeholder="qty" style="width:70px" /> | |
| <input id="inq-msg" placeholder="Your offer / message…" style="flex:1" /> | |
| <button id="inq-send" data-id="${esc(l.id)}">Send inquiry</button> | |
| </div>`}</div>`; | |
| 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 = `<span style="color:var(--ok)">Posted your ${esc(d.listing.type)} listing for ${esc(d.listing.commodity)}.</span>`; | |
| ['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 = `<span style="color:var(--danger)">${esc(e.message)}</span>`; } | |
| } | |
| /* ---------- plant clinic ---------- */ | |
| async function classifyImage() { | |
| const out = $('vision'); const f = $('img').files[0]; | |
| if (!f) { out.innerHTML = '<div class="alert info">Choose an image first.</div>'; return; } | |
| spin(out); | |
| try { | |
| const r = await postForm('/vision/classify?task=all', { file: f }); | |
| const line = (t, o) => `<div class="kv"><span class="muted">${t}</span><span class="v">${esc(o.label)} (${Math.round((o.confidence || 0) * 100)}%)</span></div><div class="cite">${esc(o.note || '')}</div>`; | |
| 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 = `<div class="alert ${r.health_status === 'Likely healthy' ? 'ok' : 'warn'}"> | |
| <b>${esc(r.diagnosis)}</b> · ${esc(r.health_status)} · severity ${esc(r.severity)} · ${Math.round((r.confidence || 0) * 100)}%</div>`; | |
| if (r.weather_note) b += `<div class="alert warn">🌦️ ${esc(r.weather_note)}</div>`; | |
| b += '<b>📋 Prescription</b>'; | |
| (r.prescription || []).forEach(p => b += `<div class="msg"><b>${esc(p.category)}:</b> ${esc(p.instruction)}</div>`); | |
| if (r.citations?.length) b += `<div class="cite">Sources: ${esc(r.citations.join(', '))}</div>`; | |
| b += `<div class="disclaimer">⚕️ ${esc(r.disclaimer)}</div>`; | |
| out.innerHTML = b; | |
| } catch (e) { out.innerHTML = errBox(e); } | |
| } | |
| /* ---------- live doctor ---------- */ | |
| const stars = (avg, count) => avg == null ? '<span class="cite">no ratings yet</span>' | |
| : `<span style="color:var(--warn)">${'★'.repeat(Math.round(avg))}${'☆'.repeat(5 - Math.round(avg))}</span> | |
| <span class="cite">${avg} (${count})</span>`; | |
| async function loadExperts() { | |
| try { | |
| const d = await getJSON('/experts'); | |
| $('experts').innerHTML = d.experts.map(e => `<div class="msg"> | |
| <b>${esc(e.name)}</b> — ${esc(e.specialization)} ${stars(e.rating_avg, e.rating_count)} | |
| <div class="cite">${esc(e.region)} · ${esc(e.languages.join(', '))} | |
| ${e.registration_no ? '· Reg ' + esc(e.registration_no) : ''}</div> | |
| <button class="ghost exp-profile" data-id="${esc(e.id)}" style="margin-top:4px">View profile</button> | |
| </div>`).join('') || '<span class="muted">No verified doctors yet.</span>'; | |
| $('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 = `<div class="card"><h3>👨⚕️ ${esc(p.name)}</h3> | |
| <div>${esc(p.specialization)} ${stars(p.rating_avg, p.rating_count)}</div> | |
| <div class="kv"><span class="muted">Region</span><span class="v">${esc(p.region)}</span></div> | |
| <div class="kv"><span class="muted">Languages</span><span class="v">${esc(p.languages.join(', '))}</span></div> | |
| <div class="kv"><span class="muted">ICAR / Reg. no.</span><span class="v">${esc(p.registration_no || '—')}</span></div> | |
| <div class="kv"><span class="muted">Credentials</span><span class="v">${esc(p.credentials || '—')}</span></div> | |
| <div class="kv"><span class="muted">Contact</span><span class="v">${esc(p.contact || '—')}</span></div>`; | |
| if (p.ratings.length) b += '<h3 style="margin-top:10px">Recent reviews</h3>' + | |
| p.ratings.slice().reverse().map(r => `<div class="msg"><span style="color:var(--warn)">${'★'.repeat(r.stars)}</span> | |
| ${esc(r.comment || '')} <span class="cite">${esc(r.at || '')}</span></div>`).join(''); | |
| b += `<div class="row" style="margin-top:10px;align-items:flex-end"> | |
| <label class="field" style="width:90px"><span>Stars</span> | |
| <select id="rate-stars">${[5, 4, 3, 2, 1].map(s => `<option value="${s}">${s} ★</option>`).join('')}</select></label> | |
| <input id="rate-comment" placeholder="Leave a review (optional)" style="flex:1"> | |
| <button id="rate-go" data-id="${esc(p.id)}">Rate</button></div> | |
| <div id="rate-msg" class="cite"></div></div>`; | |
| 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 = `<span style="color:var(--danger)">${esc(e.message)}</span>`; } | |
| } | |
| function renderConsult(c) { | |
| const out = $('consult-session'); | |
| let b = `<div class="kv"><span class="muted">Consultation ${esc(c.id)}</span><span class="v">${esc(c.status)}</span></div>`; | |
| if (c.expert) b += `<div class="alert ok">👨⚕️ <b>${esc(c.expert.name)}</b> — ${esc(c.expert.specialization)}<br> | |
| <span class="cite">${esc(c.expert.region)} · ${esc(c.expert.languages.join(', '))}</span></div>`; | |
| if (c.summary) b += `<div class="cite">Shared: ${esc(c.summary)}</div>`; | |
| const okch = (c.notifications || []).filter(n => n.ok).map(n => n.channel).join(', '); | |
| if (okch) b += `<div class="cite">🔔 Notified via: ${esc(okch)}</div>`; | |
| if (c.room_url) b += `<a href="${esc(c.room_url)}" target="_blank"><button style="margin:8px 0">🎥 Join live video room</button></a>`; | |
| b += '<div class="chat">' + (c.messages || []).map(m => { | |
| const who = { farmer: '🧑🌾 You', expert: '👨⚕️ Expert', system: 'ℹ️ System' }[m.sender] || m.sender; | |
| return `<div class="msg"><b>${who}:</b> ${esc(m.text)} ${m.at ? `<span class="cite">${esc(m.at)}</span>` : ''}</div>`; | |
| }).join('') + '</div>'; | |
| b += `<div class="row" style="margin-top:8px"><input id="cmsg" placeholder="Message the expert…" style="flex:1"> | |
| <button id="csend" data-id="${esc(c.id)}">Send</button></div>`; | |
| 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 = `<span style="color:var(--ok)">Submitted as ${esc(d.doctor.id)} — ${esc(d.message)}</span>`; | |
| ['doc-name', 'doc-spec', 'doc-region', 'doc-langs', 'doc-contact', 'doc-cred', 'doc-reg'].forEach(i => $(i).value = ''); | |
| } catch (e) { | |
| msg.innerHTML = `<span style="color:var(--danger)">${esc(e.message)}</span>`; | |
| } | |
| } | |
| 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 => `<div class="msg"> | |
| <b>${esc(c.name)}</b> <span class="badge ${c.type === 'location' ? 'low' : 'medium'}">${esc(c.type)}</span> | |
| <span class="cite">${esc(c.key)} · ${c.member_count} member(s)</span> | |
| <div class="row" style="margin-top:4px"><button class="ghost club-open" data-id="${esc(c.id)}">Open</button></div> | |
| </div>`).join('') || '<span class="muted">No clubs found — create one!</span>'; | |
| 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 = `<span style="color:var(--ok)">Created "${esc(d.club.name)}".</span>`; | |
| ['nc-name', 'nc-key', 'nc-desc', 'nc-creator'].forEach(i => $(i).value = ''); | |
| await loadClubs(); openClub(d.club.id); | |
| } catch (e) { msg.innerHTML = `<span style="color:var(--danger)">${esc(e.message)}</span>`; } | |
| } | |
| 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 => `<div class="msg"><b>${esc(p.author)}:</b> ${esc(p.text)} | |
| ${p.link ? `<a href="${esc(p.link)}" target="_blank">link</a>` : ''} | |
| ${p.at ? `<span class="cite">${esc(p.at)}</span>` : ''}</div>`).join(''); | |
| $('club-detail').innerHTML = `<div class="card"><h3>👥 ${esc(c.name)}</h3> | |
| <div class="cite">${esc(c.type)} · ${esc(c.key)} · ${c.member_count} member(s)</div> | |
| <p>${esc(c.description || '')}</p> | |
| <div class="row"> | |
| <input id="club-member" placeholder="Your name" value="${esc(CLUB_USER)}" style="flex:1" /> | |
| <button id="club-join" data-id="${esc(c.id)}">Join</button> | |
| <a href="${esc(c.room_url)}" target="_blank"><button>🎥 Join video meeting</button></a> | |
| </div> | |
| <div class="cite">Members: ${esc((c.members || []).join(', ') || '—')}</div> | |
| <h3 style="margin-top:10px">💬 Discussion & sharing</h3> | |
| <div class="chat">${posts || '<span class="muted">No posts yet.</span>'}</div> | |
| <div class="row" style="margin-top:8px"> | |
| <input id="club-post-author" placeholder="Your name" value="${esc(CLUB_USER)}" style="width:130px" /> | |
| <input id="club-post-text" placeholder="Share a message or info…" style="flex:1" /> | |
| <input id="club-post-link" placeholder="link (optional)" style="width:150px" /> | |
| <button id="club-send" data-id="${esc(c.id)}">Post</button> | |
| </div></div>`; | |
| $('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 = `<div class="alert ${box}" style="margin-top:8px"> | |
| <span class="badge ${cls}">${esc(a.verdict)}</span> for <b>${esc(d.activity)}</b> | |
| ${d.location_name ? '· ' + esc(d.location_name) : ''}</div>`; | |
| b += `<div class="cite">🪔 Panchang — Vaara ${esc(p.vaara)} · ${esc(p.tithi)} · ` | |
| + `Nakshatra ${esc(p.nakshatra)} · Yoga ${esc(p.yoga)} · Karana ${esc(p.karana)}</div>`; | |
| b += '<b>🔮 Astrological note</b>'; | |
| (a.reasons || []).forEach(r => b += `<div class="msg">• ${esc(r)}</div>`); | |
| b += '<b>🌾 Traditional practices</b>'; | |
| (d.practices || []).forEach(pr => b += `<div class="msg"><b>${esc(pr.title)}</b> | |
| <span class="cite">${esc(pr.region)} · ${esc(pr.source)}</span><br>${esc(pr.practice)}</div>`); | |
| b += `<div class="disclaimer">🪔 ${esc(d.disclaimer)}</div>`; | |
| 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 = '<option value="">All categories</option>' + | |
| d.categories.map(c => `<option value="${esc(c)}">${esc(c)}</option>`).join(''); | |
| $('fin-category').value = cur; | |
| FIN_CATS_LOADED = true; | |
| } | |
| const rows = (d.products || []).map(p => `<div class="msg"> | |
| <b>${esc(p.name)}</b> <span class="badge low">${esc(p.category)}</span> | |
| <span class="cite">· ${esc(p.provider)}</span> | |
| <div class="small" style="margin:4px 0">${esc(p.summary)}</div> | |
| <span class="cite">💸 ${esc(p.interest || '')} ${p.loan_amount ? '· ' + esc(p.loan_amount) : ''}</span> | |
| <div class="row" style="margin-top:4px"><button class="ghost fin-open" data-id="${esc(p.id)}">View & apply</button></div> | |
| </div>`).join('') || '<span class="muted">No matching products.</span>'; | |
| out.innerHTML = rows + (d.disclaimer ? `<div class="disclaimer">🏦 ${esc(d.disclaimer)}</div>` : ''); | |
| 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 => `<li>${esc(i)}</li>`).join(''); | |
| const steps = (p.application_process || []).map(s => `<li>${esc(s)}</li>`).join(''); | |
| const s = state(); | |
| $('fin-detail').innerHTML = `<div class="card"><h3>🏦 ${esc(p.name)}</h3> | |
| <div class="cite"><span class="badge low">${esc(p.category)}</span> ${esc(p.provider)}</div> | |
| <p>${esc(p.summary)}</p> | |
| <div class="grid" style="grid-template-columns:repeat(auto-fit,minmax(180px,1fr))"> | |
| ${p.interest ? `<div class="msg"><b>Interest</b><br>${esc(p.interest)}</div>` : ''} | |
| ${p.loan_amount ? `<div class="msg"><b>Loan amount</b><br>${esc(p.loan_amount)}</div>` : ''} | |
| ${p.tenure ? `<div class="msg"><b>Tenure</b><br>${esc(p.tenure)}</div>` : ''} | |
| </div> | |
| ${p.benefits ? `<h3>💰 Benefits</h3><p>${esc(p.benefits)}</p>` : ''} | |
| ${p.eligibility ? `<h3>✅ Eligibility</h3><p>${esc(p.eligibility)}</p>` : ''} | |
| ${steps ? `<h3>📝 How to apply</h3><ol>${steps}</ol>` : ''} | |
| ${(p.documents || []).length ? `<h3>📄 Documents needed</h3><ul>${li(p.documents)}</ul>` : ''} | |
| <div class="row" style="margin-top:8px"> | |
| ${p.portal ? `<a href="${esc(p.portal)}" target="_blank"><button>🔗 Official portal</button></a>` : ''} | |
| ${p.helpline ? `<span class="cite">☎ ${esc(p.helpline)}</span>` : ''} | |
| </div> | |
| <h3 style="margin-top:10px">🧾 Lodge a loan enquiry</h3> | |
| <div class="row"> | |
| <input id="fa-name" placeholder="Your name *" style="flex:1" /> | |
| <input id="fa-contact" placeholder="Phone / email *" style="flex:1" /> | |
| </div> | |
| <div class="row"> | |
| <input id="fa-amount" placeholder="Amount needed (₹, optional)" style="flex:1" /> | |
| <input id="fa-location" placeholder="Location" value="${esc(s.location)}" style="flex:1" /> | |
| </div> | |
| <input id="fa-message" placeholder="Purpose / message (optional)" /> | |
| <button id="fa-send" data-id="${esc(p.id)}" style="margin-top:8px">Submit enquiry</button> | |
| <div id="fa-msg" class="cite"></div> | |
| ${p.source ? `<div class="cite">Source: ${esc(p.source)}</div>` : ''} | |
| ${p.disclaimer ? `<div class="disclaimer">🏦 ${esc(p.disclaimer)}</div>` : ''}</div>`; | |
| $('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 = `<span style="color:var(--ok)">Enquiry ${esc(d.application.id)} lodged. ${esc(d.message)}</span>`; | |
| ['fa-name', 'fa-contact', 'fa-amount', 'fa-message'].forEach(i => $(i).value = ''); | |
| } catch (e) { msg.innerHTML = `<span style="color:var(--danger)">${esc(e.message)}</span>`; } | |
| } | |
| /* ---------- 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 => `<div class="msg"> | |
| <b>${esc(x.name)}</b> | |
| <span class="badge ${x.level === 'central' ? 'medium' : 'low'}">${esc(x.level === 'central' ? 'Central' : 'State')}</span> | |
| ${x.state ? `<span class="cite">${esc(x.state)}</span>` : ''} | |
| <span class="cite">· ${esc(x.category)}${x.update_count ? ' · 📢 ' + x.update_count : ''}</span> | |
| <div class="small" style="margin:4px 0">${esc(x.summary)}</div> | |
| <button class="ghost sub-open" data-id="${esc(x.id)}">View details</button> | |
| </div>`).join('') || '<span class="muted">No schemes match. Try clearing filters.</span>'; | |
| out.innerHTML = rows + (d.disclaimer ? `<div class="disclaimer">🏛️ ${esc(d.disclaimer)}</div>` : ''); | |
| 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 => `<li>${esc(i)}</li>`).join(''); | |
| const steps = (x.application_process || []).map(s => `<li>${esc(s)}</li>`).join(''); | |
| const ups = (x.updates || []).slice().reverse().map(u => | |
| `<div class="msg"><span class="cite">${esc(u.date)}</span><br>${esc(u.text)}</div>`).join(''); | |
| $('sub-detail').innerHTML = `<div class="card"><h3>🏛️ ${esc(x.name)}</h3> | |
| <div class="cite"><span class="badge ${x.level === 'central' ? 'medium' : 'low'}">${esc(x.level === 'central' ? 'Central' : 'State')}</span> | |
| ${x.state ? esc(x.state) + ' · ' : ''}${esc(x.category)}</div> | |
| <p>${esc(x.summary)}</p> | |
| ${x.benefits ? `<h3>💰 Benefits</h3><p>${esc(x.benefits)}</p>` : ''} | |
| ${x.eligibility ? `<h3>✅ Eligibility</h3><p>${esc(x.eligibility)}</p>` : ''} | |
| ${steps ? `<h3>📝 How to apply</h3><ol>${steps}</ol>` : ''} | |
| ${(x.documents || []).length ? `<h3>📄 Documents needed</h3><ul>${li(x.documents)}</ul>` : ''} | |
| <div class="row" style="margin-top:8px"> | |
| ${x.portal ? `<a href="${esc(x.portal)}" target="_blank"><button>🔗 Official portal</button></a>` : ''} | |
| ${x.helpline ? `<span class="cite">☎ ${esc(x.helpline)}</span>` : ''} | |
| </div> | |
| ${ups ? `<h3 style="margin-top:10px">📢 Announcements & updates</h3>${ups}` : ''} | |
| ${x.source ? `<div class="cite">Source: ${esc(x.source)}</div>` : ''} | |
| ${x.disclaimer ? `<div class="disclaimer">🏛️ ${esc(x.disclaimer)}</div>` : ''}</div>`; | |
| 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 => `<div class="msg"> | |
| <span class="cite">${esc(u.date)} · ${esc(u.scheme)}</span><br>${esc(u.text)}</div>`).join('') | |
| || '<span class="muted">No announcements.</span>'; | |
| } 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 = '<option value="">Use my location</option>' + | |
| d.covered_states.map(x => `<option value="${esc(x)}">${esc(x)}</option>`).join(''); | |
| $('lr-state').value = chosen; | |
| LR_STATES_LOADED = true; | |
| } | |
| const li = (arr) => (arr || []).map(i => `<li>${esc(i)}</li>`).join(''); | |
| const ol = (arr) => (arr || []).map(i => `<li>${esc(i)}</li>`).join(''); | |
| const stateName = d.state || 'your State'; | |
| const dlq = qs(chosen ? { state: chosen } : { location: s.location }); | |
| const uncovered = d.covered === false; | |
| out.innerHTML = `<div class="card"> | |
| <h3>🗺️ ${esc(stateName)} — ${esc(d.system || 'land records')}</h3> | |
| ${uncovered ? `<div class="alert warn">No state-specific guide for "${esc(stateName)}" yet — showing the general all-India steps. Use the official portal below.</div>` : ''} | |
| <div class="cite">Record: <b>${esc(d.record_name || '')}</b></div> | |
| <p>${esc(d.summary || '')}</p> | |
| <div class="row" style="margin:6px 0"> | |
| ${d.portal ? `<a href="${esc(d.portal)}" target="_blank"><button>🔗 Open portal</button></a>` : ''} | |
| ${d.map_portal ? `<a href="${esc(d.map_portal)}" target="_blank"><button class="ghost">🗺️ Cadastral map</button></a>` : ''} | |
| ${d.helpline ? `<span class="cite">☎ ${esc(d.helpline)}</span>` : ''} | |
| </div> | |
| <h3>🔎 Search by</h3><ul>${li(d.search_by)}</ul> | |
| <h3>📝 How to search & download</h3><ol>${ol(d.steps)}</ol> | |
| <h3>📄 The record contains</h3><ul>${li(d.contains)}</ul> | |
| ${d.notes ? `<div class="cite">${esc(d.notes)}</div>` : ''} | |
| <h3 style="margin-top:10px">⬇️ Download a printable guide</h3> | |
| <div class="row"> | |
| <a href="/land-records/guide.pdf?${dlq}" target="_blank"><button>📕 PDF guide</button></a> | |
| <a href="/land-records/guide.docx?${dlq}" target="_blank"><button class="ghost">📘 Word guide</button></a> | |
| </div> | |
| <div class="disclaimer">🗺️ ${esc(d.disclaimer || '')}</div></div>`; | |
| } 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) => `<div class="msg"> | |
| <button class="ghost radio-play" data-i="${i}">▶ Play</button> | |
| <b>${esc(s.name)}</b> | |
| <span class="cite">${esc(s.state || '')} ${s.codec ? '· ' + esc(s.codec) : ''} ${s.bitrate ? s.bitrate + 'kbps' : ''}</span> | |
| </div>`).join('') || '<span class="muted">No stations found. Try a different filter.</span>'; | |
| 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: <b>${esc(s.name)}</b>` + | |
| (s.homepage ? ` — <a href="${esc(s.homepage)}" target="_blank">station site</a>` : ''); | |
| try { | |
| const p = a.play(); | |
| if (p && p.catch) p.catch(() => { | |
| $('radio-now').innerHTML += ' <span style="color:var(--danger)">(stream unavailable - try another)</span>'; | |
| }); | |
| } 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 = '<span style="color:var(--ok)">Unlocked. Changes rebuild the live index.</span>'; | |
| } catch (e) { | |
| $('admin-area').style.display = 'none'; | |
| st.innerHTML = `<span style="color:var(--danger)">${esc(e.message)}</span>`; | |
| } | |
| } | |
| async function adminLoadDoctors() { | |
| const d = await adminReq('GET', '/admin/doctors'); | |
| const badge = (s) => `<span class="badge ${s === 'verified' ? 'low' : s === 'rejected' ? 'high' : 'medium'}">${esc(s)}</span>`; | |
| $('admin-doctors').innerHTML = (d.doctors || []).map(x => ` | |
| <div class="msg"><b>${esc(x.name)}</b> ${badge(x.status)} | |
| <div class="cite">${esc(x.specialization)} · ${esc(x.region)} · ${esc((x.languages || []).join(', '))} | |
| · ${esc(x.contact || '')} · ${esc(x.credentials || '')}</div> | |
| ${x.status !== 'verified' ? `<button class="ghost doc-ok" data-id="${esc(x.id)}">Verify</button>` : ''} | |
| ${x.status !== 'rejected' ? `<button class="ghost doc-no" data-id="${esc(x.id)}">Reject</button>` : ''} | |
| </div>`).join('') || '<span class="muted">No doctors.</span>'; | |
| $('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 => | |
| `<option value="${esc(s.id)}">${esc(s.name)} (${esc(s.level)})</option>`).join(''); | |
| } | |
| async function adminPostSubsidyUpdate() { | |
| const msg = $('asub-msg'); const text = $('asub-text').value.trim(); | |
| if (!text) { msg.innerHTML = '<span style="color:var(--danger)">Announcement text is required.</span>'; 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 = `<span style="color:var(--ok)">Posted to "${esc(d.scheme.name)}".</span>`; | |
| $('asub-text').value = ''; $('asub-date').value = ''; | |
| loadSubsidyUpdates(); // refresh the public feed | |
| } catch (e) { msg.innerHTML = `<span style="color:var(--danger)">${esc(e.message)}</span>`; } | |
| } | |
| 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 => `<div class="msg"> | |
| <b>${esc(a.name)}</b> <span class="cite">· ${esc(a.product)}</span> | |
| ${a.amount ? `<span class="cite">· ₹${a.amount}</span>` : ''} | |
| <div class="cite">☎ ${esc(a.contact)} ${a.location ? '· 📍 ' + esc(a.location) : ''} ${a.at ? '· ' + esc(a.at) : ''}</div> | |
| ${a.message ? `<div class="small">${esc(a.message)}</div>` : ''} | |
| </div>`).join('') || '<span class="muted">No loan enquiries yet.</span>'; | |
| } | |
| 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 => ` | |
| <div class="msg"><b>${esc(e.crop)}</b> <span class="cite">${esc(e.id)}</span> | |
| <span class="cite">· ${esc(e.soil_type || '')} · ${esc(e.source || '')}</span> | |
| <div class="row" style="margin-top:4px"> | |
| <button class="ghost adm-edit" data-id="${esc(e.id)}">Edit</button> | |
| <button class="ghost adm-del" data-id="${esc(e.id)}">Delete</button> | |
| </div></div>`).join('') || '<span class="muted">No entries.</span>'; | |
| $('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 = `<span style="color:var(--ok)">Saved ${esc(d.entry.id)} · KB now ${d.count} entries (index rebuilt).</span>`; | |
| adminClear(); await adminLoad(); | |
| } catch (e) { msg.innerHTML = `<span style="color:var(--danger)">${esc(e.message)}</span>`; } | |
| } | |
| async function adminDelete(id) { | |
| try { const d = await adminReq('DELETE', '/admin/kb/' + encodeURIComponent(id)); | |
| $('admin-form-msg').innerHTML = `<span class="cite">Deleted ${esc(id)} · KB now ${d.count} entries.</span>`; | |
| await adminLoad(); | |
| } catch (e) { $('admin-form-msg').innerHTML = `<span style="color:var(--danger)">${esc(e.message)}</span>`; } | |
| } | |
| /* ---------- 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); | |