JasonKishore commited on
Commit
c67a8e2
Β·
verified Β·
1 Parent(s): f765487

Update app.py

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