sofzcc commited on
Commit
bc66b8a
Β·
verified Β·
1 Parent(s): 0001faf

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +445 -391
app.py CHANGED
@@ -646,334 +646,427 @@ def delete_kb_file(filename: str) -> Tuple[str, str]:
646
  return f"❌ Could not delete `{filename}`: {e}", list_kb_files()
647
 
648
 
 
649
  # -----------------------------
650
  # GRADIO UI
651
  # -----------------------------
652
 
653
- # Build the KB file table as an HTML string (called on load + after every change)
654
- def kb_panel_html(file_table_html: str, status_msg: str = "") -> str:
655
- status_block = ""
656
- if status_msg:
657
- colour = "#065f46" if "βœ…" in status_msg else "#991b1b"
658
- bg = "#d1fae5" if "βœ…" in status_msg else "#fee2e2"
659
- status_block = (
660
- f"<div style='margin:8px 0;padding:8px 12px;background:{bg};color:{colour};"
661
- f"border-radius:8px;font-size:.81rem;line-height:1.5'>{status_msg}</div>"
662
- )
663
- return f"""
664
- <div id="kb-backdrop" class="kb-backdrop" onclick="closeKB()"></div>
665
- <div id="kb-panel" class="kb-panel-wrap">
666
- <div class="kb-panel-head">
667
- <span style="font-size:1rem;margin-right:8px">πŸ“</span>
668
- <h2>Knowledge Base</h2>
669
- <button class="kb-close-btn" onclick="closeKB()">βœ•</button>
670
- </div>
671
- <div class="kb-panel-body">
672
- <div class="kb-section-label">Files in index</div>
673
- {file_table_html}
674
- {status_block}
675
- </div>
676
- </div>
677
- """
678
-
679
- STYLE_HTML = """
680
- <style>
681
- :root{
682
- --sage:#6b8f71;--sage-l:#eaf2eb;--sage-d:#4f6e55;
683
- --sand:#f5f0e8;--cream:#fdfaf5;--stone:#e4ddd3;
684
- --ink:#2d2d2d;--muted:#7a7167;
685
- --rose:#c0717a;--rose-l:#f8eded;
686
- --r:14px;--sh:0 2px 10px rgba(0,0,0,.07);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
687
  }
688
- *{box-sizing:border-box}
689
- body,.gradio-container{
690
- background:var(--sand)!important;
691
- font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif!important;
692
- color:var(--ink)!important;
693
  }
694
- .gradio-container{max-width:900px!important;margin:0 auto!important;padding:0!important}
695
 
696
- /* ── Header ─────────────────────────────────────────── */
697
- .app-header{
698
- background:var(--cream);border-bottom:1px solid var(--stone);
699
- padding:14px 20px;display:flex;align-items:center;gap:12px;
 
700
  }
701
- .app-icon{
702
- width:38px;height:38px;background:var(--sage);border-radius:10px;
703
- display:flex;align-items:center;justify-content:center;font-size:1.15rem;flex-shrink:0;
704
  }
705
- .app-title{margin:0 0 1px;font-size:1.1rem;font-weight:650;color:var(--ink);letter-spacing:-.01em}
706
- .app-sub{margin:0;font-size:.77rem;color:var(--muted)}
707
- .manage-kb-btn{
708
- margin-left:auto;background:var(--cream);border:1.5px solid var(--stone);
709
- border-radius:8px;padding:6px 13px;font-size:.78rem;font-weight:500;
710
- color:var(--muted);cursor:pointer;display:flex;align-items:center;gap:5px;
711
- transition:background .15s,border-color .15s,color .15s;white-space:nowrap;
 
 
 
712
  }
713
- .manage-kb-btn:hover{background:var(--sage-l);border-color:var(--sage);color:var(--sage-d)}
714
-
715
- /* ── Chat bubbles ───────────────────────────────────── */
716
- .message.user>div{
717
- background:var(--sage)!important;color:#fff!important;
718
- border-radius:var(--r) var(--r) 4px var(--r)!important;
719
- padding:10px 15px!important;max-width:76%!important;
720
- font-size:.9rem!important;line-height:1.6!important;box-shadow:var(--sh)!important;
721
  }
722
- .message.bot>div{
723
- background:var(--cream)!important;border:1px solid var(--stone)!important;
724
- border-radius:var(--r) var(--r) var(--r) 4px!important;
725
- padding:12px 16px!important;max-width:80%!important;
726
- font-size:.9rem!important;line-height:1.65!important;
727
- box-shadow:var(--sh)!important;color:var(--ink)!important;
728
- }
729
- /* Hide default Gradio chatbot label */
730
- .label-wrap,.chatbot-label,label.svelte-1b6s6vi{display:none!important}
731
 
732
- /* ── "Try asking" + example pills ───────────────────── */
733
- .try-label{
734
- font-size:.7rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;
735
- color:var(--muted);padding:10px 14px 4px;background:var(--cream);
736
- }
737
- .examples-table{
738
- display:flex!important;flex-wrap:wrap!important;gap:6px!important;
739
- padding:4px 14px 10px!important;background:var(--cream);
740
- border-bottom:1px solid var(--stone)!important;
741
- }
742
- /* flatten table cells so pills flow naturally */
743
- .examples-table table,.examples-table tbody,
744
- .examples-table tr,.examples-table td{
745
- display:contents!important;border:none!important;background:none!important;
746
  }
747
- .examples-table button,.example{
748
- display:inline-flex!important;align-items:center!important;
749
- width:auto!important;white-space:normal!important;
750
- border-radius:999px!important;border:1.5px solid var(--stone)!important;
751
- color:var(--sage-d)!important;background:var(--cream)!important;
752
- padding:5px 13px!important;font-size:.78rem!important;font-weight:500!important;
753
- line-height:1.4!important;cursor:pointer!important;
754
- transition:all .15s!important;box-shadow:0 1px 2px rgba(0,0,0,.05)!important;
755
  }
756
- .examples-table button:hover,.example:hover{
757
- background:var(--sage)!important;color:#fff!important;border-color:var(--sage)!important;
 
 
 
 
 
 
758
  }
 
759
 
760
- /* ── Input bar ───────────────────────────────────────── */
761
- .chat-input-container{
762
- background:var(--cream)!important;border-top:1px solid var(--stone)!important;
763
- padding:8px 12px!important;
764
  }
765
- .chat-input-container textarea{
766
- background:var(--sand)!important;border:1.5px solid var(--stone)!important;
767
- border-radius:10px!important;padding:9px 13px!important;
768
- font-size:.88rem!important;color:var(--ink)!important;
769
- resize:none!important;transition:border-color .18s,box-shadow .18s!important;
 
 
770
  }
771
- .chat-input-container textarea:focus{
772
- border-color:var(--sage)!important;outline:none!important;
773
- box-shadow:0 0 0 3px rgba(107,143,113,.16)!important;
 
 
 
 
 
774
  }
775
- .chat-input-container textarea::placeholder{color:var(--muted)!important}
776
- .chat-input-container button[aria-label="Send"]{
777
- background:var(--sage)!important;color:#fff!important;border:none!important;
778
- border-radius:9px!important;padding:0 13px!important;height:36px!important;
779
- font-weight:600!important;transition:background .15s!important;
 
780
  }
781
- .chat-input-container button[aria-label="Send"]:hover{background:var(--sage-d)!important}
782
 
783
- /* ── Compact toolbar ─────────────────────────────────── */
784
- .tb-bar{
785
- display:flex;align-items:center;gap:2px;
786
- padding:4px 12px 5px;background:var(--cream);border-top:1px solid var(--stone);
 
 
 
 
 
 
787
  }
788
- .tb-btn{
789
- width:26px;height:26px;display:inline-flex;align-items:center;justify-content:center;
790
- background:none;border:none;border-radius:5px;cursor:pointer;color:var(--muted);
791
- transition:background .13s,color .13s;position:relative;flex-shrink:0;
792
  }
793
- .tb-btn:hover{background:var(--sage-l);color:var(--sage-d)}
794
- .tb-btn svg{width:13px;height:13px;pointer-events:none}
795
- .tb-sep{width:1px;height:14px;background:var(--stone);margin:0 2px;flex-shrink:0}
796
- .tb-btn::after{
797
- content:attr(data-tip);position:absolute;bottom:calc(100% + 5px);left:50%;
798
- transform:translateX(-50%);background:rgba(30,30,30,.9);color:#fff;
799
- font-size:.65rem;padding:3px 7px;border-radius:4px;white-space:nowrap;
800
- pointer-events:none;opacity:0;transition:opacity .12s;z-index:99;
801
  }
802
- .tb-btn:hover::after{opacity:1}
803
 
804
- /* ── KB slide-in panel ─���─────────────────────────────── */
805
- .kb-backdrop{
806
- display:none;position:fixed;inset:0;
807
- background:rgba(45,45,45,.35);z-index:1000;backdrop-filter:blur(1px);
 
808
  }
809
- .kb-backdrop.open{display:block}
810
- .kb-panel-wrap{
811
- position:fixed;top:0;right:0;bottom:0;width:360px;max-width:94vw;
812
- background:var(--cream);border-left:1px solid var(--stone);
813
- box-shadow:-6px 0 28px rgba(0,0,0,.12);z-index:1001;
814
- transform:translateX(110%);transition:transform .28s cubic-bezier(.4,0,.2,1);
815
- display:flex;flex-direction:column;overflow:hidden;
816
  }
817
- .kb-panel-wrap.open{transform:translateX(0)}
818
- .kb-panel-head{
819
- display:flex;align-items:center;padding:14px 16px;
820
- border-bottom:1px solid var(--stone);background:var(--sand);flex-shrink:0;gap:8px;
 
 
 
 
821
  }
822
- .kb-panel-head h2{margin:0;font-size:.92rem;font-weight:650;color:var(--ink);flex:1}
823
- .kb-close-btn{
824
- background:none;border:none;cursor:pointer;color:var(--muted);
825
- width:26px;height:26px;border-radius:5px;font-size:.9rem;
826
- display:flex;align-items:center;justify-content:center;transition:background .13s;
 
827
  }
828
- .kb-close-btn:hover{background:var(--stone)}
829
- .kb-panel-body{flex:1;overflow-y:auto;padding:14px 14px 24px}
830
- .kb-section-label{
831
- font-size:.68rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;
832
- color:var(--muted);margin:0 0 8px;
833
  }
834
- .kb-section-label+*{margin-top:0}
835
- .kb-divider{height:1px;background:var(--stone);margin:16px 0}
836
-
837
- /* File table */
838
- .kb-file-table{width:100%;border-collapse:collapse;font-size:.8rem}
839
- .kb-file-table th{
840
- background:var(--sand);padding:6px 8px;text-align:left;
841
- font-weight:600;font-size:.7rem;color:var(--muted);
842
- border-bottom:1px solid var(--stone);
843
  }
844
- .kb-file-table td{padding:7px 8px;border-bottom:1px solid var(--stone);color:var(--ink)}
845
- .kb-file-table tr:last-child td{border-bottom:none}
846
- .kb-del-btn{
847
- background:none;border:none;cursor:pointer;color:var(--rose);
848
- padding:2px 5px;border-radius:4px;font-size:.85rem;transition:background .13s;
 
 
 
849
  }
850
- .kb-del-btn:hover{background:var(--rose-l)}
851
-
852
- /* Upload zone */
853
- .kb-upload-zone{
854
- border:2px dashed var(--stone);border-radius:10px;background:var(--sand);
855
- padding:20px;text-align:center;font-size:.8rem;color:var(--muted);
856
- transition:border-color .18s,background .18s;cursor:pointer;margin:6px 0 10px;
857
  }
858
- .kb-upload-zone:hover{border-color:var(--sage);background:var(--sage-l)}
859
- .gr-file-drop,[data-testid="file"]{
860
- border:none!important;border-radius:10px!important;
861
- background:transparent!important;margin:0!important;
862
  }
 
863
 
864
- /* KB action buttons */
865
- .kb-upload-btn>button,.kb-upload-btn button{
866
- background:var(--sage)!important;color:#fff!important;border:none!important;
867
- border-radius:8px!important;padding:8px 0!important;font-weight:600!important;
868
- font-size:.83rem!important;width:100%!important;transition:background .15s!important;
869
- cursor:pointer!important;
870
- }
871
- .kb-upload-btn>button:hover,.kb-upload-btn button:hover{background:var(--sage-d)!important}
872
- .kb-delete-btn>button,.kb-delete-btn button{
873
- background:var(--rose-l)!important;color:var(--rose)!important;
874
- border:1px solid #e8b4b8!important;border-radius:8px!important;
875
- padding:7px 0!important;font-weight:600!important;font-size:.83rem!important;
876
- width:100%!important;transition:all .15s!important;cursor:pointer!important;
877
  }
878
- .kb-delete-btn>button:hover,.kb-delete-btn button:hover{
879
- background:var(--rose)!important;color:#fff!important;
 
 
 
880
  }
881
- .kb-textbox input,.kb-textbox textarea{
882
- border:1.5px solid var(--stone)!important;border-radius:8px!important;
883
- padding:7px 11px!important;font-size:.82rem!important;
884
- background:var(--sand)!important;color:var(--ink)!important;
885
- transition:border-color .15s!important;
886
  }
887
- .kb-textbox input:focus,.kb-textbox textarea:focus{
888
- border-color:var(--sage)!important;outline:none!important;
889
- box-shadow:0 0 0 3px rgba(107,143,113,.14)!important;
 
 
890
  }
 
891
 
892
- /* Status messages */
893
- .kb-status-ok{
894
- background:#d1fae5;color:#065f46;border-radius:7px;
895
- padding:6px 11px;font-size:.79rem;margin-top:6px;line-height:1.5;
896
  }
897
- .kb-status-err{
898
- background:#fee2e2;color:#991b1b;border-radius:7px;
899
- padding:6px 11px;font-size:.79rem;margin-top:6px;line-height:1.5;
 
 
900
  }
901
-
902
- /* Empty state */
903
- .kb-empty{
904
- text-align:center;padding:24px 12px;color:var(--muted);
905
  }
906
- .kb-empty-icon{font-size:2rem;opacity:.45;margin-bottom:6px}
907
- .kb-empty-text{font-size:.8rem;line-height:1.5}
908
-
909
- @media(max-width:640px){
910
- .gradio-container{padding:0!important}
911
- .app-header{padding:10px 12px!important}
912
- .kb-panel-wrap{width:100%;max-width:100vw}
913
- .message.user>div,.message.bot>div{max-width:95%!important}
914
- .examples-table{gap:5px!important;padding:3px 10px 8px!important}
915
  }
 
 
 
 
 
 
 
 
 
 
916
  </style>
917
 
918
  <script>
919
- function toggleKB(){
920
- document.getElementById("kb-panel").classList.toggle("open");
921
- document.getElementById("kb-backdrop").classList.toggle("open");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
922
  }
923
- function closeKB(){
924
- document.getElementById("kb-panel").classList.remove("open");
925
- document.getElementById("kb-backdrop").classList.remove("open");
 
 
 
 
926
  }
927
- function kbDeleteFile(name){
928
- // Fill the hidden Gradio textbox and click the hidden delete button
929
- var inputs = document.querySelectorAll("#kb-delete-input input, #kb-delete-input textarea");
930
- inputs.forEach(function(el){
931
- el.value = name;
932
- el.dispatchEvent(new Event("input", {bubbles:true}));
933
- el.dispatchEvent(new Event("change", {bubbles:true}));
934
- });
935
- setTimeout(function(){
936
- var btn = document.querySelector("#kb-delete-btn button, #kb-delete-btn");
937
- if(btn) btn.click();
938
- }, 80);
939
  }
940
- </script>
941
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
942
 
943
  TOOLBAR_HTML = """
944
  <div class="tb-bar">
945
- <button class="tb-btn" data-tip="Attach file to KB"
946
- onclick="document.getElementById('kb-file-attach').click()">
947
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
948
- <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"/>
949
- </svg>
950
  </button>
951
- <button class="tb-btn" data-tip="Paste from clipboard"
952
- onclick="(function(){var t=document.querySelector('.chat-input-container textarea');if(!t)return;navigator.clipboard.readText().then(function(s){var i=t.selectionStart;t.value=t.value.slice(0,i)+s+t.value.slice(t.selectionEnd);t.selectionStart=t.selectionEnd=i+s.length;t.dispatchEvent(new Event('input',{bubbles:true}))}).catch(function(){t.focus()})})()">
953
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
954
- <rect x="8" y="2" width="8" height="4" rx="1"/>
955
- <path d="M16 4h2a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2h2"/>
956
- </svg>
957
  </button>
958
  <div class="tb-sep"></div>
959
- <button class="tb-btn" data-tip="Bold"
960
- onclick="(function(){var t=document.querySelector('.chat-input-container textarea');if(!t)return;var s=t.selectionStart,e=t.selectionEnd;if(s===e)return;var v=t.value;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}))})()">
961
  <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>
962
  </button>
963
- <button class="tb-btn" data-tip="Italic"
964
- onclick="(function(){var t=document.querySelector('.chat-input-container textarea');if(!t)return;var s=t.selectionStart,e=t.selectionEnd;if(s===e)return;var v=t.value;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}))})()">
965
  <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>
966
  </button>
967
- <button class="tb-btn" data-tip="Bullet list"
968
- onclick="(function(){var t=document.querySelector('.chat-input-container textarea');if(!t)return;var lines=t.value.split('\\n'),idx=t.value.slice(0,t.selectionStart).split('\\n').length-1;lines[idx]='- '+lines[idx];t.value=lines.join('\\n');t.dispatchEvent(new Event('input',{bubbles:true}))})()">
969
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="9" y1="6" x2="20" y2="6"/><line x1="9" y1="12" x2="20" y2="12"/><line x1="9" y1="18" x2="20" y2="18"/><circle cx="4" cy="6" r="1.5" fill="currentColor" stroke="none"/><circle cx="4" cy="12" r="1.5" fill="currentColor" stroke="none"/><circle cx="4" cy="18" r="1.5" fill="currentColor" stroke="none"/></svg>
970
  </button>
971
- <button class="tb-btn" data-tip="Code"
972
- onclick="(function(){var t=document.querySelector('.chat-input-container textarea');if(!t)return;var s=t.selectionStart,e=t.selectionEnd;if(s===e)return;var v=t.value;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}))})()">
973
  <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>
974
  </button>
975
- </div>
976
- """
977
 
978
  examples = [
979
  q["query"] for q in CONFIG.get("quick_actions", []) if "query" in q
@@ -986,140 +1079,101 @@ examples = [
986
  with gr.Blocks(title=CONFIG["client"]["name"]) as demo:
987
 
988
  gr.HTML(STYLE_HTML)
 
989
 
990
- # ── App header ────────────────────────────────────────────────────
991
  gr.HTML(f"""
992
- <div class="app-header">
993
- <div class="app-icon">πŸ€–</div>
994
  <div>
995
- <p class="app-title">{CONFIG["client"]["name"]}</p>
996
- <p class="app-sub">Search the knowledge base and get answers in plain language</p>
997
- </div>
998
- <button class="manage-kb-btn" onclick="toggleKB()">
999
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
1000
- <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/>
1001
- <polyline points="14 2 14 8 20 8"/>
1002
- </svg>
1003
- Manage KB
1004
- </button>
1005
- </div>
1006
- """)
1007
-
1008
- # ── KB slide panel β€” rendered as pure HTML, outside Gradio layout ─
1009
- kb_panel_block = gr.HTML(value=list_kb_files) # hidden, just holds current table
1010
- gr.HTML("""
1011
- <div id="kb-backdrop" class="kb-backdrop" onclick="closeKB()"></div>
1012
- <div id="kb-panel" class="kb-panel-wrap">
1013
- <div class="kb-panel-head">
1014
- <span>πŸ“</span>
1015
- <h2>Knowledge Base</h2>
1016
- <button class="kb-close-btn" onclick="closeKB()">βœ•</button>
1017
  </div>
1018
- <div class="kb-panel-body" id="kb-panel-body">
1019
- <!-- File list injected by Gradio below -->
1020
- </div>
1021
- </div>
1022
- """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1023
 
1024
- # ── "Try asking" label rendered above ChatInterface ───────────────
1025
- gr.HTML('<div class="try-label">✦ Try asking</div>')
 
1026
 
1027
- # ── Main chat ─────────────────────────────────────────────────────
1028
- gr.ChatInterface(
1029
- fn=chat_respond,
1030
- examples=examples,
1031
- cache_examples=False,
1032
- )
1033
 
1034
- # ── Compact toolbar ───────────────────────────────────────────────
1035
- gr.HTML(TOOLBAR_HTML)
1036
-
1037
- # ── KB Manager widgets (visible in panel via CSS overlay) ─────────
1038
- # These are real Gradio widgets positioned inside the panel via JS injection.
1039
- with gr.Column(visible=True) as kb_widgets:
1040
- with gr.Group():
1041
- gr.HTML('<div class="kb-section-label" style="margin-top:0">Files in index</div>')
1042
- kb_file_list = gr.HTML(value=list_kb_files)
1043
- gr.HTML('<div class="kb-divider"></div>')
1044
- gr.HTML('<div class="kb-section-label">Upload documents</div>')
1045
- gr.HTML('<p style="font-size:.74rem;color:var(--muted);margin:0 0 6px">.txt Β· .md Β· .pdf Β· .docx &nbsp;Β·&nbsp; max 10 MB</p>')
1046
- upload_input = gr.File(
1047
- label="",
1048
- file_count="multiple",
1049
- file_types=[".txt", ".md", ".pdf", ".docx", ".doc"],
1050
- show_label=False,
1051
- )
1052
- upload_btn = gr.Button("⬆ Upload & Index", elem_classes="kb-upload-btn")
1053
- upload_status = gr.Markdown()
1054
- gr.HTML('<div class="kb-divider"></div>')
1055
- gr.HTML('<div class="kb-section-label">Delete a file</div>')
1056
- delete_input = gr.Textbox(
1057
- placeholder="Filename, e.g. guide.pdf",
1058
- show_label=False,
1059
- elem_id="kb-delete-input",
1060
- elem_classes="kb-textbox",
1061
- )
1062
- delete_btn = gr.Button("πŸ—‘ Delete file", elem_classes="kb-delete-btn", elem_id="kb-delete-btn")
1063
- delete_status = gr.Markdown()
1064
-
1065
- # ── Hidden file input for toolbar attach ─────────────────────────
1066
- toolbar_upload = gr.File(
1067
- label="",
1068
- file_count="multiple",
1069
- file_types=[".txt", ".md", ".pdf", ".docx", ".doc"],
1070
- visible=False,
1071
- elem_id="kb-file-attach",
1072
- )
1073
- toolbar_upload_status = gr.Markdown(visible=False)
1074
-
1075
- # ── JS: move KB widgets into the slide panel after render ─────────
1076
- gr.HTML("""
1077
- <script>
1078
- (function moveKBWidgets(){
1079
- var target = document.getElementById("kb-panel-body");
1080
- // Walk up from kb-delete-btn to find the Gradio column wrapper
1081
- function tryMove(){
1082
- var btn = document.getElementById("kb-delete-btn");
1083
- if(!btn || !target) { setTimeout(tryMove, 300); return; }
1084
- // Find the wrapping gradio column (go up ~6 levels)
1085
- var el = btn;
1086
- for(var i=0;i<8;i++){
1087
- if(!el.parentElement) break;
1088
- el = el.parentElement;
1089
- if(el.classList.contains("block") && el.parentElement &&
1090
- el.parentElement.classList.contains("block")){
1091
- target.appendChild(el.parentElement);
1092
- return;
1093
- }
1094
- }
1095
- // Fallback: move the direct block container
1096
- target.appendChild(el);
1097
- }
1098
- if(document.readyState === "loading"){
1099
- document.addEventListener("DOMContentLoaded", function(){ setTimeout(tryMove, 500); });
1100
- } else {
1101
- setTimeout(tryMove, 500);
1102
- }
1103
- })();
1104
- </script>
1105
- """)
1106
-
1107
- # ── Event wiring ─────────────────────────────────────────────────
1108
- upload_btn.click(
1109
- upload_kb_files,
1110
- inputs=[upload_input],
1111
- outputs=[upload_status, kb_file_list],
1112
- )
1113
- delete_btn.click(
1114
- delete_kb_file,
1115
- inputs=[delete_input],
1116
- outputs=[delete_status, kb_file_list],
1117
- )
1118
- toolbar_upload.change(
1119
- upload_kb_files,
1120
- inputs=[toolbar_upload],
1121
- outputs=[toolbar_upload_status, kb_file_list],
1122
- )
1123
 
1124
 
1125
  # -----------------------------
 
646
  return f"❌ Could not delete `{filename}`: {e}", list_kb_files()
647
 
648
 
649
+
650
  # -----------------------------
651
  # GRADIO UI
652
  # -----------------------------
653
 
654
+ def list_kb_files_html() -> str:
655
+ """Return a clean file list as HTML β€” filename only, no size, with hover actions menu."""
656
+ patterns = ["*.txt", "*.md", "*.pdf", "*.docx", "*.doc"]
657
+ paths: List[str] = []
658
+ for pat in patterns:
659
+ paths.extend(glob.glob(os.path.join(KB_DIR, pat)))
660
+ paths = sorted(paths)
661
+
662
+ if not paths:
663
+ return """
664
+ <div style="text-align:center;padding:32px 12px;color:#9e9790">
665
+ <div style="font-size:1.8rem;margin-bottom:8px;opacity:.5">πŸ“­</div>
666
+ <div style="font-size:.8rem;line-height:1.6">No files yet.<br>Click <strong>+ Add file</strong> above.</div>
667
+ </div>"""
668
+
669
+ ext_icons = {".pdf": "πŸ“„", ".docx": "πŸ“", ".doc": "πŸ“", ".md": "πŸ“‹", ".txt": "πŸ“ƒ"}
670
+ items = ""
671
+ for p in paths:
672
+ name = os.path.basename(p)
673
+ ext = os.path.splitext(name)[1].lower()
674
+ icon = ext_icons.get(ext, "πŸ“„")
675
+ safe = name.replace("'", "\\'").replace('"', '&quot;')
676
+ items += f"""
677
+ <div class="kb-file-row" id="row-{safe}">
678
+ <span class="kb-file-icon">{icon}</span>
679
+ <span class="kb-file-name" title="{safe}">{name}</span>
680
+ <div class="kb-file-actions">
681
+ <button class="kb-action-btn" title="Delete"
682
+ onclick="kbAction('delete','{safe}')">πŸ—‘</button>
683
+ <button class="kb-action-btn" title="Replace"
684
+ onclick="kbAction('replace','{safe}')">β†Ί</button>
685
+ </div>
686
+ </div>"""
687
+ return items
688
+
689
+
690
+ def rename_kb_file(old_name: str, new_name: str) -> Tuple[str, str]:
691
+ old_name = os.path.basename(old_name.strip())
692
+ new_name = os.path.basename(new_name.strip())
693
+ if not old_name or not new_name:
694
+ return "⚠️ Please provide both names.", list_kb_files_html()
695
+ if os.path.splitext(new_name)[1].lower() not in ALLOWED_EXTENSIONS:
696
+ return "⚠️ New name must have a supported extension.", list_kb_files_html()
697
+ src = os.path.join(KB_DIR, old_name)
698
+ dst = os.path.join(KB_DIR, new_name)
699
+ if not os.path.exists(src):
700
+ return f"⚠️ File not found: {old_name}", list_kb_files_html()
701
+ try:
702
+ os.rename(src, dst)
703
+ msg = kb_index.rebuild()
704
+ return f"βœ… Renamed to `{new_name}`. {msg}", list_kb_files_html()
705
+ except Exception as e:
706
+ return f"❌ Could not rename: {e}", list_kb_files_html()
707
+
708
+
709
+ # ── CSS ────────────────────────────────────────────────────────────────────────
710
+
711
+ STYLE_HTML = """<style>
712
+ :root {
713
+ --sage:#6b8f71; --sage-l:#eaf2eb; --sage-d:#4f6e55;
714
+ --sand:#f5f0e8; --cream:#fdfaf5; --stone:#e4ddd3; --stone2:#d4cdc3;
715
+ --ink:#2d2d2d; --muted:#7a7167; --muted2:#a09890;
716
+ --rose:#c0717a; --rose-l:#f8eded;
717
+ --r:12px; --sh:0 2px 12px rgba(0,0,0,.08);
718
  }
719
+ *, *::before, *::after { box-sizing: border-box; }
720
+ body, .gradio-container {
721
+ background: var(--sand) !important;
722
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
723
+ color: var(--ink) !important;
724
  }
725
+ .gradio-container { padding: 0 !important; max-width: 100% !important; }
726
 
727
+ /* ── Header ─────────────────────────────────────────────────── */
728
+ .app-hdr {
729
+ background: var(--cream); border-bottom: 1px solid var(--stone);
730
+ padding: 13px 20px; display: flex; align-items: center; gap: 12px;
731
+ position: sticky; top: 0; z-index: 10;
732
  }
733
+ .app-hdr-icon {
734
+ width: 36px; height: 36px; background: var(--sage); border-radius: 9px;
735
+ display: flex; align-items: center; justify-content: center; font-size: 1.1rem; flex-shrink: 0;
736
  }
737
+ .app-hdr h1 { margin: 0 0 1px; font-size: 1rem; font-weight: 650; color: var(--ink); }
738
+ .app-hdr p { margin: 0; font-size: .74rem; color: var(--muted); }
739
+
740
+ /* ── 3-column layout ─────────────────────────────────────────── */
741
+ .app-body {
742
+ display: grid;
743
+ grid-template-columns: 200px 1fr 220px;
744
+ gap: 0;
745
+ height: calc(100vh - 64px);
746
+ overflow: hidden;
747
  }
748
+ @media(max-width: 860px) {
749
+ .app-body { grid-template-columns: 1fr; height: auto; overflow: visible; }
750
+ .left-col, .right-col { display: none; }
 
 
 
 
 
751
  }
 
 
 
 
 
 
 
 
 
752
 
753
+ /* ── Left: suggested questions ───────────────────────────────── */
754
+ .left-col {
755
+ border-right: 1px solid var(--stone); background: var(--cream);
756
+ display: flex; flex-direction: column; overflow: hidden;
 
 
 
 
 
 
 
 
 
 
757
  }
758
+ .left-col-hdr {
759
+ padding: 14px 14px 8px; font-size: .68rem; font-weight: 700;
760
+ letter-spacing: .06em; text-transform: uppercase; color: var(--muted);
761
+ border-bottom: 1px solid var(--stone); flex-shrink: 0;
 
 
 
 
762
  }
763
+ .left-col-body { overflow-y: auto; padding: 10px 10px; flex: 1; }
764
+ .suggestion-pill {
765
+ display: block; width: 100%; text-align: left;
766
+ background: transparent; border: 1.5px solid var(--stone);
767
+ border-radius: 999px; padding: 7px 13px;
768
+ font-size: .77rem; color: var(--sage-d); cursor: pointer; margin-bottom: 7px;
769
+ transition: background .15s, border-color .15s, color .15s;
770
+ line-height: 1.4;
771
  }
772
+ .suggestion-pill:hover { background: var(--sage); border-color: var(--sage); color: #fff; }
773
 
774
+ /* ── Centre: chat ────────────────────────────────────────────── */
775
+ .centre-col {
776
+ display: flex; flex-direction: column; overflow: hidden;
777
+ background: var(--sand);
778
  }
779
+
780
+ /* Kill Gradio chrome */
781
+ .label-wrap, .chatbot-label, label.svelte-1b6s6vi,
782
+ .gr-prose .label-wrap, [data-testid="chatbot"] > .label-wrap { display: none !important; }
783
+ .gr-chatbot, [data-testid="chatbot"] {
784
+ flex: 1 !important; height: 100% !important;
785
+ border: none !important; border-radius: 0 !important; background: transparent !important;
786
  }
787
+ footer { display: none !important; }
788
+
789
+ /* Bubbles */
790
+ .message.user > div {
791
+ background: var(--sage) !important; color: #fff !important;
792
+ border-radius: var(--r) var(--r) 4px var(--r) !important;
793
+ padding: 9px 14px !important; max-width: 75% !important;
794
+ font-size: .88rem !important; line-height: 1.6 !important; box-shadow: var(--sh) !important;
795
  }
796
+ .message.bot > div {
797
+ background: var(--cream) !important; border: 1px solid var(--stone) !important;
798
+ border-radius: var(--r) var(--r) var(--r) 4px !important;
799
+ padding: 11px 15px !important; max-width: 80% !important;
800
+ font-size: .88rem !important; line-height: 1.65 !important;
801
+ box-shadow: var(--sh) !important; color: var(--ink) !important;
802
  }
 
803
 
804
+ /* Input bar */
805
+ .chat-input-container {
806
+ background: var(--cream) !important; border-top: 1px solid var(--stone) !important;
807
+ padding: 8px 12px !important; flex-shrink: 0;
808
+ }
809
+ .chat-input-container textarea {
810
+ background: var(--sand) !important; border: 1.5px solid var(--stone) !important;
811
+ border-radius: 10px !important; padding: 9px 13px !important;
812
+ font-size: .88rem !important; color: var(--ink) !important;
813
+ resize: none !important; transition: border-color .18s, box-shadow .18s !important;
814
  }
815
+ .chat-input-container textarea:focus {
816
+ border-color: var(--sage) !important; outline: none !important;
817
+ box-shadow: 0 0 0 3px rgba(107,143,113,.16) !important;
 
818
  }
819
+ .chat-input-container textarea::placeholder { color: var(--muted2) !important; }
820
+ .chat-input-container button[aria-label="Send"] {
821
+ background: var(--sage) !important; color: #fff !important; border: none !important;
822
+ border-radius: 9px !important; padding: 0 13px !important; height: 36px !important;
823
+ font-weight: 600 !important; transition: background .15s !important;
 
 
 
824
  }
825
+ .chat-input-container button[aria-label="Send"]:hover { background: var(--sage-d) !important; }
826
 
827
+ /* Compact toolbar */
828
+ .tb-bar {
829
+ display: flex; align-items: center; gap: 2px;
830
+ padding: 4px 12px 5px; background: var(--cream); border-top: 1px solid var(--stone);
831
+ flex-shrink: 0;
832
  }
833
+ .tb-btn {
834
+ width: 26px; height: 26px; display: inline-flex; align-items: center; justify-content: center;
835
+ background: none; border: none; border-radius: 5px; cursor: pointer; color: var(--muted);
836
+ transition: background .13s, color .13s; position: relative; flex-shrink: 0;
 
 
 
837
  }
838
+ .tb-btn:hover { background: var(--sage-l); color: var(--sage-d); }
839
+ .tb-btn svg { width: 13px; height: 13px; pointer-events: none; }
840
+ .tb-sep { width: 1px; height: 14px; background: var(--stone); margin: 0 2px; flex-shrink: 0; }
841
+ .tb-btn::after {
842
+ content: attr(data-tip); position: absolute; bottom: calc(100% + 5px); left: 50%;
843
+ transform: translateX(-50%); background: rgba(30,30,30,.9); color: #fff;
844
+ font-size: .65rem; padding: 3px 7px; border-radius: 4px; white-space: nowrap;
845
+ pointer-events: none; opacity: 0; transition: opacity .12s; z-index: 99;
846
  }
847
+ .tb-btn:hover::after { opacity: 1; }
848
+
849
+ /* ── Right: file list ────────────────────────────────────────── */
850
+ .right-col {
851
+ border-left: 1px solid var(--stone); background: var(--cream);
852
+ display: flex; flex-direction: column; overflow: hidden;
853
  }
854
+ .right-col-hdr {
855
+ padding: 12px 14px 10px; border-bottom: 1px solid var(--stone);
856
+ flex-shrink: 0; display: flex; align-items: center; justify-content: space-between;
 
 
857
  }
858
+ .right-col-title {
859
+ font-size: .68rem; font-weight: 700; letter-spacing: .06em;
860
+ text-transform: uppercase; color: var(--muted);
 
 
 
 
 
 
861
  }
862
+ /* + Add file button */
863
+ .add-file-btn-wrap { position: relative; display: inline-block; }
864
+ .add-file-btn {
865
+ background: var(--sage-l); border: 1.5px solid var(--sage);
866
+ color: var(--sage-d); border-radius: 7px; padding: 4px 10px;
867
+ font-size: .74rem; font-weight: 600; cursor: pointer;
868
+ transition: background .15s, color .15s; white-space: nowrap;
869
+ display: flex; align-items: center; gap: 4px;
870
  }
871
+ .add-file-btn:hover { background: var(--sage); color: #fff; }
872
+ /* Tooltip for add-file */
873
+ .add-file-tooltip {
874
+ display: none; position: absolute; top: calc(100% + 7px); right: 0;
875
+ background: var(--ink); color: #fff; border-radius: 8px; padding: 9px 12px;
876
+ font-size: .73rem; line-height: 1.6; white-space: nowrap; z-index: 50;
877
+ box-shadow: 0 4px 16px rgba(0,0,0,.18); min-width: 200px;
878
  }
879
+ .add-file-tooltip::before {
880
+ content: ""; position: absolute; top: -5px; right: 14px;
881
+ border-left: 5px solid transparent; border-right: 5px solid transparent;
882
+ border-bottom: 5px solid var(--ink);
883
  }
884
+ .add-file-btn-wrap:hover .add-file-tooltip { display: block; }
885
 
886
+ .right-col-body { overflow-y: auto; padding: 6px 8px; flex: 1; }
887
+
888
+ /* File rows */
889
+ .kb-file-row {
890
+ display: flex; align-items: center; gap: 7px;
891
+ padding: 7px 8px; border-radius: 8px; cursor: default;
892
+ transition: background .13s; position: relative;
 
 
 
 
 
 
893
  }
894
+ .kb-file-row:hover { background: var(--sand); }
895
+ .kb-file-icon { font-size: .85rem; flex-shrink: 0; }
896
+ .kb-file-name {
897
+ flex: 1; font-size: .78rem; color: var(--ink);
898
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
899
  }
900
+ .kb-file-actions {
901
+ display: none; gap: 2px; align-items: center; flex-shrink: 0;
 
 
 
902
  }
903
+ .kb-file-row:hover .kb-file-actions { display: flex; }
904
+ .kb-action-btn {
905
+ background: none; border: none; cursor: pointer; font-size: .8rem;
906
+ padding: 2px 4px; border-radius: 4px; color: var(--muted);
907
+ transition: background .12s, color .12s;
908
  }
909
+ .kb-action-btn:hover { background: var(--stone); color: var(--ink); }
910
 
911
+ /* Rename inline input */
912
+ .kb-rename-row {
913
+ display: none; padding: 6px 8px; gap: 6px; align-items: center;
 
914
  }
915
+ .kb-rename-row.active { display: flex; }
916
+ .kb-rename-input {
917
+ flex: 1; border: 1.5px solid var(--sage); border-radius: 6px;
918
+ padding: 4px 8px; font-size: .76rem; background: var(--sand);
919
+ color: var(--ink); outline: none;
920
  }
921
+ .kb-rename-save {
922
+ background: var(--sage); color: #fff; border: none; border-radius: 6px;
923
+ padding: 4px 9px; font-size: .73rem; font-weight: 600; cursor: pointer;
 
924
  }
925
+ .kb-rename-cancel {
926
+ background: none; border: 1px solid var(--stone); border-radius: 6px;
927
+ padding: 4px 7px; font-size: .73rem; color: var(--muted); cursor: pointer;
 
 
 
 
 
 
928
  }
929
+
930
+ /* Status toast */
931
+ .kb-toast {
932
+ position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
933
+ background: var(--ink); color: #fff; border-radius: 8px;
934
+ padding: 8px 18px; font-size: .8rem; z-index: 999;
935
+ opacity: 0; pointer-events: none; transition: opacity .2s;
936
+ white-space: nowrap;
937
+ }
938
+ .kb-toast.show { opacity: 1; }
939
  </style>
940
 
941
  <script>
942
+ /* ── KB action dispatcher ──────────────────────── */
943
+ function kbAction(action, filename) {
944
+ if (action === 'delete') {
945
+ if (!confirm('Delete ' + filename + '?')) return;
946
+ setGradioInput('kb-hidden-delete', filename);
947
+ setTimeout(function(){ clickGradioBtn('kb-delete-trigger'); }, 80);
948
+ } else if (action === 'replace') {
949
+ document.getElementById('kb-file-attach').click();
950
+ // Store target for replacement note
951
+ window._kbReplaceTarget = filename;
952
+ } else if (action === 'rename') {
953
+ var row = document.getElementById('row-' + filename);
954
+ var rrow = document.getElementById('rrow-' + filename);
955
+ if (row) row.style.display = 'none';
956
+ if (rrow) { rrow.classList.add('active'); rrow.querySelector('input').focus(); }
957
+ }
958
+ }
959
+ function kbCancelRename(filename) {
960
+ var row = document.getElementById('row-' + filename);
961
+ var rrow = document.getElementById('rrow-' + filename);
962
+ if (row) row.style.display = '';
963
+ if (rrow) rrow.classList.remove('active');
964
  }
965
+ function kbSaveRename(oldName) {
966
+ var rrow = document.getElementById('rrow-' + oldName);
967
+ var input = rrow ? rrow.querySelector('input') : null;
968
+ if (!input || !input.value.trim()) return;
969
+ setGradioInput('kb-hidden-old-name', oldName);
970
+ setGradioInput('kb-hidden-new-name', input.value.trim());
971
+ setTimeout(function(){ clickGradioBtn('kb-rename-trigger'); }, 80);
972
  }
973
+ function setGradioInput(elemId, value) {
974
+ var el = document.querySelector('#' + elemId + ' input, #' + elemId + ' textarea');
975
+ if (!el) return;
976
+ el.value = value;
977
+ el.dispatchEvent(new Event('input', { bubbles: true }));
978
+ el.dispatchEvent(new Event('change', { bubbles: true }));
 
 
 
 
 
 
979
  }
980
+ function clickGradioBtn(elemId) {
981
+ var btn = document.querySelector('#' + elemId + ' button, #' + elemId);
982
+ if (btn) btn.click();
983
+ }
984
+ function showToast(msg) {
985
+ var t = document.getElementById('kb-toast');
986
+ if (!t) return;
987
+ t.textContent = msg; t.classList.add('show');
988
+ setTimeout(function(){ t.classList.remove('show'); }, 2800);
989
+ }
990
+ /* ── Inject left & right columns into Gradio layout ── */
991
+ (function injectColumns() {
992
+ function tryInject() {
993
+ var chatCol = document.querySelector('.centre-col-inner');
994
+ if (!chatCol) { setTimeout(tryInject, 400); return; }
995
+ var body = document.querySelector('.app-body');
996
+ if (body) return; // already done
997
+
998
+ /* Wrap everything in a 3-col grid */
999
+ var wrapper = document.createElement('div');
1000
+ wrapper.className = 'app-body';
1001
+
1002
+ var left = document.getElementById('left-col-mount');
1003
+ var right = document.getElementById('right-col-mount');
1004
+ var chat = chatCol.parentElement;
1005
+
1006
+ if (left && right && chat) {
1007
+ chat.parentElement.insertBefore(wrapper, chat);
1008
+ wrapper.appendChild(left);
1009
+ wrapper.appendChild(chat);
1010
+ wrapper.appendChild(right);
1011
+ }
1012
+ }
1013
+ if (document.readyState === 'loading') {
1014
+ document.addEventListener('DOMContentLoaded', function(){ setTimeout(tryInject, 600); });
1015
+ } else {
1016
+ setTimeout(tryInject, 600);
1017
+ }
1018
+ })();
1019
+ /* toolbar helpers */
1020
+ function tbWrap(tag1, tag2) {
1021
+ var t = document.querySelector('.chat-input-container textarea');
1022
+ if (!t) return;
1023
+ var s = t.selectionStart, e = t.selectionEnd, v = t.value;
1024
+ if (s === e) return;
1025
+ t.value = v.slice(0,s) + tag1 + v.slice(s,e) + tag2 + v.slice(e);
1026
+ t.selectionStart = s; t.selectionEnd = e + tag1.length + tag2.length;
1027
+ t.dispatchEvent(new Event('input',{bubbles:true}));
1028
+ }
1029
+ function tbBullet() {
1030
+ var t = document.querySelector('.chat-input-container textarea');
1031
+ if (!t) return;
1032
+ var lines = t.value.split('\n'), idx = t.value.slice(0,t.selectionStart).split('\n').length-1;
1033
+ lines[idx] = '- ' + lines[idx]; t.value = lines.join('\n');
1034
+ t.dispatchEvent(new Event('input',{bubbles:true}));
1035
+ }
1036
+ function tbPaste() {
1037
+ var t = document.querySelector('.chat-input-container textarea');
1038
+ if (!t) return;
1039
+ navigator.clipboard.readText().then(function(s){
1040
+ var i = t.selectionStart;
1041
+ t.value = t.value.slice(0,i) + s + t.value.slice(t.selectionEnd);
1042
+ t.selectionStart = t.selectionEnd = i + s.length;
1043
+ t.dispatchEvent(new Event('input',{bubbles:true}));
1044
+ }).catch(function(){ t.focus(); });
1045
+ }
1046
+ </script>"""
1047
 
1048
  TOOLBAR_HTML = """
1049
  <div class="tb-bar">
1050
+ <button class="tb-btn" data-tip="Attach file" onclick="document.getElementById('kb-file-attach').click()">
1051
+ <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>
 
 
 
1052
  </button>
1053
+ <button class="tb-btn" data-tip="Paste" onclick="tbPaste()">
1054
+ <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>
 
 
 
 
1055
  </button>
1056
  <div class="tb-sep"></div>
1057
+ <button class="tb-btn" data-tip="Bold" onclick="tbWrap('**','**')">
 
1058
  <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>
1059
  </button>
1060
+ <button class="tb-btn" data-tip="Italic" onclick="tbWrap('*','*')">
 
1061
  <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>
1062
  </button>
1063
+ <button class="tb-btn" data-tip="Bullet list" onclick="tbBullet()">
 
1064
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="9" y1="6" x2="20" y2="6"/><line x1="9" y1="12" x2="20" y2="12"/><line x1="9" y1="18" x2="20" y2="18"/><circle cx="4" cy="6" r="1.5" fill="currentColor" stroke="none"/><circle cx="4" cy="12" r="1.5" fill="currentColor" stroke="none"/><circle cx="4" cy="18" r="1.5" fill="currentColor" stroke="none"/></svg>
1065
  </button>
1066
+ <button class="tb-btn" data-tip="Code" onclick="tbWrap('`','`')">
 
1067
  <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>
1068
  </button>
1069
+ </div>"""
 
1070
 
1071
  examples = [
1072
  q["query"] for q in CONFIG.get("quick_actions", []) if "query" in q
 
1079
  with gr.Blocks(title=CONFIG["client"]["name"]) as demo:
1080
 
1081
  gr.HTML(STYLE_HTML)
1082
+ gr.HTML('<div id="kb-toast" class="kb-toast"></div>')
1083
 
1084
+ # ── Sticky header ─────────────────────────────────────────────────
1085
  gr.HTML(f"""
1086
+ <div class="app-hdr">
1087
+ <div class="app-hdr-icon">πŸ€–</div>
1088
  <div>
1089
+ <h1>{CONFIG["client"]["name"]}</h1>
1090
+ <p>Search the knowledge base and get answers in plain language</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1091
  </div>
1092
+ </div>""")
1093
+
1094
+ # ── 3-column body ─────────────────────────────────────────────────
1095
+ with gr.Row(elem_classes="app-body"):
1096
+
1097
+ # ── LEFT: suggested questions ──────────────────────────────────
1098
+ with gr.Column(scale=1, elem_classes="left-col"):
1099
+ gr.HTML("""<div class="left-col-hdr">✦ Try asking</div>
1100
+ <div class="left-col-body">""" +
1101
+ "".join(
1102
+ f'<button class="suggestion-pill" '
1103
+ f'onclick="(function(){{var t=document.querySelector(\'.chat-input-container textarea\');'
1104
+ f'if(t){{t.value={repr(q)};t.dispatchEvent(new Event(\'input\',{{bubbles:true}}));}}}})()">'
1105
+ f'{q}</button>'
1106
+ for q in examples
1107
+ ) + "</div>")
1108
+
1109
+ # ── CENTRE: chat ───────────────────────────────────────────────
1110
+ with gr.Column(scale=3, elem_classes="centre-col"):
1111
+ gr.ChatInterface(
1112
+ fn=chat_respond,
1113
+ cache_examples=False,
1114
+ )
1115
+ gr.HTML(TOOLBAR_HTML)
1116
+
1117
+ # ── RIGHT: file list ───────────────────────────────────────────
1118
+ with gr.Column(scale=1, elem_classes="right-col"):
1119
+ gr.HTML("""
1120
+ <div class="right-col-hdr">
1121
+ <span class="right-col-title">Knowledge Base</span>
1122
+ <div class="add-file-btn-wrap">
1123
+ <button class="add-file-btn"
1124
+ onclick="document.getElementById('kb-file-attach').click()">
1125
+ + Add file
1126
+ </button>
1127
+ <div class="add-file-tooltip">
1128
+ <strong>Upload documents</strong><br>
1129
+ Formats: .txt Β· .md Β· .pdf Β· .docx<br>
1130
+ Max size: 10 MB per file<br>
1131
+ Multiple files supported<br>
1132
+ Index rebuilds automatically
1133
+ </div>
1134
+ </div>
1135
+ </div>""")
1136
+
1137
+ kb_file_list = gr.HTML(value=list_kb_files_html)
1138
+
1139
+ # Hidden upload status
1140
+ upload_status = gr.Markdown(visible=False)
1141
+
1142
+ # ── Hidden Gradio widgets for backend actions ──────────────────────
1143
+ # These are invisible β€” actions are triggered by JS above
1144
+ with gr.Row(visible=False):
1145
+ toolbar_upload = gr.File(
1146
+ label="", file_count="multiple",
1147
+ file_types=[".txt", ".md", ".pdf", ".docx", ".doc"],
1148
+ elem_id="kb-file-attach",
1149
+ )
1150
+ toolbar_status = gr.Markdown()
1151
 
1152
+ delete_input = gr.Textbox(elem_id="kb-hidden-delete", show_label=False)
1153
+ delete_btn = gr.Button("del", elem_id="kb-delete-trigger")
1154
+ delete_status= gr.Markdown()
1155
 
1156
+ old_name_input = gr.Textbox(elem_id="kb-hidden-old-name", show_label=False)
1157
+ new_name_input = gr.Textbox(elem_id="kb-hidden-new-name", show_label=False)
1158
+ rename_btn = gr.Button("ren", elem_id="kb-rename-trigger")
1159
+ rename_status = gr.Markdown()
 
 
1160
 
1161
+ # ── Event wiring ──────────────────────────────────────────────────
1162
+ def handle_upload(files):
1163
+ status, _ = upload_kb_files(files)
1164
+ return status, list_kb_files_html()
1165
+
1166
+ def handle_delete(filename):
1167
+ status, _ = delete_kb_file(filename)
1168
+ return status, list_kb_files_html()
1169
+
1170
+ def handle_rename(old, new):
1171
+ status, _ = rename_kb_file(old, new)
1172
+ return status, list_kb_files_html()
1173
+
1174
+ toolbar_upload.change(handle_upload, inputs=[toolbar_upload], outputs=[toolbar_status, kb_file_list])
1175
+ delete_btn.click( handle_delete, inputs=[delete_input], outputs=[delete_status, kb_file_list])
1176
+ rename_btn.click( handle_rename, inputs=[old_name_input, new_name_input], outputs=[rename_status, kb_file_list])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1177
 
1178
 
1179
  # -----------------------------