JasonKishore commited on
Commit
4b59f3f
Β·
verified Β·
1 Parent(s): 762df3b

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +713 -0
app.py ADDED
@@ -0,0 +1,713 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import openai
3
+ 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
+