CarsonNg commited on
Commit
fd89df3
Β·
verified Β·
1 Parent(s): 41ba234

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +287 -79
app.py CHANGED
@@ -24,15 +24,167 @@ UPLOAD_TIMEOUT = 120 # seconds
24
  MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB in bytes
25
  MAX_QUESTION_LENGTH = 1000 # characters
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  # ---------- Utility Functions ----------
29
  def get_backend_url(endpoint: str) -> str:
30
- """Get the URL for a specific backend endpoint."""
31
  return BACKEND_URLS.get(endpoint, BACKEND_URLS["query"])
32
 
33
 
34
  def format_file_size(size_bytes: int) -> str:
35
- """Format file size in human-readable format."""
36
  if size_bytes < 1024:
37
  return f"{size_bytes} bytes"
38
  elif size_bytes < 1024 * 1024:
@@ -42,11 +194,8 @@ def format_file_size(size_bytes: int) -> str:
42
 
43
 
44
  def clean_answer(answer: str) -> str:
45
- """Remove prompt-template bleed-through from the model's response."""
46
- # Truncate at [INST] β€” everything after is a leaked prompt
47
  if '[INST]' in answer:
48
  answer = answer[:answer.index('[INST]')]
49
- # Truncate at any remaining SYS markers
50
  for marker in ['<<SYS>>', '<</SYS>>', '<</SYS']:
51
  if marker in answer:
52
  answer = answer[:answer.index(marker)]
@@ -54,65 +203,74 @@ def clean_answer(answer: str) -> str:
54
 
55
 
56
  # ---------- Backend Communication Functions ----------
57
- def check_health() -> Tuple[bool, Optional[str]]:
58
- """Check if backend is running and return (index_exists, indexed_filename)."""
59
  try:
60
  resp = requests.get(get_backend_url("health"), timeout=WARMUP_TIMEOUT)
61
  if resp.status_code == 200:
62
  data = resp.json()
63
- index_exists = data.get("index_exists", False)
64
- filename = data.get("indexed_filename", None)
65
- return index_exists, filename
66
- return False, None
 
 
67
  except Exception:
68
- return False, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
 
71
- def upload_document(file_obj) -> Tuple[bool, Optional[str], str]:
72
- """
73
- Upload a document to the backend for indexing.
74
- Returns tuple: (success: bool, filename: str, message: str)
75
- """
76
  if file_obj is None:
77
- return False, None, "❌ No file selected."
78
 
79
  file_size = os.path.getsize(file_obj.name) if os.path.exists(file_obj.name) else 0
80
  if file_size > MAX_FILE_SIZE:
81
- return False, None, f"❌ File too large. Maximum size is {format_file_size(MAX_FILE_SIZE)}."
82
 
83
  try:
84
  with open(file_obj.name, "r", encoding="utf-8") as f:
85
  text_content = f.read()
 
 
86
 
87
- if not text_content or not text_content.strip():
88
- return False, None, "❌ File is empty."
89
-
90
- filename = os.path.basename(file_obj.name)
91
 
92
- payload = {"text": text_content, "filename": filename}
93
- response = requests.post(
94
- get_backend_url("upload"),
95
- json=payload,
96
- timeout=UPLOAD_TIMEOUT
97
- )
98
- response.raise_for_status()
99
- data = response.json()
100
 
101
- if data.get("status") == "success":
102
- return True, filename, f"βœ… Indexed {filename}"
103
- else:
104
- return False, None, f"❌ {data.get('message', 'Upload failed')}"
105
 
106
- except requests.exceptions.Timeout:
107
- return False, None, "❌ Upload timed out. Please try a smaller file."
108
- except UnicodeDecodeError:
109
- return False, None, "❌ Could not decode file. Please use a text file."
110
- except Exception as e:
111
- return False, None, f"❌ Upload failed: {str(e)}"
112
 
113
 
114
  def query_backend(message: str) -> Generator[str, None, None]:
115
- """Send a question to the backend and yield the response string."""
116
  if not message or not message.strip():
117
  return
118
 
@@ -125,13 +283,16 @@ def query_backend(message: str) -> Generator[str, None, None]:
125
  response = requests.post(
126
  get_backend_url("query"),
127
  json={"question": message},
128
- timeout=QUERY_TIMEOUT
129
  )
130
  response.raise_for_status()
131
  data = response.json()
132
 
133
- answer = data.get("answer", "⚠️ Received an empty response.")
134
- yield clean_answer(answer)
 
 
 
135
 
136
  except requests.exceptions.Timeout:
137
  yield "❌ Query timed out. Please try again."
@@ -141,26 +302,22 @@ def query_backend(message: str) -> Generator[str, None, None]:
141
  yield f"❌ Error: {str(e)}"
142
 
143
 
144
- def update_index_display(filename: Optional[str], is_indexed: bool) -> str:
145
  if is_indexed and filename:
146
- return f"**Indexed file:** πŸ“„ {filename}"
 
147
  elif is_indexed:
148
- return "**Indexed file:** βœ… Document indexed (filename unknown)"
149
- return "**Indexed file:** No document indexed"
150
 
151
 
152
- def load_index_state() -> Tuple[bool, Optional[str], str]:
153
- """Called on page load β€” uses /health to get current index state."""
154
- is_indexed, filename = check_health()
155
- display = update_index_display(filename, is_indexed)
156
- return is_indexed, filename, display
157
 
158
 
159
  def warmup_with_status(start_time: float = None) -> Generator[Tuple[str, bool, str, float], None, None]:
160
- """
161
- Polls the health endpoint in a background thread and ticks the timer every second.
162
- Yields: (status_message, success, button_label, start_time)
163
- """
164
  if start_time is None:
165
  start_time = time.time()
166
 
@@ -179,16 +336,33 @@ def warmup_with_status(start_time: float = None) -> Generator[Tuple[str, bool, s
179
  thread = threading.Thread(target=do_request, daemon=True)
180
  thread.start()
181
 
 
 
 
 
 
 
 
 
182
  while not result["done"]:
183
  elapsed = time.time() - start_time
184
- msg = f"⏳ Loading GPU snapshots... ({elapsed:.1f}s elapsed)"
185
- yield msg, False, f"⏳ Warming up... ({elapsed:.1f}s)", start_time
 
 
 
 
186
  time.sleep(1)
187
 
188
  elapsed = time.time() - start_time
189
  if result["success"]:
190
- msg = f"βœ… Backend ready ({elapsed:.1f}s)"
191
- yield msg, True, msg, start_time
 
 
 
 
 
192
  elif result["error"]:
193
  msg = f"❌ Cannot connect: {result['error']} ({elapsed:.1f}s)"
194
  yield msg, False, f"❌ Failed ({elapsed:.1f}s)", start_time
@@ -203,6 +377,7 @@ def create_app() -> gr.Blocks:
203
  page = gr.State("warmup")
204
  document_indexed = gr.State(False)
205
  indexed_filename = gr.State(None)
 
206
 
207
  # --- Warmup page ---
208
  with gr.Column(elem_id="warmup-page") as warmup_page:
@@ -244,22 +419,56 @@ def create_app() -> gr.Blocks:
244
  """
245
  <div style="text-align: center; margin-bottom: 2em;">
246
  <h1>πŸ“š RAG Chat Interface</h1>
247
- <p style="color: #666;">Upload a document, then ask questions about its content.</p>
248
  </div>
249
  """
250
  )
251
 
252
  with gr.Row():
253
- with gr.Column():
254
- index_status = gr.Markdown("**Indexed file:** No document indexed")
255
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  with gr.Row():
257
  with gr.Column(scale=2):
258
  file_input = gr.File(
259
  label="Upload .txt file",
260
  file_types=[".txt"],
261
  file_count="single",
262
- show_label=True
263
  )
264
  with gr.Column(scale=1):
265
  upload_btn = gr.Button("πŸ“€ Build Index", variant="primary", size="lg")
@@ -267,27 +476,25 @@ def create_app() -> gr.Blocks:
267
  with gr.Row():
268
  upload_status = gr.Markdown("**Status:** Waiting for document...")
269
 
 
 
 
 
270
  upload_btn.click(
271
  fn=upload_document,
272
  inputs=[file_input],
273
- outputs=[document_indexed, indexed_filename, upload_status]
274
  ).then(
275
- fn=update_index_display,
276
- inputs=[indexed_filename, document_indexed],
277
- outputs=[index_status]
278
  )
279
 
280
- gr.Markdown(
281
- """
282
- <div style="margin-top: 2em; margin-bottom: 1em;">
283
- <h3>πŸ’¬ Ask Questions</h3>
284
- </div>
285
- """
286
- )
287
 
288
  def chat_wrapper(message: str, history: list, is_indexed: bool) -> Generator[str, None, None]:
289
  if not is_indexed:
290
- yield "⚠️ Please upload a document first using the 'Build Index' button."
291
  return
292
  yield from query_backend(message)
293
 
@@ -296,6 +503,7 @@ def create_app() -> gr.Blocks:
296
  additional_inputs=[document_indexed],
297
  title=None,
298
  description=None,
 
299
  )
300
 
301
  # Switch pages when warmup completes
@@ -308,7 +516,7 @@ def create_app() -> gr.Blocks:
308
  # On page load, check /health and populate index state for chat page
309
  demo.load(
310
  fn=load_index_state,
311
- outputs=[document_indexed, indexed_filename, index_status]
312
  )
313
 
314
  return demo
 
24
  MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB in bytes
25
  MAX_QUESTION_LENGTH = 1000 # characters
26
 
27
+ # ---------- Sample Annual Reports ----------
28
+ SAMPLE_DOCUMENTS = {
29
+ 2023: """\
30
+ TechVision Corp β€” Annual Report 2023
31
+
32
+ LETTER TO SHAREHOLDERS
33
+ Dear Shareholders,
34
+ 2023 was a year of resilient growth despite macro headwinds. We delivered record revenue
35
+ while investing heavily in our AI platform and expanding into three new markets.
36
+
37
+ FINANCIAL HIGHLIGHTS
38
+ Total Revenue: $4.82 billion (+12% YoY)
39
+ Gross Profit: $2.17 billion (45.0% margin)
40
+ Operating Income: $681 million (14.1% margin)
41
+ Net Income: $512 million
42
+ EPS (diluted): $3.18
43
+ Free Cash Flow: $743 million
44
+ Headcount (year-end): 18,400
45
+
46
+ SEGMENT REVENUE
47
+ Cloud Services: $2.61B (+21%)
48
+ Enterprise Software:$1.44B (+4%)
49
+ Professional Svcs: $0.77B (+2%)
50
+
51
+ KEY RISKS
52
+ 1. Increasing competition from hyperscale cloud providers threatening Cloud Services margins.
53
+ 2. Customer concentration β€” top 10 clients represent 34% of revenue.
54
+ 3. Cybersecurity threats and potential data-breach liability.
55
+ 4. Regulatory scrutiny around AI model outputs in the EU (AI Act compliance costs).
56
+ 5. Foreign-exchange headwinds: 38% of revenue is denominated in non-USD currencies.
57
+
58
+ EXECUTIVE LEADERSHIP
59
+ CEO: Sarah Chen (since 2019)
60
+ CFO: Marcus Webb (since 2021)
61
+ COO: Priya Nair (since 2022)
62
+ CTO: Daniel Kowalski (since 2020)
63
+ """,
64
+ 2024: """\
65
+ TechVision Corp β€” Annual Report 2024
66
+
67
+ LETTER TO SHAREHOLDERS
68
+ Dear Shareholders,
69
+ 2024 marked our strongest growth year since IPO. AI-powered features drove record
70
+ adoption in Cloud Services and we completed the acquisition of DataBridge Inc.
71
+
72
+ FINANCIAL HIGHLIGHTS
73
+ Total Revenue: $5.74 billion (+19% YoY)
74
+ Gross Profit: $2.70 billion (47.1% margin)
75
+ Operating Income: $874 million (15.2% margin)
76
+ Net Income: $658 million
77
+ EPS (diluted): $4.07
78
+ Free Cash Flow: $921 million
79
+ Headcount (year-end): 21,750 (includes ~1,200 from DataBridge acquisition)
80
+
81
+ SEGMENT REVENUE
82
+ Cloud Services: $3.38B (+30%)
83
+ Enterprise Software:$1.52B (+6%)
84
+ Professional Svcs: $0.84B (+9%)
85
+
86
+ KEY RISKS
87
+ 1. Integration risk from DataBridge acquisition β€” potential system and culture misalignment.
88
+ 2. GPU supply constraints limiting AI infrastructure expansion capacity.
89
+ 3. Rising interest rates increasing cost of capital for planned data-center builds.
90
+ 4. Talent retention β€” attrition in AI/ML roles reached 14%, above industry average.
91
+ 5. Geopolitical risk: 12% of revenue exposed to APAC regions under trade restrictions.
92
+
93
+ EXECUTIVE LEADERSHIP
94
+ CEO: Sarah Chen (since 2019)
95
+ CFO: Marcus Webb (since 2021)
96
+ COO: James Okafor (joined 2024, replaced Priya Nair)
97
+ CTO: Daniel Kowalski (since 2020)
98
+ Chief AI Officer: Lena Park (new role, appointed 2024)
99
+ """,
100
+ 2025: """\
101
+ TechVision Corp β€” Annual Report 2025
102
+
103
+ LETTER TO SHAREHOLDERS
104
+ Dear Shareholders,
105
+ 2025 was a transformational year. We crossed $7 billion in revenue, launched TechVision AI Studio,
106
+ and completed the full integration of DataBridge. Our AI platform now serves over 6,000 enterprise clients.
107
+
108
+ FINANCIAL HIGHLIGHTS
109
+ Total Revenue: $7.11 billion (+24% YoY)
110
+ Gross Profit: $3.52 billion (49.5% margin)
111
+ Operating Income: $1.14 billion (16.0% margin)
112
+ Net Income: $867 million
113
+ EPS (diluted): $5.36
114
+ Free Cash Flow: $1.18 billion
115
+ Headcount (year-end): 25,300
116
+
117
+ SEGMENT REVENUE
118
+ Cloud Services: $4.40B (+30%)
119
+ Enterprise Software:$1.74B (+15%)
120
+ Professional Svcs: $0.97B (+15%)
121
+
122
+ KEY RISKS
123
+ 1. Commoditization of AI features β€” competitors offering similar AI tooling at lower price points.
124
+ 2. Regulatory compliance costs (EU AI Act fully effective; NIST AI RMF adoption in US contracts).
125
+ 3. Data-center energy costs up 28% YoY, pressuring infrastructure margins.
126
+ 4. Concentration in Cloud Services (62% of revenue) increases segment-specific risk exposure.
127
+ 5. Macroeconomic slowdown risk β€” enterprise IT budgets under review in 40% of Fortune 500 accounts.
128
+
129
+ EXECUTIVE LEADERSHIP
130
+ CEO: Sarah Chen (since 2019)
131
+ CFO: Rachel Torres (appointed Q2 2025, replaced Marcus Webb)
132
+ COO: James Okafor (since 2024)
133
+ CTO: Daniel Kowalski (since 2020)
134
+ Chief AI Officer: Lena Park (since 2024)
135
+ """,
136
+ 2026: """\
137
+ TechVision Corp β€” Annual Report 2026
138
+
139
+ LETTER TO SHAREHOLDERS
140
+ Dear Shareholders,
141
+ 2026 delivered another year of double-digit growth. TechVision AI Studio reached 10,000 enterprise
142
+ deployments and we launched our sovereign-cloud offering in five new countries.
143
+
144
+ FINANCIAL HIGHLIGHTS
145
+ Total Revenue: $8.63 billion (+21% YoY)
146
+ Gross Profit: $4.40 billion (51.0% margin)
147
+ Operating Income: $1.47 billion (17.0% margin)
148
+ Net Income: $1.12 billion
149
+ EPS (diluted): $6.91
150
+ Free Cash Flow: $1.55 billion
151
+ Headcount (year-end): 28,900
152
+
153
+ SEGMENT REVENUE
154
+ Cloud Services: $5.46B (+24%)
155
+ Enterprise Software:$2.07B (+19%)
156
+ Professional Svcs: $1.10B (+13%)
157
+
158
+ KEY RISKS
159
+ 1. Sovereign-cloud expansion exposes TechVision to new jurisdictional data-residency obligations.
160
+ 2. Increasing AI regulation globally β€” 23 countries now have enacted AI-specific legislation.
161
+ 3. Model hallucination liability: three enterprise clients filed claims in 2026 over AI output errors.
162
+ 4. Supply-chain risk: sole-source dependency on two semiconductor vendors for proprietary AI chips.
163
+ 5. Executive succession planning β€” CEO Sarah Chen announced intent to transition by end of 2027.
164
+
165
+ EXECUTIVE LEADERSHIP
166
+ CEO: Sarah Chen (since 2019; transition planned for 2027)
167
+ CFO: Rachel Torres (since 2025)
168
+ COO: James Okafor (since 2024)
169
+ CTO: Nina Vasquez (appointed 2026, replaced Daniel Kowalski)
170
+ Chief AI Officer: Lena Park (since 2024)
171
+ """,
172
+ }
173
+
174
+ SAMPLE_QUESTIONS = [
175
+ "What was the total revenue in respective years?",
176
+ "List the top 3 risks mentioned.",
177
+ "Who are the key executives?",
178
+ "Sort revenue by year (from highest to lowest)",
179
+ ]
180
+
181
 
182
  # ---------- Utility Functions ----------
183
  def get_backend_url(endpoint: str) -> str:
 
184
  return BACKEND_URLS.get(endpoint, BACKEND_URLS["query"])
185
 
186
 
187
  def format_file_size(size_bytes: int) -> str:
 
188
  if size_bytes < 1024:
189
  return f"{size_bytes} bytes"
190
  elif size_bytes < 1024 * 1024:
 
194
 
195
 
196
  def clean_answer(answer: str) -> str:
 
 
197
  if '[INST]' in answer:
198
  answer = answer[:answer.index('[INST]')]
 
199
  for marker in ['<<SYS>>', '<</SYS>>', '<</SYS']:
200
  if marker in answer:
201
  answer = answer[:answer.index(marker)]
 
203
 
204
 
205
  # ---------- Backend Communication Functions ----------
206
+ def check_health() -> Tuple[bool, Optional[str], Optional[int]]:
207
+ """Returns (index_exists, indexed_filename, indexed_char_count)."""
208
  try:
209
  resp = requests.get(get_backend_url("health"), timeout=WARMUP_TIMEOUT)
210
  if resp.status_code == 200:
211
  data = resp.json()
212
+ return (
213
+ data.get("index_exists", False),
214
+ data.get("indexed_filename", None),
215
+ data.get("indexed_char_count", None),
216
+ )
217
+ return False, None, None
218
  except Exception:
219
+ return False, None, None
220
+
221
+
222
+ def _upload_text(text_content: str, filename: str) -> Tuple[bool, Optional[str], Optional[int], str]:
223
+ """Core upload logic shared by file upload and sample loaders."""
224
+ payload = {"text": text_content, "filename": filename}
225
+ try:
226
+ response = requests.post(
227
+ get_backend_url("upload"),
228
+ json=payload,
229
+ timeout=UPLOAD_TIMEOUT,
230
+ )
231
+ response.raise_for_status()
232
+ data = response.json()
233
+ if data.get("status") == "success":
234
+ char_count = data.get("char_count")
235
+ return True, filename, char_count, f"βœ… Indexed **{filename}**"
236
+ else:
237
+ return False, None, None, f"❌ {data.get('message', 'Upload failed')}"
238
+ except requests.exceptions.Timeout:
239
+ return False, None, None, "❌ Upload timed out. Please try a smaller file."
240
+ except Exception as e:
241
+ return False, None, None, f"❌ Upload failed: {str(e)}"
242
 
243
 
244
+ def upload_document(file_obj) -> Tuple[bool, Optional[str], Optional[int], str]:
245
+ """Upload a user-selected .txt file."""
 
 
 
246
  if file_obj is None:
247
+ return False, None, None, "❌ No file selected."
248
 
249
  file_size = os.path.getsize(file_obj.name) if os.path.exists(file_obj.name) else 0
250
  if file_size > MAX_FILE_SIZE:
251
+ return False, None, None, f"❌ File too large. Maximum size is {format_file_size(MAX_FILE_SIZE)}."
252
 
253
  try:
254
  with open(file_obj.name, "r", encoding="utf-8") as f:
255
  text_content = f.read()
256
+ except UnicodeDecodeError:
257
+ return False, None, None, "❌ Could not decode file. Please use a UTF-8 text file."
258
 
259
+ if not text_content or not text_content.strip():
260
+ return False, None, None, "❌ File is empty."
 
 
261
 
262
+ filename = os.path.basename(file_obj.name)
263
+ return _upload_text(text_content, filename)
 
 
 
 
 
 
264
 
 
 
 
 
265
 
266
+ def load_sample_document(year: int) -> Tuple[bool, Optional[str], Optional[int], str]:
267
+ """Upload a built-in sample annual report."""
268
+ text = SAMPLE_DOCUMENTS[year]
269
+ filename = f"annual_report_{year}.txt"
270
+ return _upload_text(text, filename)
 
271
 
272
 
273
  def query_backend(message: str) -> Generator[str, None, None]:
 
274
  if not message or not message.strip():
275
  return
276
 
 
283
  response = requests.post(
284
  get_backend_url("query"),
285
  json={"question": message},
286
+ timeout=QUERY_TIMEOUT,
287
  )
288
  response.raise_for_status()
289
  data = response.json()
290
 
291
+ answer = clean_answer(data.get("answer", "⚠️ Received an empty response."))
292
+ elapsed = data.get("elapsed")
293
+ if elapsed is not None:
294
+ answer += f"\n\n<sub style='color:#888'>⏱️ {elapsed:.1f} seconds</sub>"
295
+ yield answer
296
 
297
  except requests.exceptions.Timeout:
298
  yield "❌ Query timed out. Please try again."
 
302
  yield f"❌ Error: {str(e)}"
303
 
304
 
305
+ def update_index_display(filename: Optional[str], is_indexed: bool, char_count: Optional[int] = None) -> str:
306
  if is_indexed and filename:
307
+ detail = f" ({char_count:,} characters)" if char_count else ""
308
+ return f"πŸ“„ **Indexed Document:** {filename}{detail}"
309
  elif is_indexed:
310
+ return "πŸ“„ **Indexed Document:** βœ… Document indexed (filename unknown)"
311
+ return "πŸ“„ **Indexed Document:** None"
312
 
313
 
314
+ def load_index_state() -> Tuple[bool, Optional[str], Optional[int], str]:
315
+ is_indexed, filename, char_count = check_health()
316
+ display = update_index_display(filename, is_indexed, char_count)
317
+ return is_indexed, filename, char_count, display
 
318
 
319
 
320
  def warmup_with_status(start_time: float = None) -> Generator[Tuple[str, bool, str, float], None, None]:
 
 
 
 
321
  if start_time is None:
322
  start_time = time.time()
323
 
 
336
  thread = threading.Thread(target=do_request, daemon=True)
337
  thread.start()
338
 
339
+ # Simulated step messages keyed by elapsed-time thresholds
340
+ STEPS = [
341
+ (0, "⏳ Initializing GPU model (Qwen 2.5 3B)..."),
342
+ (3, "⏳ Initializing GPU model (Qwen 2.5 3B)...\nβœ… Embedding model ready (BGE-small)"),
343
+ (6, "⏳ Initializing GPU model (Qwen 2.5 3B)...\nβœ… Embedding model ready (BGE-small)\n⏳ Loading index from volume..."),
344
+ (9, "βœ… GPU model loaded (Qwen 2.5 3B)\nβœ… Embedding model ready (BGE-small)\n⏳ Loading index from volume..."),
345
+ ]
346
+
347
  while not result["done"]:
348
  elapsed = time.time() - start_time
349
+ step_msg = STEPS[0][1]
350
+ for threshold, msg in STEPS:
351
+ if elapsed >= threshold:
352
+ step_msg = msg
353
+ status = f"{step_msg}\n\n*({elapsed:.1f}s elapsed)*"
354
+ yield status, False, f"⏳ Warming up... ({elapsed:.1f}s)", start_time
355
  time.sleep(1)
356
 
357
  elapsed = time.time() - start_time
358
  if result["success"]:
359
+ final = (
360
+ f"βœ… GPU model loaded (Qwen 2.5 3B)\n"
361
+ f"βœ… Embedding model ready (BGE-small)\n"
362
+ f"βœ… Index volume ready\n\n"
363
+ f"**βœ… Backend ready in {elapsed:.1f}s**"
364
+ )
365
+ yield final, True, f"βœ… Ready ({elapsed:.1f}s)", start_time
366
  elif result["error"]:
367
  msg = f"❌ Cannot connect: {result['error']} ({elapsed:.1f}s)"
368
  yield msg, False, f"❌ Failed ({elapsed:.1f}s)", start_time
 
377
  page = gr.State("warmup")
378
  document_indexed = gr.State(False)
379
  indexed_filename = gr.State(None)
380
+ indexed_char_count = gr.State(None)
381
 
382
  # --- Warmup page ---
383
  with gr.Column(elem_id="warmup-page") as warmup_page:
 
419
  """
420
  <div style="text-align: center; margin-bottom: 2em;">
421
  <h1>πŸ“š RAG Chat Interface</h1>
422
+ <p style="color: #666;">Upload a document or load a sample report, then ask questions.</p>
423
  </div>
424
  """
425
  )
426
 
427
  with gr.Row():
428
+ index_status = gr.Markdown("πŸ“„ **Indexed Document:** None")
 
429
 
430
+ # --- Sample document loader ---
431
+ gr.Markdown("### πŸ“‹ Load Sample Annual Report")
432
+ with gr.Row():
433
+ sample_2023_btn = gr.Button("πŸ“„ Load Sample: Annual Report 2023")
434
+ sample_2024_btn = gr.Button("πŸ“„ Load Sample: Annual Report 2024")
435
+ sample_2025_btn = gr.Button("πŸ“„ Load Sample: Annual Report 2025")
436
+ sample_2026_btn = gr.Button("πŸ“„ Load Sample: Annual Report 2026")
437
+
438
+ sample_status = gr.Markdown("")
439
+
440
+ def _make_sample_loader(year):
441
+ def loader():
442
+ return load_sample_document(year)
443
+ return loader
444
+
445
+ def _on_sample_done(success, filename, char_count, message):
446
+ display = update_index_display(filename, success, char_count)
447
+ return success, filename, char_count, message, display
448
+
449
+ for btn, year in [
450
+ (sample_2023_btn, 2023),
451
+ (sample_2024_btn, 2024),
452
+ (sample_2025_btn, 2025),
453
+ (sample_2026_btn, 2026),
454
+ ]:
455
+ btn.click(
456
+ fn=_make_sample_loader(year),
457
+ outputs=[document_indexed, indexed_filename, indexed_char_count, sample_status]
458
+ ).then(
459
+ fn=_on_sample_done,
460
+ inputs=[document_indexed, indexed_filename, indexed_char_count, sample_status],
461
+ outputs=[document_indexed, indexed_filename, indexed_char_count, sample_status, index_status]
462
+ )
463
+
464
+ gr.Markdown("### πŸ“ Or Upload Your Own Document")
465
  with gr.Row():
466
  with gr.Column(scale=2):
467
  file_input = gr.File(
468
  label="Upload .txt file",
469
  file_types=[".txt"],
470
  file_count="single",
471
+ show_label=True,
472
  )
473
  with gr.Column(scale=1):
474
  upload_btn = gr.Button("πŸ“€ Build Index", variant="primary", size="lg")
 
476
  with gr.Row():
477
  upload_status = gr.Markdown("**Status:** Waiting for document...")
478
 
479
+ def _on_upload_done(success, filename, char_count, message):
480
+ display = update_index_display(filename, success, char_count)
481
+ return success, filename, char_count, message, display
482
+
483
  upload_btn.click(
484
  fn=upload_document,
485
  inputs=[file_input],
486
+ outputs=[document_indexed, indexed_filename, indexed_char_count, upload_status]
487
  ).then(
488
+ fn=_on_upload_done,
489
+ inputs=[document_indexed, indexed_filename, indexed_char_count, upload_status],
490
+ outputs=[document_indexed, indexed_filename, indexed_char_count, upload_status, index_status]
491
  )
492
 
493
+ gr.Markdown("### πŸ’¬ Ask Questions")
 
 
 
 
 
 
494
 
495
  def chat_wrapper(message: str, history: list, is_indexed: bool) -> Generator[str, None, None]:
496
  if not is_indexed:
497
+ yield "⚠️ Please upload a document or load a sample report first."
498
  return
499
  yield from query_backend(message)
500
 
 
503
  additional_inputs=[document_indexed],
504
  title=None,
505
  description=None,
506
+ examples=SAMPLE_QUESTIONS,
507
  )
508
 
509
  # Switch pages when warmup completes
 
516
  # On page load, check /health and populate index state for chat page
517
  demo.load(
518
  fn=load_index_state,
519
+ outputs=[document_indexed, indexed_filename, indexed_char_count, index_status]
520
  )
521
 
522
  return demo