sofzcc commited on
Commit
a34d354
Β·
verified Β·
1 Parent(s): d75dbd8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +463 -474
app.py CHANGED
@@ -751,524 +751,500 @@ def rename_kb_file(old_name: str, new_name: str) -> Tuple[str, str]:
751
  return f"❌ Could not rename: {e}", list_kb_files_html()
752
 
753
 
754
- # ── CSS ────────────────────────────────────────────────────────────────────────
755
 
756
- STYLE_HTML = """<style>
757
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
758
 
759
- :root {
760
- /* Palette */
761
- --sage:#6b8f71; --sage-l:#eaf2eb; --sage-d:#4f6e55; --sage-xl:#f2f8f3;
762
- --sand:#f5f0e8; --cream:#fdfaf5; --stone:#e4ddd3; --stone2:#d0c8bd;
763
- --ink:#2d2d2d; --ink2:#4a4540; --muted:#7a7167; --muted2:#a09890;
764
- --rose:#c0717a; --rose-l:#f8eded;
765
- /* Spacing & shape */
766
- --r:13px; --r-sm:8px; --r-xs:5px;
767
- /* Shadows */
768
- --sh-sm: 0 1px 3px rgba(0,0,0,.07);
769
- --sh: 0 2px 10px rgba(0,0,0,.08);
770
- --sh-md: 0 4px 20px rgba(0,0,0,.10);
771
- --sh-lg: 0 8px 32px rgba(0,0,0,.13);
772
- }
773
 
774
- *, *::before, *::after { box-sizing: border-box; }
775
-
776
- body, .gradio-container {
777
- background: var(--sand) !important;
778
- font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
779
- color: var(--ink) !important;
780
- -webkit-font-smoothing: antialiased;
781
- }
782
- .gradio-container { padding: 0 !important; max-width: 100% !important; }
783
-
784
- /* ── Header ─────────────────────────────────────────── */
785
- .app-hdr {
786
- background: var(--cream);
787
- border-bottom: 1px solid var(--stone);
788
- padding: 0 24px;
789
- height: 58px;
790
- display: flex; align-items: center; gap: 13px;
791
- position: sticky; top: 0; z-index: 20;
792
- box-shadow: var(--sh-sm);
793
- }
794
- .app-hdr-icon {
795
- width: 36px; height: 36px;
796
- background: linear-gradient(135deg, var(--sage) 0%, var(--sage-d) 100%);
797
- border-radius: 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
798
  display: flex; align-items: center; justify-content: center;
799
- font-size: 1.1rem; flex-shrink: 0;
800
- box-shadow: 0 2px 8px rgba(107,143,113,.35);
801
  }
802
- .app-hdr h1 {
803
- margin: 0 0 1px; font-size: .97rem; font-weight: 650;
804
- color: var(--ink); letter-spacing: -.01em;
805
  }
806
- .app-hdr p { margin: 0; font-size: .73rem; color: var(--muted); }
807
- /* Subtle version badge */
808
- .app-hdr-badge {
809
  margin-left: auto;
810
- background: var(--sage-xl); border: 1px solid var(--sage-l);
811
- color: var(--sage-d); border-radius: 999px;
812
- padding: 3px 10px; font-size: .68rem; font-weight: 600;
813
- letter-spacing: .02em;
814
  }
815
 
816
- /* ── 3-column layout ──────���──────────────────────────── */
817
- .app-body {
818
- display: grid;
819
- grid-template-columns: 210px 1fr 230px;
820
- height: calc(100vh - 58px);
821
- overflow: hidden;
 
822
  }
823
- @media(max-width: 900px) {
824
- .app-body { grid-template-columns: 1fr; height: auto; overflow: visible; }
825
- .left-col, .right-col { display: none; }
826
  }
827
 
828
- /* ── Left col ────────────────────────────────────────── */
829
- .left-col {
830
- border-right: 1px solid var(--stone); background: var(--cream);
 
831
  display: flex; flex-direction: column; overflow: hidden;
832
  }
833
- .left-col-hdr {
834
- padding: 16px 14px 10px;
835
- font-size: .63rem; font-weight: 700; letter-spacing: .08em;
836
- text-transform: uppercase; color: var(--muted2);
837
- border-bottom: 1px solid var(--stone); flex-shrink: 0;
838
  display: flex; align-items: center; gap: 6px;
839
  }
840
- .left-col-hdr::before {
841
  content: "";
842
- display: inline-block; width: 6px; height: 6px;
843
- background: var(--sage); border-radius: 50%;
844
- }
845
- .left-col-body { overflow-y: auto; padding: 12px 10px 16px; flex: 1; }
846
-
847
- /* Suggestion pills β€” staggered fade-in */
848
- .suggestion-pill {
849
- display: flex; align-items: flex-start; gap: 8px;
850
- width: 100%; text-align: left;
851
- background: var(--cream); border: 1.5px solid var(--stone);
852
- border-radius: var(--r); padding: 9px 12px;
853
- font-size: .77rem; color: var(--ink2); cursor: pointer;
854
- margin-bottom: 8px; line-height: 1.45;
855
- transition: background .16s, border-color .16s, color .16s, transform .16s, box-shadow .16s;
856
- box-shadow: var(--sh-sm);
857
- opacity: 0; animation: pillFadeIn .4s ease forwards;
858
- }
859
- .suggestion-pill:nth-child(1) { animation-delay: .05s; }
860
- .suggestion-pill:nth-child(2) { animation-delay: .12s; }
861
- .suggestion-pill:nth-child(3) { animation-delay: .19s; }
862
- .suggestion-pill:nth-child(4) { animation-delay: .26s; }
863
- .suggestion-pill:nth-child(5) { animation-delay: .33s; }
864
- .suggestion-pill::before {
865
- content: "β†’"; color: var(--sage); font-size: .8rem;
866
- flex-shrink: 0; margin-top: 1px; transition: transform .16s;
867
- }
868
- .suggestion-pill:hover {
869
- background: var(--sage-xl); border-color: var(--sage);
870
- color: var(--sage-d); transform: translateX(3px);
871
- box-shadow: var(--sh);
872
- }
873
- .suggestion-pill:hover::before { transform: translateX(3px); }
874
- @keyframes pillFadeIn {
875
- from { opacity: 0; transform: translateY(6px); }
876
- to { opacity: 1; transform: translateY(0); }
877
  }
878
 
879
- /* ── Centre col ──────────────────────────────────────── */
880
- .centre-col {
881
- display: flex; flex-direction: column; overflow: hidden;
882
- background: var(--sand);
883
  }
884
 
885
- /* Kill ALL Gradio chrome */
 
 
 
 
 
 
 
886
  [data-testid="chatbot"] > .label-wrap,
887
- [data-testid="chatbot"] > div > .label-wrap,
888
  [data-testid="chatbot"] label,
889
- [data-testid="chatbot"] .label-wrap,
890
- .chatbot-label, .label-wrap span,
891
- .gr-chatbot-container .label-wrap,
892
- .chatbot > .panel > .label,
893
- div[class*="label"] > span:only-child,
894
- .block > .label-wrap { display: none !important; }
895
- .gr-chatbot, [data-testid="chatbot"] {
896
- flex: 1 !important; height: 100% !important;
897
- border: none !important; border-radius: 0 !important; background: transparent !important;
898
- }
899
- footer { display: none !important; }
900
 
901
- /* ── Chat bubbles ─────────────────────────────────────── */
902
- .message { animation: bubbleIn .22s ease; }
903
- @keyframes bubbleIn {
904
- from { opacity: 0; transform: translateY(8px); }
905
  to { opacity: 1; transform: translateY(0); }
906
  }
 
 
907
  .message.user > div {
908
- background: linear-gradient(135deg, var(--sage) 0%, var(--sage-d) 100%) !important;
909
  color: #fff !important;
910
- border-radius: var(--r) var(--r) 4px var(--r) !important;
911
- padding: 10px 15px !important; max-width: 74% !important;
912
- font-size: .875rem !important; line-height: 1.6 !important;
913
- box-shadow: 0 3px 12px rgba(79,110,85,.30) !important;
 
914
  }
915
  .message.bot > div {
916
- background: var(--cream) !important; border: 1px solid var(--stone) !important;
917
- border-radius: var(--r) var(--r) var(--r) 4px !important;
918
- padding: 12px 16px !important; max-width: 80% !important;
919
- font-size: .875rem !important; line-height: 1.68 !important;
920
- box-shadow: var(--sh) !important; color: var(--ink) !important;
921
- }
922
- /* Bot avatar dot */
923
- .message.bot::before {
924
- content: "πŸ€–";
925
- display: block; font-size: .85rem; margin-bottom: 4px;
926
- opacity: .7;
927
  }
928
 
929
- /* ── Input + toolbar card ────────────────────────────── */
930
  .chat-input-container {
931
- background: var(--cream) !important;
932
- border-top: 1px solid var(--stone) !important;
933
- padding: 12px 14px 8px !important;
934
- flex-shrink: 0;
935
  }
936
- /* Outer card border */
937
  .chat-input-container > .wrap,
938
  .chat-input-container > div:first-child {
939
- border: 1.5px solid var(--stone) !important;
940
- border-radius: var(--r) var(--r) var(--r-sm) var(--r-sm) !important;
941
- overflow: hidden !important; background: var(--sand) !important;
942
- transition: border-color .2s, box-shadow .2s !important;
 
943
  }
944
  .chat-input-container > .wrap:focus-within,
945
  .chat-input-container > div:first-child:focus-within {
946
- border-color: var(--sage) !important;
947
- box-shadow: 0 0 0 3px rgba(107,143,113,.15) !important;
948
  }
949
  .chat-input-container textarea {
950
- background: var(--sand) !important; border: none !important;
951
- border-radius: 0 !important; padding: 11px 14px !important;
952
- font-size: .875rem !important; color: var(--ink) !important;
953
- resize: none !important; font-family: inherit !important;
 
954
  line-height: 1.55 !important;
955
  }
956
  .chat-input-container textarea:focus { outline: none !important; box-shadow: none !important; }
957
- .chat-input-container textarea::placeholder { color: var(--muted2) !important; }
958
- .chat-input-container button[aria-label="Send"] {
959
- background: linear-gradient(135deg, var(--sage) 0%, var(--sage-d) 100%) !important;
960
- color: #fff !important; border: none !important;
961
- border-radius: var(--r-sm) !important; padding: 0 15px !important; height: 36px !important;
962
- font-weight: 600 !important; font-size: .82rem !important;
963
- transition: opacity .15s, transform .1s !important;
964
- margin: 0 7px 7px 0 !important; align-self: flex-end !important;
965
- box-shadow: 0 2px 8px rgba(79,110,85,.28) !important;
 
 
 
966
  }
967
  .chat-input-container button[aria-label="Send"]:hover {
968
- opacity: .88 !important; transform: translateY(-1px) !important;
969
- }
970
-
971
- /* Toolbar β€” fused bottom of input card */
972
- .tb-bar {
973
- display: flex; align-items: center; gap: 2px;
974
- padding: 5px 10px 7px;
975
- background: var(--sand); border-top: 1px solid var(--stone);
976
- border-radius: 0 0 var(--r-sm) var(--r-sm);
977
- margin: 0 -1px -1px -1px;
978
- }
979
- .tb-btn {
980
- width: 26px; height: 26px; display: inline-flex; align-items: center; justify-content: center;
981
- background: none; border: none; border-radius: var(--r-xs);
982
- cursor: pointer; color: var(--muted); transition: background .13s, color .13s;
983
- position: relative; flex-shrink: 0;
984
- }
985
- .tb-btn:hover { background: var(--sage-l); color: var(--sage-d); }
986
- .tb-btn svg { width: 13px; height: 13px; pointer-events: none; }
987
- .tb-sep { width: 1px; height: 13px; background: var(--stone2); margin: 0 3px; flex-shrink: 0; }
988
- .tb-btn::after {
989
- content: attr(data-tip); position: absolute; bottom: calc(100% + 6px); left: 50%;
990
- transform: translateX(-50%); background: var(--ink); color: #fff;
991
- font-size: .63rem; padding: 3px 7px; border-radius: 4px; white-space: nowrap;
992
- pointer-events: none; opacity: 0; transition: opacity .12s; z-index: 99;
 
 
 
 
 
 
993
  font-family: "Inter", sans-serif;
994
  }
995
- .tb-btn:hover::after { opacity: 1; }
996
 
997
- /* ── Right col ───────────────────────────────────────── */
998
- .right-col {
999
- border-left: 1px solid var(--stone); background: var(--cream);
 
1000
  display: flex; flex-direction: column; overflow: hidden;
1001
  }
1002
- .right-col-hdr {
1003
- padding: 14px 14px 12px; border-bottom: 1px solid var(--stone);
1004
- flex-shrink: 0; display: flex; align-items: center; justify-content: space-between;
 
1005
  }
1006
- .right-col-title {
1007
- font-size: .63rem; font-weight: 700; letter-spacing: .08em;
1008
- text-transform: uppercase; color: var(--muted2);
1009
  display: flex; align-items: center; gap: 6px;
1010
  }
1011
- .right-col-title::before {
1012
  content: "";
1013
- display: inline-block; width: 6px; height: 6px;
1014
- background: var(--sage); border-radius: 50%;
1015
  }
1016
- /* File count badge */
1017
- .kb-count-badge {
1018
- background: var(--sage-xl); color: var(--sage-d); border-radius: 999px;
1019
- padding: 1px 8px; font-size: .68rem; font-weight: 600;
1020
- border: 1px solid var(--sage-l);
1021
  }
1022
-
1023
  /* + Add file */
1024
- .add-file-btn-wrap { position: relative; display: inline-block; }
1025
- .add-file-btn {
1026
- background: var(--sage-xl); border: 1.5px solid var(--sage-l);
1027
- color: var(--sage-d); border-radius: var(--r-sm); padding: 5px 11px;
1028
- font-size: .74rem; font-weight: 600; cursor: pointer;
1029
- transition: all .15s; white-space: nowrap;
1030
- display: flex; align-items: center; gap: 5px;
1031
- box-shadow: var(--sh-sm);
1032
- }
1033
- .add-file-btn:hover {
1034
- background: var(--sage); color: #fff; border-color: var(--sage);
1035
- box-shadow: 0 2px 8px rgba(107,143,113,.3);
1036
- transform: translateY(-1px);
1037
- }
1038
- .add-file-tooltip {
1039
- display: none; position: absolute; top: calc(100% + 8px); right: 0;
1040
- background: var(--ink); color: #fff; border-radius: var(--r-sm);
1041
- padding: 10px 13px; font-size: .72rem; line-height: 1.65;
1042
- white-space: nowrap; z-index: 50;
1043
- box-shadow: var(--sh-lg); min-width: 210px;
1044
- animation: tooltipIn .14s ease;
 
 
 
 
 
 
 
 
 
 
 
 
1045
  }
1046
- @keyframes tooltipIn {
1047
- from { opacity: 0; transform: translateY(-3px); }
1048
- to { opacity: 1; transform: translateY(0); }
1049
- }
1050
- .add-file-tooltip::before {
1051
- content: ""; position: absolute; top: -5px; right: 14px;
1052
- border-left: 5px solid transparent; border-right: 5px solid transparent;
1053
- border-bottom: 5px solid var(--ink);
1054
- }
1055
- .add-file-tooltip strong { display: block; margin-bottom: 4px; font-size: .76rem; }
1056
- .add-file-tooltip .tt-row { display: flex; align-items: center; gap: 6px; opacity: .88; }
1057
- .add-file-tooltip .tt-row::before { content: "β€’"; color: var(--sage-l); }
1058
- .add-file-btn-wrap:hover .add-file-tooltip { display: block; }
1059
-
1060
- .right-col-body { overflow-y: auto; padding: 6px 8px 16px; flex: 1; }
1061
 
1062
  /* File rows */
1063
- .kb-file-row {
1064
- display: flex; align-items: center; gap: 8px;
1065
- padding: 7px 9px; border-radius: var(--r-sm); cursor: default;
1066
- transition: background .13s;
1067
- }
1068
- .kb-file-row:hover { background: var(--sand); }
1069
- .kb-file-icon { font-size: .82rem; flex-shrink: 0; opacity: .8; }
1070
- .kb-file-name {
1071
- flex: 1; font-size: .775rem; color: var(--ink2);
1072
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1073
  }
1074
- .kb-file-actions {
1075
- display: none; gap: 1px; align-items: center; flex-shrink: 0;
1076
- }
1077
- .kb-file-row:hover .kb-file-actions { display: flex; }
1078
- .kb-action-btn {
1079
- background: none; border: none; cursor: pointer; font-size: .78rem;
1080
- padding: 3px 5px; border-radius: 4px; color: var(--muted);
1081
- transition: background .12s, color .12s; line-height: 1;
1082
  }
1083
- .kb-action-btn:hover { background: var(--stone); color: var(--ink); }
1084
- .kb-action-btn.del:hover { background: var(--rose-l); color: var(--rose); }
1085
 
1086
- /* Rename inline */
1087
- .kb-rename-row {
1088
- display: none; padding: 5px 8px; gap: 5px; align-items: center;
1089
- }
1090
- .kb-rename-row.active { display: flex; }
1091
- .kb-rename-input {
1092
- flex: 1; border: 1.5px solid var(--sage); border-radius: var(--r-xs);
1093
- padding: 4px 8px; font-size: .75rem; background: var(--sand);
1094
- color: var(--ink); outline: none; font-family: inherit;
1095
  }
1096
- .kb-rename-save {
1097
- background: var(--sage); color: #fff; border: none; border-radius: var(--r-xs);
1098
- padding: 4px 9px; font-size: .72rem; font-weight: 600; cursor: pointer;
1099
- transition: background .13s;
1100
  }
1101
- .kb-rename-save:hover { background: var(--sage-d); }
1102
- .kb-rename-cancel {
1103
- background: none; border: 1px solid var(--stone); border-radius: var(--r-xs);
1104
- padding: 4px 7px; font-size: .72rem; color: var(--muted); cursor: pointer;
1105
  }
1106
 
1107
- /* ── Toast notification ───────────────────────────────── */
1108
- .kb-toast {
1109
- position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%) translateY(6px);
1110
- background: var(--ink); color: #fff; border-radius: var(--r-sm);
1111
- padding: 9px 20px; font-size: .79rem; z-index: 9999;
1112
- opacity: 0; pointer-events: none;
1113
- transition: opacity .22s, transform .22s;
1114
- white-space: nowrap; box-shadow: var(--sh-md);
1115
- display: flex; align-items: center; gap: 8px;
1116
- }
1117
- .kb-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
1118
-
1119
- /* ── Source chip ────────────────────────────────────────── */
1120
  .src-row {
1121
  display: flex; align-items: center; flex-wrap: wrap; gap: 5px;
1122
  margin-top: 10px; padding-top: 9px;
1123
- border-top: 1px solid var(--stone);
1124
  }
1125
- .src-label {
1126
- font-size: .67rem; font-weight: 700; letter-spacing: .07em;
1127
- text-transform: uppercase; color: var(--muted2);
1128
- margin-right: 2px; flex-shrink: 0;
1129
  }
1130
  .src-chip {
1131
  display: inline-flex; align-items: center; gap: 4px;
1132
- background: var(--sage-xl); border: 1px solid var(--sage-l);
1133
  border-radius: 999px; padding: 2px 10px 2px 7px;
1134
- transition: background .14s, border-color .14s;
1135
- animation: chipIn .22s ease;
1136
  }
1137
- .src-chip:hover { background: var(--sage-l); border-color: var(--sage); }
1138
  @keyframes chipIn {
1139
- from { opacity: 0; transform: translateY(4px) scale(.95); }
1140
- to { opacity: 1; transform: translateY(0) scale(1); }
1141
  }
1142
- .src-chip-icon { font-size: .72rem; }
1143
  .src-chip-name {
1144
- font-size: .72rem; font-weight: 500;
1145
- color: var(--sage-d); max-width: 140px;
1146
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1147
  }
1148
 
1149
- /* ── Scrollbar polish ────────────────────────────��────── */
1150
- ::-webkit-scrollbar { width: 5px; }
1151
- ::-webkit-scrollbar-track { background: transparent; }
1152
- ::-webkit-scrollbar-thumb { background: var(--stone2); border-radius: 99px; }
1153
- ::-webkit-scrollbar-thumb:hover { background: var(--muted2); }
 
 
 
 
 
 
1154
 
1155
- /* ── Selection colour ─────────────────────────────────── */
1156
- ::selection { background: rgba(107,143,113,.22); }
 
 
 
 
 
1157
  </style>
1158
 
1159
  <script>
1160
- /* ── KB action dispatcher ──────────────────────── */
1161
- function kbAction(action, filename) {
1162
  if (action === 'delete') {
1163
- if (!confirm('Delete ' + filename + '?')) return;
1164
- setGradioInput('kb-hidden-delete', filename);
1165
- setTimeout(function(){ clickGradioBtn('kb-delete-trigger'); }, 80);
1166
  } else if (action === 'replace') {
1167
  document.getElementById('kb-file-attach').click();
1168
- // Store target for replacement note
1169
- window._kbReplaceTarget = filename;
1170
  } else if (action === 'rename') {
1171
- var row = document.getElementById('row-' + filename);
1172
- var rrow = document.getElementById('rrow-' + filename);
1173
- if (row) row.style.display = 'none';
1174
- if (rrow) { rrow.classList.add('active'); rrow.querySelector('input').focus(); }
1175
  }
1176
  }
1177
- function kbCancelRename(filename) {
1178
- var row = document.getElementById('row-' + filename);
1179
- var rrow = document.getElementById('rrow-' + filename);
1180
- if (row) row.style.display = '';
1181
- if (rrow) rrow.classList.remove('active');
1182
- }
1183
- function kbSaveRename(oldName) {
1184
- var rrow = document.getElementById('rrow-' + oldName);
1185
- var input = rrow ? rrow.querySelector('input') : null;
1186
- if (!input || !input.value.trim()) return;
1187
- setGradioInput('kb-hidden-old-name', oldName);
1188
- setGradioInput('kb-hidden-new-name', input.value.trim());
1189
- setTimeout(function(){ clickGradioBtn('kb-rename-trigger'); }, 80);
1190
- }
1191
- function setGradioInput(elemId, value) {
1192
- var el = document.querySelector('#' + elemId + ' input, #' + elemId + ' textarea');
1193
  if (!el) return;
1194
- el.value = value;
1195
- el.dispatchEvent(new Event('input', { bubbles: true }));
1196
- el.dispatchEvent(new Event('change', { bubbles: true }));
1197
  }
1198
- function clickGradioBtn(elemId) {
1199
- var btn = document.querySelector('#' + elemId + ' button, #' + elemId);
1200
- if (btn) btn.click();
1201
  }
1202
  function showToast(msg) {
1203
- var t = document.getElementById('kb-toast');
1204
  if (!t) return;
1205
  t.textContent = msg; t.classList.add('show');
1206
- setTimeout(function(){ t.classList.remove('show'); }, 2800);
1207
  }
1208
- /* ── Inject left & right columns into Gradio layout ── */
1209
- (function injectColumns() {
1210
- function tryInject() {
1211
- var chatCol = document.querySelector('.centre-col-inner');
1212
- if (!chatCol) { setTimeout(tryInject, 400); return; }
1213
- var body = document.querySelector('.app-body');
1214
- if (body) return; // already done
1215
-
1216
- /* Wrap everything in a 3-col grid */
1217
- var wrapper = document.createElement('div');
1218
- wrapper.className = 'app-body';
1219
-
1220
- var left = document.getElementById('left-col-mount');
1221
- var right = document.getElementById('right-col-mount');
1222
- var chat = chatCol.parentElement;
1223
-
1224
- if (left && right && chat) {
1225
- chat.parentElement.insertBefore(wrapper, chat);
1226
- wrapper.appendChild(left);
1227
- wrapper.appendChild(chat);
1228
- wrapper.appendChild(right);
1229
- }
1230
- }
1231
- if (document.readyState === 'loading') {
1232
- document.addEventListener('DOMContentLoaded', function(){ setTimeout(tryInject, 600); });
1233
- } else {
1234
- setTimeout(tryInject, 600);
1235
- }
1236
- })();
1237
- /* Kill "Chatbot" label β€” JS fallback for Svelte-hashed classnames */
1238
- function killChatbotLabel() {
1239
- /* Find all elements whose visible text is exactly "Chatbot" and hide them */
1240
  document.querySelectorAll('label, span, div').forEach(function(el) {
1241
- if (el.children.length === 0 && el.textContent.trim() === 'Chatbot') {
1242
  el.style.display = 'none';
1243
- var wrap = el.closest('[class*="label"]');
1244
- if (wrap) wrap.style.display = 'none';
1245
  }
1246
  });
1247
  }
1248
- /* Run on load + after any Gradio re-render */
1249
- if (document.readyState === 'loading') {
1250
- document.addEventListener('DOMContentLoaded', function(){ setTimeout(killChatbotLabel, 600); });
1251
- } else {
1252
- setTimeout(killChatbotLabel, 600);
1253
- }
1254
- var _kbLabelObserver = new MutationObserver(function(){ killChatbotLabel(); });
1255
- _kbLabelObserver.observe(document.body, { childList: true, subtree: true });
1256
 
1257
- /* toolbar helpers */
1258
- function tbWrap(tag1, tag2) {
 
 
 
 
 
 
 
 
1259
  var t = document.querySelector('.chat-input-container textarea');
1260
  if (!t) return;
1261
  var s = t.selectionStart, e = t.selectionEnd, v = t.value;
1262
  if (s === e) return;
1263
- t.value = v.slice(0,s) + tag1 + v.slice(s,e) + tag2 + v.slice(e);
1264
- t.selectionStart = s; t.selectionEnd = e + tag1.length + tag2.length;
1265
  t.dispatchEvent(new Event('input',{bubbles:true}));
1266
  }
1267
  function tbBullet() {
1268
  var t = document.querySelector('.chat-input-container textarea');
1269
  if (!t) return;
1270
- var lines = t.value.split('\n'), idx = t.value.slice(0,t.selectionStart).split('\n').length-1;
1271
- lines[idx] = '- ' + lines[idx]; t.value = lines.join('\n');
1272
  t.dispatchEvent(new Event('input',{bubbles:true}));
1273
  }
1274
  function tbPaste() {
@@ -1276,35 +1252,55 @@ function tbPaste() {
1276
  if (!t) return;
1277
  navigator.clipboard.readText().then(function(s){
1278
  var i = t.selectionStart;
1279
- t.value = t.value.slice(0,i) + s + t.value.slice(t.selectionEnd);
1280
- t.selectionStart = t.selectionEnd = i + s.length;
1281
  t.dispatchEvent(new Event('input',{bubbles:true}));
1282
  }).catch(function(){ t.focus(); });
1283
  }
1284
- </script>"""
 
1285
 
1286
  TOOLBAR_HTML = """
1287
- <div class="tb-bar">
1288
- <button class="tb-btn" data-tip="Attach file" onclick="document.getElementById('kb-file-attach').click()">
1289
- <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>
 
 
1290
  </button>
1291
- <button class="tb-btn" data-tip="Paste" onclick="tbPaste()">
1292
- <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>
 
 
 
1293
  </button>
1294
- <div class="tb-sep"></div>
1295
- <button class="tb-btn" data-tip="Bold" onclick="tbWrap('**','**')">
1296
- <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>
 
 
1297
  </button>
1298
- <button class="tb-btn" data-tip="Italic" onclick="tbWrap('*','*')">
1299
- <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>
 
 
1300
  </button>
1301
- <button class="tb-btn" data-tip="Bullet list" onclick="tbBullet()">
1302
- <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>
 
 
 
 
 
1303
  </button>
1304
- <button class="tb-btn" data-tip="Code" onclick="tbWrap('`','`')">
1305
- <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>
 
 
1306
  </button>
1307
- </div>"""
 
 
 
1308
 
1309
  examples = [
1310
  q["query"] for q in CONFIG.get("quick_actions", []) if "query" in q
@@ -1314,72 +1310,67 @@ examples = [
1314
  "Why is self-service important for customer support?",
1315
  ]
1316
 
 
 
1317
  with gr.Blocks(title=CONFIG["client"]["name"]) as demo:
1318
 
1319
  gr.HTML(STYLE_HTML)
1320
- gr.HTML('<div id="kb-toast" class="kb-toast"></div>')
1321
 
1322
- # ── Sticky header ─────────────────────────────────────────────────
1323
  gr.HTML(f"""
1324
- <div class="app-hdr">
1325
- <div class="app-hdr-icon">πŸ€–</div>
1326
  <div>
1327
- <h1>{CONFIG["client"]["name"]}</h1>
1328
- <p>Search the knowledge base and get answers in plain language</p>
1329
  </div>
1330
- <span class="app-hdr-badge">MVP Β· AI-powered</span>
1331
  </div>""")
1332
 
1333
- # ── 3-column body ──────────────���──────────────────────────────────
1334
- with gr.Row(elem_classes="app-body"):
1335
-
1336
- # ── LEFT: suggested questions ──────────────────────────────────
1337
- with gr.Column(scale=1, elem_classes="left-col"):
1338
- gr.HTML("""<div class="left-col-hdr">✦ Try asking</div>
1339
- <div class="left-col-body">""" +
1340
- "".join(
1341
- f'<button class="suggestion-pill" '
1342
- f'onclick="(function(){{var t=document.querySelector(\'.chat-input-container textarea\');'
1343
- f'if(t){{t.value={repr(q)};t.dispatchEvent(new Event(\'input\',{{bubbles:true}}));}}}})()">'
1344
- f'{q}</button>'
1345
- for q in examples
1346
- ) + "</div>")
1347
-
1348
- # ── CENTRE: chat ───────────────────────────────────────────────
1349
- with gr.Column(scale=3, elem_classes="centre-col"):
1350
- gr.ChatInterface(
1351
- fn=chat_respond,
1352
- cache_examples=False,
1353
  )
 
 
 
 
 
1354
  gr.HTML(TOOLBAR_HTML)
1355
 
1356
- # ── RIGHT: file list ───────────────────────────────────────────
1357
- with gr.Column(scale=1, elem_classes="right-col"):
1358
  gr.HTML("""
1359
- <div class="right-col-hdr">
1360
- <span class="right-col-title">Knowledge Base</span>
1361
- <div class="add-file-btn-wrap">
1362
- <button class="add-file-btn"
1363
- onclick="document.getElementById('kb-file-attach').click()">
1364
  + Add file
1365
  </button>
1366
- <div class="add-file-tooltip">
1367
  <strong>Add to knowledge base</strong>
1368
- <div class="tt-row">Formats: .txt Β· .md Β· .pdf Β· .docx</div>
1369
- <div class="tt-row">Max size: 10 MB per file</div>
1370
- <div class="tt-row">Multiple files at once supported</div>
1371
- <div class="tt-row">Index rebuilds automatically</div>
1372
  </div>
1373
  </div>
1374
  </div>""")
1375
 
1376
- kb_file_list = gr.HTML(value=list_kb_files_html)
1377
-
1378
- # Hidden upload status
1379
  upload_status = gr.Markdown(visible=False)
1380
 
1381
- # ── Hidden Gradio widgets for backend actions ──────────────────────
1382
- # These are invisible β€” actions are triggered by JS above
1383
  with gr.Row(visible=False):
1384
  toolbar_upload = gr.File(
1385
  label="", file_count="multiple",
@@ -1387,17 +1378,15 @@ with gr.Blocks(title=CONFIG["client"]["name"]) as demo:
1387
  elem_id="kb-file-attach",
1388
  )
1389
  toolbar_status = gr.Markdown()
1390
-
1391
- delete_input = gr.Textbox(elem_id="kb-hidden-delete", show_label=False)
1392
- delete_btn = gr.Button("del", elem_id="kb-delete-trigger")
1393
- delete_status= gr.Markdown()
1394
-
1395
  old_name_input = gr.Textbox(elem_id="kb-hidden-old-name", show_label=False)
1396
  new_name_input = gr.Textbox(elem_id="kb-hidden-new-name", show_label=False)
1397
  rename_btn = gr.Button("ren", elem_id="kb-rename-trigger")
1398
  rename_status = gr.Markdown()
1399
 
1400
- # ── Event wiring ──────────────────────────────────────────────────
1401
  def handle_upload(files):
1402
  status, _ = upload_kb_files(files)
1403
  return status, list_kb_files_html()
@@ -1410,9 +1399,9 @@ with gr.Blocks(title=CONFIG["client"]["name"]) as demo:
1410
  status, _ = rename_kb_file(old, new)
1411
  return status, list_kb_files_html()
1412
 
1413
- toolbar_upload.change(handle_upload, inputs=[toolbar_upload], outputs=[toolbar_status, kb_file_list])
1414
- delete_btn.click( handle_delete, inputs=[delete_input], outputs=[delete_status, kb_file_list])
1415
- rename_btn.click( handle_rename, inputs=[old_name_input, new_name_input], outputs=[rename_status, kb_file_list])
1416
 
1417
 
1418
  # -----------------------------
 
751
  return f"❌ Could not rename: {e}", list_kb_files_html()
752
 
753
 
 
754
 
 
 
755
 
756
+ # ── CSS + JS ────────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
757
 
758
+ STYLE_HTML = """
759
+ <link rel="preconnect" href="https://fonts.googleapis.com">
760
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
761
+ <style>
762
+ /* ─── Reset & tokens ───────────────────────────────────────── */
763
+ :root {
764
+ --c-bg: #F6F1E9;
765
+ --c-surface: #FDFAF5;
766
+ --c-border: #E2D9CE;
767
+ --c-border2: #C9BFB3;
768
+ --c-ink: #1E1C1A;
769
+ --c-ink2: #4A4540;
770
+ --c-muted: #7A7167;
771
+ --c-muted2: #A09890;
772
+ --c-sage: #5C7E62;
773
+ --c-sage-h: #4A6550;
774
+ --c-sage-bg: #EAF2EB;
775
+ --c-sage-bg2: #D4E8D6;
776
+ --c-rose: #B5626A;
777
+ --c-rose-bg: #F8EDEE;
778
+ --r: 10px;
779
+ --r2: 7px;
780
+ --r3: 5px;
781
+ }
782
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
783
+ html, body, .gradio-container {
784
+ height: 100% !important;
785
+ background: var(--c-bg) !important;
786
+ font-family: "Inter", ui-sans-serif, system-ui, sans-serif !important;
787
+ font-size: 14px !important;
788
+ color: var(--c-ink) !important;
789
+ -webkit-font-smoothing: antialiased !important;
790
+ }
791
+ .gradio-container { max-width: 100% !important; padding: 0 !important; overflow: hidden !important; }
792
+ footer, .footer { display: none !important; }
793
+
794
+ /* ─── Header ────────────────────────────────────────────────── */
795
+ .h-bar {
796
+ height: 52px;
797
+ background: var(--c-surface);
798
+ border-bottom: 1px solid var(--c-border);
799
+ display: flex;
800
+ align-items: center;
801
+ padding: 0 20px;
802
+ gap: 11px;
803
+ position: sticky;
804
+ top: 0;
805
+ z-index: 50;
806
+ }
807
+ .h-logo {
808
+ width: 32px; height: 32px;
809
+ background: var(--c-sage);
810
+ border-radius: 8px;
811
  display: flex; align-items: center; justify-content: center;
812
+ font-size: 15px; flex-shrink: 0;
813
+ box-shadow: 0 1px 4px rgba(92,126,98,.4);
814
  }
815
+ .h-name {
816
+ font-size: 14px; font-weight: 600;
817
+ color: var(--c-ink); letter-spacing: -.01em;
818
  }
819
+ .h-sub { font-size: 11.5px; color: var(--c-muted); margin-top: 1px; }
820
+ .h-badge {
 
821
  margin-left: auto;
822
+ font-size: 10.5px; font-weight: 500; letter-spacing: .02em;
823
+ color: var(--c-sage); background: var(--c-sage-bg);
824
+ border: 1px solid var(--c-sage-bg2);
825
+ border-radius: 999px; padding: 3px 10px;
826
  }
827
 
828
+ /* ─── 3-col layout ──────────────────────────────────────────── */
829
+ .body-grid {
830
+ display: grid !important;
831
+ grid-template-columns: 196px 1fr 218px !important;
832
+ height: calc(100vh - 52px) !important;
833
+ overflow: hidden !important;
834
+ gap: 0 !important;
835
  }
836
+ @media (max-width: 860px) {
837
+ .body-grid { grid-template-columns: 1fr !important; height: auto !important; overflow: visible !important; }
838
+ .left-panel, .right-panel { display: none !important; }
839
  }
840
 
841
+ /* ─── Left panel: suggestions ───────────────────────────────── */
842
+ .left-panel {
843
+ border-right: 1px solid var(--c-border);
844
+ background: var(--c-surface);
845
  display: flex; flex-direction: column; overflow: hidden;
846
  }
847
+ .lp-head {
848
+ padding: 14px 14px 10px;
849
+ font-size: 10px; font-weight: 600; letter-spacing: .08em;
850
+ text-transform: uppercase; color: var(--c-muted2);
851
+ border-bottom: 1px solid var(--c-border);
852
  display: flex; align-items: center; gap: 6px;
853
  }
854
+ .lp-head::before {
855
  content: "";
856
+ width: 5px; height: 5px;
857
+ background: var(--c-sage); border-radius: 50%; flex-shrink: 0;
858
+ }
859
+ .lp-body { overflow-y: auto; padding: 10px 10px 16px; flex: 1; }
860
+ .s-pill {
861
+ width: 100%; display: flex; align-items: flex-start; gap: 8px;
862
+ text-align: left; background: var(--c-surface);
863
+ border: 1px solid var(--c-border); border-radius: var(--r);
864
+ padding: 9px 11px; font-size: 12px; font-weight: 400;
865
+ color: var(--c-ink2); cursor: pointer; margin-bottom: 7px;
866
+ line-height: 1.45; transition: all .15s ease;
867
+ opacity: 0; animation: fadeUp .3s ease forwards;
868
+ }
869
+ .s-pill:nth-child(1) { animation-delay: .04s; }
870
+ .s-pill:nth-child(2) { animation-delay: .10s; }
871
+ .s-pill:nth-child(3) { animation-delay: .16s; }
872
+ .s-pill:nth-child(4) { animation-delay: .22s; }
873
+ .s-pill::before {
874
+ content: "β†’"; color: var(--c-sage); font-size: 13px;
875
+ flex-shrink: 0; margin-top: 0; line-height: 1.45;
876
+ transition: transform .15s ease;
877
+ }
878
+ .s-pill:hover {
879
+ background: var(--c-sage-bg); border-color: var(--c-sage);
880
+ color: var(--c-sage-h); transform: translateX(2px);
881
+ }
882
+ .s-pill:hover::before { transform: translateX(2px); }
883
+ @keyframes fadeUp {
884
+ from { opacity: 0; transform: translateY(5px); }
885
+ to { opacity: 1; transform: translateY(0); }
 
 
 
 
 
886
  }
887
 
888
+ /* ─── Centre: chat ───────────────────────���───────────────────── */
889
+ .chat-col {
890
+ display: flex !important; flex-direction: column !important;
891
+ overflow: hidden !important; background: var(--c-bg) !important;
892
  }
893
 
894
+ /* Strip ALL Gradio chrome from the chatbot */
895
+ [data-testid="chatbot"], .gr-chatbot {
896
+ flex: 1 !important; border: none !important;
897
+ border-radius: 0 !important; background: transparent !important;
898
+ box-shadow: none !important;
899
+ }
900
+ /* Kill label every possible way */
901
+ [data-testid="chatbot"] .label-wrap,
902
  [data-testid="chatbot"] > .label-wrap,
 
903
  [data-testid="chatbot"] label,
904
+ .block > .label-wrap,
905
+ .label-wrap { display: none !important; }
 
 
 
 
 
 
 
 
 
906
 
907
+ /* ─── Bubbles ────────────────────────────────────────────────── */
908
+ @keyframes bubIn {
909
+ from { opacity: 0; transform: translateY(6px); }
 
910
  to { opacity: 1; transform: translateY(0); }
911
  }
912
+ .message { animation: bubIn .2s ease; }
913
+
914
  .message.user > div {
915
+ background: var(--c-sage) !important;
916
  color: #fff !important;
917
+ border-radius: var(--r) var(--r) 3px var(--r) !important;
918
+ padding: 9px 13px !important;
919
+ max-width: 72% !important;
920
+ font-size: 13.5px !important; line-height: 1.6 !important;
921
+ box-shadow: 0 2px 8px rgba(92,126,98,.28) !important;
922
  }
923
  .message.bot > div {
924
+ background: var(--c-surface) !important;
925
+ border: 1px solid var(--c-border) !important;
926
+ border-radius: var(--r) var(--r) var(--r) 3px !important;
927
+ padding: 11px 14px !important;
928
+ max-width: 80% !important;
929
+ font-size: 13.5px !important; line-height: 1.68 !important;
930
+ box-shadow: 0 1px 4px rgba(0,0,0,.07) !important;
931
+ color: var(--c-ink) !important;
 
 
 
932
  }
933
 
934
+ /* ─── Input card (textarea + toolbar as one unit) ────────────── */
935
  .chat-input-container {
936
+ background: var(--c-surface) !important;
937
+ border-top: 1px solid var(--c-border) !important;
938
+ padding: 10px 14px 10px !important;
939
+ flex-shrink: 0 !important;
940
  }
941
+ /* The outer input row Gradio wraps */
942
  .chat-input-container > .wrap,
943
  .chat-input-container > div:first-child {
944
+ border: 1.5px solid var(--c-border) !important;
945
+ border-radius: var(--r) !important;
946
+ background: var(--c-bg) !important;
947
+ overflow: hidden !important;
948
+ transition: border-color .18s, box-shadow .18s !important;
949
  }
950
  .chat-input-container > .wrap:focus-within,
951
  .chat-input-container > div:first-child:focus-within {
952
+ border-color: var(--c-sage) !important;
953
+ box-shadow: 0 0 0 3px rgba(92,126,98,.12) !important;
954
  }
955
  .chat-input-container textarea {
956
+ background: var(--c-bg) !important;
957
+ border: none !important; border-radius: 0 !important;
958
+ padding: 11px 13px !important;
959
+ font-size: 13.5px !important; font-family: "Inter", sans-serif !important;
960
+ color: var(--c-ink) !important; resize: none !important;
961
  line-height: 1.55 !important;
962
  }
963
  .chat-input-container textarea:focus { outline: none !important; box-shadow: none !important; }
964
+ .chat-input-container textarea::placeholder { color: var(--c-muted2) !important; }
965
+
966
+ /* Send button */
967
+ .chat-input-container button[aria-label="Send"],
968
+ .chat-input-container button.send-btn {
969
+ background: var(--c-sage) !important; color: #fff !important;
970
+ border: none !important; border-radius: var(--r2) !important;
971
+ padding: 0 14px !important; height: 34px !important;
972
+ font-size: 12.5px !important; font-weight: 600 !important;
973
+ margin: 0 6px 6px 0 !important; align-self: flex-end !important;
974
+ transition: background .14s, transform .1s !important;
975
+ box-shadow: 0 1px 4px rgba(92,126,98,.3) !important;
976
  }
977
  .chat-input-container button[aria-label="Send"]:hover {
978
+ background: var(--c-sage-h) !important;
979
+ transform: translateY(-1px) !important;
980
+ }
981
+
982
+ /* ─── Toolbar fused below textarea ───────────────────────────── */
983
+ .tb {
984
+ display: flex; align-items: center; gap: 1px;
985
+ padding: 4px 9px 6px;
986
+ background: var(--c-bg);
987
+ border-top: 1px solid var(--c-border);
988
+ border-radius: 0 0 var(--r2) var(--r2);
989
+ margin: 0 -1px -1px;
990
+ }
991
+ .tb-b {
992
+ width: 24px; height: 24px;
993
+ display: inline-flex; align-items: center; justify-content: center;
994
+ background: none; border: none; border-radius: var(--r3);
995
+ cursor: pointer; color: var(--c-muted);
996
+ transition: background .12s, color .12s; position: relative; flex-shrink: 0;
997
+ }
998
+ .tb-b svg { width: 12px; height: 12px; pointer-events: none; display: block; }
999
+ .tb-b:hover { background: var(--c-sage-bg); color: var(--c-sage); }
1000
+ .tb-div { width: 1px; height: 12px; background: var(--c-border2); margin: 0 3px; flex-shrink: 0; }
1001
+ .tb-b[data-tip]::after {
1002
+ content: attr(data-tip);
1003
+ position: absolute; bottom: calc(100% + 5px); left: 50%;
1004
+ transform: translateX(-50%);
1005
+ background: var(--c-ink); color: #fff;
1006
+ font-size: 10px; padding: 3px 7px; border-radius: 4px;
1007
+ white-space: nowrap; pointer-events: none;
1008
+ opacity: 0; transition: opacity .12s; z-index: 99;
1009
  font-family: "Inter", sans-serif;
1010
  }
1011
+ .tb-b:hover[data-tip]::after { opacity: 1; }
1012
 
1013
+ /* ─── Right panel: files ─────────────────────────────────────── */
1014
+ .right-panel {
1015
+ border-left: 1px solid var(--c-border);
1016
+ background: var(--c-surface);
1017
  display: flex; flex-direction: column; overflow: hidden;
1018
  }
1019
+ .rp-head {
1020
+ padding: 13px 14px 11px;
1021
+ border-bottom: 1px solid var(--c-border);
1022
+ flex-shrink: 0; display: flex; align-items: center;
1023
  }
1024
+ .rp-title {
1025
+ font-size: 10px; font-weight: 600; letter-spacing: .08em;
1026
+ text-transform: uppercase; color: var(--c-muted2);
1027
  display: flex; align-items: center; gap: 6px;
1028
  }
1029
+ .rp-title::before {
1030
  content: "";
1031
+ width: 5px; height: 5px;
1032
+ background: var(--c-sage); border-radius: 50%;
1033
  }
1034
+ .rp-count {
1035
+ font-size: 10px; font-weight: 600; color: var(--c-sage);
1036
+ background: var(--c-sage-bg); border: 1px solid var(--c-sage-bg2);
1037
+ border-radius: 999px; padding: 1px 7px; margin-left: 6px;
 
1038
  }
 
1039
  /* + Add file */
1040
+ .add-wrap { position: relative; margin-left: auto; }
1041
+ .add-btn {
1042
+ display: flex; align-items: center; gap: 4px;
1043
+ background: var(--c-sage-bg); border: 1px solid var(--c-sage-bg2);
1044
+ color: var(--c-sage-h); border-radius: var(--r2);
1045
+ padding: 5px 11px; font-size: 12px; font-weight: 600;
1046
+ cursor: pointer; transition: all .14s; white-space: nowrap;
1047
+ }
1048
+ .add-btn:hover {
1049
+ background: var(--c-sage); color: #fff;
1050
+ border-color: var(--c-sage);
1051
+ box-shadow: 0 1px 6px rgba(92,126,98,.3);
1052
+ }
1053
+ .add-tip {
1054
+ display: none; position: absolute; top: calc(100% + 7px); right: 0;
1055
+ background: var(--c-ink); color: #f5f5f0;
1056
+ border-radius: var(--r2); padding: 11px 13px;
1057
+ font-size: 11px; line-height: 1.7; min-width: 206px;
1058
+ box-shadow: 0 6px 24px rgba(0,0,0,.18); z-index: 60;
1059
+ }
1060
+ .add-tip strong { display: block; margin-bottom: 5px; font-size: 12px; color: #fff; }
1061
+ .add-tip em { display: flex; align-items: center; gap: 5px; opacity: .82; font-style: normal; }
1062
+ .add-tip em::before { content: "β€’"; color: #9ecfa4; }
1063
+ .add-tip::before {
1064
+ content: ""; position: absolute; top: -5px; right: 13px;
1065
+ border: 5px solid transparent; border-top: 0;
1066
+ border-bottom-color: var(--c-ink);
1067
+ }
1068
+ .add-wrap:hover .add-tip { display: block; }
1069
+
1070
+ .rp-body { overflow-y: auto; padding: 4px 8px 16px; flex: 1; }
1071
+ .rp-count-line {
1072
+ font-size: 10.5px; color: var(--c-muted2); padding: 8px 6px 4px;
1073
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1074
 
1075
  /* File rows */
1076
+ .f-row {
1077
+ display: flex; align-items: center; gap: 7px;
1078
+ padding: 7px 8px; border-radius: var(--r2);
1079
+ transition: background .12s; position: relative;
1080
+ }
1081
+ .f-row:hover { background: var(--c-bg); }
1082
+ .f-ico { font-size: 13px; flex-shrink: 0; opacity: .75; }
1083
+ .f-name {
1084
+ flex: 1; font-size: 12px; color: var(--c-ink2);
1085
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1086
  }
1087
+ .f-acts { display: none; gap: 1px; align-items: center; flex-shrink: 0; }
1088
+ .f-row:hover .f-acts { display: flex; }
1089
+ .f-btn {
1090
+ background: none; border: none; cursor: pointer;
1091
+ font-size: 12px; padding: 3px 4px; border-radius: 4px;
1092
+ color: var(--c-muted); transition: background .11s, color .11s; line-height: 1;
 
 
1093
  }
1094
+ .f-btn:hover { background: var(--c-border); color: var(--c-ink); }
1095
+ .f-btn.del:hover { background: var(--c-rose-bg); color: var(--c-rose); }
1096
 
1097
+ /* Rename row */
1098
+ .f-rename { display: none; padding: 5px 8px; gap: 5px; align-items: center; }
1099
+ .f-rename.on { display: flex; }
1100
+ .f-rename input {
1101
+ flex: 1; border: 1.5px solid var(--c-sage); border-radius: var(--r3);
1102
+ padding: 4px 8px; font-size: 11px; background: var(--c-bg);
1103
+ color: var(--c-ink); outline: none; font-family: "Inter", sans-serif;
 
 
1104
  }
1105
+ .f-rename .save {
1106
+ background: var(--c-sage); color: #fff; border: none;
1107
+ border-radius: var(--r3); padding: 4px 9px;
1108
+ font-size: 11px; font-weight: 600; cursor: pointer;
1109
  }
1110
+ .f-rename .cancel {
1111
+ background: none; border: 1px solid var(--c-border);
1112
+ border-radius: var(--r3); padding: 4px 7px;
1113
+ font-size: 11px; color: var(--c-muted); cursor: pointer;
1114
  }
1115
 
1116
+ /* ─── Source chips on bot replies ────────────────────────────── */
 
 
 
 
 
 
 
 
 
 
 
 
1117
  .src-row {
1118
  display: flex; align-items: center; flex-wrap: wrap; gap: 5px;
1119
  margin-top: 10px; padding-top: 9px;
1120
+ border-top: 1px solid var(--c-border);
1121
  }
1122
+ .src-lbl {
1123
+ font-size: 9.5px; font-weight: 700; letter-spacing: .08em;
1124
+ text-transform: uppercase; color: var(--c-muted2); flex-shrink: 0; margin-right: 1px;
 
1125
  }
1126
  .src-chip {
1127
  display: inline-flex; align-items: center; gap: 4px;
1128
+ background: var(--c-sage-bg); border: 1px solid var(--c-sage-bg2);
1129
  border-radius: 999px; padding: 2px 10px 2px 7px;
1130
+ animation: chipIn .2s ease;
1131
+ transition: background .13s, border-color .13s;
1132
  }
1133
+ .src-chip:hover { background: var(--c-sage-bg2); border-color: var(--c-sage); }
1134
  @keyframes chipIn {
1135
+ from { opacity:0; transform: scale(.92) translateY(3px); }
1136
+ to { opacity:1; transform: scale(1) translateY(0); }
1137
  }
1138
+ .src-chip-icon { font-size: 10.5px; }
1139
  .src-chip-name {
1140
+ font-size: 11px; font-weight: 500; color: var(--c-sage-h);
1141
+ max-width: 130px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
 
1142
  }
1143
 
1144
+ /* ─── Toast ──────────────────────────────────────────────────── */
1145
+ .toast {
1146
+ position: fixed; bottom: 20px; left: 50%;
1147
+ transform: translateX(-50%) translateY(8px);
1148
+ background: var(--c-ink); color: #fff;
1149
+ border-radius: var(--r2); padding: 8px 18px;
1150
+ font-size: 12px; z-index: 9999; opacity: 0;
1151
+ pointer-events: none; transition: opacity .2s, transform .2s;
1152
+ box-shadow: 0 4px 16px rgba(0,0,0,.2); white-space: nowrap;
1153
+ }
1154
+ .toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
1155
 
1156
+ /* ─── Scrollbars ─────────────────────────────────────────────── */
1157
+ * { scrollbar-width: thin; scrollbar-color: var(--c-border2) transparent; }
1158
+ ::-webkit-scrollbar { width: 4px; }
1159
+ ::-webkit-scrollbar-thumb { background: var(--c-border2); border-radius: 99px; }
1160
+
1161
+ /* ─── Text selection ─────────────────────────────────────────── */
1162
+ ::selection { background: rgba(92,126,98,.18); }
1163
  </style>
1164
 
1165
  <script>
1166
+ /* ── Action dispatcher ────────────────────────────── */
1167
+ function kbAction(action, name) {
1168
  if (action === 'delete') {
1169
+ if (!confirm('Delete ' + name + '?')) return;
1170
+ _setGI('kb-hidden-delete', name);
1171
+ setTimeout(function(){ _clickGI('kb-delete-trigger'); }, 80);
1172
  } else if (action === 'replace') {
1173
  document.getElementById('kb-file-attach').click();
 
 
1174
  } else if (action === 'rename') {
1175
+ var r = document.getElementById('row-' + name);
1176
+ var rr= document.getElementById('rrow-' + name);
1177
+ if (r) r.style.display = 'none';
1178
+ if (rr){ rr.classList.add('on'); rr.querySelector('input').focus(); }
1179
  }
1180
  }
1181
+ function kbCancelRename(name) {
1182
+ var r = document.getElementById('row-' + name);
1183
+ var rr= document.getElementById('rrow-' + name);
1184
+ if (r) r.style.display = '';
1185
+ if (rr) rr.classList.remove('on');
1186
+ }
1187
+ function kbSaveRename(old) {
1188
+ var rr = document.getElementById('rrow-' + old);
1189
+ var v = rr ? rr.querySelector('input').value.trim() : '';
1190
+ if (!v) return;
1191
+ _setGI('kb-hidden-old-name', old);
1192
+ _setGI('kb-hidden-new-name', v);
1193
+ setTimeout(function(){ _clickGI('kb-rename-trigger'); }, 80);
1194
+ }
1195
+ function _setGI(id, val) {
1196
+ var el = document.querySelector('#' + id + ' input, #' + id + ' textarea');
1197
  if (!el) return;
1198
+ el.value = val;
1199
+ ['input','change'].forEach(function(e){ el.dispatchEvent(new Event(e,{bubbles:true})); });
 
1200
  }
1201
+ function _clickGI(id) {
1202
+ var b = document.querySelector('#' + id + ' button, #' + id);
1203
+ if (b) b.click();
1204
  }
1205
  function showToast(msg) {
1206
+ var t = document.getElementById('app-toast');
1207
  if (!t) return;
1208
  t.textContent = msg; t.classList.add('show');
1209
+ setTimeout(function(){ t.classList.remove('show'); }, 2600);
1210
  }
1211
+ /* ── Kill "Chatbot" label via DOM walk ──────────────── */
1212
+ function killLabel() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1213
  document.querySelectorAll('label, span, div').forEach(function(el) {
1214
+ if (!el.children.length && el.textContent.trim() === 'Chatbot') {
1215
  el.style.display = 'none';
1216
+ var w = el.closest('[class*="label"]');
1217
+ if (w) w.style.display = 'none';
1218
  }
1219
  });
1220
  }
1221
+ new MutationObserver(killLabel).observe(document.documentElement, { childList:true, subtree:true });
1222
+ setTimeout(killLabel, 400);
1223
+ setTimeout(killLabel, 1200);
 
 
 
 
 
1224
 
1225
+ /* ── Pill click β†’ fill textarea ─────────────────────── */
1226
+ function fillChat(text) {
1227
+ var t = document.querySelector('.chat-input-container textarea');
1228
+ if (!t) return;
1229
+ t.value = text;
1230
+ t.dispatchEvent(new Event('input', { bubbles: true }));
1231
+ t.focus();
1232
+ }
1233
+ /* ── Toolbar helpers ────────────────────────────────── */
1234
+ function tbWrap(a, b) {
1235
  var t = document.querySelector('.chat-input-container textarea');
1236
  if (!t) return;
1237
  var s = t.selectionStart, e = t.selectionEnd, v = t.value;
1238
  if (s === e) return;
1239
+ t.value = v.slice(0,s)+a+v.slice(s,e)+b+v.slice(e);
1240
+ t.selectionStart = s; t.selectionEnd = e+a.length+b.length;
1241
  t.dispatchEvent(new Event('input',{bubbles:true}));
1242
  }
1243
  function tbBullet() {
1244
  var t = document.querySelector('.chat-input-container textarea');
1245
  if (!t) return;
1246
+ var ls = t.value.split('\n'), idx = t.value.slice(0,t.selectionStart).split('\n').length-1;
1247
+ ls[idx] = '- '+ls[idx]; t.value = ls.join('\n');
1248
  t.dispatchEvent(new Event('input',{bubbles:true}));
1249
  }
1250
  function tbPaste() {
 
1252
  if (!t) return;
1253
  navigator.clipboard.readText().then(function(s){
1254
  var i = t.selectionStart;
1255
+ t.value = t.value.slice(0,i)+s+t.value.slice(t.selectionEnd);
1256
+ t.selectionStart = t.selectionEnd = i+s.length;
1257
  t.dispatchEvent(new Event('input',{bubbles:true}));
1258
  }).catch(function(){ t.focus(); });
1259
  }
1260
+ </script>
1261
+ """
1262
 
1263
  TOOLBAR_HTML = """
1264
+ <div class="tb">
1265
+ <button class="tb-b" data-tip="Attach file" onclick="document.getElementById('kb-file-attach').click()">
1266
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1267
+ <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"/>
1268
+ </svg>
1269
  </button>
1270
+ <button class="tb-b" data-tip="Paste" onclick="tbPaste()">
1271
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1272
+ <rect x="8" y="2" width="8" height="4" rx="1"/>
1273
+ <path d="M16 4h2a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2h2"/>
1274
+ </svg>
1275
  </button>
1276
+ <div class="tb-div"></div>
1277
+ <button class="tb-b" data-tip="Bold" onclick="tbWrap('**','**')">
1278
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
1279
+ <path d="M6 4h8a4 4 0 010 8H6zM6 12h9a4 4 0 010 8H6z"/>
1280
+ </svg>
1281
  </button>
1282
+ <button class="tb-b" data-tip="Italic" onclick="tbWrap('*','*')">
1283
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1284
+ <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"/>
1285
+ </svg>
1286
  </button>
1287
+ <button class="tb-b" data-tip="Bullet" onclick="tbBullet()">
1288
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1289
+ <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"/>
1290
+ <circle cx="4" cy="6" r="1.5" fill="currentColor" stroke="none"/>
1291
+ <circle cx="4" cy="12" r="1.5" fill="currentColor" stroke="none"/>
1292
+ <circle cx="4" cy="18" r="1.5" fill="currentColor" stroke="none"/>
1293
+ </svg>
1294
  </button>
1295
+ <button class="tb-b" data-tip="Code" onclick="tbWrap('`','`')">
1296
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1297
+ <polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>
1298
+ </svg>
1299
  </button>
1300
+ </div>
1301
+ """
1302
+
1303
+ # ── Examples ──────────────────────────────────────────────────────────────────
1304
 
1305
  examples = [
1306
  q["query"] for q in CONFIG.get("quick_actions", []) if "query" in q
 
1310
  "Why is self-service important for customer support?",
1311
  ]
1312
 
1313
+ # ── Gradio layout ─────────────────────────────────────────────────────────────
1314
+
1315
  with gr.Blocks(title=CONFIG["client"]["name"]) as demo:
1316
 
1317
  gr.HTML(STYLE_HTML)
1318
+ gr.HTML('<div id="app-toast" class="toast"></div>')
1319
 
1320
+ # Header
1321
  gr.HTML(f"""
1322
+ <div class="h-bar">
1323
+ <div class="h-logo">πŸ€–</div>
1324
  <div>
1325
+ <div class="h-name">{CONFIG["client"]["name"]}</div>
1326
+ <div class="h-sub">Search the knowledge base and get answers in plain language</div>
1327
  </div>
1328
+ <span class="h-badge">MVP Β· AI-powered</span>
1329
  </div>""")
1330
 
1331
+ with gr.Row(elem_classes="body-grid"):
1332
+
1333
+ # ── Left: suggestions ──────────────────────────────────────────
1334
+ with gr.Column(scale=1, elem_classes="left-panel"):
1335
+ pills_html = (
1336
+ '<div class="lp-head">Try asking</div>'
1337
+ '<div class="lp-body">'
1338
+ + "".join(
1339
+ f'<button class="s-pill" onclick="fillChat({repr(q)})">{q}</button>'
1340
+ for q in examples
1341
+ )
1342
+ + "</div>"
 
 
 
 
 
 
 
 
1343
  )
1344
+ gr.HTML(pills_html)
1345
+
1346
+ # ── Centre: chat ───────────────────────────────────────────────
1347
+ with gr.Column(scale=3, elem_classes="chat-col"):
1348
+ gr.ChatInterface(fn=chat_respond, cache_examples=False)
1349
  gr.HTML(TOOLBAR_HTML)
1350
 
1351
+ # ── Right: file list ───────────────────────────────────────────
1352
+ with gr.Column(scale=1, elem_classes="right-panel"):
1353
  gr.HTML("""
1354
+ <div class="rp-head">
1355
+ <span class="rp-title">Knowledge Base</span>
1356
+ <div class="add-wrap">
1357
+ <button class="add-btn" onclick="document.getElementById('kb-file-attach').click()">
 
1358
  + Add file
1359
  </button>
1360
+ <div class="add-tip">
1361
  <strong>Add to knowledge base</strong>
1362
+ <em>Formats: .txt &nbsp;.md &nbsp;.pdf &nbsp;.docx</em>
1363
+ <em>Max 10 MB per file</em>
1364
+ <em>Multiple files at once</em>
1365
+ <em>Index rebuilds automatically</em>
1366
  </div>
1367
  </div>
1368
  </div>""")
1369
 
1370
+ kb_file_list = gr.HTML(value=list_kb_files_html)
 
 
1371
  upload_status = gr.Markdown(visible=False)
1372
 
1373
+ # Hidden Gradio widgets ────────────────────────────────────────────────────
 
1374
  with gr.Row(visible=False):
1375
  toolbar_upload = gr.File(
1376
  label="", file_count="multiple",
 
1378
  elem_id="kb-file-attach",
1379
  )
1380
  toolbar_status = gr.Markdown()
1381
+ delete_input = gr.Textbox(elem_id="kb-hidden-delete", show_label=False)
1382
+ delete_btn = gr.Button("del", elem_id="kb-delete-trigger")
1383
+ delete_status = gr.Markdown()
 
 
1384
  old_name_input = gr.Textbox(elem_id="kb-hidden-old-name", show_label=False)
1385
  new_name_input = gr.Textbox(elem_id="kb-hidden-new-name", show_label=False)
1386
  rename_btn = gr.Button("ren", elem_id="kb-rename-trigger")
1387
  rename_status = gr.Markdown()
1388
 
1389
+ # Event wiring ─────────────────────────────────────────────────────────────
1390
  def handle_upload(files):
1391
  status, _ = upload_kb_files(files)
1392
  return status, list_kb_files_html()
 
1399
  status, _ = rename_kb_file(old, new)
1400
  return status, list_kb_files_html()
1401
 
1402
+ toolbar_upload.change(handle_upload, inputs=[toolbar_upload], outputs=[toolbar_status, kb_file_list])
1403
+ delete_btn.click( handle_delete, inputs=[delete_input], outputs=[delete_status, kb_file_list])
1404
+ rename_btn.click( handle_rename, inputs=[old_name_input, new_name_input], outputs=[rename_status, kb_file_list])
1405
 
1406
 
1407
  # -----------------------------