quanliang commited on
Commit
a884cdf
·
1 Parent(s): 53b4b7e

UI changed; Added Admin Secrete for storage management for HF space

Browse files
Files changed (2) hide show
  1. index.html +889 -42
  2. main.py +126 -0
index.html CHANGED
@@ -638,6 +638,650 @@
638
  .reference-item:last-child {
639
  margin-bottom: 0;
640
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
641
  </style>
642
  </head>
643
  <body>
@@ -931,6 +1575,33 @@
931
 
932
  </div>
933
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
934
  <!-- JavaScript -->
935
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
936
  <script>
@@ -1505,18 +2176,18 @@
1505
 
1506
  // Build collapsible resources box
1507
  const resourcesBox = (prettySources) ? `
1508
- <details style="margin-top: 10px;">
1509
- <summary style="cursor: pointer; font-weight: 600; color: #4285F4;">Show sources and context</summary>
1510
- <div style="margin-top: 10px; padding: 12px; background: #f1f3f4; border-radius: 6px; border-left: 4px solid #90CAF9; white-space: pre-wrap;">
1511
- ${renderedSources}
1512
  </div>
1513
  </details>
1514
  ` : '';
1515
 
1516
  resultsDiv.innerHTML = `
1517
  <strong>✓ Q&A Complete!</strong><br>
1518
- <div style="margin-top: 15px; padding: 15px; background: #f8f9fa; border-radius: 6px; border-left: 4px solid #4285F4;">
1519
- ${renderedAnswer}
1520
  </div>
1521
  ${resourcesBox}
1522
  `;
@@ -2068,7 +2739,12 @@
2068
  aiDiv.classList.add('chat-bubble', 'ai-bubble');
2069
 
2070
  // For regular results
2071
- aiDiv.innerHTML = formatMarkdownResponse(data.response ?? data.answer ?? '');
 
 
 
 
 
2072
 
2073
  chatArea.appendChild(aiDiv);
2074
 
@@ -2704,17 +3380,101 @@
2704
  }
2705
  }
2706
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2707
  // Function to format markdown responses using marked.js library
2708
  function formatMarkdownResponse(text) {
2709
  // Type guard - ensure text is a string
2710
  if (typeof text !== 'string') {
2711
  text = String(text ?? '');
2712
  }
 
 
2713
 
2714
  // Guard against marked.js CDN being unavailable
2715
  if (typeof marked === 'undefined') {
2716
  console.warn('⚠️ Marked.js not loaded – falling back to plain text');
2717
- return text.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
2718
  }
2719
 
2720
  // Configure marked for scientific content
@@ -2731,88 +3491,173 @@
2731
 
2732
  // Custom table rendering
2733
  renderer.table = function(header, body) {
2734
- return `<table style="border-collapse: collapse; margin: 15px 0; width: 100%; font-size: 0.9rem; background: white; border-radius: 4px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
2735
- <thead style="background: #4285F4; color: white;">${header}</thead>
2736
- <tbody>${body}</tbody>
2737
- </table>`;
2738
  };
2739
 
2740
  // Custom table header rendering
2741
  renderer.tablecell = function(content, flags) {
2742
  const type = flags.header ? 'th' : 'td';
2743
- const style = flags.header
2744
- ? 'padding: 12px 8px; border: 1px solid #ddd; text-align: left; font-weight: 500;'
2745
- : 'padding: 10px 8px; border: 1px solid #ddd;';
2746
- return `<${type} style="${style}">${content}</${type}>`;
2747
  };
2748
 
2749
  // Custom header rendering
2750
  renderer.heading = function(text, level) {
2751
- const sizes = {
2752
- 1: '1.4rem',
2753
- 2: '1.3rem',
2754
- 3: '1.2rem',
2755
- 4: '1.1rem',
2756
- 5: '1.05rem',
2757
- 6: '1rem'
2758
- };
2759
- const margins = {
2760
- 1: '25px 0 15px 0',
2761
- 2: '20px 0 10px 0',
2762
- 3: '20px 0 10px 0',
2763
- 4: '15px 0 10px 0',
2764
- 5: '15px 0 8px 0',
2765
- 6: '15px 0 8px 0'
2766
- };
2767
- return `<h${level} style="margin: ${margins[level]}; color: #333; font-size: ${sizes[level]}; font-weight: 600;">${text}</h${level}>`;
2768
  };
2769
 
2770
  // Custom paragraph rendering
2771
  renderer.paragraph = function(text) {
2772
- return `<p style="margin: 8px 0; line-height: 1.5;">${text}</p>`;
2773
  };
2774
 
2775
  // Custom horizontal rule
2776
  renderer.hr = function() {
2777
- return '<hr style="margin: 15px 0; border: none; border-top: 2px solid #ddd; border-radius: 1px;">';
2778
  };
2779
 
2780
  // Custom blockquote
2781
  renderer.blockquote = function(quote) {
2782
- return `<blockquote style="margin: 10px 0; padding: 10px 15px; border-left: 4px solid #4285F4; background: #f8f9fa; font-style: italic; color: #555;">${quote}</blockquote>`;
2783
  };
2784
 
2785
  // Custom code
2786
  renderer.code = function(code, language) {
2787
- return `<pre style="background: #f1f3f4; padding: 12px; border-radius: 4px; margin: 10px 0; overflow-x: auto; font-family: monospace; font-size: 0.9em; color: #333;"><code>${code}</code></pre>`;
2788
  };
2789
 
2790
  // Custom inline code
2791
  renderer.codespan = function(code) {
2792
- return `<code style="background: #f1f3f4; padding: 2px 4px; border-radius: 3px; font-family: monospace; font-size: 0.9em; color: #d73a49;">${code}</code>`;
2793
  };
2794
 
2795
  // Custom list rendering
2796
  renderer.list = function(body, ordered) {
2797
  const type = ordered ? 'ol' : 'ul';
2798
- return `<${type} style="margin: 10px 0; padding-left: 20px; line-height: 1.5;">${body}</${type}>`;
2799
  };
2800
 
2801
  renderer.listitem = function(text) {
2802
- return `<li style="margin: 5px 0;">${text}</li>`;
2803
  };
2804
 
2805
  // Parse markdown with custom renderer
2806
  try {
2807
  const html = marked(text, { renderer: renderer });
 
 
 
 
2808
  return html;
2809
  } catch (error) {
2810
  console.error('Error parsing markdown:', error);
2811
- // Fallback to basic HTML escaping
2812
- return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
2813
  }
2814
  }
2815
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2816
  // Event listeners for hypothesis conversation
2817
  document.getElementById('start-hypothesis-conversation-btn').addEventListener('click', startHypothesisConversation);
2818
  document.getElementById('send-hypothesis-message-btn').addEventListener('click', sendHypothesisMessage);
@@ -2823,6 +3668,8 @@
2823
  sendHypothesisMessage();
2824
  }
2825
  });
 
 
2826
  </script>
2827
  </body>
2828
  </html>
 
638
  .reference-item:last-child {
639
  margin-bottom: 0;
640
  }
641
+
642
+ /* Modern UI refresh */
643
+ :root {
644
+ --bg: #f3f6ff;
645
+ --surface: #ffffff;
646
+ --surface-soft: #f8faff;
647
+ --text: #1f2937;
648
+ --muted: #5b6472;
649
+ --border: #dbe3f0;
650
+ --primary: #2563eb;
651
+ --primary-dark: #1d4ed8;
652
+ --primary-soft: #e7efff;
653
+ --secondary-soft: #e9f7ff;
654
+ --success: #1f9d66;
655
+ --bg-glow-primary: rgba(37, 99, 235, 0.12);
656
+ --bg-glow-secondary: rgba(56, 189, 248, 0.1);
657
+ --accent-shadow: rgba(37, 99, 235, 0.22);
658
+ --focus-ring: rgba(37, 99, 235, 0.18);
659
+ --chat-user-top: #e5f7ec;
660
+ --chat-user-bottom: #d5f0df;
661
+ --chat-user-border: #bee6cb;
662
+ --chat-ai-top: #ebf2ff;
663
+ --chat-ai-bottom: #dbe8ff;
664
+ --chat-ai-border: #c5d8ff;
665
+ --progress-gradient: linear-gradient(90deg, #4285F4, #34a853);
666
+ --scrollbar-thumb: #c3d2ef;
667
+ --scrollbar-track: #edf2fd;
668
+ --shadow-sm: 0 2px 8px rgba(24, 39, 75, 0.08);
669
+ --shadow-md: 0 12px 30px rgba(24, 39, 75, 0.1);
670
+ --radius-sm: 8px;
671
+ --radius-md: 14px;
672
+ --radius-lg: 20px;
673
+ }
674
+
675
+ html[data-mode="QA"] {
676
+ --bg: #f1f7ff;
677
+ --primary: #2563eb;
678
+ --primary-dark: #1d4ed8;
679
+ --primary-soft: #e7efff;
680
+ --secondary-soft: #e7f7ff;
681
+ --border: #d5e2fb;
682
+ --bg-glow-primary: rgba(37, 99, 235, 0.14);
683
+ --bg-glow-secondary: rgba(14, 165, 233, 0.1);
684
+ --accent-shadow: rgba(37, 99, 235, 0.24);
685
+ --focus-ring: rgba(37, 99, 235, 0.2);
686
+ --chat-ai-top: #ebf2ff;
687
+ --chat-ai-bottom: #dbe8ff;
688
+ --chat-ai-border: #c5d8ff;
689
+ --progress-gradient: linear-gradient(90deg, #2563eb, #0ea5e9);
690
+ --scrollbar-thumb: #bfd2f7;
691
+ --scrollbar-track: #eaf1ff;
692
+ }
693
+
694
+ html[data-mode="System Chart Extraction"] {
695
+ --bg: #f2fbf8;
696
+ --primary: #0f766e;
697
+ --primary-dark: #0f5f58;
698
+ --primary-soft: #dbf6ee;
699
+ --secondary-soft: #e9fbf3;
700
+ --border: #cce8df;
701
+ --bg-glow-primary: rgba(15, 118, 110, 0.15);
702
+ --bg-glow-secondary: rgba(16, 185, 129, 0.1);
703
+ --accent-shadow: rgba(15, 118, 110, 0.25);
704
+ --focus-ring: rgba(15, 118, 110, 0.2);
705
+ --chat-user-top: #e6f8ee;
706
+ --chat-user-bottom: #d6f0e3;
707
+ --chat-user-border: #bce2cd;
708
+ --chat-ai-top: #e3faf6;
709
+ --chat-ai-bottom: #d2f2ea;
710
+ --chat-ai-border: #b9e5da;
711
+ --progress-gradient: linear-gradient(90deg, #0f766e, #10b981);
712
+ --scrollbar-thumb: #b7ddd3;
713
+ --scrollbar-track: #e8f7f3;
714
+ }
715
+
716
+ html[data-mode="Hypotheses Generation"] {
717
+ --bg: #f7f3ff;
718
+ --primary: #7c3aed;
719
+ --primary-dark: #6d28d9;
720
+ --primary-soft: #efe6ff;
721
+ --secondary-soft: #f6ebff;
722
+ --border: #e2d5fb;
723
+ --bg-glow-primary: rgba(124, 58, 237, 0.15);
724
+ --bg-glow-secondary: rgba(236, 72, 153, 0.08);
725
+ --accent-shadow: rgba(124, 58, 237, 0.26);
726
+ --focus-ring: rgba(124, 58, 237, 0.2);
727
+ --chat-user-top: #f0e8ff;
728
+ --chat-user-bottom: #e4d7ff;
729
+ --chat-user-border: #d4c0f7;
730
+ --chat-ai-top: #f8edff;
731
+ --chat-ai-bottom: #efddff;
732
+ --chat-ai-border: #dfc7f8;
733
+ --progress-gradient: linear-gradient(90deg, #7c3aed, #ec4899);
734
+ --scrollbar-thumb: #d6c3f2;
735
+ --scrollbar-track: #f0e8ff;
736
+ }
737
+
738
+ body {
739
+ background:
740
+ radial-gradient(circle at top right, var(--bg-glow-primary), transparent 40%),
741
+ radial-gradient(circle at top left, var(--bg-glow-secondary), transparent 42%),
742
+ var(--bg);
743
+ color: var(--text);
744
+ }
745
+
746
+ #app,
747
+ .mode-selection {
748
+ border: 1px solid rgba(255, 255, 255, 0.7);
749
+ border-radius: var(--radius-lg);
750
+ box-shadow: var(--shadow-md);
751
+ backdrop-filter: blur(2px);
752
+ }
753
+
754
+ #app {
755
+ max-width: 980px;
756
+ padding: 2rem 2rem 1.75rem;
757
+ background: linear-gradient(180deg, #ffffff 0%, color-mix(in srgb, var(--primary-soft) 18%, #ffffff 82%) 100%);
758
+ }
759
+
760
+ .app-header h1 {
761
+ color: #0f172a;
762
+ letter-spacing: -0.02em;
763
+ }
764
+
765
+ .app-header p {
766
+ color: var(--muted);
767
+ font-size: 0.95rem;
768
+ }
769
+
770
+ .mode-selection {
771
+ background: linear-gradient(180deg, #ffffff 0%, #f7faff 100%);
772
+ border: 1px solid var(--border);
773
+ }
774
+
775
+ .mode-selection h2 {
776
+ color: #0f172a;
777
+ letter-spacing: -0.02em;
778
+ }
779
+
780
+ .mode-selection p {
781
+ color: var(--muted);
782
+ }
783
+
784
+ .mode-card {
785
+ border: 1px solid var(--border);
786
+ background: #ffffff;
787
+ border-radius: var(--radius-md);
788
+ box-shadow: var(--shadow-sm);
789
+ position: relative;
790
+ overflow: hidden;
791
+ }
792
+
793
+ .mode-card:hover {
794
+ border-color: color-mix(in srgb, var(--primary) 45%, #ffffff 55%);
795
+ box-shadow: 0 10px 24px var(--accent-shadow);
796
+ }
797
+
798
+ /* Landing page has a distinct neutral background */
799
+ html[data-mode=""] body {
800
+ background:
801
+ radial-gradient(circle at 0% 0%, rgba(251, 146, 60, 0.14), transparent 35%),
802
+ radial-gradient(circle at 100% 0%, rgba(59, 130, 246, 0.12), transparent 38%),
803
+ linear-gradient(180deg, #f6f2eb 0%, #eceff7 100%);
804
+ }
805
+
806
+ html[data-mode=""] .mode-selection {
807
+ background: linear-gradient(180deg, #ffffff 0%, #f7f8fc 100%);
808
+ border-color: #d7ddeb;
809
+ }
810
+
811
+ html[data-mode=""] .mode-card::before {
812
+ content: '';
813
+ position: absolute;
814
+ left: 0;
815
+ top: 0;
816
+ width: 100%;
817
+ height: 4px;
818
+ border-top-left-radius: var(--radius-md);
819
+ border-top-right-radius: var(--radius-md);
820
+ }
821
+
822
+ html[data-mode=""] .mode-card[data-mode="QA"] {
823
+ border-color: #c8dafc;
824
+ background: linear-gradient(180deg, #f4f8ff 0%, #eaf1ff 100%);
825
+ }
826
+
827
+ html[data-mode=""] .mode-card[data-mode="QA"]::before {
828
+ background: linear-gradient(90deg, #2563eb, #0ea5e9);
829
+ }
830
+
831
+ html[data-mode=""] .mode-card[data-mode="System Chart Extraction"] {
832
+ border-color: #bedfd4;
833
+ background: linear-gradient(180deg, #edf9f5 0%, #e4f5ef 100%);
834
+ }
835
+
836
+ html[data-mode=""] .mode-card[data-mode="System Chart Extraction"]::before {
837
+ background: linear-gradient(90deg, #0f766e, #10b981);
838
+ }
839
+
840
+ html[data-mode=""] .mode-card[data-mode="Hypotheses Generation"] {
841
+ border-color: #d9caf6;
842
+ background: linear-gradient(180deg, #f5f0ff 0%, #efe7ff 100%);
843
+ }
844
+
845
+ html[data-mode=""] .mode-card[data-mode="Hypotheses Generation"]::before {
846
+ background: linear-gradient(90deg, #7c3aed, #ec4899);
847
+ }
848
+
849
+ html[data-mode=""] .mode-card[data-mode="QA"]:hover {
850
+ box-shadow: 0 12px 24px rgba(37, 99, 235, 0.24);
851
+ }
852
+
853
+ html[data-mode=""] .mode-card[data-mode="System Chart Extraction"]:hover {
854
+ box-shadow: 0 12px 24px rgba(15, 118, 110, 0.24);
855
+ }
856
+
857
+ html[data-mode=""] .mode-card[data-mode="Hypotheses Generation"]:hover {
858
+ box-shadow: 0 12px 24px rgba(124, 58, 237, 0.24);
859
+ }
860
+
861
+ .section-title {
862
+ margin-top: 1.8rem;
863
+ margin-bottom: 0.8rem;
864
+ color: #0f172a;
865
+ font-size: 1.02rem;
866
+ letter-spacing: 0.01em;
867
+ }
868
+
869
+ #apiKeyForm,
870
+ #uploadForm,
871
+ #askForm,
872
+ #conversationForm,
873
+ #hypotheses-conversation-section,
874
+ #conversation-section,
875
+ .references-section {
876
+ border: 1px solid var(--border);
877
+ border-radius: var(--radius-md);
878
+ background: var(--surface);
879
+ box-shadow: var(--shadow-sm);
880
+ }
881
+
882
+ #apiKeyForm,
883
+ #uploadForm,
884
+ #askForm,
885
+ #conversationForm {
886
+ padding: 1rem 1rem 0.9rem;
887
+ }
888
+
889
+ #hypotheses-conversation-section,
890
+ #conversation-section {
891
+ margin-top: 1rem;
892
+ padding: 1rem;
893
+ background: linear-gradient(180deg, #ffffff 0%, color-mix(in srgb, var(--secondary-soft) 40%, #ffffff 60%) 100%);
894
+ }
895
+
896
+ .references-section {
897
+ background: color-mix(in srgb, var(--primary-soft) 35%, #ffffff 65%);
898
+ border-color: color-mix(in srgb, var(--primary) 20%, #ffffff 80%);
899
+ }
900
+
901
+ form label {
902
+ color: #1f2a3d;
903
+ font-weight: 500;
904
+ gap: 0.2rem;
905
+ }
906
+
907
+ form input[type="text"],
908
+ form select,
909
+ form input[type="file"],
910
+ form input[type="password"],
911
+ form input[type="number"],
912
+ textarea,
913
+ #followUpQuestion,
914
+ #hypothesis-chat-input,
915
+ #hypothesis-text-input,
916
+ #original-paper-content-input {
917
+ margin-top: 0.3rem;
918
+ border: 1px solid #c8d6ec !important;
919
+ border-radius: 10px !important;
920
+ background-color: #ffffff !important;
921
+ color: var(--text);
922
+ box-shadow: inset 0 1px 1px rgba(15, 23, 42, 0.04);
923
+ transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease;
924
+ }
925
+
926
+ form input[type="text"]:focus,
927
+ form select:focus,
928
+ form input[type="file"]:focus,
929
+ form input[type="password"]:focus,
930
+ form input[type="number"]:focus,
931
+ textarea:focus,
932
+ #followUpQuestion:focus,
933
+ #hypothesis-chat-input:focus,
934
+ #hypothesis-text-input:focus,
935
+ #original-paper-content-input:focus {
936
+ border-color: color-mix(in srgb, var(--primary) 55%, #ffffff 45%) !important;
937
+ box-shadow: 0 0 0 3px var(--focus-ring) !important;
938
+ outline: none;
939
+ }
940
+
941
+ form button,
942
+ #start-hypothesis-conversation-btn,
943
+ #send-hypothesis-message-btn,
944
+ #loadConversationBtn {
945
+ border-radius: 10px !important;
946
+ border: 1px solid transparent !important;
947
+ background: linear-gradient(180deg, #2f76ff 0%, var(--primary) 100%) !important;
948
+ color: #ffffff !important;
949
+ font-weight: 600;
950
+ letter-spacing: 0.01em;
951
+ box-shadow: 0 8px 16px var(--accent-shadow);
952
+ }
953
+
954
+ form button:hover,
955
+ #start-hypothesis-conversation-btn:hover,
956
+ #send-hypothesis-message-btn:hover,
957
+ #loadConversationBtn:hover {
958
+ background: linear-gradient(180deg, #3f83ff 0%, var(--primary-dark) 100%) !important;
959
+ box-shadow: 0 12px 22px var(--accent-shadow);
960
+ }
961
+
962
+ .info-box {
963
+ background: linear-gradient(180deg, var(--secondary-soft) 0%, var(--primary-soft) 100%);
964
+ border-left: 4px solid var(--primary);
965
+ border-radius: var(--radius-sm);
966
+ }
967
+
968
+ #advancedParameters {
969
+ border: 1px solid var(--border);
970
+ border-radius: 10px;
971
+ background: var(--surface-soft);
972
+ }
973
+
974
+ #advancedParameters summary {
975
+ color: #2a3550;
976
+ font-weight: 500;
977
+ }
978
+
979
+ #chat-area,
980
+ #hypothesis-chat-messages,
981
+ #conversation-messages {
982
+ border: 1px solid var(--border) !important;
983
+ border-radius: var(--radius-md) !important;
984
+ background: linear-gradient(180deg, #fbfcff 0%, color-mix(in srgb, var(--primary-soft) 35%, #ffffff 65%) 100%) !important;
985
+ box-shadow: inset 0 1px 2px rgba(15, 23, 42, 0.04);
986
+ }
987
+
988
+ .chat-bubble {
989
+ border-radius: 14px;
990
+ box-shadow: 0 2px 10px rgba(15, 23, 42, 0.07);
991
+ }
992
+
993
+ .user-bubble {
994
+ background: linear-gradient(180deg, var(--chat-user-top) 0%, var(--chat-user-bottom) 100%);
995
+ border: 1px solid var(--chat-user-border);
996
+ }
997
+
998
+ .ai-bubble {
999
+ background: linear-gradient(180deg, var(--chat-ai-top) 0%, var(--chat-ai-bottom) 100%);
1000
+ border: 1px solid var(--chat-ai-border);
1001
+ }
1002
+
1003
+ .model-selection-banner,
1004
+ #visualizationContainer,
1005
+ .progress-container {
1006
+ border-radius: var(--radius-md);
1007
+ border-color: var(--border);
1008
+ background: linear-gradient(180deg, #ffffff 0%, #f9fbff 100%);
1009
+ }
1010
+
1011
+ .progress-bar {
1012
+ border-radius: 99px;
1013
+ background: #e8eefb;
1014
+ }
1015
+
1016
+ .progress-bar-fill {
1017
+ border-radius: 99px;
1018
+ background: var(--progress-gradient);
1019
+ }
1020
+
1021
+ /* QA response typography */
1022
+ .qa-result-card {
1023
+ margin-top: 12px;
1024
+ padding: 14px 16px;
1025
+ background: linear-gradient(180deg, #ffffff 0%, color-mix(in srgb, var(--primary-soft) 24%, #ffffff 76%) 100%);
1026
+ border: 1px solid color-mix(in srgb, var(--primary) 22%, #ffffff 78%);
1027
+ border-left: 4px solid var(--primary);
1028
+ border-radius: 12px;
1029
+ }
1030
+
1031
+ .qa-response {
1032
+ color: #1f2937;
1033
+ line-height: 1.65;
1034
+ font-size: 0.96rem;
1035
+ }
1036
+
1037
+ .qa-response .qa-h {
1038
+ margin: 0.95rem 0 0.5rem;
1039
+ color: #0f172a;
1040
+ line-height: 1.3;
1041
+ font-weight: 700;
1042
+ letter-spacing: -0.01em;
1043
+ }
1044
+
1045
+ .qa-response .qa-h1 { font-size: 1.25rem; }
1046
+ .qa-response .qa-h2 { font-size: 1.15rem; }
1047
+ .qa-response .qa-h3 { font-size: 1.06rem; }
1048
+ .qa-response .qa-h4,
1049
+ .qa-response .qa-h5,
1050
+ .qa-response .qa-h6 { font-size: 1rem; }
1051
+
1052
+ .qa-response .qa-p {
1053
+ margin: 0.45rem 0;
1054
+ color: #1f2937;
1055
+ }
1056
+
1057
+ .qa-response .qa-list {
1058
+ margin: 0.55rem 0;
1059
+ padding-left: 1.25rem;
1060
+ }
1061
+
1062
+ .qa-response .qa-li {
1063
+ margin: 0.3rem 0;
1064
+ }
1065
+
1066
+ .qa-response .qa-hr {
1067
+ margin: 0.8rem 0;
1068
+ border: none;
1069
+ border-top: 1px solid #dbe3f0;
1070
+ }
1071
+
1072
+ .qa-response .qa-blockquote {
1073
+ margin: 0.7rem 0;
1074
+ padding: 0.7rem 0.9rem;
1075
+ border-left: 4px solid var(--primary);
1076
+ border-radius: 0 8px 8px 0;
1077
+ background: color-mix(in srgb, var(--primary-soft) 42%, #ffffff 58%);
1078
+ color: #334155;
1079
+ }
1080
+
1081
+ .qa-response .qa-pre {
1082
+ margin: 0.65rem 0;
1083
+ padding: 0.75rem 0.85rem;
1084
+ background: #0f172a;
1085
+ color: #e2e8f0;
1086
+ border-radius: 10px;
1087
+ border: 1px solid #1e293b;
1088
+ overflow-x: auto;
1089
+ font-size: 0.88rem;
1090
+ line-height: 1.55;
1091
+ }
1092
+
1093
+ .qa-response .qa-code-inline {
1094
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
1095
+ background: color-mix(in srgb, var(--primary-soft) 55%, #ffffff 45%);
1096
+ color: #1f3a8a;
1097
+ border: 1px solid color-mix(in srgb, var(--primary) 20%, #ffffff 80%);
1098
+ border-radius: 5px;
1099
+ padding: 0.08rem 0.3rem;
1100
+ font-size: 0.88em;
1101
+ }
1102
+
1103
+ .qa-response .qa-table-wrap {
1104
+ margin: 0.75rem 0;
1105
+ overflow-x: auto;
1106
+ border: 1px solid #dbe3f0;
1107
+ border-radius: 10px;
1108
+ background: #ffffff;
1109
+ }
1110
+
1111
+ .qa-response .qa-table {
1112
+ width: 100%;
1113
+ border-collapse: collapse;
1114
+ font-size: 0.9rem;
1115
+ min-width: 520px;
1116
+ }
1117
+
1118
+ .qa-response .qa-table th,
1119
+ .qa-response .qa-table td {
1120
+ border-bottom: 1px solid #e4ebf7;
1121
+ padding: 0.55rem 0.65rem;
1122
+ text-align: left;
1123
+ vertical-align: top;
1124
+ }
1125
+
1126
+ .qa-response .qa-table th {
1127
+ background: color-mix(in srgb, var(--primary-soft) 55%, #ffffff 45%);
1128
+ color: #0f172a;
1129
+ font-weight: 700;
1130
+ border-bottom: 1px solid #cfdcf4;
1131
+ }
1132
+
1133
+ .qa-response .qa-table tr:last-child td {
1134
+ border-bottom: none;
1135
+ }
1136
+
1137
+ .qa-sources-toggle {
1138
+ margin-top: 10px;
1139
+ }
1140
+
1141
+ .qa-sources-toggle summary {
1142
+ cursor: pointer;
1143
+ font-weight: 600;
1144
+ color: var(--primary);
1145
+ }
1146
+
1147
+ .qa-sources-body {
1148
+ margin-top: 10px;
1149
+ padding: 12px;
1150
+ border-radius: 10px;
1151
+ background: color-mix(in srgb, var(--primary-soft) 35%, #ffffff 65%);
1152
+ border: 1px solid color-mix(in srgb, var(--primary) 20%, #ffffff 80%);
1153
+ }
1154
+
1155
+ ::-webkit-scrollbar {
1156
+ width: 9px;
1157
+ height: 9px;
1158
+ }
1159
+
1160
+ ::-webkit-scrollbar-thumb {
1161
+ background: var(--scrollbar-thumb);
1162
+ border-radius: 999px;
1163
+ border: 2px solid var(--scrollbar-track);
1164
+ }
1165
+
1166
+ ::-webkit-scrollbar-track {
1167
+ background: var(--scrollbar-track);
1168
+ border-radius: 999px;
1169
+ }
1170
+
1171
+ /* Admin controls */
1172
+ #adminFab {
1173
+ position: fixed;
1174
+ right: 20px;
1175
+ bottom: 20px;
1176
+ z-index: 1100;
1177
+ border: 1px solid #d0d0d0;
1178
+ background: #fff;
1179
+ color: #333;
1180
+ border-radius: 999px;
1181
+ padding: 8px 14px;
1182
+ font-size: 0.85rem;
1183
+ cursor: pointer;
1184
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
1185
+ }
1186
+
1187
+ #adminFab:hover {
1188
+ background: #f7f7f7;
1189
+ }
1190
+
1191
+ #adminPanel {
1192
+ position: fixed;
1193
+ right: 20px;
1194
+ bottom: 64px;
1195
+ z-index: 1100;
1196
+ width: min(360px, calc(100vw - 2rem));
1197
+ background: #fff;
1198
+ border: 1px solid #e2e2e2;
1199
+ border-radius: 10px;
1200
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
1201
+ padding: 0.85rem;
1202
+ display: none;
1203
+ }
1204
+
1205
+ #adminPanel h4 {
1206
+ margin: 0 0 0.45rem 0;
1207
+ font-size: 0.95rem;
1208
+ color: #333;
1209
+ }
1210
+
1211
+ #adminPanel .admin-row {
1212
+ margin-bottom: 0.55rem;
1213
+ }
1214
+
1215
+ #adminPanel label {
1216
+ display: flex;
1217
+ flex-direction: column;
1218
+ font-size: 0.83rem;
1219
+ color: #444;
1220
+ gap: 0.2rem;
1221
+ }
1222
+
1223
+ #adminPanel input[type="password"],
1224
+ #adminPanel input[type="text"] {
1225
+ width: 100%;
1226
+ padding: 0.45rem;
1227
+ border: 1px solid #ccc;
1228
+ border-radius: 4px;
1229
+ background: #fafafa;
1230
+ }
1231
+
1232
+ #adminPanel .admin-actions {
1233
+ display: flex;
1234
+ gap: 0.5rem;
1235
+ margin-top: 0.45rem;
1236
+ }
1237
+
1238
+ #adminPanel button {
1239
+ border: 1px solid #cfcfcf;
1240
+ border-radius: 5px;
1241
+ background: #f9f9f9;
1242
+ color: #333;
1243
+ cursor: pointer;
1244
+ padding: 0.4rem 0.65rem;
1245
+ font-size: 0.82rem;
1246
+ }
1247
+
1248
+ #adminPanel button:hover {
1249
+ background: #f2f2f2;
1250
+ }
1251
+
1252
+ #adminPanel .danger-btn {
1253
+ border-color: #f44336;
1254
+ color: #f44336;
1255
+ background: #fff5f5;
1256
+ }
1257
+
1258
+ #adminPanel .danger-btn:hover {
1259
+ background: #ffe9e9;
1260
+ }
1261
+
1262
+ #adminPanelStatus {
1263
+ margin-top: 0.45rem;
1264
+ font-size: 0.8rem;
1265
+ line-height: 1.35;
1266
+ color: #555;
1267
+ min-height: 1.1rem;
1268
+ }
1269
+
1270
+ @media (max-width: 900px) {
1271
+ body {
1272
+ padding: 1rem;
1273
+ }
1274
+
1275
+ #app,
1276
+ .mode-selection {
1277
+ padding: 1.25rem;
1278
+ border-radius: 16px;
1279
+ }
1280
+
1281
+ .mode-cards {
1282
+ grid-template-columns: 1fr;
1283
+ }
1284
+ }
1285
  </style>
1286
  </head>
1287
  <body>
 
1575
 
1576
  </div>
1577
 
1578
+ <button id="adminFab" type="button" title="Owner admin controls">Admin</button>
1579
+ <div id="adminPanel" role="dialog" aria-modal="false" aria-label="Admin panel">
1580
+ <h4>Owner Admin</h4>
1581
+ <div class="admin-row">
1582
+ <label>
1583
+ Admin Password
1584
+ <input type="password" id="adminPasswordInput" placeholder="Enter owner password">
1585
+ </label>
1586
+ </div>
1587
+ <div class="admin-actions">
1588
+ <button type="button" id="verifyAdminBtn">Verify</button>
1589
+ </div>
1590
+
1591
+ <div id="adminDangerZone" style="display: none; margin-top: 0.7rem; border-top: 1px dashed #ddd; padding-top: 0.65rem;">
1592
+ <div class="admin-row">
1593
+ <label>
1594
+ Type confirmation phrase
1595
+ <input type="text" id="adminDeleteConfirmInput" placeholder="DELETE /data">
1596
+ </label>
1597
+ </div>
1598
+ <div class="admin-actions">
1599
+ <button type="button" id="wipeDataBtn" class="danger-btn">Delete all /data contents</button>
1600
+ </div>
1601
+ </div>
1602
+ <div id="adminPanelStatus"></div>
1603
+ </div>
1604
+
1605
  <!-- JavaScript -->
1606
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
1607
  <script>
 
2176
 
2177
  // Build collapsible resources box
2178
  const resourcesBox = (prettySources) ? `
2179
+ <details class="qa-sources-toggle">
2180
+ <summary>Show sources and context</summary>
2181
+ <div class="qa-sources-body">
2182
+ <div class="qa-response">${renderedSources}</div>
2183
  </div>
2184
  </details>
2185
  ` : '';
2186
 
2187
  resultsDiv.innerHTML = `
2188
  <strong>✓ Q&A Complete!</strong><br>
2189
+ <div class="qa-result-card">
2190
+ <div class="qa-response">${renderedAnswer}</div>
2191
  </div>
2192
  ${resourcesBox}
2193
  `;
 
2739
  aiDiv.classList.add('chat-bubble', 'ai-bubble');
2740
 
2741
  // For regular results
2742
+ const rendered = formatMarkdownResponse(data.response ?? data.answer ?? '');
2743
+ if (window.selectedMode === 'QA') {
2744
+ aiDiv.innerHTML = `<div class="qa-response">${rendered}</div>`;
2745
+ } else {
2746
+ aiDiv.innerHTML = rendered;
2747
+ }
2748
 
2749
  chatArea.appendChild(aiDiv);
2750
 
 
3380
  }
3381
  }
3382
 
3383
+ function normalizeMarkdownText(text) {
3384
+ let normalized = String(text ?? '');
3385
+
3386
+ // Normalize line endings and trim excessive outer whitespace
3387
+ normalized = normalized.replace(/\r\n/g, '\n').trim();
3388
+
3389
+ // Fix a common malformed pattern from model outputs: "*Title**" -> "**Title**"
3390
+ normalized = normalized.replace(/^\*([^*\n][\s\S]*?)\*\*(\s|$)/, '**$1**$2');
3391
+
3392
+ // Ensure list items are on their own line (e.g., "text: - **A:** ... - **B:** ...")
3393
+ normalized = normalized.replace(/:\s+-\s+/g, ':\n- ');
3394
+ normalized = normalized.replace(/\s+-\s+\*\*/g, '\n- **');
3395
+
3396
+ // Ensure headings/paragraph transitions don't collapse unexpectedly
3397
+ normalized = normalized.replace(/\n{3,}/g, '\n\n');
3398
+
3399
+ return normalized;
3400
+ }
3401
+
3402
+ function escapeHtml(text) {
3403
+ return String(text ?? '')
3404
+ .replace(/&/g, '&amp;')
3405
+ .replace(/</g, '&lt;')
3406
+ .replace(/>/g, '&gt;');
3407
+ }
3408
+
3409
+ function basicMarkdownToHtml(text) {
3410
+ const input = normalizeMarkdownText(text);
3411
+ const lines = input.split('\n');
3412
+ let html = '';
3413
+ let inList = false;
3414
+
3415
+ function inlineFormat(line) {
3416
+ let s = escapeHtml(line);
3417
+ s = s.replace(/`([^`]+)`/g, '<code class="qa-code-inline">$1</code>');
3418
+ s = s.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
3419
+ s = s.replace(/\*([^*\n]+)\*/g, '<em>$1</em>');
3420
+ return s;
3421
+ }
3422
+
3423
+ for (const rawLine of lines) {
3424
+ const line = rawLine.trim();
3425
+
3426
+ if (!line) {
3427
+ if (inList) {
3428
+ html += '</ul>';
3429
+ inList = false;
3430
+ }
3431
+ continue;
3432
+ }
3433
+
3434
+ const listMatch = line.match(/^[-*]\s+(.+)/);
3435
+ if (listMatch) {
3436
+ if (!inList) {
3437
+ html += '<ul class="qa-list">';
3438
+ inList = true;
3439
+ }
3440
+ html += `<li class="qa-li">${inlineFormat(listMatch[1])}</li>`;
3441
+ continue;
3442
+ }
3443
+
3444
+ if (inList) {
3445
+ html += '</ul>';
3446
+ inList = false;
3447
+ }
3448
+
3449
+ if (/^#{1,6}\s+/.test(line)) {
3450
+ const level = Math.min(6, (line.match(/^#+/)?.[0]?.length || 1));
3451
+ const content = line.replace(/^#{1,6}\s+/, '');
3452
+ html += `<h${level} class="qa-h qa-h${level}">${inlineFormat(content)}</h${level}>`;
3453
+ } else {
3454
+ html += `<p class="qa-p">${inlineFormat(line)}</p>`;
3455
+ }
3456
+ }
3457
+
3458
+ if (inList) {
3459
+ html += '</ul>';
3460
+ }
3461
+
3462
+ return html || `<p class="qa-p">${escapeHtml(input)}</p>`;
3463
+ }
3464
+
3465
  // Function to format markdown responses using marked.js library
3466
  function formatMarkdownResponse(text) {
3467
  // Type guard - ensure text is a string
3468
  if (typeof text !== 'string') {
3469
  text = String(text ?? '');
3470
  }
3471
+
3472
+ text = normalizeMarkdownText(text);
3473
 
3474
  // Guard against marked.js CDN being unavailable
3475
  if (typeof marked === 'undefined') {
3476
  console.warn('⚠️ Marked.js not loaded – falling back to plain text');
3477
+ return basicMarkdownToHtml(text);
3478
  }
3479
 
3480
  // Configure marked for scientific content
 
3491
 
3492
  // Custom table rendering
3493
  renderer.table = function(header, body) {
3494
+ return `<div class="qa-table-wrap"><table class="qa-table"><thead>${header}</thead><tbody>${body}</tbody></table></div>`;
 
 
 
3495
  };
3496
 
3497
  // Custom table header rendering
3498
  renderer.tablecell = function(content, flags) {
3499
  const type = flags.header ? 'th' : 'td';
3500
+ return `<${type}>${content}</${type}>`;
 
 
 
3501
  };
3502
 
3503
  // Custom header rendering
3504
  renderer.heading = function(text, level) {
3505
+ return `<h${level} class="qa-h qa-h${level}">${text}</h${level}>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3506
  };
3507
 
3508
  // Custom paragraph rendering
3509
  renderer.paragraph = function(text) {
3510
+ return `<p class="qa-p">${text}</p>`;
3511
  };
3512
 
3513
  // Custom horizontal rule
3514
  renderer.hr = function() {
3515
+ return '<hr class="qa-hr">';
3516
  };
3517
 
3518
  // Custom blockquote
3519
  renderer.blockquote = function(quote) {
3520
+ return `<blockquote class="qa-blockquote">${quote}</blockquote>`;
3521
  };
3522
 
3523
  // Custom code
3524
  renderer.code = function(code, language) {
3525
+ return `<pre class="qa-pre"><code>${code}</code></pre>`;
3526
  };
3527
 
3528
  // Custom inline code
3529
  renderer.codespan = function(code) {
3530
+ return `<code class="qa-code-inline">${code}</code>`;
3531
  };
3532
 
3533
  // Custom list rendering
3534
  renderer.list = function(body, ordered) {
3535
  const type = ordered ? 'ol' : 'ul';
3536
+ return `<${type} class="qa-list">${body}</${type}>`;
3537
  };
3538
 
3539
  renderer.listitem = function(text) {
3540
+ return `<li class="qa-li">${text}</li>`;
3541
  };
3542
 
3543
  // Parse markdown with custom renderer
3544
  try {
3545
  const html = marked(text, { renderer: renderer });
3546
+ // If markdown markers still survive parsing, use robust fallback renderer.
3547
+ if (/\*\*|`|(^|\s)-\s+\S/m.test(html.replace(/<[^>]+>/g, ''))) {
3548
+ return basicMarkdownToHtml(text);
3549
+ }
3550
  return html;
3551
  } catch (error) {
3552
  console.error('Error parsing markdown:', error);
3553
+ return basicMarkdownToHtml(text);
 
3554
  }
3555
  }
3556
 
3557
+ function setAdminStatus(message, isError = false) {
3558
+ const statusEl = document.getElementById('adminPanelStatus');
3559
+ if (!statusEl) return;
3560
+ statusEl.textContent = message || '';
3561
+ statusEl.style.color = isError ? '#d32f2f' : '#2e7d32';
3562
+ }
3563
+
3564
+ function initAdminPanel() {
3565
+ const adminFab = document.getElementById('adminFab');
3566
+ const adminPanel = document.getElementById('adminPanel');
3567
+ const verifyAdminBtn = document.getElementById('verifyAdminBtn');
3568
+ const wipeDataBtn = document.getElementById('wipeDataBtn');
3569
+ const adminDangerZone = document.getElementById('adminDangerZone');
3570
+ const adminPasswordInput = document.getElementById('adminPasswordInput');
3571
+ const adminDeleteConfirmInput = document.getElementById('adminDeleteConfirmInput');
3572
+
3573
+ if (!adminFab || !adminPanel || !verifyAdminBtn || !wipeDataBtn) {
3574
+ return;
3575
+ }
3576
+
3577
+ adminFab.addEventListener('click', () => {
3578
+ const nextDisplay = adminPanel.style.display === 'block' ? 'none' : 'block';
3579
+ adminPanel.style.display = nextDisplay;
3580
+ if (nextDisplay === 'block') {
3581
+ setAdminStatus('');
3582
+ adminPasswordInput.focus();
3583
+ }
3584
+ });
3585
+
3586
+ verifyAdminBtn.addEventListener('click', async () => {
3587
+ const password = (adminPasswordInput.value || '').trim();
3588
+ if (!password) {
3589
+ setAdminStatus('Please enter the admin password.', true);
3590
+ return;
3591
+ }
3592
+
3593
+ try {
3594
+ const formData = new FormData();
3595
+ formData.append('password', password);
3596
+
3597
+ const response = await fetch('/admin/verify_password', {
3598
+ method: 'POST',
3599
+ body: formData
3600
+ });
3601
+
3602
+ const data = await response.json();
3603
+ if (!response.ok || !data.ok) {
3604
+ adminDangerZone.style.display = 'none';
3605
+ setAdminStatus(data.message || 'Password verification failed.', true);
3606
+ return;
3607
+ }
3608
+
3609
+ adminDangerZone.style.display = 'block';
3610
+ setAdminStatus('Verified. Danger zone unlocked for this session.');
3611
+ } catch (error) {
3612
+ setAdminStatus(`Verification error: ${error.message || error}`, true);
3613
+ }
3614
+ });
3615
+
3616
+ wipeDataBtn.addEventListener('click', async () => {
3617
+ const password = (adminPasswordInput.value || '').trim();
3618
+ const confirmation = (adminDeleteConfirmInput.value || '').trim();
3619
+ if (!password) {
3620
+ setAdminStatus('Enter password before deleting.', true);
3621
+ return;
3622
+ }
3623
+ if (!confirmation) {
3624
+ setAdminStatus('Type confirmation phrase before deleting.', true);
3625
+ return;
3626
+ }
3627
+
3628
+ const proceed = window.confirm(
3629
+ 'This will permanently delete all files/folders inside /data. Continue?'
3630
+ );
3631
+ if (!proceed) {
3632
+ return;
3633
+ }
3634
+
3635
+ try {
3636
+ const formData = new FormData();
3637
+ formData.append('password', password);
3638
+ formData.append('confirmation_text', confirmation);
3639
+
3640
+ const response = await fetch('/admin/wipe_data', {
3641
+ method: 'POST',
3642
+ body: formData
3643
+ });
3644
+
3645
+ const data = await response.json();
3646
+ if (!response.ok || !data.ok) {
3647
+ setAdminStatus(data.message || 'Failed to delete /data contents.', true);
3648
+ return;
3649
+ }
3650
+
3651
+ setAdminStatus(
3652
+ `Deleted /data contents (files: ${data.deleted_files ?? 0}, folders: ${data.deleted_dirs ?? 0}).`
3653
+ );
3654
+ adminDeleteConfirmInput.value = '';
3655
+ } catch (error) {
3656
+ setAdminStatus(`Delete request failed: ${error.message || error}`, true);
3657
+ }
3658
+ });
3659
+ }
3660
+
3661
  // Event listeners for hypothesis conversation
3662
  document.getElementById('start-hypothesis-conversation-btn').addEventListener('click', startHypothesisConversation);
3663
  document.getElementById('send-hypothesis-message-btn').addEventListener('click', sendHypothesisMessage);
 
3668
  sendHypothesisMessage();
3669
  }
3670
  });
3671
+
3672
+ initAdminPanel();
3673
  </script>
3674
  </body>
3675
  </html>
main.py CHANGED
@@ -9,6 +9,8 @@ import json
9
  import re
10
  import time
11
  import random
 
 
12
  from datetime import datetime
13
  from typing import List, Tuple, Optional, Dict, Any
14
  from contextlib import asynccontextmanager
@@ -645,6 +647,9 @@ embedding_model = None
645
  llm_QA = None
646
  llm_system_chart_extraction = None
647
 
 
 
 
648
  def get_model_provider(model: str) -> Optional[str]:
649
  if model in OPENAI_MODELS:
650
  return "openai"
@@ -2879,6 +2884,127 @@ async def reload_databases():
2879
  "total_files": sum(len(files) for files in FILE_REGISTRY.values())
2880
  })
2881
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2882
  def extract_citation_markers(hypothesis_text):
2883
  """
2884
  DEPRECATED: Extract citation markers like [1], [2], etc. from hypothesis text
 
9
  import re
10
  import time
11
  import random
12
+ import hmac
13
+ import shutil
14
  from datetime import datetime
15
  from typing import List, Tuple, Optional, Dict, Any
16
  from contextlib import asynccontextmanager
 
647
  llm_QA = None
648
  llm_system_chart_extraction = None
649
 
650
+ ADMIN_DELETE_CONFIRMATION_TEXT = "DELETE /data"
651
+ DEFAULT_ADMIN_PURGE_PATH = "/data"
652
+
653
  def get_model_provider(model: str) -> Optional[str]:
654
  if model in OPENAI_MODELS:
655
  return "openai"
 
2884
  "total_files": sum(len(files) for files in FILE_REGISTRY.values())
2885
  })
2886
 
2887
+ def get_admin_password() -> Optional[str]:
2888
+ password = os.environ.get("ADMIN_PASSWORD")
2889
+ if password:
2890
+ return password.strip()
2891
+ return None
2892
+
2893
+ def get_admin_purge_root() -> str:
2894
+ configured = (os.environ.get("ADMIN_PURGE_PATH") or DEFAULT_ADMIN_PURGE_PATH).strip()
2895
+ if not configured:
2896
+ configured = DEFAULT_ADMIN_PURGE_PATH
2897
+ return os.path.abspath(configured)
2898
+
2899
+ def is_safe_admin_purge_root(path: str) -> bool:
2900
+ normalized = os.path.abspath(path)
2901
+ # Must be an absolute subpath, never root.
2902
+ if normalized in {"/", ""}:
2903
+ return False
2904
+ if not normalized.startswith("/data"):
2905
+ return False
2906
+ return True
2907
+
2908
+ def delete_directory_contents(path: str) -> Dict[str, int]:
2909
+ deleted_files = 0
2910
+ deleted_dirs = 0
2911
+ for name in os.listdir(path):
2912
+ target = os.path.join(path, name)
2913
+ if os.path.isfile(target) or os.path.islink(target):
2914
+ os.unlink(target)
2915
+ deleted_files += 1
2916
+ elif os.path.isdir(target):
2917
+ shutil.rmtree(target)
2918
+ deleted_dirs += 1
2919
+ return {"deleted_files": deleted_files, "deleted_dirs": deleted_dirs}
2920
+
2921
+ @app.post("/admin/verify_password")
2922
+ async def admin_verify_password(password: str = Form(...)):
2923
+ admin_password = get_admin_password()
2924
+ if not admin_password:
2925
+ return JSONResponse(
2926
+ content={"ok": False, "message": "ADMIN_PASSWORD is not configured on server."},
2927
+ status_code=500
2928
+ )
2929
+
2930
+ if not hmac.compare_digest(password or "", admin_password):
2931
+ return JSONResponse(
2932
+ content={"ok": False, "message": "Invalid admin password."},
2933
+ status_code=403
2934
+ )
2935
+
2936
+ return JSONResponse(content={"ok": True, "message": "Password verified."}, status_code=200)
2937
+
2938
+ @app.post("/admin/wipe_data")
2939
+ async def admin_wipe_data(
2940
+ password: str = Form(...),
2941
+ confirmation_text: str = Form(...)
2942
+ ):
2943
+ admin_password = get_admin_password()
2944
+ if not admin_password:
2945
+ return JSONResponse(
2946
+ content={"ok": False, "message": "ADMIN_PASSWORD is not configured on server."},
2947
+ status_code=500
2948
+ )
2949
+
2950
+ if not hmac.compare_digest(password or "", admin_password):
2951
+ return JSONResponse(
2952
+ content={"ok": False, "message": "Invalid admin password."},
2953
+ status_code=403
2954
+ )
2955
+
2956
+ if (confirmation_text or "").strip() != ADMIN_DELETE_CONFIRMATION_TEXT:
2957
+ return JSONResponse(
2958
+ content={
2959
+ "ok": False,
2960
+ "message": f"Invalid confirmation text. Type exactly: {ADMIN_DELETE_CONFIRMATION_TEXT}"
2961
+ },
2962
+ status_code=400
2963
+ )
2964
+
2965
+ purge_root = get_admin_purge_root()
2966
+ if not is_safe_admin_purge_root(purge_root):
2967
+ return JSONResponse(
2968
+ content={"ok": False, "message": f"Unsafe ADMIN_PURGE_PATH resolved to '{purge_root}'."},
2969
+ status_code=400
2970
+ )
2971
+
2972
+ if not os.path.exists(purge_root):
2973
+ return JSONResponse(
2974
+ content={"ok": False, "message": f"Purge path does not exist: {purge_root}"},
2975
+ status_code=404
2976
+ )
2977
+
2978
+ if not os.path.isdir(purge_root):
2979
+ return JSONResponse(
2980
+ content={"ok": False, "message": f"Purge path is not a directory: {purge_root}"},
2981
+ status_code=400
2982
+ )
2983
+
2984
+ try:
2985
+ result = delete_directory_contents(purge_root)
2986
+ logger.warning(
2987
+ "Admin data wipe executed for %s (files=%s, dirs=%s)",
2988
+ purge_root,
2989
+ result["deleted_files"],
2990
+ result["deleted_dirs"]
2991
+ )
2992
+ return JSONResponse(
2993
+ content={
2994
+ "ok": True,
2995
+ "message": f"Deleted contents of {purge_root}.",
2996
+ "deleted_files": result["deleted_files"],
2997
+ "deleted_dirs": result["deleted_dirs"]
2998
+ },
2999
+ status_code=200
3000
+ )
3001
+ except Exception as e:
3002
+ logger.exception("Admin data wipe failed: %s", str(e))
3003
+ return JSONResponse(
3004
+ content={"ok": False, "message": f"Failed to wipe data: {str(e)}"},
3005
+ status_code=500
3006
+ )
3007
+
3008
  def extract_citation_markers(hypothesis_text):
3009
  """
3010
  DEPRECATED: Extract citation markers like [1], [2], etc. from hypothesis text