Spaces:
Sleeping
Sleeping
File size: 55,097 Bytes
d27b187 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 | /* 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);
|