HakimiMasstar commited on
Commit
9cf69f2
·
1 Parent(s): 2d2ae4e

export pdf

Browse files
Files changed (2) hide show
  1. app.py +16 -4
  2. templates/index.html +174 -16
app.py CHANGED
@@ -328,7 +328,14 @@ def dashboard():
328
  if filtered_df.empty: return jsonify({"error": "No data found"})
329
 
330
  pkg = generate_analytics_package(filtered_df)
331
- pkg["preview"] = filtered_df[filtered_df['text'].str.len() < 300].head(5)[['text', 'tpb_label', 'label', 'score']].to_dict(orient='records')
 
 
 
 
 
 
 
332
  return json.dumps(pkg)
333
 
334
  @app.route('/api/analyze_bulk', methods=['POST'])
@@ -336,9 +343,14 @@ def analyze_bulk():
336
  if 'file' not in request.files: return jsonify({"error": "No file"}), 400
337
  file = request.files['file']
338
  try:
339
- df_bulk = pd.read_csv(file)
340
- except:
341
- return jsonify({"error": "Invalid CSV"}), 400
 
 
 
 
 
342
 
343
  text_col = next((c for c in df_bulk.columns if c.lower() in ['text', 'comment', 'review', 'content']), df_bulk.columns[0])
344
 
 
328
  if filtered_df.empty: return jsonify({"error": "No data found"})
329
 
330
  pkg = generate_analytics_package(filtered_df)
331
+
332
+ # Return full results for frontend pagination and export
333
+ pkg["full_results"] = filtered_df[['text', 'tpb_label', 'label', 'score']].to_dict(orient='records')
334
+
335
+ # Add decision to full results
336
+ for r in pkg["full_results"]:
337
+ r['decision'] = predict_decision(r['label'])
338
+
339
  return json.dumps(pkg)
340
 
341
  @app.route('/api/analyze_bulk', methods=['POST'])
 
343
  if 'file' not in request.files: return jsonify({"error": "No file"}), 400
344
  file = request.files['file']
345
  try:
346
+ try:
347
+ df_bulk = pd.read_csv(file)
348
+ except UnicodeDecodeError:
349
+ file.seek(0)
350
+ df_bulk = pd.read_csv(file, encoding='latin1')
351
+ except Exception as e:
352
+ print(f"CSV Load Error: {e}")
353
+ return jsonify({"error": f"Invalid CSV: {str(e)}"}), 400
354
 
355
  text_col = next((c for c in df_bulk.columns if c.lower() in ['text', 'comment', 'review', 'content']), df_bulk.columns[0])
356
 
templates/index.html CHANGED
@@ -6,11 +6,16 @@
6
  <title>HalalNLP Analytics</title>
7
  <!-- Bootstrap 5 CSS -->
8
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
 
 
9
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
10
  <!-- Plotly JS -->
11
  <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
12
  <!-- Marked JS -->
13
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
 
 
 
14
  <style>
15
  body { font-family: 'Inter', sans-serif; background-color: #F9FAFB; }
16
  .navbar { background-color: #ffffff; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
@@ -329,16 +334,20 @@
329
  <h4 class="fw-bold mb-1 text-dark">All Analyzed Inputs</h4>
330
  <p class="text-muted small mb-0">Detailed breakdown of your uploaded CSV content. Click any row to view full details.</p>
331
  </div>
 
 
 
 
332
  </div>
333
 
334
  <div class="table-responsive">
335
  <table class="table table-modern align-middle">
336
  <thead>
337
  <tr>
338
- <th style="width: 55%;">Input Text</th>
339
- <th style="width: 20%;">TPB Factor</th>
340
  <th class="text-center">Sentiment</th>
341
- <th class="text-center">Confidence</th>
342
  </tr>
343
  </thead>
344
  <tbody id="bulkTableBody"></tbody>
@@ -394,21 +403,32 @@
394
  <h4 class="fw-bold mb-1 text-dark">Keyword Analysis Result</h4>
395
  <p class="text-muted small mb-0">Detailed breakdown of posts matching your search criteria.</p>
396
  </div>
 
 
 
 
397
  </div>
398
 
399
  <div class="table-responsive">
400
  <table class="table table-modern align-middle">
401
  <thead>
402
  <tr>
403
- <th style="width: 55%;">Review Text</th>
404
- <th style="width: 20%;">TPB Factor</th>
405
  <th class="text-center">Sentiment</th>
406
- <th class="text-center">Confidence</th>
407
  </tr>
408
  </thead>
409
  <tbody id="dashPreviewBody"></tbody>
410
  </table>
411
  </div>
 
 
 
 
 
 
 
412
  </div>
413
  </div>
414
  </div>
@@ -423,6 +443,44 @@
423
  </div>
424
  </div>
425
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
  <!-- Analytics Template (Hidden) -->
427
  <template id="analyticsTemplate">
428
  <div class="row g-4 mb-4">
@@ -542,10 +600,68 @@
542
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
543
  <script>
544
  const modal = new bootstrap.Modal(document.getElementById('detailModal'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
545
 
546
  // Pagination State
547
  let currentBulkData = [];
 
548
  let currentPage = 1;
 
549
  const itemsPerPage = 10;
550
 
551
  // Word Cloud State
@@ -558,6 +674,46 @@
558
  return '<span class="badge rounded-pill bg-secondary px-3">Neutral</span>';
559
  }
560
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
561
  function toggleWcView(view) {
562
  const tpbContainer = document.getElementById('wc-tpb-container');
563
  const sentContainer = document.getElementById('wc-sent-container');
@@ -606,13 +762,17 @@
606
  const totalPages = Math.ceil(currentBulkData.length / itemsPerPage);
607
 
608
  pageData.forEach((row) => {
 
 
 
 
609
  const tr = document.createElement('tr');
610
  tr.className = `clickable-row row-sent-${row.label}`;
611
  tr.innerHTML = `
612
- <td class="fw-medium text-truncate" style="max-width: 450px;">${row.text}</td>
613
  <td class="text-muted small fw-bold text-uppercase">${row.tpb_label}</td>
614
  <td class="text-center">${getBadge(row.label)}</td>
615
- <td class="text-center font-monospace small">${(row.score * 100).toFixed(2)}%</td>`;
616
  tr.onclick = () => showRowDetail(row);
617
  tbody.appendChild(tr);
618
  });
@@ -782,16 +942,14 @@
782
  return;
783
  }
784
 
 
 
 
785
  renderAnalytics(data, 'dashAnalyticsArea');
786
 
787
- const tbody = document.getElementById('dashPreviewBody');
788
- tbody.innerHTML = data.preview.map(r => `
789
- <tr class="row-sent-${r.label}">
790
- <td style="white-space: pre-wrap;" class="fw-medium">${r.text}</td>
791
- <td class="text-muted small fw-bold text-uppercase">${r.tpb_label}</td>
792
- <td class="text-center">${getBadge(r.label)}</td>
793
- <td class="text-center font-monospace small">${(r.score * 100).toFixed(2)}%</td>
794
- </tr>`).join('');
795
 
796
  // Update title area if needed, though strictly we're just updating the table content here.
797
  // To change the "Data Preview" header, we need to target the HTML structure in the main view, not just this JS.
 
6
  <title>HalalNLP Analytics</title>
7
  <!-- Bootstrap 5 CSS -->
8
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
9
+ <!-- Bootstrap Icons -->
10
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
11
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
12
  <!-- Plotly JS -->
13
  <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
14
  <!-- Marked JS -->
15
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
16
+ <!-- jsPDF & AutoTable -->
17
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
18
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.29/jspdf.plugin.autotable.min.js"></script>
19
  <style>
20
  body { font-family: 'Inter', sans-serif; background-color: #F9FAFB; }
21
  .navbar { background-color: #ffffff; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
 
334
  <h4 class="fw-bold mb-1 text-dark">All Analyzed Inputs</h4>
335
  <p class="text-muted small mb-0">Detailed breakdown of your uploaded CSV content. Click any row to view full details.</p>
336
  </div>
337
+ <button class="btn btn-outline-secondary btn-sm" onclick="openExportModal('bulk')">
338
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-download me-1" viewBox="0 0 16 16"><path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/><path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/></svg>
339
+ Export
340
+ </button>
341
  </div>
342
 
343
  <div class="table-responsive">
344
  <table class="table table-modern align-middle">
345
  <thead>
346
  <tr>
347
+ <th style="width: 45%;">Input Text</th>
348
+ <th style="width: 15%;">TPB Factor</th>
349
  <th class="text-center">Sentiment</th>
350
+ <th class="text-center" style="width: 25%;">Purchase Prediction</th>
351
  </tr>
352
  </thead>
353
  <tbody id="bulkTableBody"></tbody>
 
403
  <h4 class="fw-bold mb-1 text-dark">Keyword Analysis Result</h4>
404
  <p class="text-muted small mb-0">Detailed breakdown of posts matching your search criteria.</p>
405
  </div>
406
+ <button class="btn btn-outline-secondary btn-sm" onclick="openExportModal('dash')">
407
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-download me-1" viewBox="0 0 16 16"><path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/><path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/></svg>
408
+ Export
409
+ </button>
410
  </div>
411
 
412
  <div class="table-responsive">
413
  <table class="table table-modern align-middle">
414
  <thead>
415
  <tr>
416
+ <th style="width: 45%;">Review Text</th>
417
+ <th style="width: 15%;">TPB Factor</th>
418
  <th class="text-center">Sentiment</th>
419
+ <th class="text-center" style="width: 25%;">Purchase Prediction</th>
420
  </tr>
421
  </thead>
422
  <tbody id="dashPreviewBody"></tbody>
423
  </table>
424
  </div>
425
+
426
+ <!-- Dashboard Pagination Controls -->
427
+ <div id="dashPagination" class="d-flex justify-content-between align-items-center mt-4">
428
+ <button class="btn btn-outline-secondary btn-sm" onclick="changeDashPage(-1)" id="btnDashPrev">Previous</button>
429
+ <span class="text-muted small fw-bold" id="pageInfoDash">Page 1 of 1</span>
430
+ <button class="btn btn-outline-secondary btn-sm" onclick="changeDashPage(1)" id="btnDashNext">Next</button>
431
+ </div>
432
  </div>
433
  </div>
434
  </div>
 
443
  </div>
444
  </div>
445
 
446
+ <!-- Export Modal -->
447
+ <div class="modal fade" id="exportModal" tabindex="-1">
448
+ <div class="modal-dialog modal-dialog-centered">
449
+ <div class="modal-content border-0 shadow-lg">
450
+ <div class="modal-header border-0 pb-0">
451
+ <h5 class="modal-title fw-bold">Export PDF Report</h5>
452
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
453
+ </div>
454
+ <div class="modal-body p-4">
455
+ <p class="text-muted small mb-4">Select the sentiment categories you want to include in your generated report.</p>
456
+ <div class="mb-4">
457
+ <label class="form-label fw-bold small text-uppercase text-secondary mb-3">Filter Sentiment Categories</label>
458
+ <div class="d-flex flex-column gap-2">
459
+ <div class="form-check p-2 border rounded-3 bg-light">
460
+ <input class="form-check-input ms-0 me-3" type="checkbox" value="positive" id="expPos" checked>
461
+ <label class="form-check-label fw-medium" for="expPos">Positive Sentiments (🟢)</label>
462
+ </div>
463
+ <div class="form-check p-2 border rounded-3 bg-light">
464
+ <input class="form-check-input ms-0 me-3" type="checkbox" value="neutral" id="expNeu" checked>
465
+ <label class="form-check-label fw-medium" for="expNeu">Neutral Sentiments (⚪)</label>
466
+ </div>
467
+ <div class="form-check p-2 border rounded-3 bg-light">
468
+ <input class="form-check-input ms-0 me-3" type="checkbox" value="negative" id="expNeg" checked>
469
+ <label class="form-check-label fw-medium" for="expNeg">Negative Sentiments (🔴)</label>
470
+ </div>
471
+ </div>
472
+ </div>
473
+ <div class="d-grid pt-2">
474
+ <button onclick="performExport()" class="btn btn-success py-3 fw-bold rounded-3 shadow-sm" style="background-color: #198754; border-color: #198754;">
475
+ <i class="bi bi-file-earmark-pdf-fill me-2"></i>
476
+ Generate PDF Report
477
+ </button>
478
+ </div>
479
+ </div>
480
+ </div>
481
+ </div>
482
+ </div>
483
+
484
  <!-- Analytics Template (Hidden) -->
485
  <template id="analyticsTemplate">
486
  <div class="row g-4 mb-4">
 
600
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
601
  <script>
602
  const modal = new bootstrap.Modal(document.getElementById('detailModal'));
603
+ const exportModal = new bootstrap.Modal(document.getElementById('exportModal'));
604
+ let exportSource = ''; // 'bulk' or 'dash'
605
+
606
+ function openExportModal(source) {
607
+ exportSource = source;
608
+ exportModal.show();
609
+ }
610
+
611
+ function performExport() {
612
+ const includePos = document.getElementById('expPos').checked;
613
+ const includeNeu = document.getElementById('expNeu').checked;
614
+ const includeNeg = document.getElementById('expNeg').checked;
615
+
616
+ const sourceData = exportSource === 'bulk' ? currentBulkData : currentDashData;
617
+ if(!sourceData || sourceData.length === 0) return alert("No data to export.");
618
+
619
+ // Filter Data
620
+ const filtered = sourceData.filter(r => {
621
+ if(r.label === 'positive' && includePos) return true;
622
+ if(r.label === 'neutral' && includeNeu) return true;
623
+ if(r.label === 'negative' && includeNeg) return true;
624
+ return false;
625
+ });
626
+
627
+ if(filtered.length === 0) return alert("No data matches your sentiment selection.");
628
+
629
+ // Generate PDF
630
+ const { jsPDF } = window.jspdf;
631
+ const doc = new jsPDF();
632
+
633
+ doc.setFontSize(18);
634
+ doc.text("HalalNLP Analysis Report", 14, 22);
635
+ doc.setFontSize(11);
636
+ doc.setTextColor(100);
637
+ doc.text(`Source: ${exportSource === 'bulk' ? 'Bulk Analysis' : 'Keyword Dashboard'} | Date: ${new Date().toLocaleDateString()}`, 14, 30);
638
+
639
+ const tableBody = filtered.map(r => [
640
+ r.text,
641
+ r.tpb_label,
642
+ r.label,
643
+ // (r.score * 100).toFixed(2) + '%', // Commented out Confidence
644
+ r.decision
645
+ ]);
646
+
647
+ doc.autoTable({
648
+ head: [['Text', 'TPB Factor', 'Sentiment', /* 'Conf.', */ 'Prediction']],
649
+ body: tableBody,
650
+ startY: 40,
651
+ styles: { fontSize: 8 },
652
+ columnStyles: { 0: { cellWidth: 80 } },
653
+ headStyles: { fillColor: [25, 135, 84] }
654
+ });
655
+
656
+ doc.save(`halal_analysis_${exportSource}_${new Date().getTime()}.pdf`);
657
+ exportModal.hide();
658
+ }
659
 
660
  // Pagination State
661
  let currentBulkData = [];
662
+ let currentDashData = [];
663
  let currentPage = 1;
664
+ let currentDashPage = 1; // New state for dashboard
665
  const itemsPerPage = 10;
666
 
667
  // Word Cloud State
 
674
  return '<span class="badge rounded-pill bg-secondary px-3">Neutral</span>';
675
  }
676
 
677
+ function renderDashTable() {
678
+ const tbody = document.getElementById('dashPreviewBody');
679
+ tbody.innerHTML = "";
680
+
681
+ const start = (currentDashPage - 1) * itemsPerPage;
682
+ const end = start + itemsPerPage;
683
+ const pageData = currentDashData.slice(start, end);
684
+ const totalPages = Math.ceil(currentDashData.length / itemsPerPage);
685
+
686
+ pageData.forEach((r) => {
687
+ let decColor = '#3b82f6';
688
+ if(r.decision.includes("High")) decColor = '#14b8a6';
689
+ if(r.decision.includes("Low")) decColor = '#1e3a8a';
690
+
691
+ const tr = document.createElement('tr');
692
+ tr.className = `clickable-row row-sent-${r.label}`;
693
+ tr.innerHTML = `
694
+ <td class="fw-medium text-truncate" style="max-width: 400px;">${r.text}</td>
695
+ <td class="text-muted small fw-bold text-uppercase">${r.tpb_label}</td>
696
+ <td class="text-center">${getBadge(r.label)}</td>
697
+ <td class="text-center"><span class="badge small" style="background-color: ${decColor}">${r.decision}</span></td>`;
698
+ tr.onclick = () => showRowDetail(r);
699
+ tbody.appendChild(tr);
700
+ });
701
+
702
+ // Update Controls
703
+ document.getElementById('pageInfoDash').innerText = `Page ${currentDashPage} of ${totalPages || 1}`;
704
+ document.getElementById('btnDashPrev').disabled = currentDashPage === 1;
705
+ document.getElementById('btnDashNext').disabled = currentDashPage === totalPages || totalPages === 0;
706
+ }
707
+
708
+ function changeDashPage(delta) {
709
+ const totalPages = Math.ceil(currentDashData.length / itemsPerPage);
710
+ const newPage = currentDashPage + delta;
711
+ if (newPage >= 1 && newPage <= totalPages) {
712
+ currentDashPage = newPage;
713
+ renderDashTable();
714
+ }
715
+ }
716
+
717
  function toggleWcView(view) {
718
  const tpbContainer = document.getElementById('wc-tpb-container');
719
  const sentContainer = document.getElementById('wc-sent-container');
 
762
  const totalPages = Math.ceil(currentBulkData.length / itemsPerPage);
763
 
764
  pageData.forEach((row) => {
765
+ let decColor = '#3b82f6'; // Blue
766
+ if(row.decision.includes("High")) decColor = '#14b8a6'; // Teal
767
+ if(row.decision.includes("Low")) decColor = '#1e3a8a'; // Navy
768
+
769
  const tr = document.createElement('tr');
770
  tr.className = `clickable-row row-sent-${row.label}`;
771
  tr.innerHTML = `
772
+ <td class="fw-medium text-truncate" style="max-width: 400px;">${row.text}</td>
773
  <td class="text-muted small fw-bold text-uppercase">${row.tpb_label}</td>
774
  <td class="text-center">${getBadge(row.label)}</td>
775
+ <td class="text-center"><span class="badge small" style="background-color: ${decColor}">${row.decision}</span></td>`;
776
  tr.onclick = () => showRowDetail(row);
777
  tbody.appendChild(tr);
778
  });
 
942
  return;
943
  }
944
 
945
+ // Save for Export (Handle both full_results if available, or preview)
946
+ currentDashData = data.full_results || data.preview;
947
+
948
  renderAnalytics(data, 'dashAnalyticsArea');
949
 
950
+ // Initialize Pagination
951
+ currentDashPage = 1;
952
+ renderDashTable();
 
 
 
 
 
953
 
954
  // Update title area if needed, though strictly we're just updating the table content here.
955
  // To change the "Data Preview" header, we need to target the HTML structure in the main view, not just this JS.