tktejask commited on
Commit
fd03406
Β·
verified Β·
1 Parent(s): 58cb4f6

Update frontend/index.html

Browse files
Files changed (1) hide show
  1. 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
- // Particles
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- // File input
555
  document.getElementById('fileInput').onchange=e=>{
556
  const f=e.target.files[0]; if(f)handleFile(f);
557
  };
558
 
559
- // Drag & drop
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
- // URL
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>