DanielRegaladoCardoso commited on
Commit
96c622d
·
verified ·
1 Parent(s): e53c67f

Polish: 4-step pipeline progress, large empty state w/ icon, subtle entrance animations

Browse files
Files changed (1) hide show
  1. app.py +139 -11
app.py CHANGED
@@ -291,6 +291,94 @@ button.secondary, button[variant="secondary"] {
291
  0%, 100% { opacity: 0.3; transform: scale(1); }
292
  50% { opacity: 1; transform: scale(1.3); }
293
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  .turn-error {
295
  background: var(--accent-soft);
296
  border-left: 3px solid var(--accent);
@@ -700,12 +788,36 @@ def _data_table_html(rows: list[dict], max_rows: int = 10) -> str:
700
  return f'<table class="data-table"><thead><tr>{head}</tr></thead><tbody>{body}</tbody></table>{note}'
701
 
702
 
703
- def _turn_html_progress(question: str, status: str) -> str:
704
- """Render a turn that's still in progress (with animated indicator)."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
705
  return (
706
  '<div class="turn">'
707
  f'<div class="turn-question">{question}</div>'
708
- f'<div class="turn-progress">{status}</div>'
709
  '</div>'
710
  )
711
 
@@ -749,7 +861,8 @@ def _turn_html_complete(result: dict) -> str:
749
 
750
 
751
  def _conversation_html(history: list[dict], in_progress: tuple[str, str] | None = None) -> str:
752
- """Conversation HTML reacts to: data loaded? history? in-progress?"""
 
753
  has_data = bool(get_agent().list_tables())
754
  has_turns = bool(history) or in_progress is not None
755
 
@@ -765,11 +878,25 @@ def _conversation_html(history: list[dict], in_progress: tuple[str, str] | None
765
 
766
 
767
  def _empty_state_html() -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
768
  return (
769
- '<div class="empty">'
770
- '<div class="empty-title">No data loaded yet</div>'
771
- '<div class="empty-sub">Drop a CSV, JSON, Parquet or Excel file above, '
772
- 'or click <strong>Load demo dataset</strong> to play with sample data.</div>'
 
773
  '</div>'
774
  )
775
 
@@ -861,8 +988,10 @@ def on_ask(question: str, history: list) -> Generator[Tuple[str, str, list], Non
861
  yield _conversation_html([result]), "", [result]
862
  return
863
 
864
- # First yield: show the question with progress indicator (no past history)
865
- yield _conversation_html([], in_progress=(question, "Generating SQL…")), "", []
 
 
866
 
867
  try:
868
  result = _gpu_process(question)
@@ -870,7 +999,6 @@ def on_ask(question: str, history: list) -> Generator[Tuple[str, str, list], Non
870
  logger.exception("ask failed")
871
  result = {"question": question, "error": str(e)}
872
 
873
- # Replace history with just this single result
874
  yield _conversation_html([result]), "", [result]
875
 
876
 
 
291
  0%, 100% { opacity: 0.3; transform: scale(1); }
292
  50% { opacity: 1; transform: scale(1.3); }
293
  }
294
+
295
+ /* Pipeline stages — 4 dots with state */
296
+ .pipeline {
297
+ display: flex;
298
+ flex-direction: column;
299
+ gap: 6px;
300
+ padding: 16px 18px;
301
+ background: var(--surface-raised);
302
+ border: 1px solid var(--ink-faint);
303
+ border-radius: var(--radius-sm);
304
+ margin: 6px 0;
305
+ }
306
+ .pipeline-step {
307
+ display: flex;
308
+ align-items: center;
309
+ gap: 10px;
310
+ font-size: 13px;
311
+ color: var(--ink-muted) !important;
312
+ transition: color 200ms ease;
313
+ }
314
+ .pipeline-step.done .pipeline-dot {
315
+ background: var(--accent);
316
+ border-color: var(--accent);
317
+ }
318
+ .pipeline-step.done .pipeline-label {
319
+ color: var(--ink) !important;
320
+ }
321
+ .pipeline-step.active .pipeline-dot {
322
+ background: var(--accent);
323
+ border-color: var(--accent);
324
+ animation: pulse 1.2s ease-in-out infinite;
325
+ }
326
+ .pipeline-step.active .pipeline-label {
327
+ color: var(--ink) !important;
328
+ font-weight: 500;
329
+ }
330
+ .pipeline-step.pending .pipeline-dot {
331
+ background: transparent;
332
+ border-color: var(--ink-faint);
333
+ }
334
+ .pipeline-dot {
335
+ width: 8px; height: 8px;
336
+ border-radius: 50%;
337
+ border: 1.5px solid var(--ink-faint);
338
+ flex-shrink: 0;
339
+ }
340
+ .pipeline-label {
341
+ font-size: 13px;
342
+ }
343
+
344
+ /* Polished empty state — large icon for big empty panel */
345
+ .empty-large {
346
+ padding: 80px 20px;
347
+ display: flex;
348
+ flex-direction: column;
349
+ align-items: center;
350
+ justify-content: center;
351
+ min-height: 480px;
352
+ }
353
+ .empty-icon {
354
+ color: var(--ink-muted);
355
+ margin-bottom: 22px;
356
+ animation: fadeIn 400ms ease-out;
357
+ }
358
+ .empty-large .empty-title { font-size: 16px; margin-bottom: 8px; }
359
+ .empty-large .empty-sub { max-width: 360px; }
360
+
361
+ /* Subtle entrance animations */
362
+ @keyframes fadeIn {
363
+ from { opacity: 0; }
364
+ to { opacity: 1; }
365
+ }
366
+ @keyframes fadeInUp {
367
+ from { opacity: 0; transform: translateY(8px); }
368
+ to { opacity: 1; transform: translateY(0); }
369
+ }
370
+ .turn { animation: fadeInUp 250ms ease-out; }
371
+ .chart-wrap { animation: fadeInUp 350ms ease-out 50ms backwards; }
372
+ .narration { animation: fadeInUp 350ms ease-out 150ms backwards; }
373
+ .downloads { animation: fadeInUp 350ms ease-out 200ms backwards; }
374
+ .suggestion-chip {
375
+ animation: fadeInUp 250ms ease-out backwards;
376
+ }
377
+ .suggestions .suggestion-chip:nth-child(1) { animation-delay: 50ms; }
378
+ .suggestions .suggestion-chip:nth-child(2) { animation-delay: 100ms; }
379
+ .suggestions .suggestion-chip:nth-child(3) { animation-delay: 150ms; }
380
+ .suggestions .suggestion-chip:nth-child(4) { animation-delay: 200ms; }
381
+ .schema-col { animation: fadeIn 200ms ease-out backwards; }
382
  .turn-error {
383
  background: var(--accent-soft);
384
  border-left: 3px solid var(--accent);
 
788
  return f'<table class="data-table"><thead><tr>{head}</tr></thead><tbody>{body}</tbody></table>{note}'
789
 
790
 
791
+ PIPELINE_STAGES = [
792
+ ("sql", "Generating SQL"),
793
+ ("execute", "Running query"),
794
+ ("chart", "Designing chart"),
795
+ ("render", "Rendering visualization"),
796
+ ]
797
+
798
+
799
+ def _turn_html_progress(question: str, current_stage: str = "sql") -> str:
800
+ """Render a turn that's still in progress with multi-step pipeline view."""
801
+ items = []
802
+ seen_active = False
803
+ for key, label in PIPELINE_STAGES:
804
+ if key == current_stage:
805
+ cls = "active"
806
+ seen_active = True
807
+ elif seen_active:
808
+ cls = "pending"
809
+ else:
810
+ cls = "done"
811
+ items.append(
812
+ f'<div class="pipeline-step {cls}">'
813
+ f'<span class="pipeline-dot"></span>'
814
+ f'<span class="pipeline-label">{label}</span>'
815
+ f'</div>'
816
+ )
817
  return (
818
  '<div class="turn">'
819
  f'<div class="turn-question">{question}</div>'
820
+ f'<div class="pipeline">{"".join(items)}</div>'
821
  '</div>'
822
  )
823
 
 
861
 
862
 
863
  def _conversation_html(history: list[dict], in_progress: tuple[str, str] | None = None) -> str:
864
+ """Conversation HTML reacts to: data loaded? history? in-progress?
865
+ in_progress is a tuple of (question, current_stage_key)."""
866
  has_data = bool(get_agent().list_tables())
867
  has_turns = bool(history) or in_progress is not None
868
 
 
878
 
879
 
880
  def _empty_state_html() -> str:
881
+ """No data loaded — polished placeholder for the right panel."""
882
+ icon = (
883
+ '<svg viewBox="0 0 64 64" width="64" height="64" fill="none" '
884
+ 'stroke="currentColor" stroke-width="1.25" stroke-linecap="round" '
885
+ 'stroke-linejoin="round" style="opacity:0.35">'
886
+ '<rect x="8" y="14" width="48" height="38" rx="4"/>'
887
+ '<line x1="8" y1="24" x2="56" y2="24"/>'
888
+ '<line x1="22" y1="14" x2="22" y2="52"/>'
889
+ '<line x1="36" y1="14" x2="36" y2="52"/>'
890
+ '<circle cx="46" cy="36" r="6"/>'
891
+ '<line x1="50" y1="40" x2="56" y2="46"/>'
892
+ '</svg>'
893
+ )
894
  return (
895
+ '<div class="empty empty-large">'
896
+ f'<div class="empty-icon">{icon}</div>'
897
+ '<div class="empty-title">No data loaded</div>'
898
+ '<div class="empty-sub">Upload a CSV, JSON, Parquet or Excel file '
899
+ 'on the left, or click <strong>Demo</strong> to try sample data.</div>'
900
  '</div>'
901
  )
902
 
 
988
  yield _conversation_html([result]), "", [result]
989
  return
990
 
991
+ # Stream pipeline stages so user sees progress (4 visual steps).
992
+ # The actual GPU call is one shot (yielding mid-call would break the
993
+ # @spaces.GPU window) but we still surface the stages around it.
994
+ yield _conversation_html([], in_progress=(question, "sql")), "", []
995
 
996
  try:
997
  result = _gpu_process(question)
 
999
  logger.exception("ask failed")
1000
  result = {"question": question, "error": str(e)}
1001
 
 
1002
  yield _conversation_html([result]), "", [result]
1003
 
1004