JasonKishore commited on
Commit
720ee62
Β·
verified Β·
1 Parent(s): 724c817

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +484 -535
app.py CHANGED
@@ -4,710 +4,659 @@ import math
4
  import os
5
  from pypdf import PdfReader
6
 
7
-
8
  # ── CONFIG ───────────────────────────────────────────────────────────────────
9
- OPENAI_API_KEY = "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"
10
- #os.environ.get("OPENAI_API_KEY")
11
  client = openai.OpenAI(api_key=OPENAI_API_KEY)
12
 
13
-
14
  EMBED_MODEL = "text-embedding-3-small"
15
  CHAT_MODEL = "gpt-4o-mini"
16
  CHUNK_SIZE = 100
17
  OVERLAP = 40
18
  TOP_K = 2
19
 
20
-
21
  # ── GLOBALS ──────────────────────────────────────────────────────────────────
22
  stored_chunks = []
23
  stored_embeddings = []
24
  doc_name = ""
25
 
26
-
27
  # ── CORE LOGIC ───────────────────────────────────────────────────────────────
28
  def split_chunks(text, size=CHUNK_SIZE, overlap=OVERLAP):
29
- words, chunks, start = text.split(), [], 0
30
- while start < len(words):
31
- chunks.append(" ".join(words[start:start + size]))
32
- start += size - overlap
33
- return chunks
34
-
35
 
36
  def embed(text):
37
- return client.embeddings.create(model=EMBED_MODEL, input=text).data[0].embedding
38
-
39
 
40
  def cosine_similarity(a, b):
41
- dot = sum(x * y for x, y in zip(a, b))
42
- normA = math.sqrt(sum(x * x for x in a))
43
- normB = math.sqrt(sum(x * x for x in b))
44
- return dot / (normA * normB)
45
-
46
 
47
  def get_top_chunks(question, k=TOP_K):
48
- q_vec = embed(question)
49
- scores = [(cosine_similarity(q_vec, cv), c)
50
- for cv, c in zip(stored_embeddings, stored_chunks)]
51
- scores.sort(reverse=True)
52
- return [c for _, c in scores[:k]]
53
-
54
 
55
  def upload_pdf(file):
56
- global stored_chunks, stored_embeddings, doc_name
57
- if file is None:
58
- return ("β€” No file selected.", gr.update(interactive=False))
59
-
60
-
61
- reader = PdfReader(file.name)
62
- full_text = "".join(page.extract_text() or "" for page in reader.pages)
63
- doc_name = file.name.split("/")[-1]
64
 
 
 
 
65
 
66
- stored_chunks = split_chunks(full_text)
67
- stored_embeddings = [embed(c) for c in stored_chunks]
68
-
69
-
70
- return (
71
- f"✦ {doc_name}\nβ†’ {len(stored_chunks)} chunks indexed and ready.",
72
- gr.update(interactive=True)
73
- )
74
 
 
 
 
 
75
 
76
  def chat(question, history):
77
- if not stored_chunks:
78
- return history + [(question, "Please index a document in the Upload tab first.")]
79
-
80
-
81
- context = "\n\n".join([f"[{i+1}] {c}" for i, c in enumerate(get_top_chunks(question))])
82
- prompt = (
83
- "Answer strictly from the document text below.\n"
84
- "If not found, reply: \"This information is not in the document.\"\n\n"
85
- f"TEXT:\n{context}\n\nQUESTION: {question}\n\nANSWER:"
86
- )
87
- response = client.chat.completions.create(
88
- model=CHAT_MODEL,
89
- messages=[
90
- {"role": "system", "content":
91
- "Strict document assistant. Only use provided text. "
92
- "If answer absent, say: This information is not in the document."},
93
- {"role": "user", "content": prompt}
94
- ]
95
- )
96
- return history + [(question, response.choices[0].message.content)]
97
-
98
-
99
 
100
 
101
  # ── CSS ───────────────────────────────────────────────────────────────────────
102
  css = """
103
  @import url('https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Mono:wght@300;400;500&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;1,9..40,300&display=swap');
104
 
105
-
106
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
107
 
108
-
109
  :root {
110
- --bg: #f4f1ec;
111
- --surface: #faf8f5;
112
- --surface-2: #f0ede7;
113
- --border: rgba(60,50,30,0.09);
114
- --border-2: rgba(60,50,30,0.15);
115
- --accent: #7c6a50;
116
- --accent-light:#a8916f;
117
- --accent-glow: rgba(124,106,80,0.08);
118
- --green: #4a8c62;
119
- --text-1: #1e1a14;
120
- --text-2: #6b6153;
121
- --text-3: #a89e90;
122
- --font-serif: 'DM Serif Display', Georgia, serif;
123
- --font-sans: 'DM Sans', sans-serif;
124
- --font-mono: 'DM Mono', monospace;
125
- --r: 10px;
126
- --r-lg: 18px;
127
- --ease: cubic-bezier(0.4,0,0.2,1);
128
- --shadow: 0 1px 4px rgba(60,50,30,0.07), 0 4px 16px rgba(60,50,30,0.04);
129
  }
130
 
131
-
132
  html, body, .gradio-container, #root {
133
- font-family: var(--font-sans) !important;
134
- background: var(--bg) !important;
135
- color: var(--text-1) !important;
136
  }
137
 
138
-
139
  .gradio-container {
140
- max-width: 1080px !important;
141
- margin: 0 auto !important;
142
- padding: 44px 28px 68px !important;
143
  }
144
 
145
-
146
  ::-webkit-scrollbar { width: 3px; }
147
  ::-webkit-scrollbar-track { background: transparent; }
148
  ::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 3px; }
149
 
150
-
151
  /* ── MASTHEAD ── */
152
  .mast {
153
- display: flex;
154
- align-items: flex-start;
155
- justify-content: space-between;
156
- padding-bottom: 26px;
157
- border-bottom: 1px solid var(--border-2);
158
- margin-bottom: 32px;
159
  }
160
  .mast-title {
161
- font-family: var(--font-serif) !important;
162
- font-size: 2.6rem;
163
- font-weight: 400;
164
- color: var(--text-1);
165
- letter-spacing: -0.025em;
166
- line-height: 1.05;
167
  }
168
  .mast-title em { font-style: italic; color: var(--accent); }
169
  .mast-sub {
170
- font-family: var(--font-mono);
171
- font-size: 0.6rem;
172
- font-weight: 300;
173
- color: var(--text-3);
174
- letter-spacing: 0.14em;
175
- text-transform: uppercase;
176
- margin-top: 9px;
177
  }
178
 
179
-
180
  /* ── PORTFOLIO BADGE ── */
181
  .portfolio-badge {
182
- display: flex;
183
- flex-direction: column;
184
- align-items: flex-end;
185
- gap: 7px;
186
  }
187
  .badge-inner {
188
- display: flex;
189
- align-items: center;
190
- gap: 8px;
191
- background: var(--surface);
192
- border: 1px solid var(--border-2);
193
- border-radius: 8px;
194
- padding: 9px 14px 9px 11px;
195
- box-shadow: var(--shadow);
196
  }
197
  .badge-dot {
198
- width: 7px;
199
- height: 7px;
200
- border-radius: 50%;
201
- background: var(--green);
202
- box-shadow: 0 0 0 2px rgba(74,140,98,0.15);
203
- animation: pulse 2.4s ease-in-out infinite;
204
  }
205
  @keyframes pulse {
206
- 0%, 100% { opacity: 1; box-shadow: 0 0 0 2px rgba(74,140,98,0.15); }
207
- 50% { opacity: 0.5; box-shadow: 0 0 0 4px rgba(74,140,98,0.08); }
208
  }
209
  .badge-text {
210
- font-family: var(--font-mono);
211
- font-size: 0.65rem;
212
- font-weight: 500;
213
- color: var(--text-1);
214
- letter-spacing: 0.04em;
215
  }
216
  .badge-sub {
217
- font-family: var(--font-mono);
218
- font-size: 0.56rem;
219
- color: var(--text-3);
220
- letter-spacing: 0.1em;
221
- text-transform: uppercase;
222
- margin-top: 2px;
223
  }
224
  .stack-chips { display: flex; gap: 5px; }
225
  .chip {
226
- font-family: var(--font-mono);
227
- font-size: 0.55rem;
228
- color: var(--text-3);
229
- background: var(--surface);
230
- border: 1px solid var(--border-2);
231
- border-radius: 4px;
232
- padding: 3px 8px;
233
- letter-spacing: 0.08em;
234
- text-transform: uppercase;
235
  }
236
 
237
-
238
  /* ── TABS ── */
239
  .tabs > .tab-nav {
240
- border-bottom: 1px solid var(--border-2) !important;
241
- background: transparent !important;
242
- margin-bottom: 28px !important;
243
- gap: 0 !important;
244
  }
245
  .tabs > .tab-nav button {
246
- font-family: var(--font-mono) !important;
247
- font-size: 0.63rem !important;
248
- font-weight: 500 !important;
249
- letter-spacing: 0.12em !important;
250
- text-transform: uppercase !important;
251
- color: var(--text-3) !important;
252
- background: transparent !important;
253
- border: none !important;
254
- border-bottom: 2px solid transparent !important;
255
- padding: 10px 22px !important;
256
- border-radius: 0 !important;
257
- transition: color 0.2s var(--ease), border-color 0.2s var(--ease) !important;
258
- margin-bottom: -1px !important;
259
- }
260
- .tabs > .tab-nav button:hover { color: var(--text-2) !important; }
261
  .tabs > .tab-nav button.selected {
262
- color: var(--accent) !important;
263
- border-bottom-color: var(--accent) !important;
264
- background: transparent !important;
265
  }
266
 
267
-
268
  /* ── PANEL ── */
269
  .panel {
270
- background: var(--surface);
271
- border: 1px solid var(--border-2);
272
- border-radius: var(--r-lg);
273
- padding: 28px 26px;
274
- box-shadow: var(--shadow);
275
  }
276
 
277
-
278
  /* ── EYEBROW ── */
279
  .eyebrow {
280
- font-family: var(--font-mono);
281
- font-size: 0.56rem;
282
- font-weight: 500;
283
- letter-spacing: 0.18em;
284
- text-transform: uppercase;
285
- color: var(--text-3);
286
- margin-bottom: 16px;
287
- display: flex;
288
- align-items: center;
289
- gap: 10px;
290
  }
291
  .eyebrow::after {
292
- content: '';
293
- flex: 1;
294
- height: 1px;
295
- background: var(--border-2);
296
  }
297
 
298
-
299
  /* ── UPLOAD GRID ── */
300
  .upload-grid {
301
- display: grid;
302
- grid-template-columns: 1fr 1fr;
303
- gap: 22px;
304
- align-items: start;
305
  }
306
  .upload-right { display: flex; flex-direction: column; gap: 16px; }
307
 
308
-
309
  /* ── SPEC GRID ── */
310
  .specs {
311
- display: grid;
312
- grid-template-columns: 1fr 1fr;
313
- gap: 7px;
314
  }
315
  .spec {
316
- background: var(--bg);
317
- border: 1px solid var(--border);
318
- border-radius: 8px;
319
- padding: 11px 13px;
320
  }
321
  .spec-k {
322
- font-family: var(--font-mono);
323
- font-size: 0.52rem;
324
- letter-spacing: 0.12em;
325
- text-transform: uppercase;
326
- color: var(--text-3);
327
- margin-bottom: 5px;
328
  }
329
  .spec-v {
330
- font-family: var(--font-mono);
331
- font-size: 0.76rem;
332
- color: var(--accent);
333
- font-weight: 400;
334
  }
335
  .spec.wide { grid-column: span 2; }
336
 
337
-
338
  /* ── ARCH BOX ── */
339
  .arch-box {
340
- background: var(--bg);
341
- border: 1px solid var(--border);
342
- border-radius: 10px;
343
- padding: 16px 18px;
344
  }
345
  .arch-title {
346
- font-family: var(--font-mono);
347
- font-size: 0.54rem;
348
- letter-spacing: 0.16em;
349
- text-transform: uppercase;
350
- color: var(--text-3);
351
- margin-bottom: 14px;
352
  }
353
  .arch-step {
354
- display: flex;
355
- align-items: flex-start;
356
- gap: 12px;
357
- margin-bottom: 11px;
358
  }
359
  .arch-step:last-child { margin-bottom: 0; }
360
  .arch-num {
361
- font-family: var(--font-mono);
362
- font-size: 0.6rem;
363
- color: var(--accent-light);
364
- min-width: 16px;
365
- margin-top: 1px;
366
- font-weight: 500;
367
  }
368
  .arch-desc {
369
- font-family: var(--font-sans);
370
- font-size: 0.79rem;
371
- font-weight: 300;
372
- color: var(--text-2);
373
- line-height: 1.55;
374
  }
375
 
376
-
377
  /* ── FILE INPUT ── */
378
  .gr-file {
379
- border: 1px dashed var(--border-2) !important;
380
- border-radius: var(--r) !important;
381
- background: var(--surface-2) !important;
382
- transition: border-color 0.2s var(--ease), background 0.2s var(--ease) !important;
383
  }
384
  .gr-file:hover {
385
- border-color: var(--accent-light) !important;
386
- background: var(--accent-glow) !important;
387
  }
388
 
389
-
390
  /* ── INPUTS ── */
391
  textarea, input[type="text"] {
392
- font-family: var(--font-sans) !important;
393
- font-size: 0.88rem !important;
394
- font-weight: 300 !important;
395
- color: var(--text-1) !important;
396
- background: var(--surface-2) !important;
397
- border: 1px solid var(--border-2) !important;
398
- border-radius: var(--r) !important;
399
- padding: 11px 15px !important;
400
- line-height: 1.65 !important;
401
- transition: border-color 0.2s var(--ease), box-shadow 0.2s var(--ease) !important;
402
- caret-color: var(--accent) !important;
403
  }
404
  textarea:focus, input[type="text"]:focus {
405
- border-color: var(--accent-light) !important;
406
- box-shadow: 0 0 0 3px var(--accent-glow) !important;
407
- outline: none !important;
408
- background: var(--surface) !important;
409
  }
410
  textarea::placeholder, input::placeholder {
411
- color: var(--text-3) !important;
412
- font-weight: 300 !important;
413
  }
414
 
415
-
416
  .status-box textarea {
417
- font-family: var(--font-mono) !important;
418
- font-size: 0.73rem !important;
419
- color: var(--text-2) !important;
420
- background: var(--bg) !important;
421
- border: 1px solid var(--border) !important;
422
- line-height: 1.9 !important;
423
  }
424
 
425
-
426
  /* ── BUTTONS ── */
427
  button { font-family: var(--font-sans) !important; cursor: pointer !important; }
428
 
429
-
430
  button.primary {
431
- background: var(--accent) !important;
432
- color: #faf8f5 !important;
433
- border: none !important;
434
- border-radius: 7px !important;
435
- padding: 10px 22px !important;
436
- font-size: 0.8rem !important;
437
- font-weight: 500 !important;
438
- letter-spacing: 0.02em !important;
439
- transition: all 0.18s var(--ease) !important;
440
- box-shadow: 0 1px 3px rgba(124,106,80,0.25) !important;
441
  }
442
  button.primary:hover {
443
- background: var(--accent-light) !important;
444
- transform: translateY(-1px) !important;
445
- box-shadow: 0 4px 14px rgba(124,106,80,0.22) !important;
446
  }
447
  button.primary:active { transform: translateY(0) !important; box-shadow: none !important; }
448
  button.primary:disabled {
449
- background: var(--surface-2) !important;
450
- color: var(--text-3) !important;
451
- box-shadow: none !important;
452
- cursor: not-allowed !important;
453
- transform: none !important;
454
  }
455
 
456
-
457
  button.secondary {
458
- background: transparent !important;
459
- color: var(--text-2) !important;
460
- border: 1px solid var(--border-2) !important;
461
- border-radius: 7px !important;
462
- padding: 10px 18px !important;
463
- font-size: 0.8rem !important;
464
- font-weight: 400 !important;
465
- transition: all 0.18s var(--ease) !important;
466
  }
467
  button.secondary:hover {
468
- color: var(--text-1) !important;
469
- border-color: var(--text-3) !important;
470
- background: var(--surface-2) !important;
471
  }
472
 
473
-
474
  /* ── CHATBOT ── */
475
  .chatbot-wrap > div {
476
- background: var(--bg) !important;
477
- border: 1px solid var(--border-2) !important;
478
- border-radius: var(--r) !important;
479
- padding: 10px !important;
480
- }
481
- .message-wrap .user-row .message {
482
- background: var(--accent) !important;
483
- color: #faf8f5 !important;
484
- border: none !important;
485
- border-radius: 14px 14px 3px 14px !important;
486
- font-size: 0.85rem !important;
487
- font-weight: 300 !important;
488
- line-height: 1.7 !important;
489
- padding: 11px 15px !important;
490
- max-width: 76% !important;
491
- margin-left: auto !important;
492
- box-shadow: 0 2px 8px rgba(124,106,80,0.15) !important;
493
- }
494
- .message-wrap .bot-row .message {
495
- background: var(--surface) !important;
496
- color: var(--text-1) !important;
497
- border: 1px solid var(--border-2) !important;
498
- border-radius: 14px 14px 14px 3px !important;
499
- font-size: 0.85rem !important;
500
- font-weight: 300 !important;
501
- line-height: 1.7 !important;
502
- padding: 11px 15px !important;
503
- max-width: 76% !important;
 
 
504
  }
505
 
506
-
507
  /* ── LABELS ── */
508
  label > span, .gr-form label span {
509
- font-family: var(--font-mono) !important;
510
- font-size: 0.56rem !important;
511
- font-weight: 500 !important;
512
- letter-spacing: 0.12em !important;
513
- text-transform: uppercase !important;
514
- color: var(--text-3) !important;
515
  }
516
 
517
-
518
  /* ── FOOTER ── */
519
  .footer {
520
- text-align: center;
521
- margin-top: 44px;
522
- padding-top: 20px;
523
- border-top: 1px solid var(--border-2);
524
- display: flex;
525
- align-items: center;
526
- justify-content: center;
527
- gap: 14px;
528
  }
529
  .footer p {
530
- font-family: var(--font-mono);
531
- font-size: 0.56rem;
532
- color: var(--text-3);
533
- letter-spacing: 0.1em;
534
- text-transform: uppercase;
535
  }
536
  .footer-sep { color: var(--text-3); font-size: 0.45rem; }
537
  """
538
 
539
-
540
  # ── UI ────────────────────────────────────────────────────────────────────────
541
- with gr.Blocks(css=css, title="Document Intelligence β€” Jason Kishore") as demo:
542
-
543
-
544
- gr.HTML("""
545
- <div class="mast">
546
- <div>
547
- <div class="mast-title">Document <em>Intelligence</em></div>
548
- <div class="mast-sub">Retrieval-Augmented Generation Β· Semantic Search Β· GPT-4o Mini</div>
549
- </div>
550
- <div class="portfolio-badge">
551
- <div class="badge-inner">
552
- <div class="badge-dot"></div>
553
- <div>
554
- <div class="badge-text">Jason Kishore</div>
555
- <div class="badge-sub">Portfolio Project</div>
556
- </div>
557
- </div>
558
- <div class="stack-chips">
559
- <span class="chip">Python</span>
560
- <span class="chip">OpenAI</span>
561
- <span class="chip">Gradio</span>
562
- <span class="chip">RAG</span>
563
- </div>
564
- </div>
565
- </div>
566
- """)
567
-
568
-
569
- with gr.Tabs(elem_classes=["tabs"]):
570
-
571
-
572
- # ── Tab 1 Β· Upload ───────────────────────────────────────────────────
573
- with gr.TabItem("01 Β· Upload & Index"):
574
- with gr.Column(elem_classes=["panel"]):
575
- gr.HTML('<div class="eyebrow">Document Ingestion</div>')
576
- gr.HTML('<div class="upload-grid">')
577
-
578
-
579
- with gr.Column():
580
- pdf_input = gr.File(label="Upload PDF", file_types=[".pdf"])
581
- upload_btn = gr.Button("Index Document", variant="primary")
582
- upload_status = gr.Textbox(
583
- label="Status",
584
- lines=2,
585
- interactive=False,
586
- value="β€” Awaiting document",
587
- elem_classes=["status-box"]
588
- )
589
-
590
-
591
- with gr.Column():
592
- gr.HTML("""
593
- <div class="upload-right">
594
- <div>
595
- <div class="eyebrow">Configuration</div>
596
- <div class="specs">
597
- <div class="spec">
598
- <div class="spec-k">Embed Model</div>
599
- <div class="spec-v">3-small</div>
600
- </div>
601
- <div class="spec">
602
- <div class="spec-k">Chat Model</div>
603
- <div class="spec-v">4o-mini</div>
604
- </div>
605
- <div class="spec">
606
- <div class="spec-k">Chunk Size</div>
607
- <div class="spec-v">100 words</div>
608
- </div>
609
- <div class="spec">
610
- <div class="spec-k">Overlap</div>
611
- <div class="spec-v">40 words</div>
612
- </div>
613
- <div class="spec wide">
614
- <div class="spec-k">Top-K Retrieval</div>
615
- <div class="spec-v">2 chunks / query</div>
616
- </div>
617
- </div>
618
- </div>
619
- <div class="arch-box">
620
- <div class="arch-title">Pipeline</div>
621
- <div class="arch-step">
622
- <div class="arch-num">01</div>
623
- <div class="arch-desc">PDF text extracted page-by-page via pypdf</div>
624
- </div>
625
- <div class="arch-step">
626
- <div class="arch-num">02</div>
627
- <div class="arch-desc">Text split into overlapping 100-word chunks</div>
628
- </div>
629
- <div class="arch-step">
630
- <div class="arch-num">03</div>
631
- <div class="arch-desc">Each chunk embedded with text-embedding-3-small</div>
632
- </div>
633
- <div class="arch-step">
634
- <div class="arch-num">04</div>
635
- <div class="arch-desc">Query embedded β†’ top-2 chunks by cosine similarity</div>
636
- </div>
637
- <div class="arch-step">
638
- <div class="arch-num">05</div>
639
- <div class="arch-desc">GPT-4o Mini answers strictly from retrieved context</div>
640
- </div>
641
- </div>
642
- </div>
643
- """)
644
-
645
-
646
- gr.HTML('</div>')
647
-
648
-
649
- # ── Tab 2 Β· Chat ─────────────────────────────────────────────────────
650
- with gr.TabItem("02 Β· Chat"):
651
- with gr.Column(elem_classes=["panel"]):
652
- gr.HTML('<div class="eyebrow">Conversation</div>')
653
-
654
-
655
- chatbot = gr.Chatbot(
656
- height=440,
657
- show_label=False,
658
- bubble_full_width=False,
659
- elem_classes=["chatbot-wrap"],
660
- placeholder=(
661
- "<div style='font-family:DM Mono,monospace;font-size:0.62rem;"
662
- "color:#a89e90;text-align:center;padding:80px 0;"
663
- "letter-spacing:0.14em;text-transform:uppercase;line-height:2.2'>"
664
- "Index a document in the Upload tab<br>to begin your session</div>"
665
- )
666
- )
667
-
668
-
669
- msg_box = gr.Textbox(
670
- placeholder="Ask a question about your document…",
671
- label="Query",
672
- lines=1,
673
- max_lines=5,
674
- )
675
-
676
-
677
- with gr.Row():
678
- send_btn = gr.Button("Send Query", variant="primary", size="sm")
679
- clear_btn = gr.Button("Clear", variant="secondary", size="sm")
680
-
681
-
682
- gr.HTML("""
683
- <div class="footer">
684
- <p>Jason Kishore Β· Portfolio Project</p>
685
- <span class="footer-sep">β—†</span>
686
- <p>Gradio Β· OpenAI Β· pypdf</p>
687
- <span class="footer-sep">β—†</span>
688
- <p>Answers grounded strictly in uploaded document</p>
689
- </div>
690
- """)
691
-
692
-
693
- # ── Events ───────────────────────────────────────────────────────────────
694
- upload_btn.click(
695
- fn=upload_pdf,
696
- inputs=pdf_input,
697
- outputs=[upload_status, send_btn]
698
- )
699
- send_btn.click(
700
- fn=chat, inputs=[msg_box, chatbot], outputs=chatbot
701
- ).then(lambda: "", outputs=msg_box)
702
-
703
-
704
- msg_box.submit(
705
- fn=chat, inputs=[msg_box, chatbot], outputs=chatbot
706
- ).then(lambda: "", outputs=msg_box)
707
-
708
-
709
- clear_btn.click(lambda: [], outputs=chatbot)
710
-
711
-
712
- demo.launch()
713
-
 
4
  import os
5
  from pypdf import PdfReader
6
 
 
7
  # ── CONFIG ───────────────────────────────────────────────────────────────────
8
+ OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
 
9
  client = openai.OpenAI(api_key=OPENAI_API_KEY)
10
 
 
11
  EMBED_MODEL = "text-embedding-3-small"
12
  CHAT_MODEL = "gpt-4o-mini"
13
  CHUNK_SIZE = 100
14
  OVERLAP = 40
15
  TOP_K = 2
16
 
 
17
  # ── GLOBALS ──────────────────────────────────────────────────────────────────
18
  stored_chunks = []
19
  stored_embeddings = []
20
  doc_name = ""
21
 
 
22
  # ── CORE LOGIC ───────────────────────────────────────────────────────────────
23
  def split_chunks(text, size=CHUNK_SIZE, overlap=OVERLAP):
24
+ words, chunks, start = text.split(), [], 0
25
+ while start < len(words):
26
+ chunks.append(" ".join(words[start:start + size]))
27
+ start += size - overlap
28
+ return chunks
 
29
 
30
  def embed(text):
31
+ return client.embeddings.create(model=EMBED_MODEL, input=text).data[0].embedding
 
32
 
33
  def cosine_similarity(a, b):
34
+ dot = sum(x * y for x, y in zip(a, b))
35
+ normA = math.sqrt(sum(x * x for x in a))
36
+ normB = math.sqrt(sum(x * x for x in b))
37
+ return dot / (normA * normB)
 
38
 
39
  def get_top_chunks(question, k=TOP_K):
40
+ q_vec = embed(question)
41
+ scores = [(cosine_similarity(q_vec, cv), c)
42
+ for cv, c in zip(stored_embeddings, stored_chunks)]
43
+ scores.sort(reverse=True)
44
+ return [c for _, c in scores[:k]]
 
45
 
46
  def upload_pdf(file):
47
+ global stored_chunks, stored_embeddings, doc_name
48
+ if file is None:
49
+ return ("β€” No file selected.", gr.update(interactive=False))
 
 
 
 
 
50
 
51
+ reader = PdfReader(file.name)
52
+ full_text = "".join(page.extract_text() or "" for page in reader.pages)
53
+ doc_name = file.name.split("/")[-1]
54
 
55
+ stored_chunks = split_chunks(full_text)
56
+ stored_embeddings = [embed(c) for c in stored_chunks]
 
 
 
 
 
 
57
 
58
+ return (
59
+ f"✦ {doc_name}\nβ†’ {len(stored_chunks)} chunks indexed and ready.",
60
+ gr.update(interactive=True)
61
+ )
62
 
63
  def chat(question, history):
64
+ if not stored_chunks:
65
+ return history + [(question, "Please index a document in the Upload tab first.")]
66
+
67
+ context = "\n\n".join([f"[{i+1}] {c}" for i, c in enumerate(get_top_chunks(question))])
68
+ prompt = (
69
+ "Answer strictly from the document text below.\n"
70
+ "If not found, reply: \"This information is not in the document.\"\n\n"
71
+ f"TEXT:\n{context}\n\nQUESTION: {question}\n\nANSWER:"
72
+ )
73
+ response = client.chat.completions.create(
74
+ model=CHAT_MODEL,
75
+ messages=[
76
+ {"role": "system", "content":
77
+ "Strict document assistant. Only use provided text. "
78
+ "If answer absent, say: This information is not in the document."},
79
+ {"role": "user", "content": prompt}
80
+ ]
81
+ )
82
+ return history + [(question, response.choices[0].message.content)]
 
 
 
83
 
84
 
85
  # ── CSS ───────────────────────────────────────────────────────────────────────
86
  css = """
87
  @import url('https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Mono:wght@300;400;500&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;1,9..40,300&display=swap');
88
 
 
89
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
90
 
 
91
  :root {
92
+ --bg: #f4f1ec;
93
+ --surface: #faf8f5;
94
+ --surface-2: #f0ede7;
95
+ --border: rgba(60,50,30,0.09);
96
+ --border-2: rgba(60,50,30,0.15);
97
+ --accent: #7c6a50;
98
+ --accent-light:#a8916f;
99
+ --accent-glow: rgba(124,106,80,0.08);
100
+ --green: #4a8c62;
101
+ --text-1: #1e1a14;
102
+ --text-2: #6b6153;
103
+ --text-3: #a89e90;
104
+ --font-serif: 'DM Serif Display', Georgia, serif;
105
+ --font-sans: 'DM Sans', sans-serif;
106
+ --font-mono: 'DM Mono', monospace;
107
+ --r: 10px;
108
+ --r-lg: 18px;
109
+ --ease: cubic-bezier(0.4,0,0.2,1);
110
+ --shadow: 0 1px 4px rgba(60,50,30,0.07), 0 4px 16px rgba(60,50,30,0.04);
111
  }
112
 
 
113
  html, body, .gradio-container, #root {
114
+ font-family: var(--font-sans) !important;
115
+ background: var(--bg) !important;
116
+ color: var(--text-1) !important;
117
  }
118
 
 
119
  .gradio-container {
120
+ max-width: 1080px !important;
121
+ margin: 0 auto !important;
122
+ padding: 44px 28px 68px !important;
123
  }
124
 
 
125
  ::-webkit-scrollbar { width: 3px; }
126
  ::-webkit-scrollbar-track { background: transparent; }
127
  ::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 3px; }
128
 
 
129
  /* ── MASTHEAD ── */
130
  .mast {
131
+ display: flex;
132
+ align-items: flex-start;
133
+ justify-content: space-between;
134
+ padding-bottom: 26px;
135
+ border-bottom: 1px solid var(--border-2);
136
+ margin-bottom: 32px;
137
  }
138
  .mast-title {
139
+ font-family: var(--font-serif) !important;
140
+ font-size: 2.6rem;
141
+ font-weight: 400;
142
+ color: var(--text-1);
143
+ letter-spacing: -0.025em;
144
+ line-height: 1.05;
145
  }
146
  .mast-title em { font-style: italic; color: var(--accent); }
147
  .mast-sub {
148
+ font-family: var(--font-mono);
149
+ font-size: 0.6rem;
150
+ font-weight: 300;
151
+ color: var(--text-3);
152
+ letter-spacing: 0.14em;
153
+ text-transform: uppercase;
154
+ margin-top: 9px;
155
  }
156
 
 
157
  /* ── PORTFOLIO BADGE ── */
158
  .portfolio-badge {
159
+ display: flex;
160
+ flex-direction: column;
161
+ align-items: flex-end;
162
+ gap: 7px;
163
  }
164
  .badge-inner {
165
+ display: flex;
166
+ align-items: center;
167
+ gap: 8px;
168
+ background: var(--surface);
169
+ border: 1px solid var(--border-2);
170
+ border-radius: 8px;
171
+ padding: 9px 14px 9px 11px;
172
+ box-shadow: var(--shadow);
173
  }
174
  .badge-dot {
175
+ width: 7px;
176
+ height: 7px;
177
+ border-radius: 50%;
178
+ background: var(--green);
179
+ box-shadow: 0 0 0 2px rgba(74,140,98,0.15);
180
+ animation: pulse 2.4s ease-in-out infinite;
181
  }
182
  @keyframes pulse {
183
+ 0%, 100% { opacity: 1; box-shadow: 0 0 0 2px rgba(74,140,98,0.15); }
184
+ 50% { opacity: 0.5; box-shadow: 0 0 0 4px rgba(74,140,98,0.08); }
185
  }
186
  .badge-text {
187
+ font-family: var(--font-mono);
188
+ font-size: 0.65rem;
189
+ font-weight: 500;
190
+ color: var(--text-1);
191
+ letter-spacing: 0.04em;
192
  }
193
  .badge-sub {
194
+ font-family: var(--font-mono);
195
+ font-size: 0.56rem;
196
+ color: var(--text-3);
197
+ letter-spacing: 0.1em;
198
+ text-transform: uppercase;
199
+ margin-top: 2px;
200
  }
201
  .stack-chips { display: flex; gap: 5px; }
202
  .chip {
203
+ font-family: var(--font-mono);
204
+ font-size: 0.55rem;
205
+ color: var(--text-3);
206
+ background: var(--surface);
207
+ border: 1px solid var(--border-2);
208
+ border-radius: 4px;
209
+ padding: 3px 8px;
210
+ letter-spacing: 0.08em;
211
+ text-transform: uppercase;
212
  }
213
 
 
214
  /* ── TABS ── */
215
  .tabs > .tab-nav {
216
+ border-bottom: 1px solid var(--border-2) !important;
217
+ background: transparent !important;
218
+ margin-bottom: 28px !important;
219
+ gap: 0 !important;
220
  }
221
  .tabs > .tab-nav button {
222
+ font-family: var(--font-mono) !important;
223
+ font-size: 0.63rem !important;
224
+ font-weight: 500 !important;
225
+ letter-spacing: 0.12em !important;
226
+ text-transform: uppercase !important;
227
+ color: var(--text-3) !important;
228
+ background: transparent !important;
229
+ border: none !important;
230
+ border-bottom: 2px solid transparent !important;
231
+ padding: 10px 22px !important;
232
+ border-radius: 0 !important;
233
+ transition: color 0.2s var(--ease), border-color 0.2s var(--ease) !important;
234
+ margin-bottom: -1px !important;
235
+ }
236
+ .tabs > .tab-nav button:hover { color: var(--text-2) !important; }
237
  .tabs > .tab-nav button.selected {
238
+ color: var(--accent) !important;
239
+ border-bottom-color: var(--accent) !important;
240
+ background: transparent !important;
241
  }
242
 
 
243
  /* ── PANEL ── */
244
  .panel {
245
+ background: var(--surface);
246
+ border: 1px solid var(--border-2);
247
+ border-radius: var(--r-lg);
248
+ padding: 28px 26px;
249
+ box-shadow: var(--shadow);
250
  }
251
 
 
252
  /* ── EYEBROW ── */
253
  .eyebrow {
254
+ font-family: var(--font-mono);
255
+ font-size: 0.56rem;
256
+ font-weight: 500;
257
+ letter-spacing: 0.18em;
258
+ text-transform: uppercase;
259
+ color: var(--text-3);
260
+ margin-bottom: 16px;
261
+ display: flex;
262
+ align-items: center;
263
+ gap: 10px;
264
  }
265
  .eyebrow::after {
266
+ content: '';
267
+ flex: 1;
268
+ height: 1px;
269
+ background: var(--border-2);
270
  }
271
 
 
272
  /* ── UPLOAD GRID ── */
273
  .upload-grid {
274
+ display: grid;
275
+ grid-template-columns: 1fr 1fr;
276
+ gap: 22px;
277
+ align-items: start;
278
  }
279
  .upload-right { display: flex; flex-direction: column; gap: 16px; }
280
 
 
281
  /* ── SPEC GRID ── */
282
  .specs {
283
+ display: grid;
284
+ grid-template-columns: 1fr 1fr;
285
+ gap: 7px;
286
  }
287
  .spec {
288
+ background: var(--bg);
289
+ border: 1px solid var(--border);
290
+ border-radius: 8px;
291
+ padding: 11px 13px;
292
  }
293
  .spec-k {
294
+ font-family: var(--font-mono);
295
+ font-size: 0.52rem;
296
+ letter-spacing: 0.12em;
297
+ text-transform: uppercase;
298
+ color: var(--text-3);
299
+ margin-bottom: 5px;
300
  }
301
  .spec-v {
302
+ font-family: var(--font-mono);
303
+ font-size: 0.76rem;
304
+ color: var(--accent);
305
+ font-weight: 400;
306
  }
307
  .spec.wide { grid-column: span 2; }
308
 
 
309
  /* ── ARCH BOX ── */
310
  .arch-box {
311
+ background: var(--bg);
312
+ border: 1px solid var(--border);
313
+ border-radius: 10px;
314
+ padding: 16px 18px;
315
  }
316
  .arch-title {
317
+ font-family: var(--font-mono);
318
+ font-size: 0.54rem;
319
+ letter-spacing: 0.16em;
320
+ text-transform: uppercase;
321
+ color: var(--text-3);
322
+ margin-bottom: 14px;
323
  }
324
  .arch-step {
325
+ display: flex;
326
+ align-items: flex-start;
327
+ gap: 12px;
328
+ margin-bottom: 11px;
329
  }
330
  .arch-step:last-child { margin-bottom: 0; }
331
  .arch-num {
332
+ font-family: var(--font-mono);
333
+ font-size: 0.6rem;
334
+ color: var(--accent-light);
335
+ min-width: 16px;
336
+ margin-top: 1px;
337
+ font-weight: 500;
338
  }
339
  .arch-desc {
340
+ font-family: var(--font-sans);
341
+ font-size: 0.79rem;
342
+ font-weight: 300;
343
+ color: var(--text-2);
344
+ line-height: 1.55;
345
  }
346
 
 
347
  /* ── FILE INPUT ── */
348
  .gr-file {
349
+ border: 1px dashed var(--border-2) !important;
350
+ border-radius: var(--r) !important;
351
+ background: var(--surface-2) !important;
352
+ transition: border-color 0.2s var(--ease), background 0.2s var(--ease) !important;
353
  }
354
  .gr-file:hover {
355
+ border-color: var(--accent-light) !important;
356
+ background: var(--accent-glow) !important;
357
  }
358
 
 
359
  /* ── INPUTS ── */
360
  textarea, input[type="text"] {
361
+ font-family: var(--font-sans) !important;
362
+ font-size: 0.88rem !important;
363
+ font-weight: 300 !important;
364
+ color: var(--text-1) !important;
365
+ background: var(--surface-2) !important;
366
+ border: 1px solid var(--border-2) !important;
367
+ border-radius: var(--r) !important;
368
+ padding: 11px 15px !important;
369
+ line-height: 1.65 !important;
370
+ transition: border-color 0.2s var(--ease), box-shadow 0.2s var(--ease) !important;
371
+ caret-color: var(--accent) !important;
372
  }
373
  textarea:focus, input[type="text"]:focus {
374
+ border-color: var(--accent-light) !important;
375
+ box-shadow: 0 0 0 3px var(--accent-glow) !important;
376
+ outline: none !important;
377
+ background: var(--surface) !important;
378
  }
379
  textarea::placeholder, input::placeholder {
380
+ color: var(--text-3) !important;
381
+ font-weight: 300 !important;
382
  }
383
 
 
384
  .status-box textarea {
385
+ font-family: var(--font-mono) !important;
386
+ font-size: 0.73rem !important;
387
+ color: var(--text-2) !important;
388
+ background: var(--bg) !important;
389
+ border: 1px solid var(--border) !important;
390
+ line-height: 1.9 !important;
391
  }
392
 
 
393
  /* ── BUTTONS ── */
394
  button { font-family: var(--font-sans) !important; cursor: pointer !important; }
395
 
 
396
  button.primary {
397
+ background: var(--accent) !important;
398
+ color: #faf8f5 !important;
399
+ border: none !important;
400
+ border-radius: 7px !important;
401
+ padding: 10px 22px !important;
402
+ font-size: 0.8rem !important;
403
+ font-weight: 500 !important;
404
+ letter-spacing: 0.02em !important;
405
+ transition: all 0.18s var(--ease) !important;
406
+ box-shadow: 0 1px 3px rgba(124,106,80,0.25) !important;
407
  }
408
  button.primary:hover {
409
+ background: var(--accent-light) !important;
410
+ transform: translateY(-1px) !important;
411
+ box-shadow: 0 4px 14px rgba(124,106,80,0.22) !important;
412
  }
413
  button.primary:active { transform: translateY(0) !important; box-shadow: none !important; }
414
  button.primary:disabled {
415
+ background: var(--surface-2) !important;
416
+ color: var(--text-3) !important;
417
+ box-shadow: none !important;
418
+ cursor: not-allowed !important;
419
+ transform: none !important;
420
  }
421
 
 
422
  button.secondary {
423
+ background: transparent !important;
424
+ color: var(--text-2) !important;
425
+ border: 1px solid var(--border-2) !important;
426
+ border-radius: 7px !important;
427
+ padding: 10px 18px !important;
428
+ font-size: 0.8rem !important;
429
+ font-weight: 400 !important;
430
+ transition: all 0.18s var(--ease) !important;
431
  }
432
  button.secondary:hover {
433
+ color: var(--text-1) !important;
434
+ border-color: var(--text-3) !important;
435
+ background: var(--surface-2) !important;
436
  }
437
 
 
438
  /* ── CHATBOT ── */
439
  .chatbot-wrap > div {
440
+ background: var(--bg) !important;
441
+ border: 1px solid var(--border-2) !important;
442
+ border-radius: var(--r) !important;
443
+ padding: 10px !important;
444
+ }
445
+ .message-wrap .user-row .message,
446
+ .message.user {
447
+ background: var(--accent) !important;
448
+ color: #faf8f5 !important;
449
+ border: none !important;
450
+ border-radius: 14px 14px 3px 14px !important;
451
+ font-size: 0.85rem !important;
452
+ font-weight: 300 !important;
453
+ line-height: 1.7 !important;
454
+ padding: 11px 15px !important;
455
+ max-width: 76% !important;
456
+ margin-left: auto !important;
457
+ box-shadow: 0 2px 8px rgba(124,106,80,0.15) !important;
458
+ }
459
+ .message-wrap .bot-row .message,
460
+ .message.bot {
461
+ background: var(--surface) !important;
462
+ color: var(--text-1) !important;
463
+ border: 1px solid var(--border-2) !important;
464
+ border-radius: 14px 14px 14px 3px !important;
465
+ font-size: 0.85rem !important;
466
+ font-weight: 300 !important;
467
+ line-height: 1.7 !important;
468
+ padding: 11px 15px !important;
469
+ max-width: 76% !important;
470
  }
471
 
 
472
  /* ── LABELS ── */
473
  label > span, .gr-form label span {
474
+ font-family: var(--font-mono) !important;
475
+ font-size: 0.56rem !important;
476
+ font-weight: 500 !important;
477
+ letter-spacing: 0.12em !important;
478
+ text-transform: uppercase !important;
479
+ color: var(--text-3) !important;
480
  }
481
 
 
482
  /* ── FOOTER ── */
483
  .footer {
484
+ text-align: center;
485
+ margin-top: 44px;
486
+ padding-top: 20px;
487
+ border-top: 1px solid var(--border-2);
488
+ display: flex;
489
+ align-items: center;
490
+ justify-content: center;
491
+ gap: 14px;
492
  }
493
  .footer p {
494
+ font-family: var(--font-mono);
495
+ font-size: 0.56rem;
496
+ color: var(--text-3);
497
+ letter-spacing: 0.1em;
498
+ text-transform: uppercase;
499
  }
500
  .footer-sep { color: var(--text-3); font-size: 0.45rem; }
501
  """
502
 
 
503
  # ── UI ────────────────────────────────────────────────────────────────────────
504
+ # ✦ Gradio 6.0: css moves to launch(), title stays in Blocks
505
+ with gr.Blocks(title="Document Intelligence β€” Jason Kishore") as demo:
506
+
507
+ gr.HTML("""
508
+ <div class="mast">
509
+ <div>
510
+ <div class="mast-title">Document <em>Intelligence</em></div>
511
+ <div class="mast-sub">Retrieval-Augmented Generation Β· Semantic Search Β· GPT-4o Mini</div>
512
+ </div>
513
+ <div class="portfolio-badge">
514
+ <div class="badge-inner">
515
+ <div class="badge-dot"></div>
516
+ <div>
517
+ <div class="badge-text">Jason Kishore</div>
518
+ <div class="badge-sub">Portfolio Project</div>
519
+ </div>
520
+ </div>
521
+ <div class="stack-chips">
522
+ <span class="chip">Python</span>
523
+ <span class="chip">OpenAI</span>
524
+ <span class="chip">Gradio</span>
525
+ <span class="chip">RAG</span>
526
+ </div>
527
+ </div>
528
+ </div>
529
+ """)
530
+
531
+ with gr.Tabs(elem_classes=["tabs"]):
532
+
533
+ # ── Tab 1 Β· Upload ──────────────��────────────────────────────────────
534
+ with gr.TabItem("01 Β· Upload & Index"):
535
+ with gr.Column(elem_classes=["panel"]):
536
+ gr.HTML('<div class="eyebrow">Document Ingestion</div>')
537
+ gr.HTML('<div class="upload-grid">')
538
+
539
+ with gr.Column():
540
+ pdf_input = gr.File(label="Upload PDF", file_types=[".pdf"])
541
+ upload_btn = gr.Button("Index Document", variant="primary")
542
+ upload_status = gr.Textbox(
543
+ label="Status",
544
+ lines=2,
545
+ interactive=False,
546
+ value="β€” Awaiting document",
547
+ elem_classes=["status-box"]
548
+ )
549
+
550
+ with gr.Column():
551
+ gr.HTML("""
552
+ <div class="upload-right">
553
+ <div>
554
+ <div class="eyebrow">Configuration</div>
555
+ <div class="specs">
556
+ <div class="spec">
557
+ <div class="spec-k">Embed Model</div>
558
+ <div class="spec-v">3-small</div>
559
+ </div>
560
+ <div class="spec">
561
+ <div class="spec-k">Chat Model</div>
562
+ <div class="spec-v">4o-mini</div>
563
+ </div>
564
+ <div class="spec">
565
+ <div class="spec-k">Chunk Size</div>
566
+ <div class="spec-v">100 words</div>
567
+ </div>
568
+ <div class="spec">
569
+ <div class="spec-k">Overlap</div>
570
+ <div class="spec-v">40 words</div>
571
+ </div>
572
+ <div class="spec wide">
573
+ <div class="spec-k">Top-K Retrieval</div>
574
+ <div class="spec-v">2 chunks / query</div>
575
+ </div>
576
+ </div>
577
+ </div>
578
+ <div class="arch-box">
579
+ <div class="arch-title">Pipeline</div>
580
+ <div class="arch-step">
581
+ <div class="arch-num">01</div>
582
+ <div class="arch-desc">PDF text extracted page-by-page via pypdf</div>
583
+ </div>
584
+ <div class="arch-step">
585
+ <div class="arch-num">02</div>
586
+ <div class="arch-desc">Text split into overlapping 100-word chunks</div>
587
+ </div>
588
+ <div class="arch-step">
589
+ <div class="arch-num">03</div>
590
+ <div class="arch-desc">Each chunk embedded with text-embedding-3-small</div>
591
+ </div>
592
+ <div class="arch-step">
593
+ <div class="arch-num">04</div>
594
+ <div class="arch-desc">Query embedded β†’ top-2 chunks by cosine similarity</div>
595
+ </div>
596
+ <div class="arch-step">
597
+ <div class="arch-num">05</div>
598
+ <div class="arch-desc">GPT-4o Mini answers strictly from retrieved context</div>
599
+ </div>
600
+ </div>
601
+ </div>
602
+ """)
603
+
604
+ gr.HTML('</div>')
605
+
606
+ # ── Tab 2 Β· Chat ─────────────────────────────────────────────────────
607
+ with gr.TabItem("02 Β· Chat"):
608
+ with gr.Column(elem_classes=["panel"]):
609
+ gr.HTML('<div class="eyebrow">Conversation</div>')
610
+
611
+ # ✦ Gradio 6.0: bubble_full_width removed
612
+ chatbot = gr.Chatbot(
613
+ height=440,
614
+ show_label=False,
615
+ elem_classes=["chatbot-wrap"],
616
+ placeholder=(
617
+ "<div style='font-family:DM Mono,monospace;font-size:0.62rem;"
618
+ "color:#a89e90;text-align:center;padding:80px 0;"
619
+ "letter-spacing:0.14em;text-transform:uppercase;line-height:2.2'>"
620
+ "Index a document in the Upload tab<br>to begin your session</div>"
621
+ )
622
+ )
623
+
624
+ msg_box = gr.Textbox(
625
+ placeholder="Ask a question about your document…",
626
+ label="Query",
627
+ lines=1,
628
+ max_lines=5,
629
+ )
630
+
631
+ with gr.Row():
632
+ send_btn = gr.Button("Send Query", variant="primary", size="sm")
633
+ clear_btn = gr.Button("Clear", variant="secondary", size="sm")
634
+
635
+ gr.HTML("""
636
+ <div class="footer">
637
+ <p>Jason Kishore Β· Portfolio Project</p>
638
+ <span class="footer-sep">β—†</span>
639
+ <p>Gradio Β· OpenAI Β· pypdf</p>
640
+ <span class="footer-sep">β—†</span>
641
+ <p>Answers grounded strictly in uploaded document</p>
642
+ </div>
643
+ """)
644
+
645
+ # ── Events ───────────────────────────────────────────────────────────────
646
+ upload_btn.click(
647
+ fn=upload_pdf,
648
+ inputs=pdf_input,
649
+ outputs=[upload_status, send_btn]
650
+ )
651
+ send_btn.click(
652
+ fn=chat, inputs=[msg_box, chatbot], outputs=chatbot
653
+ ).then(lambda: "", outputs=msg_box)
654
+
655
+ msg_box.submit(
656
+ fn=chat, inputs=[msg_box, chatbot], outputs=chatbot
657
+ ).then(lambda: "", outputs=msg_box)
658
+
659
+ clear_btn.click(lambda: [], outputs=chatbot)
660
+
661
+ # ✦ Gradio 6.0: css passed here instead of gr.Blocks()
662
+ demo.launch(css=css)