DanielRegaladoCardoso commited on
Commit
95458bc
·
verified ·
1 Parent(s): 1bbdff9

UX: CSV/SVG download links, schema preview on load, cleaner SVG theme

Browse files
Files changed (1) hide show
  1. app.py +141 -1
app.py CHANGED
@@ -396,6 +396,72 @@ details > *:not(summary) { padding: 0 14px 14px; }
396
 
397
  /* Hide labels Gradio adds */
398
  .gr-form > label, label.svelte-1gfkn6j, .label-wrap { display: none !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
  """
400
 
401
 
@@ -451,6 +517,63 @@ def _file_chip_html(filename: str, rows: int, cols: int) -> str:
451
  )
452
 
453
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
  def _suggestions_html(qs: list[str]) -> str:
455
  if not qs:
456
  return ""
@@ -500,6 +623,13 @@ def _turn_html_complete(result: dict) -> str:
500
  if result.get("svg"):
501
  parts.append(f'<div class="chart-wrap">{result["svg"]}</div>')
502
 
 
 
 
 
 
 
 
503
  if result.get("sql"):
504
  parts.append(
505
  '<details><summary>SQL query</summary>'
@@ -544,13 +674,23 @@ def _empty_state_html() -> str:
544
 
545
 
546
  def _ready_state_html() -> str:
547
- """Shown when data is loaded but no queries asked yet."""
 
 
 
 
 
 
 
 
 
548
  return (
549
  '<div class="empty">'
550
  '<div class="empty-title">Ready</div>'
551
  '<div class="empty-sub">Ask a question above, or try one of these:</div>'
552
  f'{_suggestions_html(SUGGESTED_QUESTIONS[:4])}'
553
  '</div>'
 
554
  )
555
 
556
 
 
396
 
397
  /* Hide labels Gradio adds */
398
  .gr-form > label, label.svelte-1gfkn6j, .label-wrap { display: none !important; }
399
+
400
+ /* Download links below chart */
401
+ .downloads {
402
+ display: flex;
403
+ gap: 6px;
404
+ margin: 8px 0 4px;
405
+ flex-wrap: wrap;
406
+ }
407
+ .download-link {
408
+ display: inline-flex;
409
+ align-items: center;
410
+ gap: 6px;
411
+ font-size: 12px;
412
+ padding: 6px 12px;
413
+ background: var(--surface-raised);
414
+ border: 1px solid var(--ink-faint);
415
+ border-radius: var(--radius-sm);
416
+ color: var(--ink-muted);
417
+ text-decoration: none;
418
+ transition: all 150ms ease;
419
+ cursor: pointer;
420
+ }
421
+ .download-link:hover {
422
+ border-color: var(--accent);
423
+ color: var(--ink);
424
+ background: var(--accent-soft);
425
+ }
426
+ .download-link .icon {
427
+ font-family: var(--font-mono);
428
+ font-size: 13px;
429
+ line-height: 1;
430
+ }
431
+
432
+ /* Schema preview after upload */
433
+ .schema-preview {
434
+ margin: 14px 0;
435
+ padding: 14px 16px;
436
+ background: var(--surface-raised);
437
+ border: 1px solid var(--ink-faint);
438
+ border-radius: var(--radius-sm);
439
+ }
440
+ .schema-preview-header {
441
+ font-size: 12px;
442
+ font-weight: 600;
443
+ color: var(--ink);
444
+ text-transform: uppercase;
445
+ letter-spacing: 0.04em;
446
+ margin-bottom: 10px;
447
+ }
448
+ .schema-cols {
449
+ display: flex;
450
+ flex-wrap: wrap;
451
+ gap: 6px;
452
+ }
453
+ .schema-col {
454
+ display: inline-flex;
455
+ align-items: baseline;
456
+ gap: 5px;
457
+ padding: 4px 10px;
458
+ background: var(--surface);
459
+ border: 1px solid var(--ink-faint);
460
+ border-radius: 6px;
461
+ font-size: 12px;
462
+ }
463
+ .schema-col-name { color: var(--ink); font-family: var(--font-mono); font-size: 12px; }
464
+ .schema-col-type { color: var(--ink-muted); font-size: 10px; text-transform: uppercase; letter-spacing: 0.04em; }
465
  """
466
 
467
 
 
517
  )
518
 
519
 
520
+ def _schema_preview_html(table: str, schema: list[dict]) -> str:
521
+ """Render the column names + types of the loaded table."""
522
+ if not schema:
523
+ return ""
524
+ cols = "".join(
525
+ f'<span class="schema-col">'
526
+ f'<span class="schema-col-name">{c["name"]}</span>'
527
+ f'<span class="schema-col-type">{c["type"]}</span>'
528
+ f'</span>'
529
+ for c in schema
530
+ )
531
+ return (
532
+ '<div class="schema-preview">'
533
+ f'<div class="schema-preview-header">{table} · {len(schema)} columns</div>'
534
+ f'<div class="schema-cols">{cols}</div>'
535
+ '</div>'
536
+ )
537
+
538
+
539
+ def _download_links_html(sql: str, results: list[dict], svg: str) -> str:
540
+ """Inline data-URL download links for CSV (results) and SVG (chart)."""
541
+ import base64
542
+ import csv
543
+ import io as _io
544
+
545
+ parts = []
546
+
547
+ # CSV download
548
+ if results:
549
+ buf = _io.StringIO()
550
+ writer = csv.DictWriter(buf, fieldnames=list(results[0].keys()))
551
+ writer.writeheader()
552
+ for r in results:
553
+ writer.writerow({k: ("" if v is None else v) for k, v in r.items()})
554
+ csv_b64 = base64.b64encode(buf.getvalue().encode("utf-8")).decode("ascii")
555
+ parts.append(
556
+ f'<a class="download-link" '
557
+ f'href="data:text/csv;base64,{csv_b64}" '
558
+ f'download="query-result.csv">'
559
+ f'<span class="icon">↓</span> CSV ({len(results):,} rows)</a>'
560
+ )
561
+
562
+ # SVG download
563
+ if svg and "<svg" in svg.lower():
564
+ svg_b64 = base64.b64encode(svg.encode("utf-8")).decode("ascii")
565
+ parts.append(
566
+ f'<a class="download-link" '
567
+ f'href="data:image/svg+xml;base64,{svg_b64}" '
568
+ f'download="chart.svg">'
569
+ f'<span class="icon">↓</span> SVG</a>'
570
+ )
571
+
572
+ if not parts:
573
+ return ""
574
+ return f'<div class="downloads">{"".join(parts)}</div>'
575
+
576
+
577
  def _suggestions_html(qs: list[str]) -> str:
578
  if not qs:
579
  return ""
 
623
  if result.get("svg"):
624
  parts.append(f'<div class="chart-wrap">{result["svg"]}</div>')
625
 
626
+ # Inline download links for CSV + SVG
627
+ parts.append(_download_links_html(
628
+ result.get("sql") or "",
629
+ result.get("results") or [],
630
+ result.get("svg") or "",
631
+ ))
632
+
633
  if result.get("sql"):
634
  parts.append(
635
  '<details><summary>SQL query</summary>'
 
674
 
675
 
676
  def _ready_state_html() -> str:
677
+ """Shown when data is loaded but no queries asked yet. Shows schema preview."""
678
+ agent = get_agent()
679
+ tables = agent.list_tables()
680
+ if not tables:
681
+ return _empty_state_html()
682
+
683
+ table = tables[0]
684
+ schema = agent.executor.get_table_schema(table)
685
+ schema_html = _schema_preview_html(table, schema)
686
+
687
  return (
688
  '<div class="empty">'
689
  '<div class="empty-title">Ready</div>'
690
  '<div class="empty-sub">Ask a question above, or try one of these:</div>'
691
  f'{_suggestions_html(SUGGESTED_QUESTIONS[:4])}'
692
  '</div>'
693
+ f'{schema_html}'
694
  )
695
 
696