pixel3user commited on
Commit
806144f
·
1 Parent(s): 3ac2619

Update Farm2Market demo frontend

Browse files
src/App.css CHANGED
@@ -341,6 +341,313 @@ select {
341
  animation: spin 0.8s linear infinite;
342
  }
343
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
  .thread {
345
  flex: 1;
346
  min-height: 0;
@@ -512,361 +819,683 @@ select {
512
  gap: 10px;
513
  }
514
 
515
- .marketing-studio-root {
516
- flex: 1;
517
- min-height: 0;
518
- display: flex;
519
- flex-direction: column;
520
- padding: 12px;
521
- gap: 12px;
 
 
 
522
  }
523
 
524
- .marketing-studio-header {
525
- border: 1px solid rgba(31, 143, 78, 0.2);
526
- border-radius: 16px;
527
- background: linear-gradient(145deg, rgba(255, 255, 255, 0.9), rgba(230, 246, 237, 0.76));
528
- padding: 13px 14px;
529
  display: flex;
530
  justify-content: space-between;
531
  align-items: center;
532
- gap: 12px;
533
  }
534
 
535
- .marketing-studio-header h3 {
 
 
536
  margin: 0;
537
- font-size: 16px;
538
  font-family: 'Sora', 'IBM Plex Sans', sans-serif;
539
  }
540
 
541
- .marketing-studio-header p {
542
- margin: 4px 0 0;
543
- font-size: 12px;
 
544
  color: var(--text-soft);
 
 
545
  }
546
 
547
- .marketing-studio-layout {
548
- flex: 1;
549
- min-height: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
550
  display: grid;
551
- grid-template-columns: minmax(300px, 380px) minmax(0, 1fr);
552
- gap: 12px;
 
 
553
  }
554
 
555
- .marketing-controls,
556
- .marketing-output {
557
- border: 1px solid var(--line);
558
- border-radius: 16px;
559
- background: rgba(255, 255, 255, 0.74);
560
- min-height: 0;
561
  }
562
 
563
- .marketing-controls {
564
- padding: 12px;
565
- overflow: auto;
566
- display: grid;
567
- align-content: start;
568
- gap: 10px;
569
  }
570
 
571
- .marketing-output {
572
- padding: 12px;
573
- overflow: auto;
574
- display: grid;
575
- align-content: start;
576
- gap: 10px;
577
  }
578
 
579
- .marketing-output > header h4 {
580
- margin: 0;
581
- font-family: 'Sora', 'IBM Plex Sans', sans-serif;
582
- font-size: 15px;
583
  }
584
 
585
- .marketing-output > header p {
586
- margin: 4px 0 0;
587
- color: var(--text-soft);
588
- font-size: 12px;
589
  }
590
 
591
- .marketing-field-block {
592
- display: grid;
593
- gap: 7px;
 
 
 
 
 
594
  }
595
 
596
- .marketing-field-block > label,
597
- .marketing-config-row label > span {
598
- font-size: 12px;
599
- color: var(--text-soft);
 
 
 
600
  }
601
 
602
- .marketing-config-row {
603
- display: grid;
604
- grid-template-columns: repeat(2, minmax(0, 1fr));
605
- gap: 8px;
606
  }
607
 
608
- .marketing-config-row label {
609
- display: grid;
610
- gap: 6px;
 
611
  }
612
 
613
- .marketing-product-grid {
614
- display: grid;
615
- gap: 8px;
 
616
  }
617
 
618
- .marketing-product-card {
619
- border: 1px solid rgba(31, 143, 78, 0.22);
620
- border-radius: 12px;
621
- background: linear-gradient(145deg, rgba(255, 255, 255, 0.9), rgba(227, 243, 234, 0.7));
622
- text-align: left;
623
- color: inherit;
624
- cursor: pointer;
625
- padding: 10px;
626
- display: grid;
627
- gap: 2px;
628
- transition: 120ms transform ease, 120ms border-color ease;
629
  }
630
 
631
- .marketing-product-card:hover {
632
- transform: translateY(-1px);
 
 
 
 
 
 
633
  }
634
 
635
- .marketing-product-card strong {
636
- font-size: 13px;
 
 
 
 
 
 
 
 
 
637
  }
638
 
639
- .marketing-product-card small {
640
- color: var(--text-soft);
641
- font-size: 11px;
 
 
 
 
 
 
 
 
 
 
642
  }
643
 
644
- .marketing-product-card span,
645
- .marketing-product-card em {
646
- font-size: 11px;
647
- color: #145d36;
648
- font-style: normal;
 
 
649
  }
650
 
651
- .marketing-product-card.selected {
652
- border-color: rgba(31, 143, 78, 0.5);
653
- box-shadow: inset 0 0 0 1px rgba(31, 143, 78, 0.2);
 
 
 
654
  }
655
 
656
- .hidden-input {
657
- display: none;
 
 
 
 
 
 
 
 
658
  }
659
 
660
- .marketing-upload-row {
661
- display: flex;
662
- gap: 8px;
663
- flex-wrap: wrap;
 
 
 
 
664
  }
665
 
666
- .marketing-upload-preview {
667
- border: 1px solid rgba(15, 109, 139, 0.22);
668
- border-radius: 12px;
669
- background: rgba(255, 255, 255, 0.85);
670
- padding: 8px;
671
  }
672
 
673
- .marketing-upload-preview img {
674
- width: 100%;
675
- max-height: 160px;
676
- object-fit: cover;
677
- border-radius: 9px;
678
- display: block;
679
  }
680
 
681
- .marketing-upload-preview small {
682
- margin-top: 6px;
683
- display: block;
684
- font-size: 11px;
685
- color: var(--text-soft);
686
  }
687
 
688
- .marketing-help-text {
 
 
689
  margin: 0;
690
- color: var(--text-soft);
691
- font-size: 12px;
 
692
  }
693
 
694
- .marketing-action-row {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
695
  display: flex;
696
- flex-wrap: wrap;
697
- gap: 8px;
698
  }
699
 
700
- .marketing-empty,
701
- .marketing-note,
702
- .marketing-error {
703
- border-radius: 12px;
704
- padding: 9px 10px;
705
- font-size: 12px;
706
  }
707
 
708
- .marketing-empty {
709
- border: 1px dashed rgba(31, 143, 78, 0.26);
710
- color: var(--text-soft);
711
- background: rgba(246, 252, 248, 0.8);
712
  }
713
 
714
- .marketing-note {
715
- border: 1px solid rgba(15, 109, 139, 0.3);
716
- color: #0f5570;
717
- background: rgba(232, 247, 252, 0.84);
718
  }
719
 
720
- .marketing-error {
721
- border: 1px solid rgba(166, 54, 24, 0.34);
722
- color: #8a2f1d;
723
- background: rgba(255, 236, 231, 0.88);
724
  }
725
 
726
- .marketing-stream-card {
727
- border: 1px solid rgba(31, 143, 78, 0.24);
728
- border-radius: 14px;
729
- background: rgba(255, 255, 255, 0.86);
730
- padding: 10px;
731
  }
732
 
733
- .marketing-stream-actions {
734
- display: flex;
735
- align-items: center;
736
- justify-content: space-between;
737
- gap: 8px;
738
- margin-bottom: 8px;
 
739
  }
740
 
741
- .marketing-stream-actions strong {
742
- font-size: 12px;
 
 
 
 
 
743
  }
744
 
745
- .marketing-stream-card pre {
746
- margin: 0;
747
- border-radius: 10px;
748
- padding: 10px;
749
- background: rgba(16, 26, 19, 0.87);
750
- color: #d7f5d4;
751
- font-size: 11px;
752
- white-space: pre-wrap;
753
- word-break: break-word;
754
- font-family: 'IBM Plex Mono', monospace;
755
  }
756
 
757
- .marketing-deck-grid {
 
 
 
 
758
  display: grid;
759
- gap: 8px;
760
- grid-template-columns: repeat(2, minmax(0, 1fr));
761
  }
762
 
763
- .marketing-deck-grid section {
764
- border: 1px solid rgba(15, 109, 139, 0.2);
765
- border-radius: 12px;
766
- background: rgba(255, 255, 255, 0.86);
767
- padding: 10px;
 
 
768
  }
769
 
770
- .marketing-deck-grid h5 {
771
  margin: 0;
772
- font-size: 12px;
773
- text-transform: uppercase;
774
- letter-spacing: 0.05em;
775
- color: var(--text-soft);
776
- }
777
-
778
- .marketing-deck-grid p {
779
- margin: 7px 0 0;
780
- font-size: 13px;
781
- line-height: 1.45;
782
  }
783
 
784
- .marketing-deck-grid ul {
785
- margin: 7px 0 0;
786
- padding-left: 16px;
787
- }
788
-
789
- .marketing-deck-grid li {
790
- font-size: 12px;
791
- margin-bottom: 4px;
792
  }
793
 
794
- .marketing-image-grid {
 
795
  display: grid;
796
- grid-template-columns: repeat(2, minmax(0, 1fr));
797
- gap: 10px;
 
 
 
 
 
 
798
  }
799
 
800
- .marketing-image-grid article {
801
- border: 1px solid rgba(31, 143, 78, 0.24);
802
- border-radius: 12px;
803
- background: rgba(255, 255, 255, 0.87);
 
804
  overflow: hidden;
805
  }
806
 
807
- .marketing-image-grid img {
808
- width: 100%;
809
- aspect-ratio: 16 / 10;
810
- object-fit: cover;
811
  display: block;
 
 
 
 
 
812
  }
813
 
814
- .marketing-image-meta {
815
- padding: 8px;
816
  display: grid;
817
- gap: 8px;
818
  }
819
 
820
- .marketing-image-meta strong {
821
- font-size: 12px;
 
 
 
 
 
 
 
 
822
  }
823
 
824
- .marketing-image-meta > div {
825
- display: flex;
826
- gap: 6px;
827
- flex-wrap: wrap;
828
  }
829
 
830
- .marketing-meta-card {
831
- border: 1px solid rgba(15, 109, 139, 0.24);
832
- border-radius: 12px;
833
- background: rgba(255, 255, 255, 0.88);
834
- padding: 10px;
835
  }
836
 
837
- .marketing-meta-card h5 {
838
- margin: 0;
839
- font-size: 13px;
840
- font-family: 'Sora', 'IBM Plex Sans', sans-serif;
841
  }
842
 
843
- .marketing-meta-card dl {
844
- margin: 10px 0 0;
 
 
 
845
  display: grid;
846
- grid-template-columns: repeat(2, minmax(0, 1fr));
847
- gap: 8px;
848
  }
849
 
850
- .marketing-meta-card dl div {
851
- border: 1px solid var(--line);
852
- border-radius: 10px;
853
- padding: 7px;
854
- background: rgba(255, 255, 255, 0.85);
 
 
 
 
 
855
  }
856
 
857
- .marketing-meta-card dt {
858
- font-size: 10px;
859
- text-transform: uppercase;
860
- letter-spacing: 0.06em;
861
- color: var(--text-soft);
862
  }
863
 
864
- .marketing-meta-card dd {
865
- margin: 4px 0 0;
866
- font-size: 12px;
867
  }
868
 
869
- .invoice-demo-root {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
870
  flex: 1;
871
  min-height: 0;
872
  display: flex;
@@ -875,10 +1504,10 @@ select {
875
  gap: 12px;
876
  }
877
 
878
- .invoice-demo-header {
879
- border: 1px solid rgba(15, 109, 139, 0.21);
880
  border-radius: 16px;
881
- background: linear-gradient(145deg, rgba(255, 255, 255, 0.9), rgba(231, 242, 252, 0.75));
882
  padding: 13px 14px;
883
  display: flex;
884
  justify-content: space-between;
@@ -886,796 +1515,1598 @@ select {
886
  gap: 12px;
887
  }
888
 
889
- .invoice-demo-header h3 {
890
  margin: 0;
891
  font-size: 16px;
892
  font-family: 'Sora', 'IBM Plex Sans', sans-serif;
893
  }
894
 
895
- .invoice-demo-header p {
896
  margin: 4px 0 0;
897
  font-size: 12px;
898
  color: var(--text-soft);
899
  }
900
 
901
- .invoice-demo-layout {
902
  flex: 1;
903
  min-height: 0;
904
  display: grid;
905
- grid-template-columns: minmax(300px, 390px) minmax(0, 1fr);
906
  gap: 12px;
907
  }
908
 
909
- .invoice-input-panel,
910
- .invoice-output-panel {
911
  border: 1px solid var(--line);
912
  border-radius: 16px;
913
  background: rgba(255, 255, 255, 0.74);
914
  min-height: 0;
915
  }
916
 
917
- .invoice-input-panel {
918
- overflow: auto;
919
  padding: 12px;
 
920
  display: grid;
921
  align-content: start;
922
  gap: 10px;
923
  }
924
 
925
- .invoice-output-panel {
926
- overflow: auto;
927
  padding: 12px;
 
928
  display: grid;
929
  align-content: start;
930
  gap: 10px;
931
  }
932
 
933
- .invoice-output-panel > header h4 {
934
  margin: 0;
935
- font-size: 15px;
936
  font-family: 'Sora', 'IBM Plex Sans', sans-serif;
 
937
  }
938
 
939
- .invoice-output-panel > header p {
940
  margin: 4px 0 0;
 
 
 
 
 
 
 
 
 
 
 
941
  font-size: 12px;
942
  color: var(--text-soft);
943
  }
944
 
945
- .invoice-field-grid {
946
  display: grid;
 
947
  gap: 8px;
948
  }
949
 
950
- .invoice-field-block {
951
  display: grid;
952
- gap: 7px;
953
  }
954
 
955
- .invoice-field-block > span {
956
- font-size: 12px;
957
- color: var(--text-soft);
958
  }
959
 
960
- .invoice-dropzone {
961
- border: 1px dashed rgba(15, 109, 139, 0.38);
962
  border-radius: 12px;
963
- padding: 12px;
964
  text-align: left;
965
- background: rgba(234, 246, 252, 0.65);
966
  color: inherit;
967
  cursor: pointer;
 
968
  display: grid;
969
- gap: 3px;
970
- transition: 130ms background ease, 130ms transform ease;
971
  }
972
 
973
- .invoice-dropzone:hover {
974
  transform: translateY(-1px);
975
- background: rgba(226, 241, 251, 0.84);
976
  }
977
 
978
- .invoice-dropzone strong {
979
  font-size: 13px;
980
  }
981
 
982
- .invoice-dropzone small {
983
- font-size: 11px;
984
  color: var(--text-soft);
 
985
  }
986
 
987
- .invoice-dropzone.has-file {
988
- border-style: solid;
989
- border-color: rgba(31, 143, 78, 0.36);
990
- background: rgba(229, 245, 235, 0.78);
 
991
  }
992
 
993
- .invoice-file-preview {
994
- border: 1px solid rgba(31, 143, 78, 0.26);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
995
  border-radius: 12px;
996
- background: rgba(255, 255, 255, 0.86);
997
  padding: 8px;
998
  }
999
 
1000
- .invoice-file-preview img {
1001
  width: 100%;
1002
- max-height: 170px;
1003
  object-fit: cover;
1004
  border-radius: 9px;
1005
  display: block;
1006
  }
1007
 
1008
- .invoice-file-meta {
1009
- margin-top: 8px;
1010
- display: flex;
1011
- gap: 8px;
1012
- align-items: center;
1013
- justify-content: space-between;
1014
- }
1015
-
1016
- .invoice-file-meta span {
1017
- min-width: 0;
1018
- overflow: hidden;
1019
- text-overflow: ellipsis;
1020
- white-space: nowrap;
1021
- font-size: 12px;
1022
  }
1023
 
1024
- .invoice-help-text {
1025
  margin: 0;
1026
- font-size: 12px;
1027
  color: var(--text-soft);
 
1028
  }
1029
 
1030
- .invoice-toggle-row label {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1031
  display: flex;
1032
  align-items: center;
 
1033
  gap: 8px;
 
 
 
 
1034
  font-size: 12px;
1035
- color: var(--text);
1036
  }
1037
 
1038
- .invoice-toggle-row input {
1039
  margin: 0;
1040
- accent-color: #167d99;
 
 
 
 
 
 
 
1041
  }
1042
 
1043
- .invoice-action-row {
1044
- display: flex;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1045
  gap: 8px;
 
 
 
 
 
 
 
 
 
1046
  flex-wrap: wrap;
1047
  }
1048
 
1049
- .invoice-jobs-block h4 {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1050
  margin: 0;
1051
- font-size: 13px;
1052
  font-family: 'Sora', 'IBM Plex Sans', sans-serif;
1053
  }
1054
 
1055
- .invoice-job-list {
1056
- list-style: none;
1057
- margin: 0;
1058
- padding: 0;
1059
  display: grid;
 
1060
  gap: 8px;
1061
  }
1062
 
1063
- .invoice-job-card {
1064
- border: 1px solid var(--line);
1065
- border-radius: 11px;
1066
- padding: 8px;
1067
- background: rgba(255, 255, 255, 0.84);
1068
  display: grid;
1069
  gap: 6px;
1070
  }
1071
 
1072
- .invoice-job-card header {
1073
- display: flex;
1074
- align-items: baseline;
1075
- justify-content: space-between;
1076
- gap: 10px;
1077
- }
1078
-
1079
- .invoice-job-card header strong {
1080
- min-width: 0;
1081
- font-size: 12px;
1082
- overflow: hidden;
1083
- text-overflow: ellipsis;
1084
- white-space: nowrap;
1085
- }
1086
-
1087
- .invoice-job-card header span,
1088
- .invoice-job-card small {
1089
- font-size: 10px;
1090
- color: var(--text-soft);
1091
- }
1092
-
1093
- .invoice-job-status {
1094
- margin: 0;
1095
  font-size: 11px;
1096
  color: var(--text-soft);
1097
  }
1098
 
1099
- .invoice-job-meter {
1100
- height: 7px;
1101
- border-radius: 999px;
1102
- background: rgba(22, 66, 27, 0.1);
1103
- overflow: hidden;
1104
- }
1105
-
1106
- .invoice-job-meter span {
1107
- display: block;
1108
- height: 100%;
1109
- border-radius: 999px;
1110
- background: linear-gradient(90deg, #1f8f4e, #0f6d8b);
1111
- transition: width 180ms ease;
1112
  }
1113
 
1114
- .invoice-job-card.ready {
1115
- border-color: rgba(31, 143, 78, 0.35);
 
 
1116
  }
1117
 
1118
- .invoice-job-card.failed {
1119
- border-color: rgba(166, 54, 24, 0.36);
 
 
 
1120
  }
1121
 
1122
- .invoice-empty,
1123
- .invoice-note,
1124
- .invoice-error {
1125
  border-radius: 12px;
1126
  padding: 9px 10px;
1127
  font-size: 12px;
1128
  }
1129
 
1130
- .invoice-empty {
1131
- border: 1px dashed rgba(15, 109, 139, 0.3);
1132
- color: var(--text-soft);
1133
- background: rgba(241, 249, 253, 0.75);
1134
- }
1135
-
1136
- .invoice-note {
1137
- border: 1px solid rgba(31, 143, 78, 0.3);
1138
  color: #145f39;
1139
- background: rgba(234, 249, 239, 0.85);
1140
  }
1141
 
1142
- .invoice-error {
1143
  border: 1px solid rgba(166, 54, 24, 0.36);
1144
- color: #8d2e1a;
1145
- background: rgba(255, 237, 232, 0.86);
1146
- }
1147
-
1148
- .invoice-metric-grid {
1149
- display: grid;
1150
- grid-template-columns: repeat(4, minmax(0, 1fr));
1151
- gap: 8px;
1152
- }
1153
-
1154
- .invoice-metric-card {
1155
- border: 1px solid rgba(15, 109, 139, 0.24);
1156
- border-radius: 11px;
1157
- background: rgba(255, 255, 255, 0.88);
1158
- padding: 9px;
1159
  }
1160
 
1161
- .invoice-metric-card span {
1162
- display: block;
1163
- font-size: 11px;
1164
  color: var(--text-soft);
 
1165
  }
1166
 
1167
- .invoice-metric-card strong {
1168
- display: block;
1169
- margin-top: 4px;
1170
- font-size: 14px;
1171
- }
1172
-
1173
- .invoice-preview-card,
1174
- .invoice-record-card,
1175
- .invoice-meta-card,
1176
- .invoice-json-card {
1177
  border: 1px solid rgba(15, 109, 139, 0.22);
1178
  border-radius: 12px;
1179
  background: rgba(255, 255, 255, 0.88);
1180
  padding: 10px;
1181
  }
1182
 
1183
- .invoice-preview-card h5,
1184
- .invoice-record-card h5,
1185
- .invoice-meta-card h5 {
1186
  margin: 0;
1187
  font-size: 13px;
1188
  font-family: 'Sora', 'IBM Plex Sans', sans-serif;
1189
  }
1190
 
1191
- .invoice-preview-grid {
1192
- margin: 9px 0 0;
1193
  display: grid;
1194
  grid-template-columns: repeat(3, minmax(0, 1fr));
1195
- gap: 7px;
1196
  }
1197
 
1198
- .invoice-preview-grid div {
1199
  border: 1px solid var(--line);
1200
  border-radius: 10px;
1201
- padding: 7px;
1202
- background: rgba(255, 255, 255, 0.85);
1203
  }
1204
 
1205
- .invoice-preview-grid dt {
 
1206
  font-size: 10px;
1207
  color: var(--text-soft);
1208
  text-transform: uppercase;
1209
  letter-spacing: 0.06em;
1210
  }
1211
 
1212
- .invoice-preview-grid dd {
1213
- margin: 4px 0 0;
1214
- font-size: 12px;
1215
- line-height: 1.35;
1216
  }
1217
 
1218
- .invoice-items-scroll {
1219
  margin-top: 9px;
1220
- overflow: auto;
 
 
1221
  }
1222
 
1223
- .invoice-items-table {
1224
- width: 100%;
1225
- border-collapse: collapse;
1226
- font-size: 12px;
 
1227
  }
1228
 
1229
- .invoice-items-table th,
1230
- .invoice-items-table td {
1231
- border-bottom: 1px solid var(--line);
1232
- text-align: left;
1233
- padding: 7px;
 
 
 
 
 
 
 
1234
  white-space: nowrap;
1235
  }
1236
 
1237
- .invoice-items-table th {
1238
  font-size: 10px;
1239
  color: var(--text-soft);
1240
  text-transform: uppercase;
1241
  letter-spacing: 0.06em;
1242
  }
1243
 
1244
- .invoice-raw-text {
1245
- margin: 9px 0 0;
1246
- border-radius: 10px;
1247
- background: rgba(16, 26, 19, 0.87);
1248
- color: #d9f4d5;
1249
- font-family: 'IBM Plex Mono', monospace;
 
 
1250
  font-size: 11px;
1251
- padding: 10px;
1252
- white-space: pre-wrap;
1253
- word-break: break-word;
1254
  }
1255
 
1256
- .invoice-json-card summary {
1257
- cursor: pointer;
1258
  font-size: 12px;
1259
- color: var(--accent-2);
1260
  }
1261
 
1262
- .invoice-json-card pre {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1263
  margin: 8px 0 0;
1264
- border-radius: 10px;
1265
- background: rgba(16, 26, 19, 0.9);
1266
- color: #d8f6d3;
1267
- font-family: 'IBM Plex Mono', monospace;
1268
- font-size: 11px;
1269
- padding: 9px;
1270
- overflow: auto;
1271
  }
1272
 
1273
- .invoice-meta-card dl {
1274
  margin: 9px 0 0;
1275
  display: grid;
1276
- grid-template-columns: repeat(3, minmax(0, 1fr));
1277
  gap: 7px;
1278
  }
1279
 
1280
- .invoice-meta-card dl div {
1281
  border: 1px solid var(--line);
1282
  border-radius: 10px;
1283
  padding: 7px;
1284
- background: rgba(255, 255, 255, 0.84);
1285
  }
1286
 
1287
- .invoice-meta-card dt {
1288
  font-size: 10px;
1289
  text-transform: uppercase;
1290
  letter-spacing: 0.06em;
1291
  color: var(--text-soft);
1292
  }
1293
 
1294
- .invoice-meta-card dd {
1295
  margin: 4px 0 0;
1296
  font-size: 12px;
1297
  }
1298
 
1299
- .invoice-record-card p {
1300
- margin: 8px 0 0;
1301
- font-size: 12px;
1302
- }
1303
-
1304
- .invoice-history-list {
1305
- list-style: none;
1306
- margin: 8px 0 0;
1307
- padding: 0;
1308
- display: grid;
1309
- gap: 6px;
1310
- }
1311
-
1312
- .invoice-history-list li {
1313
- border: 1px solid var(--line);
1314
- border-radius: 10px;
1315
- padding: 7px;
1316
- background: rgba(255, 255, 255, 0.84);
1317
- display: flex;
1318
- justify-content: space-between;
1319
- gap: 8px;
1320
  }
1321
 
1322
- .invoice-history-list span {
1323
- min-width: 0;
1324
- overflow: hidden;
1325
- text-overflow: ellipsis;
1326
- white-space: nowrap;
1327
  font-size: 12px;
 
1328
  }
1329
 
1330
- .invoice-history-list strong {
1331
- font-size: 12px;
 
 
 
 
 
 
 
1332
  }
1333
 
1334
- .marketplace-demo-root {
1335
  flex: 1;
1336
  min-height: 0;
1337
  display: flex;
1338
  flex-direction: column;
 
 
1339
  padding: 12px;
1340
- gap: 12px;
1341
  }
1342
 
1343
- .marketplace-demo-header {
1344
- border: 1px solid rgba(31, 143, 78, 0.2);
1345
  border-radius: 16px;
1346
- background: linear-gradient(145deg, rgba(255, 255, 255, 0.9), rgba(233, 248, 242, 0.78));
1347
- padding: 13px 14px;
 
 
 
 
 
 
 
1348
  display: flex;
1349
  justify-content: space-between;
1350
  align-items: center;
1351
- gap: 12px;
1352
  }
1353
 
1354
- .marketplace-demo-header h3 {
1355
  margin: 0;
1356
- font-size: 16px;
1357
  font-family: 'Sora', 'IBM Plex Sans', sans-serif;
1358
  }
1359
 
1360
- .marketplace-demo-header p {
1361
- margin: 4px 0 0;
1362
- font-size: 12px;
1363
  color: var(--text-soft);
 
 
1364
  }
1365
 
1366
- .marketplace-demo-layout {
1367
- flex: 1;
1368
- min-height: 0;
 
 
 
 
 
 
 
 
 
1369
  display: grid;
1370
- grid-template-columns: minmax(300px, 390px) minmax(0, 1fr);
1371
- gap: 12px;
 
 
1372
  }
1373
 
1374
- .marketplace-input-panel,
1375
- .marketplace-output-panel {
1376
- border: 1px solid var(--line);
1377
- border-radius: 16px;
1378
- background: rgba(255, 255, 255, 0.74);
1379
- min-height: 0;
1380
  }
1381
 
1382
- .marketplace-input-panel {
1383
- overflow: auto;
1384
- padding: 12px;
1385
- display: grid;
1386
- align-content: start;
1387
- gap: 10px;
1388
  }
1389
 
1390
- .marketplace-output-panel {
1391
- overflow: auto;
1392
- padding: 12px;
1393
- display: grid;
1394
- align-content: start;
1395
- gap: 10px;
1396
  }
1397
 
1398
- .marketplace-output-panel > header h4 {
1399
- margin: 0;
1400
- font-family: 'Sora', 'IBM Plex Sans', sans-serif;
1401
- font-size: 15px;
 
1402
  }
1403
 
1404
- .marketplace-output-panel > header p {
1405
- margin: 4px 0 0;
1406
- font-size: 12px;
1407
- color: var(--text-soft);
1408
  }
1409
 
1410
- .marketplace-field-block {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1411
  display: grid;
 
1412
  gap: 7px;
 
 
1413
  }
1414
 
1415
- .marketplace-field-block > span {
1416
- font-size: 12px;
1417
- color: var(--text-soft);
 
 
 
 
 
 
1418
  }
1419
 
1420
- .marketplace-filter-block {
1421
- border: 1px solid rgba(15, 109, 139, 0.18);
1422
- border-radius: 12px;
1423
- background: rgba(255, 255, 255, 0.8);
1424
- padding: 9px;
 
 
 
 
1425
  display: grid;
1426
- gap: 8px;
1427
  }
1428
 
1429
- .marketplace-filter-block h4 {
1430
- margin: 0;
1431
- font-size: 12px;
1432
- font-family: 'Sora', 'IBM Plex Sans', sans-serif;
 
1433
  }
1434
 
1435
- .marketplace-filter-grid {
1436
  display: grid;
1437
- grid-template-columns: repeat(3, minmax(0, 1fr));
1438
- gap: 8px;
1439
  }
1440
 
1441
- .marketplace-filter-grid label {
1442
- display: grid;
1443
- gap: 6px;
 
1444
  }
1445
 
1446
- .marketplace-filter-grid label span {
 
1447
  font-size: 11px;
1448
- color: var(--text-soft);
 
1449
  }
1450
 
1451
- .marketplace-action-row {
 
 
 
 
 
 
1452
  display: flex;
1453
- gap: 8px;
1454
- flex-wrap: wrap;
 
1455
  }
1456
 
1457
- .marketplace-recent-block h4 {
1458
- margin: 0;
1459
- font-size: 12px;
1460
- font-family: 'Sora', 'IBM Plex Sans', sans-serif;
 
 
1461
  }
1462
 
1463
- .marketplace-chip-row {
1464
- display: flex;
1465
- flex-wrap: wrap;
1466
- gap: 8px;
1467
- margin-top: 8px;
 
 
 
1468
  }
1469
 
1470
- .marketplace-note,
1471
- .marketplace-error,
1472
- .marketplace-empty {
1473
- border-radius: 12px;
1474
- padding: 9px 10px;
1475
- font-size: 12px;
1476
  }
1477
 
1478
- .marketplace-note {
1479
- border: 1px solid rgba(31, 143, 78, 0.28);
1480
- color: #145f39;
1481
- background: rgba(232, 249, 238, 0.86);
1482
  }
1483
 
1484
- .marketplace-error {
1485
- border: 1px solid rgba(166, 54, 24, 0.36);
1486
- color: #892d19;
1487
- background: rgba(255, 237, 232, 0.87);
 
 
 
 
 
 
 
1488
  }
1489
 
1490
- .marketplace-empty {
1491
- border: 1px dashed rgba(15, 109, 139, 0.3);
1492
- color: var(--text-soft);
1493
- background: rgba(240, 249, 253, 0.8);
 
 
1494
  }
1495
 
1496
- .marketplace-stats-card,
1497
- .marketplace-result-card,
1498
- .marketplace-meta-card {
1499
- border: 1px solid rgba(15, 109, 139, 0.22);
1500
- border-radius: 12px;
1501
- background: rgba(255, 255, 255, 0.88);
1502
- padding: 10px;
1503
  }
1504
 
1505
- .marketplace-stats-card h5,
1506
- .marketplace-result-card h5,
1507
- .marketplace-meta-card h5 {
1508
- margin: 0;
1509
- font-size: 13px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1510
  font-family: 'Sora', 'IBM Plex Sans', sans-serif;
1511
  }
1512
 
1513
- .marketplace-stat-grid {
1514
- margin-top: 9px;
1515
- display: grid;
1516
- grid-template-columns: repeat(3, minmax(0, 1fr));
1517
- gap: 8px;
 
 
 
 
 
 
 
 
 
 
 
 
1518
  }
1519
 
1520
- .marketplace-stat-grid div {
1521
- border: 1px solid var(--line);
1522
- border-radius: 10px;
1523
- padding: 8px;
1524
- background: rgba(255, 255, 255, 0.86);
1525
  }
1526
 
1527
- .marketplace-stat-grid span {
1528
- display: block;
1529
- font-size: 10px;
1530
- color: var(--text-soft);
1531
- text-transform: uppercase;
1532
- letter-spacing: 0.06em;
1533
  }
1534
 
1535
- .marketplace-stat-grid strong {
1536
- display: block;
1537
- margin-top: 4px;
1538
- font-size: 13px;
1539
  }
1540
 
1541
- .marketplace-card-grid {
1542
- margin-top: 9px;
1543
- display: grid;
1544
- grid-template-columns: repeat(2, minmax(0, 1fr));
1545
- gap: 8px;
 
 
 
1546
  }
1547
 
1548
- .marketplace-entity-card {
1549
- border: 1px solid var(--line);
1550
- border-radius: 11px;
1551
- background: rgba(255, 255, 255, 0.86);
1552
- padding: 9px;
1553
  }
1554
 
1555
- .marketplace-entity-card header {
1556
- display: flex;
1557
- align-items: baseline;
1558
- justify-content: space-between;
1559
- gap: 8px;
 
 
 
 
 
1560
  }
1561
 
1562
- .marketplace-entity-card header strong {
1563
- min-width: 0;
1564
- font-size: 13px;
1565
- overflow: hidden;
1566
- text-overflow: ellipsis;
1567
- white-space: nowrap;
1568
  }
1569
 
1570
- .marketplace-entity-card header span {
1571
  font-size: 10px;
1572
- color: var(--text-soft);
1573
- text-transform: uppercase;
1574
- letter-spacing: 0.06em;
1575
- }
1576
-
1577
- .marketplace-metric-row {
1578
- margin-top: 7px;
1579
- display: flex;
1580
- justify-content: space-between;
1581
- gap: 8px;
1582
  }
1583
 
1584
- .marketplace-metric-row span {
1585
- font-size: 11px;
1586
- color: var(--text-soft);
 
 
1587
  }
1588
 
1589
- .marketplace-metric-row strong {
1590
- font-size: 12px;
1591
- text-align: right;
 
 
 
1592
  }
1593
 
1594
- .marketplace-score-track {
1595
- margin-top: 8px;
 
1596
  }
1597
 
1598
- .marketplace-score-track small {
1599
- font-size: 10px;
 
1600
  color: var(--text-soft);
1601
- text-transform: uppercase;
1602
- letter-spacing: 0.06em;
1603
  }
1604
 
1605
- .marketplace-score-track > div {
1606
- margin-top: 5px;
1607
  height: 7px;
1608
  border-radius: 999px;
1609
- background: rgba(22, 66, 27, 0.1);
 
1610
  overflow: hidden;
1611
  }
1612
 
1613
- .marketplace-score-track > div span {
1614
  display: block;
1615
  height: 100%;
1616
- border-radius: 999px;
1617
- background: linear-gradient(90deg, #1f8f4e, #0f6d8b);
 
1618
  }
1619
 
1620
- .marketplace-help-text {
1621
- margin: 8px 0 0;
1622
- font-size: 12px;
1623
- color: var(--text-soft);
1624
  }
1625
 
1626
- .marketplace-meta-card dl {
1627
- margin: 9px 0 0;
 
 
 
 
 
1628
  display: grid;
1629
- grid-template-columns: repeat(2, minmax(0, 1fr));
1630
- gap: 7px;
1631
  }
1632
 
1633
- .marketplace-meta-card dl div {
1634
- border: 1px solid var(--line);
1635
- border-radius: 10px;
1636
- padding: 7px;
1637
- background: rgba(255, 255, 255, 0.86);
1638
  }
1639
 
1640
- .marketplace-meta-card dt {
1641
- font-size: 10px;
1642
- text-transform: uppercase;
1643
- letter-spacing: 0.06em;
1644
- color: var(--text-soft);
1645
  }
1646
 
1647
- .marketplace-meta-card dd {
1648
- margin: 4px 0 0;
1649
- font-size: 12px;
1650
  }
1651
 
1652
- .marketplace-meta-card details {
1653
- margin-top: 8px;
 
1654
  }
1655
 
1656
- .marketplace-meta-card details summary {
1657
- cursor: pointer;
1658
- font-size: 12px;
1659
- color: var(--accent-2);
1660
  }
1661
 
1662
- .marketplace-meta-card pre {
1663
- margin: 8px 0 0;
1664
- border-radius: 10px;
1665
- background: rgba(16, 26, 19, 0.88);
1666
- color: #d8f7d4;
1667
- font-family: 'IBM Plex Mono', monospace;
1668
- font-size: 11px;
1669
- padding: 9px;
1670
- overflow: auto;
1671
  }
1672
 
1673
- .voice-demo-root {
1674
- flex: 1;
1675
- min-height: 0;
1676
  display: flex;
1677
- position: relative;
1678
- padding: 12px;
 
 
 
 
 
 
 
 
 
 
 
 
1679
  }
1680
 
1681
  .voice-launcher-wrap {
@@ -2254,10 +3685,38 @@ textarea {
2254
  align-items: stretch;
2255
  }
2256
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2257
  .voice-demo-root {
2258
  padding: 8px 6px;
2259
  }
2260
 
 
 
 
 
 
 
 
 
 
 
2261
  .voice-dialog-header {
2262
  padding: 12px;
2263
  }
@@ -2270,6 +3729,20 @@ textarea {
2270
  grid-template-columns: repeat(2, minmax(0, 1fr));
2271
  }
2272
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2273
  .marketing-studio-root {
2274
  padding: 8px 6px;
2275
  gap: 8px;
@@ -2326,6 +3799,43 @@ textarea {
2326
  }
2327
 
2328
  @media (max-width: 620px) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2329
  .invoice-metric-grid,
2330
  .invoice-preview-grid,
2331
  .invoice-meta-card dl {
@@ -2337,6 +3847,28 @@ textarea {
2337
  }
2338
  }
2339
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2340
  @keyframes pulse {
2341
  0%,
2342
  100% {
@@ -2389,3 +3921,104 @@ textarea {
2389
  transform: translateY(0);
2390
  }
2391
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
341
  animation: spin 0.8s linear infinite;
342
  }
343
 
344
+ .chat-demo-video {
345
+ margin: 12px;
346
+ padding: 12px;
347
+ border-radius: 16px;
348
+ border: 1px solid rgba(31, 143, 78, 0.28);
349
+ background: linear-gradient(155deg, rgba(244, 252, 247, 0.95), rgba(230, 245, 251, 0.92));
350
+ display: grid;
351
+ gap: 10px;
352
+ animation: slide-in 220ms ease;
353
+ }
354
+
355
+ .chat-demo-head {
356
+ display: flex;
357
+ justify-content: space-between;
358
+ align-items: flex-start;
359
+ gap: 10px;
360
+ flex-wrap: wrap;
361
+ }
362
+
363
+ .chat-demo-head h3 {
364
+ margin: 0;
365
+ font-size: 15px;
366
+ font-family: 'Sora', 'IBM Plex Sans', sans-serif;
367
+ }
368
+
369
+ .chat-demo-head p {
370
+ margin: 4px 0 0;
371
+ color: var(--text-soft);
372
+ font-size: 12px;
373
+ }
374
+
375
+ .chat-demo-head small {
376
+ display: block;
377
+ margin-top: 5px;
378
+ color: var(--text-soft);
379
+ font-size: 11px;
380
+ }
381
+
382
+ .chat-demo-head-actions {
383
+ display: flex;
384
+ gap: 8px;
385
+ align-items: center;
386
+ flex-wrap: wrap;
387
+ }
388
+
389
+ .chat-demo-state {
390
+ border-radius: 999px;
391
+ padding: 5px 10px;
392
+ border: 1px solid rgba(31, 143, 78, 0.25);
393
+ background: rgba(255, 255, 255, 0.82);
394
+ font-size: 11px;
395
+ font-family: 'IBM Plex Mono', monospace;
396
+ color: #176236;
397
+ }
398
+
399
+ .chat-demo-state.playing {
400
+ border-color: rgba(31, 143, 78, 0.38);
401
+ }
402
+
403
+ .chat-demo-state.paused {
404
+ color: #0f566d;
405
+ border-color: rgba(15, 109, 139, 0.32);
406
+ }
407
+
408
+ .chat-demo-state.busy {
409
+ color: #8a540a;
410
+ border-color: rgba(202, 122, 18, 0.45);
411
+ background: rgba(255, 247, 234, 0.95);
412
+ }
413
+
414
+ .chat-demo-frame {
415
+ border-radius: 14px;
416
+ border: 1px solid rgba(19, 56, 30, 0.3);
417
+ background: linear-gradient(170deg, rgba(14, 26, 18, 0.96), rgba(22, 39, 31, 0.96));
418
+ overflow: hidden;
419
+ }
420
+
421
+ .chat-demo-windowbar {
422
+ display: flex;
423
+ align-items: center;
424
+ gap: 5px;
425
+ border-bottom: 1px solid rgba(255, 255, 255, 0.12);
426
+ padding: 8px 10px;
427
+ }
428
+
429
+ .chat-demo-windowbar > span {
430
+ width: 9px;
431
+ height: 9px;
432
+ border-radius: 999px;
433
+ display: block;
434
+ }
435
+
436
+ .chat-demo-windowbar > span:nth-child(1) {
437
+ background: #f87171;
438
+ }
439
+
440
+ .chat-demo-windowbar > span:nth-child(2) {
441
+ background: #fbbf24;
442
+ }
443
+
444
+ .chat-demo-windowbar > span:nth-child(3) {
445
+ background: #4ade80;
446
+ }
447
+
448
+ .chat-demo-windowbar small {
449
+ margin-left: 4px;
450
+ color: rgba(235, 248, 238, 0.8);
451
+ font-family: 'IBM Plex Mono', monospace;
452
+ font-size: 10px;
453
+ }
454
+
455
+ .chat-demo-canvas {
456
+ position: relative;
457
+ display: grid;
458
+ grid-template-columns: repeat(3, minmax(0, 1fr));
459
+ gap: 8px;
460
+ padding: 10px;
461
+ min-height: 172px;
462
+ }
463
+
464
+ .chat-demo-lane {
465
+ border-radius: 11px;
466
+ border: 1px solid rgba(184, 222, 193, 0.28);
467
+ background: linear-gradient(150deg, rgba(255, 255, 255, 0.08), rgba(205, 236, 230, 0.08));
468
+ padding: 9px;
469
+ display: grid;
470
+ gap: 7px;
471
+ align-content: start;
472
+ transition: transform 170ms ease, border-color 170ms ease, box-shadow 170ms ease;
473
+ }
474
+
475
+ .chat-demo-lane h4 {
476
+ margin: 0;
477
+ font-size: 11px;
478
+ letter-spacing: 0.08em;
479
+ text-transform: uppercase;
480
+ color: rgba(218, 239, 226, 0.86);
481
+ }
482
+
483
+ .chat-demo-prompt-chip {
484
+ width: fit-content;
485
+ border-radius: 999px;
486
+ border: 1px solid rgba(127, 211, 190, 0.4);
487
+ background: rgba(47, 124, 111, 0.35);
488
+ color: #dbf8ef;
489
+ font-size: 10px;
490
+ padding: 3px 8px;
491
+ }
492
+
493
+ .chat-demo-prompt-bubble {
494
+ position: relative;
495
+ border-radius: 9px;
496
+ border: 1px solid rgba(165, 222, 196, 0.32);
497
+ background: rgba(20, 62, 44, 0.8);
498
+ color: #e8fdee;
499
+ padding: 8px;
500
+ font-size: 11px;
501
+ line-height: 1.35;
502
+ min-height: 63px;
503
+ }
504
+
505
+ .chat-demo-prompt-bubble::after {
506
+ content: '';
507
+ position: absolute;
508
+ left: 8px;
509
+ right: 8px;
510
+ bottom: 6px;
511
+ height: 2px;
512
+ border-radius: 999px;
513
+ background: rgba(186, 243, 207, 0.66);
514
+ transform-origin: left;
515
+ animation: chat-demo-type 1.6s ease-in-out infinite;
516
+ }
517
+
518
+ .chat-demo-step {
519
+ border-radius: 8px;
520
+ border: 1px solid rgba(157, 202, 231, 0.26);
521
+ background: rgba(25, 54, 72, 0.65);
522
+ color: #d8edf9;
523
+ font-size: 10px;
524
+ padding: 6px;
525
+ }
526
+
527
+ .chat-demo-step.active {
528
+ border-color: rgba(142, 216, 255, 0.55);
529
+ box-shadow: inset 0 0 0 1px rgba(140, 214, 255, 0.25);
530
+ }
531
+
532
+ .chat-demo-output-card {
533
+ border-radius: 9px;
534
+ border: 1px solid rgba(174, 220, 185, 0.25);
535
+ background: rgba(34, 60, 43, 0.8);
536
+ color: #dcf7e2;
537
+ padding: 8px;
538
+ }
539
+
540
+ .chat-demo-output-card strong {
541
+ display: block;
542
+ font-size: 10px;
543
+ }
544
+
545
+ .chat-demo-output-card p {
546
+ margin: 5px 0 0;
547
+ font-size: 11px;
548
+ line-height: 1.3;
549
+ }
550
+
551
+ .chat-demo-cursor {
552
+ position: absolute;
553
+ width: 14px;
554
+ height: 14px;
555
+ border-radius: 999px;
556
+ border: 2px solid rgba(255, 255, 255, 0.95);
557
+ background: rgba(31, 143, 78, 0.7);
558
+ box-shadow: 0 0 0 3px rgba(31, 143, 78, 0.2);
559
+ left: 14%;
560
+ top: 22%;
561
+ animation: chat-demo-cursor-path 5.2s linear infinite;
562
+ pointer-events: none;
563
+ }
564
+
565
+ .chat-demo-frame[data-scene='prompt'] .chat-demo-lane.prompts,
566
+ .chat-demo-frame[data-scene='workflow'] .chat-demo-lane.workflow,
567
+ .chat-demo-frame[data-scene='output'] .chat-demo-lane.output {
568
+ border-color: rgba(136, 239, 175, 0.58);
569
+ transform: translateY(-2px);
570
+ box-shadow: 0 0 0 1px rgba(136, 239, 175, 0.22);
571
+ }
572
+
573
+ .chat-demo-meta {
574
+ display: grid;
575
+ gap: 7px;
576
+ }
577
+
578
+ .chat-demo-caption strong {
579
+ display: block;
580
+ font-size: 13px;
581
+ }
582
+
583
+ .chat-demo-caption p {
584
+ margin: 4px 0 0;
585
+ color: var(--text-soft);
586
+ font-size: 12px;
587
+ }
588
+
589
+ .chat-demo-progress-track {
590
+ height: 7px;
591
+ border-radius: 999px;
592
+ border: 1px solid rgba(31, 143, 78, 0.26);
593
+ background: rgba(255, 255, 255, 0.66);
594
+ overflow: hidden;
595
+ }
596
+
597
+ .chat-demo-progress-track span {
598
+ display: block;
599
+ height: 100%;
600
+ border-radius: inherit;
601
+ background: linear-gradient(90deg, rgba(31, 143, 78, 0.9), rgba(15, 109, 139, 0.9));
602
+ transition: width 120ms linear;
603
+ }
604
+
605
+ .chat-demo-scene-row {
606
+ display: flex;
607
+ flex-wrap: wrap;
608
+ gap: 6px;
609
+ }
610
+
611
+ .chat-demo-scene-pill {
612
+ border: 1px solid rgba(15, 109, 139, 0.25);
613
+ background: rgba(15, 109, 139, 0.08);
614
+ color: #12526a;
615
+ border-radius: 999px;
616
+ padding: 5px 9px;
617
+ font-size: 11px;
618
+ cursor: pointer;
619
+ }
620
+
621
+ .chat-demo-scene-pill.active {
622
+ color: #145e38;
623
+ border-color: rgba(31, 143, 78, 0.45);
624
+ background: rgba(31, 143, 78, 0.16);
625
+ }
626
+
627
+ .chat-demo-scene-pill.complete {
628
+ border-color: rgba(31, 143, 78, 0.25);
629
+ background: rgba(31, 143, 78, 0.08);
630
+ color: #1f6e43;
631
+ }
632
+
633
+ .chat-demo-collapsed {
634
+ margin: 12px;
635
+ border-radius: 14px;
636
+ border: 1px dashed rgba(31, 143, 78, 0.35);
637
+ background: rgba(244, 252, 247, 0.86);
638
+ padding: 10px 12px;
639
+ display: flex;
640
+ align-items: center;
641
+ justify-content: space-between;
642
+ gap: 10px;
643
+ }
644
+
645
+ .chat-demo-collapsed p {
646
+ margin: 0;
647
+ color: var(--text-soft);
648
+ font-size: 12px;
649
+ }
650
+
651
  .thread {
652
  flex: 1;
653
  min-height: 0;
 
819
  gap: 10px;
820
  }
821
 
822
+ .marketing-walk-video,
823
+ .invoice-walk-video,
824
+ .market-walk-video {
825
+ border-radius: 16px;
826
+ border: 1px solid rgba(31, 143, 78, 0.24);
827
+ background: linear-gradient(155deg, rgba(244, 252, 247, 0.94), rgba(230, 245, 251, 0.9));
828
+ padding: 10px;
829
+ display: grid;
830
+ gap: 8px;
831
+ animation: slide-in 220ms ease;
832
  }
833
 
834
+ .marketing-walk-head,
835
+ .invoice-walk-head,
836
+ .market-walk-head {
 
 
837
  display: flex;
838
  justify-content: space-between;
839
  align-items: center;
840
+ gap: 8px;
841
  }
842
 
843
+ .marketing-walk-head h3,
844
+ .invoice-walk-head h3,
845
+ .market-walk-head h3 {
846
  margin: 0;
847
+ font-size: 13px;
848
  font-family: 'Sora', 'IBM Plex Sans', sans-serif;
849
  }
850
 
851
+ .marketing-walk-head small,
852
+ .invoice-walk-head small,
853
+ .market-walk-head small {
854
+ margin-left: 6px;
855
  color: var(--text-soft);
856
+ font-size: 10px;
857
+ font-family: 'IBM Plex Mono', monospace;
858
  }
859
 
860
+ .marketing-walk-actions,
861
+ .invoice-walk-actions,
862
+ .market-walk-actions {
863
+ display: flex;
864
+ gap: 6px;
865
+ }
866
+
867
+ .marketing-walk-icon-btn,
868
+ .invoice-walk-icon-btn,
869
+ .market-walk-icon-btn {
870
+ width: 28px;
871
+ height: 28px;
872
+ border-radius: 999px;
873
+ border: 1px solid rgba(19, 67, 36, 0.28);
874
+ background: rgba(255, 255, 255, 0.82);
875
+ color: #1c5f39;
876
  display: grid;
877
+ place-items: center;
878
+ cursor: pointer;
879
+ font-family: 'IBM Plex Mono', monospace;
880
+ font-size: 11px;
881
  }
882
 
883
+ .marketing-walk-icon-btn:disabled,
884
+ .invoice-walk-icon-btn:disabled,
885
+ .market-walk-icon-btn:disabled {
886
+ opacity: 0.45;
887
+ cursor: not-allowed;
 
888
  }
889
 
890
+ .marketing-walk-frame,
891
+ .invoice-walk-frame,
892
+ .market-walk-frame {
893
+ border-radius: 14px;
894
+ border: 1px solid rgba(19, 56, 30, 0.3);
895
+ overflow: hidden;
896
  }
897
 
898
+ .marketing-walk-frame {
899
+ background: linear-gradient(170deg, rgba(17, 29, 21, 0.96), rgba(29, 43, 34, 0.96));
 
 
 
 
900
  }
901
 
902
+ .invoice-walk-frame {
903
+ background: linear-gradient(170deg, rgba(16, 24, 29, 0.96), rgba(21, 36, 41, 0.96));
 
 
904
  }
905
 
906
+ .market-walk-frame {
907
+ background: linear-gradient(170deg, rgba(18, 26, 24, 0.96), rgba(23, 39, 34, 0.96));
 
 
908
  }
909
 
910
+ .marketing-walk-windowbar,
911
+ .invoice-walk-windowbar,
912
+ .market-walk-windowbar {
913
+ display: flex;
914
+ align-items: center;
915
+ gap: 5px;
916
+ border-bottom: 1px solid rgba(255, 255, 255, 0.12);
917
+ padding: 8px 10px;
918
  }
919
 
920
+ .marketing-walk-windowbar > span,
921
+ .invoice-walk-windowbar > span,
922
+ .market-walk-windowbar > span {
923
+ width: 9px;
924
+ height: 9px;
925
+ border-radius: 999px;
926
+ display: block;
927
  }
928
 
929
+ .marketing-walk-windowbar > span:nth-child(1),
930
+ .invoice-walk-windowbar > span:nth-child(1),
931
+ .market-walk-windowbar > span:nth-child(1) {
932
+ background: #f87171;
933
  }
934
 
935
+ .marketing-walk-windowbar > span:nth-child(2),
936
+ .invoice-walk-windowbar > span:nth-child(2),
937
+ .market-walk-windowbar > span:nth-child(2) {
938
+ background: #fbbf24;
939
  }
940
 
941
+ .marketing-walk-windowbar > span:nth-child(3),
942
+ .invoice-walk-windowbar > span:nth-child(3),
943
+ .market-walk-windowbar > span:nth-child(3) {
944
+ background: #4ade80;
945
  }
946
 
947
+ .marketing-walk-windowbar small,
948
+ .invoice-walk-windowbar small,
949
+ .market-walk-windowbar small {
950
+ margin-left: 4px;
951
+ color: rgba(235, 248, 238, 0.8);
952
+ font-family: 'IBM Plex Mono', monospace;
953
+ font-size: 10px;
 
 
 
 
954
  }
955
 
956
+ .marketing-walk-canvas,
957
+ .invoice-walk-canvas,
958
+ .market-walk-canvas {
959
+ display: grid;
960
+ grid-template-columns: repeat(3, minmax(0, 1fr));
961
+ gap: 7px;
962
+ padding: 9px;
963
+ min-height: 154px;
964
  }
965
 
966
+ .marketing-walk-lane,
967
+ .invoice-walk-lane,
968
+ .market-walk-lane {
969
+ border-radius: 11px;
970
+ border: 1px solid rgba(184, 222, 193, 0.28);
971
+ background: linear-gradient(150deg, rgba(255, 255, 255, 0.08), rgba(205, 236, 230, 0.08));
972
+ padding: 8px;
973
+ display: grid;
974
+ gap: 6px;
975
+ align-content: start;
976
+ transition: transform 170ms ease, border-color 170ms ease, box-shadow 170ms ease;
977
  }
978
 
979
+ .marketing-walk-lane-index,
980
+ .invoice-walk-lane-index,
981
+ .market-walk-lane-index {
982
+ width: 18px;
983
+ height: 18px;
984
+ border-radius: 999px;
985
+ border: 1px solid rgba(223, 248, 232, 0.45);
986
+ background: rgba(255, 255, 255, 0.08);
987
+ color: rgba(225, 246, 235, 0.88);
988
+ font-size: 10px;
989
+ font-family: 'IBM Plex Mono', monospace;
990
+ display: grid;
991
+ place-items: center;
992
  }
993
 
994
+ .marketing-walk-lane-label,
995
+ .invoice-walk-lane-label,
996
+ .market-walk-lane-label {
997
+ font-size: 10px;
998
+ letter-spacing: 0.08em;
999
+ text-transform: uppercase;
1000
+ color: rgba(214, 241, 226, 0.86);
1001
  }
1002
 
1003
+ .marketing-walk-chip-row,
1004
+ .invoice-walk-chip-row,
1005
+ .market-walk-chip-row {
1006
+ display: flex;
1007
+ flex-wrap: wrap;
1008
+ gap: 4px;
1009
  }
1010
 
1011
+ .marketing-walk-chip-row span,
1012
+ .invoice-walk-chip-row span,
1013
+ .market-walk-chip-row span {
1014
+ border: 1px solid rgba(145, 216, 236, 0.36);
1015
+ padding: 2px 6px;
1016
+ font-size: 9px;
1017
+ color: rgba(197, 231, 247, 0.92);
1018
+ line-height: 1.25;
1019
+ border-radius: 999px;
1020
+ background: rgba(145, 216, 236, 0.18);
1021
  }
1022
 
1023
+ .marketing-walk-line,
1024
+ .invoice-walk-line,
1025
+ .market-walk-line {
1026
+ height: 8px;
1027
+ border-radius: 999px;
1028
+ background: linear-gradient(90deg, rgba(171, 228, 247, 0.33), rgba(217, 245, 228, 0.45), rgba(171, 228, 247, 0.33));
1029
+ background-size: 160% 100%;
1030
+ animation: chat-demo-shimmer 1.8s linear infinite;
1031
  }
1032
 
1033
+ .marketing-walk-line.lg,
1034
+ .invoice-walk-line.lg,
1035
+ .market-walk-line.lg {
1036
+ width: 94%;
 
1037
  }
1038
 
1039
+ .marketing-walk-line.md,
1040
+ .invoice-walk-line.md,
1041
+ .market-walk-line.md {
1042
+ width: 72%;
 
 
1043
  }
1044
 
1045
+ .marketing-walk-line.sm,
1046
+ .invoice-walk-line.sm,
1047
+ .market-walk-line.sm {
1048
+ width: 50%;
 
1049
  }
1050
 
1051
+ .marketing-walk-lane p,
1052
+ .invoice-walk-lane p,
1053
+ .market-walk-lane p {
1054
  margin: 0;
1055
+ font-size: 10px;
1056
+ line-height: 1.35;
1057
+ color: rgba(206, 232, 220, 0.9);
1058
  }
1059
 
1060
+ .marketing-walk-copy-card {
1061
+ border-radius: 9px;
1062
+ border: 1px solid rgba(174, 220, 185, 0.25);
1063
+ background: rgba(34, 60, 43, 0.8);
1064
+ padding: 7px;
1065
+ display: grid;
1066
+ gap: 5px;
1067
+ }
1068
+
1069
+ .marketing-walk-stream-pill {
1070
+ height: 14px;
1071
+ border-radius: 999px;
1072
+ border: 1px solid rgba(145, 216, 236, 0.3);
1073
+ background: linear-gradient(90deg, rgba(171, 228, 247, 0.24), rgba(217, 245, 228, 0.4), rgba(171, 228, 247, 0.24));
1074
+ background-size: 160% 100%;
1075
+ animation: chat-demo-shimmer 1.6s linear infinite;
1076
  display: flex;
1077
+ align-items: center;
1078
+ padding: 0 8px;
1079
  }
1080
 
1081
+ .marketing-walk-stream-pill span {
1082
+ font-size: 9px;
1083
+ color: rgba(216, 242, 226, 0.95);
1084
+ letter-spacing: 0.02em;
 
 
1085
  }
1086
 
1087
+ .marketing-walk-stream-pill.lg {
1088
+ width: 94%;
 
 
1089
  }
1090
 
1091
+ .marketing-walk-stream-pill.md {
1092
+ width: 72%;
 
 
1093
  }
1094
 
1095
+ .marketing-walk-stream-pill.sm {
1096
+ width: 50%;
 
 
1097
  }
1098
 
1099
+ .marketing-walk-visual-grid {
1100
+ display: grid;
1101
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1102
+ gap: 6px;
 
1103
  }
1104
 
1105
+ .marketing-walk-visual-grid article {
1106
+ border: 1px solid rgba(174, 220, 185, 0.25);
1107
+ border-radius: 9px;
1108
+ background: rgba(34, 60, 43, 0.8);
1109
+ padding: 6px;
1110
+ display: grid;
1111
+ gap: 4px;
1112
  }
1113
 
1114
+ .marketing-walk-visual-grid article img {
1115
+ width: 100%;
1116
+ height: 38px;
1117
+ display: block;
1118
+ object-fit: cover;
1119
+ border-radius: 7px;
1120
+ border: 1px solid rgba(145, 216, 236, 0.28);
1121
  }
1122
 
1123
+ .marketing-walk-visual-grid article small {
1124
+ font-size: 9px;
1125
+ color: rgba(206, 232, 220, 0.9);
 
 
 
 
 
 
 
1126
  }
1127
 
1128
+ .invoice-walk-dropzone {
1129
+ border: 1px dashed rgba(174, 220, 185, 0.28);
1130
+ border-radius: 9px;
1131
+ background: rgba(34, 60, 43, 0.72);
1132
+ padding: 8px;
1133
  display: grid;
1134
+ gap: 5px;
 
1135
  }
1136
 
1137
+ .invoice-walk-dropzone img {
1138
+ width: 100%;
1139
+ height: 56px;
1140
+ display: block;
1141
+ object-fit: cover;
1142
+ border-radius: 7px;
1143
+ border: 1px solid rgba(145, 216, 236, 0.28);
1144
  }
1145
 
1146
+ .invoice-walk-dropzone p {
1147
  margin: 0;
1148
+ font-size: 9px;
1149
+ color: rgba(206, 232, 220, 0.88);
 
 
 
 
 
 
 
 
1150
  }
1151
 
1152
+ .invoice-walk-ocr-nodes {
1153
+ display: grid;
1154
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1155
+ gap: 6px;
 
 
 
 
1156
  }
1157
 
1158
+ .invoice-walk-ocr-nodes span {
1159
+ min-height: 18px;
1160
  display: grid;
1161
+ place-items: center;
1162
+ padding: 2px 4px;
1163
+ font-size: 9px;
1164
+ line-height: 1.2;
1165
+ color: rgba(209, 235, 249, 0.92);
1166
+ border-radius: 7px;
1167
+ border: 1px solid rgba(157, 202, 231, 0.3);
1168
+ background: rgba(25, 54, 72, 0.66);
1169
  }
1170
 
1171
+ .invoice-walk-ocr-flow {
1172
+ height: 9px;
1173
+ border-radius: 999px;
1174
+ border: 1px solid rgba(136, 239, 175, 0.3);
1175
+ background: rgba(21, 48, 33, 0.85);
1176
  overflow: hidden;
1177
  }
1178
 
1179
+ .invoice-walk-ocr-flow span {
 
 
 
1180
  display: block;
1181
+ height: 100%;
1182
+ width: 44%;
1183
+ border-radius: inherit;
1184
+ background: linear-gradient(90deg, rgba(136, 239, 175, 0.25), rgba(136, 239, 175, 0.9));
1185
+ animation: invoice-walk-flow 1.7s ease-in-out infinite;
1186
  }
1187
 
1188
+ .invoice-walk-ocr-lines {
 
1189
  display: grid;
1190
+ gap: 4px;
1191
  }
1192
 
1193
+ .invoice-walk-stream-pill {
1194
+ height: 14px;
1195
+ border-radius: 999px;
1196
+ border: 1px solid rgba(145, 216, 236, 0.3);
1197
+ background: linear-gradient(90deg, rgba(171, 228, 247, 0.24), rgba(217, 245, 228, 0.4), rgba(171, 228, 247, 0.24));
1198
+ background-size: 160% 100%;
1199
+ animation: chat-demo-shimmer 1.6s linear infinite;
1200
+ display: flex;
1201
+ align-items: center;
1202
+ padding: 0 8px;
1203
  }
1204
 
1205
+ .invoice-walk-stream-pill span {
1206
+ font-size: 9px;
1207
+ color: rgba(216, 242, 226, 0.95);
1208
+ letter-spacing: 0.02em;
1209
  }
1210
 
1211
+ .invoice-walk-stream-pill.lg {
1212
+ width: 94%;
 
 
 
1213
  }
1214
 
1215
+ .invoice-walk-stream-pill.md {
1216
+ width: 72%;
 
 
1217
  }
1218
 
1219
+ .invoice-walk-stream-pill.sm {
1220
+ width: 50%;
1221
+ }
1222
+
1223
+ .invoice-walk-field-grid {
1224
  display: grid;
1225
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1226
+ gap: 4px;
1227
  }
1228
 
1229
+ .invoice-walk-field-grid article {
1230
+ min-height: 36px;
1231
+ display: grid;
1232
+ align-content: center;
1233
+ justify-items: center;
1234
+ gap: 2px;
1235
+ padding: 3px;
1236
+ border-radius: 7px;
1237
+ background: rgba(191, 245, 208, 0.24);
1238
+ border: 1px solid rgba(191, 245, 208, 0.3);
1239
  }
1240
 
1241
+ .invoice-walk-field-grid article small {
1242
+ font-size: 8px;
1243
+ color: rgba(205, 238, 217, 0.86);
 
 
1244
  }
1245
 
1246
+ .invoice-walk-field-grid article strong {
1247
+ font-size: 10px;
1248
+ color: rgba(230, 249, 237, 0.95);
1249
  }
1250
 
1251
+ .market-walk-rank-row {
1252
+ display: grid;
1253
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1254
+ gap: 6px;
1255
+ }
1256
+
1257
+ .market-walk-rank-row span {
1258
+ min-height: 18px;
1259
+ display: grid;
1260
+ place-items: center;
1261
+ padding: 2px 4px;
1262
+ font-size: 9px;
1263
+ line-height: 1.2;
1264
+ color: rgba(209, 235, 249, 0.92);
1265
+ border-radius: 7px;
1266
+ border: 1px solid rgba(157, 202, 231, 0.3);
1267
+ background: rgba(25, 54, 72, 0.66);
1268
+ }
1269
+
1270
+ .market-walk-rank-flow {
1271
+ height: 9px;
1272
+ border-radius: 999px;
1273
+ border: 1px solid rgba(136, 239, 175, 0.3);
1274
+ background: rgba(21, 48, 33, 0.85);
1275
+ overflow: hidden;
1276
+ }
1277
+
1278
+ .market-walk-rank-flow span {
1279
+ display: block;
1280
+ height: 100%;
1281
+ width: 44%;
1282
+ border-radius: inherit;
1283
+ background: linear-gradient(90deg, rgba(136, 239, 175, 0.25), rgba(136, 239, 175, 0.9));
1284
+ animation: market-walk-flow 1.7s ease-in-out infinite;
1285
+ }
1286
+
1287
+ .market-walk-result-grid {
1288
+ display: grid;
1289
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1290
+ gap: 4px;
1291
+ }
1292
+
1293
+ .market-walk-result-grid article {
1294
+ min-height: 36px;
1295
+ display: grid;
1296
+ align-content: center;
1297
+ justify-items: center;
1298
+ gap: 2px;
1299
+ padding: 3px;
1300
+ border-radius: 7px;
1301
+ background: rgba(191, 245, 208, 0.24);
1302
+ border: 1px solid rgba(191, 245, 208, 0.3);
1303
+ }
1304
+
1305
+ .market-walk-result-grid article small {
1306
+ font-size: 8px;
1307
+ color: rgba(205, 238, 217, 0.86);
1308
+ }
1309
+
1310
+ .market-walk-result-grid article strong {
1311
+ font-size: 10px;
1312
+ color: rgba(230, 249, 237, 0.95);
1313
+ }
1314
+
1315
+ .market-walk-query-thumb {
1316
+ width: 100%;
1317
+ height: 46px;
1318
+ display: block;
1319
+ object-fit: cover;
1320
+ border-radius: 7px;
1321
+ border: 1px solid rgba(145, 216, 236, 0.28);
1322
+ }
1323
+
1324
+ .market-walk-stream-pill {
1325
+ height: 14px;
1326
+ border-radius: 999px;
1327
+ border: 1px solid rgba(145, 216, 236, 0.3);
1328
+ background: linear-gradient(90deg, rgba(171, 228, 247, 0.24), rgba(217, 245, 228, 0.4), rgba(171, 228, 247, 0.24));
1329
+ background-size: 160% 100%;
1330
+ animation: chat-demo-shimmer 1.6s linear infinite;
1331
+ display: flex;
1332
+ align-items: center;
1333
+ padding: 0 8px;
1334
+ }
1335
+
1336
+ .market-walk-stream-pill span {
1337
+ font-size: 9px;
1338
+ color: rgba(216, 242, 226, 0.95);
1339
+ letter-spacing: 0.02em;
1340
+ }
1341
+
1342
+ .market-walk-stream-pill.lg {
1343
+ width: 94%;
1344
+ }
1345
+
1346
+ .market-walk-stream-pill.md {
1347
+ width: 72%;
1348
+ }
1349
+
1350
+ .market-walk-stream-pill.sm {
1351
+ width: 50%;
1352
+ }
1353
+
1354
+ .marketing-walk-frame[data-scene='brief'] .marketing-walk-lane.brief,
1355
+ .marketing-walk-frame[data-scene='copy'] .marketing-walk-lane.copy,
1356
+ .marketing-walk-frame[data-scene='visual'] .marketing-walk-lane.visual,
1357
+ .invoice-walk-frame[data-scene='source'] .invoice-walk-lane.source,
1358
+ .invoice-walk-frame[data-scene='ocr'] .invoice-walk-lane.ocr,
1359
+ .invoice-walk-frame[data-scene='fields'] .invoice-walk-lane.fields,
1360
+ .market-walk-frame[data-scene='query'] .market-walk-lane.query,
1361
+ .market-walk-frame[data-scene='rank'] .market-walk-lane.rank,
1362
+ .market-walk-frame[data-scene='match'] .market-walk-lane.match {
1363
+ border-color: rgba(136, 239, 175, 0.58);
1364
+ transform: translateY(-2px);
1365
+ box-shadow: 0 0 0 1px rgba(136, 239, 175, 0.22);
1366
+ }
1367
+
1368
+ .marketing-walk-meta,
1369
+ .invoice-walk-meta,
1370
+ .market-walk-meta {
1371
+ display: grid;
1372
+ gap: 7px;
1373
+ }
1374
+
1375
+ .marketing-walk-caption,
1376
+ .invoice-walk-caption,
1377
+ .market-walk-caption {
1378
+ margin: 0;
1379
+ font-size: 11px;
1380
+ color: var(--text-soft);
1381
+ text-align: center;
1382
+ }
1383
+
1384
+ .marketing-walk-progress-track,
1385
+ .invoice-walk-progress-track,
1386
+ .market-walk-progress-track {
1387
+ height: 7px;
1388
+ border-radius: 999px;
1389
+ border: 1px solid rgba(31, 143, 78, 0.26);
1390
+ background: rgba(255, 255, 255, 0.66);
1391
+ overflow: hidden;
1392
+ }
1393
+
1394
+ .marketing-walk-progress-track span,
1395
+ .invoice-walk-progress-track span,
1396
+ .market-walk-progress-track span {
1397
+ display: block;
1398
+ height: 100%;
1399
+ border-radius: inherit;
1400
+ background: linear-gradient(90deg, rgba(31, 143, 78, 0.9), rgba(15, 109, 139, 0.9));
1401
+ transition: width 120ms linear;
1402
+ }
1403
+
1404
+ .marketing-walk-scene-dots,
1405
+ .invoice-walk-scene-dots,
1406
+ .market-walk-scene-dots {
1407
+ display: flex;
1408
+ justify-content: center;
1409
+ gap: 6px;
1410
+ }
1411
+
1412
+ .marketing-walk-scene-dot,
1413
+ .invoice-walk-scene-dot,
1414
+ .market-walk-scene-dot {
1415
+ width: 20px;
1416
+ height: 20px;
1417
+ padding: 0;
1418
+ border-radius: 999px;
1419
+ border: 1px solid rgba(15, 109, 139, 0.25);
1420
+ background: rgba(15, 109, 139, 0.08);
1421
+ display: grid;
1422
+ place-items: center;
1423
+ cursor: pointer;
1424
+ }
1425
+
1426
+ .marketing-walk-scene-dot span,
1427
+ .invoice-walk-scene-dot span,
1428
+ .market-walk-scene-dot span {
1429
+ width: 6px;
1430
+ height: 6px;
1431
+ border-radius: 999px;
1432
+ background: #589db6;
1433
+ }
1434
+
1435
+ .marketing-walk-scene-dot.active,
1436
+ .invoice-walk-scene-dot.active,
1437
+ .market-walk-scene-dot.active {
1438
+ border-color: rgba(31, 143, 78, 0.45);
1439
+ background: rgba(31, 143, 78, 0.16);
1440
+ }
1441
+
1442
+ .marketing-walk-scene-dot.active span,
1443
+ .invoice-walk-scene-dot.active span,
1444
+ .market-walk-scene-dot.active span {
1445
+ background: #1f8f4e;
1446
+ }
1447
+
1448
+ .marketing-walk-scene-dot.complete,
1449
+ .invoice-walk-scene-dot.complete,
1450
+ .market-walk-scene-dot.complete {
1451
+ border-color: rgba(31, 143, 78, 0.25);
1452
+ background: rgba(31, 143, 78, 0.08);
1453
+ }
1454
+
1455
+ .marketing-walk-scene-dot.complete span,
1456
+ .invoice-walk-scene-dot.complete span,
1457
+ .market-walk-scene-dot.complete span {
1458
+ background: #2f9b5b;
1459
+ }
1460
+
1461
+ .marketing-walk-collapsed,
1462
+ .invoice-walk-collapsed,
1463
+ .market-walk-collapsed {
1464
+ border-radius: 14px;
1465
+ border: 1px dashed rgba(31, 143, 78, 0.35);
1466
+ background: rgba(244, 252, 247, 0.86);
1467
+ padding: 10px 12px;
1468
+ display: flex;
1469
+ align-items: center;
1470
+ justify-content: space-between;
1471
+ gap: 10px;
1472
+ }
1473
+
1474
+ .marketing-walk-mini-markers,
1475
+ .invoice-walk-mini-markers,
1476
+ .market-walk-mini-markers {
1477
+ display: flex;
1478
+ gap: 4px;
1479
+ }
1480
+
1481
+ .marketing-walk-mini-markers span,
1482
+ .invoice-walk-mini-markers span,
1483
+ .market-walk-mini-markers span {
1484
+ width: 8px;
1485
+ height: 8px;
1486
+ border-radius: 999px;
1487
+ background: rgba(31, 143, 78, 0.35);
1488
+ }
1489
+
1490
+ .marketing-walk-collapsed p,
1491
+ .invoice-walk-collapsed p,
1492
+ .market-walk-collapsed p {
1493
+ margin: 0;
1494
+ color: var(--text-soft);
1495
+ font-size: 12px;
1496
+ }
1497
+
1498
+ .marketing-studio-root {
1499
  flex: 1;
1500
  min-height: 0;
1501
  display: flex;
 
1504
  gap: 12px;
1505
  }
1506
 
1507
+ .marketing-studio-header {
1508
+ border: 1px solid rgba(31, 143, 78, 0.2);
1509
  border-radius: 16px;
1510
+ background: linear-gradient(145deg, rgba(255, 255, 255, 0.9), rgba(230, 246, 237, 0.76));
1511
  padding: 13px 14px;
1512
  display: flex;
1513
  justify-content: space-between;
 
1515
  gap: 12px;
1516
  }
1517
 
1518
+ .marketing-studio-header h3 {
1519
  margin: 0;
1520
  font-size: 16px;
1521
  font-family: 'Sora', 'IBM Plex Sans', sans-serif;
1522
  }
1523
 
1524
+ .marketing-studio-header p {
1525
  margin: 4px 0 0;
1526
  font-size: 12px;
1527
  color: var(--text-soft);
1528
  }
1529
 
1530
+ .marketing-studio-layout {
1531
  flex: 1;
1532
  min-height: 0;
1533
  display: grid;
1534
+ grid-template-columns: minmax(300px, 380px) minmax(0, 1fr);
1535
  gap: 12px;
1536
  }
1537
 
1538
+ .marketing-controls,
1539
+ .marketing-output {
1540
  border: 1px solid var(--line);
1541
  border-radius: 16px;
1542
  background: rgba(255, 255, 255, 0.74);
1543
  min-height: 0;
1544
  }
1545
 
1546
+ .marketing-controls {
 
1547
  padding: 12px;
1548
+ overflow: auto;
1549
  display: grid;
1550
  align-content: start;
1551
  gap: 10px;
1552
  }
1553
 
1554
+ .marketing-output {
 
1555
  padding: 12px;
1556
+ overflow: auto;
1557
  display: grid;
1558
  align-content: start;
1559
  gap: 10px;
1560
  }
1561
 
1562
+ .marketing-output > header h4 {
1563
  margin: 0;
 
1564
  font-family: 'Sora', 'IBM Plex Sans', sans-serif;
1565
+ font-size: 15px;
1566
  }
1567
 
1568
+ .marketing-output > header p {
1569
  margin: 4px 0 0;
1570
+ color: var(--text-soft);
1571
+ font-size: 12px;
1572
+ }
1573
+
1574
+ .marketing-field-block {
1575
+ display: grid;
1576
+ gap: 7px;
1577
+ }
1578
+
1579
+ .marketing-field-block > label,
1580
+ .marketing-config-row label > span {
1581
  font-size: 12px;
1582
  color: var(--text-soft);
1583
  }
1584
 
1585
+ .marketing-config-row {
1586
  display: grid;
1587
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1588
  gap: 8px;
1589
  }
1590
 
1591
+ .marketing-config-row label {
1592
  display: grid;
1593
+ gap: 6px;
1594
  }
1595
 
1596
+ .marketing-product-grid {
1597
+ display: grid;
1598
+ gap: 8px;
1599
  }
1600
 
1601
+ .marketing-product-card {
1602
+ border: 1px solid rgba(31, 143, 78, 0.22);
1603
  border-radius: 12px;
1604
+ background: linear-gradient(145deg, rgba(255, 255, 255, 0.9), rgba(227, 243, 234, 0.7));
1605
  text-align: left;
 
1606
  color: inherit;
1607
  cursor: pointer;
1608
+ padding: 10px;
1609
  display: grid;
1610
+ gap: 2px;
1611
+ transition: 120ms transform ease, 120ms border-color ease;
1612
  }
1613
 
1614
+ .marketing-product-card:hover {
1615
  transform: translateY(-1px);
 
1616
  }
1617
 
1618
+ .marketing-product-card strong {
1619
  font-size: 13px;
1620
  }
1621
 
1622
+ .marketing-product-card small {
 
1623
  color: var(--text-soft);
1624
+ font-size: 11px;
1625
  }
1626
 
1627
+ .marketing-product-card span,
1628
+ .marketing-product-card em {
1629
+ font-size: 11px;
1630
+ color: #145d36;
1631
+ font-style: normal;
1632
  }
1633
 
1634
+ .marketing-product-card.selected {
1635
+ border-color: rgba(31, 143, 78, 0.5);
1636
+ box-shadow: inset 0 0 0 1px rgba(31, 143, 78, 0.2);
1637
+ }
1638
+
1639
+ .hidden-input {
1640
+ display: none;
1641
+ }
1642
+
1643
+ .marketing-upload-row {
1644
+ display: flex;
1645
+ gap: 8px;
1646
+ flex-wrap: wrap;
1647
+ }
1648
+
1649
+ .marketing-upload-preview {
1650
+ border: 1px solid rgba(15, 109, 139, 0.22);
1651
  border-radius: 12px;
1652
+ background: rgba(255, 255, 255, 0.85);
1653
  padding: 8px;
1654
  }
1655
 
1656
+ .marketing-upload-preview img {
1657
  width: 100%;
1658
+ max-height: 160px;
1659
  object-fit: cover;
1660
  border-radius: 9px;
1661
  display: block;
1662
  }
1663
 
1664
+ .marketing-upload-preview small {
1665
+ margin-top: 6px;
1666
+ display: block;
1667
+ font-size: 11px;
1668
+ color: var(--text-soft);
 
 
 
 
 
 
 
 
 
1669
  }
1670
 
1671
+ .marketing-help-text {
1672
  margin: 0;
 
1673
  color: var(--text-soft);
1674
+ font-size: 12px;
1675
  }
1676
 
1677
+ .marketing-action-row {
1678
+ display: flex;
1679
+ flex-wrap: wrap;
1680
+ gap: 8px;
1681
+ }
1682
+
1683
+ .marketing-empty,
1684
+ .marketing-note,
1685
+ .marketing-error {
1686
+ border-radius: 12px;
1687
+ padding: 9px 10px;
1688
+ font-size: 12px;
1689
+ }
1690
+
1691
+ .marketing-empty {
1692
+ border: 1px dashed rgba(31, 143, 78, 0.26);
1693
+ color: var(--text-soft);
1694
+ background: rgba(246, 252, 248, 0.8);
1695
+ }
1696
+
1697
+ .marketing-note {
1698
+ border: 1px solid rgba(15, 109, 139, 0.3);
1699
+ color: #0f5570;
1700
+ background: rgba(232, 247, 252, 0.84);
1701
+ }
1702
+
1703
+ .marketing-error {
1704
+ border: 1px solid rgba(166, 54, 24, 0.34);
1705
+ color: #8a2f1d;
1706
+ background: rgba(255, 236, 231, 0.88);
1707
+ }
1708
+
1709
+ .marketing-stream-card {
1710
+ border: 1px solid rgba(31, 143, 78, 0.24);
1711
+ border-radius: 14px;
1712
+ background: rgba(255, 255, 255, 0.86);
1713
+ padding: 10px;
1714
+ }
1715
+
1716
+ .marketing-stream-actions {
1717
  display: flex;
1718
  align-items: center;
1719
+ justify-content: space-between;
1720
  gap: 8px;
1721
+ margin-bottom: 8px;
1722
+ }
1723
+
1724
+ .marketing-stream-actions strong {
1725
  font-size: 12px;
 
1726
  }
1727
 
1728
+ .marketing-stream-card pre {
1729
  margin: 0;
1730
+ border-radius: 10px;
1731
+ padding: 10px;
1732
+ background: rgba(16, 26, 19, 0.87);
1733
+ color: #d7f5d4;
1734
+ font-size: 11px;
1735
+ white-space: pre-wrap;
1736
+ word-break: break-word;
1737
+ font-family: 'IBM Plex Mono', monospace;
1738
  }
1739
 
1740
+ .marketing-deck-grid {
1741
+ display: grid;
1742
+ gap: 8px;
1743
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1744
+ }
1745
+
1746
+ .marketing-deck-grid section {
1747
+ border: 1px solid rgba(15, 109, 139, 0.2);
1748
+ border-radius: 12px;
1749
+ background: rgba(255, 255, 255, 0.86);
1750
+ padding: 10px;
1751
+ }
1752
+
1753
+ .marketing-deck-grid h5 {
1754
+ margin: 0;
1755
+ font-size: 12px;
1756
+ text-transform: uppercase;
1757
+ letter-spacing: 0.05em;
1758
+ color: var(--text-soft);
1759
+ }
1760
+
1761
+ .marketing-deck-grid p {
1762
+ margin: 7px 0 0;
1763
+ font-size: 13px;
1764
+ line-height: 1.45;
1765
+ }
1766
+
1767
+ .marketing-deck-grid ul {
1768
+ margin: 7px 0 0;
1769
+ padding-left: 16px;
1770
+ }
1771
+
1772
+ .marketing-deck-grid li {
1773
+ font-size: 12px;
1774
+ margin-bottom: 4px;
1775
+ }
1776
+
1777
+ .marketing-image-grid {
1778
+ display: grid;
1779
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1780
+ gap: 10px;
1781
+ }
1782
+
1783
+ .marketing-image-grid article {
1784
+ border: 1px solid rgba(31, 143, 78, 0.24);
1785
+ border-radius: 12px;
1786
+ background: rgba(255, 255, 255, 0.87);
1787
+ overflow: hidden;
1788
+ }
1789
+
1790
+ .marketing-image-grid img {
1791
+ width: 100%;
1792
+ aspect-ratio: 16 / 10;
1793
+ object-fit: cover;
1794
+ display: block;
1795
+ }
1796
+
1797
+ .marketing-image-meta {
1798
+ padding: 8px;
1799
+ display: grid;
1800
  gap: 8px;
1801
+ }
1802
+
1803
+ .marketing-image-meta strong {
1804
+ font-size: 12px;
1805
+ }
1806
+
1807
+ .marketing-image-meta > div {
1808
+ display: flex;
1809
+ gap: 6px;
1810
  flex-wrap: wrap;
1811
  }
1812
 
1813
+ .marketing-meta-card {
1814
+ border: 1px solid rgba(15, 109, 139, 0.24);
1815
+ border-radius: 12px;
1816
+ background: rgba(255, 255, 255, 0.88);
1817
+ padding: 10px;
1818
+ }
1819
+
1820
+ .marketing-meta-card h5 {
1821
+ margin: 0;
1822
+ font-size: 13px;
1823
+ font-family: 'Sora', 'IBM Plex Sans', sans-serif;
1824
+ }
1825
+
1826
+ .marketing-meta-card dl {
1827
+ margin: 10px 0 0;
1828
+ display: grid;
1829
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1830
+ gap: 8px;
1831
+ }
1832
+
1833
+ .marketing-meta-card dl div {
1834
+ border: 1px solid var(--line);
1835
+ border-radius: 10px;
1836
+ padding: 7px;
1837
+ background: rgba(255, 255, 255, 0.85);
1838
+ }
1839
+
1840
+ .marketing-meta-card dt {
1841
+ font-size: 10px;
1842
+ text-transform: uppercase;
1843
+ letter-spacing: 0.06em;
1844
+ color: var(--text-soft);
1845
+ }
1846
+
1847
+ .marketing-meta-card dd {
1848
+ margin: 4px 0 0;
1849
+ font-size: 12px;
1850
+ }
1851
+
1852
+ .invoice-demo-root {
1853
+ flex: 1;
1854
+ min-height: 0;
1855
+ display: flex;
1856
+ flex-direction: column;
1857
+ padding: 12px;
1858
+ gap: 12px;
1859
+ }
1860
+
1861
+ .invoice-demo-header {
1862
+ border: 1px solid rgba(15, 109, 139, 0.21);
1863
+ border-radius: 16px;
1864
+ background: linear-gradient(145deg, rgba(255, 255, 255, 0.9), rgba(231, 242, 252, 0.75));
1865
+ padding: 13px 14px;
1866
+ display: flex;
1867
+ justify-content: space-between;
1868
+ align-items: center;
1869
+ gap: 12px;
1870
+ }
1871
+
1872
+ .invoice-demo-header h3 {
1873
+ margin: 0;
1874
+ font-size: 16px;
1875
+ font-family: 'Sora', 'IBM Plex Sans', sans-serif;
1876
+ }
1877
+
1878
+ .invoice-demo-header p {
1879
+ margin: 4px 0 0;
1880
+ font-size: 12px;
1881
+ color: var(--text-soft);
1882
+ }
1883
+
1884
+ .invoice-demo-layout {
1885
+ flex: 1;
1886
+ min-height: 0;
1887
+ display: grid;
1888
+ grid-template-columns: minmax(300px, 390px) minmax(0, 1fr);
1889
+ gap: 12px;
1890
+ }
1891
+
1892
+ .invoice-input-panel,
1893
+ .invoice-output-panel {
1894
+ border: 1px solid var(--line);
1895
+ border-radius: 16px;
1896
+ background: rgba(255, 255, 255, 0.74);
1897
+ min-height: 0;
1898
+ }
1899
+
1900
+ .invoice-input-panel {
1901
+ overflow: auto;
1902
+ padding: 12px;
1903
+ display: grid;
1904
+ align-content: start;
1905
+ gap: 10px;
1906
+ }
1907
+
1908
+ .invoice-output-panel {
1909
+ overflow: auto;
1910
+ padding: 12px;
1911
+ display: grid;
1912
+ align-content: start;
1913
+ gap: 10px;
1914
+ }
1915
+
1916
+ .invoice-output-panel > header h4 {
1917
+ margin: 0;
1918
+ font-size: 15px;
1919
+ font-family: 'Sora', 'IBM Plex Sans', sans-serif;
1920
+ }
1921
+
1922
+ .invoice-output-panel > header p {
1923
+ margin: 4px 0 0;
1924
+ font-size: 12px;
1925
+ color: var(--text-soft);
1926
+ }
1927
+
1928
+ .invoice-field-grid {
1929
+ display: grid;
1930
+ gap: 8px;
1931
+ }
1932
+
1933
+ .invoice-field-block {
1934
+ display: grid;
1935
+ gap: 7px;
1936
+ }
1937
+
1938
+ .invoice-field-block > span {
1939
+ font-size: 12px;
1940
+ color: var(--text-soft);
1941
+ }
1942
+
1943
+ .invoice-dropzone {
1944
+ border: 1px dashed rgba(15, 109, 139, 0.38);
1945
+ border-radius: 12px;
1946
+ padding: 12px;
1947
+ text-align: left;
1948
+ background: rgba(234, 246, 252, 0.65);
1949
+ color: inherit;
1950
+ cursor: pointer;
1951
+ display: grid;
1952
+ gap: 3px;
1953
+ transition: 130ms background ease, 130ms transform ease;
1954
+ }
1955
+
1956
+ .invoice-dropzone:hover {
1957
+ transform: translateY(-1px);
1958
+ background: rgba(226, 241, 251, 0.84);
1959
+ }
1960
+
1961
+ .invoice-dropzone strong {
1962
+ font-size: 13px;
1963
+ }
1964
+
1965
+ .invoice-dropzone small {
1966
+ font-size: 11px;
1967
+ color: var(--text-soft);
1968
+ }
1969
+
1970
+ .invoice-dropzone.has-file {
1971
+ border-style: solid;
1972
+ border-color: rgba(31, 143, 78, 0.36);
1973
+ background: rgba(229, 245, 235, 0.78);
1974
+ }
1975
+
1976
+ .invoice-file-preview {
1977
+ border: 1px solid rgba(31, 143, 78, 0.26);
1978
+ border-radius: 12px;
1979
+ background: rgba(255, 255, 255, 0.86);
1980
+ padding: 8px;
1981
+ }
1982
+
1983
+ .invoice-file-preview img {
1984
+ width: 100%;
1985
+ max-height: 170px;
1986
+ object-fit: cover;
1987
+ border-radius: 9px;
1988
+ display: block;
1989
+ }
1990
+
1991
+ .invoice-file-meta {
1992
+ margin-top: 8px;
1993
+ display: flex;
1994
+ gap: 8px;
1995
+ align-items: center;
1996
+ justify-content: space-between;
1997
+ }
1998
+
1999
+ .invoice-file-meta span {
2000
+ min-width: 0;
2001
+ overflow: hidden;
2002
+ text-overflow: ellipsis;
2003
+ white-space: nowrap;
2004
+ font-size: 12px;
2005
+ }
2006
+
2007
+ .invoice-help-text {
2008
+ margin: 0;
2009
+ font-size: 12px;
2010
+ color: var(--text-soft);
2011
+ }
2012
+
2013
+ .invoice-toggle-row label {
2014
+ display: flex;
2015
+ align-items: center;
2016
+ gap: 8px;
2017
+ font-size: 12px;
2018
+ color: var(--text);
2019
+ }
2020
+
2021
+ .invoice-toggle-row input {
2022
+ margin: 0;
2023
+ accent-color: #167d99;
2024
+ }
2025
+
2026
+ .invoice-action-row {
2027
+ display: flex;
2028
+ gap: 8px;
2029
+ flex-wrap: wrap;
2030
+ }
2031
+
2032
+ .invoice-jobs-block h4 {
2033
+ margin: 0;
2034
+ font-size: 13px;
2035
+ font-family: 'Sora', 'IBM Plex Sans', sans-serif;
2036
+ }
2037
+
2038
+ .invoice-job-list {
2039
+ list-style: none;
2040
+ margin: 0;
2041
+ padding: 0;
2042
+ display: grid;
2043
+ gap: 8px;
2044
+ }
2045
+
2046
+ .invoice-job-card {
2047
+ border: 1px solid var(--line);
2048
+ border-radius: 11px;
2049
+ padding: 8px;
2050
+ background: rgba(255, 255, 255, 0.84);
2051
+ display: grid;
2052
+ gap: 6px;
2053
+ }
2054
+
2055
+ .invoice-job-card header {
2056
+ display: flex;
2057
+ align-items: baseline;
2058
+ justify-content: space-between;
2059
+ gap: 10px;
2060
+ }
2061
+
2062
+ .invoice-job-card header strong {
2063
+ min-width: 0;
2064
+ font-size: 12px;
2065
+ overflow: hidden;
2066
+ text-overflow: ellipsis;
2067
+ white-space: nowrap;
2068
+ }
2069
+
2070
+ .invoice-job-card header span,
2071
+ .invoice-job-card small {
2072
+ font-size: 10px;
2073
+ color: var(--text-soft);
2074
+ }
2075
+
2076
+ .invoice-job-status {
2077
+ margin: 0;
2078
+ font-size: 11px;
2079
+ color: var(--text-soft);
2080
+ }
2081
+
2082
+ .invoice-job-meter {
2083
+ height: 7px;
2084
+ border-radius: 999px;
2085
+ background: rgba(22, 66, 27, 0.1);
2086
+ overflow: hidden;
2087
+ }
2088
+
2089
+ .invoice-job-meter span {
2090
+ display: block;
2091
+ height: 100%;
2092
+ border-radius: 999px;
2093
+ background: linear-gradient(90deg, #1f8f4e, #0f6d8b);
2094
+ transition: width 180ms ease;
2095
+ }
2096
+
2097
+ .invoice-job-card.ready {
2098
+ border-color: rgba(31, 143, 78, 0.35);
2099
+ }
2100
+
2101
+ .invoice-job-card.failed {
2102
+ border-color: rgba(166, 54, 24, 0.36);
2103
+ }
2104
+
2105
+ .invoice-empty,
2106
+ .invoice-note,
2107
+ .invoice-error {
2108
+ border-radius: 12px;
2109
+ padding: 9px 10px;
2110
+ font-size: 12px;
2111
+ }
2112
+
2113
+ .invoice-empty {
2114
+ border: 1px dashed rgba(15, 109, 139, 0.3);
2115
+ color: var(--text-soft);
2116
+ background: rgba(241, 249, 253, 0.75);
2117
+ }
2118
+
2119
+ .invoice-note {
2120
+ border: 1px solid rgba(31, 143, 78, 0.3);
2121
+ color: #145f39;
2122
+ background: rgba(234, 249, 239, 0.85);
2123
+ }
2124
+
2125
+ .invoice-error {
2126
+ border: 1px solid rgba(166, 54, 24, 0.36);
2127
+ color: #8d2e1a;
2128
+ background: rgba(255, 237, 232, 0.86);
2129
+ }
2130
+
2131
+ .invoice-metric-grid {
2132
+ display: grid;
2133
+ grid-template-columns: repeat(4, minmax(0, 1fr));
2134
+ gap: 8px;
2135
+ }
2136
+
2137
+ .invoice-metric-card {
2138
+ border: 1px solid rgba(15, 109, 139, 0.24);
2139
+ border-radius: 11px;
2140
+ background: rgba(255, 255, 255, 0.88);
2141
+ padding: 9px;
2142
+ }
2143
+
2144
+ .invoice-metric-card span {
2145
+ display: block;
2146
+ font-size: 11px;
2147
+ color: var(--text-soft);
2148
+ }
2149
+
2150
+ .invoice-metric-card strong {
2151
+ display: block;
2152
+ margin-top: 4px;
2153
+ font-size: 14px;
2154
+ }
2155
+
2156
+ .invoice-preview-card,
2157
+ .invoice-record-card,
2158
+ .invoice-meta-card,
2159
+ .invoice-json-card {
2160
+ border: 1px solid rgba(15, 109, 139, 0.22);
2161
+ border-radius: 12px;
2162
+ background: rgba(255, 255, 255, 0.88);
2163
+ padding: 10px;
2164
+ }
2165
+
2166
+ .invoice-preview-card h5,
2167
+ .invoice-record-card h5,
2168
+ .invoice-meta-card h5 {
2169
+ margin: 0;
2170
+ font-size: 13px;
2171
+ font-family: 'Sora', 'IBM Plex Sans', sans-serif;
2172
+ }
2173
+
2174
+ .invoice-preview-grid {
2175
+ margin: 9px 0 0;
2176
+ display: grid;
2177
+ grid-template-columns: repeat(3, minmax(0, 1fr));
2178
+ gap: 7px;
2179
+ }
2180
+
2181
+ .invoice-preview-grid div {
2182
+ border: 1px solid var(--line);
2183
+ border-radius: 10px;
2184
+ padding: 7px;
2185
+ background: rgba(255, 255, 255, 0.85);
2186
+ }
2187
+
2188
+ .invoice-preview-grid dt {
2189
+ font-size: 10px;
2190
+ color: var(--text-soft);
2191
+ text-transform: uppercase;
2192
+ letter-spacing: 0.06em;
2193
+ }
2194
+
2195
+ .invoice-preview-grid dd {
2196
+ margin: 4px 0 0;
2197
+ font-size: 12px;
2198
+ line-height: 1.35;
2199
+ }
2200
+
2201
+ .invoice-items-scroll {
2202
+ margin-top: 9px;
2203
+ overflow: auto;
2204
+ }
2205
+
2206
+ .invoice-items-table {
2207
+ width: 100%;
2208
+ border-collapse: collapse;
2209
+ font-size: 12px;
2210
+ }
2211
+
2212
+ .invoice-items-table th,
2213
+ .invoice-items-table td {
2214
+ border-bottom: 1px solid var(--line);
2215
+ text-align: left;
2216
+ padding: 7px;
2217
+ white-space: nowrap;
2218
+ }
2219
+
2220
+ .invoice-items-table th {
2221
+ font-size: 10px;
2222
+ color: var(--text-soft);
2223
+ text-transform: uppercase;
2224
+ letter-spacing: 0.06em;
2225
+ }
2226
+
2227
+ .invoice-raw-text {
2228
+ margin: 9px 0 0;
2229
+ border-radius: 10px;
2230
+ background: rgba(16, 26, 19, 0.87);
2231
+ color: #d9f4d5;
2232
+ font-family: 'IBM Plex Mono', monospace;
2233
+ font-size: 11px;
2234
+ padding: 10px;
2235
+ white-space: pre-wrap;
2236
+ word-break: break-word;
2237
+ }
2238
+
2239
+ .invoice-json-card summary {
2240
+ cursor: pointer;
2241
+ font-size: 12px;
2242
+ color: var(--accent-2);
2243
+ }
2244
+
2245
+ .invoice-json-card pre {
2246
+ margin: 8px 0 0;
2247
+ border-radius: 10px;
2248
+ background: rgba(16, 26, 19, 0.9);
2249
+ color: #d8f6d3;
2250
+ font-family: 'IBM Plex Mono', monospace;
2251
+ font-size: 11px;
2252
+ padding: 9px;
2253
+ overflow: auto;
2254
+ }
2255
+
2256
+ .invoice-meta-card dl {
2257
+ margin: 9px 0 0;
2258
+ display: grid;
2259
+ grid-template-columns: repeat(3, minmax(0, 1fr));
2260
+ gap: 7px;
2261
+ }
2262
+
2263
+ .invoice-meta-card dl div {
2264
+ border: 1px solid var(--line);
2265
+ border-radius: 10px;
2266
+ padding: 7px;
2267
+ background: rgba(255, 255, 255, 0.84);
2268
+ }
2269
+
2270
+ .invoice-meta-card dt {
2271
+ font-size: 10px;
2272
+ text-transform: uppercase;
2273
+ letter-spacing: 0.06em;
2274
+ color: var(--text-soft);
2275
+ }
2276
+
2277
+ .invoice-meta-card dd {
2278
+ margin: 4px 0 0;
2279
+ font-size: 12px;
2280
+ }
2281
+
2282
+ .invoice-record-card p {
2283
+ margin: 8px 0 0;
2284
+ font-size: 12px;
2285
+ }
2286
+
2287
+ .invoice-history-list {
2288
+ list-style: none;
2289
+ margin: 8px 0 0;
2290
+ padding: 0;
2291
+ display: grid;
2292
+ gap: 6px;
2293
+ }
2294
+
2295
+ .invoice-history-list li {
2296
+ border: 1px solid var(--line);
2297
+ border-radius: 10px;
2298
+ padding: 7px;
2299
+ background: rgba(255, 255, 255, 0.84);
2300
+ display: flex;
2301
+ justify-content: space-between;
2302
+ gap: 8px;
2303
+ }
2304
+
2305
+ .invoice-history-list span {
2306
+ min-width: 0;
2307
+ overflow: hidden;
2308
+ text-overflow: ellipsis;
2309
+ white-space: nowrap;
2310
+ font-size: 12px;
2311
+ }
2312
+
2313
+ .invoice-history-list strong {
2314
+ font-size: 12px;
2315
+ }
2316
+
2317
+ .marketplace-demo-root {
2318
+ flex: 1;
2319
+ min-height: 0;
2320
+ display: flex;
2321
+ flex-direction: column;
2322
+ padding: 12px;
2323
+ gap: 12px;
2324
+ }
2325
+
2326
+ .marketplace-demo-header {
2327
+ border: 1px solid rgba(31, 143, 78, 0.2);
2328
+ border-radius: 16px;
2329
+ background: linear-gradient(145deg, rgba(255, 255, 255, 0.9), rgba(233, 248, 242, 0.78));
2330
+ padding: 13px 14px;
2331
+ display: flex;
2332
+ justify-content: space-between;
2333
+ align-items: center;
2334
+ gap: 12px;
2335
+ }
2336
+
2337
+ .marketplace-demo-header h3 {
2338
+ margin: 0;
2339
+ font-size: 16px;
2340
+ font-family: 'Sora', 'IBM Plex Sans', sans-serif;
2341
+ }
2342
+
2343
+ .marketplace-demo-header p {
2344
+ margin: 4px 0 0;
2345
+ font-size: 12px;
2346
+ color: var(--text-soft);
2347
+ }
2348
+
2349
+ .marketplace-demo-layout {
2350
+ flex: 1;
2351
+ min-height: 0;
2352
+ display: grid;
2353
+ grid-template-columns: minmax(300px, 390px) minmax(0, 1fr);
2354
+ gap: 12px;
2355
+ }
2356
+
2357
+ .marketplace-input-panel,
2358
+ .marketplace-output-panel {
2359
+ border: 1px solid var(--line);
2360
+ border-radius: 16px;
2361
+ background: rgba(255, 255, 255, 0.74);
2362
+ min-height: 0;
2363
+ }
2364
+
2365
+ .marketplace-input-panel {
2366
+ overflow: auto;
2367
+ padding: 12px;
2368
+ display: grid;
2369
+ align-content: start;
2370
+ gap: 10px;
2371
+ }
2372
+
2373
+ .marketplace-output-panel {
2374
+ overflow: auto;
2375
+ padding: 12px;
2376
+ display: grid;
2377
+ align-content: start;
2378
+ gap: 10px;
2379
+ }
2380
+
2381
+ .marketplace-output-panel > header h4 {
2382
+ margin: 0;
2383
+ font-family: 'Sora', 'IBM Plex Sans', sans-serif;
2384
+ font-size: 15px;
2385
+ }
2386
+
2387
+ .marketplace-output-panel > header p {
2388
+ margin: 4px 0 0;
2389
+ font-size: 12px;
2390
+ color: var(--text-soft);
2391
+ }
2392
+
2393
+ .marketplace-field-block {
2394
+ display: grid;
2395
+ gap: 7px;
2396
+ }
2397
+
2398
+ .marketplace-field-block > span {
2399
+ font-size: 12px;
2400
+ color: var(--text-soft);
2401
+ }
2402
+
2403
+ .marketplace-filter-block {
2404
+ border: 1px solid rgba(15, 109, 139, 0.18);
2405
+ border-radius: 12px;
2406
+ background: rgba(255, 255, 255, 0.8);
2407
+ padding: 9px;
2408
+ display: grid;
2409
+ gap: 8px;
2410
+ }
2411
+
2412
+ .marketplace-filter-block h4 {
2413
  margin: 0;
2414
+ font-size: 12px;
2415
  font-family: 'Sora', 'IBM Plex Sans', sans-serif;
2416
  }
2417
 
2418
+ .marketplace-filter-grid {
 
 
 
2419
  display: grid;
2420
+ grid-template-columns: repeat(3, minmax(0, 1fr));
2421
  gap: 8px;
2422
  }
2423
 
2424
+ .marketplace-filter-grid label {
 
 
 
 
2425
  display: grid;
2426
  gap: 6px;
2427
  }
2428
 
2429
+ .marketplace-filter-grid label span {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2430
  font-size: 11px;
2431
  color: var(--text-soft);
2432
  }
2433
 
2434
+ .marketplace-action-row {
2435
+ display: flex;
2436
+ gap: 8px;
2437
+ flex-wrap: wrap;
 
 
 
 
 
 
 
 
 
2438
  }
2439
 
2440
+ .marketplace-recent-block h4 {
2441
+ margin: 0;
2442
+ font-size: 12px;
2443
+ font-family: 'Sora', 'IBM Plex Sans', sans-serif;
2444
  }
2445
 
2446
+ .marketplace-chip-row {
2447
+ display: flex;
2448
+ flex-wrap: wrap;
2449
+ gap: 8px;
2450
+ margin-top: 8px;
2451
  }
2452
 
2453
+ .marketplace-note,
2454
+ .marketplace-error,
2455
+ .marketplace-empty {
2456
  border-radius: 12px;
2457
  padding: 9px 10px;
2458
  font-size: 12px;
2459
  }
2460
 
2461
+ .marketplace-note {
2462
+ border: 1px solid rgba(31, 143, 78, 0.28);
 
 
 
 
 
 
2463
  color: #145f39;
2464
+ background: rgba(232, 249, 238, 0.86);
2465
  }
2466
 
2467
+ .marketplace-error {
2468
  border: 1px solid rgba(166, 54, 24, 0.36);
2469
+ color: #892d19;
2470
+ background: rgba(255, 237, 232, 0.87);
 
 
 
 
 
 
 
 
 
 
 
 
 
2471
  }
2472
 
2473
+ .marketplace-empty {
2474
+ border: 1px dashed rgba(15, 109, 139, 0.3);
 
2475
  color: var(--text-soft);
2476
+ background: rgba(240, 249, 253, 0.8);
2477
  }
2478
 
2479
+ .marketplace-stats-card,
2480
+ .marketplace-result-card,
2481
+ .marketplace-meta-card {
 
 
 
 
 
 
 
2482
  border: 1px solid rgba(15, 109, 139, 0.22);
2483
  border-radius: 12px;
2484
  background: rgba(255, 255, 255, 0.88);
2485
  padding: 10px;
2486
  }
2487
 
2488
+ .marketplace-stats-card h5,
2489
+ .marketplace-result-card h5,
2490
+ .marketplace-meta-card h5 {
2491
  margin: 0;
2492
  font-size: 13px;
2493
  font-family: 'Sora', 'IBM Plex Sans', sans-serif;
2494
  }
2495
 
2496
+ .marketplace-stat-grid {
2497
+ margin-top: 9px;
2498
  display: grid;
2499
  grid-template-columns: repeat(3, minmax(0, 1fr));
2500
+ gap: 8px;
2501
  }
2502
 
2503
+ .marketplace-stat-grid div {
2504
  border: 1px solid var(--line);
2505
  border-radius: 10px;
2506
+ padding: 8px;
2507
+ background: rgba(255, 255, 255, 0.86);
2508
  }
2509
 
2510
+ .marketplace-stat-grid span {
2511
+ display: block;
2512
  font-size: 10px;
2513
  color: var(--text-soft);
2514
  text-transform: uppercase;
2515
  letter-spacing: 0.06em;
2516
  }
2517
 
2518
+ .marketplace-stat-grid strong {
2519
+ display: block;
2520
+ margin-top: 4px;
2521
+ font-size: 13px;
2522
  }
2523
 
2524
+ .marketplace-card-grid {
2525
  margin-top: 9px;
2526
+ display: grid;
2527
+ grid-template-columns: repeat(2, minmax(0, 1fr));
2528
+ gap: 8px;
2529
  }
2530
 
2531
+ .marketplace-entity-card {
2532
+ border: 1px solid var(--line);
2533
+ border-radius: 11px;
2534
+ background: rgba(255, 255, 255, 0.86);
2535
+ padding: 9px;
2536
  }
2537
 
2538
+ .marketplace-entity-card header {
2539
+ display: flex;
2540
+ align-items: baseline;
2541
+ justify-content: space-between;
2542
+ gap: 8px;
2543
+ }
2544
+
2545
+ .marketplace-entity-card header strong {
2546
+ min-width: 0;
2547
+ font-size: 13px;
2548
+ overflow: hidden;
2549
+ text-overflow: ellipsis;
2550
  white-space: nowrap;
2551
  }
2552
 
2553
+ .marketplace-entity-card header span {
2554
  font-size: 10px;
2555
  color: var(--text-soft);
2556
  text-transform: uppercase;
2557
  letter-spacing: 0.06em;
2558
  }
2559
 
2560
+ .marketplace-metric-row {
2561
+ margin-top: 7px;
2562
+ display: flex;
2563
+ justify-content: space-between;
2564
+ gap: 8px;
2565
+ }
2566
+
2567
+ .marketplace-metric-row span {
2568
  font-size: 11px;
2569
+ color: var(--text-soft);
 
 
2570
  }
2571
 
2572
+ .marketplace-metric-row strong {
 
2573
  font-size: 12px;
2574
+ text-align: right;
2575
  }
2576
 
2577
+ .marketplace-score-track {
2578
+ margin-top: 8px;
2579
+ }
2580
+
2581
+ .marketplace-score-track small {
2582
+ font-size: 10px;
2583
+ color: var(--text-soft);
2584
+ text-transform: uppercase;
2585
+ letter-spacing: 0.06em;
2586
+ }
2587
+
2588
+ .marketplace-score-track > div {
2589
+ margin-top: 5px;
2590
+ height: 7px;
2591
+ border-radius: 999px;
2592
+ background: rgba(22, 66, 27, 0.1);
2593
+ overflow: hidden;
2594
+ }
2595
+
2596
+ .marketplace-score-track > div span {
2597
+ display: block;
2598
+ height: 100%;
2599
+ border-radius: 999px;
2600
+ background: linear-gradient(90deg, #1f8f4e, #0f6d8b);
2601
+ }
2602
+
2603
+ .marketplace-help-text {
2604
  margin: 8px 0 0;
2605
+ font-size: 12px;
2606
+ color: var(--text-soft);
 
 
 
 
 
2607
  }
2608
 
2609
+ .marketplace-meta-card dl {
2610
  margin: 9px 0 0;
2611
  display: grid;
2612
+ grid-template-columns: repeat(2, minmax(0, 1fr));
2613
  gap: 7px;
2614
  }
2615
 
2616
+ .marketplace-meta-card dl div {
2617
  border: 1px solid var(--line);
2618
  border-radius: 10px;
2619
  padding: 7px;
2620
+ background: rgba(255, 255, 255, 0.86);
2621
  }
2622
 
2623
+ .marketplace-meta-card dt {
2624
  font-size: 10px;
2625
  text-transform: uppercase;
2626
  letter-spacing: 0.06em;
2627
  color: var(--text-soft);
2628
  }
2629
 
2630
+ .marketplace-meta-card dd {
2631
  margin: 4px 0 0;
2632
  font-size: 12px;
2633
  }
2634
 
2635
+ .marketplace-meta-card details {
2636
+ margin-top: 8px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2637
  }
2638
 
2639
+ .marketplace-meta-card details summary {
2640
+ cursor: pointer;
 
 
 
2641
  font-size: 12px;
2642
+ color: var(--accent-2);
2643
  }
2644
 
2645
+ .marketplace-meta-card pre {
2646
+ margin: 8px 0 0;
2647
+ border-radius: 10px;
2648
+ background: rgba(16, 26, 19, 0.88);
2649
+ color: #d8f7d4;
2650
+ font-family: 'IBM Plex Mono', monospace;
2651
+ font-size: 11px;
2652
+ padding: 9px;
2653
+ overflow: auto;
2654
  }
2655
 
2656
+ .voice-demo-root {
2657
  flex: 1;
2658
  min-height: 0;
2659
  display: flex;
2660
  flex-direction: column;
2661
+ gap: 10px;
2662
+ position: relative;
2663
  padding: 12px;
 
2664
  }
2665
 
2666
+ .voice-walk-video {
 
2667
  border-radius: 16px;
2668
+ border: 1px solid rgba(31, 143, 78, 0.24);
2669
+ background: linear-gradient(155deg, rgba(244, 252, 247, 0.94), rgba(230, 245, 251, 0.9));
2670
+ padding: 10px;
2671
+ display: grid;
2672
+ gap: 8px;
2673
+ animation: slide-in 220ms ease;
2674
+ }
2675
+
2676
+ .voice-walk-head {
2677
  display: flex;
2678
  justify-content: space-between;
2679
  align-items: center;
2680
+ gap: 8px;
2681
  }
2682
 
2683
+ .voice-walk-head h3 {
2684
  margin: 0;
2685
+ font-size: 13px;
2686
  font-family: 'Sora', 'IBM Plex Sans', sans-serif;
2687
  }
2688
 
2689
+ .voice-walk-head small {
2690
+ margin-left: 6px;
 
2691
  color: var(--text-soft);
2692
+ font-size: 10px;
2693
+ font-family: 'IBM Plex Mono', monospace;
2694
  }
2695
 
2696
+ .voice-walk-actions {
2697
+ display: flex;
2698
+ gap: 6px;
2699
+ }
2700
+
2701
+ .voice-walk-icon-btn {
2702
+ width: 28px;
2703
+ height: 28px;
2704
+ border-radius: 999px;
2705
+ border: 1px solid rgba(19, 67, 36, 0.28);
2706
+ background: rgba(255, 255, 255, 0.82);
2707
+ color: #1c5f39;
2708
  display: grid;
2709
+ place-items: center;
2710
+ cursor: pointer;
2711
+ font-family: 'IBM Plex Mono', monospace;
2712
+ font-size: 11px;
2713
  }
2714
 
2715
+ .voice-walk-icon-btn:disabled {
2716
+ opacity: 0.45;
2717
+ cursor: not-allowed;
 
 
 
2718
  }
2719
 
2720
+ .voice-walk-frame {
2721
+ border-radius: 14px;
2722
+ border: 1px solid rgba(19, 56, 30, 0.3);
2723
+ background: linear-gradient(170deg, rgba(12, 24, 19, 0.96), rgba(19, 39, 34, 0.96));
2724
+ overflow: hidden;
 
2725
  }
2726
 
2727
+ .voice-walk-windowbar {
2728
+ display: flex;
2729
+ align-items: center;
2730
+ gap: 5px;
2731
+ border-bottom: 1px solid rgba(255, 255, 255, 0.12);
2732
+ padding: 8px 10px;
2733
  }
2734
 
2735
+ .voice-walk-windowbar > span {
2736
+ width: 9px;
2737
+ height: 9px;
2738
+ border-radius: 999px;
2739
+ display: block;
2740
  }
2741
 
2742
+ .voice-walk-windowbar > span:nth-child(1) {
2743
+ background: #f87171;
 
 
2744
  }
2745
 
2746
+ .voice-walk-windowbar > span:nth-child(2) {
2747
+ background: #fbbf24;
2748
+ }
2749
+
2750
+ .voice-walk-windowbar > span:nth-child(3) {
2751
+ background: #4ade80;
2752
+ }
2753
+
2754
+ .voice-walk-windowbar small {
2755
+ margin-left: 4px;
2756
+ color: rgba(235, 248, 238, 0.8);
2757
+ font-family: 'IBM Plex Mono', monospace;
2758
+ font-size: 10px;
2759
+ }
2760
+
2761
+ .voice-walk-canvas {
2762
  display: grid;
2763
+ grid-template-columns: repeat(3, minmax(0, 1fr));
2764
  gap: 7px;
2765
+ padding: 9px;
2766
+ min-height: 156px;
2767
  }
2768
 
2769
+ .voice-walk-lane {
2770
+ border-radius: 11px;
2771
+ border: 1px solid rgba(184, 222, 193, 0.28);
2772
+ background: linear-gradient(150deg, rgba(255, 255, 255, 0.08), rgba(205, 236, 230, 0.08));
2773
+ padding: 8px;
2774
+ display: grid;
2775
+ gap: 6px;
2776
+ align-content: start;
2777
+ transition: transform 170ms ease, border-color 170ms ease, box-shadow 170ms ease;
2778
  }
2779
 
2780
+ .voice-walk-lane-index {
2781
+ width: 18px;
2782
+ height: 18px;
2783
+ border-radius: 999px;
2784
+ border: 1px solid rgba(223, 248, 232, 0.45);
2785
+ background: rgba(255, 255, 255, 0.08);
2786
+ color: rgba(225, 246, 235, 0.88);
2787
+ font-size: 10px;
2788
+ font-family: 'IBM Plex Mono', monospace;
2789
  display: grid;
2790
+ place-items: center;
2791
  }
2792
 
2793
+ .voice-walk-lane-label {
2794
+ font-size: 10px;
2795
+ letter-spacing: 0.08em;
2796
+ text-transform: uppercase;
2797
+ color: rgba(214, 241, 226, 0.86);
2798
  }
2799
 
2800
+ .voice-walk-step-copy {
2801
  display: grid;
2802
+ gap: 4px;
2803
+ margin-top: 2px;
2804
  }
2805
 
2806
+ .voice-walk-step-copy strong {
2807
+ font-size: 11px;
2808
+ color: rgba(230, 248, 237, 0.96);
2809
+ font-family: 'Sora', 'IBM Plex Sans', sans-serif;
2810
  }
2811
 
2812
+ .voice-walk-step-copy p {
2813
+ margin: 0;
2814
  font-size: 11px;
2815
+ line-height: 1.35;
2816
+ color: rgba(214, 236, 223, 0.9);
2817
  }
2818
 
2819
+ .voice-walk-step-copy small {
2820
+ font-size: 10px;
2821
+ line-height: 1.35;
2822
+ color: rgba(176, 213, 195, 0.92);
2823
+ }
2824
+
2825
+ .voice-walk-wave {
2826
  display: flex;
2827
+ align-items: end;
2828
+ gap: 4px;
2829
+ height: 54px;
2830
  }
2831
 
2832
+ .voice-walk-wave span {
2833
+ width: 6px;
2834
+ height: 18px;
2835
+ border-radius: 999px;
2836
+ background: linear-gradient(180deg, rgba(166, 237, 194, 0.95), rgba(66, 161, 106, 0.9));
2837
+ animation: voice-walk-wave 1.1s ease-in-out infinite;
2838
  }
2839
 
2840
+ .voice-walk-mic {
2841
+ width: 26px;
2842
+ height: 26px;
2843
+ border-radius: 999px;
2844
+ border: 1px solid rgba(135, 224, 169, 0.56);
2845
+ background: rgba(135, 224, 169, 0.22);
2846
+ display: grid;
2847
+ place-items: center;
2848
  }
2849
 
2850
+ .voice-walk-mic span {
2851
+ width: 12px;
2852
+ height: 12px;
2853
+ border-radius: 999px;
2854
+ background: rgba(135, 224, 169, 0.86);
2855
+ animation: pulse 1.1s ease-in-out infinite;
2856
  }
2857
 
2858
+ .voice-walk-route-nodes {
2859
+ display: grid;
2860
+ grid-template-columns: repeat(3, minmax(0, 1fr));
2861
+ gap: 6px;
2862
  }
2863
 
2864
+ .voice-walk-route-nodes span {
2865
+ min-height: 18px;
2866
+ display: grid;
2867
+ place-items: center;
2868
+ padding: 2px 4px;
2869
+ font-size: 9px;
2870
+ line-height: 1.2;
2871
+ color: rgba(209, 235, 249, 0.92);
2872
+ border-radius: 7px;
2873
+ border: 1px solid rgba(157, 202, 231, 0.3);
2874
+ background: rgba(25, 54, 72, 0.66);
2875
  }
2876
 
2877
+ .voice-walk-route-flow {
2878
+ height: 9px;
2879
+ border-radius: 999px;
2880
+ border: 1px solid rgba(136, 239, 175, 0.3);
2881
+ background: rgba(21, 48, 33, 0.85);
2882
+ overflow: hidden;
2883
  }
2884
 
2885
+ .voice-walk-route-flow span {
2886
+ display: block;
2887
+ height: 100%;
2888
+ width: 44%;
2889
+ border-radius: inherit;
2890
+ background: linear-gradient(90deg, rgba(136, 239, 175, 0.25), rgba(136, 239, 175, 0.9));
2891
+ animation: voice-walk-flow 1.7s ease-in-out infinite;
2892
  }
2893
 
2894
+ .voice-walk-route-chips {
2895
+ display: flex;
2896
+ gap: 4px;
2897
+ flex-wrap: wrap;
2898
+ }
2899
+
2900
+ .voice-walk-route-chips span {
2901
+ min-height: 14px;
2902
+ padding: 2px 6px;
2903
+ font-size: 9px;
2904
+ line-height: 1.2;
2905
+ color: rgba(197, 231, 247, 0.92);
2906
+ border: 1px solid rgba(145, 216, 236, 0.36);
2907
+ border-radius: 999px;
2908
+ background: rgba(145, 216, 236, 0.18);
2909
+ }
2910
+
2911
+ .voice-walk-card {
2912
+ border-radius: 9px;
2913
+ border: 1px solid rgba(174, 220, 185, 0.25);
2914
+ background: rgba(34, 60, 43, 0.8);
2915
+ padding: 7px;
2916
+ display: grid;
2917
+ gap: 6px;
2918
+ }
2919
+
2920
+ .voice-walk-card-head strong {
2921
+ font-size: 11px;
2922
+ color: rgba(230, 248, 237, 0.96);
2923
  font-family: 'Sora', 'IBM Plex Sans', sans-serif;
2924
  }
2925
 
2926
+ .voice-walk-card-body {
2927
+ margin: 0;
2928
+ font-size: 10px;
2929
+ line-height: 1.35;
2930
+ color: rgba(208, 234, 219, 0.9);
2931
+ }
2932
+
2933
+ .voice-walk-line {
2934
+ height: 8px;
2935
+ border-radius: 999px;
2936
+ background: linear-gradient(90deg, rgba(171, 228, 247, 0.33), rgba(217, 245, 228, 0.45), rgba(171, 228, 247, 0.33));
2937
+ background-size: 160% 100%;
2938
+ animation: chat-demo-shimmer 1.8s linear infinite;
2939
+ }
2940
+
2941
+ .voice-walk-line.lg {
2942
+ width: 94%;
2943
  }
2944
 
2945
+ .voice-walk-line.md {
2946
+ width: 72%;
 
 
 
2947
  }
2948
 
2949
+ .voice-walk-line.sm {
2950
+ width: 50%;
 
 
 
 
2951
  }
2952
 
2953
+ .voice-walk-chip-row {
2954
+ display: flex;
2955
+ gap: 4px;
2956
+ flex-wrap: wrap;
2957
  }
2958
 
2959
+ .voice-walk-chip-row span {
2960
+ border: 1px solid rgba(145, 216, 236, 0.36);
2961
+ padding: 2px 6px;
2962
+ font-size: 9px;
2963
+ color: rgba(197, 231, 247, 0.92);
2964
+ line-height: 1.25;
2965
+ border-radius: 999px;
2966
+ background: rgba(140, 214, 255, 0.18);
2967
  }
2968
 
2969
+ .voice-walk-grid {
2970
+ display: grid;
2971
+ grid-template-columns: repeat(3, minmax(0, 1fr));
2972
+ gap: 4px;
 
2973
  }
2974
 
2975
+ .voice-walk-grid span {
2976
+ min-height: 36px;
2977
+ display: grid;
2978
+ align-content: center;
2979
+ justify-items: center;
2980
+ gap: 2px;
2981
+ padding: 3px;
2982
+ border-radius: 7px;
2983
+ background: rgba(191, 245, 208, 0.24);
2984
+ border: 1px solid rgba(191, 245, 208, 0.3);
2985
  }
2986
 
2987
+ .voice-walk-grid span small {
2988
+ font-size: 8px;
2989
+ color: rgba(205, 238, 217, 0.86);
 
 
 
2990
  }
2991
 
2992
+ .voice-walk-grid span strong {
2993
  font-size: 10px;
2994
+ color: rgba(230, 249, 237, 0.95);
 
 
 
 
 
 
 
 
 
2995
  }
2996
 
2997
+ .voice-walk-card-note {
2998
+ margin: 0;
2999
+ font-size: 9px;
3000
+ line-height: 1.3;
3001
+ color: rgba(176, 213, 195, 0.92);
3002
  }
3003
 
3004
+ .voice-walk-frame[data-scene='listen'] .voice-walk-lane.listen,
3005
+ .voice-walk-frame[data-scene='route'] .voice-walk-lane.route,
3006
+ .voice-walk-frame[data-scene='cards'] .voice-walk-lane.cards {
3007
+ border-color: rgba(136, 239, 175, 0.58);
3008
+ transform: translateY(-2px);
3009
+ box-shadow: 0 0 0 1px rgba(136, 239, 175, 0.22);
3010
  }
3011
 
3012
+ .voice-walk-meta {
3013
+ display: grid;
3014
+ gap: 7px;
3015
  }
3016
 
3017
+ .voice-walk-caption {
3018
+ margin: 0;
3019
+ font-size: 11px;
3020
  color: var(--text-soft);
3021
+ text-align: center;
 
3022
  }
3023
 
3024
+ .voice-walk-progress-track {
 
3025
  height: 7px;
3026
  border-radius: 999px;
3027
+ border: 1px solid rgba(31, 143, 78, 0.26);
3028
+ background: rgba(255, 255, 255, 0.66);
3029
  overflow: hidden;
3030
  }
3031
 
3032
+ .voice-walk-progress-track span {
3033
  display: block;
3034
  height: 100%;
3035
+ border-radius: inherit;
3036
+ background: linear-gradient(90deg, rgba(31, 143, 78, 0.9), rgba(15, 109, 139, 0.9));
3037
+ transition: width 120ms linear;
3038
  }
3039
 
3040
+ .voice-walk-scene-dots {
3041
+ display: flex;
3042
+ justify-content: center;
3043
+ gap: 6px;
3044
  }
3045
 
3046
+ .voice-walk-scene-dot {
3047
+ width: 20px;
3048
+ height: 20px;
3049
+ padding: 0;
3050
+ border-radius: 999px;
3051
+ border: 1px solid rgba(15, 109, 139, 0.25);
3052
+ background: rgba(15, 109, 139, 0.08);
3053
  display: grid;
3054
+ place-items: center;
3055
+ cursor: pointer;
3056
  }
3057
 
3058
+ .voice-walk-scene-dot span {
3059
+ width: 6px;
3060
+ height: 6px;
3061
+ border-radius: 999px;
3062
+ background: #589db6;
3063
  }
3064
 
3065
+ .voice-walk-scene-dot.active {
3066
+ border-color: rgba(31, 143, 78, 0.45);
3067
+ background: rgba(31, 143, 78, 0.16);
 
 
3068
  }
3069
 
3070
+ .voice-walk-scene-dot.active span {
3071
+ background: #1f8f4e;
 
3072
  }
3073
 
3074
+ .voice-walk-scene-dot.complete {
3075
+ border-color: rgba(31, 143, 78, 0.25);
3076
+ background: rgba(31, 143, 78, 0.08);
3077
  }
3078
 
3079
+ .voice-walk-scene-dot.complete span {
3080
+ background: #2f9b5b;
 
 
3081
  }
3082
 
3083
+ .voice-walk-collapsed {
3084
+ border-radius: 14px;
3085
+ border: 1px dashed rgba(31, 143, 78, 0.35);
3086
+ background: rgba(244, 252, 247, 0.86);
3087
+ padding: 10px 12px;
3088
+ display: flex;
3089
+ align-items: center;
3090
+ justify-content: space-between;
3091
+ gap: 10px;
3092
  }
3093
 
3094
+ .voice-walk-mini-markers {
 
 
3095
  display: flex;
3096
+ gap: 4px;
3097
+ }
3098
+
3099
+ .voice-walk-mini-markers span {
3100
+ width: 8px;
3101
+ height: 8px;
3102
+ border-radius: 999px;
3103
+ background: rgba(31, 143, 78, 0.35);
3104
+ }
3105
+
3106
+ .voice-walk-collapsed p {
3107
+ margin: 0;
3108
+ color: var(--text-soft);
3109
+ font-size: 12px;
3110
  }
3111
 
3112
  .voice-launcher-wrap {
 
3685
  align-items: stretch;
3686
  }
3687
 
3688
+ .chat-demo-video,
3689
+ .chat-demo-collapsed {
3690
+ margin: 8px;
3691
+ }
3692
+
3693
+ .chat-demo-canvas {
3694
+ grid-template-columns: minmax(0, 1fr);
3695
+ min-height: auto;
3696
+ }
3697
+
3698
+ .chat-demo-head-actions {
3699
+ width: 100%;
3700
+ }
3701
+
3702
+ .chat-demo-cursor {
3703
+ display: none;
3704
+ }
3705
+
3706
  .voice-demo-root {
3707
  padding: 8px 6px;
3708
  }
3709
 
3710
+ .voice-walk-canvas {
3711
+ grid-template-columns: minmax(0, 1fr);
3712
+ min-height: auto;
3713
+ }
3714
+
3715
+ .voice-walk-actions {
3716
+ width: 100%;
3717
+ justify-content: flex-end;
3718
+ }
3719
+
3720
  .voice-dialog-header {
3721
  padding: 12px;
3722
  }
 
3729
  grid-template-columns: repeat(2, minmax(0, 1fr));
3730
  }
3731
 
3732
+ .marketing-walk-canvas,
3733
+ .invoice-walk-canvas,
3734
+ .market-walk-canvas {
3735
+ grid-template-columns: minmax(0, 1fr);
3736
+ min-height: auto;
3737
+ }
3738
+
3739
+ .marketing-walk-actions,
3740
+ .invoice-walk-actions,
3741
+ .market-walk-actions {
3742
+ width: 100%;
3743
+ justify-content: flex-end;
3744
+ }
3745
+
3746
  .marketing-studio-root {
3747
  padding: 8px 6px;
3748
  gap: 8px;
 
3799
  }
3800
 
3801
  @media (max-width: 620px) {
3802
+ .chat-demo-collapsed {
3803
+ flex-direction: column;
3804
+ align-items: stretch;
3805
+ }
3806
+
3807
+ .chat-demo-head-actions {
3808
+ display: grid;
3809
+ width: 100%;
3810
+ grid-template-columns: repeat(3, minmax(0, 1fr));
3811
+ }
3812
+
3813
+ .marketing-walk-collapsed,
3814
+ .invoice-walk-collapsed,
3815
+ .market-walk-collapsed {
3816
+ flex-direction: column;
3817
+ align-items: stretch;
3818
+ }
3819
+
3820
+ .marketing-walk-actions,
3821
+ .invoice-walk-actions,
3822
+ .market-walk-actions {
3823
+ display: grid;
3824
+ width: 100%;
3825
+ grid-template-columns: repeat(3, minmax(0, 1fr));
3826
+ }
3827
+
3828
+ .voice-walk-collapsed {
3829
+ flex-direction: column;
3830
+ align-items: stretch;
3831
+ }
3832
+
3833
+ .voice-walk-actions {
3834
+ display: grid;
3835
+ width: 100%;
3836
+ grid-template-columns: repeat(3, minmax(0, 1fr));
3837
+ }
3838
+
3839
  .invoice-metric-grid,
3840
  .invoice-preview-grid,
3841
  .invoice-meta-card dl {
 
3847
  }
3848
  }
3849
 
3850
+ @media (prefers-reduced-motion: reduce) {
3851
+ .chat-demo-cursor,
3852
+ .chat-demo-prompt-bubble::after,
3853
+ .chat-demo-line,
3854
+ .chat-demo-flow span,
3855
+ .chat-demo-action-dot,
3856
+ .voice-walk-wave span,
3857
+ .voice-walk-route-flow span,
3858
+ .voice-walk-mic span,
3859
+ .voice-walk-line,
3860
+ .marketing-walk-line,
3861
+ .marketing-walk-stream-pill,
3862
+ .invoice-walk-line,
3863
+ .invoice-walk-stream-pill,
3864
+ .invoice-walk-ocr-flow span,
3865
+ .market-walk-line,
3866
+ .market-walk-stream-pill,
3867
+ .market-walk-rank-flow span {
3868
+ animation: none;
3869
+ }
3870
+ }
3871
+
3872
  @keyframes pulse {
3873
  0%,
3874
  100% {
 
3921
  transform: translateY(0);
3922
  }
3923
  }
3924
+
3925
+ @keyframes chat-demo-type {
3926
+ 0%,
3927
+ 100% {
3928
+ transform: scaleX(0.24);
3929
+ opacity: 0.6;
3930
+ }
3931
+ 50% {
3932
+ transform: scaleX(1);
3933
+ opacity: 1;
3934
+ }
3935
+ }
3936
+
3937
+ @keyframes chat-demo-shimmer {
3938
+ 0%,
3939
+ 100% {
3940
+ background-position: 0% 0%;
3941
+ }
3942
+ 50% {
3943
+ background-position: 100% 0%;
3944
+ }
3945
+ }
3946
+
3947
+ @keyframes chat-demo-cursor-path {
3948
+ 0%,
3949
+ 18% {
3950
+ left: 14%;
3951
+ top: 22%;
3952
+ }
3953
+ 28%,
3954
+ 50% {
3955
+ left: 48%;
3956
+ top: 34%;
3957
+ }
3958
+ 62%,
3959
+ 82% {
3960
+ left: 81%;
3961
+ top: 42%;
3962
+ }
3963
+ 100% {
3964
+ left: 14%;
3965
+ top: 22%;
3966
+ }
3967
+ }
3968
+
3969
+ @keyframes voice-walk-wave {
3970
+ 0%,
3971
+ 100% {
3972
+ height: 16px;
3973
+ opacity: 0.5;
3974
+ }
3975
+ 50% {
3976
+ height: 52px;
3977
+ opacity: 1;
3978
+ }
3979
+ }
3980
+
3981
+ @keyframes voice-walk-flow {
3982
+ 0% {
3983
+ transform: translateX(-90%);
3984
+ opacity: 0.5;
3985
+ }
3986
+ 50% {
3987
+ transform: translateX(30%);
3988
+ opacity: 1;
3989
+ }
3990
+ 100% {
3991
+ transform: translateX(120%);
3992
+ opacity: 0.4;
3993
+ }
3994
+ }
3995
+
3996
+ @keyframes invoice-walk-flow {
3997
+ 0% {
3998
+ transform: translateX(-90%);
3999
+ opacity: 0.5;
4000
+ }
4001
+ 50% {
4002
+ transform: translateX(30%);
4003
+ opacity: 1;
4004
+ }
4005
+ 100% {
4006
+ transform: translateX(120%);
4007
+ opacity: 0.4;
4008
+ }
4009
+ }
4010
+
4011
+ @keyframes market-walk-flow {
4012
+ 0% {
4013
+ transform: translateX(-90%);
4014
+ opacity: 0.5;
4015
+ }
4016
+ 50% {
4017
+ transform: translateX(30%);
4018
+ opacity: 1;
4019
+ }
4020
+ 100% {
4021
+ transform: translateX(120%);
4022
+ opacity: 0.4;
4023
+ }
4024
+ }
src/App.tsx CHANGED
@@ -5,14 +5,115 @@ import VoiceDemoPanel from './VoiceDemoPanel';
5
  import MarketingStudioPanel from './MarketingStudioPanel';
6
  import InvoiceDemoPanel from './InvoiceDemoPanel';
7
  import MarketplaceDemoPanel from './MarketplaceDemoPanel';
8
-
9
- const TAB_DEFINITIONS: Array<{ id: DemoTabId; label: string; subtitle: string }> = [
10
- { id: 'chat', label: 'Chat Agent', subtitle: 'Tool-first orchestration' },
11
- { id: 'voice', label: 'Voice Agent', subtitle: 'Speech-style actions' },
12
- { id: 'marketing', label: 'Marketing Studio', subtitle: 'Copy + visual workflow' },
13
- { id: 'invoice', label: 'Invoice AI', subtitle: 'OCR + extraction demo' },
14
- { id: 'marketplace', label: 'Marketplace', subtitle: 'Search + ranking + stats' },
15
- ];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  function timestampLabel(): string {
18
  return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
@@ -62,6 +163,8 @@ function App() {
62
  const firstRenderRef = useRef(true);
63
  const threadRef = useRef<HTMLDivElement | null>(null);
64
 
 
 
65
  const quickPrompts = useMemo(() => getQuickPrompts(activeTab, locale), [activeTab, locale]);
66
  const showTracePanel = isWorking || traceItems.length > 0;
67
 
@@ -158,7 +261,7 @@ function App() {
158
  }),
159
  ]);
160
  } catch (error) {
161
- const message = error instanceof Error ? error.message : 'Unknown runtime error';
162
  setMessages((prev) => [
163
  ...prev,
164
  createMessage(
@@ -183,20 +286,20 @@ function App() {
183
 
184
  <header className="topbar">
185
  <div>
186
- <p className="eyebrow">Farm2Market Demo</p>
187
- <h1>Agent Workspace</h1>
188
  </div>
189
 
190
  <div className="topbar-actions">
191
- <span className={`status-pill ${isWorking ? 'busy' : 'idle'}`}>{isWorking ? 'Running' : 'Idle'}</span>
192
  <button className="ghost-btn mobile-only" type="button" onClick={() => setMobileControlsOpen((prev) => !prev)}>
193
- {mobileControlsOpen ? 'Hide Controls' : 'Show Controls'}
194
  </button>
195
  </div>
196
  </header>
197
 
198
- <nav className="tab-row" aria-label="Agent tabs">
199
- {TAB_DEFINITIONS.map((tab) => (
200
  <button
201
  key={tab.id}
202
  type="button"
@@ -212,28 +315,28 @@ function App() {
212
  <div className="workspace">
213
  <aside className="panel controls-panel">
214
  <div className="panel-header">
215
- <h2>Controls</h2>
216
- <p>Switch scenario and prompt packs.</p>
217
  </div>
218
 
219
  <label className="field">
220
- <span>Demo Mode</span>
221
  <select value={mode} onChange={(event) => setMode(event.target.value as DemoMode)}>
222
- <option value="mock">Mock (stable)</option>
223
- <option value="live">Live (backend)</option>
224
  </select>
225
  </label>
226
 
227
  <label className="field">
228
- <span>Language</span>
229
  <select value={locale} onChange={(event) => setLocale(event.target.value as DemoLocale)}>
230
- <option value="en">English</option>
231
- <option value="zh-TW">繁體中文</option>
232
  </select>
233
  </label>
234
 
235
  <section className="prompt-bank">
236
- <h3>Try Prompts</h3>
237
  <div className="chip-list">
238
  {quickPrompts.map((prompt) => (
239
  <button
@@ -276,8 +379,8 @@ function App() {
276
  {progressVisible && progressSteps.length > 0 ? (
277
  <section className="progress-panel" aria-live="polite">
278
  <header>
279
- <h3>{locale === 'zh-TW' ? '模型處理進度' : 'Model Progress'}</h3>
280
- <span>{isWorking ? (locale === 'zh-TW' ? '執行中' : 'In progress') : locale === 'zh-TW' ? '完成' : 'Completed'}</span>
281
  </header>
282
  <ul>
283
  {progressSteps.map((step) => (
@@ -339,11 +442,13 @@ function App() {
339
  />
340
  ) : (
341
  <>
 
 
342
  <div className="thread" ref={threadRef}>
343
  {messages.map((message) => (
344
  <article key={message.id} className={`message ${message.role}`}>
345
  <header>
346
- <span>{message.role}</span>
347
  <time>{message.timestamp}</time>
348
  </header>
349
  <p>{message.text}</p>
@@ -390,7 +495,7 @@ function App() {
390
 
391
  {message.jsonPayload ? (
392
  <details className="payload-viewer">
393
- <summary>{locale === 'zh-TW' ? '結構化輸出' : 'Structured Output'}</summary>
394
  <pre>{JSON.stringify(message.jsonPayload, null, 2)}</pre>
395
  </details>
396
  ) : null}
@@ -400,10 +505,10 @@ function App() {
400
  {isWorking ? (
401
  <article className="message assistant loading">
402
  <header>
403
- <span>assistant</span>
404
  <time>{timestampLabel()}</time>
405
  </header>
406
- <p>{locale === 'zh-TW' ? '正在處理中,請稍候…' : 'Working through the request…'}</p>
407
  <div className="typing-dots" aria-hidden="true">
408
  <span />
409
  <span />
@@ -419,17 +524,15 @@ function App() {
419
  onChange={(event) => setInput(event.target.value)}
420
  rows={3}
421
  placeholder={
422
- locale === 'zh-TW'
423
- ? '描述你要示範的功能,例如:幫我搜尋 200 以下雞蛋'
424
- : 'Describe what to demo, e.g. search eggs below 200'
425
  }
426
  />
427
  <div className="composer-actions">
428
  <button type="button" className="ghost-btn" onClick={clearPanels} disabled={isWorking}>
429
- Clear Panels
430
  </button>
431
  <button type="submit" className="primary-btn" disabled={isWorking || !input.trim()}>
432
- {isWorking ? 'Running…' : 'Run Demo'}
433
  </button>
434
  </div>
435
  </form>
@@ -440,8 +543,8 @@ function App() {
440
  {showTracePanel ? (
441
  <aside className="panel trace-panel">
442
  <div className="panel-header">
443
- <h2>Agent Trace</h2>
444
- <p>{isWorking ? 'Live events' : 'Last run summary'}</p>
445
  </div>
446
 
447
  {traceItems.length ? (
@@ -449,14 +552,14 @@ function App() {
449
  {traceItems.map((item) => (
450
  <li key={item.id} className={`trace-${item.status}`}>
451
  <header>
452
- <span className="kind">{item.kind}</span>
453
  <strong>{item.title}</strong>
454
  <time>{item.timestamp}</time>
455
  </header>
456
  <p>{item.detail}</p>
457
  {item.payload ? (
458
  <details>
459
- <summary>payload</summary>
460
  <pre>{JSON.stringify(item.payload, null, 2)}</pre>
461
  </details>
462
  ) : null}
@@ -465,7 +568,7 @@ function App() {
465
  </ul>
466
  ) : (
467
  <div className="trace-empty">
468
- <p>{locale === 'zh-TW' ? '等待追蹤事件…' : 'Waiting for trace events…'}</p>
469
  </div>
470
  )}
471
  </aside>
 
5
  import MarketingStudioPanel from './MarketingStudioPanel';
6
  import InvoiceDemoPanel from './InvoiceDemoPanel';
7
  import MarketplaceDemoPanel from './MarketplaceDemoPanel';
8
+ import ChatFeatureDemo from './ChatFeatureDemo';
9
+
10
+ const TAB_DEFINITIONS: Record<DemoLocale, Array<{ id: DemoTabId; label: string; subtitle: string }>> = {
11
+ en: [
12
+ { id: 'chat', label: 'Chat Agent', subtitle: 'Tool-first orchestration' },
13
+ { id: 'voice', label: 'Voice Agent', subtitle: 'Speech-style actions' },
14
+ { id: 'marketing', label: 'Marketing Studio', subtitle: 'Copy + visual workflow' },
15
+ { id: 'invoice', label: 'Invoice AI', subtitle: 'OCR + extraction demo' },
16
+ { id: 'marketplace', label: 'Marketplace', subtitle: 'Search + ranking + stats' },
17
+ ],
18
+ 'zh-TW': [
19
+ { id: 'chat', label: '聊天代理', subtitle: '工具優先協作流程' },
20
+ { id: 'voice', label: '語音代理', subtitle: '語音式操作流程' },
21
+ { id: 'marketing', label: '行銷工作室', subtitle: '文案 + 視覺素材流程' },
22
+ { id: 'invoice', label: '發票 AI', subtitle: 'OCR + 欄位擷取示範' },
23
+ { id: 'marketplace', label: '市集探索', subtitle: '搜尋 + 排名 + 統計' },
24
+ ],
25
+ };
26
+
27
+ const APP_COPY = {
28
+ en: {
29
+ eyebrow: 'Farm2Market Demo',
30
+ title: 'Agent Workspace',
31
+ statusRunning: 'Running',
32
+ statusIdle: 'Idle',
33
+ hideControls: 'Hide Controls',
34
+ showControls: 'Show Controls',
35
+ tabAria: 'Agent tabs',
36
+ controlsTitle: 'Controls',
37
+ controlsSubtitle: 'Switch scenario and prompt packs.',
38
+ demoMode: 'Demo Mode',
39
+ modeMock: 'Mock (stable)',
40
+ modeLive: 'Live (backend)',
41
+ language: 'Language',
42
+ english: 'English',
43
+ traditionalChinese: 'Traditional Chinese',
44
+ tryPrompts: 'Try Prompts',
45
+ progressTitle: 'Model Progress',
46
+ progressRunning: 'In progress',
47
+ progressDone: 'Completed',
48
+ structuredOutput: 'Structured Output',
49
+ workingMessage: 'Working through the request…',
50
+ composerPlaceholder: 'Describe what to demo, e.g. search eggs below 200',
51
+ clearPanels: 'Clear Panels',
52
+ runningAction: 'Running…',
53
+ runDemo: 'Run Demo',
54
+ traceTitle: 'Agent Trace',
55
+ traceLive: 'Live events',
56
+ traceLast: 'Last run summary',
57
+ tracePayload: 'payload',
58
+ traceWaiting: 'Waiting for trace events…',
59
+ unknownError: 'Unknown runtime error',
60
+ roleLabels: {
61
+ user: 'user',
62
+ assistant: 'assistant',
63
+ system: 'system',
64
+ },
65
+ kindLabels: {
66
+ planner: 'planner',
67
+ tool: 'tool',
68
+ validator: 'validator',
69
+ renderer: 'renderer',
70
+ },
71
+ },
72
+ 'zh-TW': {
73
+ eyebrow: 'Farm2Market 示範',
74
+ title: '代理工作台',
75
+ statusRunning: '執行中',
76
+ statusIdle: '待命',
77
+ hideControls: '隱藏控制',
78
+ showControls: '顯示控制',
79
+ tabAria: '代理分頁',
80
+ controlsTitle: '控制面板',
81
+ controlsSubtitle: '切換情境與提示組合。',
82
+ demoMode: '示範模式',
83
+ modeMock: '模擬(穩定)',
84
+ modeLive: '即時(後端)',
85
+ language: '語言',
86
+ english: '英文',
87
+ traditionalChinese: '繁體中文',
88
+ tryPrompts: '快速提示',
89
+ progressTitle: '模型處理進度',
90
+ progressRunning: '執行中',
91
+ progressDone: '完成',
92
+ structuredOutput: '結構化輸出',
93
+ workingMessage: '正在處理中,請稍候…',
94
+ composerPlaceholder: '描述你要示範的功能,例如:幫我搜尋 200 以下雞蛋',
95
+ clearPanels: '清除面板',
96
+ runningAction: '執行中…',
97
+ runDemo: '執行示範',
98
+ traceTitle: '代理追蹤',
99
+ traceLive: '即時事件',
100
+ traceLast: '最近一次摘要',
101
+ tracePayload: '載荷',
102
+ traceWaiting: '等待追蹤事件…',
103
+ unknownError: '未知執行錯誤',
104
+ roleLabels: {
105
+ user: '使用者',
106
+ assistant: '助理',
107
+ system: '系統',
108
+ },
109
+ kindLabels: {
110
+ planner: '規劃',
111
+ tool: '工具',
112
+ validator: '驗證',
113
+ renderer: '輸出',
114
+ },
115
+ },
116
+ } as const;
117
 
118
  function timestampLabel(): string {
119
  return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
 
163
  const firstRenderRef = useRef(true);
164
  const threadRef = useRef<HTMLDivElement | null>(null);
165
 
166
+ const copy = APP_COPY[locale];
167
+ const tabs = TAB_DEFINITIONS[locale];
168
  const quickPrompts = useMemo(() => getQuickPrompts(activeTab, locale), [activeTab, locale]);
169
  const showTracePanel = isWorking || traceItems.length > 0;
170
 
 
261
  }),
262
  ]);
263
  } catch (error) {
264
+ const message = error instanceof Error ? error.message : copy.unknownError;
265
  setMessages((prev) => [
266
  ...prev,
267
  createMessage(
 
286
 
287
  <header className="topbar">
288
  <div>
289
+ <p className="eyebrow">{copy.eyebrow}</p>
290
+ <h1>{copy.title}</h1>
291
  </div>
292
 
293
  <div className="topbar-actions">
294
+ <span className={`status-pill ${isWorking ? 'busy' : 'idle'}`}>{isWorking ? copy.statusRunning : copy.statusIdle}</span>
295
  <button className="ghost-btn mobile-only" type="button" onClick={() => setMobileControlsOpen((prev) => !prev)}>
296
+ {mobileControlsOpen ? copy.hideControls : copy.showControls}
297
  </button>
298
  </div>
299
  </header>
300
 
301
+ <nav className="tab-row" aria-label={copy.tabAria}>
302
+ {tabs.map((tab) => (
303
  <button
304
  key={tab.id}
305
  type="button"
 
315
  <div className="workspace">
316
  <aside className="panel controls-panel">
317
  <div className="panel-header">
318
+ <h2>{copy.controlsTitle}</h2>
319
+ <p>{copy.controlsSubtitle}</p>
320
  </div>
321
 
322
  <label className="field">
323
+ <span>{copy.demoMode}</span>
324
  <select value={mode} onChange={(event) => setMode(event.target.value as DemoMode)}>
325
+ <option value="mock">{copy.modeMock}</option>
326
+ <option value="live">{copy.modeLive}</option>
327
  </select>
328
  </label>
329
 
330
  <label className="field">
331
+ <span>{copy.language}</span>
332
  <select value={locale} onChange={(event) => setLocale(event.target.value as DemoLocale)}>
333
+ <option value="en">{copy.english}</option>
334
+ <option value="zh-TW">{copy.traditionalChinese}</option>
335
  </select>
336
  </label>
337
 
338
  <section className="prompt-bank">
339
+ <h3>{copy.tryPrompts}</h3>
340
  <div className="chip-list">
341
  {quickPrompts.map((prompt) => (
342
  <button
 
379
  {progressVisible && progressSteps.length > 0 ? (
380
  <section className="progress-panel" aria-live="polite">
381
  <header>
382
+ <h3>{copy.progressTitle}</h3>
383
+ <span>{isWorking ? copy.progressRunning : copy.progressDone}</span>
384
  </header>
385
  <ul>
386
  {progressSteps.map((step) => (
 
442
  />
443
  ) : (
444
  <>
445
+ <ChatFeatureDemo locale={locale} isBusy={isWorking} />
446
+
447
  <div className="thread" ref={threadRef}>
448
  {messages.map((message) => (
449
  <article key={message.id} className={`message ${message.role}`}>
450
  <header>
451
+ <span>{copy.roleLabels[message.role]}</span>
452
  <time>{message.timestamp}</time>
453
  </header>
454
  <p>{message.text}</p>
 
495
 
496
  {message.jsonPayload ? (
497
  <details className="payload-viewer">
498
+ <summary>{copy.structuredOutput}</summary>
499
  <pre>{JSON.stringify(message.jsonPayload, null, 2)}</pre>
500
  </details>
501
  ) : null}
 
505
  {isWorking ? (
506
  <article className="message assistant loading">
507
  <header>
508
+ <span>{copy.roleLabels.assistant}</span>
509
  <time>{timestampLabel()}</time>
510
  </header>
511
+ <p>{copy.workingMessage}</p>
512
  <div className="typing-dots" aria-hidden="true">
513
  <span />
514
  <span />
 
524
  onChange={(event) => setInput(event.target.value)}
525
  rows={3}
526
  placeholder={
527
+ copy.composerPlaceholder
 
 
528
  }
529
  />
530
  <div className="composer-actions">
531
  <button type="button" className="ghost-btn" onClick={clearPanels} disabled={isWorking}>
532
+ {copy.clearPanels}
533
  </button>
534
  <button type="submit" className="primary-btn" disabled={isWorking || !input.trim()}>
535
+ {isWorking ? copy.runningAction : copy.runDemo}
536
  </button>
537
  </div>
538
  </form>
 
543
  {showTracePanel ? (
544
  <aside className="panel trace-panel">
545
  <div className="panel-header">
546
+ <h2>{copy.traceTitle}</h2>
547
+ <p>{isWorking ? copy.traceLive : copy.traceLast}</p>
548
  </div>
549
 
550
  {traceItems.length ? (
 
552
  {traceItems.map((item) => (
553
  <li key={item.id} className={`trace-${item.status}`}>
554
  <header>
555
+ <span className="kind">{copy.kindLabels[item.kind]}</span>
556
  <strong>{item.title}</strong>
557
  <time>{item.timestamp}</time>
558
  </header>
559
  <p>{item.detail}</p>
560
  {item.payload ? (
561
  <details>
562
+ <summary>{copy.tracePayload}</summary>
563
  <pre>{JSON.stringify(item.payload, null, 2)}</pre>
564
  </details>
565
  ) : null}
 
568
  </ul>
569
  ) : (
570
  <div className="trace-empty">
571
+ <p>{copy.traceWaiting}</p>
572
  </div>
573
  )}
574
  </aside>
src/ChatFeatureDemo.tsx ADDED
@@ -0,0 +1,325 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import type { DemoLocale } from './types';
3
+
4
+ type ChatDemoScene = {
5
+ id: 'prompt' | 'workflow' | 'output';
6
+ title: string;
7
+ caption: string;
8
+ prompt: string;
9
+ tool: string;
10
+ result: string;
11
+ durationMs: number;
12
+ };
13
+
14
+ type ChatFeatureDemoProps = {
15
+ locale: DemoLocale;
16
+ isBusy: boolean;
17
+ };
18
+
19
+ const COPY = {
20
+ en: {
21
+ title: 'Feature Walkthrough',
22
+ subtitle: 'Video-style preview of how the Chat Agent works',
23
+ stateBusy: 'Live run active',
24
+ statePlaying: 'Playing demo',
25
+ statePaused: 'Demo paused',
26
+ play: 'Play',
27
+ pause: 'Pause',
28
+ replay: 'Replay',
29
+ hide: 'Hide',
30
+ show: 'Show Demo',
31
+ hiddenBusy: 'Walkthrough hidden while the agent is running.',
32
+ hiddenIdle: 'Walkthrough hidden.',
33
+ reducedMotion: 'Autoplay is disabled because reduced motion is enabled.',
34
+ videoName: 'chat_agent_walkthrough.mp4',
35
+ lanePrompt: 'Prompt',
36
+ laneWorkflow: 'Workflow',
37
+ laneOutput: 'Output',
38
+ quickPromptLabel: 'quick prompt',
39
+ workflowProgress: 'progress panel update',
40
+ workflowTrace: 'trace event recorded',
41
+ resultCardTitle: 'Result Card',
42
+ payloadCardTitle: 'Structured Payload',
43
+ payloadCardText: 'JSON preview + action chips',
44
+ },
45
+ 'zh-TW': {
46
+ title: '功能導覽',
47
+ subtitle: '以影片風格快速示範 Chat Agent 使用方式',
48
+ stateBusy: '執行中',
49
+ statePlaying: '導覽播放中',
50
+ statePaused: '導覽已暫停',
51
+ play: '播放',
52
+ pause: '暫停',
53
+ replay: '重播',
54
+ hide: '隱藏',
55
+ show: '顯示導覽',
56
+ hiddenBusy: '模型執行中,已暫時隱藏導覽。',
57
+ hiddenIdle: '導覽已隱藏。',
58
+ reducedMotion: '你啟用了減少動畫,已停用自動播放。',
59
+ videoName: '聊天導覽影片.mp4',
60
+ lanePrompt: '輸入',
61
+ laneWorkflow: '流程',
62
+ laneOutput: '輸出',
63
+ quickPromptLabel: '快速提示',
64
+ workflowProgress: '進度面板更新',
65
+ workflowTrace: '追蹤事件記錄',
66
+ resultCardTitle: '結果卡片',
67
+ payloadCardTitle: '結構化載荷',
68
+ payloadCardText: 'JSON 預覽 + 動作標籤',
69
+ },
70
+ } as const;
71
+
72
+ const SCENES = {
73
+ en: [
74
+ {
75
+ id: 'prompt',
76
+ title: 'Step 1: Prompt and context',
77
+ caption: 'User chooses a quick prompt or types intent in natural language.',
78
+ prompt: 'Find eggs under NT$200 and rank by freshness.',
79
+ tool: 'Prompt parser prepares query tokens and constraints',
80
+ result: 'Input normalized and queued',
81
+ durationMs: 1700,
82
+ },
83
+ {
84
+ id: 'workflow',
85
+ title: 'Step 2: Tool orchestration',
86
+ caption: 'Planner selects tools and the progress panel updates in real time.',
87
+ prompt: 'Routing: product_search_public + score ranking',
88
+ tool: 'Planner validates filters and runs tool chain',
89
+ result: 'Structured candidate set is ready',
90
+ durationMs: 1750,
91
+ },
92
+ {
93
+ id: 'output',
94
+ title: 'Step 3: Structured answer',
95
+ caption: 'Assistant responds with cards, metrics, actions, and JSON payload.',
96
+ prompt: 'Rendering response bundle for UI cards',
97
+ tool: 'Renderer composes summary and recommendation cards',
98
+ result: 'Chat output card with payload attached',
99
+ durationMs: 1750,
100
+ },
101
+ ],
102
+ 'zh-TW': [
103
+ {
104
+ id: 'prompt',
105
+ title: '步驟 1:輸入需求',
106
+ caption: '使用者可點擊快速提示,或直接輸入自然語句。',
107
+ prompt: '找出 200 元以下雞蛋,並依新鮮度排序。',
108
+ tool: '系統先解析條件與語意',
109
+ result: '已建立查詢上下文',
110
+ durationMs: 1700,
111
+ },
112
+ {
113
+ id: 'workflow',
114
+ title: '步驟 2:工具編排',
115
+ caption: '規劃器決定工具路徑,進度面板同步顯示執行狀態。',
116
+ prompt: '路由:product_search_public + 排名計算',
117
+ tool: '規劃器驗證條件並串接工具',
118
+ result: '已取得結構化候選結果',
119
+ durationMs: 1750,
120
+ },
121
+ {
122
+ id: 'output',
123
+ title: '步驟 3:輸出答案',
124
+ caption: '最後輸出摘要、卡片與 JSON 結構化資料。',
125
+ prompt: '生成可視化回覆內容',
126
+ tool: 'Renderer 整理摘要與推薦卡片',
127
+ result: '可直接展示的結果已完成',
128
+ durationMs: 1750,
129
+ },
130
+ ],
131
+ } as const satisfies Record<DemoLocale, ChatDemoScene[]>;
132
+
133
+ function ChatFeatureDemo({ locale, isBusy }: ChatFeatureDemoProps) {
134
+ const copy = COPY[locale];
135
+ const scenes = useMemo(() => SCENES[locale], [locale]);
136
+
137
+ const [sceneIndex, setSceneIndex] = useState(0);
138
+ const [elapsedMs, setElapsedMs] = useState(0);
139
+ const [isPlaying, setIsPlaying] = useState(true);
140
+ const [isCollapsed, setIsCollapsed] = useState(false);
141
+ const [reducedMotion, setReducedMotion] = useState(false);
142
+
143
+ useEffect(() => {
144
+ const media = window.matchMedia('(prefers-reduced-motion: reduce)');
145
+ const apply = () => setReducedMotion(media.matches);
146
+
147
+ apply();
148
+ if (media.addEventListener) {
149
+ media.addEventListener('change', apply);
150
+ return () => media.removeEventListener('change', apply);
151
+ }
152
+
153
+ media.addListener(apply);
154
+ return () => media.removeListener(apply);
155
+ }, []);
156
+
157
+ useEffect(() => {
158
+ if (reducedMotion) {
159
+ setIsPlaying(false);
160
+ }
161
+ }, [reducedMotion]);
162
+
163
+ useEffect(() => {
164
+ setSceneIndex(0);
165
+ setElapsedMs(0);
166
+ setIsCollapsed(false);
167
+ if (!reducedMotion) {
168
+ setIsPlaying(true);
169
+ }
170
+ }, [locale, reducedMotion]);
171
+
172
+ useEffect(() => {
173
+ if (!isBusy) return;
174
+ setIsCollapsed(true);
175
+ setIsPlaying(false);
176
+ }, [isBusy]);
177
+
178
+ useEffect(() => {
179
+ if (!isPlaying || isCollapsed || isBusy || reducedMotion) return;
180
+
181
+ const timer = window.setInterval(() => {
182
+ setElapsedMs((prev) => {
183
+ const scene = scenes[sceneIndex];
184
+ if (!scene) return 0;
185
+ const next = prev + 120;
186
+ if (next < scene.durationMs) return next;
187
+ setSceneIndex((current) => (current + 1) % scenes.length);
188
+ return 0;
189
+ });
190
+ }, 120);
191
+
192
+ return () => window.clearInterval(timer);
193
+ }, [isPlaying, isCollapsed, isBusy, reducedMotion, scenes, sceneIndex]);
194
+
195
+ const activeScene = scenes[sceneIndex] ?? scenes[0];
196
+ const totalDuration = scenes.reduce((sum, scene) => sum + scene.durationMs, 0);
197
+ const previousDuration = scenes.slice(0, sceneIndex).reduce((sum, scene) => sum + scene.durationMs, 0);
198
+ const progress = totalDuration > 0 ? Math.round(((previousDuration + elapsedMs) / totalDuration) * 100) : 0;
199
+
200
+ const replay = () => {
201
+ setSceneIndex(0);
202
+ setElapsedMs(0);
203
+ if (!reducedMotion && !isBusy) {
204
+ setIsPlaying(true);
205
+ }
206
+ };
207
+
208
+ if (isCollapsed) {
209
+ return (
210
+ <section className="chat-demo-collapsed" aria-live="polite">
211
+ <p>{isBusy ? copy.hiddenBusy : copy.hiddenIdle}</p>
212
+ {!isBusy ? (
213
+ <button
214
+ type="button"
215
+ className="ghost-btn"
216
+ onClick={() => {
217
+ setIsCollapsed(false);
218
+ replay();
219
+ }}
220
+ >
221
+ {copy.show}
222
+ </button>
223
+ ) : null}
224
+ </section>
225
+ );
226
+ }
227
+
228
+ return (
229
+ <section className="chat-demo-video" aria-label={copy.title}>
230
+ <header className="chat-demo-head">
231
+ <div>
232
+ <h3>{copy.title}</h3>
233
+ <p>{copy.subtitle}</p>
234
+ {reducedMotion ? <small>{copy.reducedMotion}</small> : null}
235
+ </div>
236
+ <div className="chat-demo-head-actions">
237
+ <span className={`chat-demo-state ${isBusy ? 'busy' : isPlaying ? 'playing' : 'paused'}`}>
238
+ {isBusy ? copy.stateBusy : isPlaying ? copy.statePlaying : copy.statePaused}
239
+ </span>
240
+ <button
241
+ type="button"
242
+ className="ghost-btn"
243
+ onClick={() => setIsPlaying((prev) => !prev)}
244
+ disabled={isBusy || reducedMotion}
245
+ >
246
+ {isPlaying ? copy.pause : copy.play}
247
+ </button>
248
+ <button type="button" className="ghost-btn" onClick={replay} disabled={isBusy}>
249
+ {copy.replay}
250
+ </button>
251
+ <button type="button" className="ghost-btn" onClick={() => setIsCollapsed(true)}>
252
+ {copy.hide}
253
+ </button>
254
+ </div>
255
+ </header>
256
+
257
+ <div className="chat-demo-frame" data-scene={activeScene.id}>
258
+ <div className="chat-demo-windowbar">
259
+ <span />
260
+ <span />
261
+ <span />
262
+ <small>{copy.videoName}</small>
263
+ </div>
264
+
265
+ <div className="chat-demo-canvas">
266
+ <section className="chat-demo-lane prompts">
267
+ <h4>{copy.lanePrompt}</h4>
268
+ <div className="chat-demo-prompt-chip">{copy.quickPromptLabel}</div>
269
+ <div className="chat-demo-prompt-bubble">{activeScene.prompt}</div>
270
+ </section>
271
+
272
+ <section className="chat-demo-lane workflow">
273
+ <h4>{copy.laneWorkflow}</h4>
274
+ <div className="chat-demo-step active">{activeScene.tool}</div>
275
+ <div className="chat-demo-step">{copy.workflowProgress}</div>
276
+ <div className="chat-demo-step">{copy.workflowTrace}</div>
277
+ </section>
278
+
279
+ <section className="chat-demo-lane output">
280
+ <h4>{copy.laneOutput}</h4>
281
+ <article className="chat-demo-output-card">
282
+ <strong>{copy.resultCardTitle}</strong>
283
+ <p>{activeScene.result}</p>
284
+ </article>
285
+ <article className="chat-demo-output-card">
286
+ <strong>{copy.payloadCardTitle}</strong>
287
+ <p>{copy.payloadCardText}</p>
288
+ </article>
289
+ </section>
290
+
291
+ <span className="chat-demo-cursor" aria-hidden="true" />
292
+ </div>
293
+ </div>
294
+
295
+ <footer className="chat-demo-meta">
296
+ <div className="chat-demo-caption">
297
+ <strong>{activeScene.title}</strong>
298
+ <p>{activeScene.caption}</p>
299
+ </div>
300
+
301
+ <div className="chat-demo-progress-track" aria-hidden="true">
302
+ <span style={{ width: `${Math.min(100, Math.max(0, progress))}%` }} />
303
+ </div>
304
+
305
+ <div className="chat-demo-scene-row">
306
+ {scenes.map((scene, index) => (
307
+ <button
308
+ key={scene.id}
309
+ type="button"
310
+ className={`chat-demo-scene-pill ${index === sceneIndex ? 'active' : index < sceneIndex ? 'complete' : ''}`}
311
+ onClick={() => {
312
+ setSceneIndex(index);
313
+ setElapsedMs(0);
314
+ }}
315
+ >
316
+ {scene.title}
317
+ </button>
318
+ ))}
319
+ </div>
320
+ </footer>
321
+ </section>
322
+ );
323
+ }
324
+
325
+ export default ChatFeatureDemo;
src/InvoiceDemoPanel.tsx CHANGED
@@ -1,5 +1,6 @@
1
  import { ChangeEvent, useEffect, useRef, useState } from 'react';
2
  import type { DemoLocale, ProgressStep, ProgressStepStatus, TraceItem } from './types';
 
3
 
4
  type InvoiceDirection = 'sale' | 'purchase';
5
  type InvoiceStatus = 'draft' | 'open' | 'paid' | 'overdue';
@@ -718,17 +719,18 @@ export default function InvoiceDemoPanel({
718
  const handleUpload = async (event: ChangeEvent<HTMLInputElement>) => {
719
  const file = event.target.files?.[0];
720
  if (!file) return;
 
721
 
722
  const previewUrl = await new Promise<string>((resolve, reject) => {
723
  const reader = new FileReader();
724
  reader.onload = () => {
725
  if (typeof reader.result !== 'string') {
726
- reject(new Error('Unable to read file'));
727
  return;
728
  }
729
  resolve(reader.result);
730
  };
731
- reader.onerror = () => reject(new Error('Unable to read file'));
732
  reader.readAsDataURL(file);
733
  });
734
 
@@ -765,6 +767,8 @@ export default function InvoiceDemoPanel({
765
 
766
  return (
767
  <div className="invoice-demo-root">
 
 
768
  <header className="invoice-demo-header">
769
  <div>
770
  <h3>{copy.title}</h3>
 
1
  import { ChangeEvent, useEffect, useRef, useState } from 'react';
2
  import type { DemoLocale, ProgressStep, ProgressStepStatus, TraceItem } from './types';
3
+ import InvoiceFeatureDemo from './InvoiceFeatureDemo';
4
 
5
  type InvoiceDirection = 'sale' | 'purchase';
6
  type InvoiceStatus = 'draft' | 'open' | 'paid' | 'overdue';
 
719
  const handleUpload = async (event: ChangeEvent<HTMLInputElement>) => {
720
  const file = event.target.files?.[0];
721
  if (!file) return;
722
+ const readErrorMessage = locale === 'zh-TW' ? '無法讀取檔案' : 'Unable to read file';
723
 
724
  const previewUrl = await new Promise<string>((resolve, reject) => {
725
  const reader = new FileReader();
726
  reader.onload = () => {
727
  if (typeof reader.result !== 'string') {
728
+ reject(new Error(readErrorMessage));
729
  return;
730
  }
731
  resolve(reader.result);
732
  };
733
+ reader.onerror = () => reject(new Error(readErrorMessage));
734
  reader.readAsDataURL(file);
735
  });
736
 
 
767
 
768
  return (
769
  <div className="invoice-demo-root">
770
+ <InvoiceFeatureDemo locale={locale} isBusy={isScanning} />
771
+
772
  <header className="invoice-demo-header">
773
  <div>
774
  <h3>{copy.title}</h3>
src/InvoiceFeatureDemo.tsx ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import type { DemoLocale } from './types';
3
+
4
+ type InvoiceScene = {
5
+ id: 'source' | 'ocr' | 'fields';
6
+ durationMs: number;
7
+ };
8
+
9
+ type InvoiceFeatureDemoProps = {
10
+ locale: DemoLocale;
11
+ isBusy: boolean;
12
+ };
13
+
14
+ const COPY = {
15
+ en: {
16
+ title: 'Invoice Demo',
17
+ busy: 'Running',
18
+ hidden: 'Hidden',
19
+ play: 'Play',
20
+ pause: 'Pause',
21
+ replay: 'Replay',
22
+ hide: 'Hide',
23
+ show: 'Show',
24
+ videoName: 'invoice_demo.webm',
25
+ step: 'Step',
26
+ lanes: {
27
+ source: 'Source',
28
+ ocr: 'OCR',
29
+ fields: 'Fields',
30
+ },
31
+ captions: {
32
+ source: 'Upload and validate invoice source',
33
+ ocr: 'Extract raw text and parse blocks',
34
+ fields: 'Return structured invoice fields',
35
+ },
36
+ words: {
37
+ source: ['File', 'Role', 'Note'],
38
+ ocr: ['Read', 'Parse', 'Check'],
39
+ fields: ['Total', 'Due', 'Status'],
40
+ streamWords: ['Reading', 'Parsing', 'Checking', 'Packing'],
41
+ sourceImage: 'https://upload.wikimedia.org/wikipedia/commons/9/90/Standard_Fapiao.jpg',
42
+ },
43
+ },
44
+ 'zh-TW': {
45
+ title: '發票導覽',
46
+ busy: '執行中',
47
+ hidden: '已隱藏',
48
+ play: '播放',
49
+ pause: '暫停',
50
+ replay: '重播',
51
+ hide: '隱藏',
52
+ show: '顯示',
53
+ videoName: '發票導覽影片.webm',
54
+ step: '步驟',
55
+ lanes: {
56
+ source: '來源',
57
+ ocr: '辨識',
58
+ fields: '欄位',
59
+ },
60
+ captions: {
61
+ source: '上傳並驗證發票來源',
62
+ ocr: '擷取原文並解析區塊',
63
+ fields: '輸出結構化發票欄位',
64
+ },
65
+ words: {
66
+ source: ['檔案', '角色', '備註'],
67
+ ocr: ['讀取', '解析', '驗證'],
68
+ fields: ['總額', '到期', '狀態'],
69
+ streamWords: ['讀取中', '解析中', '驗證中', '封裝中'],
70
+ sourceImage: 'https://upload.wikimedia.org/wikipedia/commons/9/90/Standard_Fapiao.jpg',
71
+ },
72
+ },
73
+ } as const;
74
+
75
+ const SCENES: InvoiceScene[] = [
76
+ { id: 'source', durationMs: 1650 },
77
+ { id: 'ocr', durationMs: 1750 },
78
+ { id: 'fields', durationMs: 1700 },
79
+ ];
80
+
81
+ export default function InvoiceFeatureDemo({ locale, isBusy }: InvoiceFeatureDemoProps) {
82
+ const copy = COPY[locale];
83
+ const scenes = useMemo(() => SCENES, []);
84
+
85
+ const [sceneIndex, setSceneIndex] = useState(0);
86
+ const [elapsedMs, setElapsedMs] = useState(0);
87
+ const [isPlaying, setIsPlaying] = useState(true);
88
+ const [isCollapsed, setIsCollapsed] = useState(false);
89
+ const [reducedMotion, setReducedMotion] = useState(false);
90
+
91
+ useEffect(() => {
92
+ const media = window.matchMedia('(prefers-reduced-motion: reduce)');
93
+ const apply = () => setReducedMotion(media.matches);
94
+ apply();
95
+ if (media.addEventListener) {
96
+ media.addEventListener('change', apply);
97
+ return () => media.removeEventListener('change', apply);
98
+ }
99
+ media.addListener(apply);
100
+ return () => media.removeListener(apply);
101
+ }, []);
102
+
103
+ useEffect(() => {
104
+ if (reducedMotion) setIsPlaying(false);
105
+ }, [reducedMotion]);
106
+
107
+ useEffect(() => {
108
+ setSceneIndex(0);
109
+ setElapsedMs(0);
110
+ setIsCollapsed(false);
111
+ if (!reducedMotion) setIsPlaying(true);
112
+ }, [locale, reducedMotion]);
113
+
114
+ useEffect(() => {
115
+ if (!isBusy) return;
116
+ setIsCollapsed(true);
117
+ setIsPlaying(false);
118
+ }, [isBusy]);
119
+
120
+ useEffect(() => {
121
+ if (!isPlaying || isCollapsed || isBusy || reducedMotion) return;
122
+ const timer = window.setInterval(() => {
123
+ setElapsedMs((prev) => {
124
+ const scene = scenes[sceneIndex];
125
+ if (!scene) return 0;
126
+ const next = prev + 120;
127
+ if (next < scene.durationMs) return next;
128
+ setSceneIndex((current) => (current + 1) % scenes.length);
129
+ return 0;
130
+ });
131
+ }, 120);
132
+ return () => window.clearInterval(timer);
133
+ }, [isPlaying, isCollapsed, isBusy, reducedMotion, scenes, sceneIndex]);
134
+
135
+ const activeScene = scenes[sceneIndex] ?? scenes[0];
136
+ const totalDuration = scenes.reduce((sum, scene) => sum + scene.durationMs, 0);
137
+ const previousDuration = scenes.slice(0, sceneIndex).reduce((sum, scene) => sum + scene.durationMs, 0);
138
+ const progress = totalDuration > 0 ? Math.round(((previousDuration + elapsedMs) / totalDuration) * 100) : 0;
139
+ const sceneCaption = copy.captions[activeScene.id];
140
+ const streamOffset = Math.floor(elapsedMs / 420);
141
+ const activeStreamWords = copy.words.streamWords.map(
142
+ (_word, index) => copy.words.streamWords[(streamOffset + index) % copy.words.streamWords.length]
143
+ );
144
+
145
+ const replay = () => {
146
+ setSceneIndex(0);
147
+ setElapsedMs(0);
148
+ if (!reducedMotion && !isBusy) setIsPlaying(true);
149
+ };
150
+
151
+ if (isCollapsed) {
152
+ return (
153
+ <section className="invoice-walk-collapsed" aria-live="polite">
154
+ <div className="invoice-walk-mini-markers" aria-hidden="true">
155
+ <span />
156
+ <span />
157
+ <span />
158
+ </div>
159
+ <p>{isBusy ? copy.busy : copy.hidden}</p>
160
+ {!isBusy ? (
161
+ <button
162
+ type="button"
163
+ className="ghost-btn"
164
+ onClick={() => {
165
+ setIsCollapsed(false);
166
+ replay();
167
+ }}
168
+ >
169
+ {copy.show}
170
+ </button>
171
+ ) : null}
172
+ </section>
173
+ );
174
+ }
175
+
176
+ return (
177
+ <section className="invoice-walk-video" aria-label={copy.title}>
178
+ <header className="invoice-walk-head">
179
+ <div>
180
+ <h3>{copy.title}</h3>
181
+ <small>
182
+ {copy.step} {sceneIndex + 1}/{scenes.length}
183
+ </small>
184
+ </div>
185
+ <div className="invoice-walk-actions">
186
+ <button
187
+ type="button"
188
+ className="invoice-walk-icon-btn"
189
+ onClick={() => setIsPlaying((prev) => !prev)}
190
+ disabled={isBusy || reducedMotion}
191
+ aria-label={isPlaying ? copy.pause : copy.play}
192
+ >
193
+ {isPlaying ? '||' : '>'}
194
+ </button>
195
+ <button type="button" className="invoice-walk-icon-btn" onClick={replay} disabled={isBusy} aria-label={copy.replay}>
196
+ R
197
+ </button>
198
+ <button type="button" className="invoice-walk-icon-btn" onClick={() => setIsCollapsed(true)} aria-label={copy.hide}>
199
+ X
200
+ </button>
201
+ </div>
202
+ </header>
203
+
204
+ <div className="invoice-walk-frame" data-scene={activeScene.id}>
205
+ <div className="invoice-walk-windowbar">
206
+ <span />
207
+ <span />
208
+ <span />
209
+ <small>{copy.videoName}</small>
210
+ </div>
211
+
212
+ <div className="invoice-walk-canvas">
213
+ <section className="invoice-walk-lane source">
214
+ <span className="invoice-walk-lane-index">1</span>
215
+ <small className="invoice-walk-lane-label">{copy.lanes.source}</small>
216
+ <div className="invoice-walk-chip-row">
217
+ {copy.words.source.map((item) => (
218
+ <span key={item}>{item}</span>
219
+ ))}
220
+ </div>
221
+ <div className="invoice-walk-dropzone">
222
+ <img
223
+ src={copy.words.sourceImage}
224
+ alt={locale === 'zh-TW' ? '發票樣本' : 'invoice sample'}
225
+ loading="lazy"
226
+ referrerPolicy="no-referrer"
227
+ />
228
+ <p>{locale === 'zh-TW' ? 'invoice_sample.jpg' : 'invoice_sample.jpg'}</p>
229
+ </div>
230
+ </section>
231
+
232
+ <section className="invoice-walk-lane ocr">
233
+ <span className="invoice-walk-lane-index">2</span>
234
+ <small className="invoice-walk-lane-label">{copy.lanes.ocr}</small>
235
+ <div className="invoice-walk-ocr-nodes">
236
+ {copy.words.ocr.map((item) => (
237
+ <span key={item}>{item}</span>
238
+ ))}
239
+ </div>
240
+ <div className="invoice-walk-ocr-flow">
241
+ <span />
242
+ </div>
243
+ <div className="invoice-walk-ocr-lines">
244
+ {activeStreamWords.slice(0, 3).map((word, index) => (
245
+ <div key={`${word}-${index}`} className={`invoice-walk-stream-pill ${index === 0 ? 'lg' : index === 1 ? 'md' : 'sm'}`}>
246
+ <span>{word}</span>
247
+ </div>
248
+ ))}
249
+ </div>
250
+ </section>
251
+
252
+ <section className="invoice-walk-lane fields">
253
+ <span className="invoice-walk-lane-index">3</span>
254
+ <small className="invoice-walk-lane-label">{copy.lanes.fields}</small>
255
+ <div className="invoice-walk-field-grid">
256
+ {copy.words.fields.map((item) => (
257
+ <article key={item}>
258
+ <small>{item}</small>
259
+ <strong>{item === copy.words.fields[0] ? 'NT$ 6,665' : item === copy.words.fields[1] ? '2026-03-31' : locale === 'zh-TW' ? '草稿' : 'Draft'}</strong>
260
+ </article>
261
+ ))}
262
+ </div>
263
+ <p>{locale === 'zh-TW' ? '已產生 JSON 與可讀預覽。' : 'JSON payload and readable preview generated.'}</p>
264
+ </section>
265
+ </div>
266
+ </div>
267
+
268
+ <footer className="invoice-walk-meta">
269
+ <div className="invoice-walk-progress-track" aria-hidden="true">
270
+ <span style={{ width: `${Math.min(100, Math.max(0, progress))}%` }} />
271
+ </div>
272
+ <p className="invoice-walk-caption">{sceneCaption}</p>
273
+ <div className="invoice-walk-scene-dots">
274
+ {scenes.map((scene, index) => (
275
+ <button
276
+ key={scene.id}
277
+ type="button"
278
+ className={`invoice-walk-scene-dot ${index === sceneIndex ? 'active' : index < sceneIndex ? 'complete' : ''}`}
279
+ onClick={() => {
280
+ setSceneIndex(index);
281
+ setElapsedMs(0);
282
+ }}
283
+ aria-label={`${copy.step} ${index + 1}`}
284
+ >
285
+ <span />
286
+ </button>
287
+ ))}
288
+ </div>
289
+ </footer>
290
+ </section>
291
+ );
292
+ }
src/MarketingFeatureDemo.tsx ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import type { DemoLocale } from './types';
3
+
4
+ type MarketingScene = {
5
+ id: 'brief' | 'copy' | 'visual';
6
+ durationMs: number;
7
+ };
8
+
9
+ type MarketingFeatureDemoProps = {
10
+ locale: DemoLocale;
11
+ isBusy: boolean;
12
+ };
13
+
14
+ const COPY = {
15
+ en: {
16
+ title: 'Marketing Demo',
17
+ busy: 'Running',
18
+ hidden: 'Hidden',
19
+ play: 'Play',
20
+ pause: 'Pause',
21
+ replay: 'Replay',
22
+ hide: 'Hide',
23
+ show: 'Show',
24
+ videoName: 'marketing_demo.webm',
25
+ step: 'Step',
26
+ lanes: {
27
+ brief: 'Brief',
28
+ copy: 'Copy',
29
+ visual: 'Visual',
30
+ },
31
+ captions: {
32
+ brief: 'Collect product + tone inputs',
33
+ copy: 'Stream campaign copy blocks',
34
+ visual: 'Generate image variations',
35
+ },
36
+ words: {
37
+ briefChips: ['Product', 'Tone', 'Channel'],
38
+ copyChips: ['Headline', 'Caption', 'CTA', 'Tags'],
39
+ visualCards: ['Hero', 'Social'],
40
+ streamWords: ['Drafting', 'Refining', 'Scoring', 'Polishing'],
41
+ visualImages: [
42
+ 'https://upload.wikimedia.org/wikipedia/commons/d/dd/Eggs_in_basket_2020_G1.jpg',
43
+ 'https://upload.wikimedia.org/wikipedia/commons/c/c2/Fresh_Vegetables_2.jpg',
44
+ ],
45
+ },
46
+ },
47
+ 'zh-TW': {
48
+ title: '行銷導覽',
49
+ busy: '執行中',
50
+ hidden: '已隱藏',
51
+ play: '播放',
52
+ pause: '暫停',
53
+ replay: '重播',
54
+ hide: '隱藏',
55
+ show: '顯示',
56
+ videoName: '行銷導覽影片.webm',
57
+ step: '步驟',
58
+ lanes: {
59
+ brief: '需求',
60
+ copy: '文案',
61
+ visual: '素材',
62
+ },
63
+ captions: {
64
+ brief: '收集產品與語氣設定',
65
+ copy: '串流活動文案區塊',
66
+ visual: '生成多版本圖片',
67
+ },
68
+ words: {
69
+ briefChips: ['產品', '語氣', '渠道'],
70
+ copyChips: ['標題', '內文', 'CTA', '標籤'],
71
+ visualCards: ['主視覺', '社群版'],
72
+ streamWords: ['生成', '潤稿', '評估', '優化'],
73
+ visualImages: [
74
+ 'https://upload.wikimedia.org/wikipedia/commons/d/dd/Eggs_in_basket_2020_G1.jpg',
75
+ 'https://upload.wikimedia.org/wikipedia/commons/c/c2/Fresh_Vegetables_2.jpg',
76
+ ],
77
+ },
78
+ },
79
+ } as const;
80
+
81
+ const SCENES: MarketingScene[] = [
82
+ { id: 'brief', durationMs: 1650 },
83
+ { id: 'copy', durationMs: 1750 },
84
+ { id: 'visual', durationMs: 1700 },
85
+ ];
86
+
87
+ export default function MarketingFeatureDemo({ locale, isBusy }: MarketingFeatureDemoProps) {
88
+ const copy = COPY[locale];
89
+ const scenes = useMemo(() => SCENES, []);
90
+
91
+ const [sceneIndex, setSceneIndex] = useState(0);
92
+ const [elapsedMs, setElapsedMs] = useState(0);
93
+ const [isPlaying, setIsPlaying] = useState(true);
94
+ const [isCollapsed, setIsCollapsed] = useState(false);
95
+ const [reducedMotion, setReducedMotion] = useState(false);
96
+
97
+ useEffect(() => {
98
+ const media = window.matchMedia('(prefers-reduced-motion: reduce)');
99
+ const apply = () => setReducedMotion(media.matches);
100
+ apply();
101
+ if (media.addEventListener) {
102
+ media.addEventListener('change', apply);
103
+ return () => media.removeEventListener('change', apply);
104
+ }
105
+ media.addListener(apply);
106
+ return () => media.removeListener(apply);
107
+ }, []);
108
+
109
+ useEffect(() => {
110
+ if (reducedMotion) setIsPlaying(false);
111
+ }, [reducedMotion]);
112
+
113
+ useEffect(() => {
114
+ setSceneIndex(0);
115
+ setElapsedMs(0);
116
+ setIsCollapsed(false);
117
+ if (!reducedMotion) setIsPlaying(true);
118
+ }, [locale, reducedMotion]);
119
+
120
+ useEffect(() => {
121
+ if (!isBusy) return;
122
+ setIsCollapsed(true);
123
+ setIsPlaying(false);
124
+ }, [isBusy]);
125
+
126
+ useEffect(() => {
127
+ if (!isPlaying || isCollapsed || isBusy || reducedMotion) return;
128
+ const timer = window.setInterval(() => {
129
+ setElapsedMs((prev) => {
130
+ const scene = scenes[sceneIndex];
131
+ if (!scene) return 0;
132
+ const next = prev + 120;
133
+ if (next < scene.durationMs) return next;
134
+ setSceneIndex((current) => (current + 1) % scenes.length);
135
+ return 0;
136
+ });
137
+ }, 120);
138
+ return () => window.clearInterval(timer);
139
+ }, [isPlaying, isCollapsed, isBusy, reducedMotion, scenes, sceneIndex]);
140
+
141
+ const activeScene = scenes[sceneIndex] ?? scenes[0];
142
+ const totalDuration = scenes.reduce((sum, scene) => sum + scene.durationMs, 0);
143
+ const previousDuration = scenes.slice(0, sceneIndex).reduce((sum, scene) => sum + scene.durationMs, 0);
144
+ const progress = totalDuration > 0 ? Math.round(((previousDuration + elapsedMs) / totalDuration) * 100) : 0;
145
+ const sceneCaption = copy.captions[activeScene.id];
146
+ const streamOffset = Math.floor(elapsedMs / 420);
147
+ const activeStreamWords = copy.words.streamWords.map(
148
+ (_word, index) => copy.words.streamWords[(streamOffset + index) % copy.words.streamWords.length]
149
+ );
150
+
151
+ const replay = () => {
152
+ setSceneIndex(0);
153
+ setElapsedMs(0);
154
+ if (!reducedMotion && !isBusy) setIsPlaying(true);
155
+ };
156
+
157
+ if (isCollapsed) {
158
+ return (
159
+ <section className="marketing-walk-collapsed" aria-live="polite">
160
+ <div className="marketing-walk-mini-markers" aria-hidden="true">
161
+ <span />
162
+ <span />
163
+ <span />
164
+ </div>
165
+ <p>{isBusy ? copy.busy : copy.hidden}</p>
166
+ {!isBusy ? (
167
+ <button
168
+ type="button"
169
+ className="ghost-btn"
170
+ onClick={() => {
171
+ setIsCollapsed(false);
172
+ replay();
173
+ }}
174
+ >
175
+ {copy.show}
176
+ </button>
177
+ ) : null}
178
+ </section>
179
+ );
180
+ }
181
+
182
+ return (
183
+ <section className="marketing-walk-video" aria-label={copy.title}>
184
+ <header className="marketing-walk-head">
185
+ <div>
186
+ <h3>{copy.title}</h3>
187
+ <small>
188
+ {copy.step} {sceneIndex + 1}/{scenes.length}
189
+ </small>
190
+ </div>
191
+ <div className="marketing-walk-actions">
192
+ <button
193
+ type="button"
194
+ className="marketing-walk-icon-btn"
195
+ onClick={() => setIsPlaying((prev) => !prev)}
196
+ disabled={isBusy || reducedMotion}
197
+ aria-label={isPlaying ? copy.pause : copy.play}
198
+ >
199
+ {isPlaying ? '||' : '>'}
200
+ </button>
201
+ <button type="button" className="marketing-walk-icon-btn" onClick={replay} disabled={isBusy} aria-label={copy.replay}>
202
+ R
203
+ </button>
204
+ <button type="button" className="marketing-walk-icon-btn" onClick={() => setIsCollapsed(true)} aria-label={copy.hide}>
205
+ X
206
+ </button>
207
+ </div>
208
+ </header>
209
+
210
+ <div className="marketing-walk-frame" data-scene={activeScene.id}>
211
+ <div className="marketing-walk-windowbar">
212
+ <span />
213
+ <span />
214
+ <span />
215
+ <small>{copy.videoName}</small>
216
+ </div>
217
+
218
+ <div className="marketing-walk-canvas">
219
+ <section className="marketing-walk-lane brief">
220
+ <span className="marketing-walk-lane-index">1</span>
221
+ <small className="marketing-walk-lane-label">{copy.lanes.brief}</small>
222
+ <div className="marketing-walk-chip-row">
223
+ {copy.words.briefChips.map((item) => (
224
+ <span key={item}>{item}</span>
225
+ ))}
226
+ </div>
227
+ <div className="marketing-walk-line lg" />
228
+ <div className="marketing-walk-line md" />
229
+ <p>{locale === 'zh-TW' ? '產品資訊與活動目標已整理。' : 'Product context and campaign goal prepared.'}</p>
230
+ </section>
231
+
232
+ <section className="marketing-walk-lane copy">
233
+ <span className="marketing-walk-lane-index">2</span>
234
+ <small className="marketing-walk-lane-label">{copy.lanes.copy}</small>
235
+ <div className="marketing-walk-copy-card">
236
+ {activeStreamWords.slice(0, 3).map((word, index) => (
237
+ <div key={`${word}-${index}`} className={`marketing-walk-stream-pill ${index === 0 ? 'lg' : index === 1 ? 'sm' : 'md'}`}>
238
+ <span>{word}</span>
239
+ </div>
240
+ ))}
241
+ </div>
242
+ <div className="marketing-walk-chip-row">
243
+ {copy.words.copyChips.map((item) => (
244
+ <span key={item}>{item}</span>
245
+ ))}
246
+ </div>
247
+ </section>
248
+
249
+ <section className="marketing-walk-lane visual">
250
+ <span className="marketing-walk-lane-index">3</span>
251
+ <small className="marketing-walk-lane-label">{copy.lanes.visual}</small>
252
+ <div className="marketing-walk-visual-grid">
253
+ {copy.words.visualCards.map((label, index) => (
254
+ <article key={label}>
255
+ <img
256
+ src={copy.words.visualImages[index % copy.words.visualImages.length]}
257
+ alt={label}
258
+ loading="lazy"
259
+ referrerPolicy="no-referrer"
260
+ />
261
+ <small>{label}</small>
262
+ </article>
263
+ ))}
264
+ </div>
265
+ <p>{locale === 'zh-TW' ? '輸出可下載圖像與套用按鈕。' : 'Download-ready assets and apply actions.'}</p>
266
+ </section>
267
+ </div>
268
+ </div>
269
+
270
+ <footer className="marketing-walk-meta">
271
+ <div className="marketing-walk-progress-track" aria-hidden="true">
272
+ <span style={{ width: `${Math.min(100, Math.max(0, progress))}%` }} />
273
+ </div>
274
+ <p className="marketing-walk-caption">{sceneCaption}</p>
275
+ <div className="marketing-walk-scene-dots">
276
+ {scenes.map((scene, index) => (
277
+ <button
278
+ key={scene.id}
279
+ type="button"
280
+ className={`marketing-walk-scene-dot ${index === sceneIndex ? 'active' : index < sceneIndex ? 'complete' : ''}`}
281
+ onClick={() => {
282
+ setSceneIndex(index);
283
+ setElapsedMs(0);
284
+ }}
285
+ aria-label={`${copy.step} ${index + 1}`}
286
+ >
287
+ <span />
288
+ </button>
289
+ ))}
290
+ </div>
291
+ </footer>
292
+ </section>
293
+ );
294
+ }
src/MarketingStudioPanel.tsx CHANGED
@@ -1,5 +1,6 @@
1
  import { ChangeEvent, useEffect, useMemo, useRef, useState } from 'react';
2
  import type { DemoLocale, ProgressStep, ProgressStepStatus, TraceItem } from './types';
 
3
 
4
  type MarketingStepTemplate = {
5
  id: string;
@@ -70,6 +71,12 @@ const COPY = {
70
  outputTitle: 'Generated Campaign',
71
  outputSubtitle: 'Streaming copy + image assets',
72
  metadataTitle: 'Run Metadata',
 
 
 
 
 
 
73
  noOutput: 'Run generation to see marketing copy and visuals.',
74
  streamError: 'Generation halted before completion.',
75
  stoppedByUser: 'Generation stopped by user.',
@@ -98,6 +105,12 @@ const COPY = {
98
  outputTitle: '生成結果',
99
  outputSubtitle: '即時文案串流 + 圖像素材',
100
  metadataTitle: '執行資訊',
 
 
 
 
 
 
101
  noOutput: '執行生成後可在此查看文案與圖像。',
102
  streamError: '生成流程尚未完整結束。',
103
  stoppedByUser: '已由使用者停止生成。',
@@ -334,12 +347,13 @@ function makePalette(seed: number): [string, string, string] {
334
  return palettes[seed % palettes.length];
335
  }
336
 
337
- function generateMockImageDataUrl(product: MarketingProduct, deck: MarketingDeck, index: number): string {
338
  const canvas = document.createElement('canvas');
339
  canvas.width = 960;
340
  canvas.height = 640;
341
  const ctx = canvas.getContext('2d');
342
  if (!ctx) return '';
 
343
 
344
  const [primary, secondary, light] = makePalette(index + product.name.length);
345
  const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
@@ -386,10 +400,10 @@ function generateMockImageDataUrl(product: MarketingProduct, deck: MarketingDeck
386
  ctx.fillRect(620, 120, 240, 360);
387
  ctx.fillStyle = '#1f8f4e';
388
  ctx.font = 'bold 22px "Sora", sans-serif';
389
- ctx.fillText('FARM2MARKET', 646, 170);
390
  ctx.font = '18px "IBM Plex Sans", sans-serif';
391
- ctx.fillText(index % 2 === 0 ? 'Hero Variant' : 'Social Variant', 646, 206);
392
- ctx.fillText(`Stock: ${product.stock}`, 646, 246);
393
 
394
  return canvas.toDataURL('image/png');
395
  }
@@ -550,8 +564,8 @@ export default function MarketingStudioPanel({
550
  await sleep(480);
551
  if (!mountedRef.current || runTokenRef.current !== runToken) return;
552
 
553
- const imageOne = generateMockImageDataUrl(selectedProduct, deckPayload, 0);
554
- const imageTwo = generateMockImageDataUrl(selectedProduct, deckPayload, 1);
555
 
556
  const generated: GeneratedImage[] = [imageOne, imageTwo].map((dataUrl, index) => ({
557
  id: makeId('img'),
@@ -605,17 +619,18 @@ export default function MarketingStudioPanel({
605
  const handleUpload = async (event: ChangeEvent<HTMLInputElement>) => {
606
  const file = event.target.files?.[0];
607
  if (!file) return;
 
608
 
609
  const dataUrl = await new Promise<string>((resolve, reject) => {
610
  const reader = new FileReader();
611
  reader.onload = () => {
612
  if (typeof reader.result !== 'string') {
613
- reject(new Error('Unable to read file'));
614
  return;
615
  }
616
  resolve(reader.result);
617
  };
618
- reader.onerror = () => reject(new Error('Unable to read file'));
619
  reader.readAsDataURL(file);
620
  });
621
 
@@ -674,6 +689,8 @@ export default function MarketingStudioPanel({
674
 
675
  return (
676
  <div className="marketing-studio-root">
 
 
677
  <header className="marketing-studio-header">
678
  <div>
679
  <h3>{copy.title}</h3>
@@ -864,20 +881,24 @@ export default function MarketingStudioPanel({
864
  <h5>{copy.metadataTitle}</h5>
865
  <dl>
866
  <div>
867
- <dt>Model</dt>
868
  <dd>{meta.model}</dd>
869
  </div>
870
  <div>
871
- <dt>{locale === 'zh-TW' ? '開始時間' : 'Started at'}</dt>
872
  <dd>{meta.startedAt}</dd>
873
  </div>
874
  <div>
875
- <dt>{locale === 'zh-TW' ? '耗時' : 'Duration'}</dt>
876
  <dd>{meta.durationMs} ms</dd>
877
  </div>
878
  <div>
879
- <dt>Events</dt>
880
- <dd>{images.length > 0 ? `${images.length} image + text stream` : 'text stream'}</dd>
 
 
 
 
881
  </div>
882
  </dl>
883
  </article>
 
1
  import { ChangeEvent, useEffect, useMemo, useRef, useState } from 'react';
2
  import type { DemoLocale, ProgressStep, ProgressStepStatus, TraceItem } from './types';
3
+ import MarketingFeatureDemo from './MarketingFeatureDemo';
4
 
5
  type MarketingStepTemplate = {
6
  id: string;
 
71
  outputTitle: 'Generated Campaign',
72
  outputSubtitle: 'Streaming copy + image assets',
73
  metadataTitle: 'Run Metadata',
74
+ metadataModel: 'Model',
75
+ metadataStartedAt: 'Started at',
76
+ metadataDuration: 'Duration',
77
+ metadataEvents: 'Events',
78
+ metadataEventsWithImages: '{count} image + text stream',
79
+ metadataEventsTextOnly: 'text stream',
80
  noOutput: 'Run generation to see marketing copy and visuals.',
81
  streamError: 'Generation halted before completion.',
82
  stoppedByUser: 'Generation stopped by user.',
 
105
  outputTitle: '生成結果',
106
  outputSubtitle: '即時文案串流 + 圖像素材',
107
  metadataTitle: '執行資訊',
108
+ metadataModel: '模型',
109
+ metadataStartedAt: '開始時間',
110
+ metadataDuration: '耗時',
111
+ metadataEvents: '事件',
112
+ metadataEventsWithImages: '{count} 張圖片 + 文字串流',
113
+ metadataEventsTextOnly: '文字串流',
114
  noOutput: '執行生成後可在此查看文案與圖像。',
115
  streamError: '生成流程尚未完整結束。',
116
  stoppedByUser: '已由使用者停止生成。',
 
347
  return palettes[seed % palettes.length];
348
  }
349
 
350
+ function generateMockImageDataUrl(product: MarketingProduct, deck: MarketingDeck, index: number, locale: DemoLocale): string {
351
  const canvas = document.createElement('canvas');
352
  canvas.width = 960;
353
  canvas.height = 640;
354
  const ctx = canvas.getContext('2d');
355
  if (!ctx) return '';
356
+ const isZh = locale === 'zh-TW';
357
 
358
  const [primary, secondary, light] = makePalette(index + product.name.length);
359
  const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
 
400
  ctx.fillRect(620, 120, 240, 360);
401
  ctx.fillStyle = '#1f8f4e';
402
  ctx.font = 'bold 22px "Sora", sans-serif';
403
+ ctx.fillText(isZh ? '農鏈市集' : 'FARM2MARKET', 646, 170);
404
  ctx.font = '18px "IBM Plex Sans", sans-serif';
405
+ ctx.fillText(isZh ? (index % 2 === 0 ? '主視覺版' : '社群版') : index % 2 === 0 ? 'Hero Variant' : 'Social Variant', 646, 206);
406
+ ctx.fillText(isZh ? `庫存:${product.stock}` : `Stock: ${product.stock}`, 646, 246);
407
 
408
  return canvas.toDataURL('image/png');
409
  }
 
564
  await sleep(480);
565
  if (!mountedRef.current || runTokenRef.current !== runToken) return;
566
 
567
+ const imageOne = generateMockImageDataUrl(selectedProduct, deckPayload, 0, locale);
568
+ const imageTwo = generateMockImageDataUrl(selectedProduct, deckPayload, 1, locale);
569
 
570
  const generated: GeneratedImage[] = [imageOne, imageTwo].map((dataUrl, index) => ({
571
  id: makeId('img'),
 
619
  const handleUpload = async (event: ChangeEvent<HTMLInputElement>) => {
620
  const file = event.target.files?.[0];
621
  if (!file) return;
622
+ const readErrorMessage = locale === 'zh-TW' ? '無法讀取檔案' : 'Unable to read file';
623
 
624
  const dataUrl = await new Promise<string>((resolve, reject) => {
625
  const reader = new FileReader();
626
  reader.onload = () => {
627
  if (typeof reader.result !== 'string') {
628
+ reject(new Error(readErrorMessage));
629
  return;
630
  }
631
  resolve(reader.result);
632
  };
633
+ reader.onerror = () => reject(new Error(readErrorMessage));
634
  reader.readAsDataURL(file);
635
  });
636
 
 
689
 
690
  return (
691
  <div className="marketing-studio-root">
692
+ <MarketingFeatureDemo locale={locale} isBusy={isGenerating} />
693
+
694
  <header className="marketing-studio-header">
695
  <div>
696
  <h3>{copy.title}</h3>
 
881
  <h5>{copy.metadataTitle}</h5>
882
  <dl>
883
  <div>
884
+ <dt>{copy.metadataModel}</dt>
885
  <dd>{meta.model}</dd>
886
  </div>
887
  <div>
888
+ <dt>{copy.metadataStartedAt}</dt>
889
  <dd>{meta.startedAt}</dd>
890
  </div>
891
  <div>
892
+ <dt>{copy.metadataDuration}</dt>
893
  <dd>{meta.durationMs} ms</dd>
894
  </div>
895
  <div>
896
+ <dt>{copy.metadataEvents}</dt>
897
+ <dd>
898
+ {images.length > 0
899
+ ? copy.metadataEventsWithImages.replace('{count}', String(images.length))
900
+ : copy.metadataEventsTextOnly}
901
+ </dd>
902
  </div>
903
  </dl>
904
  </article>
src/MarketplaceDemoPanel.tsx CHANGED
@@ -1,5 +1,6 @@
1
  import { useEffect, useMemo, useRef, useState } from 'react';
2
  import type { DemoLocale, ProgressStep, ProgressStepStatus, TraceItem } from './types';
 
3
 
4
  type MarketplaceMode = 'hybrid' | 'products' | 'stores' | 'stats';
5
 
@@ -266,6 +267,46 @@ function buildTrace(
266
  };
267
  }
268
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  function computeMarketplaceStats(products: ProductRecord[], stores: StoreRecord[]): MarketplaceStats {
270
  const responseRateAverage =
271
  stores.length > 0 ? stores.reduce((sum, store) => sum + store.responseRate, 0) / stores.length : 0;
@@ -442,6 +483,7 @@ const COPY = {
442
  focus: 'Buying focus',
443
  response: 'Response',
444
  listings: 'Listings',
 
445
  open: 'Open store',
446
  contact: 'Draft outreach',
447
  },
@@ -510,6 +552,7 @@ const COPY = {
510
  focus: '採購重點',
511
  response: '回覆率',
512
  listings: '需求數',
 
513
  open: '開啟店家',
514
  contact: '草擬聯絡',
515
  },
@@ -542,6 +585,12 @@ export default function MarketplaceDemoPanel({
542
  onConsumeQueuedPrompt,
543
  }: MarketplaceDemoPanelProps) {
544
  const copy = COPY[locale];
 
 
 
 
 
 
545
  const [mode, setMode] = useState<MarketplaceMode>('hybrid');
546
  const [query, setQuery] = useState('');
547
  const [category, setCategory] = useState('all');
@@ -796,6 +845,8 @@ export default function MarketplaceDemoPanel({
796
 
797
  return (
798
  <div className="marketplace-demo-root">
 
 
799
  <header className="marketplace-demo-header">
800
  <div>
801
  <h3>{copy.title}</h3>
@@ -837,7 +888,7 @@ export default function MarketplaceDemoPanel({
837
  <select value={category} onChange={(event) => setCategory(event.target.value)} disabled={isRunning}>
838
  {categoryOptions.map((item) => (
839
  <option key={item} value={item}>
840
- {item}
841
  </option>
842
  ))}
843
  </select>
@@ -868,11 +919,11 @@ export default function MarketplaceDemoPanel({
868
  <label>
869
  <span>{copy.storeTypeLabel}</span>
870
  <select value={storeType} onChange={(event) => setStoreType(event.target.value)} disabled={isRunning}>
871
- <option value="all">all</option>
872
- <option value="retail">retail</option>
873
- <option value="restaurant">restaurant</option>
874
- <option value="wholesale">wholesale</option>
875
- <option value="co-op">co-op</option>
876
  </select>
877
  </label>
878
  <label>
@@ -979,7 +1030,7 @@ export default function MarketplaceDemoPanel({
979
  <section key={item.id} className="marketplace-entity-card">
980
  <header>
981
  <strong>{item.name}</strong>
982
- <span>{item.category}</span>
983
  </header>
984
  <div className="marketplace-metric-row">
985
  <span>{copy.product.price}</span>
@@ -1029,7 +1080,7 @@ export default function MarketplaceDemoPanel({
1029
  <section key={item.id} className="marketplace-entity-card">
1030
  <header>
1031
  <strong>{item.name}</strong>
1032
- <span>{item.type}</span>
1033
  </header>
1034
  <div className="marketplace-metric-row">
1035
  <span>{copy.store.location}</span>
@@ -1045,10 +1096,10 @@ export default function MarketplaceDemoPanel({
1045
  </div>
1046
  <div className="marketplace-metric-row">
1047
  <span>{copy.store.focus}</span>
1048
- <strong>{item.buyingFocus.slice(0, 2).join(', ')}</strong>
1049
  </div>
1050
  <div className="marketplace-score-track">
1051
- <small>Score</small>
1052
  <div>
1053
  <span style={{ width: `${Math.min(100, item.score)}%` }} />
1054
  </div>
@@ -1080,7 +1131,7 @@ export default function MarketplaceDemoPanel({
1080
  </div>
1081
  <div>
1082
  <dt>{copy.labels.mode}</dt>
1083
- <dd>{meta.mode}</dd>
1084
  </div>
1085
  <div>
1086
  <dt>{copy.labels.startedAt}</dt>
 
1
  import { useEffect, useMemo, useRef, useState } from 'react';
2
  import type { DemoLocale, ProgressStep, ProgressStepStatus, TraceItem } from './types';
3
+ import MarketplaceFeatureDemo from './MarketplaceFeatureDemo';
4
 
5
  type MarketplaceMode = 'hybrid' | 'products' | 'stores' | 'stats';
6
 
 
267
  };
268
  }
269
 
270
+ function categoryLabel(locale: DemoLocale, value: string): string {
271
+ if (locale !== 'zh-TW') return value;
272
+ const labels: Record<string, string> = {
273
+ all: '全部',
274
+ Eggs: '蛋品',
275
+ Vegetables: '蔬菜',
276
+ Produce: '農產',
277
+ Tofu: '豆製品',
278
+ Grains: '穀物',
279
+ Fruit: '水果',
280
+ };
281
+ return labels[value] ?? value;
282
+ }
283
+
284
+ function storeTypeLabel(locale: DemoLocale, value: string): string {
285
+ if (locale !== 'zh-TW') return value;
286
+ const labels: Record<string, string> = {
287
+ all: '全部',
288
+ retail: '零售',
289
+ restaurant: '餐飲',
290
+ wholesale: '批發',
291
+ 'co-op': '合作社',
292
+ };
293
+ return labels[value] ?? value;
294
+ }
295
+
296
+ function focusLabel(locale: DemoLocale, value: string): string {
297
+ if (locale !== 'zh-TW') return value;
298
+ const labels: Record<string, string> = {
299
+ Eggs: '蛋品',
300
+ Fruit: '水果',
301
+ Vegetables: '蔬菜',
302
+ Tofu: '豆製品',
303
+ Produce: '農產',
304
+ Grains: '穀物',
305
+ Rice: '米',
306
+ };
307
+ return labels[value] ?? value;
308
+ }
309
+
310
  function computeMarketplaceStats(products: ProductRecord[], stores: StoreRecord[]): MarketplaceStats {
311
  const responseRateAverage =
312
  stores.length > 0 ? stores.reduce((sum, store) => sum + store.responseRate, 0) / stores.length : 0;
 
483
  focus: 'Buying focus',
484
  response: 'Response',
485
  listings: 'Listings',
486
+ score: 'Score',
487
  open: 'Open store',
488
  contact: 'Draft outreach',
489
  },
 
552
  focus: '採購重點',
553
  response: '回覆率',
554
  listings: '需求數',
555
+ score: '分數',
556
  open: '開啟店家',
557
  contact: '草擬聯絡',
558
  },
 
585
  onConsumeQueuedPrompt,
586
  }: MarketplaceDemoPanelProps) {
587
  const copy = COPY[locale];
588
+ const modeLabels: Record<MarketplaceMode, string> = {
589
+ hybrid: copy.modeHybrid,
590
+ products: copy.modeProducts,
591
+ stores: copy.modeStores,
592
+ stats: copy.modeStats,
593
+ };
594
  const [mode, setMode] = useState<MarketplaceMode>('hybrid');
595
  const [query, setQuery] = useState('');
596
  const [category, setCategory] = useState('all');
 
845
 
846
  return (
847
  <div className="marketplace-demo-root">
848
+ <MarketplaceFeatureDemo locale={locale} isBusy={isRunning} />
849
+
850
  <header className="marketplace-demo-header">
851
  <div>
852
  <h3>{copy.title}</h3>
 
888
  <select value={category} onChange={(event) => setCategory(event.target.value)} disabled={isRunning}>
889
  {categoryOptions.map((item) => (
890
  <option key={item} value={item}>
891
+ {categoryLabel(locale, item)}
892
  </option>
893
  ))}
894
  </select>
 
919
  <label>
920
  <span>{copy.storeTypeLabel}</span>
921
  <select value={storeType} onChange={(event) => setStoreType(event.target.value)} disabled={isRunning}>
922
+ <option value="all">{storeTypeLabel(locale, 'all')}</option>
923
+ <option value="retail">{storeTypeLabel(locale, 'retail')}</option>
924
+ <option value="restaurant">{storeTypeLabel(locale, 'restaurant')}</option>
925
+ <option value="wholesale">{storeTypeLabel(locale, 'wholesale')}</option>
926
+ <option value="co-op">{storeTypeLabel(locale, 'co-op')}</option>
927
  </select>
928
  </label>
929
  <label>
 
1030
  <section key={item.id} className="marketplace-entity-card">
1031
  <header>
1032
  <strong>{item.name}</strong>
1033
+ <span>{categoryLabel(locale, item.category)}</span>
1034
  </header>
1035
  <div className="marketplace-metric-row">
1036
  <span>{copy.product.price}</span>
 
1080
  <section key={item.id} className="marketplace-entity-card">
1081
  <header>
1082
  <strong>{item.name}</strong>
1083
+ <span>{storeTypeLabel(locale, item.type)}</span>
1084
  </header>
1085
  <div className="marketplace-metric-row">
1086
  <span>{copy.store.location}</span>
 
1096
  </div>
1097
  <div className="marketplace-metric-row">
1098
  <span>{copy.store.focus}</span>
1099
+ <strong>{item.buyingFocus.slice(0, 2).map((focus) => focusLabel(locale, focus)).join(', ')}</strong>
1100
  </div>
1101
  <div className="marketplace-score-track">
1102
+ <small>{copy.store.score}</small>
1103
  <div>
1104
  <span style={{ width: `${Math.min(100, item.score)}%` }} />
1105
  </div>
 
1131
  </div>
1132
  <div>
1133
  <dt>{copy.labels.mode}</dt>
1134
+ <dd>{modeLabels[meta.mode]}</dd>
1135
  </div>
1136
  <div>
1137
  <dt>{copy.labels.startedAt}</dt>
src/MarketplaceFeatureDemo.tsx ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import type { DemoLocale } from './types';
3
+
4
+ type MarketplaceScene = {
5
+ id: 'query' | 'rank' | 'match';
6
+ durationMs: number;
7
+ };
8
+
9
+ type MarketplaceFeatureDemoProps = {
10
+ locale: DemoLocale;
11
+ isBusy: boolean;
12
+ };
13
+
14
+ const COPY = {
15
+ en: {
16
+ title: 'Marketplace Demo',
17
+ busy: 'Running',
18
+ hidden: 'Hidden',
19
+ play: 'Play',
20
+ pause: 'Pause',
21
+ replay: 'Replay',
22
+ hide: 'Hide',
23
+ show: 'Show',
24
+ videoName: 'marketplace_demo.webm',
25
+ step: 'Step',
26
+ lanes: {
27
+ query: 'Query',
28
+ rank: 'Rank',
29
+ match: 'Match',
30
+ },
31
+ captions: {
32
+ query: 'Collect search and filter intent',
33
+ rank: 'Score records by relevance',
34
+ match: 'Render product + store cards',
35
+ },
36
+ words: {
37
+ query: ['Vegetables', 'Taipei', 'Under200'],
38
+ rank: ['Parse', 'Score', 'Sort'],
39
+ match: ['Product', 'Store', 'Stats'],
40
+ streamWords: ['Searching', 'Filtering', 'Ranking', 'Matching'],
41
+ queryImage: 'https://upload.wikimedia.org/wikipedia/commons/c/c2/Fresh_Vegetables_2.jpg',
42
+ },
43
+ },
44
+ 'zh-TW': {
45
+ title: '市集導覽',
46
+ busy: '執行中',
47
+ hidden: '已隱藏',
48
+ play: '播放',
49
+ pause: '暫停',
50
+ replay: '重播',
51
+ hide: '隱藏',
52
+ show: '顯示',
53
+ videoName: '市集導覽影片.webm',
54
+ step: '步驟',
55
+ lanes: {
56
+ query: '查詢',
57
+ rank: '排序',
58
+ match: '結果',
59
+ },
60
+ captions: {
61
+ query: '收集搜尋與篩選條件',
62
+ rank: '依相關性計分排序',
63
+ match: '輸出商品與店家卡片',
64
+ },
65
+ words: {
66
+ query: ['蔬菜', '台北', '200內'],
67
+ rank: ['解析', '計分', '排序'],
68
+ match: ['商品', '店家', '統計'],
69
+ streamWords: ['搜尋中', '篩選中', '排序中', '配對中'],
70
+ queryImage: 'https://upload.wikimedia.org/wikipedia/commons/c/c2/Fresh_Vegetables_2.jpg',
71
+ },
72
+ },
73
+ } as const;
74
+
75
+ const SCENES: MarketplaceScene[] = [
76
+ { id: 'query', durationMs: 1650 },
77
+ { id: 'rank', durationMs: 1750 },
78
+ { id: 'match', durationMs: 1700 },
79
+ ];
80
+
81
+ export default function MarketplaceFeatureDemo({ locale, isBusy }: MarketplaceFeatureDemoProps) {
82
+ const copy = COPY[locale];
83
+ const scenes = useMemo(() => SCENES, []);
84
+
85
+ const [sceneIndex, setSceneIndex] = useState(0);
86
+ const [elapsedMs, setElapsedMs] = useState(0);
87
+ const [isPlaying, setIsPlaying] = useState(true);
88
+ const [isCollapsed, setIsCollapsed] = useState(false);
89
+ const [reducedMotion, setReducedMotion] = useState(false);
90
+
91
+ useEffect(() => {
92
+ const media = window.matchMedia('(prefers-reduced-motion: reduce)');
93
+ const apply = () => setReducedMotion(media.matches);
94
+ apply();
95
+ if (media.addEventListener) {
96
+ media.addEventListener('change', apply);
97
+ return () => media.removeEventListener('change', apply);
98
+ }
99
+ media.addListener(apply);
100
+ return () => media.removeListener(apply);
101
+ }, []);
102
+
103
+ useEffect(() => {
104
+ if (reducedMotion) setIsPlaying(false);
105
+ }, [reducedMotion]);
106
+
107
+ useEffect(() => {
108
+ setSceneIndex(0);
109
+ setElapsedMs(0);
110
+ setIsCollapsed(false);
111
+ if (!reducedMotion) setIsPlaying(true);
112
+ }, [locale, reducedMotion]);
113
+
114
+ useEffect(() => {
115
+ if (!isBusy) return;
116
+ setIsCollapsed(true);
117
+ setIsPlaying(false);
118
+ }, [isBusy]);
119
+
120
+ useEffect(() => {
121
+ if (!isPlaying || isCollapsed || isBusy || reducedMotion) return;
122
+ const timer = window.setInterval(() => {
123
+ setElapsedMs((prev) => {
124
+ const scene = scenes[sceneIndex];
125
+ if (!scene) return 0;
126
+ const next = prev + 120;
127
+ if (next < scene.durationMs) return next;
128
+ setSceneIndex((current) => (current + 1) % scenes.length);
129
+ return 0;
130
+ });
131
+ }, 120);
132
+ return () => window.clearInterval(timer);
133
+ }, [isPlaying, isCollapsed, isBusy, reducedMotion, scenes, sceneIndex]);
134
+
135
+ const activeScene = scenes[sceneIndex] ?? scenes[0];
136
+ const totalDuration = scenes.reduce((sum, scene) => sum + scene.durationMs, 0);
137
+ const previousDuration = scenes.slice(0, sceneIndex).reduce((sum, scene) => sum + scene.durationMs, 0);
138
+ const progress = totalDuration > 0 ? Math.round(((previousDuration + elapsedMs) / totalDuration) * 100) : 0;
139
+ const sceneCaption = copy.captions[activeScene.id];
140
+ const streamOffset = Math.floor(elapsedMs / 420);
141
+ const activeStreamWords = copy.words.streamWords.map(
142
+ (_word, index) => copy.words.streamWords[(streamOffset + index) % copy.words.streamWords.length]
143
+ );
144
+
145
+ const replay = () => {
146
+ setSceneIndex(0);
147
+ setElapsedMs(0);
148
+ if (!reducedMotion && !isBusy) setIsPlaying(true);
149
+ };
150
+
151
+ if (isCollapsed) {
152
+ return (
153
+ <section className="market-walk-collapsed" aria-live="polite">
154
+ <div className="market-walk-mini-markers" aria-hidden="true">
155
+ <span />
156
+ <span />
157
+ <span />
158
+ </div>
159
+ <p>{isBusy ? copy.busy : copy.hidden}</p>
160
+ {!isBusy ? (
161
+ <button
162
+ type="button"
163
+ className="ghost-btn"
164
+ onClick={() => {
165
+ setIsCollapsed(false);
166
+ replay();
167
+ }}
168
+ >
169
+ {copy.show}
170
+ </button>
171
+ ) : null}
172
+ </section>
173
+ );
174
+ }
175
+
176
+ return (
177
+ <section className="market-walk-video" aria-label={copy.title}>
178
+ <header className="market-walk-head">
179
+ <div>
180
+ <h3>{copy.title}</h3>
181
+ <small>
182
+ {copy.step} {sceneIndex + 1}/{scenes.length}
183
+ </small>
184
+ </div>
185
+ <div className="market-walk-actions">
186
+ <button
187
+ type="button"
188
+ className="market-walk-icon-btn"
189
+ onClick={() => setIsPlaying((prev) => !prev)}
190
+ disabled={isBusy || reducedMotion}
191
+ aria-label={isPlaying ? copy.pause : copy.play}
192
+ >
193
+ {isPlaying ? '||' : '>'}
194
+ </button>
195
+ <button type="button" className="market-walk-icon-btn" onClick={replay} disabled={isBusy} aria-label={copy.replay}>
196
+ R
197
+ </button>
198
+ <button type="button" className="market-walk-icon-btn" onClick={() => setIsCollapsed(true)} aria-label={copy.hide}>
199
+ X
200
+ </button>
201
+ </div>
202
+ </header>
203
+
204
+ <div className="market-walk-frame" data-scene={activeScene.id}>
205
+ <div className="market-walk-windowbar">
206
+ <span />
207
+ <span />
208
+ <span />
209
+ <small>{copy.videoName}</small>
210
+ </div>
211
+
212
+ <div className="market-walk-canvas">
213
+ <section className="market-walk-lane query">
214
+ <span className="market-walk-lane-index">1</span>
215
+ <small className="market-walk-lane-label">{copy.lanes.query}</small>
216
+ <div className="market-walk-chip-row">
217
+ {copy.words.query.map((item) => (
218
+ <span key={item}>{item}</span>
219
+ ))}
220
+ </div>
221
+ <img
222
+ src={copy.words.queryImage}
223
+ alt={locale === 'zh-TW' ? '市集查詢示意圖' : 'market query sample'}
224
+ loading="lazy"
225
+ referrerPolicy="no-referrer"
226
+ className="market-walk-query-thumb"
227
+ />
228
+ <div className="market-walk-stream-pill lg">
229
+ <span>{activeStreamWords[0]}</span>
230
+ </div>
231
+ <div className="market-walk-stream-pill md">
232
+ <span>{activeStreamWords[1]}</span>
233
+ </div>
234
+ <p>{locale === 'zh-TW' ? '查詢與價格區間已套用。' : 'Query text and price range applied.'}</p>
235
+ </section>
236
+
237
+ <section className="market-walk-lane rank">
238
+ <span className="market-walk-lane-index">2</span>
239
+ <small className="market-walk-lane-label">{copy.lanes.rank}</small>
240
+ <div className="market-walk-rank-row">
241
+ {copy.words.rank.map((item) => (
242
+ <span key={item}>{item}</span>
243
+ ))}
244
+ </div>
245
+ <div className="market-walk-rank-flow">
246
+ <span />
247
+ </div>
248
+ <div className="market-walk-stream-pill sm">
249
+ <span>{activeStreamWords[2]}</span>
250
+ </div>
251
+ </section>
252
+
253
+ <section className="market-walk-lane match">
254
+ <span className="market-walk-lane-index">3</span>
255
+ <small className="market-walk-lane-label">{copy.lanes.match}</small>
256
+ <div className="market-walk-result-grid">
257
+ {copy.words.match.map((item) => (
258
+ <article key={item}>
259
+ <small>{item}</small>
260
+ <strong>{item === copy.words.match[0] ? '6' : item === copy.words.match[1] ? '4' : '42'}</strong>
261
+ </article>
262
+ ))}
263
+ </div>
264
+ <p>{locale === 'zh-TW' ? '已輸出排序卡片與統計摘要。' : 'Ranked cards and stats summary rendered.'}</p>
265
+ </section>
266
+ </div>
267
+ </div>
268
+
269
+ <footer className="market-walk-meta">
270
+ <div className="market-walk-progress-track" aria-hidden="true">
271
+ <span style={{ width: `${Math.min(100, Math.max(0, progress))}%` }} />
272
+ </div>
273
+ <p className="market-walk-caption">{sceneCaption}</p>
274
+ <div className="market-walk-scene-dots">
275
+ {scenes.map((scene, index) => (
276
+ <button
277
+ key={scene.id}
278
+ type="button"
279
+ className={`market-walk-scene-dot ${index === sceneIndex ? 'active' : index < sceneIndex ? 'complete' : ''}`}
280
+ onClick={() => {
281
+ setSceneIndex(index);
282
+ setElapsedMs(0);
283
+ }}
284
+ aria-label={`${copy.step} ${index + 1}`}
285
+ >
286
+ <span />
287
+ </button>
288
+ ))}
289
+ </div>
290
+ </footer>
291
+ </section>
292
+ );
293
+ }
src/VoiceDemoPanel.tsx CHANGED
@@ -1,5 +1,6 @@
1
  import { useEffect, useMemo, useRef, useState } from 'react';
2
  import type { DemoLocale, ProgressStep, ProgressStepStatus, TraceItem } from './types';
 
3
 
4
  type VoiceStatus = 'idle' | 'connecting' | 'ready' | 'listening' | 'processing' | 'error';
5
  type VoiceRole = 'assistant' | 'user' | 'system';
@@ -85,6 +86,11 @@ const COPY = {
85
  micStop: 'Stop',
86
  tutorialRequest: 'Start a voice walkthrough for this page with key actions.',
87
  clearSession: 'Clear Session',
 
 
 
 
 
88
  },
89
  'zh-TW': {
90
  title: '語音助理',
@@ -110,6 +116,11 @@ const COPY = {
110
  micStop: '停止',
111
  tutorialRequest: '請開始這個頁面的語音導覽,並說明主要操作。',
112
  clearSession: '清除對話',
 
 
 
 
 
113
  },
114
  } as const;
115
 
@@ -653,8 +664,12 @@ export default function VoiceDemoPanel({
653
  );
654
  };
655
 
 
 
656
  return (
657
  <div className="voice-demo-root">
 
 
658
  {isOpen ? (
659
  <section className="voice-dialog">
660
  <header className="voice-dialog-header">
@@ -676,7 +691,7 @@ export default function VoiceDemoPanel({
676
  {entries.map((entry) => (
677
  <article key={entry.id} className={`voice-entry ${entry.role}`}>
678
  <header>
679
- <span>{entry.role}</span>
680
  <time>{entry.timestamp}</time>
681
  </header>
682
  {entry.text ? <p>{entry.text}</p> : null}
 
1
  import { useEffect, useMemo, useRef, useState } from 'react';
2
  import type { DemoLocale, ProgressStep, ProgressStepStatus, TraceItem } from './types';
3
+ import VoiceFeatureDemo from './VoiceFeatureDemo';
4
 
5
  type VoiceStatus = 'idle' | 'connecting' | 'ready' | 'listening' | 'processing' | 'error';
6
  type VoiceRole = 'assistant' | 'user' | 'system';
 
86
  micStop: 'Stop',
87
  tutorialRequest: 'Start a voice walkthrough for this page with key actions.',
88
  clearSession: 'Clear Session',
89
+ roleLabels: {
90
+ assistant: 'assistant',
91
+ user: 'user',
92
+ system: 'system',
93
+ },
94
  },
95
  'zh-TW': {
96
  title: '語音助理',
 
116
  micStop: '停止',
117
  tutorialRequest: '請開始這個頁面的語音導覽,並說明主要操作。',
118
  clearSession: '清除對話',
119
+ roleLabels: {
120
+ assistant: '助理',
121
+ user: '使用者',
122
+ system: '系統',
123
+ },
124
  },
125
  } as const;
126
 
 
664
  );
665
  };
666
 
667
+ const isDemoBusy = status === 'connecting' || status === 'listening' || status === 'processing';
668
+
669
  return (
670
  <div className="voice-demo-root">
671
+ <VoiceFeatureDemo locale={locale} isBusy={isDemoBusy} />
672
+
673
  {isOpen ? (
674
  <section className="voice-dialog">
675
  <header className="voice-dialog-header">
 
691
  {entries.map((entry) => (
692
  <article key={entry.id} className={`voice-entry ${entry.role}`}>
693
  <header>
694
+ <span>{copy.roleLabels[entry.role]}</span>
695
  <time>{entry.timestamp}</time>
696
  </header>
697
  {entry.text ? <p>{entry.text}</p> : null}
src/VoiceFeatureDemo.tsx ADDED
@@ -0,0 +1,373 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import type { DemoLocale } from './types';
3
+
4
+ type VoiceScene = {
5
+ id: 'listen' | 'route' | 'cards';
6
+ durationMs: number;
7
+ };
8
+
9
+ type VoiceFeatureDemoProps = {
10
+ locale: DemoLocale;
11
+ isBusy: boolean;
12
+ };
13
+
14
+ const COPY = {
15
+ en: {
16
+ title: 'Voice Demo',
17
+ busy: 'Running',
18
+ hidden: 'Hidden',
19
+ play: 'Play',
20
+ pause: 'Pause',
21
+ replay: 'Replay',
22
+ hide: 'Hide',
23
+ show: 'Show',
24
+ videoName: 'voice_demo.webm',
25
+ step: 'Step',
26
+ lanes: {
27
+ listen: 'Listen',
28
+ route: 'Route',
29
+ cards: 'Cards',
30
+ },
31
+ routeWords: {
32
+ nodes: ['Parse', 'Plan', 'Call'],
33
+ chips: ['Stock', 'Price', 'Store', 'Safe'],
34
+ },
35
+ captions: {
36
+ listen: 'Capture voice and intent',
37
+ route: 'Plan tool sequence',
38
+ cards: 'Return voice cards',
39
+ },
40
+ stepCopy: {
41
+ listen: {
42
+ title: 'Live Voice Capture',
43
+ body:
44
+ 'Microphone audio is sampled in real time, normalized for volume variance, and segmented into phrase-sized chunks for stable transcript quality.',
45
+ detail:
46
+ 'Noise suppression, language hinting, and intent candidate extraction run before any tool is called.',
47
+ },
48
+ route: {
49
+ title: 'Tool Route Planning',
50
+ body:
51
+ 'The planner maps your spoken goal into executable actions, selects the best tool chain, and prepares argument payloads with guardrails.',
52
+ detail:
53
+ 'Validation checks include category filters, price constraints, availability context, and fallback routing when confidence is low.',
54
+ },
55
+ cards: {
56
+ title: 'Structured Voice Response',
57
+ body:
58
+ 'Results are transformed into concise spoken output plus visual cards so users can confirm key data without opening raw payloads.',
59
+ detail:
60
+ 'Action chips are attached for immediate follow-up tasks such as editing stock, opening product details, or triggering the next workflow.',
61
+ },
62
+ },
63
+ cardVisual: {
64
+ summaryTitle: 'Assistant Summary',
65
+ summaryBody: 'I updated stock to 48, synced pickup details, and prepared two follow-up actions.',
66
+ actions: ['Open Product', 'Edit Again', 'Share Update'],
67
+ statsTitle: 'Result Snapshot',
68
+ stats: [
69
+ { label: 'Confidence', value: '96%' },
70
+ { label: 'Tool Calls', value: '3' },
71
+ { label: 'Latency', value: '1.2s' },
72
+ ],
73
+ statsNote: 'Structured payload attached to voice response card.',
74
+ },
75
+ },
76
+ 'zh-TW': {
77
+ title: '語音導覽',
78
+ busy: '執行中',
79
+ hidden: '已隱藏',
80
+ play: '播放',
81
+ pause: '暫停',
82
+ replay: '重播',
83
+ hide: '隱藏',
84
+ show: '顯示',
85
+ videoName: '語音導覽影片.webm',
86
+ step: '步驟',
87
+ lanes: {
88
+ listen: '收音',
89
+ route: '路由',
90
+ cards: '結果',
91
+ },
92
+ routeWords: {
93
+ nodes: ['解析', '規劃', '呼叫'],
94
+ chips: ['庫存', '價格', '店家', '安全'],
95
+ },
96
+ captions: {
97
+ listen: '接收語音與意圖',
98
+ route: '規劃工具流程',
99
+ cards: '回傳語音卡片',
100
+ },
101
+ stepCopy: {
102
+ listen: {
103
+ title: '即時語音收音',
104
+ body: '系統會即時擷取麥克風音訊,先做音量與分段標準化,再輸出更穩定的語音文字片段。',
105
+ detail: '在呼叫工具前,會先完成降噪、語系判斷與初步意圖分類。',
106
+ },
107
+ route: {
108
+ title: '工具路由規劃',
109
+ body: '規劃器會把語音目標轉成可執行步驟,選擇最佳工具鏈並組裝對應參數。',
110
+ detail: '同時檢查分類、價格、庫存與容錯路由,確保低信心情況也能安全回應。',
111
+ },
112
+ cards: {
113
+ title: '語音結果封裝',
114
+ body: '最終回覆會整合口語摘要與視覺卡片,讓使用者快速理解結果而不必直接讀 JSON。',
115
+ detail: '卡片會附帶後續操作按鈕,例如更新庫存、查看商品頁、或直接進入下一步。',
116
+ },
117
+ },
118
+ cardVisual: {
119
+ summaryTitle: '語音摘要',
120
+ summaryBody: '已更新庫存為 48,補上取貨資訊,並整理兩個後續操作。',
121
+ actions: ['查看商品', '再次編輯', '分享更新'],
122
+ statsTitle: '結果快照',
123
+ stats: [
124
+ { label: '信心值', value: '96%' },
125
+ { label: '工具數', value: '3' },
126
+ { label: '延遲', value: '1.2s' },
127
+ ],
128
+ statsNote: '已將結構化載荷附加到語音卡片。',
129
+ },
130
+ },
131
+ } as const;
132
+
133
+ const SCENES: VoiceScene[] = [
134
+ { id: 'listen', durationMs: 1700 },
135
+ { id: 'route', durationMs: 1750 },
136
+ { id: 'cards', durationMs: 1700 },
137
+ ];
138
+
139
+ export default function VoiceFeatureDemo({ locale, isBusy }: VoiceFeatureDemoProps) {
140
+ const copy = COPY[locale];
141
+ const scenes = useMemo(() => SCENES, []);
142
+
143
+ const [sceneIndex, setSceneIndex] = useState(0);
144
+ const [elapsedMs, setElapsedMs] = useState(0);
145
+ const [isPlaying, setIsPlaying] = useState(true);
146
+ const [isCollapsed, setIsCollapsed] = useState(false);
147
+ const [reducedMotion, setReducedMotion] = useState(false);
148
+
149
+ useEffect(() => {
150
+ const media = window.matchMedia('(prefers-reduced-motion: reduce)');
151
+ const apply = () => setReducedMotion(media.matches);
152
+
153
+ apply();
154
+ if (media.addEventListener) {
155
+ media.addEventListener('change', apply);
156
+ return () => media.removeEventListener('change', apply);
157
+ }
158
+
159
+ media.addListener(apply);
160
+ return () => media.removeListener(apply);
161
+ }, []);
162
+
163
+ useEffect(() => {
164
+ if (reducedMotion) setIsPlaying(false);
165
+ }, [reducedMotion]);
166
+
167
+ useEffect(() => {
168
+ setSceneIndex(0);
169
+ setElapsedMs(0);
170
+ setIsCollapsed(false);
171
+ if (!reducedMotion) setIsPlaying(true);
172
+ }, [locale, reducedMotion]);
173
+
174
+ useEffect(() => {
175
+ if (!isBusy) return;
176
+ setIsCollapsed(true);
177
+ setIsPlaying(false);
178
+ }, [isBusy]);
179
+
180
+ useEffect(() => {
181
+ if (!isPlaying || isCollapsed || isBusy || reducedMotion) return;
182
+
183
+ const timer = window.setInterval(() => {
184
+ setElapsedMs((prev) => {
185
+ const scene = scenes[sceneIndex];
186
+ if (!scene) return 0;
187
+ const next = prev + 120;
188
+ if (next < scene.durationMs) return next;
189
+ setSceneIndex((current) => (current + 1) % scenes.length);
190
+ return 0;
191
+ });
192
+ }, 120);
193
+
194
+ return () => window.clearInterval(timer);
195
+ }, [isPlaying, isCollapsed, isBusy, reducedMotion, scenes, sceneIndex]);
196
+
197
+ const activeScene = scenes[sceneIndex] ?? scenes[0];
198
+ const totalDuration = scenes.reduce((sum, scene) => sum + scene.durationMs, 0);
199
+ const previousDuration = scenes.slice(0, sceneIndex).reduce((sum, scene) => sum + scene.durationMs, 0);
200
+ const progress = totalDuration > 0 ? Math.round(((previousDuration + elapsedMs) / totalDuration) * 100) : 0;
201
+ const sceneCaption = copy.captions[activeScene.id];
202
+
203
+ const replay = () => {
204
+ setSceneIndex(0);
205
+ setElapsedMs(0);
206
+ if (!reducedMotion && !isBusy) setIsPlaying(true);
207
+ };
208
+
209
+ if (isCollapsed) {
210
+ return (
211
+ <section className="voice-walk-collapsed" aria-live="polite">
212
+ <div className="voice-walk-mini-markers" aria-hidden="true">
213
+ <span />
214
+ <span />
215
+ <span />
216
+ </div>
217
+ <p>{isBusy ? copy.busy : copy.hidden}</p>
218
+ {!isBusy ? (
219
+ <button
220
+ type="button"
221
+ className="ghost-btn"
222
+ onClick={() => {
223
+ setIsCollapsed(false);
224
+ replay();
225
+ }}
226
+ >
227
+ {copy.show}
228
+ </button>
229
+ ) : null}
230
+ </section>
231
+ );
232
+ }
233
+
234
+ return (
235
+ <section className="voice-walk-video" aria-label={copy.title}>
236
+ <header className="voice-walk-head">
237
+ <div>
238
+ <h3>{copy.title}</h3>
239
+ <small>
240
+ {copy.step} {sceneIndex + 1}/{scenes.length}
241
+ </small>
242
+ </div>
243
+ <div className="voice-walk-actions">
244
+ <button
245
+ type="button"
246
+ className="voice-walk-icon-btn"
247
+ onClick={() => setIsPlaying((prev) => !prev)}
248
+ disabled={isBusy || reducedMotion}
249
+ aria-label={isPlaying ? copy.pause : copy.play}
250
+ >
251
+ {isPlaying ? '||' : '>'}
252
+ </button>
253
+ <button type="button" className="voice-walk-icon-btn" onClick={replay} disabled={isBusy} aria-label={copy.replay}>
254
+ R
255
+ </button>
256
+ <button type="button" className="voice-walk-icon-btn" onClick={() => setIsCollapsed(true)} aria-label={copy.hide}>
257
+ X
258
+ </button>
259
+ </div>
260
+ </header>
261
+
262
+ <div className="voice-walk-frame" data-scene={activeScene.id}>
263
+ <div className="voice-walk-windowbar">
264
+ <span />
265
+ <span />
266
+ <span />
267
+ <small>{copy.videoName}</small>
268
+ </div>
269
+
270
+ <div className="voice-walk-canvas">
271
+ <section className="voice-walk-lane listen">
272
+ <span className="voice-walk-lane-index">1</span>
273
+ <small className="voice-walk-lane-label">{copy.lanes.listen}</small>
274
+ <div className="voice-walk-wave">
275
+ {Array.from({ length: 10 }).map((_, index) => (
276
+ <span key={`wave-${index}`} style={{ animationDelay: `${index * 0.08}s` }} />
277
+ ))}
278
+ </div>
279
+ <div className="voice-walk-mic">
280
+ <span />
281
+ </div>
282
+ <div className="voice-walk-step-copy">
283
+ <strong>{copy.stepCopy.listen.title}</strong>
284
+ <p>{copy.stepCopy.listen.body}</p>
285
+ <small>{copy.stepCopy.listen.detail}</small>
286
+ </div>
287
+ </section>
288
+
289
+ <section className="voice-walk-lane route">
290
+ <span className="voice-walk-lane-index">2</span>
291
+ <small className="voice-walk-lane-label">{copy.lanes.route}</small>
292
+ <div className="voice-walk-route-nodes">
293
+ {copy.routeWords.nodes.map((word) => (
294
+ <span key={word}>{word}</span>
295
+ ))}
296
+ </div>
297
+ <div className="voice-walk-route-flow">
298
+ <span />
299
+ </div>
300
+ <div className="voice-walk-route-chips">
301
+ {copy.routeWords.chips.map((word) => (
302
+ <span key={word}>{word}</span>
303
+ ))}
304
+ </div>
305
+ <div className="voice-walk-step-copy">
306
+ <strong>{copy.stepCopy.route.title}</strong>
307
+ <p>{copy.stepCopy.route.body}</p>
308
+ <small>{copy.stepCopy.route.detail}</small>
309
+ </div>
310
+ </section>
311
+
312
+ <section className="voice-walk-lane cards">
313
+ <span className="voice-walk-lane-index">3</span>
314
+ <small className="voice-walk-lane-label">{copy.lanes.cards}</small>
315
+ <article className="voice-walk-card">
316
+ <header className="voice-walk-card-head">
317
+ <strong>{copy.cardVisual.summaryTitle}</strong>
318
+ </header>
319
+ <p className="voice-walk-card-body">{copy.cardVisual.summaryBody}</p>
320
+ <div className="voice-walk-chip-row">
321
+ {copy.cardVisual.actions.map((action) => (
322
+ <span key={action}>{action}</span>
323
+ ))}
324
+ </div>
325
+ </article>
326
+ <article className="voice-walk-card">
327
+ <header className="voice-walk-card-head">
328
+ <strong>{copy.cardVisual.statsTitle}</strong>
329
+ </header>
330
+ <div className="voice-walk-grid">
331
+ {copy.cardVisual.stats.map((item) => (
332
+ <span key={item.label}>
333
+ <small>{item.label}</small>
334
+ <strong>{item.value}</strong>
335
+ </span>
336
+ ))}
337
+ </div>
338
+ <p className="voice-walk-card-note">{copy.cardVisual.statsNote}</p>
339
+ </article>
340
+ <div className="voice-walk-step-copy">
341
+ <strong>{copy.stepCopy.cards.title}</strong>
342
+ <p>{copy.stepCopy.cards.body}</p>
343
+ <small>{copy.stepCopy.cards.detail}</small>
344
+ </div>
345
+ </section>
346
+ </div>
347
+ </div>
348
+
349
+ <footer className="voice-walk-meta">
350
+ <div className="voice-walk-progress-track" aria-hidden="true">
351
+ <span style={{ width: `${Math.min(100, Math.max(0, progress))}%` }} />
352
+ </div>
353
+ <p className="voice-walk-caption">{sceneCaption}</p>
354
+ <div className="voice-walk-scene-dots">
355
+ {scenes.map((scene, index) => (
356
+ <button
357
+ key={scene.id}
358
+ type="button"
359
+ className={`voice-walk-scene-dot ${index === sceneIndex ? 'active' : index < sceneIndex ? 'complete' : ''}`}
360
+ onClick={() => {
361
+ setSceneIndex(index);
362
+ setElapsedMs(0);
363
+ }}
364
+ aria-label={`${copy.step} ${index + 1}`}
365
+ >
366
+ <span />
367
+ </button>
368
+ ))}
369
+ </div>
370
+ </footer>
371
+ </section>
372
+ );
373
+ }
src/mockAgent.ts CHANGED
@@ -27,157 +27,311 @@ interface RunMockAgentParams {
27
  onTrace: (item: TraceItem) => void;
28
  }
29
 
30
- const WORKFLOWS: Record<DemoTabId, WorkflowTemplate[]> = {
31
- chat: [
32
- {
33
- label: 'Understanding user intent',
34
- detail: 'Parsing the latest message and session context.',
35
- doneDetail: 'Intent and preferred action recognized.',
36
- kind: 'planner',
37
- durationMs: 600,
38
- },
39
- {
40
- label: 'Planning tool route',
41
- detail: 'Selecting the best backend function and argument set.',
42
- doneDetail: 'Tool route and arguments prepared.',
43
- kind: 'planner',
44
- durationMs: 700,
45
- },
46
- {
47
- label: 'Executing tool call',
48
- detail: 'Running product/store/statistics action.',
49
- doneDetail: 'Tool returned structured data.',
50
- kind: 'tool',
51
- durationMs: 900,
52
- },
53
- {
54
- label: 'Rendering response',
55
- detail: 'Composing concise answer + rich cards.',
56
- doneDetail: 'Final message and cards generated.',
57
- kind: 'renderer',
58
- durationMs: 500,
59
- },
60
- ],
61
- voice: [
62
- {
63
- label: 'Normalizing transcript',
64
- detail: 'Detecting language and cleaning transcript text.',
65
- doneDetail: 'Transcript normalized.',
66
- kind: 'validator',
67
- durationMs: 600,
68
- },
69
- {
70
- label: 'Choosing assistant tool',
71
- detail: 'Mapping spoken intent to product/store workflows.',
72
- doneDetail: 'Voice tool selected.',
73
- kind: 'planner',
74
- durationMs: 650,
75
- },
76
- {
77
- label: 'Running voice tool',
78
- detail: 'Calling backend action and collecting UI payload.',
79
- doneDetail: 'Voice tool output ready.',
80
- kind: 'tool',
81
- durationMs: 900,
82
- },
83
- {
84
- label: 'Synthesizing summary',
85
- detail: 'Producing short speech-safe answer.',
86
- doneDetail: 'Final voice summary composed.',
87
- kind: 'renderer',
88
- durationMs: 450,
89
- },
90
- ],
91
- marketing: [
92
- {
93
- label: 'Reading campaign brief',
94
- detail: 'Extracting tone, audience, and product context.',
95
- doneDetail: 'Campaign brief parsed.',
96
- kind: 'planner',
97
- durationMs: 700,
98
- },
99
- {
100
- label: 'Generating marketing assets',
101
- detail: 'Streaming copy and image enhancement instructions.',
102
- doneDetail: 'Marketing draft generated.',
103
- kind: 'tool',
104
- durationMs: 1200,
105
- },
106
- {
107
- label: 'Quality checks',
108
- detail: 'Verifying CTA clarity and consistency.',
109
- doneDetail: 'Draft passed validation.',
110
- kind: 'validator',
111
- durationMs: 550,
112
- },
113
- {
114
- label: 'Preparing showcase output',
115
- detail: 'Building final campaign cards and metadata.',
116
- doneDetail: 'Marketing output ready.',
117
- kind: 'renderer',
118
- durationMs: 450,
119
- },
120
- ],
121
- invoice: [
122
- {
123
- label: 'Pre-processing invoice image',
124
- detail: 'Normalizing orientation and text contrast.',
125
- doneDetail: 'Image prepared for extraction.',
126
- kind: 'validator',
127
- durationMs: 700,
128
- },
129
- {
130
- label: 'Extracting structured fields',
131
- detail: 'Detecting invoice number, totals, and dates.',
132
- doneDetail: 'Invoice fields extracted.',
133
- kind: 'tool',
134
- durationMs: 1100,
135
- },
136
- {
137
- label: 'Verifying consistency',
138
- detail: 'Checking totals, currency, and item count.',
139
- doneDetail: 'Extraction validated.',
140
- kind: 'validator',
141
- durationMs: 650,
142
- },
143
- {
144
- label: 'Formatting result view',
145
- detail: 'Generating clean JSON + summary cards.',
146
- doneDetail: 'Invoice output rendered.',
147
- kind: 'renderer',
148
- durationMs: 500,
149
- },
150
- ],
151
- marketplace: [
152
- {
153
- label: 'Interpreting filters',
154
- detail: 'Parsing category, location, and price limits.',
155
- doneDetail: 'Search filters resolved.',
156
- kind: 'planner',
157
- durationMs: 650,
158
- },
159
- {
160
- label: 'Querying marketplace data',
161
- detail: 'Searching products and buyer/store entities.',
162
- doneDetail: 'Results retrieved.',
163
- kind: 'tool',
164
- durationMs: 950,
165
- },
166
- {
167
- label: 'Ranking matches',
168
- detail: 'Sorting by relevance and availability.',
169
- doneDetail: 'Top matches selected.',
170
- kind: 'validator',
171
- durationMs: 600,
172
- },
173
- {
174
- label: 'Composing answer',
175
- detail: 'Preparing concise brief with cards.',
176
- doneDetail: 'Marketplace summary ready.',
177
- kind: 'renderer',
178
- durationMs: 450,
179
- },
180
- ],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  };
182
 
183
  const QUICK_PROMPTS: Record<DemoLocale, Record<DemoTabId, string[]>> = {
@@ -309,7 +463,7 @@ function buildCards(tab: DemoTabId, input: string, locale: DemoLocale): DemoCard
309
  title: locale === 'zh-TW' ? '放牧雞蛋' : 'Free-range Eggs',
310
  subtitle: locale === 'zh-TW' ? '新竹・可週配' : 'Hsinchu • weekly delivery',
311
  metrics: [
312
- { label: locale === 'zh-TW' ? '單價' : 'Price', value: 'NT$ 118 / box' },
313
  { label: locale === 'zh-TW' ? '庫存' : 'Stock', value: '42' },
314
  ],
315
  tags: [topic, locale === 'zh-TW' ? '熱門' : 'trending'],
@@ -318,7 +472,7 @@ function buildCards(tab: DemoTabId, input: string, locale: DemoLocale): DemoCard
318
  title: locale === 'zh-TW' ? '有機蔬菜組' : 'Organic Veg Pack',
319
  subtitle: locale === 'zh-TW' ? '桃園・當日採收' : 'Taoyuan • same-day harvest',
320
  metrics: [
321
- { label: locale === 'zh-TW' ? '單價' : 'Price', value: 'NT$ 220 / set' },
322
  { label: locale === 'zh-TW' ? '庫存' : 'Stock', value: '18' },
323
  ],
324
  },
@@ -340,8 +494,8 @@ function buildCards(tab: DemoTabId, input: string, locale: DemoLocale): DemoCard
340
  title: locale === 'zh-TW' ? '代理回覆摘��' : 'Agent Response Snapshot',
341
  subtitle: topic,
342
  metrics: [
343
- { label: locale === 'zh-TW' ? '路由' : 'Route', value: 'Tool-first' },
344
- { label: locale === 'zh-TW' ? '狀態' : 'Status', value: 'Completed' },
345
  ],
346
  actions: locale === 'zh-TW' ? ['複製結果', '繼續追問'] : ['Copy result', 'Ask follow-up'],
347
  },
@@ -386,7 +540,7 @@ function buildPayload(tab: DemoTabId, locale: DemoLocale): Record<string, unknow
386
  products: 426,
387
  stores: 198,
388
  },
389
- ranking: 'price + stock + location',
390
  };
391
  }
392
 
@@ -481,7 +635,7 @@ export function buildWelcomeText(tab: DemoTabId, locale: DemoLocale): string {
481
 
482
  export async function runMockAgent(params: RunMockAgentParams): Promise<MockAgentResult> {
483
  const { tab, input, locale, mode, onStepStatus, onTrace, onWorkflowInit } = params;
484
- const workflow = WORKFLOWS[tab];
485
  const steps: ProgressStep[] = workflow.map((item, index) => ({
486
  id: `step-${index + 1}`,
487
  label: item.label,
 
27
  onTrace: (item: TraceItem) => void;
28
  }
29
 
30
+ const WORKFLOWS: Record<DemoLocale, Record<DemoTabId, WorkflowTemplate[]>> = {
31
+ en: {
32
+ chat: [
33
+ {
34
+ label: 'Understanding user intent',
35
+ detail: 'Parsing the latest message and session context.',
36
+ doneDetail: 'Intent and preferred action recognized.',
37
+ kind: 'planner',
38
+ durationMs: 600,
39
+ },
40
+ {
41
+ label: 'Planning tool route',
42
+ detail: 'Selecting the best backend function and argument set.',
43
+ doneDetail: 'Tool route and arguments prepared.',
44
+ kind: 'planner',
45
+ durationMs: 700,
46
+ },
47
+ {
48
+ label: 'Executing tool call',
49
+ detail: 'Running product/store/statistics action.',
50
+ doneDetail: 'Tool returned structured data.',
51
+ kind: 'tool',
52
+ durationMs: 900,
53
+ },
54
+ {
55
+ label: 'Rendering response',
56
+ detail: 'Composing concise answer + rich cards.',
57
+ doneDetail: 'Final message and cards generated.',
58
+ kind: 'renderer',
59
+ durationMs: 500,
60
+ },
61
+ ],
62
+ voice: [
63
+ {
64
+ label: 'Normalizing transcript',
65
+ detail: 'Detecting language and cleaning transcript text.',
66
+ doneDetail: 'Transcript normalized.',
67
+ kind: 'validator',
68
+ durationMs: 600,
69
+ },
70
+ {
71
+ label: 'Choosing assistant tool',
72
+ detail: 'Mapping spoken intent to product/store workflows.',
73
+ doneDetail: 'Voice tool selected.',
74
+ kind: 'planner',
75
+ durationMs: 650,
76
+ },
77
+ {
78
+ label: 'Running voice tool',
79
+ detail: 'Calling backend action and collecting UI payload.',
80
+ doneDetail: 'Voice tool output ready.',
81
+ kind: 'tool',
82
+ durationMs: 900,
83
+ },
84
+ {
85
+ label: 'Synthesizing summary',
86
+ detail: 'Producing short speech-safe answer.',
87
+ doneDetail: 'Final voice summary composed.',
88
+ kind: 'renderer',
89
+ durationMs: 450,
90
+ },
91
+ ],
92
+ marketing: [
93
+ {
94
+ label: 'Reading campaign brief',
95
+ detail: 'Extracting tone, audience, and product context.',
96
+ doneDetail: 'Campaign brief parsed.',
97
+ kind: 'planner',
98
+ durationMs: 700,
99
+ },
100
+ {
101
+ label: 'Generating marketing assets',
102
+ detail: 'Streaming copy and image enhancement instructions.',
103
+ doneDetail: 'Marketing draft generated.',
104
+ kind: 'tool',
105
+ durationMs: 1200,
106
+ },
107
+ {
108
+ label: 'Quality checks',
109
+ detail: 'Verifying CTA clarity and consistency.',
110
+ doneDetail: 'Draft passed validation.',
111
+ kind: 'validator',
112
+ durationMs: 550,
113
+ },
114
+ {
115
+ label: 'Preparing showcase output',
116
+ detail: 'Building final campaign cards and metadata.',
117
+ doneDetail: 'Marketing output ready.',
118
+ kind: 'renderer',
119
+ durationMs: 450,
120
+ },
121
+ ],
122
+ invoice: [
123
+ {
124
+ label: 'Pre-processing invoice image',
125
+ detail: 'Normalizing orientation and text contrast.',
126
+ doneDetail: 'Image prepared for extraction.',
127
+ kind: 'validator',
128
+ durationMs: 700,
129
+ },
130
+ {
131
+ label: 'Extracting structured fields',
132
+ detail: 'Detecting invoice number, totals, and dates.',
133
+ doneDetail: 'Invoice fields extracted.',
134
+ kind: 'tool',
135
+ durationMs: 1100,
136
+ },
137
+ {
138
+ label: 'Verifying consistency',
139
+ detail: 'Checking totals, currency, and item count.',
140
+ doneDetail: 'Extraction validated.',
141
+ kind: 'validator',
142
+ durationMs: 650,
143
+ },
144
+ {
145
+ label: 'Formatting result view',
146
+ detail: 'Generating clean JSON + summary cards.',
147
+ doneDetail: 'Invoice output rendered.',
148
+ kind: 'renderer',
149
+ durationMs: 500,
150
+ },
151
+ ],
152
+ marketplace: [
153
+ {
154
+ label: 'Interpreting filters',
155
+ detail: 'Parsing category, location, and price limits.',
156
+ doneDetail: 'Search filters resolved.',
157
+ kind: 'planner',
158
+ durationMs: 650,
159
+ },
160
+ {
161
+ label: 'Querying marketplace data',
162
+ detail: 'Searching products and buyer/store entities.',
163
+ doneDetail: 'Results retrieved.',
164
+ kind: 'tool',
165
+ durationMs: 950,
166
+ },
167
+ {
168
+ label: 'Ranking matches',
169
+ detail: 'Sorting by relevance and availability.',
170
+ doneDetail: 'Top matches selected.',
171
+ kind: 'validator',
172
+ durationMs: 600,
173
+ },
174
+ {
175
+ label: 'Composing answer',
176
+ detail: 'Preparing concise brief with cards.',
177
+ doneDetail: 'Marketplace summary ready.',
178
+ kind: 'renderer',
179
+ durationMs: 450,
180
+ },
181
+ ],
182
+ },
183
+ 'zh-TW': {
184
+ chat: [
185
+ {
186
+ label: '理解使用者意圖',
187
+ detail: '解析最新訊息與對話上下文。',
188
+ doneDetail: '意圖與偏好操作已辨識。',
189
+ kind: 'planner',
190
+ durationMs: 600,
191
+ },
192
+ {
193
+ label: '規劃工具路由',
194
+ detail: '選擇最佳後端函式與參數組合。',
195
+ doneDetail: '工具路徑與參數已準備。',
196
+ kind: 'planner',
197
+ durationMs: 700,
198
+ },
199
+ {
200
+ label: '執行工具呼叫',
201
+ detail: '執行商品、店家或統計操作。',
202
+ doneDetail: '工具已回傳結構化資料。',
203
+ kind: 'tool',
204
+ durationMs: 900,
205
+ },
206
+ {
207
+ label: '渲染回覆內容',
208
+ detail: '整理精簡文字與卡片輸出。',
209
+ doneDetail: '最終訊息與卡片已生成。',
210
+ kind: 'renderer',
211
+ durationMs: 500,
212
+ },
213
+ ],
214
+ voice: [
215
+ {
216
+ label: '標準化語音轉寫',
217
+ detail: '偵測語系並清理語音文字。',
218
+ doneDetail: '語音文字已標準化。',
219
+ kind: 'validator',
220
+ durationMs: 600,
221
+ },
222
+ {
223
+ label: '選擇語音工具',
224
+ detail: '將口語意圖映射到商品/市集流程。',
225
+ doneDetail: '語音工具已選定。',
226
+ kind: 'planner',
227
+ durationMs: 650,
228
+ },
229
+ {
230
+ label: '執行語音工具',
231
+ detail: '呼叫動作並收集 UI 載荷。',
232
+ doneDetail: '語音工具輸出已就緒。',
233
+ kind: 'tool',
234
+ durationMs: 900,
235
+ },
236
+ {
237
+ label: '合成語音摘要',
238
+ detail: '產生簡短可口語播報的回覆。',
239
+ doneDetail: '最終語音摘要已完成。',
240
+ kind: 'renderer',
241
+ durationMs: 450,
242
+ },
243
+ ],
244
+ marketing: [
245
+ {
246
+ label: '讀取行銷需求',
247
+ detail: '擷取語氣、受眾與產品脈絡。',
248
+ doneDetail: '活動需求解析完成。',
249
+ kind: 'planner',
250
+ durationMs: 700,
251
+ },
252
+ {
253
+ label: '生成行銷素材',
254
+ detail: '串流輸出文案與圖像增強指令。',
255
+ doneDetail: '行銷草案已生成。',
256
+ kind: 'tool',
257
+ durationMs: 1200,
258
+ },
259
+ {
260
+ label: '品質檢查',
261
+ detail: '驗證 CTA 清晰度與內容一致性。',
262
+ doneDetail: '草案通過檢查。',
263
+ kind: 'validator',
264
+ durationMs: 550,
265
+ },
266
+ {
267
+ label: '封裝展示輸出',
268
+ detail: '建立最終活動卡片與執行資訊。',
269
+ doneDetail: '行銷輸出已就緒。',
270
+ kind: 'renderer',
271
+ durationMs: 450,
272
+ },
273
+ ],
274
+ invoice: [
275
+ {
276
+ label: '發票影像前處理',
277
+ detail: '標準化方向與文字對比。',
278
+ doneDetail: '影像已準備可供擷取。',
279
+ kind: 'validator',
280
+ durationMs: 700,
281
+ },
282
+ {
283
+ label: '擷取結構化欄位',
284
+ detail: '辨識發票號碼、金額與日期。',
285
+ doneDetail: '發票欄位擷取完成。',
286
+ kind: 'tool',
287
+ durationMs: 1100,
288
+ },
289
+ {
290
+ label: '一致性驗證',
291
+ detail: '檢查總額、幣別與品項數。',
292
+ doneDetail: '擷取結果驗證完成。',
293
+ kind: 'validator',
294
+ durationMs: 650,
295
+ },
296
+ {
297
+ label: '格式化結果檢視',
298
+ detail: '生成乾淨 JSON 與摘要卡片。',
299
+ doneDetail: '發票輸出已渲染。',
300
+ kind: 'renderer',
301
+ durationMs: 500,
302
+ },
303
+ ],
304
+ marketplace: [
305
+ {
306
+ label: '解析篩選條件',
307
+ detail: '整理品類、地區與價格限制。',
308
+ doneDetail: '搜尋條件已完成解析。',
309
+ kind: 'planner',
310
+ durationMs: 650,
311
+ },
312
+ {
313
+ label: '查詢市集資料',
314
+ detail: '搜尋商品與買家/店家實體。',
315
+ doneDetail: '市集結果已取回。',
316
+ kind: 'tool',
317
+ durationMs: 950,
318
+ },
319
+ {
320
+ label: '匹配結果排序',
321
+ detail: '依相關性與可用性排序。',
322
+ doneDetail: '已選出最佳匹配項目。',
323
+ kind: 'validator',
324
+ durationMs: 600,
325
+ },
326
+ {
327
+ label: '組裝最終回覆',
328
+ detail: '整理精簡摘要與結果卡片。',
329
+ doneDetail: '市集摘要已就緒。',
330
+ kind: 'renderer',
331
+ durationMs: 450,
332
+ },
333
+ ],
334
+ },
335
  };
336
 
337
  const QUICK_PROMPTS: Record<DemoLocale, Record<DemoTabId, string[]>> = {
 
463
  title: locale === 'zh-TW' ? '放牧雞蛋' : 'Free-range Eggs',
464
  subtitle: locale === 'zh-TW' ? '新竹・可週配' : 'Hsinchu • weekly delivery',
465
  metrics: [
466
+ { label: locale === 'zh-TW' ? '單價' : 'Price', value: locale === 'zh-TW' ? 'NT$ 118 / 盒' : 'NT$ 118 / box' },
467
  { label: locale === 'zh-TW' ? '庫存' : 'Stock', value: '42' },
468
  ],
469
  tags: [topic, locale === 'zh-TW' ? '熱門' : 'trending'],
 
472
  title: locale === 'zh-TW' ? '有機蔬菜組' : 'Organic Veg Pack',
473
  subtitle: locale === 'zh-TW' ? '桃園・當日採收' : 'Taoyuan • same-day harvest',
474
  metrics: [
475
+ { label: locale === 'zh-TW' ? '單價' : 'Price', value: locale === 'zh-TW' ? 'NT$ 220 / 組' : 'NT$ 220 / set' },
476
  { label: locale === 'zh-TW' ? '庫存' : 'Stock', value: '18' },
477
  ],
478
  },
 
494
  title: locale === 'zh-TW' ? '代理回覆摘��' : 'Agent Response Snapshot',
495
  subtitle: topic,
496
  metrics: [
497
+ { label: locale === 'zh-TW' ? '路由' : 'Route', value: locale === 'zh-TW' ? '工具優先' : 'Tool-first' },
498
+ { label: locale === 'zh-TW' ? '狀態' : 'Status', value: locale === 'zh-TW' ? '已完成' : 'Completed' },
499
  ],
500
  actions: locale === 'zh-TW' ? ['複製結果', '繼續追問'] : ['Copy result', 'Ask follow-up'],
501
  },
 
540
  products: 426,
541
  stores: 198,
542
  },
543
+ ranking: locale === 'zh-TW' ? '價格 + 庫存 + 地區' : 'price + stock + location',
544
  };
545
  }
546
 
 
635
 
636
  export async function runMockAgent(params: RunMockAgentParams): Promise<MockAgentResult> {
637
  const { tab, input, locale, mode, onStepStatus, onTrace, onWorkflowInit } = params;
638
+ const workflow = WORKFLOWS[locale][tab];
639
  const steps: ProgressStep[] = workflow.map((item, index) => ({
640
  id: `step-${index + 1}`,
641
  label: item.label,