sofzcc commited on
Commit
fbfedd4
Β·
verified Β·
1 Parent(s): 969b626

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +323 -339
app.py CHANGED
@@ -543,17 +543,54 @@ def chat_respond(message: str, history: list) -> str:
543
  # -----------------------------
544
 
545
  def list_kb_files() -> str:
 
546
  patterns = ["*.txt", "*.md", "*.pdf", "*.docx", "*.doc"]
547
  paths: List[str] = []
548
  for pat in patterns:
549
  paths.extend(glob.glob(os.path.join(KB_DIR, pat)))
550
  paths = sorted(paths)
 
551
  if not paths:
552
- return "_No files yet. Upload documents to get started._"
553
- rows = ["| File | Size |", "|------|------|"]
 
 
 
 
 
 
 
554
  for p in paths:
555
- rows.append(f"| {os.path.basename(p)} | {os.path.getsize(p)/1024:.1f} KB |")
556
- return "\n".join(rows)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
557
 
558
 
559
  def upload_kb_files(files) -> Tuple[str, str]:
@@ -610,307 +647,239 @@ def delete_kb_file(filename: str) -> Tuple[str, str]:
610
 
611
 
612
  # -----------------------------
613
- # GRADIO UI β€” Single-page layout
614
- # Styles injected via gr.HTML to stay compatible with all Gradio versions.
615
  # -----------------------------
616
 
617
  STYLE_HTML = """
618
  <style>
619
- /* ── Design tokens: soft, warm palette ───────────────── */
620
  :root {
621
- --sage: #6b8f71; /* soft sage green β€” primary accent */
622
- --sage-l: #e8f0e9; /* sage tint β€” hover fill */
623
- --sage-d: #4f6e55; /* darker sage β€” active state */
624
- --sand: #f5f0e8; /* warm sand β€” page background */
625
- --cream: #fdfaf5; /* cream β€” surface / cards */
626
- --stone: #e8e2d9; /* light stone β€” borders */
627
- --ink: #2d2d2d; /* near-black β€” primary text */
628
- --muted: #7a7167; /* warm grey β€” secondary text */
629
- --rose: #c0717a; /* muted rose β€” danger / delete */
630
- --rose-l: #f5e6e8; /* rose tint β€” danger hover */
631
- --radius: 14px;
632
- --shadow: 0 2px 10px rgba(0,0,0,.07);
633
- --shadow-md: 0 4px 20px rgba(0,0,0,.10);
634
- }
635
-
636
- /* ── Base ─────────────────────────────────────────────── */
637
- body, .gradio-container {
638
- background: var(--sand) !important;
639
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif !important;
640
- color: var(--ink) !important;
641
- }
642
- .gradio-container { max-width: 1100px !important; margin: 0 auto !important; }
643
-
644
- /* ── Header ───────────────────────────────────────────── */
645
- .kb-header {
646
- background: var(--cream);
647
- border-bottom: 1px solid var(--stone);
648
- padding: 18px 24px 14px;
649
- display: flex;
650
- align-items: center;
651
- gap: 14px;
652
- }
653
- .kb-header-icon {
654
- width: 42px; height: 42px;
655
- background: var(--sage);
656
- border-radius: 12px;
657
- display: flex; align-items: center; justify-content: center;
658
- font-size: 1.3rem; flex-shrink: 0;
659
  }
660
- .kb-header h1 {
661
- margin: 0 0 2px;
662
- font-size: clamp(1.05rem, 3vw, 1.35rem);
663
- font-weight: 650;
664
- color: var(--ink);
665
- letter-spacing: -.01em;
666
- }
667
- .kb-header p { margin: 0; font-size: .83rem; color: var(--muted); }
668
-
669
- /* ── Chat bubbles ─────────────────────────────────────── */
670
- .message.user > div {
671
- background: var(--sage) !important;
672
- color: #fff !important;
673
- border-radius: var(--radius) var(--radius) 4px var(--radius) !important;
674
- padding: 10px 15px !important;
675
- max-width: 80% !important;
676
- font-size: clamp(.84rem, 2vw, .94rem) !important;
677
- line-height: 1.6 !important;
678
- box-shadow: var(--shadow) !important;
679
- }
680
- .message.bot > div {
681
- background: var(--cream) !important;
682
- border: 1px solid var(--stone) !important;
683
- border-radius: var(--radius) var(--radius) var(--radius) 4px !important;
684
- padding: 12px 16px !important;
685
- max-width: 84% !important;
686
- font-size: clamp(.84rem, 2vw, .94rem) !important;
687
- line-height: 1.65 !important;
688
- box-shadow: var(--shadow) !important;
689
- color: var(--ink) !important;
690
- }
691
-
692
- /* ── Suggested question pills β€” auto-sizing ───────────── */
693
- .examples-table { display: flex !important; flex-wrap: wrap !important; gap: 8px !important; padding: 10px 0 4px !important; }
694
- .examples-table button, .example {
695
- display: inline-flex !important;
696
- align-items: center !important;
697
- width: auto !important; /* KEY: no forced width β€” wraps to content */
698
- white-space: normal !important; /* allow text to wrap if very long */
699
- border-radius: 999px !important;
700
- border: 1.5px solid var(--sage) !important;
701
- color: var(--sage-d) !important;
702
- background: var(--cream) !important;
703
- padding: 6px 16px !important;
704
- font-size: .8rem !important;
705
- font-weight: 500 !important;
706
- line-height: 1.4 !important;
707
- text-align: center !important;
708
- cursor: pointer !important;
709
- transition: background .18s, color .18s, border-color .18s !important;
710
- box-shadow: 0 1px 3px rgba(0,0,0,.06) !important;
711
- }
712
- .examples-table button:hover, .example:hover {
713
- background: var(--sage) !important;
714
- color: #fff !important;
715
- border-color: var(--sage) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
716
  }
 
717
 
718
- /* ── Chat input area ──────────────────────────────────── */
719
- .chat-input-container {
720
- background: var(--cream) !important;
721
- border-top: 1px solid var(--stone) !important;
722
- padding: 10px 14px !important;
723
- border-radius: 0 0 var(--radius) var(--radius) !important;
724
- }
725
- .chat-input-container textarea {
726
- background: var(--sand) !important;
727
- border: 1.5px solid var(--stone) !important;
728
- border-radius: 12px !important;
729
- padding: 10px 14px !important;
730
- font-size: .92rem !important;
731
- color: var(--ink) !important;
732
- resize: none !important;
733
- transition: border-color .2s, box-shadow .2s !important;
734
- min-height: 52px !important;
735
- }
736
- .chat-input-container textarea:focus {
737
- border-color: var(--sage) !important;
738
- box-shadow: 0 0 0 3px rgba(107,143,113,.18) !important;
739
- outline: none !important;
740
- }
741
- .chat-input-container textarea::placeholder { color: var(--muted) !important; }
742
-
743
- /* Send button */
744
- .chat-input-container button[aria-label="Send"],
745
- .chat-input-container button.send-btn {
746
- background: var(--sage) !important;
747
- color: #fff !important;
748
- border: none !important;
749
- border-radius: 10px !important;
750
- padding: 0 16px !important;
751
- height: 40px !important;
752
- font-weight: 600 !important;
753
- transition: background .18s, transform .1s !important;
754
- }
755
- .chat-input-container button[aria-label="Send"]:hover { background: var(--sage-d) !important; transform: translateY(-1px) !important; }
756
-
757
- /* ── Input toolbar (file attach, image, paste, format) ── */
758
- .input-toolbar {
759
- display: flex;
760
- align-items: center;
761
- gap: 4px;
762
- padding: 6px 14px 0;
763
- background: var(--cream);
764
- border-top: 1px solid var(--stone);
765
- flex-wrap: wrap;
766
- }
767
- .toolbar-btn {
768
- display: inline-flex;
769
- align-items: center;
770
- gap: 5px;
771
- background: transparent;
772
- border: 1px solid var(--stone);
773
- border-radius: 8px;
774
- padding: 4px 10px;
775
- font-size: .76rem;
776
- color: var(--muted);
777
- cursor: pointer;
778
- transition: background .15s, color .15s, border-color .15s;
779
- white-space: nowrap;
780
- }
781
- .toolbar-btn:hover { background: var(--sage-l); color: var(--sage-d); border-color: var(--sage); }
782
- .toolbar-btn svg { width: 13px; height: 13px; flex-shrink: 0; }
783
- .toolbar-sep { width: 1px; height: 18px; background: var(--stone); margin: 0 2px; }
784
-
785
- /* ── KB panel (inline, below chat) ───────────────────── */
786
- .kb-panel {
787
- background: var(--cream);
788
- border: 1px solid var(--stone);
789
- border-radius: var(--radius);
790
- margin: 12px 0 0;
791
- overflow: hidden;
792
- box-shadow: var(--shadow);
793
  }
794
- .kb-panel-header {
795
- background: var(--sand);
796
- border-bottom: 1px solid var(--stone);
797
- padding: 10px 16px;
798
- display: flex;
799
- align-items: center;
800
- gap: 8px;
801
- cursor: pointer;
802
- user-select: none;
803
  }
804
- .kb-panel-header span { font-weight: 600; font-size: .88rem; color: var(--ink); flex: 1; }
805
- .kb-panel-header small { font-size: .75rem; color: var(--muted); }
806
- .kb-panel-body { padding: 16px; }
807
-
808
- /* ── File table ───────────────────────────────────────── */
809
- .file-table { width: 100%; border-collapse: collapse; font-size: .83rem; margin-top: 4px; }
810
- .file-table th { background: var(--sand); padding: 7px 12px; text-align: left; font-weight: 600; color: var(--muted); border-bottom: 1px solid var(--stone); }
811
- .file-table td { padding: 7px 12px; border-bottom: 1px solid var(--stone); color: var(--ink); }
812
- .file-table tr:last-child td { border-bottom: none; }
813
-
814
- /* ── Buttons ──────────────────────────────────────────── */
815
- button.upload-btn {
816
- background: var(--sage) !important; color: #fff !important;
817
- border: none !important; border-radius: 9px !important;
818
- padding: 9px 20px !important; font-weight: 600 !important;
819
- font-size: .86rem !important; width: 100% !important;
820
- transition: background .18s !important; cursor: pointer !important;
821
- }
822
- button.upload-btn:hover { background: var(--sage-d) !important; }
823
- button.delete-btn {
824
- background: var(--rose-l) !important; color: var(--rose) !important;
825
- border: 1px solid var(--rose) !important; border-radius: 9px !important;
826
- padding: 9px 20px !important; font-weight: 600 !important;
827
- font-size: .86rem !important; width: 100% !important;
828
- transition: background .18s !important; cursor: pointer !important;
829
- }
830
- button.delete-btn:hover { background: var(--rose) !important; color: #fff !important; }
831
-
832
- /* ── Status pills ─────────────────────────────────────── */
833
- .status-ok { display:inline-block; background:#d1fae5; color:#065f46; border-radius:6px; padding:5px 12px; font-size:.82rem; }
834
- .status-err { display:inline-block; background:#fee2e2; color:#991b1b; border-radius:6px; padding:5px 12px; font-size:.82rem; }
835
-
836
- /* ── Gradio file drop zone ────────────────────────────── */
837
- .gr-file-drop, [data-testid="file"] {
838
- border: 2px dashed var(--stone) !important;
839
- border-radius: var(--radius) !important;
840
- background: var(--sand) !important;
841
- transition: border-color .2s, background .2s !important;
842
- }
843
- .gr-file-drop:hover, [data-testid="file"]:hover {
844
- border-color: var(--sage) !important;
845
- background: var(--sage-l) !important;
846
- }
847
-
848
- /* ── Mobile ───────────────────────────────────────────── */
849
- @media (max-width: 640px) {
850
- .gradio-container { padding: 0 !important; }
851
- .kb-header { padding: 12px 14px 10px !important; }
852
- .message.user > div, .message.bot > div { max-width: 95% !important; }
853
- .examples-table { gap: 6px !important; }
854
- .input-toolbar { padding: 5px 10px 0 !important; gap: 3px !important; }
855
- .toolbar-btn { padding: 4px 8px !important; font-size: .72rem !important; }
856
- .kb-panel-body > .row { flex-direction: column !important; }
857
- }
858
- </style>
859
  """
860
 
861
- # ── Input toolbar HTML ────────────────────────────────────────────────────────
862
- # Rendered above the Gradio chat input; provides affordances for attach, image,
863
- # paste and formatting hints. Clicking "Attach file" programmatically triggers
864
- # the hidden Gradio File component via JS.
865
  TOOLBAR_HTML = """
866
  <div class="input-toolbar" id="input-toolbar">
867
- <button class="toolbar-btn" onclick="document.getElementById('kb-upload-trigger').click()" title="Attach a document to add to the knowledge base">
868
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/></svg>
869
- Attach file
 
 
870
  </button>
871
- <button class="toolbar-btn" onclick="
872
- var t=document.querySelector('.chat-input-container textarea');
873
- if(t){navigator.clipboard.readText().then(txt=>{var s=t.selectionStart,e=t.selectionEnd;t.value=t.value.slice(0,s)+txt+t.value.slice(e);t.selectionStart=t.selectionEnd=s+txt.length;t.dispatchEvent(new Event('input',{bubbles:true}))}).catch(()=>t.focus())}
874
- " title="Paste clipboard text into the message box">
875
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="8" y="2" width="8" height="4" rx="1"/><path d="M16 4h2a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2h2"/></svg>
876
- Paste
877
  </button>
878
- <div class="toolbar-sep"></div>
879
- <button class="toolbar-btn" onclick="
880
- var t=document.querySelector('.chat-input-container textarea');
881
- if(t){var s=t.selectionStart,e=t.selectionEnd,sel=t.value.slice(s,e);
882
- if(sel){t.value=t.value.slice(0,s)+'**'+sel+'**'+t.value.slice(e);t.dispatchEvent(new Event('input',{bubbles:true}))}}
883
- " title="Bold selected text">
884
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M6 4h8a4 4 0 010 8H6zM6 12h9a4 4 0 010 8H6z"/></svg>
885
- Bold
886
  </button>
887
- <button class="toolbar-btn" onclick="
888
- var t=document.querySelector('.chat-input-container textarea');
889
- if(t){var s=t.selectionStart,e=t.selectionEnd,sel=t.value.slice(s,e);
890
- if(sel){t.value=t.value.slice(0,s)+'*'+sel+'*'+t.value.slice(e);t.dispatchEvent(new Event('input',{bubbles:true}))}}
891
- " title="Italic selected text">
892
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="19" y1="4" x2="10" y2="4"/><line x1="14" y1="20" x2="5" y2="20"/><line x1="15" y1="4" x2="9" y2="20"/></svg>
893
- Italic
894
  </button>
895
- <button class="toolbar-btn" onclick="
896
- var t=document.querySelector('.chat-input-container textarea');
897
- if(t){var lines=t.value.split('\n');var s=t.selectionStart;var lineIdx=t.value.slice(0,s).split('\n').length-1;
898
- lines[lineIdx]='- '+lines[lineIdx];t.value=lines.join('\n');t.dispatchEvent(new Event('input',{bubbles:true}))}
899
- " title="Add bullet point">
900
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><circle cx="3" cy="6" r="1" fill="currentColor"/><circle cx="3" cy="12" r="1" fill="currentColor"/><circle cx="3" cy="18" r="1" fill="currentColor"/></svg>
901
- List
 
 
902
  </button>
903
- <button class="toolbar-btn" onclick="
904
- var t=document.querySelector('.chat-input-container textarea');
905
- if(t){var s=t.selectionStart,e=t.selectionEnd,sel=t.value.slice(s,e);
906
- if(sel){t.value=t.value.slice(0,s)+'`'+sel+'`'+t.value.slice(e);t.dispatchEvent(new Event('input',{bubbles:true}))}}
907
- " title="Inline code format">
908
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
909
- Code
910
  </button>
911
  </div>
912
  """
913
 
 
 
 
 
 
 
914
  examples = [
915
  q["query"]
916
  for q in CONFIG.get("quick_actions", [])
@@ -921,80 +890,95 @@ examples = [
921
  "Why is self-service important for customer support?",
922
  ]
923
 
 
 
 
 
924
  with gr.Blocks(title=CONFIG["client"]["name"]) as demo:
925
 
926
  gr.HTML(STYLE_HTML)
927
 
928
- # ── Header ────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
929
  gr.HTML(f"""
930
  <div class="kb-header">
931
- <div class="kb-header-icon">πŸ€–</div>
932
  <div>
933
  <h1>{CONFIG["client"]["name"]}</h1>
934
  <p>Search the knowledge base and get answers in plain language</p>
935
  </div>
 
 
 
 
 
 
 
 
 
936
  </div>
937
  """)
938
 
939
- # ── Main layout: chat left, KB panel right ─────────────────────────
940
- with gr.Row():
941
 
942
- # ── Left: full chat interface ──────────────────────────────────
943
- with gr.Column(scale=3):
944
 
945
- gr.ChatInterface(
946
- fn=chat_respond,
947
- examples=examples,
948
- cache_examples=False,
949
- )
950
 
951
- # Input toolbar rendered after the chat interface
952
- gr.HTML(TOOLBAR_HTML)
953
 
954
- # Hidden file input triggered by toolbar "Attach file" button.
955
- # Uploaded files are added to the KB and indexed automatically.
956
- toolbar_upload = gr.File(
957
- label="",
958
- file_count="multiple",
959
- file_types=[".txt", ".md", ".pdf", ".docx", ".doc"],
960
- visible=False,
961
- elem_id="kb-upload-trigger",
962
- )
963
- toolbar_upload_status = gr.Markdown(visible=False)
964
-
965
- # ── Right: KB manager panel ────────────────────────────────────
966
- with gr.Column(scale=1):
967
-
968
- gr.HTML("""
969
- <div class="kb-panel">
970
- <div class="kb-panel-header">
971
- <span>πŸ“ Knowledge Base</span>
972
- <small>Files Β· Upload Β· Delete</small>
973
- </div>
974
- </div>
975
- """)
976
-
977
- with gr.Group():
978
- kb_file_list = gr.Markdown(value=list_kb_files)
979
-
980
- upload_input = gr.File(
981
- label="Upload files (.txt .md .pdf .docx, max 10 MB)",
982
- file_count="multiple",
983
- file_types=[".txt", ".md", ".pdf", ".docx", ".doc"],
984
- )
985
- upload_btn = gr.Button("⬆️ Upload & Index", variant="primary", elem_classes="upload-btn")
986
- upload_status = gr.Markdown()
987
-
988
- gr.HTML("<div style='height:1px;background:var(--stone,#e8e2d9);margin:10px 0'></div>")
989
-
990
- delete_input = gr.Textbox(
991
- placeholder="Filename to delete, e.g. guide.pdf",
992
- show_label=False,
993
- )
994
- delete_btn = gr.Button("πŸ—‘οΈ Delete file", variant="stop", elem_classes="delete-btn")
995
- delete_status = gr.Markdown()
996
-
997
- # ── Event wiring ───────────────────────────────────────────────────
998
  upload_btn.click(
999
  upload_kb_files,
1000
  inputs=[upload_input],
 
543
  # -----------------------------
544
 
545
  def list_kb_files() -> str:
546
+ """Return an HTML file table with per-row delete buttons."""
547
  patterns = ["*.txt", "*.md", "*.pdf", "*.docx", "*.doc"]
548
  paths: List[str] = []
549
  for pat in patterns:
550
  paths.extend(glob.glob(os.path.join(KB_DIR, pat)))
551
  paths = sorted(paths)
552
+
553
  if not paths:
554
+ return (
555
+ "<div style='text-align:center;padding:28px 16px;color:var(--muted,#7a7167)'>"
556
+ "<div style='font-size:2rem;margin-bottom:8px'>πŸ“­</div>"
557
+ "<div style='font-size:.85rem'>No files yet.<br>Upload documents to get started.</div>"
558
+ "</div>"
559
+ )
560
+
561
+ ext_icons = {".pdf": "πŸ“„", ".docx": "πŸ“", ".doc": "πŸ“", ".md": "πŸ“‹", ".txt": "πŸ“ƒ"}
562
+ rows_html = ""
563
  for p in paths:
564
+ name = os.path.basename(p)
565
+ ext = os.path.splitext(name)[1].lower()
566
+ icon = ext_icons.get(ext, "πŸ“„")
567
+ size = os.path.getsize(p) / 1024
568
+ size_str = f"{size:.0f} KB" if size < 1024 else f"{size/1024:.1f} MB"
569
+ rows_html += (
570
+ f"<tr>"
571
+ f"<td style='padding:8px 10px;border-bottom:1px solid var(--stone,#e8e2d9);max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap'>"
572
+ f"<span style='margin-right:6px'>{icon}</span>"
573
+ f"<span title='{name}' style='font-size:.82rem;color:var(--ink,#2d2d2d)'>{name}</span>"
574
+ f"</td>"
575
+ f"<td style='padding:8px 6px;border-bottom:1px solid var(--stone,#e8e2d9);font-size:.78rem;color:var(--muted,#7a7167);white-space:nowrap'>{size_str}</td>"
576
+ f"<td style='padding:8px 6px;border-bottom:1px solid var(--stone,#e8e2d9);text-align:right'>"
577
+ f"<button onclick='(function(){{var t=document.querySelector(\"#delete-input textarea, #delete-input input\");if(t){{t.value=\"{name}\";t.dispatchEvent(new Event(\"input\",{{bubbles:true}}));}}var b=document.querySelector(\"#delete-btn\");if(b)b.click();}})();' "
578
+ f"style='background:none;border:none;cursor:pointer;color:var(--rose,#c0717a);font-size:.85rem;padding:2px 4px;border-radius:4px;transition:background .15s' "
579
+ f"title='Delete {name}' onmouseover='this.style.background=\"var(--rose-l,#f5e6e8)\"' onmouseout='this.style.background=\"none\"'>πŸ—‘</button>"
580
+ f"</td>"
581
+ f"</tr>"
582
+ )
583
+
584
+ return (
585
+ f"<table style='width:100%;border-collapse:collapse;font-size:.82rem'>"
586
+ f"<thead><tr>"
587
+ f"<th style='padding:7px 10px;background:var(--sand,#f5f0e8);text-align:left;font-weight:600;font-size:.75rem;color:var(--muted,#7a7167);border-bottom:1px solid var(--stone,#e8e2d9)'>File</th>"
588
+ f"<th style='padding:7px 6px;background:var(--sand,#f5f0e8);text-align:left;font-weight:600;font-size:.75rem;color:var(--muted,#7a7167);border-bottom:1px solid var(--stone,#e8e2d9)'>Size</th>"
589
+ f"<th style='padding:7px 6px;background:var(--sand,#f5f0e8);border-bottom:1px solid var(--stone,#e8e2d9)'></th>"
590
+ f"</tr></thead>"
591
+ f"<tbody>{rows_html}</tbody>"
592
+ f"</table>"
593
+ )
594
 
595
 
596
  def upload_kb_files(files) -> Tuple[str, str]:
 
647
 
648
 
649
  # -----------------------------
650
+ # GRADIO UI
 
651
  # -----------------------------
652
 
653
  STYLE_HTML = """
654
  <style>
655
+ /* ── Tokens ───────────────────────────── */
656
  :root {
657
+ --sage:#6b8f71; --sage-l:#e8f0e9; --sage-d:#4f6e55;
658
+ --sand:#f5f0e8; --cream:#fdfaf5; --stone:#e8e2d9;
659
+ --ink:#2d2d2d; --muted:#7a7167;
660
+ --rose:#c0717a; --rose-l:#f5e6e8;
661
+ --radius:14px; --shadow:0 2px 10px rgba(0,0,0,.07);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
662
  }
663
+ body,.gradio-container{background:var(--sand)!important;
664
+ font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif!important;color:var(--ink)!important}
665
+ .gradio-container{max-width:1120px!important;margin:0 auto!important}
666
+
667
+ /* ── Header ──────────────────────────── */
668
+ .kb-header{background:var(--cream);border-bottom:1px solid var(--stone);
669
+ padding:16px 22px 14px;display:flex;align-items:center;gap:14px}
670
+ .kb-icon{width:40px;height:40px;background:var(--sage);border-radius:11px;
671
+ display:flex;align-items:center;justify-content:center;font-size:1.2rem;flex-shrink:0}
672
+ .kb-header h1{margin:0 0 2px;font-size:clamp(1rem,3vw,1.3rem);font-weight:650;
673
+ color:var(--ink);letter-spacing:-.01em}
674
+ .kb-header p{margin:0;font-size:.8rem;color:var(--muted)}
675
+ .kb-header-actions{margin-left:auto;display:flex;gap:8px;align-items:center}
676
+ .kb-toggle-btn{background:transparent;border:1.5px solid var(--stone);
677
+ border-radius:8px;padding:6px 12px;font-size:.78rem;font-weight:500;
678
+ color:var(--muted);cursor:pointer;transition:all .18s;display:flex;align-items:center;gap:5px}
679
+ .kb-toggle-btn:hover{background:var(--sage-l);border-color:var(--sage);color:var(--sage-d)}
680
+
681
+ /* ── Chat bubbles ─────────────────────── */
682
+ .message.user>div{background:var(--sage)!important;color:#fff!important;
683
+ border-radius:var(--radius) var(--radius) 4px var(--radius)!important;
684
+ padding:10px 15px!important;max-width:78%!important;
685
+ font-size:clamp(.84rem,2vw,.93rem)!important;line-height:1.6!important;box-shadow:var(--shadow)!important}
686
+ .message.bot>div{background:var(--cream)!important;border:1px solid var(--stone)!important;
687
+ border-radius:var(--radius) var(--radius) var(--radius) 4px!important;
688
+ padding:12px 16px!important;max-width:82%!important;
689
+ font-size:clamp(.84rem,2vw,.93rem)!important;line-height:1.65!important;
690
+ box-shadow:var(--shadow)!important;color:var(--ink)!important}
691
+
692
+ /* Hide the default "Chatbot" label */
693
+ .chatbot-label,.label-wrap{display:none!important}
694
+
695
+ /* ── Suggested pills β€” auto-sizing ───── */
696
+ .examples-table{display:flex!important;flex-wrap:wrap!important;gap:7px!important;
697
+ padding:8px 16px 12px!important;background:var(--cream);border-top:1px solid var(--stone)}
698
+ .examples-table td,.examples-table th{padding:0!important;border:none!important;background:none!important}
699
+ .examples-table button,.example{
700
+ display:inline-flex!important;align-items:center!important;
701
+ width:auto!important;white-space:normal!important;
702
+ border-radius:999px!important;border:1.5px solid var(--stone)!important;
703
+ color:var(--sage-d)!important;background:var(--cream)!important;
704
+ padding:5px 14px!important;font-size:.78rem!important;font-weight:500!important;
705
+ line-height:1.4!important;text-align:left!important;cursor:pointer!important;
706
+ transition:background .15s,color .15s,border-color .15s!important;
707
+ box-shadow:0 1px 3px rgba(0,0,0,.05)!important}
708
+ .examples-table button:hover,.example:hover{
709
+ background:var(--sage)!important;color:#fff!important;border-color:var(--sage)!important}
710
+
711
+ /* "Try asking" label above pills */
712
+ .try-label{font-size:.73rem;font-weight:600;color:var(--muted);
713
+ letter-spacing:.04em;text-transform:uppercase;padding:10px 16px 0;background:var(--cream)}
714
+
715
+ /* ── Chat input ───────────────────────── */
716
+ .chat-input-container{background:var(--cream)!important;
717
+ border-top:1px solid var(--stone)!important;padding:10px 14px!important}
718
+ .chat-input-container textarea{background:var(--sand)!important;
719
+ border:1.5px solid var(--stone)!important;border-radius:12px!important;
720
+ padding:10px 14px!important;font-size:.9rem!important;color:var(--ink)!important;
721
+ resize:none!important;transition:border-color .2s,box-shadow .2s!important}
722
+ .chat-input-container textarea:focus{border-color:var(--sage)!important;outline:none!important;
723
+ box-shadow:0 0 0 3px rgba(107,143,113,.18)!important}
724
+ .chat-input-container textarea::placeholder{color:var(--muted)!important}
725
+ .chat-input-container button[aria-label="Send"]{
726
+ background:var(--sage)!important;color:#fff!important;border:none!important;
727
+ border-radius:10px!important;padding:0 14px!important;height:38px!important;
728
+ font-weight:600!important;transition:background .18s!important}
729
+ .chat-input-container button[aria-label="Send"]:hover{background:var(--sage-d)!important}
730
+
731
+ /* ── Compact icon toolbar ─────────────── */
732
+ .input-toolbar{display:flex;align-items:center;gap:3px;
733
+ padding:5px 14px 6px;background:var(--cream);
734
+ border-top:1px solid var(--stone)}
735
+ .tb-btn{display:inline-flex;align-items:center;justify-content:center;
736
+ width:28px;height:28px;background:transparent;border:none;
737
+ border-radius:6px;cursor:pointer;color:var(--muted);
738
+ transition:background .15s,color .15s;position:relative}
739
+ .tb-btn:hover{background:var(--sage-l);color:var(--sage-d)}
740
+ .tb-btn svg{width:14px;height:14px;pointer-events:none}
741
+ .tb-sep{width:1px;height:16px;background:var(--stone);margin:0 3px}
742
+ /* Tooltip */
743
+ .tb-btn::after{content:attr(data-tip);position:absolute;bottom:calc(100% + 6px);
744
+ left:50%;transform:translateX(-50%);background:var(--ink);color:#fff;
745
+ font-size:.68rem;padding:3px 7px;border-radius:5px;white-space:nowrap;
746
+ pointer-events:none;opacity:0;transition:opacity .15s}
747
+ .tb-btn:hover::after{opacity:1}
748
+
749
+ /* ── KB panel (slide-in overlay) ─────── */
750
+ .kb-panel-wrap{position:fixed;top:0;right:0;bottom:0;width:340px;max-width:92vw;
751
+ background:var(--cream);border-left:1px solid var(--stone);
752
+ box-shadow:-4px 0 20px rgba(0,0,0,.10);z-index:1000;
753
+ transform:translateX(100%);transition:transform .28s cubic-bezier(.4,0,.2,1);
754
+ display:flex;flex-direction:column;overflow:hidden}
755
+ .kb-panel-wrap.open{transform:translateX(0)}
756
+ .kb-panel-head{display:flex;align-items:center;padding:14px 16px;
757
+ border-bottom:1px solid var(--stone);background:var(--sand);flex-shrink:0}
758
+ .kb-panel-head h2{margin:0;font-size:.95rem;font-weight:650;color:var(--ink);flex:1}
759
+ .kb-close-btn{background:none;border:none;cursor:pointer;color:var(--muted);
760
+ font-size:1.1rem;padding:2px 6px;border-radius:6px;transition:background .15s}
761
+ .kb-close-btn:hover{background:var(--stone)}
762
+ .kb-panel-body{flex:1;overflow-y:auto;padding:14px 14px 20px}
763
+ .kb-section-label{font-size:.72rem;font-weight:700;color:var(--muted);
764
+ letter-spacing:.05em;text-transform:uppercase;margin:14px 0 7px}
765
+ .kb-section-label:first-child{margin-top:0}
766
+
767
+ /* File table inside panel */
768
+ .kb-file-table{width:100%;border-collapse:collapse;font-size:.81rem}
769
+ .kb-file-table th{background:var(--sand);padding:6px 8px;text-align:left;
770
+ font-weight:600;font-size:.72rem;color:var(--muted);border-bottom:1px solid var(--stone)}
771
+ .kb-file-table td{padding:7px 8px;border-bottom:1px solid var(--stone);color:var(--ink)}
772
+
773
+ /* Upload zone */
774
+ .gr-file-drop,[data-testid="file"]{border:2px dashed var(--stone)!important;
775
+ border-radius:var(--radius)!important;background:var(--sand)!important;
776
+ transition:border-color .2s,background .2s!important}
777
+ .gr-file-drop:hover,[data-testid="file"]:hover{border-color:var(--sage)!important;
778
+ background:var(--sage-l)!important}
779
+
780
+ /* KB action buttons */
781
+ .kb-upload-btn button{background:var(--sage)!important;color:#fff!important;
782
+ border:none!important;border-radius:9px!important;padding:9px 0!important;
783
+ font-weight:600!important;font-size:.85rem!important;width:100%!important;
784
+ transition:background .18s!important}
785
+ .kb-upload-btn button:hover{background:var(--sage-d)!important}
786
+ .kb-delete-btn button{background:var(--rose-l)!important;color:var(--rose)!important;
787
+ border:1px solid var(--rose)!important;border-radius:9px!important;padding:8px 0!important;
788
+ font-weight:600!important;font-size:.85rem!important;width:100%!important;
789
+ transition:background .18s,color .18s!important}
790
+ .kb-delete-btn button:hover{background:var(--rose)!important;color:#fff!important}
791
+
792
+ /* Overlay backdrop */
793
+ .kb-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.25);z-index:999}
794
+ .kb-backdrop.open{display:block}
795
+
796
+ /* ── Empty state ─────────────────────── */
797
+ .empty-state{display:flex;flex-direction:column;align-items:center;
798
+ justify-content:center;padding:40px 20px;gap:10px;color:var(--muted);text-align:center}
799
+ .empty-state-icon{font-size:2.6rem;opacity:.5}
800
+ .empty-state-title{font-size:.95rem;font-weight:600;color:var(--ink)}
801
+ .empty-state-sub{font-size:.82rem;max-width:260px;line-height:1.5}
802
+
803
+ /* ── Mobile ──────────────────────────── */
804
+ @media(max-width:640px){
805
+ .gradio-container{padding:0!important}
806
+ .kb-header{padding:10px 14px!important}
807
+ .kb-panel-wrap{width:100%;max-width:100vw}
808
+ .message.user>div,.message.bot>div{max-width:95%!important}
809
+ .examples-table{gap:5px!important;padding:6px 10px 8px!important}
810
+ .input-toolbar{padding:4px 10px 5px!important}
811
  }
812
+ </style>
813
 
814
+ <script>
815
+ function toggleKB(){
816
+ var p=document.getElementById("kb-panel");
817
+ var b=document.getElementById("kb-backdrop");
818
+ if(p){p.classList.toggle("open");b&&b.classList.toggle("open")}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
819
  }
820
+ function closeKB(){
821
+ var p=document.getElementById("kb-panel");
822
+ var b=document.getElementById("kb-backdrop");
823
+ if(p){p.classList.remove("open");b&&b.classList.remove("open")}
 
 
 
 
 
824
  }
825
+ </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
826
  """
827
 
828
+ # Toolbar β€” compact icon-only buttons with tooltips
 
 
 
829
  TOOLBAR_HTML = """
830
  <div class="input-toolbar" id="input-toolbar">
831
+ <button class="tb-btn" data-tip="Attach file to KB"
832
+ onclick="document.getElementById('kb-upload-trigger').click()">
833
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
834
+ <path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/>
835
+ </svg>
836
  </button>
837
+ <button class="tb-btn" data-tip="Paste from clipboard"
838
+ onclick="(function(){var t=document.querySelector('.chat-input-container textarea');if(t){navigator.clipboard.readText().then(function(txt){var s=t.selectionStart;t.value=t.value.slice(0,s)+txt+t.value.slice(t.selectionEnd);t.selectionStart=t.selectionEnd=s+txt.length;t.dispatchEvent(new Event('input',{bubbles:true}))}).catch(function(){t.focus()})}})()">
839
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
840
+ <rect x="8" y="2" width="8" height="4" rx="1"/>
841
+ <path d="M16 4h2a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2h2"/>
842
+ </svg>
843
  </button>
844
+ <div class="tb-sep"></div>
845
+ <button class="tb-btn" data-tip="Bold (select text first)"
846
+ onclick="(function(){var t=document.querySelector('.chat-input-container textarea');if(!t)return;var s=t.selectionStart,e=t.selectionEnd,v=t.value;if(s===e)return;t.value=v.slice(0,s)+'**'+v.slice(s,e)+'**'+v.slice(e);t.selectionStart=s;t.selectionEnd=e+4;t.dispatchEvent(new Event('input',{bubbles:true}))})()">
847
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
848
+ <path d="M6 4h8a4 4 0 010 8H6zM6 12h9a4 4 0 010 8H6z"/>
849
+ </svg>
 
 
850
  </button>
851
+ <button class="tb-btn" data-tip="Italic (select text first)"
852
+ onclick="(function(){var t=document.querySelector('.chat-input-container textarea');if(!t)return;var s=t.selectionStart,e=t.selectionEnd,v=t.value;if(s===e)return;t.value=v.slice(0,s)+'*'+v.slice(s,e)+'*'+v.slice(e);t.selectionStart=s;t.selectionEnd=e+2;t.dispatchEvent(new Event('input',{bubbles:true}))})()">
853
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
854
+ <line x1="19" y1="4" x2="10" y2="4"/><line x1="14" y1="20" x2="5" y2="20"/>
855
+ <line x1="15" y1="4" x2="9" y2="20"/>
856
+ </svg>
 
857
  </button>
858
+ <button class="tb-btn" data-tip="Add bullet to current line"
859
+ onclick="(function(){var t=document.querySelector('.chat-input-container textarea');if(!t)return;var lines=t.value.split('\n'),pos=t.selectionStart,idx=t.value.slice(0,pos).split('\n').length-1;lines[idx]='- '+lines[idx];t.value=lines.join('\n');t.dispatchEvent(new Event('input',{bubbles:true}))})()">
860
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
861
+ <line x1="9" y1="6" x2="20" y2="6"/><line x1="9" y1="12" x2="20" y2="12"/>
862
+ <line x1="9" y1="18" x2="20" y2="18"/>
863
+ <circle cx="4" cy="6" r="1.5" fill="currentColor" stroke="none"/>
864
+ <circle cx="4" cy="12" r="1.5" fill="currentColor" stroke="none"/>
865
+ <circle cx="4" cy="18" r="1.5" fill="currentColor" stroke="none"/>
866
+ </svg>
867
  </button>
868
+ <button class="tb-btn" data-tip="Inline code (select text first)"
869
+ onclick="(function(){var t=document.querySelector('.chat-input-container textarea');if(!t)return;var s=t.selectionStart,e=t.selectionEnd,v=t.value;if(s===e)return;t.value=v.slice(0,s)+'`'+v.slice(s,e)+'`'+v.slice(e);t.selectionStart=s;t.selectionEnd=e+2;t.dispatchEvent(new Event('input',{bubbles:true}))})()">
870
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
871
+ <polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>
872
+ </svg>
 
 
873
  </button>
874
  </div>
875
  """
876
 
877
+ # Welcome greeting injected as first bot message
878
+ WELCOME_MSG = CONFIG["messages"].get(
879
+ "welcome",
880
+ "Ask me anything about the documents in the knowledge base!"
881
+ )
882
+
883
  examples = [
884
  q["query"]
885
  for q in CONFIG.get("quick_actions", [])
 
890
  "Why is self-service important for customer support?",
891
  ]
892
 
893
+ def chat_respond_with_greeting(message: str, history: list) -> str:
894
+ """Wrapper that injects a greeting on the first turn."""
895
+ return chat_respond(message, history)
896
+
897
  with gr.Blocks(title=CONFIG["client"]["name"]) as demo:
898
 
899
  gr.HTML(STYLE_HTML)
900
 
901
+ # ── Backdrop for KB panel ──────────────────────────────────────────
902
+ gr.HTML('<div class="kb-backdrop" id="kb-backdrop" onclick="closeKB()"></div>')
903
+
904
+ # ── KB slide-in panel ──────────────────────────────────────────────
905
+ with gr.Group(elem_id="kb-panel", elem_classes="kb-panel-wrap"):
906
+ gr.HTML("""
907
+ <div class="kb-panel-head">
908
+ <h2>πŸ“ Knowledge Base</h2>
909
+ <button class="kb-close-btn" onclick="closeKB()" title="Close">βœ•</button>
910
+ </div>
911
+ """)
912
+ with gr.Column(elem_classes="kb-panel-body"):
913
+ gr.HTML('<div class="kb-section-label">Files in index</div>')
914
+ kb_file_list = gr.HTML(value=list_kb_files)
915
+
916
+ gr.HTML('<div class="kb-section-label">Upload documents</div>')
917
+ gr.HTML('<div style="font-size:.75rem;color:var(--muted);margin-bottom:6px">.txt Β· .md Β· .pdf Β· .docx &nbsp;Β·&nbsp; max 10 MB each</div>')
918
+ upload_input = gr.File(
919
+ label="",
920
+ file_count="multiple",
921
+ file_types=[".txt", ".md", ".pdf", ".docx", ".doc"],
922
+ show_label=False,
923
+ )
924
+ upload_btn = gr.Button("⬆ Upload & Index", elem_classes="kb-upload-btn")
925
+ upload_status = gr.Markdown()
926
+
927
+ gr.HTML('<div class="kb-section-label" style="margin-top:18px">Delete a file</div>')
928
+ delete_input = gr.Textbox(
929
+ placeholder="Filename, e.g. guide.pdf",
930
+ show_label=False,
931
+ elem_id="delete-input",
932
+ )
933
+ delete_btn = gr.Button("πŸ—‘ Delete", elem_classes="kb-delete-btn", elem_id="delete-btn")
934
+ delete_status = gr.Markdown()
935
+
936
+ # ── Page header ────────────────────────────────────────────────────
937
  gr.HTML(f"""
938
  <div class="kb-header">
939
+ <div class="kb-icon">πŸ€–</div>
940
  <div>
941
  <h1>{CONFIG["client"]["name"]}</h1>
942
  <p>Search the knowledge base and get answers in plain language</p>
943
  </div>
944
+ <div class="kb-header-actions">
945
+ <button class="kb-toggle-btn" onclick="toggleKB()">
946
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
947
+ <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/>
948
+ <polyline points="14 2 14 8 20 8"/>
949
+ </svg>
950
+ Manage KB
951
+ </button>
952
+ </div>
953
  </div>
954
  """)
955
 
956
+ # ── Chat β€” full width, with empty state ───────────────────────────
957
+ with gr.Column():
958
 
959
+ # "Try asking" label + examples rendered above the chat interface
960
+ gr.HTML('<div class="try-label">✦ Try asking</div>')
961
 
962
+ gr.ChatInterface(
963
+ fn=chat_respond,
964
+ examples=examples,
965
+ cache_examples=False,
966
+ )
967
 
968
+ # Compact icon toolbar
969
+ gr.HTML(TOOLBAR_HTML)
970
 
971
+ # Hidden file input for toolbar attach button
972
+ toolbar_upload = gr.File(
973
+ label="",
974
+ file_count="multiple",
975
+ file_types=[".txt", ".md", ".pdf", ".docx", ".doc"],
976
+ visible=False,
977
+ elem_id="kb-upload-trigger",
978
+ )
979
+ toolbar_upload_status = gr.Markdown(visible=False)
980
+
981
+ # ── Event wiring ──────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
982
  upload_btn.click(
983
  upload_kb_files,
984
  inputs=[upload_input],