Update frontend/index.html
Browse files- frontend/index.html +260 -4
frontend/index.html
CHANGED
|
@@ -453,6 +453,114 @@ h1 em{color:var(--leaf-l);font-style:italic}
|
|
| 453 |
}
|
| 454 |
.reset-btn:hover{background:rgba(45,106,79,0.1);border-color:var(--leaf-l)}
|
| 455 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 456 |
/* ββ Responsive ββ */
|
| 457 |
@media(max-width:540px){
|
| 458 |
.info-grid{grid-template-columns:1fr}
|
|
@@ -461,6 +569,8 @@ h1 em{color:var(--leaf-l);font-style:italic}
|
|
| 461 |
.result-hero{flex-direction:column;gap:0.8rem}
|
| 462 |
.url-row{flex-direction:column}
|
| 463 |
.stats-row{gap:0.8rem}
|
|
|
|
|
|
|
| 464 |
}
|
| 465 |
</style>
|
| 466 |
</head>
|
|
@@ -530,16 +640,148 @@ h1 em{color:var(--leaf-l);font-style:italic}
|
|
| 530 |
<div id="result-section">
|
| 531 |
<div class="result-main" id="result-main"></div>
|
| 532 |
<div class="top5-card" id="top5-card"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 533 |
<button class="reset-btn" onclick="resetAll()">πΏ Analyze another leaf</button>
|
| 534 |
</div>
|
| 535 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 536 |
</div><!-- /wrap -->
|
| 537 |
|
| 538 |
<script>
|
| 539 |
const API = window.location.origin;
|
| 540 |
let blob = null, imgUrl = null;
|
| 541 |
|
| 542 |
-
/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 543 |
(function(){
|
| 544 |
const c = document.getElementById('particles');
|
| 545 |
for(let i=0;i<18;i++){
|
|
@@ -551,12 +793,12 @@ let blob = null, imgUrl = null;
|
|
| 551 |
}
|
| 552 |
})();
|
| 553 |
|
| 554 |
-
/
|
| 555 |
document.getElementById('fileInput').onchange=e=>{
|
| 556 |
const f=e.target.files[0]; if(f)handleFile(f);
|
| 557 |
};
|
| 558 |
|
| 559 |
-
/
|
| 560 |
const dz=document.getElementById('drop-zone');
|
| 561 |
// Click-to-upload support (fix Choose image)
|
| 562 |
document.getElementById('pick-btn').onclick = (e) => {
|
|
@@ -579,7 +821,7 @@ dz.ondrop=e=>{
|
|
| 579 |
else showError('Please drop an image file.');
|
| 580 |
};
|
| 581 |
|
| 582 |
-
/
|
| 583 |
document.getElementById('url-btn').onclick=()=>{
|
| 584 |
const u=document.getElementById('url-input').value.trim();
|
| 585 |
if(!u){showError('Enter a URL first.');return}
|
|
@@ -624,7 +866,18 @@ async function analyze(){
|
|
| 624 |
}
|
| 625 |
const d=await res.json();
|
| 626 |
if(!res.ok||!d.success){showError(d.error||'Prediction failed.');return}
|
|
|
|
|
|
|
| 627 |
renderResult(d.result,d.inference_ms);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 628 |
}catch(e){
|
| 629 |
showError(`Cannot reach server. Make sure app.py is running: python app.py`);
|
| 630 |
}finally{
|
|
@@ -780,6 +1033,9 @@ function clearError(){document.getElementById('error-box').style.display='none'}
|
|
| 780 |
showError('Server not running. Start it with: python app.py');
|
| 781 |
}
|
| 782 |
})();
|
|
|
|
|
|
|
|
|
|
| 783 |
</script>
|
| 784 |
</body>
|
| 785 |
</html>
|
|
|
|
| 453 |
}
|
| 454 |
.reset-btn:hover{background:rgba(45,106,79,0.1);border-color:var(--leaf-l)}
|
| 455 |
|
| 456 |
+
/* ββ History (NEW) ββ */
|
| 457 |
+
.history-actions{
|
| 458 |
+
display:flex;
|
| 459 |
+
gap:10px;
|
| 460 |
+
margin-top:1rem;
|
| 461 |
+
}
|
| 462 |
+
.history-btn{
|
| 463 |
+
flex:1;
|
| 464 |
+
display:flex;align-items:center;justify-content:center;gap:8px;
|
| 465 |
+
background:rgba(45,106,79,0.15);
|
| 466 |
+
border:0.5px solid var(--border);
|
| 467 |
+
color:var(--leaf-xl);
|
| 468 |
+
padding:0.9rem;border-radius:var(--r);
|
| 469 |
+
font-family:'Inter',sans-serif;
|
| 470 |
+
font-size:0.9rem;font-weight:500;
|
| 471 |
+
cursor:pointer;
|
| 472 |
+
transition:all 0.2s;
|
| 473 |
+
animation:fadeUp 0.5s 0.25s ease both;
|
| 474 |
+
}
|
| 475 |
+
.history-btn:hover{background:rgba(45,106,79,0.3);border-color:var(--leaf-l)}
|
| 476 |
+
.history-btn:disabled{opacity:0.55;cursor:not-allowed}
|
| 477 |
+
|
| 478 |
+
#history-section{
|
| 479 |
+
margin-top:2.2rem;
|
| 480 |
+
padding-top:1.2rem;
|
| 481 |
+
}
|
| 482 |
+
.history-card{
|
| 483 |
+
background:rgba(13,31,18,0.85);
|
| 484 |
+
border:0.5px solid var(--border);
|
| 485 |
+
border-radius:var(--r-lg);
|
| 486 |
+
backdrop-filter:blur(16px);
|
| 487 |
+
overflow:hidden;
|
| 488 |
+
margin-top:1rem;
|
| 489 |
+
}
|
| 490 |
+
.history-head{
|
| 491 |
+
padding:1.2rem 1.4rem;
|
| 492 |
+
border-bottom:0.5px solid var(--border);
|
| 493 |
+
display:flex;align-items:center;justify-content:space-between;gap:12px;
|
| 494 |
+
}
|
| 495 |
+
.history-title{
|
| 496 |
+
font-family:'Instrument Serif',serif;
|
| 497 |
+
font-size:1.4rem;
|
| 498 |
+
color:var(--leaf-xl);
|
| 499 |
+
}
|
| 500 |
+
.history-sub{
|
| 501 |
+
font-size:0.8rem;
|
| 502 |
+
color:var(--muted);
|
| 503 |
+
margin-top:4px;
|
| 504 |
+
}
|
| 505 |
+
.history-clear{
|
| 506 |
+
background:transparent;
|
| 507 |
+
border:0.5px solid var(--border);
|
| 508 |
+
color:var(--muted);
|
| 509 |
+
padding:0.55rem 0.9rem;
|
| 510 |
+
border-radius:100px;
|
| 511 |
+
cursor:pointer;
|
| 512 |
+
transition:all 0.2s;
|
| 513 |
+
font-size:0.82rem;
|
| 514 |
+
}
|
| 515 |
+
.history-clear:hover{border-color:var(--leaf-l);color:var(--leaf-xl);background:rgba(45,106,79,0.1)}
|
| 516 |
+
|
| 517 |
+
.history-list{padding:1rem 1.4rem 1.4rem}
|
| 518 |
+
.hitem{
|
| 519 |
+
display:flex;
|
| 520 |
+
gap:12px;
|
| 521 |
+
padding:0.9rem 0;
|
| 522 |
+
border-bottom:0.5px solid var(--border);
|
| 523 |
+
}
|
| 524 |
+
.hitem:last-child{border-bottom:none;padding-bottom:0}
|
| 525 |
+
.hthumb{
|
| 526 |
+
width:64px;height:64px;
|
| 527 |
+
border-radius:12px;
|
| 528 |
+
border:0.5px solid var(--border);
|
| 529 |
+
object-fit:cover;
|
| 530 |
+
flex-shrink:0;
|
| 531 |
+
box-shadow:0 10px 25px rgba(0,0,0,0.35);
|
| 532 |
+
}
|
| 533 |
+
.hmeta{flex:1;min-width:0}
|
| 534 |
+
.htitle{
|
| 535 |
+
font-size:0.92rem;
|
| 536 |
+
color:var(--text);
|
| 537 |
+
font-weight:500;
|
| 538 |
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
| 539 |
+
}
|
| 540 |
+
.hrow{display:flex;flex-wrap:wrap;gap:10px;margin-top:6px}
|
| 541 |
+
.hpill{
|
| 542 |
+
font-size:0.72rem;
|
| 543 |
+
letter-spacing:0.02em;
|
| 544 |
+
color:var(--muted);
|
| 545 |
+
border:0.5px solid var(--border);
|
| 546 |
+
padding:4px 10px;
|
| 547 |
+
border-radius:100px;
|
| 548 |
+
background:rgba(45,106,79,0.08);
|
| 549 |
+
}
|
| 550 |
+
.hactions{display:flex;flex-direction:column;gap:8px;flex-shrink:0}
|
| 551 |
+
.hbtn{
|
| 552 |
+
background:rgba(45,106,79,0.15);
|
| 553 |
+
border:0.5px solid var(--border);
|
| 554 |
+
color:var(--leaf-xl);
|
| 555 |
+
padding:0.55rem 0.9rem;
|
| 556 |
+
border-radius:12px;
|
| 557 |
+
cursor:pointer;
|
| 558 |
+
transition:all 0.2s;
|
| 559 |
+
font-size:0.82rem;
|
| 560 |
+
white-space:nowrap;
|
| 561 |
+
}
|
| 562 |
+
.hbtn:hover{background:rgba(45,106,79,0.3);border-color:var(--leaf-l)}
|
| 563 |
+
|
| 564 |
/* ββ Responsive ββ */
|
| 565 |
@media(max-width:540px){
|
| 566 |
.info-grid{grid-template-columns:1fr}
|
|
|
|
| 569 |
.result-hero{flex-direction:column;gap:0.8rem}
|
| 570 |
.url-row{flex-direction:column}
|
| 571 |
.stats-row{gap:0.8rem}
|
| 572 |
+
.history-actions{flex-direction:column}
|
| 573 |
+
.hactions{flex-direction:row}
|
| 574 |
}
|
| 575 |
</style>
|
| 576 |
</head>
|
|
|
|
| 640 |
<div id="result-section">
|
| 641 |
<div class="result-main" id="result-main"></div>
|
| 642 |
<div class="top5-card" id="top5-card"></div>
|
| 643 |
+
|
| 644 |
+
<!-- NEW: history buttons -->
|
| 645 |
+
<div class="history-actions">
|
| 646 |
+
<button class="history-btn" id="view-history-btn" type="button" onclick="scrollToHistory()">π View past results</button>
|
| 647 |
+
<button class="history-btn" id="save-hint-btn" type="button" disabled title="History saves automatically after each analysis.">β
Saved to history</button>
|
| 648 |
+
</div>
|
| 649 |
+
|
| 650 |
<button class="reset-btn" onclick="resetAll()">πΏ Analyze another leaf</button>
|
| 651 |
</div>
|
| 652 |
|
| 653 |
+
<!-- NEW: History section -->
|
| 654 |
+
<section id="history-section">
|
| 655 |
+
<div class="history-card">
|
| 656 |
+
<div class="history-head">
|
| 657 |
+
<div>
|
| 658 |
+
<div class="history-title">Past results</div>
|
| 659 |
+
<div class="history-sub">Stored in this browser (localStorage). Click βReviewβ to re-open an older result.</div>
|
| 660 |
+
</div>
|
| 661 |
+
<button class="history-clear" type="button" onclick="clearHistory()">Clear</button>
|
| 662 |
+
</div>
|
| 663 |
+
<div class="history-list" id="history-list">
|
| 664 |
+
<!-- injected -->
|
| 665 |
+
</div>
|
| 666 |
+
</div>
|
| 667 |
+
</section>
|
| 668 |
+
|
| 669 |
</div><!-- /wrap -->
|
| 670 |
|
| 671 |
<script>
|
| 672 |
const API = window.location.origin;
|
| 673 |
let blob = null, imgUrl = null;
|
| 674 |
|
| 675 |
+
/* ββ History storage (NEW) ββ */
|
| 676 |
+
const HISTORY_KEY = 'leafscan_history_v1';
|
| 677 |
+
const HISTORY_MAX = 25;
|
| 678 |
+
|
| 679 |
+
function loadHistory(){
|
| 680 |
+
try{
|
| 681 |
+
const raw = localStorage.getItem(HISTORY_KEY);
|
| 682 |
+
const arr = raw ? JSON.parse(raw) : [];
|
| 683 |
+
return Array.isArray(arr) ? arr : [];
|
| 684 |
+
}catch{
|
| 685 |
+
return [];
|
| 686 |
+
}
|
| 687 |
+
}
|
| 688 |
+
function saveHistory(arr){
|
| 689 |
+
try{
|
| 690 |
+
localStorage.setItem(HISTORY_KEY, JSON.stringify(arr));
|
| 691 |
+
}catch(e){
|
| 692 |
+
// ignore quota errors
|
| 693 |
+
}
|
| 694 |
+
}
|
| 695 |
+
function addToHistory(entry){
|
| 696 |
+
const hist = loadHistory();
|
| 697 |
+
hist.unshift(entry);
|
| 698 |
+
const trimmed = hist.slice(0, HISTORY_MAX);
|
| 699 |
+
saveHistory(trimmed);
|
| 700 |
+
renderHistory();
|
| 701 |
+
}
|
| 702 |
+
function clearHistory(){
|
| 703 |
+
localStorage.removeItem(HISTORY_KEY);
|
| 704 |
+
renderHistory();
|
| 705 |
+
}
|
| 706 |
+
function scrollToHistory(){
|
| 707 |
+
const el = document.getElementById('history-section');
|
| 708 |
+
if(el) el.scrollIntoView({behavior:'smooth', block:'start'});
|
| 709 |
+
}
|
| 710 |
+
function renderHistory(){
|
| 711 |
+
const list = document.getElementById('history-list');
|
| 712 |
+
const hist = loadHistory();
|
| 713 |
+
|
| 714 |
+
// disable view button if empty
|
| 715 |
+
const viewBtn = document.getElementById('view-history-btn');
|
| 716 |
+
if(viewBtn) viewBtn.disabled = hist.length === 0;
|
| 717 |
+
|
| 718 |
+
if(!hist.length){
|
| 719 |
+
list.innerHTML = `<div class="info-val" style="padding:0.6rem 0;color:var(--muted);">
|
| 720 |
+
No past results yet. Run an analysis and it will appear here.
|
| 721 |
+
</div>`;
|
| 722 |
+
return;
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
list.innerHTML = hist.map((h, idx) => {
|
| 726 |
+
const title = h.is_leaf
|
| 727 |
+
? `${h.plant} β ${h.disease}`
|
| 728 |
+
: `Not a leaf`;
|
| 729 |
+
|
| 730 |
+
const when = new Date(h.ts).toLocaleString();
|
| 731 |
+
|
| 732 |
+
const conf = (h.confidence !== undefined && h.confidence !== null)
|
| 733 |
+
? `${Math.round(h.confidence*100)}%`
|
| 734 |
+
: '';
|
| 735 |
+
|
| 736 |
+
const sev = h.severity || '';
|
| 737 |
+
|
| 738 |
+
const ms = (h.inference_ms !== undefined && h.inference_ms !== null)
|
| 739 |
+
? `${Math.round(h.inference_ms)} ms`
|
| 740 |
+
: '';
|
| 741 |
+
|
| 742 |
+
return `<div class="hitem">
|
| 743 |
+
<img class="hthumb" src="${h.thumbnail || ''}" alt="thumbnail"/>
|
| 744 |
+
<div class="hmeta">
|
| 745 |
+
<div class="htitle" title="${escHtml(title)}">${escHtml(title)}</div>
|
| 746 |
+
<div class="hrow">
|
| 747 |
+
<span class="hpill">π ${escHtml(when)}</span>
|
| 748 |
+
${conf ? `<span class="hpill">π― ${escHtml(conf)}</span>` : ``}
|
| 749 |
+
${sev ? `<span class="hpill">β ${escHtml(sev)}</span>` : ``}
|
| 750 |
+
${ms ? `<span class="hpill">β± ${escHtml(ms)}</span>` : ``}
|
| 751 |
+
</div>
|
| 752 |
+
</div>
|
| 753 |
+
<div class="hactions">
|
| 754 |
+
<button class="hbtn" type="button" onclick="reviewHistory(${idx})">Review</button>
|
| 755 |
+
<button class="hbtn" type="button" onclick="deleteHistory(${idx})">Delete</button>
|
| 756 |
+
</div>
|
| 757 |
+
</div>`;
|
| 758 |
+
}).join('');
|
| 759 |
+
}
|
| 760 |
+
function deleteHistory(idx){
|
| 761 |
+
const hist = loadHistory();
|
| 762 |
+
hist.splice(idx, 1);
|
| 763 |
+
saveHistory(hist);
|
| 764 |
+
renderHistory();
|
| 765 |
+
}
|
| 766 |
+
function reviewHistory(idx){
|
| 767 |
+
const hist = loadHistory();
|
| 768 |
+
const h = hist[idx];
|
| 769 |
+
if(!h) return;
|
| 770 |
+
|
| 771 |
+
// Show result section and render the saved result
|
| 772 |
+
clearError();
|
| 773 |
+
document.getElementById('result-section').style.display='block';
|
| 774 |
+
document.getElementById('loading').style.display='none';
|
| 775 |
+
|
| 776 |
+
// ensure upload area isn't hiding important actions
|
| 777 |
+
// (do not alter existing flow too much)
|
| 778 |
+
renderResult(h, h.inference_ms);
|
| 779 |
+
// scroll back up to results
|
| 780 |
+
const res = document.getElementById('result-section');
|
| 781 |
+
if(res) res.scrollIntoView({behavior:'smooth', block:'start'});
|
| 782 |
+
}
|
| 783 |
+
|
| 784 |
+
/* ββ Particles ββ */
|
| 785 |
(function(){
|
| 786 |
const c = document.getElementById('particles');
|
| 787 |
for(let i=0;i<18;i++){
|
|
|
|
| 793 |
}
|
| 794 |
})();
|
| 795 |
|
| 796 |
+
/* ββ File input ββ */
|
| 797 |
document.getElementById('fileInput').onchange=e=>{
|
| 798 |
const f=e.target.files[0]; if(f)handleFile(f);
|
| 799 |
};
|
| 800 |
|
| 801 |
+
/* ββ Drag & drop ββ */
|
| 802 |
const dz=document.getElementById('drop-zone');
|
| 803 |
// Click-to-upload support (fix Choose image)
|
| 804 |
document.getElementById('pick-btn').onclick = (e) => {
|
|
|
|
| 821 |
else showError('Please drop an image file.');
|
| 822 |
};
|
| 823 |
|
| 824 |
+
/* ββ URL ββ */
|
| 825 |
document.getElementById('url-btn').onclick=()=>{
|
| 826 |
const u=document.getElementById('url-input').value.trim();
|
| 827 |
if(!u){showError('Enter a URL first.');return}
|
|
|
|
| 866 |
}
|
| 867 |
const d=await res.json();
|
| 868 |
if(!res.ok||!d.success){showError(d.error||'Prediction failed.');return}
|
| 869 |
+
|
| 870 |
+
// render result
|
| 871 |
renderResult(d.result,d.inference_ms);
|
| 872 |
+
|
| 873 |
+
// NEW: save into history for review later
|
| 874 |
+
const entry = {
|
| 875 |
+
ts: Date.now(),
|
| 876 |
+
inference_ms: d.inference_ms,
|
| 877 |
+
...d.result
|
| 878 |
+
};
|
| 879 |
+
addToHistory(entry);
|
| 880 |
+
|
| 881 |
}catch(e){
|
| 882 |
showError(`Cannot reach server. Make sure app.py is running: python app.py`);
|
| 883 |
}finally{
|
|
|
|
| 1033 |
showError('Server not running. Start it with: python app.py');
|
| 1034 |
}
|
| 1035 |
})();
|
| 1036 |
+
|
| 1037 |
+
// NEW: render history on load
|
| 1038 |
+
renderHistory();
|
| 1039 |
</script>
|
| 1040 |
</body>
|
| 1041 |
</html>
|