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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +365 -81
app.py CHANGED
@@ -610,58 +610,307 @@ def delete_kb_file(filename: str) -> Tuple[str, str]:
610
 
611
 
612
  # -----------------------------
613
- # GRADIO UI
614
- # β€” css= not supported in all Gradio versions;
615
- # inject styles via gr.HTML(<style>) inside the Blocks instead.
616
  # -----------------------------
617
 
618
  STYLE_HTML = """
619
  <style>
 
620
  :root {
621
- --accent:#4f6ef7; --accent-h:#3a56d4;
622
- --danger:#ef4444; --danger-h:#dc2626;
623
- --surface:#fff; --bg:#f4f6fb;
624
- --border:#e5e7eb; --muted:#6b7280;
625
- --radius:12px; --shadow:0 2px 12px rgba(0,0,0,.08);
 
 
 
 
 
 
 
 
626
  }
627
- body,.gradio-container{background:var(--bg)!important;
628
- font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif!important}
629
- .app-header{background:linear-gradient(135deg,var(--accent) 0%,#7c3aed 100%);
630
- color:#fff;padding:20px 24px 16px;border-radius:var(--radius) var(--radius) 0 0;margin-bottom:0}
631
- .app-header h1{margin:0 0 4px;font-size:clamp(1.2rem,4vw,1.6rem);font-weight:700;letter-spacing:-.02em}
632
- .app-header p{margin:0;opacity:.85;font-size:clamp(.8rem,2.5vw,.92rem)}
633
- .tabs>.tab-nav{background:var(--surface)!important;border-bottom:2px solid var(--border)!important;padding:0 16px!important;gap:4px!important}
634
- .tabs>.tab-nav button{border-radius:var(--radius) var(--radius) 0 0!important;padding:10px 18px!important;
635
- font-weight:500!important;font-size:.88rem!important;color:var(--muted)!important;
636
- border:none!important;background:transparent!important;transition:color .2s,background .2s!important}
637
- .tabs>.tab-nav button.selected{color:var(--accent)!important;border-bottom:2px solid var(--accent)!important}
638
- .message.user>div{background:var(--accent)!important;color:#fff!important;
639
- border-radius:var(--radius) var(--radius) 4px var(--radius)!important;
640
- padding:10px 14px!important;max-width:82%!important;
641
- font-size:clamp(.85rem,2.5vw,.95rem)!important;line-height:1.55!important;box-shadow:var(--shadow)!important}
642
- .message.bot>div{background:var(--surface)!important;border:1px solid var(--border)!important;
643
- border-radius:var(--radius) var(--radius) var(--radius) 4px!important;
644
- padding:10px 14px!important;max-width:88%!important;
645
- font-size:clamp(.85rem,2.5vw,.95rem)!important;line-height:1.6!important;box-shadow:var(--shadow)!important}
646
- .chat-input-container textarea{border-radius:24px!important;border:1.5px solid var(--border)!important;
647
- padding:10px 16px!important;font-size:.93rem!important;resize:none!important;transition:border-color .2s!important}
648
- .chat-input-container textarea:focus{border-color:var(--accent)!important;outline:none!important;
649
- box-shadow:0 0 0 3px rgba(79,110,247,.15)!important}
650
- .examples-table button,.example{border-radius:999px!important;border:1.5px solid var(--accent)!important;
651
- color:var(--accent)!important;background:transparent!important;padding:6px 14px!important;
652
- font-size:.82rem!important;font-weight:500!important;transition:background .2s,color .2s!important;white-space:nowrap!important}
653
- .examples-table button:hover,.example:hover{background:var(--accent)!important;color:#fff!important}
654
- @media(max-width:640px){
655
- .gradio-container{padding:0!important}
656
- .app-header{border-radius:0!important;padding:14px 16px 12px!important}
657
- .tabs>.tab-nav button{padding:8px 12px!important;font-size:.82rem!important}
658
- .message.user>div,.message.bot>div{max-width:95%!important;font-size:.88rem!important}
659
- .kb-row{flex-direction:column!important}
660
- .kb-row>*{width:100%!important}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
661
  }
662
  </style>
663
  """
664
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
665
  examples = [
666
  q["query"]
667
  for q in CONFIG.get("quick_actions", [])
@@ -674,58 +923,93 @@ examples = [
674
 
675
  with gr.Blocks(title=CONFIG["client"]["name"]) as demo:
676
 
677
- # Inject styles β€” compatible with all Gradio versions
678
  gr.HTML(STYLE_HTML)
679
 
 
680
  gr.HTML(f"""
681
- <div class="app-header">
682
- <h1>πŸ€– {CONFIG["client"]["name"]}</h1>
683
- <p>Ask me anything β€” I'll search the knowledge base and answer in plain language.</p>
684
- </div>
 
 
 
685
  """)
686
 
687
- with gr.Tabs(elem_classes="tabs"):
 
 
 
 
688
 
689
- # ── Tab 1: Chat ──────────────────────────────────────────────
690
- with gr.TabItem("πŸ’¬ Chat"):
691
  gr.ChatInterface(
692
  fn=chat_respond,
693
  examples=examples,
694
  cache_examples=False,
695
  )
696
 
697
- # ── Tab 2: KB Manager ────────────────────────────────────────
698
- with gr.TabItem("πŸ“ KB Manager"):
699
- gr.HTML(
700
- "<div style='padding:16px 0 4px'>"
701
- "<strong>Manage Knowledge Base Documents</strong><br>"
702
- "<span style='color:#6b7280;font-size:.88rem'>"
703
- "Supported: .txt Β· .md Β· .pdf Β· .docx β€” Index rebuilds automatically after every change"
704
- "</span></div>"
 
 
 
705
  )
706
-
707
- with gr.Row(elem_classes="kb-row"):
708
-
709
- with gr.Column(scale=2):
710
- upload_input = gr.File(
711
- label="Drop files here or click to browse (max 10 MB each)",
712
- file_count="multiple",
713
- file_types=[".txt", ".md", ".pdf", ".docx", ".doc"],
714
- )
715
- upload_btn = gr.Button("⬆️ Upload & Index", variant="primary")
716
- upload_status = gr.Markdown()
717
-
718
- with gr.Column(scale=1):
719
- gr.Markdown("**Current KB Files**")
720
- kb_file_list = gr.Markdown(value=list_kb_files)
721
- gr.HTML("<hr style='border:none;border-top:1px solid #e5e7eb;margin:12px 0'>")
722
- gr.Markdown("**Delete a File**")
723
- delete_input = gr.Textbox(placeholder="Exact filename, e.g. guide.pdf", show_label=False)
724
- delete_btn = gr.Button("πŸ—‘οΈ Delete", variant="stop")
725
- delete_status = gr.Markdown()
726
-
727
- upload_btn.click(upload_kb_files, inputs=[upload_input], outputs=[upload_status, kb_file_list])
728
- delete_btn.click(delete_kb_file, inputs=[delete_input], outputs=[delete_status, kb_file_list])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
729
 
730
 
731
  # -----------------------------
 
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", [])
 
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],
1001
+ outputs=[upload_status, kb_file_list],
1002
+ )
1003
+ delete_btn.click(
1004
+ delete_kb_file,
1005
+ inputs=[delete_input],
1006
+ outputs=[delete_status, kb_file_list],
1007
+ )
1008
+ toolbar_upload.change(
1009
+ upload_kb_files,
1010
+ inputs=[toolbar_upload],
1011
+ outputs=[toolbar_upload_status, kb_file_list],
1012
+ )
1013
 
1014
 
1015
  # -----------------------------