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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +365 -239
app.py CHANGED
@@ -650,271 +650,399 @@ def delete_kb_file(filename: str) -> Tuple[str, str]:
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", [])
886
- if "query" in q
887
  ] or [
888
  "What makes a good knowledge base article?",
889
  "How could a KB assistant help support agents?",
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",
@@ -923,62 +1051,60 @@ with gr.Blocks(title=CONFIG["client"]["name"]) as demo:
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],
 
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
 
 
980
  ] or [
981
  "What makes a good knowledge base article?",
982
  "How could a KB assistant help support agents?",
983
  "Why is self-service important for customer support?",
984
  ]
985
 
 
 
 
 
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",
 
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],