Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -610,58 +610,307 @@ def delete_kb_file(filename: str) -> Tuple[str, str]:
|
|
| 610 |
|
| 611 |
|
| 612 |
# -----------------------------
|
| 613 |
-
# GRADIO UI
|
| 614 |
-
#
|
| 615 |
-
# inject styles via gr.HTML(<style>) inside the Blocks instead.
|
| 616 |
# -----------------------------
|
| 617 |
|
| 618 |
STYLE_HTML = """
|
| 619 |
<style>
|
|
|
|
| 620 |
:root {
|
| 621 |
-
--
|
| 622 |
-
--
|
| 623 |
-
--
|
| 624 |
-
--
|
| 625 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 626 |
}
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
.
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
.
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
.
|
| 638 |
-
|
| 639 |
-
border-
|
| 640 |
-
padding:
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
</
|
|
|
|
|
|
|
|
|
|
| 685 |
""")
|
| 686 |
|
| 687 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
"
|
| 704 |
-
"
|
|
|
|
|
|
|
|
|
|
| 705 |
)
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
# -----------------------------
|