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

Update Farm2Market demo frontend

Browse files
package-lock.json CHANGED
@@ -8,6 +8,7 @@
8
  "name": "farm2market-hf-demo-frontend",
9
  "version": "0.1.0",
10
  "dependencies": {
 
11
  "react": "^18.3.1",
12
  "react-dom": "^18.3.1"
13
  },
@@ -650,6 +651,599 @@
650
  "node": ">=12"
651
  }
652
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
653
  "node_modules/@jridgewell/gen-mapping": {
654
  "version": "0.3.13",
655
  "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -695,6 +1289,60 @@
695
  "@jridgewell/sourcemap-codec": "^1.4.14"
696
  }
697
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
698
  "node_modules/@rolldown/pluginutils": {
699
  "version": "1.0.0-beta.27",
700
  "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
@@ -1073,6 +1721,14 @@
1073
  "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
1074
  "dev": true
1075
  },
 
 
 
 
 
 
 
 
1076
  "node_modules/@types/prop-types": {
1077
  "version": "15.7.15",
1078
  "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
@@ -1118,6 +1774,28 @@
1118
  "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1119
  }
1120
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1121
  "node_modules/baseline-browser-mapping": {
1122
  "version": "2.10.0",
1123
  "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
@@ -1183,6 +1861,35 @@
1183
  }
1184
  ]
1185
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1186
  "node_modules/convert-source-map": {
1187
  "version": "2.0.0",
1188
  "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -1218,6 +1925,11 @@
1218
  "integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==",
1219
  "dev": true
1220
  },
 
 
 
 
 
1221
  "node_modules/esbuild": {
1222
  "version": "0.21.5",
1223
  "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
@@ -1260,11 +1972,79 @@
1260
  "version": "3.2.0",
1261
  "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1262
  "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1263
- "dev": true,
1264
  "engines": {
1265
  "node": ">=6"
1266
  }
1267
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1268
  "node_modules/fsevents": {
1269
  "version": "2.3.3",
1270
  "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1288,6 +2068,32 @@
1288
  "node": ">=6.9.0"
1289
  }
1290
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1291
  "node_modules/js-tokens": {
1292
  "version": "4.0.0",
1293
  "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -1317,6 +2123,16 @@
1317
  "node": ">=6"
1318
  }
1319
  },
 
 
 
 
 
 
 
 
 
 
1320
  "node_modules/loose-envify": {
1321
  "version": "1.4.0",
1322
  "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -1401,6 +2217,29 @@
1401
  "node": "^10 || ^12 || >=14"
1402
  }
1403
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1404
  "node_modules/react": {
1405
  "version": "18.3.1",
1406
  "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
@@ -1433,6 +2272,14 @@
1433
  "node": ">=0.10.0"
1434
  }
1435
  },
 
 
 
 
 
 
 
 
1436
  "node_modules/rollup": {
1437
  "version": "4.59.0",
1438
  "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
@@ -1477,6 +2324,25 @@
1477
  "fsevents": "~2.3.2"
1478
  }
1479
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1480
  "node_modules/scheduler": {
1481
  "version": "0.23.2",
1482
  "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
@@ -1503,6 +2369,35 @@
1503
  "node": ">=0.10.0"
1504
  }
1505
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1506
  "node_modules/typescript": {
1507
  "version": "5.9.3",
1508
  "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -1516,6 +2411,11 @@
1516
  "node": ">=14.17"
1517
  }
1518
  },
 
 
 
 
 
1519
  "node_modules/update-browserslist-db": {
1520
  "version": "1.2.3",
1521
  "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
@@ -1605,11 +2505,86 @@
1605
  }
1606
  }
1607
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1608
  "node_modules/yallist": {
1609
  "version": "3.1.1",
1610
  "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
1611
  "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
1612
  "dev": true
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1613
  }
1614
  }
1615
  }
 
8
  "name": "farm2market-hf-demo-frontend",
9
  "version": "0.1.0",
10
  "dependencies": {
11
+ "firebase": "^11.10.0",
12
  "react": "^18.3.1",
13
  "react-dom": "^18.3.1"
14
  },
 
651
  "node": ">=12"
652
  }
653
  },
654
+ "node_modules/@firebase/ai": {
655
+ "version": "1.4.1",
656
+ "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-1.4.1.tgz",
657
+ "integrity": "sha512-bcusQfA/tHjUjBTnMx6jdoPMpDl3r8K15Z+snHz9wq0Foox0F/V+kNLXucEOHoTL2hTc9l+onZCyBJs2QoIC3g==",
658
+ "dependencies": {
659
+ "@firebase/app-check-interop-types": "0.3.3",
660
+ "@firebase/component": "0.6.18",
661
+ "@firebase/logger": "0.4.4",
662
+ "@firebase/util": "1.12.1",
663
+ "tslib": "^2.1.0"
664
+ },
665
+ "engines": {
666
+ "node": ">=18.0.0"
667
+ },
668
+ "peerDependencies": {
669
+ "@firebase/app": "0.x",
670
+ "@firebase/app-types": "0.x"
671
+ }
672
+ },
673
+ "node_modules/@firebase/analytics": {
674
+ "version": "0.10.17",
675
+ "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.17.tgz",
676
+ "integrity": "sha512-n5vfBbvzduMou/2cqsnKrIes4auaBjdhg8QNA2ZQZ59QgtO2QiwBaXQZQE4O4sgB0Ds1tvLgUUkY+pwzu6/xEg==",
677
+ "dependencies": {
678
+ "@firebase/component": "0.6.18",
679
+ "@firebase/installations": "0.6.18",
680
+ "@firebase/logger": "0.4.4",
681
+ "@firebase/util": "1.12.1",
682
+ "tslib": "^2.1.0"
683
+ },
684
+ "peerDependencies": {
685
+ "@firebase/app": "0.x"
686
+ }
687
+ },
688
+ "node_modules/@firebase/analytics-compat": {
689
+ "version": "0.2.23",
690
+ "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.23.tgz",
691
+ "integrity": "sha512-3AdO10RN18G5AzREPoFgYhW6vWXr3u+OYQv6pl3CX6Fky8QRk0AHurZlY3Q1xkXO0TDxIsdhO3y65HF7PBOJDw==",
692
+ "dependencies": {
693
+ "@firebase/analytics": "0.10.17",
694
+ "@firebase/analytics-types": "0.8.3",
695
+ "@firebase/component": "0.6.18",
696
+ "@firebase/util": "1.12.1",
697
+ "tslib": "^2.1.0"
698
+ },
699
+ "peerDependencies": {
700
+ "@firebase/app-compat": "0.x"
701
+ }
702
+ },
703
+ "node_modules/@firebase/analytics-types": {
704
+ "version": "0.8.3",
705
+ "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.3.tgz",
706
+ "integrity": "sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg=="
707
+ },
708
+ "node_modules/@firebase/app": {
709
+ "version": "0.13.2",
710
+ "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.13.2.tgz",
711
+ "integrity": "sha512-jwtMmJa1BXXDCiDx1vC6SFN/+HfYG53UkfJa6qeN5ogvOunzbFDO3wISZy5n9xgYFUrEP6M7e8EG++riHNTv9w==",
712
+ "dependencies": {
713
+ "@firebase/component": "0.6.18",
714
+ "@firebase/logger": "0.4.4",
715
+ "@firebase/util": "1.12.1",
716
+ "idb": "7.1.1",
717
+ "tslib": "^2.1.0"
718
+ },
719
+ "engines": {
720
+ "node": ">=18.0.0"
721
+ }
722
+ },
723
+ "node_modules/@firebase/app-check": {
724
+ "version": "0.10.1",
725
+ "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.10.1.tgz",
726
+ "integrity": "sha512-MgNdlms9Qb0oSny87pwpjKush9qUwCJhfmTJHDfrcKo4neLGiSeVE4qJkzP7EQTIUFKp84pbTxobSAXkiuQVYQ==",
727
+ "dependencies": {
728
+ "@firebase/component": "0.6.18",
729
+ "@firebase/logger": "0.4.4",
730
+ "@firebase/util": "1.12.1",
731
+ "tslib": "^2.1.0"
732
+ },
733
+ "engines": {
734
+ "node": ">=18.0.0"
735
+ },
736
+ "peerDependencies": {
737
+ "@firebase/app": "0.x"
738
+ }
739
+ },
740
+ "node_modules/@firebase/app-check-compat": {
741
+ "version": "0.3.26",
742
+ "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.26.tgz",
743
+ "integrity": "sha512-PkX+XJMLDea6nmnopzFKlr+s2LMQGqdyT2DHdbx1v1dPSqOol2YzgpgymmhC67vitXVpNvS3m/AiWQWWhhRRPQ==",
744
+ "dependencies": {
745
+ "@firebase/app-check": "0.10.1",
746
+ "@firebase/app-check-types": "0.5.3",
747
+ "@firebase/component": "0.6.18",
748
+ "@firebase/logger": "0.4.4",
749
+ "@firebase/util": "1.12.1",
750
+ "tslib": "^2.1.0"
751
+ },
752
+ "engines": {
753
+ "node": ">=18.0.0"
754
+ },
755
+ "peerDependencies": {
756
+ "@firebase/app-compat": "0.x"
757
+ }
758
+ },
759
+ "node_modules/@firebase/app-check-interop-types": {
760
+ "version": "0.3.3",
761
+ "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz",
762
+ "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A=="
763
+ },
764
+ "node_modules/@firebase/app-check-types": {
765
+ "version": "0.5.3",
766
+ "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.3.tgz",
767
+ "integrity": "sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng=="
768
+ },
769
+ "node_modules/@firebase/app-compat": {
770
+ "version": "0.4.2",
771
+ "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.4.2.tgz",
772
+ "integrity": "sha512-LssbyKHlwLeiV8GBATyOyjmHcMpX/tFjzRUCS1jnwGAew1VsBB4fJowyS5Ud5LdFbYpJeS+IQoC+RQxpK7eH3Q==",
773
+ "dependencies": {
774
+ "@firebase/app": "0.13.2",
775
+ "@firebase/component": "0.6.18",
776
+ "@firebase/logger": "0.4.4",
777
+ "@firebase/util": "1.12.1",
778
+ "tslib": "^2.1.0"
779
+ },
780
+ "engines": {
781
+ "node": ">=18.0.0"
782
+ }
783
+ },
784
+ "node_modules/@firebase/app-types": {
785
+ "version": "0.9.3",
786
+ "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz",
787
+ "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw=="
788
+ },
789
+ "node_modules/@firebase/auth-compat": {
790
+ "version": "0.5.28",
791
+ "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.5.28.tgz",
792
+ "integrity": "sha512-HpMSo/cc6Y8IX7bkRIaPPqT//Jt83iWy5rmDWeThXQCAImstkdNo3giFLORJwrZw2ptiGkOij64EH1ztNJzc7Q==",
793
+ "dependencies": {
794
+ "@firebase/auth": "1.10.8",
795
+ "@firebase/auth-types": "0.13.0",
796
+ "@firebase/component": "0.6.18",
797
+ "@firebase/util": "1.12.1",
798
+ "tslib": "^2.1.0"
799
+ },
800
+ "engines": {
801
+ "node": ">=18.0.0"
802
+ },
803
+ "peerDependencies": {
804
+ "@firebase/app-compat": "0.x"
805
+ }
806
+ },
807
+ "node_modules/@firebase/auth-compat/node_modules/@firebase/auth": {
808
+ "version": "1.10.8",
809
+ "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.10.8.tgz",
810
+ "integrity": "sha512-GpuTz5ap8zumr/ocnPY57ZanX02COsXloY6Y/2LYPAuXYiaJRf6BAGDEdRq1BMjP93kqQnKNuKZUTMZbQ8MNYA==",
811
+ "dependencies": {
812
+ "@firebase/component": "0.6.18",
813
+ "@firebase/logger": "0.4.4",
814
+ "@firebase/util": "1.12.1",
815
+ "tslib": "^2.1.0"
816
+ },
817
+ "engines": {
818
+ "node": ">=18.0.0"
819
+ },
820
+ "peerDependencies": {
821
+ "@firebase/app": "0.x",
822
+ "@react-native-async-storage/async-storage": "^1.18.1"
823
+ },
824
+ "peerDependenciesMeta": {
825
+ "@react-native-async-storage/async-storage": {
826
+ "optional": true
827
+ }
828
+ }
829
+ },
830
+ "node_modules/@firebase/auth-interop-types": {
831
+ "version": "0.2.4",
832
+ "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz",
833
+ "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA=="
834
+ },
835
+ "node_modules/@firebase/auth-types": {
836
+ "version": "0.13.0",
837
+ "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.0.tgz",
838
+ "integrity": "sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==",
839
+ "peerDependencies": {
840
+ "@firebase/app-types": "0.x",
841
+ "@firebase/util": "1.x"
842
+ }
843
+ },
844
+ "node_modules/@firebase/component": {
845
+ "version": "0.6.18",
846
+ "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.18.tgz",
847
+ "integrity": "sha512-n28kPCkE2dL2U28fSxZJjzPPVpKsQminJ6NrzcKXAI0E/lYC8YhfwpyllScqVEvAI3J2QgJZWYgrX+1qGI+SQQ==",
848
+ "dependencies": {
849
+ "@firebase/util": "1.12.1",
850
+ "tslib": "^2.1.0"
851
+ },
852
+ "engines": {
853
+ "node": ">=18.0.0"
854
+ }
855
+ },
856
+ "node_modules/@firebase/data-connect": {
857
+ "version": "0.3.10",
858
+ "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.3.10.tgz",
859
+ "integrity": "sha512-VMVk7zxIkgwlVQIWHOKFahmleIjiVFwFOjmakXPd/LDgaB/5vzwsB5DWIYo+3KhGxWpidQlR8geCIn39YflJIQ==",
860
+ "dependencies": {
861
+ "@firebase/auth-interop-types": "0.2.4",
862
+ "@firebase/component": "0.6.18",
863
+ "@firebase/logger": "0.4.4",
864
+ "@firebase/util": "1.12.1",
865
+ "tslib": "^2.1.0"
866
+ },
867
+ "peerDependencies": {
868
+ "@firebase/app": "0.x"
869
+ }
870
+ },
871
+ "node_modules/@firebase/database": {
872
+ "version": "1.0.20",
873
+ "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.20.tgz",
874
+ "integrity": "sha512-H9Rpj1pQ1yc9+4HQOotFGLxqAXwOzCHsRSRjcQFNOr8lhUt6LeYjf0NSRL04sc4X0dWe8DsCvYKxMYvFG/iOJw==",
875
+ "dependencies": {
876
+ "@firebase/app-check-interop-types": "0.3.3",
877
+ "@firebase/auth-interop-types": "0.2.4",
878
+ "@firebase/component": "0.6.18",
879
+ "@firebase/logger": "0.4.4",
880
+ "@firebase/util": "1.12.1",
881
+ "faye-websocket": "0.11.4",
882
+ "tslib": "^2.1.0"
883
+ },
884
+ "engines": {
885
+ "node": ">=18.0.0"
886
+ }
887
+ },
888
+ "node_modules/@firebase/database-compat": {
889
+ "version": "2.0.11",
890
+ "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.0.11.tgz",
891
+ "integrity": "sha512-itEsHARSsYS95+udF/TtIzNeQ0Uhx4uIna0sk4E0wQJBUnLc/G1X6D7oRljoOuwwCezRLGvWBRyNrugv/esOEw==",
892
+ "dependencies": {
893
+ "@firebase/component": "0.6.18",
894
+ "@firebase/database": "1.0.20",
895
+ "@firebase/database-types": "1.0.15",
896
+ "@firebase/logger": "0.4.4",
897
+ "@firebase/util": "1.12.1",
898
+ "tslib": "^2.1.0"
899
+ },
900
+ "engines": {
901
+ "node": ">=18.0.0"
902
+ }
903
+ },
904
+ "node_modules/@firebase/database-types": {
905
+ "version": "1.0.15",
906
+ "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.15.tgz",
907
+ "integrity": "sha512-XWHJ0VUJ0k2E9HDMlKxlgy/ZuTa9EvHCGLjaKSUvrQnwhgZuRU5N3yX6SZ+ftf2hTzZmfRkv+b3QRvGg40bKNw==",
908
+ "dependencies": {
909
+ "@firebase/app-types": "0.9.3",
910
+ "@firebase/util": "1.12.1"
911
+ }
912
+ },
913
+ "node_modules/@firebase/firestore": {
914
+ "version": "4.8.0",
915
+ "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.8.0.tgz",
916
+ "integrity": "sha512-QSRk+Q1/CaabKyqn3C32KSFiOdZpSqI9rpLK5BHPcooElumOBooPFa6YkDdiT+/KhJtel36LdAacha9BptMj2A==",
917
+ "dependencies": {
918
+ "@firebase/component": "0.6.18",
919
+ "@firebase/logger": "0.4.4",
920
+ "@firebase/util": "1.12.1",
921
+ "@firebase/webchannel-wrapper": "1.0.3",
922
+ "@grpc/grpc-js": "~1.9.0",
923
+ "@grpc/proto-loader": "^0.7.8",
924
+ "tslib": "^2.1.0"
925
+ },
926
+ "engines": {
927
+ "node": ">=18.0.0"
928
+ },
929
+ "peerDependencies": {
930
+ "@firebase/app": "0.x"
931
+ }
932
+ },
933
+ "node_modules/@firebase/firestore-compat": {
934
+ "version": "0.3.53",
935
+ "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.53.tgz",
936
+ "integrity": "sha512-qI3yZL8ljwAYWrTousWYbemay2YZa+udLWugjdjju2KODWtLG94DfO4NALJgPLv8CVGcDHNFXoyQexdRA0Cz8Q==",
937
+ "dependencies": {
938
+ "@firebase/component": "0.6.18",
939
+ "@firebase/firestore": "4.8.0",
940
+ "@firebase/firestore-types": "3.0.3",
941
+ "@firebase/util": "1.12.1",
942
+ "tslib": "^2.1.0"
943
+ },
944
+ "engines": {
945
+ "node": ">=18.0.0"
946
+ },
947
+ "peerDependencies": {
948
+ "@firebase/app-compat": "0.x"
949
+ }
950
+ },
951
+ "node_modules/@firebase/firestore-types": {
952
+ "version": "3.0.3",
953
+ "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.3.tgz",
954
+ "integrity": "sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==",
955
+ "peerDependencies": {
956
+ "@firebase/app-types": "0.x",
957
+ "@firebase/util": "1.x"
958
+ }
959
+ },
960
+ "node_modules/@firebase/functions": {
961
+ "version": "0.12.9",
962
+ "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.12.9.tgz",
963
+ "integrity": "sha512-FG95w6vjbUXN84Ehezc2SDjGmGq225UYbHrb/ptkRT7OTuCiQRErOQuyt1jI1tvcDekdNog+anIObihNFz79Lg==",
964
+ "dependencies": {
965
+ "@firebase/app-check-interop-types": "0.3.3",
966
+ "@firebase/auth-interop-types": "0.2.4",
967
+ "@firebase/component": "0.6.18",
968
+ "@firebase/messaging-interop-types": "0.2.3",
969
+ "@firebase/util": "1.12.1",
970
+ "tslib": "^2.1.0"
971
+ },
972
+ "engines": {
973
+ "node": ">=18.0.0"
974
+ },
975
+ "peerDependencies": {
976
+ "@firebase/app": "0.x"
977
+ }
978
+ },
979
+ "node_modules/@firebase/functions-compat": {
980
+ "version": "0.3.26",
981
+ "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.26.tgz",
982
+ "integrity": "sha512-A798/6ff5LcG2LTWqaGazbFYnjBW8zc65YfID/en83ALmkhu2b0G8ykvQnLtakbV9ajrMYPn7Yc/XcYsZIUsjA==",
983
+ "dependencies": {
984
+ "@firebase/component": "0.6.18",
985
+ "@firebase/functions": "0.12.9",
986
+ "@firebase/functions-types": "0.6.3",
987
+ "@firebase/util": "1.12.1",
988
+ "tslib": "^2.1.0"
989
+ },
990
+ "engines": {
991
+ "node": ">=18.0.0"
992
+ },
993
+ "peerDependencies": {
994
+ "@firebase/app-compat": "0.x"
995
+ }
996
+ },
997
+ "node_modules/@firebase/functions-types": {
998
+ "version": "0.6.3",
999
+ "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.3.tgz",
1000
+ "integrity": "sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg=="
1001
+ },
1002
+ "node_modules/@firebase/installations": {
1003
+ "version": "0.6.18",
1004
+ "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.18.tgz",
1005
+ "integrity": "sha512-NQ86uGAcvO8nBRwVltRL9QQ4Reidc/3whdAasgeWCPIcrhOKDuNpAALa6eCVryLnK14ua2DqekCOX5uC9XbU/A==",
1006
+ "dependencies": {
1007
+ "@firebase/component": "0.6.18",
1008
+ "@firebase/util": "1.12.1",
1009
+ "idb": "7.1.1",
1010
+ "tslib": "^2.1.0"
1011
+ },
1012
+ "peerDependencies": {
1013
+ "@firebase/app": "0.x"
1014
+ }
1015
+ },
1016
+ "node_modules/@firebase/installations-compat": {
1017
+ "version": "0.2.18",
1018
+ "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.18.tgz",
1019
+ "integrity": "sha512-aLFohRpJO5kKBL/XYL4tN+GdwEB/Q6Vo9eZOM/6Kic7asSUgmSfGPpGUZO1OAaSRGwF4Lqnvi1f/f9VZnKzChw==",
1020
+ "dependencies": {
1021
+ "@firebase/component": "0.6.18",
1022
+ "@firebase/installations": "0.6.18",
1023
+ "@firebase/installations-types": "0.5.3",
1024
+ "@firebase/util": "1.12.1",
1025
+ "tslib": "^2.1.0"
1026
+ },
1027
+ "peerDependencies": {
1028
+ "@firebase/app-compat": "0.x"
1029
+ }
1030
+ },
1031
+ "node_modules/@firebase/installations-types": {
1032
+ "version": "0.5.3",
1033
+ "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.3.tgz",
1034
+ "integrity": "sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==",
1035
+ "peerDependencies": {
1036
+ "@firebase/app-types": "0.x"
1037
+ }
1038
+ },
1039
+ "node_modules/@firebase/logger": {
1040
+ "version": "0.4.4",
1041
+ "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.4.tgz",
1042
+ "integrity": "sha512-mH0PEh1zoXGnaR8gD1DeGeNZtWFKbnz9hDO91dIml3iou1gpOnLqXQ2dJfB71dj6dpmUjcQ6phY3ZZJbjErr9g==",
1043
+ "dependencies": {
1044
+ "tslib": "^2.1.0"
1045
+ },
1046
+ "engines": {
1047
+ "node": ">=18.0.0"
1048
+ }
1049
+ },
1050
+ "node_modules/@firebase/messaging": {
1051
+ "version": "0.12.22",
1052
+ "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.22.tgz",
1053
+ "integrity": "sha512-GJcrPLc+Hu7nk+XQ70Okt3M1u1eRr2ZvpMbzbc54oTPJZySHcX9ccZGVFcsZbSZ6o1uqumm8Oc7OFkD3Rn1/og==",
1054
+ "dependencies": {
1055
+ "@firebase/component": "0.6.18",
1056
+ "@firebase/installations": "0.6.18",
1057
+ "@firebase/messaging-interop-types": "0.2.3",
1058
+ "@firebase/util": "1.12.1",
1059
+ "idb": "7.1.1",
1060
+ "tslib": "^2.1.0"
1061
+ },
1062
+ "peerDependencies": {
1063
+ "@firebase/app": "0.x"
1064
+ }
1065
+ },
1066
+ "node_modules/@firebase/messaging-compat": {
1067
+ "version": "0.2.22",
1068
+ "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.22.tgz",
1069
+ "integrity": "sha512-5ZHtRnj6YO6f/QPa/KU6gryjmX4Kg33Kn4gRpNU6M1K47Gm8kcQwPkX7erRUYEH1mIWptfvjvXMHWoZaWjkU7A==",
1070
+ "dependencies": {
1071
+ "@firebase/component": "0.6.18",
1072
+ "@firebase/messaging": "0.12.22",
1073
+ "@firebase/util": "1.12.1",
1074
+ "tslib": "^2.1.0"
1075
+ },
1076
+ "peerDependencies": {
1077
+ "@firebase/app-compat": "0.x"
1078
+ }
1079
+ },
1080
+ "node_modules/@firebase/messaging-interop-types": {
1081
+ "version": "0.2.3",
1082
+ "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz",
1083
+ "integrity": "sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q=="
1084
+ },
1085
+ "node_modules/@firebase/performance": {
1086
+ "version": "0.7.7",
1087
+ "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.7.tgz",
1088
+ "integrity": "sha512-JTlTQNZKAd4+Q5sodpw6CN+6NmwbY72av3Lb6wUKTsL7rb3cuBIhQSrslWbVz0SwK3x0ZNcqX24qtRbwKiv+6w==",
1089
+ "dependencies": {
1090
+ "@firebase/component": "0.6.18",
1091
+ "@firebase/installations": "0.6.18",
1092
+ "@firebase/logger": "0.4.4",
1093
+ "@firebase/util": "1.12.1",
1094
+ "tslib": "^2.1.0",
1095
+ "web-vitals": "^4.2.4"
1096
+ },
1097
+ "peerDependencies": {
1098
+ "@firebase/app": "0.x"
1099
+ }
1100
+ },
1101
+ "node_modules/@firebase/performance-compat": {
1102
+ "version": "0.2.20",
1103
+ "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.20.tgz",
1104
+ "integrity": "sha512-XkFK5NmOKCBuqOKWeRgBUFZZGz9SzdTZp4OqeUg+5nyjapTiZ4XoiiUL8z7mB2q+63rPmBl7msv682J3rcDXIQ==",
1105
+ "dependencies": {
1106
+ "@firebase/component": "0.6.18",
1107
+ "@firebase/logger": "0.4.4",
1108
+ "@firebase/performance": "0.7.7",
1109
+ "@firebase/performance-types": "0.2.3",
1110
+ "@firebase/util": "1.12.1",
1111
+ "tslib": "^2.1.0"
1112
+ },
1113
+ "peerDependencies": {
1114
+ "@firebase/app-compat": "0.x"
1115
+ }
1116
+ },
1117
+ "node_modules/@firebase/performance-types": {
1118
+ "version": "0.2.3",
1119
+ "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.3.tgz",
1120
+ "integrity": "sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ=="
1121
+ },
1122
+ "node_modules/@firebase/remote-config": {
1123
+ "version": "0.6.5",
1124
+ "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.6.5.tgz",
1125
+ "integrity": "sha512-fU0c8HY0vrVHwC+zQ/fpXSqHyDMuuuglV94VF6Yonhz8Fg2J+KOowPGANM0SZkLvVOYpTeWp3ZmM+F6NjwWLnw==",
1126
+ "dependencies": {
1127
+ "@firebase/component": "0.6.18",
1128
+ "@firebase/installations": "0.6.18",
1129
+ "@firebase/logger": "0.4.4",
1130
+ "@firebase/util": "1.12.1",
1131
+ "tslib": "^2.1.0"
1132
+ },
1133
+ "peerDependencies": {
1134
+ "@firebase/app": "0.x"
1135
+ }
1136
+ },
1137
+ "node_modules/@firebase/remote-config-compat": {
1138
+ "version": "0.2.18",
1139
+ "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.18.tgz",
1140
+ "integrity": "sha512-YiETpldhDy7zUrnS8e+3l7cNs0sL7+tVAxvVYU0lu7O+qLHbmdtAxmgY+wJqWdW2c9nDvBFec7QiF58pEUu0qQ==",
1141
+ "dependencies": {
1142
+ "@firebase/component": "0.6.18",
1143
+ "@firebase/logger": "0.4.4",
1144
+ "@firebase/remote-config": "0.6.5",
1145
+ "@firebase/remote-config-types": "0.4.0",
1146
+ "@firebase/util": "1.12.1",
1147
+ "tslib": "^2.1.0"
1148
+ },
1149
+ "peerDependencies": {
1150
+ "@firebase/app-compat": "0.x"
1151
+ }
1152
+ },
1153
+ "node_modules/@firebase/remote-config-types": {
1154
+ "version": "0.4.0",
1155
+ "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.4.0.tgz",
1156
+ "integrity": "sha512-7p3mRE/ldCNYt8fmWMQ/MSGRmXYlJ15Rvs9Rk17t8p0WwZDbeK7eRmoI1tvCPaDzn9Oqh+yD6Lw+sGLsLg4kKg=="
1157
+ },
1158
+ "node_modules/@firebase/storage": {
1159
+ "version": "0.13.14",
1160
+ "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.13.14.tgz",
1161
+ "integrity": "sha512-xTq5ixxORzx+bfqCpsh+o3fxOsGoDjC1nO0Mq2+KsOcny3l7beyBhP/y1u5T6mgsFQwI1j6oAkbT5cWdDBx87g==",
1162
+ "dependencies": {
1163
+ "@firebase/component": "0.6.18",
1164
+ "@firebase/util": "1.12.1",
1165
+ "tslib": "^2.1.0"
1166
+ },
1167
+ "engines": {
1168
+ "node": ">=18.0.0"
1169
+ },
1170
+ "peerDependencies": {
1171
+ "@firebase/app": "0.x"
1172
+ }
1173
+ },
1174
+ "node_modules/@firebase/storage-compat": {
1175
+ "version": "0.3.24",
1176
+ "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.24.tgz",
1177
+ "integrity": "sha512-XHn2tLniiP7BFKJaPZ0P8YQXKiVJX+bMyE2j2YWjYfaddqiJnROJYqSomwW6L3Y+gZAga35ONXUJQju6MB6SOQ==",
1178
+ "dependencies": {
1179
+ "@firebase/component": "0.6.18",
1180
+ "@firebase/storage": "0.13.14",
1181
+ "@firebase/storage-types": "0.8.3",
1182
+ "@firebase/util": "1.12.1",
1183
+ "tslib": "^2.1.0"
1184
+ },
1185
+ "engines": {
1186
+ "node": ">=18.0.0"
1187
+ },
1188
+ "peerDependencies": {
1189
+ "@firebase/app-compat": "0.x"
1190
+ }
1191
+ },
1192
+ "node_modules/@firebase/storage-types": {
1193
+ "version": "0.8.3",
1194
+ "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.3.tgz",
1195
+ "integrity": "sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==",
1196
+ "peerDependencies": {
1197
+ "@firebase/app-types": "0.x",
1198
+ "@firebase/util": "1.x"
1199
+ }
1200
+ },
1201
+ "node_modules/@firebase/util": {
1202
+ "version": "1.12.1",
1203
+ "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.12.1.tgz",
1204
+ "integrity": "sha512-zGlBn/9Dnya5ta9bX/fgEoNC3Cp8s6h+uYPYaDieZsFOAdHP/ExzQ/eaDgxD3GOROdPkLKpvKY0iIzr9adle0w==",
1205
+ "hasInstallScript": true,
1206
+ "dependencies": {
1207
+ "tslib": "^2.1.0"
1208
+ },
1209
+ "engines": {
1210
+ "node": ">=18.0.0"
1211
+ }
1212
+ },
1213
+ "node_modules/@firebase/webchannel-wrapper": {
1214
+ "version": "1.0.3",
1215
+ "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.3.tgz",
1216
+ "integrity": "sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ=="
1217
+ },
1218
+ "node_modules/@grpc/grpc-js": {
1219
+ "version": "1.9.15",
1220
+ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz",
1221
+ "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==",
1222
+ "dependencies": {
1223
+ "@grpc/proto-loader": "^0.7.8",
1224
+ "@types/node": ">=12.12.47"
1225
+ },
1226
+ "engines": {
1227
+ "node": "^8.13.0 || >=10.10.0"
1228
+ }
1229
+ },
1230
+ "node_modules/@grpc/proto-loader": {
1231
+ "version": "0.7.15",
1232
+ "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz",
1233
+ "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==",
1234
+ "dependencies": {
1235
+ "lodash.camelcase": "^4.3.0",
1236
+ "long": "^5.0.0",
1237
+ "protobufjs": "^7.2.5",
1238
+ "yargs": "^17.7.2"
1239
+ },
1240
+ "bin": {
1241
+ "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
1242
+ },
1243
+ "engines": {
1244
+ "node": ">=6"
1245
+ }
1246
+ },
1247
  "node_modules/@jridgewell/gen-mapping": {
1248
  "version": "0.3.13",
1249
  "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
 
1289
  "@jridgewell/sourcemap-codec": "^1.4.14"
1290
  }
1291
  },
1292
+ "node_modules/@protobufjs/aspromise": {
1293
+ "version": "1.1.2",
1294
+ "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
1295
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
1296
+ },
1297
+ "node_modules/@protobufjs/base64": {
1298
+ "version": "1.1.2",
1299
+ "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
1300
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
1301
+ },
1302
+ "node_modules/@protobufjs/codegen": {
1303
+ "version": "2.0.4",
1304
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
1305
+ "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
1306
+ },
1307
+ "node_modules/@protobufjs/eventemitter": {
1308
+ "version": "1.1.0",
1309
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
1310
+ "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
1311
+ },
1312
+ "node_modules/@protobufjs/fetch": {
1313
+ "version": "1.1.0",
1314
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
1315
+ "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
1316
+ "dependencies": {
1317
+ "@protobufjs/aspromise": "^1.1.1",
1318
+ "@protobufjs/inquire": "^1.1.0"
1319
+ }
1320
+ },
1321
+ "node_modules/@protobufjs/float": {
1322
+ "version": "1.0.2",
1323
+ "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
1324
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
1325
+ },
1326
+ "node_modules/@protobufjs/inquire": {
1327
+ "version": "1.1.0",
1328
+ "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
1329
+ "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
1330
+ },
1331
+ "node_modules/@protobufjs/path": {
1332
+ "version": "1.1.2",
1333
+ "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
1334
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
1335
+ },
1336
+ "node_modules/@protobufjs/pool": {
1337
+ "version": "1.1.0",
1338
+ "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
1339
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
1340
+ },
1341
+ "node_modules/@protobufjs/utf8": {
1342
+ "version": "1.1.0",
1343
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
1344
+ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
1345
+ },
1346
  "node_modules/@rolldown/pluginutils": {
1347
  "version": "1.0.0-beta.27",
1348
  "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
 
1721
  "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
1722
  "dev": true
1723
  },
1724
+ "node_modules/@types/node": {
1725
+ "version": "25.5.0",
1726
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
1727
+ "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
1728
+ "dependencies": {
1729
+ "undici-types": "~7.18.0"
1730
+ }
1731
+ },
1732
  "node_modules/@types/prop-types": {
1733
  "version": "15.7.15",
1734
  "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
 
1774
  "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1775
  }
1776
  },
1777
+ "node_modules/ansi-regex": {
1778
+ "version": "5.0.1",
1779
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
1780
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
1781
+ "engines": {
1782
+ "node": ">=8"
1783
+ }
1784
+ },
1785
+ "node_modules/ansi-styles": {
1786
+ "version": "4.3.0",
1787
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
1788
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
1789
+ "dependencies": {
1790
+ "color-convert": "^2.0.1"
1791
+ },
1792
+ "engines": {
1793
+ "node": ">=8"
1794
+ },
1795
+ "funding": {
1796
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
1797
+ }
1798
+ },
1799
  "node_modules/baseline-browser-mapping": {
1800
  "version": "2.10.0",
1801
  "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
 
1861
  }
1862
  ]
1863
  },
1864
+ "node_modules/cliui": {
1865
+ "version": "8.0.1",
1866
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
1867
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
1868
+ "dependencies": {
1869
+ "string-width": "^4.2.0",
1870
+ "strip-ansi": "^6.0.1",
1871
+ "wrap-ansi": "^7.0.0"
1872
+ },
1873
+ "engines": {
1874
+ "node": ">=12"
1875
+ }
1876
+ },
1877
+ "node_modules/color-convert": {
1878
+ "version": "2.0.1",
1879
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
1880
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
1881
+ "dependencies": {
1882
+ "color-name": "~1.1.4"
1883
+ },
1884
+ "engines": {
1885
+ "node": ">=7.0.0"
1886
+ }
1887
+ },
1888
+ "node_modules/color-name": {
1889
+ "version": "1.1.4",
1890
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
1891
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
1892
+ },
1893
  "node_modules/convert-source-map": {
1894
  "version": "2.0.0",
1895
  "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
 
1925
  "integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==",
1926
  "dev": true
1927
  },
1928
+ "node_modules/emoji-regex": {
1929
+ "version": "8.0.0",
1930
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
1931
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
1932
+ },
1933
  "node_modules/esbuild": {
1934
  "version": "0.21.5",
1935
  "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
 
1972
  "version": "3.2.0",
1973
  "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1974
  "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
 
1975
  "engines": {
1976
  "node": ">=6"
1977
  }
1978
  },
1979
+ "node_modules/faye-websocket": {
1980
+ "version": "0.11.4",
1981
+ "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz",
1982
+ "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==",
1983
+ "dependencies": {
1984
+ "websocket-driver": ">=0.5.1"
1985
+ },
1986
+ "engines": {
1987
+ "node": ">=0.8.0"
1988
+ }
1989
+ },
1990
+ "node_modules/firebase": {
1991
+ "version": "11.10.0",
1992
+ "resolved": "https://registry.npmjs.org/firebase/-/firebase-11.10.0.tgz",
1993
+ "integrity": "sha512-nKBXoDzF0DrXTBQJlZa+sbC5By99ysYU1D6PkMRYknm0nCW7rJly47q492Ht7Ndz5MeYSBuboKuhS1e6mFC03w==",
1994
+ "dependencies": {
1995
+ "@firebase/ai": "1.4.1",
1996
+ "@firebase/analytics": "0.10.17",
1997
+ "@firebase/analytics-compat": "0.2.23",
1998
+ "@firebase/app": "0.13.2",
1999
+ "@firebase/app-check": "0.10.1",
2000
+ "@firebase/app-check-compat": "0.3.26",
2001
+ "@firebase/app-compat": "0.4.2",
2002
+ "@firebase/app-types": "0.9.3",
2003
+ "@firebase/auth": "1.10.8",
2004
+ "@firebase/auth-compat": "0.5.28",
2005
+ "@firebase/data-connect": "0.3.10",
2006
+ "@firebase/database": "1.0.20",
2007
+ "@firebase/database-compat": "2.0.11",
2008
+ "@firebase/firestore": "4.8.0",
2009
+ "@firebase/firestore-compat": "0.3.53",
2010
+ "@firebase/functions": "0.12.9",
2011
+ "@firebase/functions-compat": "0.3.26",
2012
+ "@firebase/installations": "0.6.18",
2013
+ "@firebase/installations-compat": "0.2.18",
2014
+ "@firebase/messaging": "0.12.22",
2015
+ "@firebase/messaging-compat": "0.2.22",
2016
+ "@firebase/performance": "0.7.7",
2017
+ "@firebase/performance-compat": "0.2.20",
2018
+ "@firebase/remote-config": "0.6.5",
2019
+ "@firebase/remote-config-compat": "0.2.18",
2020
+ "@firebase/storage": "0.13.14",
2021
+ "@firebase/storage-compat": "0.3.24",
2022
+ "@firebase/util": "1.12.1"
2023
+ }
2024
+ },
2025
+ "node_modules/firebase/node_modules/@firebase/auth": {
2026
+ "version": "1.10.8",
2027
+ "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.10.8.tgz",
2028
+ "integrity": "sha512-GpuTz5ap8zumr/ocnPY57ZanX02COsXloY6Y/2LYPAuXYiaJRf6BAGDEdRq1BMjP93kqQnKNuKZUTMZbQ8MNYA==",
2029
+ "dependencies": {
2030
+ "@firebase/component": "0.6.18",
2031
+ "@firebase/logger": "0.4.4",
2032
+ "@firebase/util": "1.12.1",
2033
+ "tslib": "^2.1.0"
2034
+ },
2035
+ "engines": {
2036
+ "node": ">=18.0.0"
2037
+ },
2038
+ "peerDependencies": {
2039
+ "@firebase/app": "0.x",
2040
+ "@react-native-async-storage/async-storage": "^1.18.1"
2041
+ },
2042
+ "peerDependenciesMeta": {
2043
+ "@react-native-async-storage/async-storage": {
2044
+ "optional": true
2045
+ }
2046
+ }
2047
+ },
2048
  "node_modules/fsevents": {
2049
  "version": "2.3.3",
2050
  "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
 
2068
  "node": ">=6.9.0"
2069
  }
2070
  },
2071
+ "node_modules/get-caller-file": {
2072
+ "version": "2.0.5",
2073
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
2074
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
2075
+ "engines": {
2076
+ "node": "6.* || 8.* || >= 10.*"
2077
+ }
2078
+ },
2079
+ "node_modules/http-parser-js": {
2080
+ "version": "0.5.10",
2081
+ "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz",
2082
+ "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA=="
2083
+ },
2084
+ "node_modules/idb": {
2085
+ "version": "7.1.1",
2086
+ "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
2087
+ "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ=="
2088
+ },
2089
+ "node_modules/is-fullwidth-code-point": {
2090
+ "version": "3.0.0",
2091
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
2092
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
2093
+ "engines": {
2094
+ "node": ">=8"
2095
+ }
2096
+ },
2097
  "node_modules/js-tokens": {
2098
  "version": "4.0.0",
2099
  "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
 
2123
  "node": ">=6"
2124
  }
2125
  },
2126
+ "node_modules/lodash.camelcase": {
2127
+ "version": "4.3.0",
2128
+ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
2129
+ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
2130
+ },
2131
+ "node_modules/long": {
2132
+ "version": "5.3.2",
2133
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
2134
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="
2135
+ },
2136
  "node_modules/loose-envify": {
2137
  "version": "1.4.0",
2138
  "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
 
2217
  "node": "^10 || ^12 || >=14"
2218
  }
2219
  },
2220
+ "node_modules/protobufjs": {
2221
+ "version": "7.5.4",
2222
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
2223
+ "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
2224
+ "hasInstallScript": true,
2225
+ "dependencies": {
2226
+ "@protobufjs/aspromise": "^1.1.2",
2227
+ "@protobufjs/base64": "^1.1.2",
2228
+ "@protobufjs/codegen": "^2.0.4",
2229
+ "@protobufjs/eventemitter": "^1.1.0",
2230
+ "@protobufjs/fetch": "^1.1.0",
2231
+ "@protobufjs/float": "^1.0.2",
2232
+ "@protobufjs/inquire": "^1.1.0",
2233
+ "@protobufjs/path": "^1.1.2",
2234
+ "@protobufjs/pool": "^1.1.0",
2235
+ "@protobufjs/utf8": "^1.1.0",
2236
+ "@types/node": ">=13.7.0",
2237
+ "long": "^5.0.0"
2238
+ },
2239
+ "engines": {
2240
+ "node": ">=12.0.0"
2241
+ }
2242
+ },
2243
  "node_modules/react": {
2244
  "version": "18.3.1",
2245
  "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
 
2272
  "node": ">=0.10.0"
2273
  }
2274
  },
2275
+ "node_modules/require-directory": {
2276
+ "version": "2.1.1",
2277
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
2278
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
2279
+ "engines": {
2280
+ "node": ">=0.10.0"
2281
+ }
2282
+ },
2283
  "node_modules/rollup": {
2284
  "version": "4.59.0",
2285
  "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
 
2324
  "fsevents": "~2.3.2"
2325
  }
2326
  },
2327
+ "node_modules/safe-buffer": {
2328
+ "version": "5.2.1",
2329
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
2330
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
2331
+ "funding": [
2332
+ {
2333
+ "type": "github",
2334
+ "url": "https://github.com/sponsors/feross"
2335
+ },
2336
+ {
2337
+ "type": "patreon",
2338
+ "url": "https://www.patreon.com/feross"
2339
+ },
2340
+ {
2341
+ "type": "consulting",
2342
+ "url": "https://feross.org/support"
2343
+ }
2344
+ ]
2345
+ },
2346
  "node_modules/scheduler": {
2347
  "version": "0.23.2",
2348
  "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
 
2369
  "node": ">=0.10.0"
2370
  }
2371
  },
2372
+ "node_modules/string-width": {
2373
+ "version": "4.2.3",
2374
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
2375
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
2376
+ "dependencies": {
2377
+ "emoji-regex": "^8.0.0",
2378
+ "is-fullwidth-code-point": "^3.0.0",
2379
+ "strip-ansi": "^6.0.1"
2380
+ },
2381
+ "engines": {
2382
+ "node": ">=8"
2383
+ }
2384
+ },
2385
+ "node_modules/strip-ansi": {
2386
+ "version": "6.0.1",
2387
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
2388
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
2389
+ "dependencies": {
2390
+ "ansi-regex": "^5.0.1"
2391
+ },
2392
+ "engines": {
2393
+ "node": ">=8"
2394
+ }
2395
+ },
2396
+ "node_modules/tslib": {
2397
+ "version": "2.8.1",
2398
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
2399
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
2400
+ },
2401
  "node_modules/typescript": {
2402
  "version": "5.9.3",
2403
  "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
 
2411
  "node": ">=14.17"
2412
  }
2413
  },
2414
+ "node_modules/undici-types": {
2415
+ "version": "7.18.2",
2416
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
2417
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="
2418
+ },
2419
  "node_modules/update-browserslist-db": {
2420
  "version": "1.2.3",
2421
  "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
 
2505
  }
2506
  }
2507
  },
2508
+ "node_modules/web-vitals": {
2509
+ "version": "4.2.4",
2510
+ "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
2511
+ "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw=="
2512
+ },
2513
+ "node_modules/websocket-driver": {
2514
+ "version": "0.7.4",
2515
+ "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
2516
+ "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==",
2517
+ "dependencies": {
2518
+ "http-parser-js": ">=0.5.1",
2519
+ "safe-buffer": ">=5.1.0",
2520
+ "websocket-extensions": ">=0.1.1"
2521
+ },
2522
+ "engines": {
2523
+ "node": ">=0.8.0"
2524
+ }
2525
+ },
2526
+ "node_modules/websocket-extensions": {
2527
+ "version": "0.1.4",
2528
+ "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
2529
+ "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
2530
+ "engines": {
2531
+ "node": ">=0.8.0"
2532
+ }
2533
+ },
2534
+ "node_modules/wrap-ansi": {
2535
+ "version": "7.0.0",
2536
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
2537
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
2538
+ "dependencies": {
2539
+ "ansi-styles": "^4.0.0",
2540
+ "string-width": "^4.1.0",
2541
+ "strip-ansi": "^6.0.0"
2542
+ },
2543
+ "engines": {
2544
+ "node": ">=10"
2545
+ },
2546
+ "funding": {
2547
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
2548
+ }
2549
+ },
2550
+ "node_modules/y18n": {
2551
+ "version": "5.0.8",
2552
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
2553
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
2554
+ "engines": {
2555
+ "node": ">=10"
2556
+ }
2557
+ },
2558
  "node_modules/yallist": {
2559
  "version": "3.1.1",
2560
  "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
2561
  "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
2562
  "dev": true
2563
+ },
2564
+ "node_modules/yargs": {
2565
+ "version": "17.7.2",
2566
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
2567
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
2568
+ "dependencies": {
2569
+ "cliui": "^8.0.1",
2570
+ "escalade": "^3.1.1",
2571
+ "get-caller-file": "^2.0.5",
2572
+ "require-directory": "^2.1.1",
2573
+ "string-width": "^4.2.3",
2574
+ "y18n": "^5.0.5",
2575
+ "yargs-parser": "^21.1.1"
2576
+ },
2577
+ "engines": {
2578
+ "node": ">=12"
2579
+ }
2580
+ },
2581
+ "node_modules/yargs-parser": {
2582
+ "version": "21.1.1",
2583
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
2584
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
2585
+ "engines": {
2586
+ "node": ">=12"
2587
+ }
2588
  }
2589
  }
2590
  }
package.json CHANGED
@@ -9,6 +9,7 @@
9
  "preview": "vite preview"
10
  },
11
  "dependencies": {
 
12
  "react": "^18.3.1",
13
  "react-dom": "^18.3.1"
14
  },
 
9
  "preview": "vite preview"
10
  },
11
  "dependencies": {
12
+ "firebase": "^11.10.0",
13
  "react": "^18.3.1",
14
  "react-dom": "^18.3.1"
15
  },
src/App.tsx CHANGED
@@ -6,6 +6,7 @@ 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: [
@@ -237,21 +238,37 @@ function App() {
237
  setIsWorking(true);
238
 
239
  try {
240
- const result = await runMockAgent({
241
- tab: activeTab,
242
- input: trimmed,
243
- locale,
244
- mode,
245
- onWorkflowInit: (steps) => {
246
- setProgressSteps(steps);
247
- },
248
- onStepStatus: (stepId, status, detail) => {
249
- updateStepStatus(stepId, status, detail);
250
- },
251
- onTrace: (item) => {
252
- pushTrace(item);
253
- },
254
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
 
256
  setMessages((prev) => [
257
  ...prev,
@@ -398,6 +415,7 @@ function App() {
398
 
399
  {activeTab === 'voice' ? (
400
  <VoiceDemoPanel
 
401
  locale={locale}
402
  onWorkingChange={setIsWorking}
403
  onWorkflowInit={setProgressSteps}
@@ -409,6 +427,7 @@ function App() {
409
  />
410
  ) : activeTab === 'marketing' ? (
411
  <MarketingStudioPanel
 
412
  locale={locale}
413
  onWorkingChange={setIsWorking}
414
  onWorkflowInit={setProgressSteps}
@@ -420,6 +439,7 @@ function App() {
420
  />
421
  ) : activeTab === 'invoice' ? (
422
  <InvoiceDemoPanel
 
423
  locale={locale}
424
  onWorkingChange={setIsWorking}
425
  onWorkflowInit={setProgressSteps}
@@ -431,6 +451,7 @@ function App() {
431
  />
432
  ) : activeTab === 'marketplace' ? (
433
  <MarketplaceDemoPanel
 
434
  locale={locale}
435
  onWorkingChange={setIsWorking}
436
  onWorkflowInit={setProgressSteps}
 
6
  import InvoiceDemoPanel from './InvoiceDemoPanel';
7
  import MarketplaceDemoPanel from './MarketplaceDemoPanel';
8
  import ChatFeatureDemo from './ChatFeatureDemo';
9
+ import { runLiveAgent } from './liveAgent';
10
 
11
  const TAB_DEFINITIONS: Record<DemoLocale, Array<{ id: DemoTabId; label: string; subtitle: string }>> = {
12
  en: [
 
238
  setIsWorking(true);
239
 
240
  try {
241
+ const result =
242
+ mode === 'live'
243
+ ? await runLiveAgent({
244
+ tab: activeTab,
245
+ input: trimmed,
246
+ locale,
247
+ onWorkflowInit: (steps) => {
248
+ setProgressSteps(steps);
249
+ },
250
+ onStepStatus: (stepId, status, detail) => {
251
+ updateStepStatus(stepId, status, detail);
252
+ },
253
+ onTrace: (item) => {
254
+ pushTrace(item);
255
+ },
256
+ })
257
+ : await runMockAgent({
258
+ tab: activeTab,
259
+ input: trimmed,
260
+ locale,
261
+ mode,
262
+ onWorkflowInit: (steps) => {
263
+ setProgressSteps(steps);
264
+ },
265
+ onStepStatus: (stepId, status, detail) => {
266
+ updateStepStatus(stepId, status, detail);
267
+ },
268
+ onTrace: (item) => {
269
+ pushTrace(item);
270
+ },
271
+ });
272
 
273
  setMessages((prev) => [
274
  ...prev,
 
415
 
416
  {activeTab === 'voice' ? (
417
  <VoiceDemoPanel
418
+ mode={mode}
419
  locale={locale}
420
  onWorkingChange={setIsWorking}
421
  onWorkflowInit={setProgressSteps}
 
427
  />
428
  ) : activeTab === 'marketing' ? (
429
  <MarketingStudioPanel
430
+ mode={mode}
431
  locale={locale}
432
  onWorkingChange={setIsWorking}
433
  onWorkflowInit={setProgressSteps}
 
439
  />
440
  ) : activeTab === 'invoice' ? (
441
  <InvoiceDemoPanel
442
+ mode={mode}
443
  locale={locale}
444
  onWorkingChange={setIsWorking}
445
  onWorkflowInit={setProgressSteps}
 
451
  />
452
  ) : activeTab === 'marketplace' ? (
453
  <MarketplaceDemoPanel
454
+ mode={mode}
455
  locale={locale}
456
  onWorkingChange={setIsWorking}
457
  onWorkflowInit={setProgressSteps}
src/InvoiceDemoPanel.tsx CHANGED
@@ -1,6 +1,8 @@
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';
@@ -67,6 +69,7 @@ type InvoiceStepTemplate = {
67
  };
68
 
69
  type InvoiceDemoPanelProps = {
 
70
  locale: DemoLocale;
71
  onWorkingChange: (working: boolean) => void;
72
  onWorkflowInit: (steps: ProgressStep[]) => void;
@@ -101,6 +104,7 @@ const COPY = {
101
  outputTitle: 'Extraction Output',
102
  outputSubtitle: 'JSON + human-friendly invoice preview',
103
  emptyOutput: 'Upload or run a synthetic scan to view extracted invoice fields.',
 
104
  stoppedByUser: 'Invoice scan was stopped by user.',
105
  recordCreated: 'Draft invoice record created in demo history.',
106
  reviewReady: 'Extraction completed. Review fields before creating a record.',
@@ -166,6 +170,7 @@ const COPY = {
166
  outputTitle: '擷取結果',
167
  outputSubtitle: 'JSON 與可閱讀發票預覽',
168
  emptyOutput: '可先上傳圖片,或直接執行模擬掃描查看欄位結果。',
 
169
  stoppedByUser: '掃描流程已由使用者停止。',
170
  recordCreated: '已在示範歷史中建立草稿發票。',
171
  reviewReady: '欄位擷取完成,請先檢查後再建立紀錄。',
@@ -487,7 +492,47 @@ function buildMockExtraction(locale: DemoLocale, role: WorkspaceRole, note: stri
487
  };
488
  }
489
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
490
  export default function InvoiceDemoPanel({
 
491
  locale,
492
  onWorkingChange,
493
  onWorkflowInit,
@@ -578,8 +623,8 @@ export default function InvoiceDemoPanel({
578
  const runExtraction = async (forcedNote?: string) => {
579
  const activeNote = (forcedNote ?? scanNote).trim();
580
  const source = uploadedFile?.name || (locale === 'zh-TW' ? '模擬發票樣本' : 'Synthetic invoice sample');
581
- const steps = buildSteps(locale, createRecord);
582
- const payload = buildMockExtraction(locale, role, activeNote);
583
  const runToken = runTokenRef.current + 1;
584
  runTokenRef.current = runToken;
585
  const startedAtMs = Date.now();
@@ -616,6 +661,92 @@ export default function InvoiceDemoPanel({
616
  );
617
 
618
  try {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
619
  setJobState(jobId, { status: 'scanning', progress: 16 });
620
  const sourceReady = await runStep(runToken, steps[0], {
621
  source: uploadedFile ? uploadedFile.mimeType : 'synthetic',
@@ -666,12 +797,12 @@ export default function InvoiceDemoPanel({
666
  if (!validated) return;
667
 
668
  const packaged = await runStep(runToken, steps[4], {
669
- createRecord,
670
  invoiceNumber: payload.invoice.invoiceNumber,
671
  });
672
  if (!packaged) return;
673
 
674
- if (createRecord) {
675
  const record: InvoiceRecord = {
676
  id: makeId('record'),
677
  invoiceNumber: payload.invoice.invoiceNumber,
@@ -763,6 +894,12 @@ export default function InvoiceDemoPanel({
763
  };
764
  }, []);
765
 
 
 
 
 
 
 
766
  const balance = extraction ? extraction.invoice.total - extraction.invoice.paid : 0;
767
 
768
  return (
@@ -834,7 +971,7 @@ export default function InvoiceDemoPanel({
834
  type="checkbox"
835
  checked={createRecord}
836
  onChange={(event) => setCreateRecord(event.target.checked)}
837
- disabled={isScanning}
838
  />
839
  <span>{copy.createRecord}</span>
840
  </label>
 
1
  import { ChangeEvent, useEffect, useRef, useState } from 'react';
2
+ import type { DemoLocale, DemoMode, ProgressStep, ProgressStepStatus, TraceItem } from './types';
3
  import InvoiceFeatureDemo from './InvoiceFeatureDemo';
4
+ import { parseDataUrl, scanInvoiceImage } from './liveApi';
5
+ import type { LiveInvoiceExtraction } from './liveApi';
6
 
7
  type InvoiceDirection = 'sale' | 'purchase';
8
  type InvoiceStatus = 'draft' | 'open' | 'paid' | 'overdue';
 
69
  };
70
 
71
  type InvoiceDemoPanelProps = {
72
+ mode: DemoMode;
73
  locale: DemoLocale;
74
  onWorkingChange: (working: boolean) => void;
75
  onWorkflowInit: (steps: ProgressStep[]) => void;
 
104
  outputTitle: 'Extraction Output',
105
  outputSubtitle: 'JSON + human-friendly invoice preview',
106
  emptyOutput: 'Upload or run a synthetic scan to view extracted invoice fields.',
107
+ liveUploadRequired: 'Live mode requires an uploaded invoice image.',
108
  stoppedByUser: 'Invoice scan was stopped by user.',
109
  recordCreated: 'Draft invoice record created in demo history.',
110
  reviewReady: 'Extraction completed. Review fields before creating a record.',
 
170
  outputTitle: '擷取結果',
171
  outputSubtitle: 'JSON 與可閱讀發票預覽',
172
  emptyOutput: '可先上傳圖片,或直接執行模擬掃描查看欄位結果。',
173
+ liveUploadRequired: '即時模式需要先上傳發票圖片。',
174
  stoppedByUser: '掃描流程已由使用者停止。',
175
  recordCreated: '已在示範歷史中建立草稿發票。',
176
  reviewReady: '欄位擷取完成,請先檢查後再建立紀錄。',
 
492
  };
493
  }
494
 
495
+ function toNumber(value: unknown, fallback = 0): number {
496
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
497
+ }
498
+
499
+ function normalizeLiveExtraction(payload: LiveInvoiceExtraction | undefined, locale: DemoLocale): InvoiceExtraction {
500
+ const invoice = payload?.invoice || {};
501
+ const items = Array.isArray(invoice.items) ? invoice.items : [];
502
+
503
+ return {
504
+ provider: payload?.provider === 'gemini' || payload?.provider === 'mock' ? payload.provider : 'gemini',
505
+ model: payload?.model || (locale === 'zh-TW' ? 'gemini(live)' : 'gemini (live)'),
506
+ confidence: toNumber(payload?.confidence, 0.5),
507
+ warnings: Array.isArray(payload?.warnings) ? payload.warnings.map((item) => String(item)) : [],
508
+ rawText: typeof payload?.rawText === 'string' ? payload.rawText : '',
509
+ invoice: {
510
+ direction: invoice.direction === 'purchase' ? 'purchase' : 'sale',
511
+ status:
512
+ invoice.status === 'open' || invoice.status === 'paid' || invoice.status === 'overdue'
513
+ ? invoice.status
514
+ : 'draft',
515
+ invoiceNumber: typeof invoice.invoiceNumber === 'string' ? invoice.invoiceNumber : '',
516
+ counterpartyName: typeof invoice.counterpartyName === 'string' ? invoice.counterpartyName : '',
517
+ description: typeof invoice.description === 'string' ? invoice.description : '',
518
+ currency: typeof invoice.currency === 'string' ? invoice.currency : 'TWD',
519
+ total: toNumber(invoice.total),
520
+ paid: toNumber(invoice.paid),
521
+ issueDate: typeof invoice.issueDate === 'string' ? invoice.issueDate : '',
522
+ dueDate: typeof invoice.dueDate === 'string' ? invoice.dueDate : '',
523
+ deliveryDate: typeof invoice.deliveryDate === 'string' ? invoice.deliveryDate : '',
524
+ items: items.map((item) => ({
525
+ description: typeof item.description === 'string' ? item.description : '',
526
+ quantity: toNumber(item.quantity),
527
+ unitPrice: toNumber(item.unitPrice),
528
+ lineTotal: toNumber(item.lineTotal),
529
+ })),
530
+ },
531
+ };
532
+ }
533
+
534
  export default function InvoiceDemoPanel({
535
+ mode,
536
  locale,
537
  onWorkingChange,
538
  onWorkflowInit,
 
623
  const runExtraction = async (forcedNote?: string) => {
624
  const activeNote = (forcedNote ?? scanNote).trim();
625
  const source = uploadedFile?.name || (locale === 'zh-TW' ? '模擬發票樣本' : 'Synthetic invoice sample');
626
+ const liveCreateRecord = mode === 'live' ? false : createRecord;
627
+ const steps = buildSteps(locale, liveCreateRecord);
628
  const runToken = runTokenRef.current + 1;
629
  runTokenRef.current = runToken;
630
  const startedAtMs = Date.now();
 
661
  );
662
 
663
  try {
664
+ if (mode === 'live') {
665
+ if (!uploadedFile) {
666
+ throw new Error(copy.liveUploadRequired);
667
+ }
668
+
669
+ setJobState(jobId, { status: 'scanning', progress: 16 });
670
+ const sourceReady = await runStep(runToken, steps[0], {
671
+ source: uploadedFile.mimeType,
672
+ });
673
+ if (!sourceReady) return;
674
+
675
+ const ocrStep = steps[1];
676
+ currentStepRef.current = ocrStep.id;
677
+ onStepStatus(ocrStep.id, 'in_progress', ocrStep.runningDetail);
678
+ onTrace(makeTrace(ocrStep.kind, ocrStep.label, ocrStep.runningDetail, 'running'));
679
+
680
+ const imagePayload = parseDataUrl(uploadedFile.previewUrl);
681
+ const response = await scanInvoiceImage({
682
+ dataBase64: imagePayload.base64,
683
+ mimeType: imagePayload.mimeType,
684
+ locale,
685
+ createInvoice: liveCreateRecord,
686
+ });
687
+ const payload = normalizeLiveExtraction(response.extraction, locale);
688
+ if (!mountedRef.current || runTokenRef.current !== runToken) return;
689
+
690
+ const chunks = splitTextChunks(payload.rawText || '');
691
+ for (let i = 0; i < chunks.length; i += 1) {
692
+ if (!mountedRef.current || runTokenRef.current !== runToken) return;
693
+ setRawTextStream((prev) => prev + chunks[i]);
694
+ if (i % 5 === 0) {
695
+ onTrace(
696
+ makeTrace(
697
+ 'tool',
698
+ locale === 'zh-TW' ? 'OCR 片段' : 'OCR chunk',
699
+ locale === 'zh-TW' ? `已輸出第 ${i + 1} 段` : `Chunk ${i + 1} streamed`,
700
+ 'ok'
701
+ )
702
+ );
703
+ }
704
+ await sleep(55);
705
+ }
706
+
707
+ onStepStatus(ocrStep.id, 'completed', ocrStep.doneDetail);
708
+ onTrace(makeTrace(ocrStep.kind, ocrStep.label, ocrStep.doneDetail, 'ok', { chars: payload.rawText.length }));
709
+
710
+ setJobState(jobId, { status: 'extracting', progress: 62 });
711
+ const extracted = await runStep(runToken, steps[2], {
712
+ fields: 12,
713
+ items: payload.invoice.items.length,
714
+ });
715
+ if (!extracted) return;
716
+ setExtraction(payload);
717
+
718
+ setJobState(jobId, { status: 'validating', progress: 82 });
719
+ const validated = await runStep(runToken, steps[3], {
720
+ total: payload.invoice.total,
721
+ paid: payload.invoice.paid,
722
+ warnings: payload.warnings.length,
723
+ });
724
+ if (!validated) return;
725
+
726
+ const packaged = await runStep(runToken, steps[4], {
727
+ createRecord: liveCreateRecord,
728
+ invoiceNumber: payload.invoice.invoiceNumber,
729
+ mode: 'live',
730
+ });
731
+ if (!packaged) return;
732
+
733
+ setNote(copy.reviewReady);
734
+ setJobState(jobId, {
735
+ status: 'ready',
736
+ progress: 100,
737
+ invoiceNumber: payload.invoice.invoiceNumber,
738
+ });
739
+ setMeta({
740
+ startedAt: new Date(startedAtMs).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }),
741
+ durationMs: Date.now() - startedAtMs,
742
+ model: payload.model,
743
+ provider: payload.provider,
744
+ source,
745
+ });
746
+ return;
747
+ }
748
+
749
+ const payload = buildMockExtraction(locale, role, activeNote);
750
  setJobState(jobId, { status: 'scanning', progress: 16 });
751
  const sourceReady = await runStep(runToken, steps[0], {
752
  source: uploadedFile ? uploadedFile.mimeType : 'synthetic',
 
797
  if (!validated) return;
798
 
799
  const packaged = await runStep(runToken, steps[4], {
800
+ createRecord: liveCreateRecord,
801
  invoiceNumber: payload.invoice.invoiceNumber,
802
  });
803
  if (!packaged) return;
804
 
805
+ if (liveCreateRecord) {
806
  const record: InvoiceRecord = {
807
  id: makeId('record'),
808
  invoiceNumber: payload.invoice.invoiceNumber,
 
894
  };
895
  }, []);
896
 
897
+ useEffect(() => {
898
+ if (mode === 'live') {
899
+ setCreateRecord(false);
900
+ }
901
+ }, [mode]);
902
+
903
  const balance = extraction ? extraction.invoice.total - extraction.invoice.paid : 0;
904
 
905
  return (
 
971
  type="checkbox"
972
  checked={createRecord}
973
  onChange={(event) => setCreateRecord(event.target.checked)}
974
+ disabled={isScanning || mode === 'live'}
975
  />
976
  <span>{copy.createRecord}</span>
977
  </label>
src/MarketingStudioPanel.tsx CHANGED
@@ -1,6 +1,7 @@
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;
@@ -37,6 +38,7 @@ type GeneratedImage = {
37
  };
38
 
39
  type MarketingStudioPanelProps = {
 
40
  locale: DemoLocale;
41
  onWorkingChange: (working: boolean) => void;
42
  onWorkflowInit: (steps: ProgressStep[]) => void;
@@ -337,6 +339,59 @@ function deckToMarkdown(locale: DemoLocale, deck: MarketingDeck): string {
337
  ].join('\n');
338
  }
339
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  function makePalette(seed: number): [string, string, string] {
341
  const palettes: Array<[string, string, string]> = [
342
  ['#1f8f4e', '#7dcf9b', '#edf8e6'],
@@ -409,6 +464,7 @@ function generateMockImageDataUrl(product: MarketingProduct, deck: MarketingDeck
409
  }
410
 
411
  export default function MarketingStudioPanel({
 
412
  locale,
413
  onWorkingChange,
414
  onWorkflowInit,
@@ -437,6 +493,7 @@ export default function MarketingStudioPanel({
437
  const [note, setNote] = useState<string | null>(null);
438
 
439
  const fileInputRef = useRef<HTMLInputElement | null>(null);
 
440
  const runTokenRef = useRef(0);
441
  const mountedRef = useRef(true);
442
  const currentStepRef = useRef<string | null>(null);
@@ -471,6 +528,8 @@ export default function MarketingStudioPanel({
471
  const stopGeneration = () => {
472
  if (!isGenerating) return;
473
  runTokenRef.current += 1;
 
 
474
  if (currentStepRef.current) {
475
  onStepStatus(currentStepRef.current, 'error', copy.stoppedByUser);
476
  }
@@ -531,6 +590,122 @@ export default function MarketingStudioPanel({
531
  });
532
  if (!imageReady) return;
533
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
  const deckPayload = createDeck(locale, selectedProduct, activePrompt, tone, channel);
535
  const markdown = deckToMarkdown(locale, deckPayload);
536
  const chunks = splitTextChunks(markdown);
@@ -602,7 +777,12 @@ export default function MarketingStudioPanel({
602
  durationMs: Date.now() - startedAtMs,
603
  });
604
  } catch (runtimeError) {
605
- const message = runtimeError instanceof Error ? runtimeError.message : copy.streamError;
 
 
 
 
 
606
  setError(message);
607
  if (currentStepRef.current) {
608
  onStepStatus(currentStepRef.current, 'error', message);
@@ -610,6 +790,7 @@ export default function MarketingStudioPanel({
610
  onTrace(makeTrace('tool', locale === 'zh-TW' ? '生成錯誤' : 'Generation error', message, 'error'));
611
  } finally {
612
  if (runTokenRef.current === runToken) {
 
613
  setIsGenerating(false);
614
  onWorkingChange(false);
615
  }
@@ -674,6 +855,8 @@ export default function MarketingStudioPanel({
674
  return () => {
675
  mountedRef.current = false;
676
  runTokenRef.current += 1;
 
 
677
  onWorkingChange(false);
678
  };
679
  }, []);
 
1
  import { ChangeEvent, useEffect, useMemo, useRef, useState } from 'react';
2
+ import type { DemoLocale, DemoMode, ProgressStep, ProgressStepStatus, TraceItem } from './types';
3
  import MarketingFeatureDemo from './MarketingFeatureDemo';
4
+ import { parseDataUrl, streamMarketingGeneration } from './liveApi';
5
 
6
  type MarketingStepTemplate = {
7
  id: string;
 
38
  };
39
 
40
  type MarketingStudioPanelProps = {
41
+ mode: DemoMode;
42
  locale: DemoLocale;
43
  onWorkingChange: (working: boolean) => void;
44
  onWorkflowInit: (steps: ProgressStep[]) => void;
 
339
  ].join('\n');
340
  }
341
 
342
+ function parseDeckFromLiveText(locale: DemoLocale, text: string): MarketingDeck {
343
+ const fallback = locale === 'zh-TW'
344
+ ? {
345
+ headline: '已生成行銷內容',
346
+ caption: text.trim().slice(0, 220) || '已從後端串流取得文案結果。',
347
+ cta: '立即查看並套用',
348
+ hashtags: ['#Farm2Market'],
349
+ designNotes: ['已使用後端即時生成結果。'],
350
+ }
351
+ : {
352
+ headline: 'Marketing content generated',
353
+ caption: text.trim().slice(0, 220) || 'Live backend copy has been streamed.',
354
+ cta: 'Review and apply now',
355
+ hashtags: ['#Farm2Market'],
356
+ designNotes: ['Generated from live backend stream.'],
357
+ };
358
+
359
+ const normalized = text
360
+ .split('\n')
361
+ .map((line) => line.trim())
362
+ .filter(Boolean);
363
+
364
+ if (!normalized.length) return fallback;
365
+
366
+ const hashTags = Array.from(
367
+ new Set(
368
+ (text.match(/#[\p{L}\p{N}_-]+/gu) || [])
369
+ .map((tag) => tag.trim())
370
+ .filter(Boolean)
371
+ )
372
+ );
373
+
374
+ const headline = normalized[0]?.replace(/^#+\s*/, '') || fallback.headline;
375
+ const caption = normalized.slice(1, 4).join(' ') || fallback.caption;
376
+
377
+ const ctaLine = normalized.find((line) =>
378
+ locale === 'zh-TW' ? /(立即|現在|下單|預購|CTA|行動)/.test(line) : /(cta|order|buy now|reserve|shop)/i.test(line)
379
+ );
380
+
381
+ const noteLines = normalized
382
+ .filter((line) => line.startsWith('-') || line.startsWith('•'))
383
+ .map((line) => line.replace(/^[-•]\s*/, ''))
384
+ .slice(0, 3);
385
+
386
+ return {
387
+ headline,
388
+ caption,
389
+ cta: ctaLine || fallback.cta,
390
+ hashtags: hashTags.length ? hashTags.slice(0, 6) : fallback.hashtags,
391
+ designNotes: noteLines.length ? noteLines : fallback.designNotes,
392
+ };
393
+ }
394
+
395
  function makePalette(seed: number): [string, string, string] {
396
  const palettes: Array<[string, string, string]> = [
397
  ['#1f8f4e', '#7dcf9b', '#edf8e6'],
 
464
  }
465
 
466
  export default function MarketingStudioPanel({
467
+ mode,
468
  locale,
469
  onWorkingChange,
470
  onWorkflowInit,
 
493
  const [note, setNote] = useState<string | null>(null);
494
 
495
  const fileInputRef = useRef<HTMLInputElement | null>(null);
496
+ const abortRef = useRef<AbortController | null>(null);
497
  const runTokenRef = useRef(0);
498
  const mountedRef = useRef(true);
499
  const currentStepRef = useRef<string | null>(null);
 
528
  const stopGeneration = () => {
529
  if (!isGenerating) return;
530
  runTokenRef.current += 1;
531
+ abortRef.current?.abort();
532
+ abortRef.current = null;
533
  if (currentStepRef.current) {
534
  onStepStatus(currentStepRef.current, 'error', copy.stoppedByUser);
535
  }
 
590
  });
591
  if (!imageReady) return;
592
 
593
+ if (mode === 'live') {
594
+ const textStep = steps[2];
595
+ const imageStep = steps[3];
596
+ const controller = new AbortController();
597
+ abortRef.current = controller;
598
+ currentStepRef.current = textStep.id;
599
+ onStepStatus(textStep.id, 'in_progress', textStep.runningDetail);
600
+ onTrace(makeTrace(textStep.kind, textStep.label, textStep.runningDetail, 'running'));
601
+
602
+ let imageStepStarted = false;
603
+ let streamedImages = 0;
604
+ let streamedText = '';
605
+
606
+ await streamMarketingGeneration({
607
+ prompt: activePrompt,
608
+ locale,
609
+ product: {
610
+ name: selectedProduct.name,
611
+ category: selectedProduct.category,
612
+ description: selectedProduct.name,
613
+ },
614
+ image: uploadedImage ? parseDataUrl(uploadedImage.previewUrl) : undefined,
615
+ signal: controller.signal,
616
+ onEvent: (event) => {
617
+ if (!mountedRef.current || runTokenRef.current !== runToken) return;
618
+
619
+ if (event.type === 'text') {
620
+ streamedText += event.text;
621
+ setStreamText((prev) => prev + event.text);
622
+ return;
623
+ }
624
+
625
+ if (event.type === 'image') {
626
+ streamedImages += 1;
627
+ if (!imageStepStarted) {
628
+ imageStepStarted = true;
629
+ currentStepRef.current = imageStep.id;
630
+ onStepStatus(imageStep.id, 'in_progress', imageStep.runningDetail);
631
+ onTrace(makeTrace(imageStep.kind, imageStep.label, imageStep.runningDetail, 'running'));
632
+ }
633
+ const dataUrl = `data:${event.mimeType || 'image/png'};base64,${event.base64}`;
634
+ setImages((prev) => [
635
+ ...prev,
636
+ {
637
+ id: makeId('img'),
638
+ dataUrl,
639
+ base64: event.base64,
640
+ mimeType: event.mimeType || 'image/png',
641
+ label:
642
+ locale === 'zh-TW'
643
+ ? streamedImages === 1
644
+ ? '主視覺'
645
+ : `社群版型 ${streamedImages - 1}`
646
+ : streamedImages === 1
647
+ ? 'Hero visual'
648
+ : `Social variation ${streamedImages - 1}`,
649
+ },
650
+ ]);
651
+ onTrace(
652
+ makeTrace(
653
+ 'tool',
654
+ locale === 'zh-TW' ? '圖片片段' : 'Image chunk',
655
+ locale === 'zh-TW' ? `已接收第 ${streamedImages} 張圖片` : `Received image ${streamedImages}`,
656
+ 'ok'
657
+ )
658
+ );
659
+ return;
660
+ }
661
+
662
+ if (event.type === 'error') {
663
+ throw new Error(event.message || copy.streamError);
664
+ }
665
+ },
666
+ });
667
+
668
+ if (!mountedRef.current || runTokenRef.current !== runToken) return;
669
+ abortRef.current = null;
670
+
671
+ onStepStatus(textStep.id, 'completed', textStep.doneDetail);
672
+ onTrace(makeTrace(textStep.kind, textStep.label, textStep.doneDetail, 'ok', { chars: streamedText.length }));
673
+
674
+ if (imageStepStarted) {
675
+ onStepStatus(imageStep.id, 'completed', imageStep.doneDetail);
676
+ onTrace(makeTrace(imageStep.kind, imageStep.label, imageStep.doneDetail, 'ok', { images: streamedImages }));
677
+ } else {
678
+ onStepStatus(imageStep.id, 'completed', locale === 'zh-TW' ? '本次未回傳圖片。' : 'No image chunks in this run.');
679
+ onTrace(
680
+ makeTrace(
681
+ imageStep.kind,
682
+ imageStep.label,
683
+ locale === 'zh-TW' ? '本次只回傳文字。' : 'Text-only response returned.',
684
+ 'ok'
685
+ )
686
+ );
687
+ }
688
+
689
+ const finalDeck = parseDeckFromLiveText(locale, streamedText);
690
+ setDeck(finalDeck);
691
+
692
+ const packaged = await runStep(runToken, steps[4], {
693
+ output: {
694
+ text: true,
695
+ images: streamedImages,
696
+ mode: 'live',
697
+ },
698
+ });
699
+ if (!packaged) return;
700
+
701
+ setMeta({
702
+ model: 'gemini-2.5-flash-image (live)',
703
+ startedAt: new Date(startedAtMs).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }),
704
+ durationMs: Date.now() - startedAtMs,
705
+ });
706
+ return;
707
+ }
708
+
709
  const deckPayload = createDeck(locale, selectedProduct, activePrompt, tone, channel);
710
  const markdown = deckToMarkdown(locale, deckPayload);
711
  const chunks = splitTextChunks(markdown);
 
777
  durationMs: Date.now() - startedAtMs,
778
  });
779
  } catch (runtimeError) {
780
+ const message =
781
+ runtimeError instanceof Error
782
+ ? runtimeError.name === 'AbortError'
783
+ ? copy.stoppedByUser
784
+ : runtimeError.message
785
+ : copy.streamError;
786
  setError(message);
787
  if (currentStepRef.current) {
788
  onStepStatus(currentStepRef.current, 'error', message);
 
790
  onTrace(makeTrace('tool', locale === 'zh-TW' ? '生成錯誤' : 'Generation error', message, 'error'));
791
  } finally {
792
  if (runTokenRef.current === runToken) {
793
+ abortRef.current = null;
794
  setIsGenerating(false);
795
  onWorkingChange(false);
796
  }
 
855
  return () => {
856
  mountedRef.current = false;
857
  runTokenRef.current += 1;
858
+ abortRef.current?.abort();
859
+ abortRef.current = null;
860
  onWorkingChange(false);
861
  };
862
  }, []);
src/MarketplaceDemoPanel.tsx CHANGED
@@ -1,6 +1,8 @@
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
 
@@ -50,6 +52,7 @@ type MarketplaceStepTemplate = {
50
  };
51
 
52
  type MarketplaceDemoPanelProps = {
 
53
  locale: DemoLocale;
54
  onWorkingChange: (working: boolean) => void;
55
  onWorkflowInit: (steps: ProgressStep[]) => void;
@@ -322,6 +325,71 @@ function computeMarketplaceStats(products: ProductRecord[], stores: StoreRecord[
322
  };
323
  }
324
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  function scoreProduct(
326
  record: ProductRecord,
327
  queryTokens: string[],
@@ -575,6 +643,7 @@ const COPY = {
575
  } as const;
576
 
577
  export default function MarketplaceDemoPanel({
 
578
  locale,
579
  onWorkingChange,
580
  onWorkflowInit,
@@ -705,6 +774,132 @@ export default function MarketplaceDemoPanel({
705
  });
706
  if (!parsed) return;
707
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
708
  const productStep = steps.find((step) => step.id === 'market-step-2');
709
  if (productStep) {
710
  toolCalls.push('product_search_public');
 
1
  import { useEffect, useMemo, useRef, useState } from 'react';
2
+ import type { DemoLocale, DemoMode, ProgressStep, ProgressStepStatus, TraceItem } from './types';
3
  import MarketplaceFeatureDemo from './MarketplaceFeatureDemo';
4
+ import { callVoiceTool } from './liveApi';
5
+ import type { LiveVoiceToolUi } from './liveApi';
6
 
7
  type MarketplaceMode = 'hybrid' | 'products' | 'stores' | 'stats';
8
 
 
52
  };
53
 
54
  type MarketplaceDemoPanelProps = {
55
+ mode: DemoMode;
56
  locale: DemoLocale;
57
  onWorkingChange: (working: boolean) => void;
58
  onWorkflowInit: (steps: ProgressStep[]) => void;
 
325
  };
326
  }
327
 
328
+ function toRankedProductsFromUi(ui: LiveVoiceToolUi | undefined, limit: number): RankedProduct[] {
329
+ const rows = Array.isArray(ui?.items) ? ui.items : [];
330
+ return rows.slice(0, limit).map((item, index) => {
331
+ const row = typeof item === 'object' && item ? (item as Record<string, unknown>) : {};
332
+ return {
333
+ id: typeof row.id === 'string' ? row.id : `live-product-${index}`,
334
+ name: typeof row.name === 'string' ? row.name : `Product ${index + 1}`,
335
+ category: typeof row.category === 'string' ? row.category : 'General',
336
+ price: typeof row.price === 'number' ? row.price : 0,
337
+ unit: typeof row.unit === 'string' ? row.unit : 'unit',
338
+ stock: typeof row.stock === 'number' ? row.stock : 0,
339
+ tags: [],
340
+ location: typeof row.location === 'string' ? row.location : '-',
341
+ seller: typeof row.seller === 'string' ? row.seller : '-',
342
+ trend: Math.max(10, 100 - index * 8),
343
+ score: Math.max(18, 100 - index * 12),
344
+ };
345
+ });
346
+ }
347
+
348
+ function toRankedStoresFromUi(ui: LiveVoiceToolUi | undefined, limit: number): RankedStore[] {
349
+ const rows = Array.isArray(ui?.items) ? ui.items : [];
350
+ return rows.slice(0, limit).map((item, index) => {
351
+ const row = typeof item === 'object' && item ? (item as Record<string, unknown>) : {};
352
+ const sourceType =
353
+ typeof row.type === 'string' &&
354
+ (row.type === 'retail' || row.type === 'restaurant' || row.type === 'wholesale' || row.type === 'co-op')
355
+ ? row.type
356
+ : 'retail';
357
+ return {
358
+ id: typeof row.id === 'string' ? row.id : `live-store-${index}`,
359
+ name: typeof row.name === 'string' ? row.name : `Store ${index + 1}`,
360
+ type: sourceType,
361
+ location: typeof row.location === 'string' ? row.location : '-',
362
+ buyingFocus: [],
363
+ responseRate: 0.8,
364
+ activeListings: typeof row.activeListings === 'number' ? row.activeListings : 0,
365
+ rating: typeof row.rating === 'number' ? row.rating : 4,
366
+ score: Math.max(20, 100 - index * 12),
367
+ };
368
+ });
369
+ }
370
+
371
+ function toMarketplaceStatsFromUi(ui: LiveVoiceToolUi | undefined): MarketplaceStats | null {
372
+ const rows = Array.isArray(ui?.items) ? ui.items : [];
373
+ if (!rows.length) return null;
374
+ const map: Record<string, number> = {};
375
+ for (const row of rows) {
376
+ if (typeof row !== 'object' || !row) continue;
377
+ const item = row as Record<string, unknown>;
378
+ const label = typeof item.label === 'string' ? item.label.toLowerCase() : '';
379
+ const value = typeof item.value === 'number' ? item.value : typeof item.value === 'string' ? Number(item.value) : NaN;
380
+ if (!label || Number.isNaN(value)) continue;
381
+ map[label] = value;
382
+ }
383
+ return {
384
+ sellers: map.sellers ?? map['賣家'] ?? 0,
385
+ buyers: map.buyers ?? map['買家'] ?? 0,
386
+ products: map.products ?? map['產品'] ?? map['商品'] ?? 0,
387
+ stores: map.stores ?? map['店家'] ?? 0,
388
+ avgResponseRate: 0,
389
+ weeklyDemandIndex: 0,
390
+ };
391
+ }
392
+
393
  function scoreProduct(
394
  record: ProductRecord,
395
  queryTokens: string[],
 
643
  } as const;
644
 
645
  export default function MarketplaceDemoPanel({
646
+ mode: demoMode,
647
  locale,
648
  onWorkingChange,
649
  onWorkflowInit,
 
774
  });
775
  if (!parsed) return;
776
 
777
+ if (demoMode === 'live') {
778
+ let liveProducts: RankedProduct[] = [];
779
+ let liveStores: RankedStore[] = [];
780
+ let liveStats: MarketplaceStats | null = null;
781
+
782
+ const productStep = steps.find((step) => step.id === 'market-step-2');
783
+ if (productStep) {
784
+ toolCalls.push('product_search_public');
785
+ const productReady = await stepRunner(runToken, productStep, {
786
+ args: {
787
+ nameContains: activeQuery || undefined,
788
+ categoryEquals: category !== 'all' ? category : undefined,
789
+ minPrice,
790
+ maxPrice,
791
+ limit,
792
+ },
793
+ });
794
+ if (!productReady) return;
795
+ const tool = await callVoiceTool({
796
+ name: 'product_search_public',
797
+ locale,
798
+ args: {
799
+ nameContains: activeQuery || undefined,
800
+ categoryEquals: category !== 'all' ? category : undefined,
801
+ minPrice,
802
+ maxPrice,
803
+ limit,
804
+ },
805
+ });
806
+ liveProducts = toRankedProductsFromUi(tool.ui, limit);
807
+ setProducts(liveProducts);
808
+ onTrace(
809
+ buildTrace(
810
+ 'tool',
811
+ locale === 'zh-TW' ? '商品結果' : 'Product results',
812
+ locale === 'zh-TW' ? `回傳 ${liveProducts.length} 筆商品` : `${liveProducts.length} products returned`,
813
+ 'ok',
814
+ { count: liveProducts.length, uiKind: tool.ui?.kind || null }
815
+ )
816
+ );
817
+ }
818
+
819
+ const storeStep = steps.find((step) => step.id === 'market-step-3');
820
+ if (storeStep) {
821
+ toolCalls.push('store_search_public');
822
+ const storeReady = await stepRunner(runToken, storeStep, {
823
+ args: {
824
+ nameContains: activeQuery || undefined,
825
+ typeEquals: storeType !== 'all' ? storeType : undefined,
826
+ locationContains: location || undefined,
827
+ limit,
828
+ },
829
+ });
830
+ if (!storeReady) return;
831
+ const tool = await callVoiceTool({
832
+ name: 'store_search_public',
833
+ locale,
834
+ args: {
835
+ nameContains: activeQuery || undefined,
836
+ typeEquals: storeType !== 'all' ? storeType : undefined,
837
+ locationContains: location || undefined,
838
+ limit,
839
+ },
840
+ });
841
+ liveStores = toRankedStoresFromUi(tool.ui, limit);
842
+ setStores(liveStores);
843
+ onTrace(
844
+ buildTrace(
845
+ 'tool',
846
+ locale === 'zh-TW' ? '店家結果' : 'Store results',
847
+ locale === 'zh-TW' ? `回傳 ${liveStores.length} 筆店家` : `${liveStores.length} stores returned`,
848
+ 'ok',
849
+ { count: liveStores.length, uiKind: tool.ui?.kind || null }
850
+ )
851
+ );
852
+ }
853
+
854
+ const statsStep = steps.find((step) => step.id === 'market-step-4');
855
+ if (statsStep) {
856
+ toolCalls.push('marketplace_get_stats');
857
+ const statsReady = await stepRunner(runToken, statsStep);
858
+ if (!statsReady) return;
859
+ const tool = await callVoiceTool({
860
+ name: 'marketplace_get_stats',
861
+ locale,
862
+ });
863
+ liveStats = toMarketplaceStatsFromUi(tool.ui) || computeMarketplaceStats(liveProducts, liveStores);
864
+ setStats(liveStats);
865
+ onTrace(
866
+ buildTrace(
867
+ 'tool',
868
+ locale === 'zh-TW' ? '市集統計' : 'Marketplace stats',
869
+ locale === 'zh-TW' ? '已取得即時統計。' : 'Fetched live marketplace stats.',
870
+ 'ok',
871
+ { uiKind: tool.ui?.kind || null }
872
+ )
873
+ );
874
+ }
875
+
876
+ const renderStep = steps.find((step) => step.id === 'market-step-5');
877
+ if (renderStep) {
878
+ const rendered = await stepRunner(runToken, renderStep, {
879
+ products: liveProducts.length,
880
+ stores: liveStores.length,
881
+ stats: Boolean(liveStats),
882
+ mode: 'live',
883
+ });
884
+ if (!rendered) return;
885
+ }
886
+
887
+ const summaryText =
888
+ locale === 'zh-TW'
889
+ ? `已完成即時市集搜尋:商品 ${liveProducts.length} 筆,店家 ${liveStores.length} 筆。`
890
+ : `Live marketplace search completed: ${liveProducts.length} products and ${liveStores.length} stores.`;
891
+ setSummary(summaryText);
892
+
893
+ setMeta({
894
+ query: activeQuery || '*',
895
+ mode,
896
+ startedAt: new Date(startMs).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }),
897
+ durationMs: Date.now() - startMs,
898
+ toolCalls,
899
+ });
900
+ return;
901
+ }
902
+
903
  const productStep = steps.find((step) => step.id === 'market-step-2');
904
  if (productStep) {
905
  toolCalls.push('product_search_public');
src/VoiceDemoPanel.tsx CHANGED
@@ -1,6 +1,8 @@
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';
@@ -51,6 +53,7 @@ type WorkflowStepTemplate = {
51
  };
52
 
53
  type VoiceDemoPanelProps = {
 
54
  locale: DemoLocale;
55
  onWorkingChange: (working: boolean) => void;
56
  onWorkflowInit: (steps: ProgressStep[]) => void;
@@ -61,6 +64,92 @@ type VoiceDemoPanelProps = {
61
  onConsumeQueuedPrompt: () => void;
62
  };
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  const COPY = {
65
  en: {
66
  title: 'Voice Assistant',
@@ -347,6 +436,7 @@ function buildStatsCard(locale: DemoLocale): VoiceToolCard {
347
  }
348
 
349
  export default function VoiceDemoPanel({
 
350
  locale,
351
  onWorkingChange,
352
  onWorkflowInit,
@@ -490,10 +580,100 @@ export default function VoiceDemoPanel({
490
  onWorkingChange(false);
491
  };
492
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
493
  const simulateVoiceTurn = async (transcript?: string) => {
494
  const sample = transcript || VOICE_SAMPLE_INPUTS[locale][Math.floor(Math.random() * VOICE_SAMPLE_INPUTS[locale].length)];
495
  appendEntry({ role: 'user', text: sample });
496
 
 
 
 
 
 
497
  const steps = buildTurnSteps(locale);
498
  const summary =
499
  locale === 'zh-TW'
@@ -537,6 +717,10 @@ export default function VoiceDemoPanel({
537
  await openAssistant();
538
  }
539
  appendEntry({ role: 'user', text: copy.tutorialRequest });
 
 
 
 
540
  await runWorkflow(buildTutorialSteps(locale), {
541
  text:
542
  locale === 'zh-TW'
 
1
  import { useEffect, useMemo, useRef, useState } from 'react';
2
+ import type { DemoLocale, DemoMode, ProgressStep, ProgressStepStatus, TraceItem } from './types';
3
  import VoiceFeatureDemo from './VoiceFeatureDemo';
4
+ import { sendVoiceAssistantMessage } from './liveApi';
5
+ import type { LiveVoiceToolUi } from './liveApi';
6
 
7
  type VoiceStatus = 'idle' | 'connecting' | 'ready' | 'listening' | 'processing' | 'error';
8
  type VoiceRole = 'assistant' | 'user' | 'system';
 
53
  };
54
 
55
  type VoiceDemoPanelProps = {
56
+ mode: DemoMode;
57
  locale: DemoLocale;
58
  onWorkingChange: (working: boolean) => void;
59
  onWorkflowInit: (steps: ProgressStep[]) => void;
 
64
  onConsumeQueuedPrompt: () => void;
65
  };
66
 
67
+ function uiActionLabels(ui: LiveVoiceToolUi | undefined): string[] {
68
+ if (!ui || !Array.isArray(ui.actions)) return [];
69
+ return ui.actions
70
+ .map((action) => (action && typeof action.label === 'string' ? action.label : ''))
71
+ .filter(Boolean)
72
+ .slice(0, 4);
73
+ }
74
+
75
+ function mapLiveUiToCard(ui: LiveVoiceToolUi | undefined, locale: DemoLocale): VoiceToolCard | undefined {
76
+ if (!ui) return undefined;
77
+ const kind = typeof ui.kind === 'string' ? ui.kind : '';
78
+ const fallbackTitle = locale === 'zh-TW' ? '工具結果' : 'Tool Result';
79
+ const title = typeof ui.title === 'string' ? ui.title : fallbackTitle;
80
+ const description = typeof ui.description === 'string' ? ui.description : '';
81
+ const actions = uiActionLabels(ui);
82
+
83
+ if (kind === 'product_created' || kind === 'product_updated') {
84
+ const product = typeof ui.product === 'object' && ui.product ? (ui.product as Record<string, unknown>) : {};
85
+ return {
86
+ kind: 'product_updated',
87
+ title,
88
+ description,
89
+ product: {
90
+ name: typeof product.name === 'string' ? product.name : locale === 'zh-TW' ? '未命名產品' : 'Unnamed product',
91
+ price:
92
+ typeof product.price === 'number'
93
+ ? `NT$ ${product.price}${typeof product.unit === 'string' && product.unit ? ` / ${product.unit}` : ''}`
94
+ : '-',
95
+ stock: typeof product.stock === 'number' ? String(product.stock) : '-',
96
+ category: typeof product.category === 'string' ? product.category : '-',
97
+ },
98
+ actions: actions.length ? actions : locale === 'zh-TW' ? ['查看產品'] : ['Open product'],
99
+ };
100
+ }
101
+
102
+ if (kind === 'stats' || kind === 'marketplace_stats') {
103
+ const items = Array.isArray(ui.items) ? ui.items : [];
104
+ const stats = items
105
+ .map((item) => {
106
+ if (typeof item !== 'object' || !item) return null;
107
+ const row = item as Record<string, unknown>;
108
+ if (typeof row.label !== 'string') return null;
109
+ return {
110
+ label: row.label,
111
+ value:
112
+ typeof row.value === 'number'
113
+ ? String(row.value)
114
+ : typeof row.value === 'string'
115
+ ? row.value
116
+ : '-',
117
+ };
118
+ })
119
+ .filter((row): row is { label: string; value: string } => Boolean(row))
120
+ .slice(0, 6);
121
+ return {
122
+ kind: 'stats',
123
+ title,
124
+ description,
125
+ stats,
126
+ actions: actions.length ? actions : locale === 'zh-TW' ? ['查看詳情'] : ['View details'],
127
+ };
128
+ }
129
+
130
+ const items = Array.isArray(ui.items) ? ui.items : [];
131
+ const textItems = items
132
+ .map((item) => {
133
+ if (typeof item !== 'object' || !item) return '';
134
+ const row = item as Record<string, unknown>;
135
+ if (typeof row.title === 'string') return row.title;
136
+ if (typeof row.name === 'string') return row.name;
137
+ if (typeof row.label === 'string') return row.label;
138
+ return '';
139
+ })
140
+ .filter(Boolean)
141
+ .slice(0, 5);
142
+
143
+ if (!textItems.length && !description) return undefined;
144
+ return {
145
+ kind: 'toolkit',
146
+ title,
147
+ description: description || (locale === 'zh-TW' ? '已收到後端工具輸出。' : 'Backend tool output received.'),
148
+ items: textItems.length ? textItems : [locale === 'zh-TW' ? '已完成即時工具執行' : 'Live tool execution completed'],
149
+ actions: actions.length ? actions : locale === 'zh-TW' ? ['下一步'] : ['Next step'],
150
+ };
151
+ }
152
+
153
  const COPY = {
154
  en: {
155
  title: 'Voice Assistant',
 
436
  }
437
 
438
  export default function VoiceDemoPanel({
439
+ mode,
440
  locale,
441
  onWorkingChange,
442
  onWorkflowInit,
 
580
  onWorkingChange(false);
581
  };
582
 
583
+ const runLiveVoiceTurn = async (transcript: string) => {
584
+ runRef.current += 1;
585
+ const runId = runRef.current;
586
+ onWorkingChange(true);
587
+ setStatus('processing');
588
+
589
+ const steps = buildTurnSteps(locale);
590
+ onWorkflowInit(
591
+ steps.map((step) => ({
592
+ id: step.id,
593
+ label: step.label,
594
+ detail: step.runningDetail,
595
+ status: 'pending',
596
+ }))
597
+ );
598
+
599
+ try {
600
+ onStepStatus(steps[0].id, 'in_progress', steps[0].runningDetail);
601
+ onTrace(buildTrace(steps[0].kind, steps[0].label, steps[0].runningDetail, 'running'));
602
+ await sleep(160);
603
+ if (!mountedRef.current || runRef.current !== runId) return;
604
+ onStepStatus(steps[0].id, 'completed', steps[0].doneDetail);
605
+ onTrace(buildTrace(steps[0].kind, steps[0].label, steps[0].doneDetail, 'ok'));
606
+
607
+ onStepStatus(steps[1].id, 'in_progress', steps[1].runningDetail);
608
+ onTrace(buildTrace(steps[1].kind, steps[1].label, steps[1].runningDetail, 'running'));
609
+ const response = await sendVoiceAssistantMessage({
610
+ message: transcript,
611
+ locale,
612
+ pageRoute: '/hf-demo/voice',
613
+ pageContext: locale === 'zh-TW' ? 'Hugging Face Voice Agent Demo' : 'Hugging Face Voice Agent Demo',
614
+ });
615
+ if (!mountedRef.current || runRef.current !== runId) return;
616
+ onStepStatus(steps[1].id, 'completed', steps[1].doneDetail);
617
+ onTrace(
618
+ buildTrace(steps[1].kind, steps[1].label, steps[1].doneDetail, 'ok', {
619
+ messages: response.messages?.length ?? 0,
620
+ uiKind: response.ui?.kind || null,
621
+ })
622
+ );
623
+
624
+ onStepStatus(steps[2].id, 'in_progress', steps[2].runningDetail);
625
+ onTrace(buildTrace(steps[2].kind, steps[2].label, steps[2].runningDetail, 'running'));
626
+ const assistantTexts = (response.messages || [])
627
+ .filter((message) => message.role === 'assistant' && message.text)
628
+ .map((message) => message.text);
629
+ if (assistantTexts.length) {
630
+ for (const text of assistantTexts) {
631
+ appendEntry({ role: 'assistant', text });
632
+ }
633
+ } else {
634
+ appendEntry({
635
+ role: 'assistant',
636
+ text: locale === 'zh-TW' ? '已完成後端語音工具流程。' : 'Live voice tool flow completed.',
637
+ });
638
+ }
639
+ const liveCard = mapLiveUiToCard(response.ui, locale);
640
+ if (liveCard) {
641
+ appendEntry({ role: 'assistant', card: liveCard });
642
+ }
643
+ onStepStatus(steps[2].id, 'completed', steps[2].doneDetail);
644
+ onTrace(
645
+ buildTrace(steps[2].kind, steps[2].label, steps[2].doneDetail, 'ok', {
646
+ card: liveCard?.kind || null,
647
+ })
648
+ );
649
+
650
+ onStepStatus(steps[3].id, 'in_progress', steps[3].runningDetail);
651
+ onTrace(buildTrace(steps[3].kind, steps[3].label, steps[3].runningDetail, 'running'));
652
+ await sleep(120);
653
+ if (!mountedRef.current || runRef.current !== runId) return;
654
+ onStepStatus(steps[3].id, 'completed', steps[3].doneDetail);
655
+ onTrace(buildTrace(steps[3].kind, steps[3].label, steps[3].doneDetail, 'ok'));
656
+
657
+ setStatus('ready');
658
+ onWorkingChange(false);
659
+ } catch (error) {
660
+ const message = error instanceof Error ? error.message : locale === 'zh-TW' ? '語音流程發生錯誤。' : 'Voice flow failed.';
661
+ onStepStatus(steps[1].id, 'error', message);
662
+ onTrace(buildTrace('tool', locale === 'zh-TW' ? '語音錯誤' : 'Voice error', message, 'error'));
663
+ setStatus('error');
664
+ onWorkingChange(false);
665
+ }
666
+ };
667
+
668
  const simulateVoiceTurn = async (transcript?: string) => {
669
  const sample = transcript || VOICE_SAMPLE_INPUTS[locale][Math.floor(Math.random() * VOICE_SAMPLE_INPUTS[locale].length)];
670
  appendEntry({ role: 'user', text: sample });
671
 
672
+ if (mode === 'live') {
673
+ await runLiveVoiceTurn(sample);
674
+ return;
675
+ }
676
+
677
  const steps = buildTurnSteps(locale);
678
  const summary =
679
  locale === 'zh-TW'
 
717
  await openAssistant();
718
  }
719
  appendEntry({ role: 'user', text: copy.tutorialRequest });
720
+ if (mode === 'live') {
721
+ await runLiveVoiceTurn(copy.tutorialRequest);
722
+ return;
723
+ }
724
  await runWorkflow(buildTutorialSteps(locale), {
725
  text:
726
  locale === 'zh-TW'
src/liveAgent.ts ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { callVoiceTool, sendVoiceAssistantMessage } from './liveApi';
2
+ import type { LiveVoiceToolResponse } from './liveApi';
3
+ import type { DemoCard, DemoLocale, DemoTabId, MockAgentResult, ProgressStep, ProgressStepStatus, TraceItem } from './types';
4
+
5
+ type RunLiveAgentParams = {
6
+ tab: DemoTabId;
7
+ input: string;
8
+ locale: DemoLocale;
9
+ onWorkflowInit: (steps: ProgressStep[]) => void;
10
+ onStepStatus: (stepId: string, status: ProgressStepStatus, detail?: string) => void;
11
+ onTrace: (item: TraceItem) => void;
12
+ };
13
+
14
+ function nowLabel(): string {
15
+ return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
16
+ }
17
+
18
+ let sequence = 0;
19
+ function nextId(prefix: string): string {
20
+ sequence += 1;
21
+ return `${prefix}-${Date.now().toString(36)}-${sequence.toString(36)}`;
22
+ }
23
+
24
+ function trace(kind: TraceItem['kind'], title: string, detail: string, status: TraceItem['status'], payload?: Record<string, unknown>): TraceItem {
25
+ return {
26
+ id: nextId('trace'),
27
+ kind,
28
+ title,
29
+ detail,
30
+ status,
31
+ payload,
32
+ timestamp: nowLabel(),
33
+ };
34
+ }
35
+
36
+ function uiActions(ui: Record<string, unknown>): string[] {
37
+ const actions = Array.isArray(ui.actions) ? ui.actions : [];
38
+ return actions
39
+ .map((entry) => {
40
+ if (typeof entry !== 'object' || !entry) return '';
41
+ const label = (entry as { label?: unknown }).label;
42
+ return typeof label === 'string' ? label : '';
43
+ })
44
+ .filter(Boolean)
45
+ .slice(0, 4);
46
+ }
47
+
48
+ function toMetric(label: string, value: unknown): { label: string; value: string } {
49
+ return { label, value: typeof value === 'number' ? String(value) : typeof value === 'string' ? value : '-' };
50
+ }
51
+
52
+ function uiToCards(ui: Record<string, unknown> | undefined, locale: DemoLocale): DemoCard[] {
53
+ if (!ui) return [];
54
+ const kind = typeof ui.kind === 'string' ? ui.kind : '';
55
+ const title = typeof ui.title === 'string' ? ui.title : locale === 'zh-TW' ? '工具結果' : 'Tool Output';
56
+ const description = typeof ui.description === 'string' ? ui.description : undefined;
57
+ const actions = uiActions(ui);
58
+
59
+ if (kind === 'stats' || kind === 'marketplace_stats') {
60
+ const items = Array.isArray(ui.items) ? ui.items : [];
61
+ const metrics = items
62
+ .map((item) => {
63
+ if (typeof item !== 'object' || !item) return null;
64
+ const label = (item as { label?: unknown }).label;
65
+ const value = (item as { value?: unknown }).value;
66
+ return typeof label === 'string' ? toMetric(label, value) : null;
67
+ })
68
+ .filter((metric): metric is { label: string; value: string } => Boolean(metric))
69
+ .slice(0, 6);
70
+
71
+ return [{ title, subtitle: description, metrics, actions }];
72
+ }
73
+
74
+ if (kind === 'product_created' || kind === 'product_updated') {
75
+ const product = typeof ui.product === 'object' && ui.product ? (ui.product as Record<string, unknown>) : {};
76
+ const metrics = [
77
+ toMetric(locale === 'zh-TW' ? '價格' : 'Price', product.price),
78
+ toMetric(locale === 'zh-TW' ? '庫存' : 'Stock', product.stock),
79
+ toMetric(locale === 'zh-TW' ? '分類' : 'Category', product.category),
80
+ ];
81
+ return [
82
+ {
83
+ title,
84
+ subtitle: typeof product.name === 'string' ? product.name : description,
85
+ metrics,
86
+ actions,
87
+ },
88
+ ];
89
+ }
90
+
91
+ if (kind === 'product_list' || kind === 'store_list' || kind === 'marketing_list') {
92
+ const items = Array.isArray(ui.items) ? ui.items : [];
93
+ const cards = items.slice(0, 3).map((item, index) => {
94
+ if (typeof item !== 'object' || !item) {
95
+ return { title: `${title} ${index + 1}` } as DemoCard;
96
+ }
97
+ const row = item as Record<string, unknown>;
98
+ const rowTitle =
99
+ typeof row.name === 'string'
100
+ ? row.name
101
+ : typeof row.title === 'string'
102
+ ? row.title
103
+ : `${title} ${index + 1}`;
104
+ const metrics: Array<{ label: string; value: string }> = [];
105
+ if (row.price !== undefined) metrics.push(toMetric(locale === 'zh-TW' ? '價格' : 'Price', row.price));
106
+ if (row.stock !== undefined) metrics.push(toMetric(locale === 'zh-TW' ? '庫存' : 'Stock', row.stock));
107
+ if (row.location !== undefined) metrics.push(toMetric(locale === 'zh-TW' ? '地區' : 'Location', row.location));
108
+ if (row.type !== undefined) metrics.push(toMetric(locale === 'zh-TW' ? '類型' : 'Type', row.type));
109
+ return {
110
+ title: rowTitle,
111
+ subtitle: typeof row.description === 'string' ? row.description : undefined,
112
+ metrics: metrics.length ? metrics : undefined,
113
+ } as DemoCard;
114
+ });
115
+ return cards.length ? cards : [{ title, subtitle: description, actions }];
116
+ }
117
+
118
+ return [{ title, subtitle: description, actions }];
119
+ }
120
+
121
+ function assistantSummary(messages: Array<{ role?: unknown; text?: unknown }>, locale: DemoLocale): string {
122
+ const textList = messages
123
+ .filter((message) => message && message.role === 'assistant' && typeof message.text === 'string')
124
+ .map((message) => String(message.text).trim())
125
+ .filter(Boolean);
126
+ if (!textList.length) {
127
+ return locale === 'zh-TW' ? '工具流程已完成。' : 'Tool workflow finished.';
128
+ }
129
+ return textList[textList.length - 1];
130
+ }
131
+
132
+ export async function runLiveAgent(params: RunLiveAgentParams): Promise<MockAgentResult> {
133
+ const { tab, input, locale, onWorkflowInit, onStepStatus, onTrace } = params;
134
+
135
+ if (tab !== 'chat') {
136
+ throw new Error('Live agent is currently supported for chat tab only.');
137
+ }
138
+
139
+ const steps: ProgressStep[] = [
140
+ {
141
+ id: 'live-step-1',
142
+ label: locale === 'zh-TW' ? '呼叫語音助理後端' : 'Call voice assistant backend',
143
+ detail: locale === 'zh-TW' ? '傳送需求並等待回覆。' : 'Submitting request and waiting for response.',
144
+ status: 'pending',
145
+ },
146
+ {
147
+ id: 'live-step-2',
148
+ label: locale === 'zh-TW' ? '補充工具資料' : 'Fetch supplemental tool data',
149
+ detail: locale === 'zh-TW' ? '取得即時市集統計。' : 'Fetching live marketplace stats.',
150
+ status: 'pending',
151
+ },
152
+ {
153
+ id: 'live-step-3',
154
+ label: locale === 'zh-TW' ? '渲染回覆卡片' : 'Render response cards',
155
+ detail: locale === 'zh-TW' ? '整理訊息與結構化輸出。' : 'Formatting messages and structured payload.',
156
+ status: 'pending',
157
+ },
158
+ ];
159
+
160
+ onWorkflowInit(steps);
161
+
162
+ onStepStatus(steps[0].id, 'in_progress', steps[0].detail);
163
+ onTrace(
164
+ trace(
165
+ 'planner',
166
+ locale === 'zh-TW' ? '聊天請求' : 'Chat request',
167
+ locale === 'zh-TW' ? '正在呼叫 /api/voice/assistant。' : 'Calling /api/voice/assistant.',
168
+ 'running'
169
+ )
170
+ );
171
+
172
+ const assistant = await sendVoiceAssistantMessage({
173
+ message: input,
174
+ locale,
175
+ pageRoute: '/hf-demo/chat',
176
+ pageContext: locale === 'zh-TW' ? 'Hugging Face Chat Agent Demo' : 'Hugging Face Chat Agent Demo',
177
+ });
178
+
179
+ onStepStatus(steps[0].id, 'completed', locale === 'zh-TW' ? '已收到助理回覆。' : 'Assistant response received.');
180
+ onTrace(
181
+ trace(
182
+ 'planner',
183
+ locale === 'zh-TW' ? '聊天回覆' : 'Chat response',
184
+ locale === 'zh-TW' ? '語音助理回覆已到達。' : 'Voice assistant returned output.',
185
+ 'ok',
186
+ { messageCount: assistant.messages?.length ?? 0, uiKind: assistant.ui?.kind || null }
187
+ )
188
+ );
189
+
190
+ onStepStatus(steps[1].id, 'in_progress', steps[1].detail);
191
+ onTrace(
192
+ trace(
193
+ 'tool',
194
+ locale === 'zh-TW' ? '補充工具查詢' : 'Supplemental tool query',
195
+ locale === 'zh-TW' ? '呼叫 marketplace_get_stats。' : 'Calling marketplace_get_stats.',
196
+ 'running'
197
+ )
198
+ );
199
+
200
+ let statsPayload: LiveVoiceToolResponse | null = null;
201
+ try {
202
+ statsPayload = await callVoiceTool({
203
+ name: 'marketplace_get_stats',
204
+ locale,
205
+ });
206
+ onStepStatus(steps[1].id, 'completed', locale === 'zh-TW' ? '即時統計已取得。' : 'Live stats fetched.');
207
+ onTrace(
208
+ trace(
209
+ 'tool',
210
+ locale === 'zh-TW' ? '統計工具結果' : 'Stats tool result',
211
+ locale === 'zh-TW' ? '市集統計已附加。' : 'Marketplace stats attached.',
212
+ 'ok',
213
+ { uiKind: statsPayload.ui?.kind || null }
214
+ )
215
+ );
216
+ } catch (error) {
217
+ const message = error instanceof Error ? error.message : 'Unknown live tool error';
218
+ onStepStatus(steps[1].id, 'error', message);
219
+ onTrace(
220
+ trace(
221
+ 'tool',
222
+ locale === 'zh-TW' ? '統計工具失敗' : 'Stats tool failed',
223
+ message,
224
+ 'error'
225
+ )
226
+ );
227
+ }
228
+
229
+ onStepStatus(steps[2].id, 'in_progress', steps[2].detail);
230
+ onTrace(
231
+ trace(
232
+ 'renderer',
233
+ locale === 'zh-TW' ? '結果渲染' : 'Result rendering',
234
+ locale === 'zh-TW' ? '正在整理卡片與輸出。' : 'Formatting cards and payload.',
235
+ 'running'
236
+ )
237
+ );
238
+
239
+ const cards = [
240
+ ...uiToCards(assistant.ui as Record<string, unknown> | undefined, locale),
241
+ ...uiToCards(statsPayload?.ui as Record<string, unknown> | undefined, locale),
242
+ ];
243
+
244
+ const summary = assistantSummary(assistant.messages || [], locale);
245
+ const payload: Record<string, unknown> = {
246
+ assistant,
247
+ statsPayload,
248
+ source: 'live-backend',
249
+ };
250
+
251
+ onStepStatus(steps[2].id, 'completed', locale === 'zh-TW' ? '即時回覆已完成。' : 'Live response rendered.');
252
+ onTrace(
253
+ trace(
254
+ 'renderer',
255
+ locale === 'zh-TW' ? '渲染完成' : 'Rendering complete',
256
+ locale === 'zh-TW' ? '回覆內容與載荷已就緒。' : 'Response and payload are ready.',
257
+ 'ok',
258
+ { cards: cards.length }
259
+ )
260
+ );
261
+
262
+ return {
263
+ summary,
264
+ cards,
265
+ payload,
266
+ };
267
+ }
src/liveApi.ts ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getApp, getApps, initializeApp } from 'firebase/app';
2
+ import { getAuth, signInAnonymously } from 'firebase/auth';
3
+ import type { DemoLocale } from './types';
4
+
5
+ type FirebaseEnvConfig = {
6
+ apiKey: string;
7
+ authDomain: string;
8
+ projectId: string;
9
+ appId: string;
10
+ messagingSenderId?: string;
11
+ storageBucket?: string;
12
+ };
13
+
14
+ type VoiceRole = 'assistant' | 'system';
15
+
16
+ export type LiveVoiceMessage = {
17
+ role: VoiceRole;
18
+ text: string;
19
+ };
20
+
21
+ export type LiveVoiceToolAction = {
22
+ label?: string;
23
+ prompt?: string;
24
+ href?: string;
25
+ auto?: boolean;
26
+ };
27
+
28
+ export type LiveVoiceToolUi = {
29
+ kind?: string;
30
+ title?: string;
31
+ description?: string;
32
+ items?: Array<Record<string, unknown>>;
33
+ actions?: LiveVoiceToolAction[];
34
+ [key: string]: unknown;
35
+ };
36
+
37
+ export type LiveVoiceAssistantResponse = {
38
+ messages?: LiveVoiceMessage[];
39
+ ui?: LiveVoiceToolUi;
40
+ };
41
+
42
+ export type LiveVoiceToolResponse = {
43
+ messages?: LiveVoiceMessage[];
44
+ ui?: LiveVoiceToolUi;
45
+ };
46
+
47
+ export type LiveMarketingStreamEvent =
48
+ | { type: 'text'; text: string }
49
+ | { type: 'image'; base64: string; mimeType?: string }
50
+ | { type: 'done' }
51
+ | { type: 'error'; message: string };
52
+
53
+ export type LiveInvoiceItem = {
54
+ description?: string;
55
+ quantity?: number;
56
+ unitPrice?: number;
57
+ lineTotal?: number;
58
+ };
59
+
60
+ export type LiveInvoiceExtraction = {
61
+ provider?: 'gemini' | 'mock' | string;
62
+ model?: string;
63
+ confidence?: number;
64
+ warnings?: string[];
65
+ rawText?: string;
66
+ invoice?: {
67
+ direction?: 'sale' | 'purchase' | string;
68
+ status?: 'draft' | 'open' | 'paid' | 'overdue' | string;
69
+ invoiceNumber?: string;
70
+ counterpartyName?: string;
71
+ description?: string;
72
+ currency?: string;
73
+ total?: number;
74
+ paid?: number;
75
+ issueDate?: string;
76
+ dueDate?: string;
77
+ deliveryDate?: string;
78
+ items?: LiveInvoiceItem[];
79
+ };
80
+ };
81
+
82
+ export type LiveInvoiceScanResponse = {
83
+ extraction?: LiveInvoiceExtraction;
84
+ created?: { id?: string } | null;
85
+ };
86
+
87
+ const DEMO_USER_ID = (import.meta.env.VITE_DEMO_USER_ID as string | undefined)?.trim() || '';
88
+ const HF_DEMO_API_KEY = (import.meta.env.VITE_HF_DEMO_API_KEY as string | undefined)?.trim() || '';
89
+
90
+ const firebaseConfig: FirebaseEnvConfig = {
91
+ apiKey: (import.meta.env.VITE_FIREBASE_API_KEY as string | undefined)?.trim() || '',
92
+ authDomain: (import.meta.env.VITE_FIREBASE_AUTH_DOMAIN as string | undefined)?.trim() || '',
93
+ projectId: (import.meta.env.VITE_FIREBASE_PROJECT_ID as string | undefined)?.trim() || '',
94
+ appId: (import.meta.env.VITE_FIREBASE_APP_ID as string | undefined)?.trim() || '',
95
+ messagingSenderId: (import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID as string | undefined)?.trim() || undefined,
96
+ storageBucket: (import.meta.env.VITE_FIREBASE_STORAGE_BUCKET as string | undefined)?.trim() || undefined,
97
+ };
98
+
99
+ let authTokenPromise: Promise<string | null> | null = null;
100
+
101
+ function getLiveApiBaseUrl(): string {
102
+ const raw =
103
+ (import.meta.env.VITE_DEMO_API_BASE_URL as string | undefined)?.trim() ||
104
+ (import.meta.env.VITE_API_BASE_URL as string | undefined)?.trim() ||
105
+ '';
106
+
107
+ if (!raw) {
108
+ throw new Error('Missing `VITE_DEMO_API_BASE_URL` for live mode.');
109
+ }
110
+
111
+ return raw.endsWith('/api') ? raw.slice(0, -4) : raw.replace(/\/$/, '');
112
+ }
113
+
114
+ function hasFirebaseConfig(config: FirebaseEnvConfig): boolean {
115
+ return Boolean(config.apiKey && config.authDomain && config.projectId && config.appId);
116
+ }
117
+
118
+ async function getIdToken(): Promise<string | null> {
119
+ if (!hasFirebaseConfig(firebaseConfig)) return null;
120
+ if (!authTokenPromise) {
121
+ authTokenPromise = (async () => {
122
+ const app = getApps().some((entry) => entry.name === 'hf-demo-app')
123
+ ? getApp('hf-demo-app')
124
+ : initializeApp(firebaseConfig, 'hf-demo-app');
125
+ const auth = getAuth(app);
126
+ if (!auth.currentUser) {
127
+ await signInAnonymously(auth);
128
+ }
129
+ return auth.currentUser ? await auth.currentUser.getIdToken(true) : null;
130
+ })();
131
+ }
132
+ return authTokenPromise;
133
+ }
134
+
135
+ async function parseErrorResponse(response: Response): Promise<string> {
136
+ try {
137
+ const json = (await response.json()) as { error?: string; message?: string; hint?: string };
138
+ return json.message || json.error || `Request failed with status ${response.status}`;
139
+ } catch {
140
+ const text = await response.text().catch(() => '');
141
+ return text || `Request failed with status ${response.status}`;
142
+ }
143
+ }
144
+
145
+ async function requestJson<T>(path: string, init: RequestInit = {}): Promise<T> {
146
+ const baseUrl = getLiveApiBaseUrl();
147
+ const token = await getIdToken();
148
+ const headers = new Headers(init.headers || {});
149
+ if (!headers.has('Content-Type')) {
150
+ headers.set('Content-Type', 'application/json');
151
+ }
152
+ if (token) {
153
+ headers.set('Authorization', `Bearer ${token}`);
154
+ }
155
+ if (HF_DEMO_API_KEY) {
156
+ headers.set('x-hf-demo-key', HF_DEMO_API_KEY);
157
+ }
158
+ const response = await fetch(`${baseUrl}${path}`, {
159
+ ...init,
160
+ headers,
161
+ });
162
+ if (!response.ok) {
163
+ const message = await parseErrorResponse(response);
164
+ throw new Error(message);
165
+ }
166
+ return (await response.json()) as T;
167
+ }
168
+
169
+ function withDemoUser<T extends Record<string, unknown>>(body: T): T {
170
+ if (!DEMO_USER_ID) return body;
171
+ return { ...body, userId: DEMO_USER_ID };
172
+ }
173
+
174
+ export function parseDataUrl(dataUrl: string): { base64: string; mimeType: string } {
175
+ const trimmed = dataUrl.trim();
176
+ const match = trimmed.match(/^data:([^;]+);base64,(.+)$/);
177
+ if (!match) {
178
+ return { base64: trimmed, mimeType: 'image/png' };
179
+ }
180
+ return { mimeType: match[1], base64: match[2] };
181
+ }
182
+
183
+ export async function sendVoiceAssistantMessage(params: {
184
+ message: string;
185
+ locale: DemoLocale;
186
+ pageRoute?: string;
187
+ pageContext?: string;
188
+ }): Promise<LiveVoiceAssistantResponse> {
189
+ return await requestJson<LiveVoiceAssistantResponse>(
190
+ '/api/voice/assistant',
191
+ {
192
+ method: 'POST',
193
+ body: JSON.stringify(
194
+ withDemoUser({
195
+ message: params.message,
196
+ locale: params.locale,
197
+ pageRoute: params.pageRoute,
198
+ pageContext: params.pageContext,
199
+ })
200
+ ),
201
+ }
202
+ );
203
+ }
204
+
205
+ export async function callVoiceTool(params: {
206
+ name: string;
207
+ args?: Record<string, unknown>;
208
+ locale: DemoLocale;
209
+ }): Promise<LiveVoiceToolResponse> {
210
+ return await requestJson<LiveVoiceToolResponse>(
211
+ '/api/voice/tool',
212
+ {
213
+ method: 'POST',
214
+ body: JSON.stringify(
215
+ withDemoUser({
216
+ name: params.name,
217
+ args: params.args || {},
218
+ locale: params.locale,
219
+ })
220
+ ),
221
+ }
222
+ );
223
+ }
224
+
225
+ export async function scanInvoiceImage(params: {
226
+ dataBase64: string;
227
+ mimeType?: string;
228
+ locale: DemoLocale;
229
+ createInvoice: boolean;
230
+ }): Promise<LiveInvoiceScanResponse> {
231
+ return await requestJson<LiveInvoiceScanResponse>(
232
+ '/api/invoices/scan',
233
+ {
234
+ method: 'POST',
235
+ body: JSON.stringify({
236
+ dataBase64: params.dataBase64,
237
+ mimeType: params.mimeType,
238
+ locale: params.locale,
239
+ createInvoice: params.createInvoice,
240
+ }),
241
+ }
242
+ );
243
+ }
244
+
245
+ export async function streamMarketingGeneration(params: {
246
+ prompt: string;
247
+ locale: DemoLocale;
248
+ product: { name: string; category: string; description?: string };
249
+ image?: { base64: string; mimeType: string };
250
+ signal?: AbortSignal;
251
+ onEvent: (event: LiveMarketingStreamEvent) => void;
252
+ }): Promise<void> {
253
+ const baseUrl = getLiveApiBaseUrl();
254
+ const token = await getIdToken();
255
+ const headers = new Headers({ 'Content-Type': 'application/json' });
256
+ if (token) {
257
+ headers.set('Authorization', `Bearer ${token}`);
258
+ }
259
+ if (HF_DEMO_API_KEY) {
260
+ headers.set('x-hf-demo-key', HF_DEMO_API_KEY);
261
+ }
262
+
263
+ const response = await fetch(`${baseUrl}/api/marketing/generate-stream`, {
264
+ method: 'POST',
265
+ headers,
266
+ signal: params.signal,
267
+ body: JSON.stringify({
268
+ prompt: params.prompt,
269
+ language: params.locale,
270
+ product: params.product,
271
+ image: params.image,
272
+ }),
273
+ });
274
+
275
+ if (!response.ok || !response.body) {
276
+ const message = await parseErrorResponse(response);
277
+ throw new Error(message);
278
+ }
279
+
280
+ const reader = response.body.getReader();
281
+ const decoder = new TextDecoder('utf-8');
282
+ let buffer = '';
283
+
284
+ while (true) {
285
+ const { value, done } = await reader.read();
286
+ if (done) break;
287
+ buffer += decoder.decode(value, { stream: true });
288
+ const lines = buffer.split('\n');
289
+ buffer = lines.pop() ?? '';
290
+
291
+ for (const line of lines) {
292
+ const trimmed = line.trim();
293
+ if (!trimmed) continue;
294
+ try {
295
+ const event = JSON.parse(trimmed) as LiveMarketingStreamEvent;
296
+ params.onEvent(event);
297
+ } catch {
298
+ // Ignore malformed chunks from network boundaries.
299
+ }
300
+ }
301
+ }
302
+ }
src/vite-env.d.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ /// <reference types="vite/client" />