pkheria commited on
Commit
7dcc090
Β·
1 Parent(s): 33c0d0b
Files changed (4) hide show
  1. README.md +1 -1
  2. app/ui/gradio_app.py +21 -36
  3. app/ui/theme.py +340 -145
  4. uv.lock +19 -0
README.md CHANGED
@@ -5,7 +5,7 @@ colorFrom: blue
5
  colorTo: purple
6
  sdk: gradio
7
  app_file: app.py
8
- pinned: false
9
  license: mit
10
  short_description: AI knowledge hub for groups, powered by Nvidia
11
  ---
 
5
  colorTo: purple
6
  sdk: gradio
7
  app_file: app.py
8
+ pinned: true
9
  license: mit
10
  short_description: AI knowledge hub for groups, powered by Nvidia
11
  ---
app/ui/gradio_app.py CHANGED
@@ -10,14 +10,19 @@ from app.utils.zerogpu import gpu
10
  logger = logging.getLogger(__name__)
11
 
12
  THEME = gr.themes.Base(
13
- primary_hue="cyan",
14
- secondary_hue="lime",
15
- neutral_hue="slate",
16
  radius_size="sm",
17
  font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
18
  font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "ui-monospace", "monospace"],
19
  )
20
 
 
 
 
 
 
21
 
22
  def _format_metadata(metadata: dict) -> str:
23
  if not metadata:
@@ -33,16 +38,14 @@ def _ingest(
33
  url: str,
34
  pdf_file: str | None,
35
  manual_transcript: str,
36
- chunk_size: int,
37
- chunk_overlap: int,
38
  collection_name: str,
39
  ):
40
  logger.info(
41
  "Ingest requested url=%s pdf_file=%s chunk_size=%s chunk_overlap=%s collection=%s",
42
  url,
43
  pdf_file,
44
- chunk_size,
45
- chunk_overlap,
46
  collection_name,
47
  )
48
  try:
@@ -51,8 +54,8 @@ def _ingest(
51
  result = ingest_source(
52
  url=url,
53
  pdf_path=pdf_file,
54
- chunk_size=chunk_size,
55
- chunk_overlap=chunk_overlap,
56
  collection_name=collection_name,
57
  manual_transcript=manual_transcript,
58
  )
@@ -90,12 +93,12 @@ def _ingest(
90
 
91
 
92
  @gpu()
93
- def _search(query: str, limit: int, collection_name: str):
94
- logger.info("Search requested query=%s limit=%s collection=%s", query, limit, collection_name)
95
  try:
96
  from app.services.ingestion import search_knowledge_base
97
 
98
- results = search_knowledge_base(query, limit=limit, collection_name=collection_name)
99
  except Exception as exc:
100
  if "MPS backend out of memory" in str(exc):
101
  return (
@@ -127,12 +130,12 @@ def _search(query: str, limit: int, collection_name: str):
127
 
128
 
129
  @gpu()
130
- def _answer(query: str, limit: int, collection_name: str):
131
- logger.info("Answer requested query=%s limit=%s collection=%s", query, limit, collection_name)
132
  try:
133
  from app.services.ingestion import answer_from_knowledge_base
134
 
135
- result = answer_from_knowledge_base(query, limit=limit, collection_name=collection_name)
136
  except Exception as exc:
137
  if "MPS backend out of memory" in str(exc):
138
  return (
@@ -211,21 +214,6 @@ Extract text, chunk it cleanly, embed locally, and use NVIDIA chat for grounded
211
  placeholder="If hosted YouTube transcript extraction is blocked, paste the transcript here and keep the YouTube URL above.",
212
  lines=5,
213
  )
214
- with gr.Row():
215
- chunk_size = gr.Slider(
216
- 400,
217
- 2500,
218
- value=settings.CHUNK_SIZE,
219
- step=50,
220
- label="Chunk size",
221
- )
222
- chunk_overlap = gr.Slider(
223
- 0,
224
- 600,
225
- value=settings.CHUNK_OVERLAP,
226
- step=25,
227
- label="Chunk overlap",
228
- )
229
  collection_name_ingest = gr.Textbox(
230
  label="Collection Name",
231
  value=settings.QDRANT_COLLECTION_NAME,
@@ -276,8 +264,6 @@ Extract text, chunk it cleanly, embed locally, and use NVIDIA chat for grounded
276
  source_url,
277
  pdf_file,
278
  manual_transcript,
279
- chunk_size,
280
- chunk_overlap,
281
  collection_name_ingest,
282
  ],
283
  outputs=[
@@ -296,14 +282,13 @@ Extract text, chunk it cleanly, embed locally, and use NVIDIA chat for grounded
296
  with gr.Row(equal_height=True):
297
  with gr.Column(scale=3, elem_classes=["kh-panel"]):
298
  gr.Markdown(
299
- "### Retrieval Probe\n<div class='kh-subhead'>Run a quick similarity search against the same Qdrant collection after ingestion.</div>"
300
  )
301
  query = gr.Textbox(
302
  label="Search query",
303
  placeholder="Ask a question or enter keywords",
304
  lines=4,
305
  )
306
- limit = gr.Slider(1, 10, value=5, step=1, label="Results")
307
  collection_name_retrieve = gr.Textbox(
308
  label="Collection Name",
309
  value=settings.QDRANT_COLLECTION_NAME,
@@ -326,12 +311,12 @@ Extract text, chunk it cleanly, embed locally, and use NVIDIA chat for grounded
326
 
327
  search_btn.click(
328
  fn=_search,
329
- inputs=[query, limit, collection_name_retrieve],
330
  outputs=search_results,
331
  )
332
  answer_btn.click(
333
  fn=_answer,
334
- inputs=[query, limit, collection_name_retrieve],
335
  outputs=[answer_output, reasoning_output, search_results],
336
  )
337
 
 
10
  logger = logging.getLogger(__name__)
11
 
12
  THEME = gr.themes.Base(
13
+ primary_hue="teal",
14
+ secondary_hue="yellow",
15
+ neutral_hue="stone",
16
  radius_size="sm",
17
  font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
18
  font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "ui-monospace", "monospace"],
19
  )
20
 
21
+ # Fixed pipeline constants
22
+ CHUNK_SIZE = 1200
23
+ CHUNK_OVERLAP = 200
24
+ RETRIEVE_K = 3
25
+
26
 
27
  def _format_metadata(metadata: dict) -> str:
28
  if not metadata:
 
38
  url: str,
39
  pdf_file: str | None,
40
  manual_transcript: str,
 
 
41
  collection_name: str,
42
  ):
43
  logger.info(
44
  "Ingest requested url=%s pdf_file=%s chunk_size=%s chunk_overlap=%s collection=%s",
45
  url,
46
  pdf_file,
47
+ CHUNK_SIZE,
48
+ CHUNK_OVERLAP,
49
  collection_name,
50
  )
51
  try:
 
54
  result = ingest_source(
55
  url=url,
56
  pdf_path=pdf_file,
57
+ chunk_size=CHUNK_SIZE,
58
+ chunk_overlap=CHUNK_OVERLAP,
59
  collection_name=collection_name,
60
  manual_transcript=manual_transcript,
61
  )
 
93
 
94
 
95
  @gpu()
96
+ def _search(query: str, collection_name: str):
97
+ logger.info("Search requested query=%s limit=%s collection=%s", query, RETRIEVE_K, collection_name)
98
  try:
99
  from app.services.ingestion import search_knowledge_base
100
 
101
+ results = search_knowledge_base(query, limit=RETRIEVE_K, collection_name=collection_name)
102
  except Exception as exc:
103
  if "MPS backend out of memory" in str(exc):
104
  return (
 
130
 
131
 
132
  @gpu()
133
+ def _answer(query: str, collection_name: str):
134
+ logger.info("Answer requested query=%s limit=%s collection=%s", query, RETRIEVE_K, collection_name)
135
  try:
136
  from app.services.ingestion import answer_from_knowledge_base
137
 
138
+ result = answer_from_knowledge_base(query, limit=RETRIEVE_K, collection_name=collection_name)
139
  except Exception as exc:
140
  if "MPS backend out of memory" in str(exc):
141
  return (
 
214
  placeholder="If hosted YouTube transcript extraction is blocked, paste the transcript here and keep the YouTube URL above.",
215
  lines=5,
216
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  collection_name_ingest = gr.Textbox(
218
  label="Collection Name",
219
  value=settings.QDRANT_COLLECTION_NAME,
 
264
  source_url,
265
  pdf_file,
266
  manual_transcript,
 
 
267
  collection_name_ingest,
268
  ],
269
  outputs=[
 
282
  with gr.Row(equal_height=True):
283
  with gr.Column(scale=3, elem_classes=["kh-panel"]):
284
  gr.Markdown(
285
+ "### Retrieval Probe\n<div class='kh-subhead'>Run a similarity search against the Qdrant collection. Returns top 3 matches.</div>"
286
  )
287
  query = gr.Textbox(
288
  label="Search query",
289
  placeholder="Ask a question or enter keywords",
290
  lines=4,
291
  )
 
292
  collection_name_retrieve = gr.Textbox(
293
  label="Collection Name",
294
  value=settings.QDRANT_COLLECTION_NAME,
 
311
 
312
  search_btn.click(
313
  fn=_search,
314
+ inputs=[query, collection_name_retrieve],
315
  outputs=search_results,
316
  )
317
  answer_btn.click(
318
  fn=_answer,
319
+ inputs=[query, collection_name_retrieve],
320
  outputs=[answer_output, reasoning_output, search_results],
321
  )
322
 
app/ui/theme.py CHANGED
@@ -1,7 +1,7 @@
1
  HEAD = """
2
  <link rel="preconnect" href="https://fonts.googleapis.com">
3
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
4
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
5
  """
6
 
7
  JS = """
@@ -9,250 +9,445 @@ JS = """
9
  const root = document.querySelector('.gradio-container');
10
  if (!root) return;
11
  root.dataset.ready = 'true';
12
- const marker = document.createElement('div');
13
- marker.className = 'kh-scanline';
14
- root.prepend(marker);
15
  }
16
  """
17
 
18
  CSS = """
19
- :root {
20
- --kh-bg: #080b0f;
21
- --kh-surface: rgba(18, 24, 32, 0.78);
22
- --kh-surface-strong: rgba(27, 36, 48, 0.92);
23
- --kh-ink: #f7fbff;
24
- --kh-muted: #a7b4c2;
25
- --kh-soft: #d8e1ea;
26
- --kh-line: rgba(255, 255, 255, 0.12);
27
- --kh-cyan: #20d6c7;
28
- --kh-lime: #b8f45d;
29
- --kh-rose: #ff6b8a;
30
- --kh-amber: #ffcf5c;
31
- --kh-shadow: rgba(0, 0, 0, 0.32);
32
- }
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  .gradio-container {
35
- min-height: 100vh;
36
- background:
37
- radial-gradient(circle at 18% 8%, rgba(32, 214, 199, 0.22), transparent 30%),
38
- radial-gradient(circle at 86% 12%, rgba(255, 207, 92, 0.16), transparent 28%),
39
- linear-gradient(135deg, #080b0f 0%, #101720 48%, #0b1017 100%) !important;
40
- color: var(--kh-ink);
41
- font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
42
- }
43
-
44
- .kh-scanline {
45
- position: fixed;
46
- inset: 0;
47
- pointer-events: none;
48
- background-image: linear-gradient(rgba(255,255,255,0.035) 1px, transparent 1px);
49
- background-size: 100% 4px;
50
- mask-image: linear-gradient(to bottom, transparent, black 18%, black 72%, transparent);
51
- opacity: 0.18;
52
- z-index: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  }
54
 
 
55
  #kh-shell {
56
  position: relative;
57
  z-index: 1;
58
  max-width: 1220px;
59
  margin: 0 auto;
60
- padding: 28px 18px 42px;
61
  }
62
 
 
63
  #kh-title {
64
- padding: 34px 0 22px;
65
  border-bottom: 1px solid var(--kh-line);
 
66
  }
67
 
68
  #kh-title h1 {
69
- max-width: 920px;
70
- color: var(--kh-ink);
71
- font-size: clamp(2.6rem, 6vw, 6rem);
72
- font-weight: 800;
73
- line-height: 0.9;
74
- margin: 0 0 14px;
75
- letter-spacing: 0;
 
 
76
  }
77
 
78
  #kh-title p {
79
- max-width: 780px;
80
- color: var(--kh-muted);
81
- font-size: 1.04rem;
82
- line-height: 1.65;
 
83
  }
84
 
 
85
  #kh-title code,
86
  .kh-chip code {
87
- color: var(--kh-lime);
88
- background: rgba(184, 244, 93, 0.09);
89
- border: 1px solid rgba(184, 244, 93, 0.18);
90
- border-radius: 6px;
91
  padding: 2px 6px;
92
- font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
93
- }
94
-
95
- .kh-panel {
96
- border: 1px solid var(--kh-line);
97
- border-radius: 8px;
98
- background: linear-gradient(180deg, var(--kh-surface-strong), var(--kh-surface));
99
- box-shadow: 0 24px 70px var(--kh-shadow);
100
- backdrop-filter: blur(18px);
101
- padding: 18px !important;
102
- }
103
-
104
- .kh-panel label,
105
- .kh-panel .label-wrap span {
106
- color: var(--kh-soft) !important;
107
- font-weight: 700 !important;
108
- }
109
-
110
- .kh-subhead {
111
- margin: 8px 0 16px;
112
- color: var(--kh-muted);
113
- font-size: 0.95rem;
114
  }
115
 
 
116
  .kh-chip-row {
117
  display: flex;
118
  flex-wrap: wrap;
119
- gap: 10px;
120
  margin-top: 18px;
 
121
  }
122
 
123
  .kh-chip {
124
  border: 1px solid var(--kh-line);
125
  border-radius: 999px;
126
- padding: 8px 12px;
127
- color: var(--kh-soft);
128
- background: rgba(255, 255, 255, 0.055);
129
- font-size: 0.9rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  }
131
 
132
- .kh-stat {
133
- min-height: 92px;
134
- border: 1px solid var(--kh-line);
135
- border-radius: 8px;
136
- padding: 14px 16px;
137
- background: rgba(255, 255, 255, 0.055);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  }
139
 
140
- .kh-stat .wrap,
141
- .kh-stat input {
 
 
 
 
 
 
 
 
 
 
 
 
142
  background: transparent !important;
 
 
 
143
  }
144
 
 
145
  .tabs {
146
- margin-top: 20px;
 
 
 
 
 
 
 
147
  }
148
 
149
  .tab-nav button {
150
  color: var(--kh-muted) !important;
151
- border-radius: 8px !important;
152
- font-weight: 700 !important;
 
 
 
 
 
 
 
 
 
 
153
  }
154
 
155
  .tab-nav button.selected {
156
- color: var(--kh-ink) !important;
157
- background: linear-gradient(135deg, rgba(32, 214, 199, 0.22), rgba(184, 244, 93, 0.12)) !important;
158
- border: 1px solid rgba(32, 214, 199, 0.34) !important;
 
159
  }
160
 
 
161
  textarea,
162
- input {
 
 
 
163
  color: var(--kh-ink) !important;
164
- background: rgba(3, 7, 12, 0.52) !important;
165
- border-color: rgba(255, 255, 255, 0.12) !important;
166
- font-size: 0.96rem !important;
 
 
 
 
 
 
 
 
 
 
 
167
  }
168
 
169
  textarea::placeholder,
170
  input::placeholder {
171
- color: rgba(216, 225, 234, 0.46) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  }
173
 
 
174
  #kh-status {
175
  min-height: 130px;
 
176
  }
177
 
178
  #kh-status h3 {
179
- color: var(--kh-lime);
 
 
 
 
180
  margin-top: 0;
181
  }
182
 
 
183
  #kh-text-preview textarea {
184
  min-height: 430px !important;
185
- line-height: 1.6 !important;
186
- font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace !important;
187
- font-size: 0.9rem !important;
 
188
  }
189
 
 
190
  #kh-search-results {
191
  min-height: 410px;
 
192
  }
193
 
194
- #kh-answer,
195
- #kh-reasoning {
196
- min-height: 240px;
 
 
197
  }
198
 
199
  #kh-answer {
 
200
  font-size: 1.02rem;
201
- line-height: 1.7;
202
- }
203
-
204
- #kh-search-results h3 {
205
- color: var(--kh-cyan);
206
  }
207
 
208
  #kh-reasoning {
209
- color: var(--kh-muted);
210
- font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
211
- font-size: 0.86rem;
212
- line-height: 1.6;
 
213
  }
214
 
215
- .prose,
216
- .markdown {
 
217
  color: var(--kh-soft) !important;
 
218
  }
219
 
220
- .prose strong,
221
- .markdown strong {
 
 
222
  color: var(--kh-ink) !important;
223
  }
224
 
225
- button.primary {
226
- min-height: 46px;
227
- background: linear-gradient(135deg, var(--kh-cyan), var(--kh-lime)) !important;
228
- color: #061015 !important;
229
- border: 0 !important;
230
- border-radius: 8px !important;
231
- font-weight: 800 !important;
232
- box-shadow: 0 16px 34px rgba(32, 214, 199, 0.2);
233
- }
234
-
235
- button.secondary {
236
- border-radius: 8px !important;
237
  }
238
 
239
- .file-preview,
240
- .upload-container {
241
- border-color: rgba(32, 214, 199, 0.26) !important;
242
- background: rgba(32, 214, 199, 0.055) !important;
 
 
 
243
  }
244
 
245
- @media (max-width: 760px) {
246
- #kh-shell {
247
- padding: 18px 10px 32px;
248
- }
 
249
 
250
- #kh-title h1 {
251
- font-size: clamp(2.25rem, 15vw, 4.2rem);
252
- }
253
 
254
- .kh-panel {
255
- padding: 14px !important;
256
- }
 
 
257
  }
258
  """
 
1
  HEAD = """
2
  <link rel="preconnect" href="https://fonts.googleapis.com">
3
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
4
+ <link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
5
  """
6
 
7
  JS = """
 
9
  const root = document.querySelector('.gradio-container');
10
  if (!root) return;
11
  root.dataset.ready = 'true';
 
 
 
12
  }
13
  """
14
 
15
  CSS = """
16
+ /* ═══════════════════════════════════════════════
17
+ KNOWLEDGEHUB β€” WARM LIGHT THEME
18
+ Palette: parchment bg Β· sage green Β· warm amber
19
+ Texture: fine cross-hatch + dot grid overlay
20
+ ═══════════════════════════════════════════════ */
 
 
 
 
 
 
 
 
 
21
 
22
+ :root {
23
+ --kh-bg: #ede8db;
24
+ --kh-bg2: #e8e2d4;
25
+ --kh-surface: #faf7f0;
26
+ --kh-surface2: #f5f1e6;
27
+ --kh-ink: #1e1b15;
28
+ --kh-muted: #6b6254;
29
+ --kh-soft: #3d3828;
30
+ --kh-line: rgba(80, 65, 35, 0.14);
31
+ --kh-sage: #3d7a6a;
32
+ --kh-sage-dim: rgba(61, 122, 106, 0.15);
33
+ --kh-amber: #b87428;
34
+ --kh-shadow: rgba(80, 60, 20, 0.10);
35
+ --kh-shadow-md: rgba(80, 60, 20, 0.16);
36
+ }
37
+
38
+ /* ══ PAGE BACKGROUND β€” cross-hatch + dot texture ══ */
39
  .gradio-container {
40
+ min-height: 100vh !important;
41
+ background-color: var(--kh-bg) !important;
42
+ /* Cross-hatch lines */
43
+ background-image:
44
+ linear-gradient(rgba(61,122,106,0.07) 1px, transparent 1px),
45
+ linear-gradient(90deg, rgba(61,122,106,0.07) 1px, transparent 1px),
46
+ linear-gradient(rgba(184,116,40,0.04) 1px, transparent 1px),
47
+ linear-gradient(90deg, rgba(184,116,40,0.04) 1px, transparent 1px),
48
+ radial-gradient(ellipse 65% 45% at 5% 0%, rgba(61,122,106,0.13) 0%, transparent 60%),
49
+ radial-gradient(ellipse 55% 40% at 96% 4%, rgba(184,116,40,0.11) 0%, transparent 55%),
50
+ radial-gradient(ellipse 80% 55% at 50% 105%, rgba(61,122,106,0.08) 0%, transparent 60%) !important;
51
+ background-size:
52
+ 32px 32px,
53
+ 32px 32px,
54
+ 8px 8px,
55
+ 8px 8px,
56
+ 100% 100%,
57
+ 100% 100%,
58
+ 100% 100% !important;
59
+ color: var(--kh-ink) !important;
60
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif !important;
61
+ }
62
+
63
+ /* ══ NUKE ALL DARK BACKGROUNDS from Gradio internals ══ */
64
+ /* Every block, wrap, form, and container Gradio might use */
65
+ .gradio-container *,
66
+ .gradio-container .block,
67
+ .gradio-container .form,
68
+ .gradio-container .wrap,
69
+ .gradio-container .gap,
70
+ .gradio-container .container,
71
+ .gradio-container .svelte-1f354aw,
72
+ .gradio-container .input-wrap,
73
+ .gradio-container .component-wrapper,
74
+ .gradio-container [data-testid],
75
+ .gradio-container .lg,
76
+ .gradio-container .sm,
77
+ .gradio-container .stretch {
78
+ background-color: transparent !important;
79
+ color: inherit !important;
80
+ }
81
+
82
+ /* Force all Gradio block panels to parchment */
83
+ .gradio-container .block.padded,
84
+ .gradio-container .block.padded.hide-container,
85
+ .gradio-container div.block,
86
+ .gradio-container fieldset,
87
+ .gradio-container label.block {
88
+ background-color: var(--kh-surface) !important;
89
+ border-color: var(--kh-line) !important;
90
+ color: var(--kh-ink) !important;
91
  }
92
 
93
+ /* ══ SHELL ══ */
94
  #kh-shell {
95
  position: relative;
96
  z-index: 1;
97
  max-width: 1220px;
98
  margin: 0 auto;
99
+ padding: 32px 20px 52px;
100
  }
101
 
102
+ /* ══ TITLE ══ */
103
  #kh-title {
104
+ padding: 36px 0 26px;
105
  border-bottom: 1px solid var(--kh-line);
106
+ background: transparent !important;
107
  }
108
 
109
  #kh-title h1 {
110
+ max-width: 960px;
111
+ color: var(--kh-ink) !important;
112
+ font-family: "Instrument Serif", Georgia, "Times New Roman", serif !important;
113
+ font-size: clamp(2.8rem, 6.5vw, 6.2rem);
114
+ font-weight: 400;
115
+ font-style: italic;
116
+ line-height: 0.92;
117
+ margin: 0 0 16px;
118
+ letter-spacing: -0.01em;
119
  }
120
 
121
  #kh-title p {
122
+ max-width: 760px;
123
+ color: var(--kh-muted) !important;
124
+ font-size: 1.02rem;
125
+ line-height: 1.7;
126
+ margin: 6px 0 0;
127
  }
128
 
129
+ /* ══ CODE CHIPS ══ */
130
  #kh-title code,
131
  .kh-chip code {
132
+ color: var(--kh-sage) !important;
133
+ background: rgba(61, 122, 106, 0.11) !important;
134
+ border: 1px solid rgba(61, 122, 106, 0.22) !important;
135
+ border-radius: 5px;
136
  padding: 2px 6px;
137
+ font-family: "JetBrains Mono", ui-monospace, monospace;
138
+ font-size: 0.87em;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  }
140
 
141
+ /* ══ CHIP ROW ══ */
142
  .kh-chip-row {
143
  display: flex;
144
  flex-wrap: wrap;
145
+ gap: 9px;
146
  margin-top: 18px;
147
+ background: transparent !important;
148
  }
149
 
150
  .kh-chip {
151
  border: 1px solid var(--kh-line);
152
  border-radius: 999px;
153
+ padding: 6px 14px;
154
+ color: var(--kh-soft) !important;
155
+ background: rgba(250, 247, 240, 0.85) !important;
156
+ font-size: 0.87rem;
157
+ font-weight: 500;
158
+ box-shadow: 0 1px 3px var(--kh-shadow);
159
+ }
160
+
161
+ /* ══ PANELS β€” clean lifted paper, no inner texture ══ */
162
+ .kh-panel,
163
+ .kh-panel.block,
164
+ .kh-panel > .block {
165
+ border: 1px solid rgba(80, 65, 35, 0.13) !important;
166
+ border-top: 2.5px solid rgba(61, 122, 106, 0.38) !important;
167
+ border-radius: 14px !important;
168
+ background: #fdfaf3 !important;
169
+ box-shadow:
170
+ 0 1px 0 rgba(255,255,255,0.95) inset,
171
+ 0 4px 6px -2px rgba(80,60,20,0.06),
172
+ 0 12px 32px rgba(80,60,20,0.09);
173
+ padding: 22px !important;
174
+ color: var(--kh-ink) !important;
175
  }
176
 
177
+ /* Force all children of panels to be light too */
178
+ .kh-panel *,
179
+ .kh-panel .block,
180
+ .kh-panel .wrap,
181
+ .kh-panel .form,
182
+ .kh-panel fieldset {
183
+ background-color: transparent !important;
184
+ color: var(--kh-ink) !important;
185
+ border-color: var(--kh-line) !important;
186
+ box-shadow: none !important;
187
+ }
188
+
189
+ /* Labels inside panels */
190
+ .kh-panel label span,
191
+ .kh-panel .label-wrap span,
192
+ .kh-panel span.text-gray-500,
193
+ .kh-panel span.text-sm {
194
+ color: var(--kh-soft) !important;
195
+ font-weight: 600 !important;
196
+ font-size: 0.89rem !important;
197
+ letter-spacing: 0.01em;
198
+ }
199
+
200
+ .kh-subhead {
201
+ margin: 6px 0 16px;
202
+ color: var(--kh-muted) !important;
203
+ font-size: 0.93rem;
204
+ line-height: 1.6;
205
  }
206
 
207
+ /* ══ STAT BOXES ══ */
208
+ .kh-stat,
209
+ .kh-stat > .block,
210
+ .kh-stat .wrap {
211
+ min-height: 88px;
212
+ border: 1px solid rgba(80,65,35,0.11) !important;
213
+ border-radius: 10px !important;
214
+ padding: 12px 16px;
215
+ background: rgba(232, 226, 212, 0.6) !important;
216
+ box-shadow: 0 1px 0 rgba(255,255,255,0.85) inset;
217
+ }
218
+
219
+ .kh-stat input,
220
+ .kh-stat textarea {
221
  background: transparent !important;
222
+ border: none !important;
223
+ box-shadow: none !important;
224
+ color: var(--kh-ink) !important;
225
  }
226
 
227
+ /* ══ TABS ══ */
228
  .tabs {
229
+ margin-top: 22px;
230
+ background: transparent !important;
231
+ }
232
+
233
+ .tab-nav {
234
+ background: transparent !important;
235
+ border-bottom: 1px solid var(--kh-line) !important;
236
+ gap: 4px;
237
  }
238
 
239
  .tab-nav button {
240
  color: var(--kh-muted) !important;
241
+ background: transparent !important;
242
+ border: 1px solid transparent !important;
243
+ border-radius: 8px 8px 0 0 !important;
244
+ font-weight: 600 !important;
245
+ font-size: 0.95rem !important;
246
+ padding: 9px 20px !important;
247
+ transition: all 0.15s ease;
248
+ }
249
+
250
+ .tab-nav button:hover {
251
+ color: var(--kh-sage) !important;
252
+ background: rgba(61,122,106,0.07) !important;
253
  }
254
 
255
  .tab-nav button.selected {
256
+ color: var(--kh-sage) !important;
257
+ background: var(--kh-surface) !important;
258
+ border: 1px solid var(--kh-line) !important;
259
+ border-bottom-color: var(--kh-surface) !important;
260
  }
261
 
262
+ /* ══ INPUTS & TEXTAREAS ══ */
263
  textarea,
264
+ input[type="text"],
265
+ input[type="number"],
266
+ input[type="search"],
267
+ input[type="email"] {
268
  color: var(--kh-ink) !important;
269
+ background: rgba(255, 252, 244, 0.92) !important;
270
+ border: 1px solid rgba(80,65,35,0.18) !important;
271
+ border-radius: 8px !important;
272
+ font-size: 0.94rem !important;
273
+ font-family: Inter, ui-sans-serif, system-ui, sans-serif !important;
274
+ box-shadow: 0 1px 2px rgba(80,60,20,0.05) inset !important;
275
+ transition: border-color 0.15s, box-shadow 0.15s;
276
+ }
277
+
278
+ textarea:focus,
279
+ input[type="text"]:focus {
280
+ border-color: rgba(61,122,106,0.5) !important;
281
+ box-shadow: 0 0 0 3px rgba(61,122,106,0.11), 0 1px 3px rgba(80,60,20,0.06) inset !important;
282
+ outline: none !important;
283
  }
284
 
285
  textarea::placeholder,
286
  input::placeholder {
287
+ color: rgba(107,98,84,0.52) !important;
288
+ }
289
+
290
+ /* ══ UPLOAD / FILE BLOCK ══ */
291
+ .upload-container,
292
+ .file-preview,
293
+ [data-testid="file"],
294
+ .file-upload {
295
+ background:
296
+ repeating-linear-gradient(
297
+ 45deg,
298
+ rgba(61,122,106,0.04) 0px,
299
+ rgba(61,122,106,0.04) 1px,
300
+ transparent 1px,
301
+ transparent 10px
302
+ ) !important;
303
+ border: 1.5px dashed rgba(61,122,106,0.35) !important;
304
+ border-radius: 10px !important;
305
+ color: var(--kh-muted) !important;
306
+ }
307
+
308
+ .upload-container *,
309
+ .file-preview * {
310
+ background: transparent !important;
311
+ color: var(--kh-muted) !important;
312
+ }
313
+
314
+ /* ══ BUTTONS ══ */
315
+ button.primary,
316
+ button[variant="primary"],
317
+ .primary {
318
+ min-height: 44px;
319
+ background: linear-gradient(135deg, #3d7a6a 0%, #2e6055 100%) !important;
320
+ color: #fff !important;
321
+ border: none !important;
322
+ border-radius: 9px !important;
323
+ font-weight: 700 !important;
324
+ font-size: 0.94rem !important;
325
+ letter-spacing: 0.01em;
326
+ box-shadow: 0 4px 14px rgba(61,122,106,0.30) !important;
327
+ transition: transform 0.12s, box-shadow 0.12s;
328
+ }
329
+
330
+ button.primary:hover {
331
+ transform: translateY(-1px);
332
+ box-shadow: 0 7px 20px rgba(61,122,106,0.38) !important;
333
+ }
334
+
335
+ button.secondary,
336
+ button[variant="secondary"],
337
+ .secondary {
338
+ background: rgba(232,226,212,0.8) !important;
339
+ color: var(--kh-soft) !important;
340
+ border: 1px solid rgba(80,65,35,0.18) !important;
341
+ border-radius: 9px !important;
342
+ font-weight: 600 !important;
343
+ }
344
+
345
+ button.secondary:hover {
346
+ background: rgba(61,122,106,0.10) !important;
347
+ border-color: rgba(61,122,106,0.32) !important;
348
+ color: var(--kh-sage) !important;
349
+ }
350
+
351
+ /* ══ SLIDER ══ */
352
+ input[type="range"] {
353
+ accent-color: var(--kh-sage) !important;
354
  }
355
 
356
+ /* ══ STATUS BOX ══ */
357
  #kh-status {
358
  min-height: 130px;
359
+ color: var(--kh-ink) !important;
360
  }
361
 
362
  #kh-status h3 {
363
+ color: var(--kh-sage) !important;
364
+ font-family: "Instrument Serif", Georgia, serif !important;
365
+ font-style: italic;
366
+ font-weight: 400;
367
+ font-size: 1.22rem;
368
  margin-top: 0;
369
  }
370
 
371
+ /* ══ TEXT PREVIEW ══ */
372
  #kh-text-preview textarea {
373
  min-height: 430px !important;
374
+ line-height: 1.65 !important;
375
+ font-family: "JetBrains Mono", ui-monospace, monospace !important;
376
+ font-size: 0.87rem !important;
377
+ background: rgba(232,226,212,0.45) !important;
378
  }
379
 
380
+ /* ══ SEARCH RESULTS / ANSWER / REASONING ══ */
381
  #kh-search-results {
382
  min-height: 410px;
383
+ color: var(--kh-ink) !important;
384
  }
385
 
386
+ #kh-search-results h3 {
387
+ color: var(--kh-sage) !important;
388
+ font-family: "Instrument Serif", Georgia, serif !important;
389
+ font-style: italic;
390
+ font-weight: 400;
391
  }
392
 
393
  #kh-answer {
394
+ min-height: 240px;
395
  font-size: 1.02rem;
396
+ line-height: 1.75;
397
+ color: var(--kh-ink) !important;
 
 
 
398
  }
399
 
400
  #kh-reasoning {
401
+ min-height: 240px;
402
+ color: var(--kh-muted) !important;
403
+ font-family: "JetBrains Mono", ui-monospace, monospace;
404
+ font-size: 0.85rem;
405
+ line-height: 1.65;
406
  }
407
 
408
+ /* ══ MARKDOWN / PROSE ══ */
409
+ .prose, .markdown,
410
+ .prose *, .markdown * {
411
  color: var(--kh-soft) !important;
412
+ background: transparent !important;
413
  }
414
 
415
+ .prose strong, .markdown strong,
416
+ .prose h1, .markdown h1,
417
+ .prose h2, .markdown h2,
418
+ .prose h3, .markdown h3 {
419
  color: var(--kh-ink) !important;
420
  }
421
 
422
+ .prose h3, .markdown h3 {
423
+ font-family: "Instrument Serif", Georgia, serif !important;
424
+ font-style: italic;
425
+ font-weight: 400;
426
+ font-size: 1.13rem;
 
 
 
 
 
 
 
427
  }
428
 
429
+ code {
430
+ color: var(--kh-sage) !important;
431
+ background: rgba(61,122,106,0.09) !important;
432
+ border: 1px solid rgba(61,122,106,0.18) !important;
433
+ border-radius: 4px;
434
+ padding: 1px 5px;
435
+ font-family: "JetBrains Mono", ui-monospace, monospace;
436
  }
437
 
438
+ /* ══ SCROLLBAR ══ */
439
+ ::-webkit-scrollbar { width: 7px; height: 7px; }
440
+ ::-webkit-scrollbar-track { background: rgba(232,226,212,0.4); }
441
+ ::-webkit-scrollbar-thumb { background: rgba(61,122,106,0.32); border-radius: 99px; }
442
+ ::-webkit-scrollbar-thumb:hover { background: rgba(61,122,106,0.52); }
443
 
444
+ /* ══ DIVIDERS / MISC ══ */
445
+ hr { border-color: var(--kh-line) !important; }
 
446
 
447
+ /* ══ RESPONSIVE ══ */
448
+ @media (max-width: 760px) {
449
+ #kh-shell { padding: 18px 12px 36px; }
450
+ #kh-title h1 { font-size: clamp(2.2rem, 13vw, 3.8rem); }
451
+ .kh-panel { padding: 16px !important; }
452
  }
453
  """
uv.lock CHANGED
@@ -1208,6 +1208,7 @@ dependencies = [
1208
  { name = "qdrant-client" },
1209
  { name = "requests" },
1210
  { name = "sentence-transformers" },
 
1211
  { name = "torchvision" },
1212
  { name = "youtube-transcript-api" },
1213
  ]
@@ -1232,6 +1233,7 @@ requires-dist = [
1232
  { name = "requests", specifier = ">=2.32.3" },
1233
  { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6.0" },
1234
  { name = "sentence-transformers", specifier = ">=3.0.1" },
 
1235
  { name = "torchvision", specifier = ">=0.27.0" },
1236
  { name = "youtube-transcript-api", specifier = ">=0.6.2" },
1237
  ]
@@ -3420,6 +3422,23 @@ wheels = [
3420
  { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
3421
  ]
3422
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3423
  [[package]]
3424
  name = "starlette"
3425
  version = "1.2.1"
 
1208
  { name = "qdrant-client" },
1209
  { name = "requests" },
1210
  { name = "sentence-transformers" },
1211
+ { name = "spaces" },
1212
  { name = "torchvision" },
1213
  { name = "youtube-transcript-api" },
1214
  ]
 
1233
  { name = "requests", specifier = ">=2.32.3" },
1234
  { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6.0" },
1235
  { name = "sentence-transformers", specifier = ">=3.0.1" },
1236
+ { name = "spaces" },
1237
  { name = "torchvision", specifier = ">=0.27.0" },
1238
  { name = "youtube-transcript-api", specifier = ">=0.6.2" },
1239
  ]
 
3422
  { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
3423
  ]
3424
 
3425
+ [[package]]
3426
+ name = "spaces"
3427
+ version = "0.50.4"
3428
+ source = { registry = "https://pypi.org/simple" }
3429
+ dependencies = [
3430
+ { name = "gradio" },
3431
+ { name = "httpx" },
3432
+ { name = "packaging" },
3433
+ { name = "pydantic" },
3434
+ { name = "requests" },
3435
+ { name = "typing-extensions" },
3436
+ ]
3437
+ sdist = { url = "https://files.pythonhosted.org/packages/7f/9d/9a06c91f01ed57ac4749e00f8eaf591ed5e3c6ec7c4499ce5c6f59fbc1dc/spaces-0.50.4.tar.gz", hash = "sha256:0aaed1d041d21c35c14b5a5cee5be677ad0ea26a26a5f09c488264ab1c031753", size = 88304, upload-time = "2026-06-04T15:41:13.17Z" }
3438
+ wheels = [
3439
+ { url = "https://files.pythonhosted.org/packages/c5/01/e02d0352836c68bd9e307c369c62800c9437d6243e64e1d8660543e50617/spaces-0.50.4-py3-none-any.whl", hash = "sha256:ad4383748b7a4219940d9ba786bb3eebbbd9cc946d21334bee1557bb20469355", size = 111340, upload-time = "2026-06-04T15:41:11.732Z" },
3440
+ ]
3441
+
3442
  [[package]]
3443
  name = "starlette"
3444
  version = "1.2.1"