Shizu0n commited on
Commit
03cc0b0
·
1 Parent(s): a416873

feat: ui/ux improvements

Browse files
Files changed (1) hide show
  1. app.py +669 -462
app.py CHANGED
@@ -1,6 +1,7 @@
1
  import gc
2
  import html
3
  import inspect
 
4
  import threading
5
  import time
6
 
@@ -70,7 +71,28 @@ PROMPT_TEMPLATE = (
70
  "<|assistant|>"
71
  )
72
 
 
 
 
 
 
 
 
 
73
  EMPTY_VALIDATOR = '<span class="validator-badge validator-empty">No SQL yet</span>'
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
  _current_model_id = None
76
  _model = None
@@ -190,6 +212,68 @@ def clean_generation(text):
190
  return cleaned
191
 
192
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  def validate_sql(sql_text):
194
  sql_text = (sql_text or "").strip()
195
  if not sql_text:
@@ -224,12 +308,12 @@ def render_header():
224
  <section class="top-panel">
225
  <div>
226
  <h1>Phi-3 Mini SQL Generator</h1>
227
- <p>QLoRA fine-tuned - b-mc2/sql-create-context - T4 GPU - 21 min</p>
228
  </div>
229
  <div class="top-badges">
230
  <span class="badge badge-green">73.5% exact match</span>
231
  <span class="badge badge-cream">+71.5pp vs base</span>
232
- <span class="badge badge-light">CPU - lazy load</span>
233
  </div>
234
  </section>
235
  """
@@ -238,8 +322,7 @@ def render_header():
238
  def render_step(number, title):
239
  return f"""
240
  <div class="step-title">
241
- <span class="step-index">{number}</span>
242
- <h2>{title}</h2>
243
  </div>
244
  """
245
 
@@ -248,8 +331,6 @@ def render_model_card(model_key, selected_key):
248
  model_def = model_by_key(model_key)
249
  selected = model_key == selected_key
250
  state_class = " selected" if selected else ""
251
- action = "Selected" if selected else "Select"
252
- ring = "selected-mark" if selected else "empty-mark"
253
  return f"""
254
  <article class="model-card{state_class}" role="button" tabindex="0">
255
  <div class="model-tag">{model_def["tag"]}</div>
@@ -260,8 +341,7 @@ def render_model_card(model_key, selected_key):
260
  <small>exact match</small>
261
  </div>
262
  <div class="model-card-footer">
263
- <span>{action}</span>
264
- <span class="{ring}"></span>
265
  </div>
266
  </article>
267
  """
@@ -292,21 +372,51 @@ def render_loading_overlay(model_key=None, visible=False):
292
  return (
293
  '<div class="loading-overlay">'
294
  '<div class="loading-card">'
295
- f'<h2>Loading {model_def["short_label"]} model...</h2>'
296
  '<div class="loading-line"><span></span></div>'
297
- '<p>First load: ~3-5 min - model stays cached for this session</p>'
298
  "</div>"
299
  "</div>"
300
  )
301
 
302
 
303
  def model_metadata(model_key=None):
304
- return model_by_key(model_key or DEFAULT_MODEL_KEY)["metadata"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
 
306
 
307
  def query_control_updates(can_generate):
308
- input_updates = [gr.update(interactive=True) for _ in range(7)]
309
- return [*input_updates, gr.update(interactive=can_generate)]
310
 
311
 
312
  def render_message(message="", kind="error"):
@@ -339,11 +449,11 @@ def load_selected_model(selected_key):
339
  render_status(selected_key, None, state="loading"),
340
  render_loading_overlay(selected_key, visible=True),
341
  model_metadata(selected_key),
342
- gr.update(interactive=False),
343
  *query_control_updates(False),
344
  "",
345
  EMPTY_VALIDATOR,
346
- gr.update(interactive=False),
347
  render_message(),
348
  gr.update(visible=False),
349
  )
@@ -361,7 +471,7 @@ def load_selected_model(selected_key):
361
  *query_control_updates(False),
362
  "",
363
  EMPTY_VALIDATOR,
364
- gr.update(interactive=False),
365
  render_message(error),
366
  gr.update(visible=False),
367
  )
@@ -377,18 +487,28 @@ def load_selected_model(selected_key):
377
  *query_control_updates(True),
378
  "",
379
  EMPTY_VALIDATOR,
380
- gr.update(interactive=False),
381
  render_message(f"Loaded {model_def['model_id']} in {elapsed}s.", kind="ok"),
382
  gr.update(visible=False),
383
  )
384
 
385
 
386
  def set_preset(name):
387
- return PRESETS[name]
 
 
 
 
 
 
 
 
 
 
388
 
389
 
390
- def comparison_updates(saved_state, current_sql, loaded_key, schema, question):
391
- if not saved_state:
392
  return gr.update(visible=False), "", "", "", ""
393
 
394
  loaded_def = model_by_key(loaded_key) if loaded_key else model_by_key(DEFAULT_MODEL_KEY)
@@ -402,39 +522,55 @@ def comparison_updates(saved_state, current_sql, loaded_key, schema, question):
402
 
403
 
404
  def render_compare_label(prefix, model_label, metric):
405
- metric_html = f'<strong>{metric} match</strong>' if metric else ""
406
- return f'<div class="compare-head"><span>{prefix} - {model_label}</span>{metric_html}</div>'
 
 
 
407
 
408
 
409
- def generate_sql(schema, question, loaded_key, saved_state):
410
- schema = (schema or "").strip()
411
- question = (question or "").strip()
 
412
  if not loaded_key or _model is None or _tokenizer is None:
413
- compare = comparison_updates(saved_state, "", loaded_key, schema, question)
414
  return (
 
 
 
 
415
  "",
416
  EMPTY_VALIDATOR,
417
- gr.update(interactive=False),
418
  render_message("Load a model before generating SQL."),
419
  *compare,
420
  )
421
- if not schema or not question:
422
- compare = comparison_updates(saved_state, "", loaded_key, schema, question)
423
  return (
 
 
 
 
424
  "",
425
  EMPTY_VALIDATOR,
426
- gr.update(interactive=False),
427
- render_message("Schema and question are required."),
428
  *compare,
429
  )
430
 
431
  model_def = model_by_key(loaded_key)
432
  if _current_model_id != model_def["model_id"]:
433
- compare = comparison_updates(saved_state, "", loaded_key, schema, question)
434
  return (
 
 
 
 
435
  "",
436
  EMPTY_VALIDATOR,
437
- gr.update(interactive=False),
438
  render_message("Loaded model state is inconsistent. Reload the selected model."),
439
  *compare,
440
  )
@@ -443,40 +579,58 @@ def generate_sql(schema, question, loaded_key, saved_state):
443
  try:
444
  torch, _, _, _ = import_model_runtime()
445
  with _model_lock:
446
- prompt = PROMPT_TEMPLATE.format(schema=schema, question=question)
447
  inputs = _tokenizer(prompt, return_tensors="pt")
448
  input_length = inputs["input_ids"].shape[-1]
449
  with torch.no_grad():
450
  output_ids = _model.generate(
451
  **inputs,
452
- max_new_tokens=150,
453
  do_sample=False,
454
  use_cache=False,
455
  )
456
  generated_ids = output_ids[0][input_length:]
457
- sql_text = clean_generation(_tokenizer.decode(generated_ids, skip_special_tokens=False))
458
  except Exception as exc:
459
- compare = comparison_updates(saved_state, "", loaded_key, schema, question)
460
  return (
 
 
 
 
461
  "",
462
  EMPTY_VALIDATOR,
463
- gr.update(interactive=False),
464
  render_message(f"Generation failed: {type(exc).__name__}: {exc}"),
465
  *compare,
466
  )
467
 
468
  elapsed = int(time.time() - started)
469
- compare = comparison_updates(saved_state, sql_text, loaded_key, schema, question)
 
 
 
 
 
 
 
 
 
 
470
  return (
 
 
 
 
471
  str(sql_text),
472
- validate_sql(sql_text),
473
- gr.update(interactive=bool(sql_text.strip())),
474
- render_message(f"Generated with {model_def['model_id']} in {elapsed}s.", kind="ok"),
475
  *compare,
476
  )
477
 
478
 
479
- def save_for_comparison(sql_text, loaded_key, schema, question):
480
  sql_text = (sql_text or "").strip()
481
  if not sql_text or not loaded_key:
482
  return (
@@ -486,7 +640,7 @@ def save_for_comparison(sql_text, loaded_key, schema, question):
486
  "",
487
  "",
488
  "",
489
- gr.update(interactive=False),
490
  render_message("Generate SQL before saving a comparison."),
491
  )
492
 
@@ -495,8 +649,8 @@ def save_for_comparison(sql_text, loaded_key, schema, question):
495
  "sql": sql_text,
496
  "model_label": model_def["short_label"],
497
  "match": model_def["exact_match"],
498
- "schema": schema or "",
499
- "question": question or "",
500
  }
501
  return (
502
  saved,
@@ -511,201 +665,188 @@ def save_for_comparison(sql_text, loaded_key, schema, question):
511
 
512
 
513
  CSS = """
514
- @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@500;600;700;800&family=Space+Mono:wght@400;700&display=swap');
515
 
516
  :root {
517
- --bg: #000000;
518
- --panel: #242422;
519
- --panel-2: #2e2f2d;
520
- --panel-3: #111110;
521
- --line: #686866;
522
- --line-bright: #f3f0ea;
523
- --text: #f4f1ea;
524
- --muted: #b8b4ad;
525
- --mint: #19b991;
526
- --mint-soft: #dff8ef;
527
- --cream: #fff2dd;
528
- --cream-text: #8b4f12;
529
- --code-blue: #2878a9;
 
 
 
530
  }
531
 
532
  * {
533
  box-sizing: border-box;
534
  }
535
 
536
- .gradio-container {
537
- background: var(--bg) !important;
538
- color: var(--text) !important;
539
- font-family: Outfit, ui-sans-serif, system-ui, sans-serif !important;
540
- }
541
-
542
  .gradio-container .main,
543
  .gradio-container .wrap,
544
  .gradio-container .contain {
545
- background: var(--bg) !important;
 
 
546
  }
547
 
548
  .app-shell {
549
- max-width: 1210px;
550
- margin: 28px auto 52px;
551
- padding: 0 22px;
552
  }
553
 
554
  .top-panel {
555
- align-items: flex-start;
556
- background: var(--panel);
557
- border: 2px solid #454542;
558
- border-radius: 12px;
559
  display: grid;
560
- gap: 24px;
561
- grid-template-columns: 1fr auto;
562
- min-height: 118px;
563
- padding: 18px 25px;
564
  }
565
 
566
  .top-panel h1 {
567
- color: var(--text);
568
- font-size: 28px;
569
- font-weight: 600;
570
  letter-spacing: 0;
571
- line-height: 1.1;
572
- margin: 0 0 10px;
573
  }
574
 
575
  .top-panel p {
576
- color: var(--muted);
577
- font-size: 19px;
578
- font-weight: 800;
579
  letter-spacing: 0;
 
580
  margin: 0;
581
  }
582
 
583
  .top-badges {
 
584
  display: flex;
585
  flex-wrap: wrap;
586
- gap: 10px;
587
  justify-content: flex-end;
588
- max-width: 560px;
589
  }
590
 
591
- .badge {
592
- border-radius: 7px;
 
 
593
  display: inline-flex;
594
- font-size: 18px;
595
- font-weight: 700;
 
596
  line-height: 1;
597
- padding: 10px 14px;
598
  }
599
 
600
- .badge-green {
601
- background: #dcf5ee;
602
- color: #147963;
 
603
  }
604
 
605
- .badge-cream {
606
- background: var(--cream);
607
- color: var(--cream-text);
 
608
  }
609
 
610
- .badge-light {
611
- background: #f2f1ed;
612
- color: #6f6b65;
 
 
613
  }
614
 
615
  .step-title {
616
- align-items: center;
617
- display: flex;
618
- gap: 10px;
619
- margin: 42px 0 14px;
 
 
 
620
  }
621
 
622
- .step-index {
623
- align-items: center;
624
- background: var(--mint-soft);
625
- border: 2px solid #8ce8d0;
626
- border-radius: 50%;
627
- color: #11866e;
628
  display: inline-flex;
629
- font-family: Space Mono, monospace;
630
- font-size: 17px;
631
- font-weight: 700;
632
- height: 32px;
633
- justify-content: center;
634
- width: 32px;
635
  }
636
 
637
- .step-title h2 {
638
- color: #cfcac1;
639
- font-family: Space Mono, monospace;
640
- font-size: 22px;
641
- font-weight: 700;
642
- letter-spacing: -0.03em;
643
- line-height: 1;
644
- margin: 0;
645
- text-transform: uppercase;
646
- }
647
-
648
- .model-grid {
649
  display: grid;
650
- gap: 18px;
651
  grid-template-columns: repeat(2, minmax(0, 1fr));
652
  }
653
 
654
- .model-grid > div {
 
 
655
  min-width: 0;
656
  }
657
 
658
  .model-card {
659
- background: var(--panel-2);
660
- border: 2px solid #454542;
661
- border-radius: 10px;
662
  cursor: pointer;
663
- height: 100%;
664
- min-height: 266px;
665
- padding: 27px 25px 24px;
666
- transition: border-color 180ms ease, transform 180ms ease;
667
  }
668
 
669
- .model-card.selected {
670
- border-color: var(--mint);
671
  }
672
 
673
- .model-card:hover {
674
- transform: translateY(-2px);
675
  }
676
 
677
  .model-tag {
678
- background: #fff3df;
679
- border-radius: 5px;
680
- color: #8b5a18;
681
- display: inline-flex;
682
- font-size: 17px;
683
- font-weight: 700;
684
- line-height: 1;
685
- margin-bottom: 16px;
686
- padding: 7px 10px;
687
  }
688
 
689
  .model-card.selected .model-tag {
690
- background: #e3f7f0;
691
- color: #16816a;
692
  }
693
 
694
  .model-card h3 {
695
- color: #f6f3ec;
696
- font-size: 24px;
697
- font-weight: 700;
698
- letter-spacing: -0.02em;
 
699
  margin: 0 0 8px;
700
  }
701
 
702
  .model-card code {
703
- color: #aaa7a0;
704
  display: block;
705
- font-family: Space Mono, monospace;
706
- font-size: 17px;
707
- font-weight: 700;
708
- margin-bottom: 17px;
 
709
  overflow: hidden;
710
  text-overflow: ellipsis;
711
  white-space: nowrap;
@@ -715,185 +856,257 @@ CSS = """
715
  align-items: baseline;
716
  display: flex;
717
  gap: 8px;
718
- margin-bottom: 24px;
719
  }
720
 
721
  .model-score span {
722
- color: #f7f3eb;
723
- font-size: 40px;
724
  font-weight: 500;
725
- letter-spacing: -0.04em;
726
  line-height: 1;
727
  }
728
 
729
  .model-card.selected .model-score span {
730
- color: #06896d;
731
  }
732
 
733
- .model-score small {
734
- color: #cac5ba;
735
- font-size: 18px;
736
- font-weight: 700;
 
737
  }
738
 
739
  .model-card-footer {
740
- align-items: center;
741
- color: #aaa59c;
742
  display: flex;
743
- font-size: 18px;
744
- font-weight: 700;
745
- justify-content: space-between;
746
  }
747
 
748
- .model-card.selected .model-card-footer {
749
- color: #06896d;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
750
  }
751
 
752
- .empty-mark,
753
- .selected-mark {
754
  border-radius: 50%;
755
  display: inline-flex;
756
- height: 35px;
757
- width: 35px;
758
  }
759
 
760
- .empty-mark {
761
- border: 2px solid #686866;
 
762
  }
763
 
764
- .selected-mark {
765
- align-items: center;
766
- background: #d9fbf1;
767
- border: 2px solid #86e6cc;
768
- justify-content: center;
769
  }
770
 
771
- .selected-mark::after {
772
- border: 2px solid #178870;
773
- border-radius: 2px;
774
- content: "";
775
- height: 9px;
776
- width: 9px;
777
  }
778
 
779
- .model-actions {
780
- margin-top: 28px;
 
 
 
 
 
781
  }
782
 
783
- .gradio-container button {
784
- border-radius: 6px !important;
785
- font-family: Space Mono, monospace !important;
786
- font-weight: 700 !important;
787
- transition: background 180ms ease, border-color 180ms ease, color 180ms ease, transform 180ms ease !important;
 
788
  }
789
 
790
- .gradio-container button.primary,
791
- .gradio-container .primary > button,
792
- #load-button button,
793
- #generate-button button,
794
- #save-button button {
795
- background: #000 !important;
796
- border: 2px solid var(--line-bright) !important;
797
- color: #f7f3ec !important;
798
  }
799
 
800
- #load-button,
801
- #generate-button,
802
- #save-button {
803
- width: 100% !important;
 
804
  }
805
 
806
- #load-button button,
807
- #generate-button button,
808
- #save-button button {
809
- min-height: 62px !important;
810
- width: 100% !important;
 
811
  }
812
 
813
- #generate-button button {
814
- font-size: 24px !important;
815
  }
816
 
817
- #load-button button:hover,
818
- #generate-button button:hover,
819
- #save-button button:hover {
820
- background: #f4f0e8 !important;
821
- color: #050505 !important;
822
- transform: translateY(-1px);
823
  }
824
 
825
- #save-button button {
826
- font-size: 22px !important;
 
 
827
  }
828
 
829
- .status-pill {
830
  align-items: center;
831
- border-radius: 18px;
832
- display: inline-flex;
833
- font-size: 21px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
834
  font-weight: 500;
835
- gap: 12px;
836
- margin: 44px 0 0;
837
- padding: 9px 18px;
838
  }
839
 
840
- .status-pill span {
841
- border-radius: 50%;
842
- display: inline-flex;
843
- height: 11px;
844
- width: 11px;
 
 
845
  }
846
 
847
- .status-ready,
848
- .status-loading {
849
- background: #dff8ef;
850
- border: 2px solid #76e4c9;
851
- color: #137a65;
 
 
 
 
852
  }
853
 
854
- .status-ready span,
855
- .status-loading span {
856
- background: var(--mint);
857
  }
858
 
859
- .status-empty {
860
- background: #1f1f1d;
861
- border: 2px solid #53534f;
862
- color: #c4bfb6;
863
  }
864
 
865
- .status-empty span {
866
- background: #77736c;
867
  }
868
 
869
- .section-divider {
870
- border-top: 2px solid #e8e3db;
871
- margin: 42px 0 0;
 
 
 
 
872
  }
873
 
874
- .preset-label {
875
- color: #f2eee6;
876
- font-family: Space Mono, monospace;
877
- font-size: 20px;
878
- font-weight: 700;
879
- margin: 0 0 10px;
880
  }
881
 
882
  .preset-row-gradio {
883
- gap: 8px !important;
 
884
  }
885
 
886
  .preset-row-gradio button {
887
- background: #242422 !important;
888
- border: 2px solid #62625f !important;
889
- color: #d4d0c8 !important;
890
- font-size: 16px !important;
891
- min-height: 38px !important;
 
 
892
  }
893
 
894
  .preset-row-gradio button:hover {
895
- border-color: #f3f0ea !important;
896
- color: #f3f0ea !important;
897
  }
898
 
899
  .gradio-container .block,
@@ -905,153 +1118,140 @@ CSS = """
905
 
906
  .gradio-container label,
907
  .gradio-container .label-wrap span {
908
- color: #f4f0e8 !important;
909
- font-family: Space Mono, monospace !important;
910
- font-size: 20px !important;
911
- font-weight: 700 !important;
912
  }
913
 
914
  textarea,
915
  input,
916
  .cm-editor {
917
- background: #242422 !important;
918
- border: 2px solid #646461 !important;
919
- border-radius: 10px !important;
920
- color: #f2eee6 !important;
921
- font-family: Space Mono, monospace !important;
922
- font-size: 18px !important;
923
- font-weight: 700 !important;
 
 
 
 
 
 
924
  }
925
 
926
  textarea {
927
- min-height: 108px !important;
 
 
 
 
 
928
  }
929
 
930
  .output-shell {
931
- border: 2px solid var(--line-bright);
932
- border-radius: 10px;
933
- margin-top: 14px;
 
934
  overflow: hidden;
935
  }
936
 
937
  .output-head {
938
  align-items: center;
939
- background: #121210;
940
- border-bottom: 1px solid #3f3f3c;
941
  display: flex;
942
  justify-content: space-between;
943
- min-height: 57px;
944
- padding: 0 21px;
945
  }
946
 
947
  .output-head span:first-child {
948
- color: #c8c3bb;
949
- font-family: Space Mono, monospace;
950
- font-size: 17px;
951
- font-weight: 700;
952
- }
953
-
954
- .validator-badge {
955
- border-radius: 5px;
956
- display: inline-flex;
957
- font-size: 17px;
958
- font-weight: 700;
959
- line-height: 1;
960
- padding: 8px 12px;
961
- }
962
-
963
- .validator-ok {
964
- background: #edf9df;
965
- color: #54822d;
966
- }
967
-
968
- .validator-warn {
969
- background: #fff3df;
970
- color: #8b5a18;
971
- }
972
-
973
- .validator-empty {
974
- background: #272724;
975
- color: #b9b3aa;
976
  }
977
 
978
  .validator-detail {
979
- color: var(--muted);
980
- font-size: 12px;
981
  margin-left: 8px;
982
  }
983
 
984
  .output-shell .cm-editor,
985
  .output-shell pre,
986
- .output-shell code {
 
 
 
987
  border: 0 !important;
 
 
988
  }
989
 
990
  .message-box {
991
- color: #cfc7bc;
992
- font-size: 15px;
993
- font-weight: 700;
994
- min-height: 28px;
995
  padding-top: 8px;
996
  }
997
 
998
  .message-error {
999
- color: #f4b4a8;
1000
  }
1001
 
1002
  .message-ok {
1003
- color: #9ee6d1;
1004
- }
1005
-
1006
- .metadata-wrap {
1007
- margin-top: 36px;
1008
  }
1009
 
1010
  .comparison-panel {
1011
- margin-top: 40px;
1012
- }
1013
-
1014
- .compare-grid {
1015
- display: grid;
1016
- gap: 18px;
1017
- grid-template-columns: repeat(2, minmax(0, 1fr));
1018
  }
1019
 
1020
  .compare-card {
1021
- border: 2px solid var(--line-bright);
1022
- border-radius: 10px;
 
1023
  overflow: hidden;
1024
  }
1025
 
1026
  .compare-card.current {
1027
- border-color: #6de4c8;
1028
  }
1029
 
1030
  .compare-head {
1031
  align-items: center;
1032
- background: #131311;
1033
- color: #c8c3bb;
1034
  display: flex;
1035
- font-size: 18px;
1036
- font-weight: 700;
1037
- gap: 28px;
1038
- min-height: 62px;
1039
- padding: 0 22px;
 
1040
  }
1041
 
1042
  .compare-card.current .compare-head,
1043
  .current-compare-head .compare-head {
1044
- background: #dff8ef;
1045
- color: #147963;
1046
  }
1047
 
1048
  .compare-head strong {
1049
  color: inherit;
 
1050
  }
1051
 
1052
  .loading-overlay {
1053
  align-items: center;
1054
- background: rgba(0, 0, 0, 0.72);
1055
  bottom: 0;
1056
  display: flex;
1057
  justify-content: center;
@@ -1067,56 +1267,63 @@ textarea {
1067
  }
1068
 
1069
  .loading-card {
1070
- background: #242422;
1071
- border: 2px solid #75e3c9;
1072
- border-radius: 10px;
1073
- max-width: 620px;
1074
- padding: 28px;
1075
- width: min(92vw, 620px);
1076
  }
1077
 
1078
- .loading-card h2 {
1079
- color: #f4f0e8;
1080
- font-size: 25px;
1081
- margin: 0 0 18px;
 
 
1082
  }
1083
 
1084
  .loading-line {
1085
- background: #111110;
1086
  border-radius: 999px;
1087
- height: 16px;
1088
  overflow: hidden;
1089
  }
1090
 
1091
  .loading-line span {
1092
  animation: loadingPulse 1.3s infinite ease-in-out;
1093
- background: #19b991;
1094
  border-radius: inherit;
1095
  display: block;
1096
  height: 100%;
1097
- width: 52%;
1098
  }
1099
 
1100
  .loading-card p {
1101
- color: #d2ccc2;
1102
- font-size: 17px;
1103
- font-weight: 700;
1104
- margin: 14px 0 0;
 
1105
  }
1106
 
1107
  @keyframes loadingPulse {
1108
- 0% { transform: translateX(-35%); }
1109
- 50% { transform: translateX(40%); }
1110
- 100% { transform: translateX(115%); }
1111
  }
1112
 
1113
- @media (max-width: 820px) {
1114
  .top-panel,
1115
  .model-grid,
1116
  .compare-grid {
1117
  grid-template-columns: 1fr;
1118
  }
1119
 
 
 
 
 
1120
  .top-badges {
1121
  justify-content: flex-start;
1122
  }
@@ -1127,59 +1334,66 @@ textarea {
1127
  }
1128
  """
1129
 
1130
-
1131
- blocks_kwargs = {"title": "Phi-3 Mini SQL Generator"}
1132
-
1133
- with gr.Blocks(**blocks_kwargs) as demo:
1134
  selected_model_key = gr.State(value=DEFAULT_MODEL_KEY)
1135
  loaded_key_state = gr.State(value=None)
1136
  saved_output = gr.State(value=None)
 
 
1137
 
1138
  with gr.Column(elem_classes=["app-shell"]):
1139
  loading_overlay = gr.HTML(render_loading_overlay(visible=False))
1140
  gr.HTML(render_header())
1141
 
1142
- gr.HTML(render_step("1", "Select Model"))
1143
  with gr.Row(elem_classes=["model-grid"]):
1144
  base_model_card = gr.HTML(render_model_card(BASE_MODEL_KEY, DEFAULT_MODEL_KEY))
1145
  fine_tuned_model_card = gr.HTML(render_model_card(FINE_TUNED_MODEL_KEY, DEFAULT_MODEL_KEY))
1146
- with gr.Row(elem_classes=["model-actions"]):
1147
- base_select = gr.Button("Select base", size="md")
1148
- fine_tuned_select = gr.Button("Select fine-tuned", size="md")
1149
  load_button = gr.Button("Load selected model", variant="primary", elem_id="load-button")
1150
  model_status = gr.HTML(render_status(DEFAULT_MODEL_KEY, None))
1151
-
1152
- gr.HTML(render_step("2", "Query Input"))
1153
- gr.HTML('<div class="preset-label">SQL table schema</div>')
1154
- with gr.Row(elem_classes=["preset-row-gradio"]):
1155
- employees_preset = gr.Button("employees", size="sm")
1156
- orders_preset = gr.Button("orders", size="sm")
1157
- students_preset = gr.Button("students", size="sm")
1158
- products_preset = gr.Button("products", size="sm")
1159
- sales_preset = gr.Button("sales", size="sm")
1160
- schema_input = gr.Textbox(
1161
- label="",
1162
- value=PRESETS["employees"],
1163
- lines=4,
1164
- max_lines=8,
1165
- interactive=True,
1166
- show_label=False,
1167
- )
1168
- question_input = gr.Textbox(
1169
- label="Question",
1170
- value="What is the average salary per department?",
1171
- lines=2,
1172
- interactive=True,
1173
- )
1174
- generate_button = gr.Button(
1175
- "Generate SQL",
1176
- variant="primary",
1177
- interactive=False,
1178
- elem_id="generate-button",
1179
- )
 
 
 
 
 
 
 
 
 
 
 
1180
 
1181
  gr.HTML('<div class="section-divider"></div>')
1182
- gr.HTML(render_step("3", "Output"))
1183
  with gr.Column(elem_classes=["output-shell"]):
1184
  with gr.Row(elem_classes=["output-head"]):
1185
  gr.HTML("<span>SQL</span>")
@@ -1194,18 +1408,11 @@ with gr.Blocks(**blocks_kwargs) as demo:
1194
  save_button = gr.Button(
1195
  "Save for comparison",
1196
  interactive=False,
 
1197
  elem_id="save-button",
1198
  )
1199
  error_output = gr.HTML(render_message())
1200
 
1201
- with gr.Column(elem_classes=["metadata-wrap"]):
1202
- model_info = gr.Textbox(
1203
- label="Model metadata",
1204
- value=model_metadata(DEFAULT_MODEL_KEY),
1205
- lines=5,
1206
- interactive=False,
1207
- )
1208
-
1209
  with gr.Column(visible=False, elem_classes=["comparison-panel"]) as comparison_panel:
1210
  with gr.Row(elem_classes=["compare-grid"]):
1211
  with gr.Column(elem_classes=["compare-card"]):
@@ -1226,22 +1433,12 @@ with gr.Blocks(**blocks_kwargs) as demo:
1226
  students_preset,
1227
  products_preset,
1228
  sales_preset,
1229
- schema_input,
1230
- question_input,
1231
- generate_button,
1232
  save_button,
1233
  error_output,
1234
  ]
1235
- base_select.click(
1236
- select_model,
1237
- inputs=[gr.State(BASE_MODEL_KEY), loaded_key_state],
1238
- outputs=model_state_outputs,
1239
- )
1240
- fine_tuned_select.click(
1241
- select_model,
1242
- inputs=[gr.State(FINE_TUNED_MODEL_KEY), loaded_key_state],
1243
- outputs=model_state_outputs,
1244
- )
1245
  base_model_card.click(
1246
  select_model,
1247
  inputs=[gr.State(BASE_MODEL_KEY), loaded_key_state],
@@ -1267,41 +1464,54 @@ with gr.Blocks(**blocks_kwargs) as demo:
1267
  students_preset,
1268
  products_preset,
1269
  sales_preset,
1270
- schema_input,
1271
- question_input,
1272
- generate_button,
1273
  sql_output,
1274
  validator_output,
1275
  save_button,
1276
  error_output,
1277
  comparison_panel,
1278
  ],
 
1279
  )
1280
 
1281
- employees_preset.click(set_preset, inputs=gr.State("employees"), outputs=schema_input)
1282
- orders_preset.click(set_preset, inputs=gr.State("orders"), outputs=schema_input)
1283
- students_preset.click(set_preset, inputs=gr.State("students"), outputs=schema_input)
1284
- products_preset.click(set_preset, inputs=gr.State("products"), outputs=schema_input)
1285
- sales_preset.click(set_preset, inputs=gr.State("sales"), outputs=schema_input)
1286
-
1287
- generate_button.click(
1288
- generate_sql,
1289
- inputs=[schema_input, question_input, loaded_key_state, saved_output],
1290
- outputs=[
1291
- sql_output,
1292
- validator_output,
1293
- save_button,
1294
- error_output,
1295
- comparison_panel,
1296
- saved_model_label,
1297
- saved_sql,
1298
- current_model_label,
1299
- current_sql,
1300
- ],
 
 
 
 
 
 
 
 
 
 
 
 
1301
  )
1302
  save_button.click(
1303
  save_for_comparison,
1304
- inputs=[sql_output, loaded_key_state, schema_input, question_input],
1305
  outputs=[
1306
  saved_output,
1307
  comparison_panel,
@@ -1321,7 +1531,4 @@ demo.queue(**queue_kwargs)
1321
 
1322
 
1323
  if __name__ == "__main__":
1324
- launch_kwargs = {}
1325
- if "css" in inspect.signature(demo.launch).parameters:
1326
- launch_kwargs["css"] = CSS
1327
- demo.launch(**launch_kwargs)
 
1
  import gc
2
  import html
3
  import inspect
4
+ import re
5
  import threading
6
  import time
7
 
 
71
  "<|assistant|>"
72
  )
73
 
74
+ GENERAL_PROMPT_TEMPLATE = (
75
+ "<|user|>\n"
76
+ "You are Phi-3 Mini in a SQL generator demo. Reply naturally and briefly. "
77
+ "If the user asks for SQL, provide only the SQL query.\n\n"
78
+ "User: {message}<|end|>\n"
79
+ "<|assistant|>"
80
+ )
81
+
82
  EMPTY_VALIDATOR = '<span class="validator-badge validator-empty">No SQL yet</span>'
83
+ CHAT_VALIDATOR = '<span class="validator-badge validator-empty">Chat response</span>'
84
+ EMPTY_CHAT_OUTPUT = ""
85
+ LOAD_SCROLL_JS = """
86
+ (selectedKey) => {
87
+ setTimeout(() => {
88
+ document.querySelector("#query-section")?.scrollIntoView({
89
+ behavior: "smooth",
90
+ block: "start"
91
+ });
92
+ }, 50);
93
+ return selectedKey;
94
+ }
95
+ """
96
 
97
  _current_model_id = None
98
  _model = None
 
212
  return cleaned
213
 
214
 
215
+ def is_sql_like(text):
216
+ text = (text or "").strip()
217
+ if not text:
218
+ return False
219
+ first_word = re.match(r"^\s*([A-Za-z]+)", text)
220
+ if not first_word:
221
+ return False
222
+ return first_word.group(1).upper() in {
223
+ "SELECT",
224
+ "WITH",
225
+ "INSERT",
226
+ "UPDATE",
227
+ "DELETE",
228
+ "CREATE",
229
+ "ALTER",
230
+ "DROP",
231
+ }
232
+
233
+
234
+ def is_sql_intent(message, schema):
235
+ message = (message or "").strip().lower()
236
+ schema = (schema or "").strip()
237
+ if schema:
238
+ return True
239
+ if not message:
240
+ return False
241
+ sql_terms = {
242
+ "sql",
243
+ "query",
244
+ "select",
245
+ "table",
246
+ "schema",
247
+ "database",
248
+ "join",
249
+ "group by",
250
+ "order by",
251
+ "where",
252
+ "average",
253
+ "count",
254
+ "sum",
255
+ "rows",
256
+ "columns",
257
+ }
258
+ return any(term in message for term in sql_terms)
259
+
260
+
261
+ def build_generation_prompt(schema, message):
262
+ schema = (schema or "").strip()
263
+ message = (message or "").strip()
264
+ if is_sql_intent(message, schema):
265
+ table_schema = schema or "No explicit schema provided. Infer the table and columns only if the request includes them."
266
+ return PROMPT_TEMPLATE.format(schema=table_schema, question=message)
267
+ return GENERAL_PROMPT_TEMPLATE.format(message=message)
268
+
269
+
270
+ def format_generation_result(text):
271
+ cleaned = clean_generation(text)
272
+ if is_sql_like(cleaned):
273
+ return str(cleaned), EMPTY_CHAT_OUTPUT, validate_sql(cleaned)
274
+ return "", str(cleaned), CHAT_VALIDATOR
275
+
276
+
277
  def validate_sql(sql_text):
278
  sql_text = (sql_text or "").strip()
279
  if not sql_text:
 
308
  <section class="top-panel">
309
  <div>
310
  <h1>Phi-3 Mini SQL Generator</h1>
311
+ <p>QLoRA fine-tuned - b-mc2/sql-create-context</p>
312
  </div>
313
  <div class="top-badges">
314
  <span class="badge badge-green">73.5% exact match</span>
315
  <span class="badge badge-cream">+71.5pp vs base</span>
316
+ <span class="badge badge-light">CPU lazy load</span>
317
  </div>
318
  </section>
319
  """
 
322
  def render_step(number, title):
323
  return f"""
324
  <div class="step-title">
325
+ <span>{number} &mdash; {title}</span>
 
326
  </div>
327
  """
328
 
 
331
  model_def = model_by_key(model_key)
332
  selected = model_key == selected_key
333
  state_class = " selected" if selected else ""
 
 
334
  return f"""
335
  <article class="model-card{state_class}" role="button" tabindex="0">
336
  <div class="model-tag">{model_def["tag"]}</div>
 
341
  <small>exact match</small>
342
  </div>
343
  <div class="model-card-footer">
344
+ <span>{model_def["label"]}</span>
 
345
  </div>
346
  </article>
347
  """
 
372
  return (
373
  '<div class="loading-overlay">'
374
  '<div class="loading-card">'
375
+ f'<div class="loading-title">Loading {model_def["short_label"]} model</div>'
376
  '<div class="loading-line"><span></span></div>'
377
+ '<p>First load: ~3-5 min &mdash; cached for session</p>'
378
  "</div>"
379
  "</div>"
380
  )
381
 
382
 
383
  def model_metadata(model_key=None):
384
+ return """
385
+ <section class="stats-row">
386
+ <div class="stat-card"><strong>73.5%</strong><span>exact match</span></div>
387
+ <div class="stat-card"><strong>+71.5pp</strong><span>vs base</span></div>
388
+ <div class="stat-card"><strong>1,000</strong><span>examples</span></div>
389
+ <div class="stat-card"><strong>21 min</strong><span>T4 training</span></div>
390
+ </section>
391
+ """
392
+
393
+
394
+ def schema_name_by_value(schema):
395
+ schema = (schema or "").strip()
396
+ for name, value in PRESETS.items():
397
+ if value == schema:
398
+ return name
399
+ return "custom"
400
+
401
+
402
+ def render_schema_context(schema=""):
403
+ schema = (schema or "").strip()
404
+ if not schema:
405
+ return '<div class="schema-context empty"></div>'
406
+ label = schema_name_by_value(schema)
407
+ escaped_schema = html.escape(schema)
408
+ escaped_label = html.escape(label)
409
+ return (
410
+ '<div class="schema-context">'
411
+ f'<span>Context: {escaped_label}</span>'
412
+ f'<code>{escaped_schema}</code>'
413
+ "</div>"
414
+ )
415
 
416
 
417
  def query_control_updates(can_generate):
418
+ context_updates = [gr.update(interactive=True) for _ in range(6)]
419
+ return [*context_updates, gr.update(interactive=True), gr.update(interactive=can_generate)]
420
 
421
 
422
  def render_message(message="", kind="error"):
 
449
  render_status(selected_key, None, state="loading"),
450
  render_loading_overlay(selected_key, visible=True),
451
  model_metadata(selected_key),
452
+ gr.update(interactive=False, visible=False),
453
  *query_control_updates(False),
454
  "",
455
  EMPTY_VALIDATOR,
456
+ gr.update(interactive=False, visible=False),
457
  render_message(),
458
  gr.update(visible=False),
459
  )
 
471
  *query_control_updates(False),
472
  "",
473
  EMPTY_VALIDATOR,
474
+ gr.update(interactive=False, visible=False),
475
  render_message(error),
476
  gr.update(visible=False),
477
  )
 
487
  *query_control_updates(True),
488
  "",
489
  EMPTY_VALIDATOR,
490
+ gr.update(interactive=False, visible=False),
491
  render_message(f"Loaded {model_def['model_id']} in {elapsed}s.", kind="ok"),
492
  gr.update(visible=False),
493
  )
494
 
495
 
496
  def set_preset(name):
497
+ schema = PRESETS[name]
498
+ return schema, render_schema_context(schema), gr.update(visible=True)
499
+
500
+
501
+ def clear_schema_context():
502
+ return "", render_schema_context(""), gr.update(visible=False)
503
+
504
+
505
+ def trim_chat_history(chat_history, max_exchanges=10):
506
+ history = list(chat_history or [])
507
+ return history[-max_exchanges * 2 :]
508
 
509
 
510
+ def comparison_updates(saved_state, current_sql, loaded_key):
511
+ if not saved_state or not (current_sql or "").strip():
512
  return gr.update(visible=False), "", "", "", ""
513
 
514
  loaded_def = model_by_key(loaded_key) if loaded_key else model_by_key(DEFAULT_MODEL_KEY)
 
522
 
523
 
524
  def render_compare_label(prefix, model_label, metric):
525
+ metric_html = f"<strong>{html.escape(metric)} match</strong>" if metric else ""
526
+ return (
527
+ f'<div class="compare-head"><span>{html.escape(prefix)} - '
528
+ f"{html.escape(model_label)}</span>{metric_html}</div>"
529
+ )
530
 
531
 
532
+ def generate_response(message, chat_history, active_schema, loaded_key, saved_state):
533
+ message = (message or "").strip()
534
+ active_schema = (active_schema or "").strip()
535
+ chat_history = list(chat_history or [])
536
  if not loaded_key or _model is None or _tokenizer is None:
537
+ compare = comparison_updates(saved_state, "", loaded_key)
538
  return (
539
+ chat_history,
540
+ message,
541
+ active_schema,
542
+ "",
543
  "",
544
  EMPTY_VALIDATOR,
545
+ gr.update(interactive=False, visible=False),
546
  render_message("Load a model before generating SQL."),
547
  *compare,
548
  )
549
+ if not message:
550
+ compare = comparison_updates(saved_state, "", loaded_key)
551
  return (
552
+ chat_history,
553
+ "",
554
+ active_schema,
555
+ "",
556
  "",
557
  EMPTY_VALIDATOR,
558
+ gr.update(interactive=False, visible=False),
559
+ render_message("Type a message before sending."),
560
  *compare,
561
  )
562
 
563
  model_def = model_by_key(loaded_key)
564
  if _current_model_id != model_def["model_id"]:
565
+ compare = comparison_updates(saved_state, "", loaded_key)
566
  return (
567
+ chat_history,
568
+ message,
569
+ active_schema,
570
+ "",
571
  "",
572
  EMPTY_VALIDATOR,
573
+ gr.update(interactive=False, visible=False),
574
  render_message("Loaded model state is inconsistent. Reload the selected model."),
575
  *compare,
576
  )
 
579
  try:
580
  torch, _, _, _ = import_model_runtime()
581
  with _model_lock:
582
+ prompt = build_generation_prompt(active_schema, message)
583
  inputs = _tokenizer(prompt, return_tensors="pt")
584
  input_length = inputs["input_ids"].shape[-1]
585
  with torch.no_grad():
586
  output_ids = _model.generate(
587
  **inputs,
588
+ max_new_tokens=80,
589
  do_sample=False,
590
  use_cache=False,
591
  )
592
  generated_ids = output_ids[0][input_length:]
593
+ generated_text = _tokenizer.decode(generated_ids, skip_special_tokens=False)
594
  except Exception as exc:
595
+ compare = comparison_updates(saved_state, "", loaded_key)
596
  return (
597
+ chat_history,
598
+ message,
599
+ active_schema,
600
+ "",
601
  "",
602
  EMPTY_VALIDATOR,
603
+ gr.update(interactive=False, visible=False),
604
  render_message(f"Generation failed: {type(exc).__name__}: {exc}"),
605
  *compare,
606
  )
607
 
608
  elapsed = int(time.time() - started)
609
+ sql_text, chat_text, validator = format_generation_result(generated_text)
610
+ display_response = f"```sql\n{sql_text}\n```" if sql_text else chat_text
611
+ new_history = trim_chat_history(
612
+ [
613
+ *chat_history,
614
+ {"role": "user", "content": message},
615
+ {"role": "assistant", "content": display_response},
616
+ ]
617
+ )
618
+ compare = comparison_updates(saved_state, sql_text, loaded_key)
619
+ response_kind = "SQL" if sql_text.strip() else "chat response"
620
  return (
621
+ new_history,
622
+ "",
623
+ active_schema,
624
+ message,
625
  str(sql_text),
626
+ validator,
627
+ gr.update(interactive=bool(sql_text.strip()), visible=bool(sql_text.strip())),
628
+ render_message(f"Generated {response_kind} with {model_def['model_id']} in {elapsed}s.", kind="ok"),
629
  *compare,
630
  )
631
 
632
 
633
+ def save_for_comparison(sql_text, loaded_key, active_schema, last_message):
634
  sql_text = (sql_text or "").strip()
635
  if not sql_text or not loaded_key:
636
  return (
 
640
  "",
641
  "",
642
  "",
643
+ gr.update(interactive=False, visible=False),
644
  render_message("Generate SQL before saving a comparison."),
645
  )
646
 
 
649
  "sql": sql_text,
650
  "model_label": model_def["short_label"],
651
  "match": model_def["exact_match"],
652
+ "schema_context": active_schema or "",
653
+ "user_message": last_message or "",
654
  }
655
  return (
656
  saved,
 
665
 
666
 
667
  CSS = """
668
+ @import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;500;700&display=swap');
669
 
670
  :root {
671
+ --bg-base: #0c0c0b;
672
+ --bg-surface: #1a1a18;
673
+ --bg-raised: #242422;
674
+ --border: rgba(255, 255, 255, 0.1);
675
+ --border-hi: rgba(255, 255, 255, 0.22);
676
+ --text-hi: #f2ede4;
677
+ --text-mid: #9e9a93;
678
+ --text-lo: #5f5d58;
679
+ --text-primary: var(--text-hi);
680
+ --text-secondary: var(--text-mid);
681
+ --text-muted: var(--text-lo);
682
+ --teal: #1D9E75;
683
+ --teal-soft: #dff8ef;
684
+ --teal-text: #0F6E56;
685
+ --amber-soft: #FAEEDA;
686
+ --amber-text: #854F0B;
687
  }
688
 
689
  * {
690
  box-sizing: border-box;
691
  }
692
 
693
+ .gradio-container,
 
 
 
 
 
694
  .gradio-container .main,
695
  .gradio-container .wrap,
696
  .gradio-container .contain {
697
+ background: var(--bg-base) !important;
698
+ color: var(--text-primary) !important;
699
+ font-family: Space Mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace !important;
700
  }
701
 
702
  .app-shell {
703
+ max-width: 1120px;
704
+ margin: 22px auto 44px;
705
+ padding: 0 20px;
706
  }
707
 
708
  .top-panel {
709
+ align-items: center;
710
+ background: var(--bg-surface);
711
+ border: 0.5px solid var(--border);
712
+ border-radius: 6px;
713
  display: grid;
714
+ gap: 16px;
715
+ grid-template-columns: minmax(0, 1fr) auto;
716
+ padding: 14px 16px;
 
717
  }
718
 
719
  .top-panel h1 {
720
+ color: var(--text-primary);
721
+ font-size: 15px;
722
+ font-weight: 500;
723
  letter-spacing: 0;
724
+ line-height: 1.25;
725
+ margin: 0 0 4px;
726
  }
727
 
728
  .top-panel p {
729
+ color: var(--text-secondary);
730
+ font-size: 13px;
731
+ font-weight: 400;
732
  letter-spacing: 0;
733
+ line-height: 1.35;
734
  margin: 0;
735
  }
736
 
737
  .top-badges {
738
+ align-items: center;
739
  display: flex;
740
  flex-wrap: wrap;
741
+ gap: 8px;
742
  justify-content: flex-end;
 
743
  }
744
 
745
+ .badge,
746
+ .validator-badge,
747
+ .model-tag {
748
+ border-radius: 5px;
749
  display: inline-flex;
750
+ font-size: 11px;
751
+ font-weight: 500;
752
+ letter-spacing: 0;
753
  line-height: 1;
754
+ padding: 6px 8px;
755
  }
756
 
757
+ .badge-green,
758
+ .validator-ok {
759
+ background: var(--teal-soft);
760
+ color: var(--teal-text);
761
  }
762
 
763
+ .badge-cream,
764
+ .validator-warn {
765
+ background: var(--amber-soft);
766
+ color: var(--amber-text);
767
  }
768
 
769
+ .badge-light,
770
+ .validator-empty {
771
+ background: var(--bg-raised);
772
+ color: var(--text-secondary);
773
+ border: 0.5px solid var(--border);
774
  }
775
 
776
  .step-title {
777
+ color: var(--text-secondary);
778
+ font-size: 11px;
779
+ font-weight: 500;
780
+ letter-spacing: 0.08em;
781
+ line-height: 1;
782
+ margin: 32px 0 12px;
783
+ text-transform: uppercase;
784
  }
785
 
786
+ .step-title span {
 
 
 
 
 
787
  display: inline-flex;
 
 
 
 
 
 
788
  }
789
 
790
+ .model-grid,
791
+ .compare-grid,
792
+ .stats-row {
 
 
 
 
 
 
 
 
 
793
  display: grid;
794
+ gap: 12px;
795
  grid-template-columns: repeat(2, minmax(0, 1fr));
796
  }
797
 
798
+ .model-grid > div,
799
+ .compare-grid > div,
800
+ .stats-row > div {
801
  min-width: 0;
802
  }
803
 
804
  .model-card {
805
+ background: var(--bg-surface);
806
+ border: 0.5px solid var(--border);
807
+ border-radius: 6px;
808
  cursor: pointer;
809
+ min-height: 176px;
810
+ padding: 16px;
811
+ transition: border-color 160ms ease, background 160ms ease;
 
812
  }
813
 
814
+ .model-card:hover {
815
+ border-color: var(--border-hi);
816
  }
817
 
818
+ .model-card.selected {
819
+ border: 1.5px solid var(--teal);
820
  }
821
 
822
  .model-tag {
823
+ background: var(--amber-soft);
824
+ color: var(--amber-text);
825
+ margin-bottom: 18px;
 
 
 
 
 
 
826
  }
827
 
828
  .model-card.selected .model-tag {
829
+ background: var(--teal-soft);
830
+ color: var(--teal-text);
831
  }
832
 
833
  .model-card h3 {
834
+ color: var(--text-primary);
835
+ font-size: 15px;
836
+ font-weight: 500;
837
+ letter-spacing: 0;
838
+ line-height: 1.3;
839
  margin: 0 0 8px;
840
  }
841
 
842
  .model-card code {
843
+ color: var(--text-secondary);
844
  display: block;
845
+ font-family: inherit;
846
+ font-size: 12px;
847
+ font-weight: 400;
848
+ line-height: 1.35;
849
+ margin-bottom: 18px;
850
  overflow: hidden;
851
  text-overflow: ellipsis;
852
  white-space: nowrap;
 
856
  align-items: baseline;
857
  display: flex;
858
  gap: 8px;
859
+ margin-bottom: 16px;
860
  }
861
 
862
  .model-score span {
863
+ color: var(--text-primary);
864
+ font-size: 28px;
865
  font-weight: 500;
866
+ letter-spacing: 0;
867
  line-height: 1;
868
  }
869
 
870
  .model-card.selected .model-score span {
871
+ color: var(--teal);
872
  }
873
 
874
+ .model-score small,
875
+ .model-card-footer {
876
+ color: var(--text-secondary);
877
+ font-size: 13px;
878
+ font-weight: 400;
879
  }
880
 
881
  .model-card-footer {
 
 
882
  display: flex;
 
 
 
883
  }
884
 
885
+ #load-button,
886
+ #generate-button,
887
+ #save-button {
888
+ width: 100% !important;
889
+ }
890
+
891
+ .gradio-container button {
892
+ border-radius: 6px !important;
893
+ font-family: Space Mono, ui-monospace, monospace !important;
894
+ font-size: 11px !important;
895
+ font-weight: 500 !important;
896
+ letter-spacing: 0 !important;
897
+ transition: background 160ms ease, border-color 160ms ease, color 160ms ease, opacity 160ms ease !important;
898
+ }
899
+
900
+ #load-button button,
901
+ #generate-button button {
902
+ background: var(--bg-raised) !important;
903
+ border: 0.5px solid var(--border-hi) !important;
904
+ color: var(--text-primary) !important;
905
+ min-height: 40px !important;
906
+ width: 100% !important;
907
+ }
908
+
909
+ #load-button button:hover,
910
+ #generate-button button:hover {
911
+ background: var(--text-primary) !important;
912
+ color: var(--bg-base) !important;
913
+ }
914
+
915
+ #save-button button {
916
+ background: transparent !important;
917
+ border: 0.5px solid var(--border-hi) !important;
918
+ color: var(--text-primary) !important;
919
+ min-height: 38px !important;
920
+ width: 100% !important;
921
+ }
922
+
923
+ #save-button button:hover {
924
+ border-color: var(--text-primary) !important;
925
+ }
926
+
927
+ #save-button button:disabled,
928
+ #generate-button button:disabled {
929
+ opacity: 0.4 !important;
930
+ }
931
+
932
+ .status-pill {
933
+ align-items: center;
934
+ background: var(--bg-surface);
935
+ border: 0.5px solid var(--border);
936
+ border-radius: 6px;
937
+ color: var(--text-secondary);
938
+ display: inline-flex;
939
+ font-size: 13px;
940
+ font-weight: 400;
941
+ gap: 8px;
942
+ margin: 12px 0 0;
943
+ padding: 8px 10px;
944
  }
945
 
946
+ .status-pill span {
947
+ background: var(--text-muted);
948
  border-radius: 50%;
949
  display: inline-flex;
950
+ height: 7px;
951
+ width: 7px;
952
  }
953
 
954
+ .status-ready span,
955
+ .status-loading span {
956
+ background: var(--teal);
957
  }
958
 
959
+ .stats-row {
960
+ grid-template-columns: repeat(4, minmax(0, 1fr));
961
+ margin-top: 12px;
 
 
962
  }
963
 
964
+ .stat-card {
965
+ background: var(--bg-surface);
966
+ border: 0.5px solid var(--border);
967
+ border-radius: 6px;
968
+ padding: 12px;
 
969
  }
970
 
971
+ .stat-card strong {
972
+ color: var(--text-primary);
973
+ display: block;
974
+ font-size: 15px;
975
+ font-weight: 500;
976
+ line-height: 1.2;
977
+ margin-bottom: 4px;
978
  }
979
 
980
+ .stat-card span {
981
+ color: var(--text-secondary);
982
+ display: block;
983
+ font-size: 11px;
984
+ font-weight: 400;
985
+ line-height: 1.25;
986
  }
987
 
988
+ .query-section {
989
+ scroll-margin-top: 24px;
 
 
 
 
 
 
990
  }
991
 
992
+ .chat-history {
993
+ background: var(--bg-surface) !important;
994
+ border: 0.5px solid var(--border) !important;
995
+ border-radius: 6px !important;
996
+ margin-bottom: 14px;
997
  }
998
 
999
+ .chat-history .bubble-wrap,
1000
+ .chat-history .message,
1001
+ .chat-history .prose {
1002
+ font-family: Space Mono, ui-monospace, monospace !important;
1003
+ font-size: 13px !important;
1004
+ line-height: 1.45 !important;
1005
  }
1006
 
1007
+ .chat-history .message-row {
1008
+ padding: 6px 8px !important;
1009
  }
1010
 
1011
+ .chat-history pre,
1012
+ .chat-history code {
1013
+ font-family: Space Mono, ui-monospace, monospace !important;
1014
+ font-size: 12px !important;
 
 
1015
  }
1016
 
1017
+ .schema-context-row {
1018
+ align-items: center;
1019
+ gap: 8px !important;
1020
+ margin: 8px 0 12px;
1021
  }
1022
 
1023
+ .schema-context {
1024
  align-items: center;
1025
+ background: var(--bg-surface);
1026
+ border: 0.5px solid var(--border);
1027
+ border-radius: 6px;
1028
+ color: var(--text-secondary);
1029
+ display: flex;
1030
+ gap: 8px;
1031
+ min-height: 30px;
1032
+ padding: 7px 9px;
1033
+ }
1034
+
1035
+ .schema-context.empty {
1036
+ display: none;
1037
+ }
1038
+
1039
+ .schema-context span {
1040
+ color: var(--teal);
1041
+ font-size: 11px;
1042
  font-weight: 500;
 
 
 
1043
  }
1044
 
1045
+ .schema-context code {
1046
+ color: var(--text-secondary);
1047
+ font-family: inherit;
1048
+ font-size: 11px;
1049
+ overflow: hidden;
1050
+ text-overflow: ellipsis;
1051
+ white-space: nowrap;
1052
  }
1053
 
1054
+ .field-label,
1055
+ .preset-label {
1056
+ color: var(--text-secondary);
1057
+ font-size: 11px;
1058
+ font-weight: 500;
1059
+ letter-spacing: 0.08em;
1060
+ line-height: 1;
1061
+ margin: 0 0 10px;
1062
+ text-transform: uppercase;
1063
  }
1064
 
1065
+ .composer-row {
1066
+ align-items: stretch;
1067
+ gap: 8px !important;
1068
  }
1069
 
1070
+ #message-input {
1071
+ flex: 1 1 auto;
 
 
1072
  }
1073
 
1074
+ #message-input textarea {
1075
+ min-height: 42px !important;
1076
  }
1077
 
1078
+ #clear-schema-button button {
1079
+ background: transparent !important;
1080
+ border: 0.5px solid var(--border) !important;
1081
+ color: var(--text-secondary) !important;
1082
+ min-height: 30px !important;
1083
+ min-width: 34px !important;
1084
+ width: 34px !important;
1085
  }
1086
 
1087
+ #clear-schema-button button:hover {
1088
+ border-color: var(--border-hi) !important;
1089
+ color: var(--text-primary) !important;
 
 
 
1090
  }
1091
 
1092
  .preset-row-gradio {
1093
+ gap: 6px !important;
1094
+ margin-bottom: 10px;
1095
  }
1096
 
1097
  .preset-row-gradio button {
1098
+ background: var(--bg-raised) !important;
1099
+ border: 0.5px solid var(--border) !important;
1100
+ border-radius: 999px !important;
1101
+ color: var(--text-secondary) !important;
1102
+ font-size: 11px !important;
1103
+ min-height: 28px !important;
1104
+ padding: 0 10px !important;
1105
  }
1106
 
1107
  .preset-row-gradio button:hover {
1108
+ border-color: var(--border-hi) !important;
1109
+ color: var(--text-primary) !important;
1110
  }
1111
 
1112
  .gradio-container .block,
 
1118
 
1119
  .gradio-container label,
1120
  .gradio-container .label-wrap span {
1121
+ color: var(--text-secondary) !important;
1122
+ font-family: Space Mono, ui-monospace, monospace !important;
1123
+ font-size: 11px !important;
1124
+ font-weight: 500 !important;
1125
  }
1126
 
1127
  textarea,
1128
  input,
1129
  .cm-editor {
1130
+ background: var(--bg-raised) !important;
1131
+ border: 0.5px solid var(--border) !important;
1132
+ border-radius: 6px !important;
1133
+ color: var(--text-primary) !important;
1134
+ font-family: Space Mono, ui-monospace, monospace !important;
1135
+ font-size: 13px !important;
1136
+ font-weight: 400 !important;
1137
+ line-height: 1.45 !important;
1138
+ }
1139
+
1140
+ textarea::placeholder,
1141
+ input::placeholder {
1142
+ color: var(--text-muted) !important;
1143
  }
1144
 
1145
  textarea {
1146
+ min-height: 132px !important;
1147
+ }
1148
+
1149
+ .section-divider {
1150
+ border-top: 0.5px solid var(--border);
1151
+ margin: 28px 0 0;
1152
  }
1153
 
1154
  .output-shell {
1155
+ background: var(--bg-surface);
1156
+ border: 0.5px solid var(--border);
1157
+ border-radius: 6px;
1158
+ margin-top: 12px;
1159
  overflow: hidden;
1160
  }
1161
 
1162
  .output-head {
1163
  align-items: center;
1164
+ background: var(--bg-surface);
1165
+ border-bottom: 0.5px solid var(--border);
1166
  display: flex;
1167
  justify-content: space-between;
1168
+ min-height: 34px;
1169
+ padding: 0 12px;
1170
  }
1171
 
1172
  .output-head span:first-child {
1173
+ color: var(--text-secondary);
1174
+ font-size: 11px;
1175
+ font-weight: 500;
1176
+ letter-spacing: 0.08em;
1177
+ text-transform: uppercase;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1178
  }
1179
 
1180
  .validator-detail {
1181
+ color: var(--text-secondary);
1182
+ font-size: 11px;
1183
  margin-left: 8px;
1184
  }
1185
 
1186
  .output-shell .cm-editor,
1187
  .output-shell pre,
1188
+ .output-shell code,
1189
+ .compare-card .cm-editor,
1190
+ .compare-card pre,
1191
+ .compare-card code {
1192
  border: 0 !important;
1193
+ font-size: 12px !important;
1194
+ font-weight: 400 !important;
1195
  }
1196
 
1197
  .message-box {
1198
+ color: var(--text-secondary);
1199
+ font-size: 11px;
1200
+ font-weight: 400;
1201
+ min-height: 24px;
1202
  padding-top: 8px;
1203
  }
1204
 
1205
  .message-error {
1206
+ color: var(--amber-text);
1207
  }
1208
 
1209
  .message-ok {
1210
+ color: var(--teal);
 
 
 
 
1211
  }
1212
 
1213
  .comparison-panel {
1214
+ margin-top: 28px;
 
 
 
 
 
 
1215
  }
1216
 
1217
  .compare-card {
1218
+ background: var(--bg-surface);
1219
+ border: 0.5px solid var(--border);
1220
+ border-radius: 6px;
1221
  overflow: hidden;
1222
  }
1223
 
1224
  .compare-card.current {
1225
+ border-color: rgba(29, 158, 117, 0.45);
1226
  }
1227
 
1228
  .compare-head {
1229
  align-items: center;
1230
+ background: var(--amber-soft);
1231
+ color: var(--amber-text);
1232
  display: flex;
1233
+ font-size: 11px;
1234
+ font-weight: 500;
1235
+ gap: 16px;
1236
+ justify-content: space-between;
1237
+ min-height: 34px;
1238
+ padding: 0 12px;
1239
  }
1240
 
1241
  .compare-card.current .compare-head,
1242
  .current-compare-head .compare-head {
1243
+ background: var(--teal-soft);
1244
+ color: var(--teal-text);
1245
  }
1246
 
1247
  .compare-head strong {
1248
  color: inherit;
1249
+ font-weight: 500;
1250
  }
1251
 
1252
  .loading-overlay {
1253
  align-items: center;
1254
+ background: rgba(0, 0, 0, 0.6);
1255
  bottom: 0;
1256
  display: flex;
1257
  justify-content: center;
 
1267
  }
1268
 
1269
  .loading-card {
1270
+ background: var(--bg-surface);
1271
+ border: 0.5px solid var(--border-hi);
1272
+ border-radius: 6px;
1273
+ max-width: 480px;
1274
+ padding: 18px;
1275
+ width: min(90vw, 480px);
1276
  }
1277
 
1278
+ .loading-title {
1279
+ color: var(--text-primary);
1280
+ font-size: 13px;
1281
+ font-weight: 500;
1282
+ line-height: 1.35;
1283
+ margin-bottom: 12px;
1284
  }
1285
 
1286
  .loading-line {
1287
+ background: var(--bg-raised);
1288
  border-radius: 999px;
1289
+ height: 8px;
1290
  overflow: hidden;
1291
  }
1292
 
1293
  .loading-line span {
1294
  animation: loadingPulse 1.3s infinite ease-in-out;
1295
+ background: var(--teal);
1296
  border-radius: inherit;
1297
  display: block;
1298
  height: 100%;
1299
+ width: 45%;
1300
  }
1301
 
1302
  .loading-card p {
1303
+ color: var(--text-secondary);
1304
+ font-size: 11px;
1305
+ font-weight: 400;
1306
+ line-height: 1.4;
1307
+ margin: 12px 0 0;
1308
  }
1309
 
1310
  @keyframes loadingPulse {
1311
+ 0% { transform: translateX(-70%); }
1312
+ 50% { transform: translateX(70%); }
1313
+ 100% { transform: translateX(220%); }
1314
  }
1315
 
1316
+ @media (max-width: 860px) {
1317
  .top-panel,
1318
  .model-grid,
1319
  .compare-grid {
1320
  grid-template-columns: 1fr;
1321
  }
1322
 
1323
+ .stats-row {
1324
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1325
+ }
1326
+
1327
  .top-badges {
1328
  justify-content: flex-start;
1329
  }
 
1334
  }
1335
  """
1336
 
1337
+ with gr.Blocks(css=CSS, title="Phi-3 Mini SQL Generator") as demo:
 
 
 
1338
  selected_model_key = gr.State(value=DEFAULT_MODEL_KEY)
1339
  loaded_key_state = gr.State(value=None)
1340
  saved_output = gr.State(value=None)
1341
+ active_schema = gr.State(value="")
1342
+ last_user_message = gr.State(value="")
1343
 
1344
  with gr.Column(elem_classes=["app-shell"]):
1345
  loading_overlay = gr.HTML(render_loading_overlay(visible=False))
1346
  gr.HTML(render_header())
1347
 
1348
+ gr.HTML(render_step("01", "Model"))
1349
  with gr.Row(elem_classes=["model-grid"]):
1350
  base_model_card = gr.HTML(render_model_card(BASE_MODEL_KEY, DEFAULT_MODEL_KEY))
1351
  fine_tuned_model_card = gr.HTML(render_model_card(FINE_TUNED_MODEL_KEY, DEFAULT_MODEL_KEY))
 
 
 
1352
  load_button = gr.Button("Load selected model", variant="primary", elem_id="load-button")
1353
  model_status = gr.HTML(render_status(DEFAULT_MODEL_KEY, None))
1354
+ model_info = gr.HTML(model_metadata(DEFAULT_MODEL_KEY))
1355
+
1356
+ with gr.Column(elem_id="query-section", elem_classes=["query-section"]):
1357
+ gr.HTML(render_step("02", "Chat"))
1358
+ chatbot_kwargs = {
1359
+ "label": "",
1360
+ "height": 360,
1361
+ "show_label": False,
1362
+ "elem_classes": ["chat-history"],
1363
+ }
1364
+ if "type" in inspect.signature(gr.Chatbot).parameters:
1365
+ chatbot_kwargs["type"] = "messages"
1366
+ chatbot = gr.Chatbot(**chatbot_kwargs)
1367
+ gr.HTML('<div class="field-label">Schema context</div>')
1368
+ with gr.Row(elem_classes=["preset-row-gradio", "prompt-chip-row"]):
1369
+ employees_preset = gr.Button("employees", size="sm")
1370
+ orders_preset = gr.Button("orders", size="sm")
1371
+ students_preset = gr.Button("students", size="sm")
1372
+ products_preset = gr.Button("products", size="sm")
1373
+ sales_preset = gr.Button("sales", size="sm")
1374
+ with gr.Row(elem_classes=["schema-context-row"]):
1375
+ active_schema_pill = gr.HTML(render_schema_context(""))
1376
+ clear_schema_button = gr.Button("x", size="sm", visible=False, elem_id="clear-schema-button")
1377
+ with gr.Row(elem_classes=["composer-row"]):
1378
+ message_input = gr.Textbox(
1379
+ label="",
1380
+ value="",
1381
+ placeholder="Type a message...",
1382
+ lines=1,
1383
+ max_lines=5,
1384
+ interactive=True,
1385
+ show_label=False,
1386
+ elem_id="message-input",
1387
+ )
1388
+ send_button = gr.Button(
1389
+ "Send",
1390
+ variant="primary",
1391
+ interactive=False,
1392
+ elem_id="generate-button",
1393
+ )
1394
 
1395
  gr.HTML('<div class="section-divider"></div>')
1396
+ gr.HTML(render_step("03", "Output"))
1397
  with gr.Column(elem_classes=["output-shell"]):
1398
  with gr.Row(elem_classes=["output-head"]):
1399
  gr.HTML("<span>SQL</span>")
 
1408
  save_button = gr.Button(
1409
  "Save for comparison",
1410
  interactive=False,
1411
+ visible=False,
1412
  elem_id="save-button",
1413
  )
1414
  error_output = gr.HTML(render_message())
1415
 
 
 
 
 
 
 
 
 
1416
  with gr.Column(visible=False, elem_classes=["comparison-panel"]) as comparison_panel:
1417
  with gr.Row(elem_classes=["compare-grid"]):
1418
  with gr.Column(elem_classes=["compare-card"]):
 
1433
  students_preset,
1434
  products_preset,
1435
  sales_preset,
1436
+ clear_schema_button,
1437
+ message_input,
1438
+ send_button,
1439
  save_button,
1440
  error_output,
1441
  ]
 
 
 
 
 
 
 
 
 
 
1442
  base_model_card.click(
1443
  select_model,
1444
  inputs=[gr.State(BASE_MODEL_KEY), loaded_key_state],
 
1464
  students_preset,
1465
  products_preset,
1466
  sales_preset,
1467
+ clear_schema_button,
1468
+ message_input,
1469
+ send_button,
1470
  sql_output,
1471
  validator_output,
1472
  save_button,
1473
  error_output,
1474
  comparison_panel,
1475
  ],
1476
+ js=LOAD_SCROLL_JS,
1477
  )
1478
 
1479
+ schema_context_outputs = [active_schema, active_schema_pill, clear_schema_button]
1480
+ employees_preset.click(set_preset, inputs=gr.State("employees"), outputs=schema_context_outputs)
1481
+ orders_preset.click(set_preset, inputs=gr.State("orders"), outputs=schema_context_outputs)
1482
+ students_preset.click(set_preset, inputs=gr.State("students"), outputs=schema_context_outputs)
1483
+ products_preset.click(set_preset, inputs=gr.State("products"), outputs=schema_context_outputs)
1484
+ sales_preset.click(set_preset, inputs=gr.State("sales"), outputs=schema_context_outputs)
1485
+ clear_schema_button.click(clear_schema_context, outputs=schema_context_outputs)
1486
+
1487
+ chat_generation_outputs = [
1488
+ chatbot,
1489
+ message_input,
1490
+ active_schema,
1491
+ last_user_message,
1492
+ sql_output,
1493
+ validator_output,
1494
+ save_button,
1495
+ error_output,
1496
+ comparison_panel,
1497
+ saved_model_label,
1498
+ saved_sql,
1499
+ current_model_label,
1500
+ current_sql,
1501
+ ]
1502
+ send_button.click(
1503
+ generate_response,
1504
+ inputs=[message_input, chatbot, active_schema, loaded_key_state, saved_output],
1505
+ outputs=chat_generation_outputs,
1506
+ )
1507
+ message_input.submit(
1508
+ generate_response,
1509
+ inputs=[message_input, chatbot, active_schema, loaded_key_state, saved_output],
1510
+ outputs=chat_generation_outputs,
1511
  )
1512
  save_button.click(
1513
  save_for_comparison,
1514
+ inputs=[sql_output, loaded_key_state, active_schema, last_user_message],
1515
  outputs=[
1516
  saved_output,
1517
  comparison_panel,
 
1531
 
1532
 
1533
  if __name__ == "__main__":
1534
+ demo.launch()