This view is limited to 50 files because it contains too many changes. See the raw diff here.
Files changed (50) hide show
  1. .gitattributes +0 -217
  2. .github/workflows/auto-sync.yml +0 -30
  3. .ref_update +1 -0
  4. README.md +2 -11
  5. TRIGGER_REBUILD.txt +0 -1
  6. add-product-fix.js +0 -987
  7. added_img/bep-tu-2-vung-nau-boss-bki689n-0.jpg +0 -0
  8. added_img/bt1-big-test-0.jpg +0 -3
  9. added_img/gx1-test-grob-0.jpg +0 -0
  10. ai-search-v2.js +0 -1167
  11. canzy_products_img/canzy_product_CZ007R.png +0 -3
  12. canzy_products_img/canzy_product_CZ007R_v2026.png +0 -3
  13. canzy_products_img/canzy_product_CZ03T.png +0 -0
  14. canzy_products_img/canzy_product_CZ03T_v2026.png +0 -3
  15. canzy_products_img/canzy_product_CZ09DL.png +0 -3
  16. canzy_products_img/canzy_product_CZ11EBN.png +0 -3
  17. canzy_products_img/canzy_product_CZ11EBN_v2026.png +0 -3
  18. canzy_products_img/canzy_product_CZ11QAMSERIAL80.png +0 -3
  19. canzy_products_img/canzy_product_CZ11QAMSERIAL80_v2026.png +0 -3
  20. canzy_products_img/canzy_product_CZ15YA.png +0 -0
  21. canzy_products_img/canzy_product_CZ15YA_v2026.png +0 -0
  22. canzy_products_img/canzy_product_CZ16EUGRUNIQUE.png +0 -3
  23. canzy_products_img/canzy_product_CZ16EUGRUNIQUE_v2026.png +0 -3
  24. canzy_products_img/canzy_product_CZ34GSS.png +0 -3
  25. canzy_products_img/canzy_product_CZ34GSS_v2026.png +0 -3
  26. canzy_products_img/canzy_product_CZ68DT.png +0 -3
  27. canzy_products_img/canzy_product_CZ68DT_v2026.png +0 -3
  28. canzy_products_img/canzy_product_CZ700AUTO.png +0 -3
  29. canzy_products_img/canzy_product_CZ700AUTO_v2026.png +0 -3
  30. canzy_products_img/canzy_product_CZ70B50.png +0 -3
  31. canzy_products_img/canzy_product_CZ70B50_v2026.png +0 -3
  32. canzy_products_img/canzy_product_CZ70C66.png +0 -3
  33. canzy_products_img/canzy_product_CZ70C66_v2026.png +0 -3
  34. canzy_products_img/canzy_product_CZ70I50.png +0 -3
  35. canzy_products_img/canzy_product_CZ70I50_v2026.png +0 -3
  36. canzy_products_img/canzy_product_CZ780K.png +0 -3
  37. canzy_products_img/canzy_product_CZ780K_v2026.png +0 -3
  38. canzy_products_img/canzy_product_CZ806A5G.png +0 -3
  39. canzy_products_img/canzy_product_CZ806A5G_v2026.png +0 -3
  40. canzy_products_img/canzy_product_CZ815TDB_v2026.png +0 -3
  41. canzy_products_img/canzy_product_CZ816SDR.png +0 -3
  42. canzy_products_img/canzy_product_CZ816SDR_v2026.png +0 -3
  43. canzy_products_img/canzy_product_CZ899.png +0 -3
  44. canzy_products_img/canzy_product_CZ899_v2026.png +0 -3
  45. canzy_products_img/canzy_product_CZ9979GRH.png +0 -3
  46. canzy_products_img/canzy_product_CZ9979GRH_v2026.png +0 -0
  47. canzy_products_img/canzy_product_CZ9979GRS.png +0 -0
  48. canzy_products_img/canzy_product_CZ9979GRS_v2026.png +0 -0
  49. canzy_products_img/canzy_product_CZ9979GRT.png +0 -0
  50. canzy_products_img/canzy_product_CZ9979GRT_v2026.png +0 -0
.gitattributes CHANGED
@@ -4056,220 +4056,3 @@ images/products/khop-noi-l95mm-hafele-97205064-1-1-2296612.jpg filter=lfs diff=l
4056
  images/products/khung-keo-co-gia-treo-ao-vest-hafele-80579030-1-1-2571591.jpg filter=lfs diff=lfs merge=lfs -text
4057
  images/products/khung-keo-co-gia-treo-ao-vest-hafele-80579130-1-1-2571673.jpg filter=lfs diff=lfs merge=lfs -text
4058
  images/products/khung-nhom-alusion-h02-hafele-56358343-1-1-2562308.jpg filter=lfs diff=lfs merge=lfs -text
4059
- grob_products_img/grob_p10_img0.jpeg filter=lfs diff=lfs merge=lfs -text
4060
- grob_products_img/grob_p11_img0.jpeg filter=lfs diff=lfs merge=lfs -text
4061
- grob_products_img/grob_p15_img0.jpeg filter=lfs diff=lfs merge=lfs -text
4062
- grob_products_img/grob_p17_img0.jpeg filter=lfs diff=lfs merge=lfs -text
4063
- grob_products_img/grob_p19_img2.jpeg filter=lfs diff=lfs merge=lfs -text
4064
- grob_products_img/grob_p20_img0.jpeg filter=lfs diff=lfs merge=lfs -text
4065
- grob_products_img/grob_p21_img0.jpeg filter=lfs diff=lfs merge=lfs -text
4066
- grob_products_img/grob_p21_img1.jpeg filter=lfs diff=lfs merge=lfs -text
4067
- grob_products_img/grob_p22_img0.jpeg filter=lfs diff=lfs merge=lfs -text
4068
- grob_products_img/grob_p23_img1.jpeg filter=lfs diff=lfs merge=lfs -text
4069
- grob_products_img/grob_p24_img1.jpeg filter=lfs diff=lfs merge=lfs -text
4070
- grob_products_img/grob_p28_img1.jpeg filter=lfs diff=lfs merge=lfs -text
4071
- grob_products_img/grob_p7_img1.jpeg filter=lfs diff=lfs merge=lfs -text
4072
- grob_products_img/grob_p8_img0.jpeg filter=lfs diff=lfs merge=lfs -text
4073
- grob_products_img/grob_p8_img1.jpeg filter=lfs diff=lfs merge=lfs -text
4074
- grob_products_img/grob_p8_img3.jpeg filter=lfs diff=lfs merge=lfs -text
4075
- grob_products_img/grob_p9_img0.jpeg filter=lfs diff=lfs merge=lfs -text
4076
- san-pham/mi7502es-malloca-bep-tu-2-vung-nau-75cm/thumb.jpg filter=lfs diff=lfs merge=lfs -text
4077
- san-pham/mi803es-malloca-bep-tu-3-vung-nau-80cm/thumb.jpg filter=lfs diff=lfs merge=lfs -text
4078
- san-pham/mir9021es-malloca-bep-tu-ket-hop-hong-ngoai-3-vung-nau-80cm/thumb.jpg filter=lfs diff=lfs merge=lfs -text
4079
- san-pham/mi593es-malloca-bep-tu-3-vung-nau-59cm/thumb.jpg filter=lfs diff=lfs merge=lfs -text
4080
- grob_img/grob_p10.jpeg filter=lfs diff=lfs merge=lfs -text
4081
- grob_img/grob_p101.jpeg filter=lfs diff=lfs merge=lfs -text
4082
- grob_img/grob_p102.jpeg filter=lfs diff=lfs merge=lfs -text
4083
- grob_img/grob_p106.jpeg filter=lfs diff=lfs merge=lfs -text
4084
- grob_img/grob_p109.jpeg filter=lfs diff=lfs merge=lfs -text
4085
- grob_img/grob_p11.jpeg filter=lfs diff=lfs merge=lfs -text
4086
- grob_img/grob_p110.jpeg filter=lfs diff=lfs merge=lfs -text
4087
- grob_img/grob_p111.jpeg filter=lfs diff=lfs merge=lfs -text
4088
- grob_img/grob_p118.jpeg filter=lfs diff=lfs merge=lfs -text
4089
- grob_img/grob_p119.jpeg filter=lfs diff=lfs merge=lfs -text
4090
- grob_img/grob_p120.jpeg filter=lfs diff=lfs merge=lfs -text
4091
- grob_img/grob_p125.jpeg filter=lfs diff=lfs merge=lfs -text
4092
- grob_img/grob_p130.jpeg filter=lfs diff=lfs merge=lfs -text
4093
- grob_img/grob_p131.jpeg filter=lfs diff=lfs merge=lfs -text
4094
- grob_img/grob_p132.jpeg filter=lfs diff=lfs merge=lfs -text
4095
- grob_img/grob_p133.jpeg filter=lfs diff=lfs merge=lfs -text
4096
- grob_img/grob_p134.jpeg filter=lfs diff=lfs merge=lfs -text
4097
- grob_img/grob_p140.jpeg filter=lfs diff=lfs merge=lfs -text
4098
- grob_img/grob_p141.jpeg filter=lfs diff=lfs merge=lfs -text
4099
- grob_img/grob_p166.jpeg filter=lfs diff=lfs merge=lfs -text
4100
- grob_img/grob_p17.jpeg filter=lfs diff=lfs merge=lfs -text
4101
- grob_img/grob_p174.jpeg filter=lfs diff=lfs merge=lfs -text
4102
- grob_img/grob_p175.jpeg filter=lfs diff=lfs merge=lfs -text
4103
- grob_img/grob_p177.jpeg filter=lfs diff=lfs merge=lfs -text
4104
- grob_img/grob_p178.jpeg filter=lfs diff=lfs merge=lfs -text
4105
- grob_img/grob_p179.jpeg filter=lfs diff=lfs merge=lfs -text
4106
- grob_img/grob_p180.jpeg filter=lfs diff=lfs merge=lfs -text
4107
- grob_img/grob_p183.jpeg filter=lfs diff=lfs merge=lfs -text
4108
- grob_img/grob_p184.jpeg filter=lfs diff=lfs merge=lfs -text
4109
- grob_img/grob_p185.jpeg filter=lfs diff=lfs merge=lfs -text
4110
- grob_img/grob_p186.jpeg filter=lfs diff=lfs merge=lfs -text
4111
- grob_img/grob_p19.jpeg filter=lfs diff=lfs merge=lfs -text
4112
- grob_img/grob_p199.jpeg filter=lfs diff=lfs merge=lfs -text
4113
- grob_img/grob_p2.jpeg filter=lfs diff=lfs merge=lfs -text
4114
- grob_img/grob_p201.jpeg filter=lfs diff=lfs merge=lfs -text
4115
- grob_img/grob_p21.jpeg filter=lfs diff=lfs merge=lfs -text
4116
- grob_img/grob_p23.jpeg filter=lfs diff=lfs merge=lfs -text
4117
- grob_img/grob_p24.jpeg filter=lfs diff=lfs merge=lfs -text
4118
- grob_img/grob_p28.jpeg filter=lfs diff=lfs merge=lfs -text
4119
- grob_img/grob_p29.jpeg filter=lfs diff=lfs merge=lfs -text
4120
- grob_img/grob_p3.jpeg filter=lfs diff=lfs merge=lfs -text
4121
- grob_img/grob_p30.jpeg filter=lfs diff=lfs merge=lfs -text
4122
- grob_img/grob_p31.jpeg filter=lfs diff=lfs merge=lfs -text
4123
- grob_img/grob_p32.jpeg filter=lfs diff=lfs merge=lfs -text
4124
- grob_img/grob_p33.jpeg filter=lfs diff=lfs merge=lfs -text
4125
- grob_img/grob_p34.jpeg filter=lfs diff=lfs merge=lfs -text
4126
- grob_img/grob_p35.jpeg filter=lfs diff=lfs merge=lfs -text
4127
- grob_img/grob_p36.jpeg filter=lfs diff=lfs merge=lfs -text
4128
- grob_img/grob_p37.jpeg filter=lfs diff=lfs merge=lfs -text
4129
- grob_img/grob_p38.jpeg filter=lfs diff=lfs merge=lfs -text
4130
- grob_img/grob_p39.jpeg filter=lfs diff=lfs merge=lfs -text
4131
- grob_img/grob_p40.jpeg filter=lfs diff=lfs merge=lfs -text
4132
- grob_img/grob_p41.jpeg filter=lfs diff=lfs merge=lfs -text
4133
- added_img/bt1-big-test-0.jpg filter=lfs diff=lfs merge=lfs -text
4134
- canzy_products_img/canzy_product_CZ007R.png filter=lfs diff=lfs merge=lfs -text
4135
- canzy_products_img/canzy_product_CZ09DL.png filter=lfs diff=lfs merge=lfs -text
4136
- canzy_products_img/canzy_product_CZ11EBN.png filter=lfs diff=lfs merge=lfs -text
4137
- canzy_products_img/canzy_product_CZ11QAMSERIAL80.png filter=lfs diff=lfs merge=lfs -text
4138
- canzy_products_img/canzy_product_CZ16EUGRUNIQUE.png filter=lfs diff=lfs merge=lfs -text
4139
- canzy_products_img/canzy_product_CZ34GSS.png filter=lfs diff=lfs merge=lfs -text
4140
- canzy_products_img/canzy_product_CZ68DT.png filter=lfs diff=lfs merge=lfs -text
4141
- canzy_products_img/canzy_product_CZ700AUTO.png filter=lfs diff=lfs merge=lfs -text
4142
- canzy_products_img/canzy_product_CZ70B50.png filter=lfs diff=lfs merge=lfs -text
4143
- canzy_products_img/canzy_product_CZ70C66.png filter=lfs diff=lfs merge=lfs -text
4144
- canzy_products_img/canzy_product_CZ70I50.png filter=lfs diff=lfs merge=lfs -text
4145
- canzy_products_img/canzy_product_CZ780K.png filter=lfs diff=lfs merge=lfs -text
4146
- canzy_products_img/canzy_product_CZ806A5G.png filter=lfs diff=lfs merge=lfs -text
4147
- canzy_products_img/canzy_product_CZ816SDR.png filter=lfs diff=lfs merge=lfs -text
4148
- canzy_products_img/canzy_product_CZ899.png filter=lfs diff=lfs merge=lfs -text
4149
- canzy_products_img/canzy_product_CZ9979GRH.png filter=lfs diff=lfs merge=lfs -text
4150
- canzy_products_img/canzy_product_CZB6001TE.png filter=lfs diff=lfs merge=lfs -text
4151
- canzy_products_img/canzy_product_CZB70SG.png filter=lfs diff=lfs merge=lfs -text
4152
- canzy_products_img/canzy_product_CZD12GS.png filter=lfs diff=lfs merge=lfs -text
4153
- canzy_products_img/canzy_product_CZD33315C.png filter=lfs diff=lfs merge=lfs -text
4154
- canzy_products_img/canzy_product_CZD777711C.png filter=lfs diff=lfs merge=lfs -text
4155
- canzy_products_img/canzy_product_CZEH70AW.png filter=lfs diff=lfs merge=lfs -text
4156
- canzy_products_img/canzy_product_CZH1060AT.png filter=lfs diff=lfs merge=lfs -text
4157
- canzy_products_img/canzy_product_CZH1070AT.png filter=lfs diff=lfs merge=lfs -text
4158
- canzy_products_img/canzy_product_CZH1160AT.png filter=lfs diff=lfs merge=lfs -text
4159
- canzy_products_img/canzy_product_CZH1170AT.png filter=lfs diff=lfs merge=lfs -text
4160
- canzy_products_img/canzy_product_CZH1170KC.png filter=lfs diff=lfs merge=lfs -text
4161
- canzy_products_img/canzy_product_CZH1270AT.png filter=lfs diff=lfs merge=lfs -text
4162
- canzy_products_img/canzy_product_CZH1370AT.png filter=lfs diff=lfs merge=lfs -text
4163
- canzy_products_img/canzy_product_CZH1470AT.png filter=lfs diff=lfs merge=lfs -text
4164
- canzy_products_img/canzy_product_CZH1470CT.png filter=lfs diff=lfs merge=lfs -text
4165
- canzy_products_img/canzy_product_CZH1570AT.png filter=lfs diff=lfs merge=lfs -text
4166
- canzy_products_img/canzy_product_CZH175AT.png filter=lfs diff=lfs merge=lfs -text
4167
- canzy_products_img/canzy_product_CZH190ATC.png filter=lfs diff=lfs merge=lfs -text
4168
- canzy_products_img/canzy_product_CZH2070CD.png filter=lfs diff=lfs merge=lfs -text
4169
- canzy_products_img/canzy_product_CZH2170CD.png filter=lfs diff=lfs merge=lfs -text
4170
- canzy_products_img/canzy_product_CZH2270CDC.png filter=lfs diff=lfs merge=lfs -text
4171
- canzy_products_img/canzy_product_CZH2277CDC.png filter=lfs diff=lfs merge=lfs -text
4172
- canzy_products_img/canzy_product_CZH245CT.png filter=lfs diff=lfs merge=lfs -text
4173
- canzy_products_img/canzy_product_CZH271CT.png filter=lfs diff=lfs merge=lfs -text
4174
- canzy_products_img/canzy_product_CZH276CT.png filter=lfs diff=lfs merge=lfs -text
4175
- canzy_products_img/canzy_product_CZH278AT.png filter=lfs diff=lfs merge=lfs -text
4176
- canzy_products_img/canzy_product_CZH287AT.png filter=lfs diff=lfs merge=lfs -text
4177
- canzy_products_img/canzy_product_CZH290CT.png filter=lfs diff=lfs merge=lfs -text
4178
- canzy_products_img/canzy_product_CZH345CT.png filter=lfs diff=lfs merge=lfs -text
4179
- canzy_products_img/canzy_product_CZH470VTC.png filter=lfs diff=lfs merge=lfs -text
4180
- canzy_products_img/canzy_product_CZH570CTC.png filter=lfs diff=lfs merge=lfs -text
4181
- canzy_products_img/canzy_product_CZH8070KCC.png filter=lfs diff=lfs merge=lfs -text
4182
- canzy_products_img/canzy_product_CZH829CT.png filter=lfs diff=lfs merge=lfs -text
4183
- canzy_products_img/canzy_product_CZH839VT.png filter=lfs diff=lfs merge=lfs -text
4184
- canzy_products_img/canzy_product_CZH8668CTCNEW.png filter=lfs diff=lfs merge=lfs -text
4185
- canzy_products_img/canzy_product_CZH8689CTCNEW.png filter=lfs diff=lfs merge=lfs -text
4186
- canzy_products_img/canzy_product_CZH900VT.png filter=lfs diff=lfs merge=lfs -text
4187
- canzy_products_img/canzy_product_CZHN001.png filter=lfs diff=lfs merge=lfs -text
4188
- canzy_products_img/canzy_product_CZI25Y22.png filter=lfs diff=lfs merge=lfs -text
4189
- canzy_products_img/canzy_product_CZI60002D.png filter=lfs diff=lfs merge=lfs -text
4190
- canzy_products_img/canzy_product_CZID077EU.png filter=lfs diff=lfs merge=lfs -text
4191
- canzy_products_img/canzy_product_CZIDCISLAND.png filter=lfs diff=lfs merge=lfs -text
4192
- canzy_products_img/canzy_product_CZKD9.png filter=lfs diff=lfs merge=lfs -text
4193
- canzy_products_img/canzy_product_CZL10060.png filter=lfs diff=lfs merge=lfs -text
4194
- canzy_products_img/canzy_product_CZL10073.png filter=lfs diff=lfs merge=lfs -text
4195
- canzy_products_img/canzy_product_CZM36825.png filter=lfs diff=lfs merge=lfs -text
4196
- canzy_products_img/canzy_product_CZM38831.png filter=lfs diff=lfs merge=lfs -text
4197
- canzy_products_img/canzy_product_CZML870DC.png filter=lfs diff=lfs merge=lfs -text
4198
- canzy_products_img/canzy_product_CZML877R.png filter=lfs diff=lfs merge=lfs -text
4199
- canzy_products_img/canzy_product_CZML970DC.png filter=lfs diff=lfs merge=lfs -text
4200
- canzy_products_img/canzy_product_CZN70SG.png filter=lfs diff=lfs merge=lfs -text
4201
- canzy_products_img/canzy_product_CZR11PLUS.png filter=lfs diff=lfs merge=lfs -text
4202
- canzy_products_img/canzy_product_CZTK170.png filter=lfs diff=lfs merge=lfs -text
4203
- canzy_products_img/canzy_product_CZWOK28.png filter=lfs diff=lfs merge=lfs -text
4204
- canzy_products_img/canzy_product_CZ16EUGRUNIQUE_v2026.png filter=lfs diff=lfs merge=lfs -text
4205
- canzy_products_img/canzy_product_CZD777711C_v2026.png filter=lfs diff=lfs merge=lfs -text
4206
- canzy_products_img/canzy_product_CZ11QAMSERIAL80_v2026.png filter=lfs diff=lfs merge=lfs -text
4207
- canzy_products_img/canzy_product_CZID077EU_v2026.png filter=lfs diff=lfs merge=lfs -text
4208
- canzy_products_img/canzy_product_CZTL875R_v2026.png filter=lfs diff=lfs merge=lfs -text
4209
- canzy_products_img/canzy_product_CZT52SERIII_v2026.png filter=lfs diff=lfs merge=lfs -text
4210
- canzy_products_img/canzy_product_CZML87ODC_v2026.png filter=lfs diff=lfs merge=lfs -text
4211
- canzy_products_img/canzy_product_CZB3200TEC_v2026.png filter=lfs diff=lfs merge=lfs -text
4212
- canzy_products_img/canzy_product_CZ03T_v2026.png filter=lfs diff=lfs merge=lfs -text
4213
- canzy_products_img/canzy_product_CZB9698TE_v2026.png filter=lfs diff=lfs merge=lfs -text
4214
- canzy_products_img/canzy_product_CZB6001TE_v2026.png filter=lfs diff=lfs merge=lfs -text
4215
- canzy_products_img/canzy_product_CZI60002D_v2026.png filter=lfs diff=lfs merge=lfs -text
4216
- canzy_products_img/canzy_product_CZHN001_v2026.png filter=lfs diff=lfs merge=lfs -text
4217
- canzy_products_img/canzy_product_CZKD9_v2026.png filter=lfs diff=lfs merge=lfs -text
4218
- canzy_products_img/canzy_product_CZH600DI_v2026.png filter=lfs diff=lfs merge=lfs -text
4219
- canzy_products_img/canzy_product_CZH900VT_v2026.png filter=lfs diff=lfs merge=lfs -text
4220
- canzy_products_img/canzy_product_CZH470VTC_v2026.png filter=lfs diff=lfs merge=lfs -text
4221
- canzy_products_img/canzy_product_CZEH70AW_v2026.png filter=lfs diff=lfs merge=lfs -text
4222
- canzy_products_img/canzy_product_CZH8668CTCNEW_v2026.png filter=lfs diff=lfs merge=lfs -text
4223
- canzy_products_img/canzy_product_CZH8689CTCNEW_v2026.png filter=lfs diff=lfs merge=lfs -text
4224
- canzy_products_img/canzy_product_CZH345CT_v2026.png filter=lfs diff=lfs merge=lfs -text
4225
- canzy_products_img/canzy_product_CZH245CT_v2026.png filter=lfs diff=lfs merge=lfs -text
4226
- canzy_products_img/canzy_product_CZH290CT_v2026.png filter=lfs diff=lfs merge=lfs -text
4227
- canzy_products_img/canzy_product_CZH271CT_v2026.png filter=lfs diff=lfs merge=lfs -text
4228
- canzy_products_img/canzy_product_CZTK170_v2026.png filter=lfs diff=lfs merge=lfs -text
4229
- canzy_products_img/canzy_product_CZH570CTC_v2026.png filter=lfs diff=lfs merge=lfs -text
4230
- canzy_products_img/canzy_product_CZH276CT_v2026.png filter=lfs diff=lfs merge=lfs -text
4231
- canzy_products_img/canzy_product_CZH1470CT_v2026.png filter=lfs diff=lfs merge=lfs -text
4232
- canzy_products_img/canzy_product_CZML970DC_v2026.png filter=lfs diff=lfs merge=lfs -text
4233
- canzy_products_img/canzy_product_CZML870DC_v2026.png filter=lfs diff=lfs merge=lfs -text
4234
- canzy_products_img/canzy_product_CZ70C66_v2026.png filter=lfs diff=lfs merge=lfs -text
4235
- canzy_products_img/canzy_product_CZH8070KCC_v2026.png filter=lfs diff=lfs merge=lfs -text
4236
- canzy_products_img/canzy_product_CZB70SG_v2026.png filter=lfs diff=lfs merge=lfs -text
4237
- canzy_products_img/canzy_product_CZN70SG_v2026.png filter=lfs diff=lfs merge=lfs -text
4238
- canzy_products_img/canzy_product_CZ806A5G_v2026.png filter=lfs diff=lfs merge=lfs -text
4239
- canzy_products_img/canzy_product_CZH1170KC_v2026.png filter=lfs diff=lfs merge=lfs -text
4240
- canzy_products_img/canzy_product_CZ68DT_v2026.png filter=lfs diff=lfs merge=lfs -text
4241
- canzy_products_img/canzy_product_CZH175AT_v2026.png filter=lfs diff=lfs merge=lfs -text
4242
- canzy_products_img/canzy_product_CZH278AT_v2026.png filter=lfs diff=lfs merge=lfs -text
4243
- canzy_products_img/canzy_product_CZH287AT_v2026.png filter=lfs diff=lfs merge=lfs -text
4244
- canzy_products_img/canzy_product_CZ700AUTO_v2026.png filter=lfs diff=lfs merge=lfs -text
4245
- canzy_products_img/canzy_product_CZH190ATC_v2026.png filter=lfs diff=lfs merge=lfs -text
4246
- canzy_products_img/canzy_product_CZH1370AT_v2026.png filter=lfs diff=lfs merge=lfs -text
4247
- canzy_products_img/canzy_product_CZH1270AT_v2026.png filter=lfs diff=lfs merge=lfs -text
4248
- canzy_products_img/canzy_product_CZH1470AT_v2026.png filter=lfs diff=lfs merge=lfs -text
4249
- canzy_products_img/canzy_product_CZH1070AT_v2026.png filter=lfs diff=lfs merge=lfs -text
4250
- canzy_products_img/canzy_product_CZH1060AT_v2026.png filter=lfs diff=lfs merge=lfs -text
4251
- canzy_products_img/canzy_product_CZH1570AT_v2026.png filter=lfs diff=lfs merge=lfs -text
4252
- canzy_products_img/canzy_product_CZH1160AT_v2026.png filter=lfs diff=lfs merge=lfs -text
4253
- canzy_products_img/canzy_product_CZH2170CD_v2026.png filter=lfs diff=lfs merge=lfs -text
4254
- canzy_products_img/canzy_product_CZ70I50_v2026.png filter=lfs diff=lfs merge=lfs -text
4255
- canzy_products_img/canzy_product_CZH2070CD_v2026.png filter=lfs diff=lfs merge=lfs -text
4256
- canzy_products_img/canzy_product_CZ70B50_v2026.png filter=lfs diff=lfs merge=lfs -text
4257
- canzy_products_img/canzy_product_CZH2277CDC_v2026.png filter=lfs diff=lfs merge=lfs -text
4258
- canzy_products_img/canzy_product_CZH2270CDC_v2026.png filter=lfs diff=lfs merge=lfs -text
4259
- canzy_products_img/canzy_product_CZL10073_v2026.png filter=lfs diff=lfs merge=lfs -text
4260
- canzy_products_img/canzy_product_CZ11EBN_v2026.png filter=lfs diff=lfs merge=lfs -text
4261
- canzy_products_img/canzy_product_CZO9DL_v2026.png filter=lfs diff=lfs merge=lfs -text
4262
- canzy_products_img/canzy_product_CZL10060_v2026.png filter=lfs diff=lfs merge=lfs -text
4263
- canzy_products_img/canzy_product_CZM36825_v2026.png filter=lfs diff=lfs merge=lfs -text
4264
- canzy_products_img/canzy_product_CZM38831_v2026.png filter=lfs diff=lfs merge=lfs -text
4265
- canzy_products_img/canzy_product_CZ34GSS_v2026.png filter=lfs diff=lfs merge=lfs -text
4266
- canzy_products_img/canzy_product_CZ816SDR_v2026.png filter=lfs diff=lfs merge=lfs -text
4267
- canzy_products_img/canzy_product_CZ815TDB_v2026.png filter=lfs diff=lfs merge=lfs -text
4268
- canzy_products_img/canzy_product_CZD12GS_v2026.png filter=lfs diff=lfs merge=lfs -text
4269
- canzy_products_img/canzy_product_CZI25Y22_v2026.png filter=lfs diff=lfs merge=lfs -text
4270
- canzy_products_img/canzy_product_CZR11PLUS_v2026.png filter=lfs diff=lfs merge=lfs -text
4271
- canzy_products_img/canzy_product_CZ780K_v2026.png filter=lfs diff=lfs merge=lfs -text
4272
- canzy_products_img/canzy_product_CZ007R_v2026.png filter=lfs diff=lfs merge=lfs -text
4273
- canzy_products_img/canzy_product_CZ899_v2026.png filter=lfs diff=lfs merge=lfs -text
4274
- canzy_products_img/canzy_product_CZWOK28_v2026.png filter=lfs diff=lfs merge=lfs -text
4275
- canzy_products_img/canzy_product_CZH829CT_v2026.png filter=lfs diff=lfs merge=lfs -text
 
4056
  images/products/khung-keo-co-gia-treo-ao-vest-hafele-80579030-1-1-2571591.jpg filter=lfs diff=lfs merge=lfs -text
4057
  images/products/khung-keo-co-gia-treo-ao-vest-hafele-80579130-1-1-2571673.jpg filter=lfs diff=lfs merge=lfs -text
4058
  images/products/khung-nhom-alusion-h02-hafele-56358343-1-1-2562308.jpg filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/workflows/auto-sync.yml DELETED
@@ -1,30 +0,0 @@
1
- name: Auto Sync Products
2
-
3
- on:
4
- schedule:
5
- # Run daily at 00:00 UTC (8:00 AM Vietnam time)
6
- - cron: '0 0 * * *'
7
- workflow_dispatch: # Allow manual trigger
8
-
9
- jobs:
10
- sync:
11
- runs-on: ubuntu-latest
12
- steps:
13
- - name: Checkout code
14
- uses: actions/checkout@v4
15
-
16
- - name: Set up Python
17
- uses: actions/setup-python@v5
18
- with:
19
- python-version: '3.11'
20
-
21
- - name: Install dependencies
22
- run: |
23
- python -m pip install --upgrade pip
24
- pip install huggingface_hub datasets
25
-
26
- - name: Run auto-sync
27
- env:
28
- HF_TOKEN: ${{ secrets.HF_TOKEN }}
29
- run: |
30
- python3 sync_products.py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.ref_update ADDED
@@ -0,0 +1 @@
 
 
1
+ {"ref": "refs/heads/main", "sha": "9e6dfe0d06d39fbbdea6c476555616a3cacd3078"}
README.md CHANGED
@@ -1,12 +1,3 @@
1
- ---
2
- title: V AISTUDIO Products
3
- emoji: 📉
4
- colorFrom: green
5
- colorTo: pink
6
- sdk: static
7
- pinned: false
8
- tags:
9
- - ml-intern
10
- ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ # V.AISTUDIO
 
 
 
 
 
 
 
 
 
2
 
3
+ Restored to commit 9e6dfe0d06d39fbbdea6c476555616a3cacd3078
TRIGGER_REBUILD.txt DELETED
@@ -1 +0,0 @@
1
- rebuild 1785899558 count-sync
 
 
add-product-fix.js DELETED
@@ -1,987 +0,0 @@
1
- /**
2
- * V.AI STUDIO — Add Product Modal Fix (v2)
3
- * =====================================================================
4
- * FIXES:
5
- * 1. MODAL: closeAddProductModal() / submitAddProductUrl() /
6
- * openAddProductModal() were never defined -> modal couldn't close or add.
7
- * 2. SECRET: on the live static Space the VAISTUDIO env secret is served
8
- * REDACTED (HF_TOKEN_REDACTED), so the client could never validate it and
9
- * the "Mã truy cập không đúng" error appeared even with the correct code.
10
- * -> We now validate against the injected env secret when it is real, and
11
- * against a documented local override (VAISTUDIO2024) otherwise; the
12
- * accepted code is shown as a hint so the owner is never locked out.
13
- * 3. SCRAPING: the old flow only accepted same-site /san-pham/<slug>/ URLs.
14
- * Now it accepts ANY product URL (external stores like imundex.vn too),
15
- * fetched through public CORS proxies (browser fetch to foreign origins is
16
- * CORS-blocked), and parsed with a robust extractor (JSON-LD, og:*, price
17
- * patterns, Mã số/Tên sản phẩm tables, galleries, spec tables).
18
- *
19
- * The new product is persisted to localStorage and injected live into the
20
- * catalog (grid/search/detail). A best-effort push to products_url_added.json
21
- * is attempted.
22
- */
23
- (function () {
24
- 'use strict';
25
- if (window.__VAI_ADD_PROD_FIX2__) return;
26
- window.__VAI_ADD_PROD_FIX2__ = true;
27
-
28
- /* ------------------------- CONFIG ------------------------- */
29
- var ENV_SECRET = (window.huggingface && window.huggingface.variables && window.huggingface.variables.VAISTUDIO) || '';
30
- var LOCAL_SECRET = 'VAISTUDIO2024'; // documented default used when env is redacted
31
- var SITE = (location.origin && location.origin.indexOf('hf.space') >= 0)
32
- ? location.origin : 'https://bep40-v-aistudio.static.hf.space';
33
-
34
- var PROXIES = [
35
- function (u) { return 'https://api.allorigins.win/raw?url=' + encodeURIComponent(u); },
36
- function (u) { return 'https://corsproxy.io/?url=' + encodeURIComponent(u); },
37
- function (u) { return 'https://api.codetabs.com/v1/proxy?quest=' + encodeURIComponent(u); }
38
- ];
39
-
40
- // Dedicated server-side scraper (reliable, no CORS/proxy flakiness). Called first.
41
- var SCRAPER = 'https://bep40-vai-product-scraper.hf.space/api/scrape';
42
- var PERSIST_API = 'https://bep40-vai-product-scraper.hf.space/api/persist';
43
- var OCR_API = 'https://bep40-vai-product-scraper.hf.space/api/ocr';
44
- var SCRAPER_TOKEN = LOCAL_SECRET;
45
-
46
- // OCR-selected images (each is a data-URL string). Used for both OCR + product gallery.
47
- var OCR_IMAGES = [];
48
-
49
- /* ------------------------- IMAGE UPLOAD / PASTE ------------------------- */
50
- // Reads files and APPENDS them into OCR_IMAGES (fixed: previous version kept
51
- // them only in a local array so file-picked images were never OCR'd).
52
- function readFilesAsDataURL(fileList, cbDone) {
53
- var files = Array.prototype.slice.call(fileList || []);
54
- if (!files.length) { cbDone && cbDone(0); return; }
55
- var pending = files.length, added = 0;
56
- files.forEach(function (f) {
57
- if (!f || !/^image\//.test(f.type || '')) { pending--; if (pending === 0) cbDone && cbDone(added); return; }
58
- var reader = new FileReader();
59
- reader.onload = function (e) {
60
- OCR_IMAGES.push(e.target.result);
61
- added++;
62
- pending--;
63
- if (pending === 0) { renderImgPreview(); cbDone && cbDone(added); }
64
- };
65
- reader.onerror = function () { pending--; if (pending === 0) { renderImgPreview(); cbDone && cbDone(added); } };
66
- reader.readAsDataURL(f);
67
- });
68
- }
69
- function renderImgPreview() {
70
- var box = document.getElementById('addImgPreview');
71
- if (!box) return;
72
- if (!OCR_IMAGES.length) { box.innerHTML = ''; return; }
73
- box.innerHTML = OCR_IMAGES.map(function (src, i) {
74
- return '<div class="add-img-thumb"><img src="' + esc(src) + '">' +
75
- '<button class="rm" onclick="removeAddImage(' + i + ')" title="Xóa">×</button>' +
76
- '<button class="crop" onclick="openCropModal(' + i + ')" title="Cắt ảnh"><i class="fas fa-crop-alt"></i></button></div>';
77
- }).join('');
78
- }
79
- function addImageDataURL(url) {
80
- OCR_IMAGES.push(url);
81
- renderImgPreview();
82
- }
83
- window.removeAddImage = function (i) {
84
- OCR_IMAGES.splice(i, 1);
85
- renderImgPreview();
86
- var pv = document.getElementById('addPvGallery');
87
- if (pv) renderPreviewImages();
88
- };
89
- /* ------------------------- IMAGE CROP ------------------------- */
90
- var _cropIdx = -1, _cropImg = null, _crop = null, _cropEls = {};
91
- function loadImage(src) {
92
- return new Promise(function (res, rej) {
93
- var im = new Image();
94
- im.onload = function () { res(im); };
95
- im.onerror = rej;
96
- im.src = src;
97
- });
98
- }
99
- // Resize the natural image so it fully fits inside the stage, returning the
100
- // actual on-screen dimensions (in CSS px) and the pixel scale factor.
101
- function fitCropImage(img, stage) {
102
- var stageW = stage.clientWidth || Math.max(stage.offsetWidth, 320);
103
- var stageH = stage.clientHeight || Math.max(stage.offsetHeight, 320);
104
- var maxW = Math.min(stageW - 16, 560);
105
- var maxH = Math.min(stageH - 16, window.innerHeight * 0.5);
106
- var iw = img.naturalWidth || img.width || 1;
107
- var ih = img.naturalHeight || img.height || 1;
108
- var scale = Math.min(maxW / iw, maxH / ih, 1);
109
- var dispW = Math.max(1, Math.round(iw * scale));
110
- var dispH = Math.max(1, Math.round(ih * scale));
111
- img.style.width = dispW + 'px';
112
- img.style.height = dispH + 'px';
113
- return { dispW: dispW, dispH: dispH, iw: iw, ih: ih, scale: scale };
114
- }
115
- window.openCropModal = async function (idx) {
116
- var overlay = document.getElementById('addCropOverlay');
117
- if (!overlay) return;
118
- var src = OCR_IMAGES && OCR_IMAGES[idx];
119
- if (!src) return;
120
- _cropIdx = idx;
121
- try { _cropImg = await loadImage(src); }
122
- catch (e) { alert('Không đọc được ảnh để cắt.'); return; }
123
- var stage = document.getElementById('addCropStage');
124
- if (!stage) return;
125
- overlay.style.display = 'flex';
126
- stage.innerHTML = '';
127
- // WRAPPER: sized exactly to the displayed image, centered in the flex stage.
128
- // The crop box lives INSIDE the wrapper at image-relative coordinates, so
129
- // crop math is exact — no stage-centering offset to drift the result.
130
- var wrap = document.createElement('div');
131
- wrap.className = 'add-crop-wrap';
132
- var img = document.createElement('img');
133
- img.src = src;
134
- img.draggable = false;
135
- wrap.appendChild(img);
136
- stage.appendChild(wrap);
137
- _cropEls.wrap = wrap;
138
- requestAnimationFrame(function () {
139
- var fit = fitCropImage(img, stage);
140
- wrap.style.width = fit.dispW + 'px';
141
- wrap.style.height = fit.dispH + 'px';
142
- // Crop box = centered 85% selection, clamped to the displayed image box.
143
- var bw = Math.round(fit.dispW * 0.85), bh = Math.round(fit.dispH * 0.85);
144
- var bx = Math.round((fit.dispW - bw) / 2), by = Math.round((fit.dispH - bh) / 2);
145
- var box = document.createElement('div');
146
- box.className = 'add-crop-box';
147
- box.style.left = bx + 'px'; box.style.top = by + 'px';
148
- box.style.width = bw + 'px'; box.style.height = bh + 'px';
149
- wrap.appendChild(box);
150
- _crop = { x: bx, y: by, w: bw, h: bh, iw: fit.iw, ih: fit.ih, dispW: fit.dispW, dispH: fit.dispH };
151
- _cropEls.box = box;
152
- addCropHandles(box);
153
- attachCropInteractions(stage, wrap, img);
154
- _cropEls.img = img;
155
- });
156
- };
157
- function addCropHandles(box) {
158
- box.innerHTML = '';
159
- var names = ['nw','n','ne','e','se','s','sw','w'];
160
- names.forEach(function (n) {
161
- var h = document.createElement('div');
162
- h.className = 'add-crop-handle h-' + n;
163
- box.appendChild(h);
164
- });
165
- }
166
- function attachCropInteractions(stage, wrap, img) {
167
- var dragging = null; // {mode:'move'|handle, handle:'nw'.., startX,startY, box0}
168
- // Mouse/touch coordinates are measured relative to the WRAPPER, so they
169
- // map 1:1 onto the displayed image box (image-relative).
170
- function ptOf(e) {
171
- var t = e.touches && e.touches[0] || (e.changedTouches && e.changedTouches[0]) || e;
172
- var r = wrap.getBoundingClientRect();
173
- return { x: t.clientX - r.left, y: t.clientY - r.top };
174
- }
175
- stage.onmousedown = stage.ontouchstart = function (e) {
176
- var t = e.target;
177
- var handle = null;
178
- while (t && t !== stage && t !== document) {
179
- if (t.className && String(t.className).indexOf('add-crop-handle') >= 0) { handle = t; break; }
180
- t = t.parentNode;
181
- }
182
- var pt = ptOf(e);
183
- var box = _cropEls.box;
184
- if (handle) {
185
- var cls = String(handle.className);
186
- var n = (cls.match(/h-(nw|n|ne|e|se|s|sw|w)/) || [])[1] || 'se';
187
- dragging = { mode: 'handle', handle: n, startX: pt.x, startY: pt.y, box0: cloneBox() };
188
- } else if (e.target === box || (wrap && e.target === wrap)) {
189
- dragging = { mode: 'move', startX: pt.x, startY: pt.y, box0: cloneBox() };
190
- } else {
191
- return;
192
- }
193
- e.preventDefault();
194
- };
195
- document.onmousemove = document.ontouchmove = function (e) {
196
- if (!dragging) return;
197
- var pt = ptOf(e);
198
- var dx = pt.x - dragging.startX, dy = pt.y - dragging.startY;
199
- if (dragging.mode === 'move') {
200
- moveBox(dragging.box0, dx, dy);
201
- } else {
202
- resizeBox(dragging.box0, dragging.handle, dx, dy);
203
- }
204
- e.preventDefault();
205
- };
206
- function endDrag() { dragging = null; }
207
- document.onmouseup = document.ontouchend = endDrag;
208
- stage.ondragstart = function () { return false; };
209
- }
210
- function cloneBox() {
211
- var c = _crop;
212
- return { x: c.x, y: c.y, w: c.w, h: c.h };
213
- }
214
- function clamp2(v, a, b) { return Math.max(a, Math.min(b, v)); }
215
- function moveBox(b0, dx, dy) {
216
- var c = _crop;
217
- var nx = clamp2(b0.x + dx, 0, c.dispW - b0.w);
218
- var ny = clamp2(b0.y + dy, 0, c.dispH - b0.h);
219
- c.x = nx; c.y = ny;
220
- paintBox();
221
- }
222
- function resizeBox(b0, handle, dx, dy) {
223
- var c = _crop;
224
- var x = b0.x, y = b0.y, w = b0.w, h = b0.h;
225
- if (handle.indexOf('e') >= 0) { w = b0.w + dx; }
226
- if (handle.indexOf('s') >= 0) { h = b0.h + dy; }
227
- if (handle.indexOf('w') >= 0) { x = b0.x + dx; w = b0.w - dx; }
228
- if (handle.indexOf('n') >= 0) { y = b0.y + dy; h = b0.h - dy; }
229
- var min = 20;
230
- if (x < 0) { w += x; x = 0; }
231
- if (y < 0) { h += y; y = 0; }
232
- if (x + w > c.dispW) { w = c.dispW - x; }
233
- if (y + h > c.dispH) { h = c.dispH - y; }
234
- if (w < min || h < min) return;
235
- c.x = x; c.y = y; c.w = w; c.h = h;
236
- paintBox();
237
- }
238
- function paintBox() {
239
- var box = _cropEls.box;
240
- if (!box || !_crop) return;
241
- box.style.left = _crop.x + 'px';
242
- box.style.top = _crop.y + 'px';
243
- box.style.width = _crop.w + 'px';
244
- box.style.height = _crop.h + 'px';
245
- }
246
- window.closeCropModal = function () {
247
- var o = document.getElementById('addCropOverlay');
248
- if (o) o.style.display = 'none';
249
- document.onmousemove = document.onmouseup = document.ontouchmove = document.ontouchend = null;
250
- };
251
- window.applyCrop = async function () {
252
- if (_cropIdx < 0 || !_cropImg || !_crop) { closeCropModal(); return; }
253
- var c = _crop;
254
- var dispW = c.dispW || c.iw, dispH = c.dispH || c.ih;
255
- // Clamp against the real image-relative box (in display px).
256
- c.x = clamp2(c.x, 0, Math.max(0, dispW - 1));
257
- c.y = clamp2(c.y, 0, Math.max(0, dispH - 1));
258
- c.w = clamp2(c.w, 1, dispW - c.x);
259
- c.h = clamp2(c.h, 1, dispH - c.y);
260
- var sx = c.x / dispW * c.iw;
261
- var sy = c.y / dispH * c.ih;
262
- var sw = c.w / dispW * c.iw;
263
- var sh = c.h / dispH * c.ih;
264
- var cv = document.createElement('canvas');
265
- cv.width = Math.max(1, Math.round(sw));
266
- cv.height = Math.max(1, Math.round(sh));
267
- var ctx = cv.getContext('2d');
268
- ctx.drawImage(_cropImg, sx, sy, sw, sh, 0, 0, cv.width, cv.height);
269
- OCR_IMAGES[_cropIdx] = cv.toDataURL('image/jpeg', 0.92);
270
- renderImgPreview();
271
- var pv = document.getElementById('addPvGallery');
272
- if (pv) renderPreviewImages();
273
- _cropIdx = -1; _crop = null; _cropImg = null;
274
- closeCropModal();
275
- };
276
-
277
- function bindImgZone() {
278
- var zone = document.getElementById('addImgZone');
279
- var input = document.getElementById('addImgInput');
280
- if (!zone || !input) return;
281
- zone.addEventListener('click', function () { input.click(); });
282
- input.addEventListener('change', function () {
283
- readFilesAsDataURL(input.files, function () { input.value = ''; });
284
- });
285
- zone.addEventListener('dragover', function (e) { e.preventDefault(); zone.classList.add('drag'); });
286
- zone.addEventListener('dragleave', function () { zone.classList.remove('drag'); });
287
- zone.addEventListener('drop', function (e) {
288
- e.preventDefault(); zone.classList.remove('drag');
289
- readFilesAsDataURL(e.dataTransfer.files);
290
- });
291
-
292
- // Unified image-paste handling: works whether the focus is on the chat-style
293
- // box or anywhere else in the modal. Two hooks: a direct 'paste' on the
294
- // textarea (mobile often fires it there) plus a document-level fallback.
295
- function handlePaste(e, clip) {
296
- var hasImg = false;
297
- var items = clip && clip.items;
298
- if (!items) return false;
299
- for (var i = 0; i < items.length; i++) {
300
- if (items[i].type && /^image\//.test(items[i].type)) {
301
- hasImg = true;
302
- var blob = items[i].getAsFile();
303
- if (!blob) continue;
304
- var reader = new FileReader();
305
- reader.onload = function (ev) { addImageDataURL(ev.target.result); };
306
- reader.readAsDataURL(blob);
307
- }
308
- }
309
- return hasImg;
310
- }
311
- var ta = document.getElementById('addPasteInput');
312
- if (ta) {
313
- ta.addEventListener('paste', function (e) {
314
- var m = document.getElementById('addProductOverlay');
315
- if (!m || !m.classList.contains('open')) return;
316
- if (handlePaste(e, e.clipboardData)) {
317
- e.preventDefault(); e.stopPropagation();
318
- ta.value = '';
319
- }
320
- });
321
- }
322
- document.addEventListener('paste', function (e) {
323
- var m = document.getElementById('addProductOverlay');
324
- if (!m || !m.classList.contains('open')) return;
325
- // Only handle here if the event target is NOT the textarea (already handled above)
326
- if (e.target && e.target.id === 'addPasteInput') return;
327
- if (handlePaste(e, e.clipboardData)) {
328
- e.preventDefault();
329
- if (ta) ta.value = '';
330
- }
331
- });
332
- }
333
- if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bindImgZone);
334
- else bindImgZone();
335
-
336
- /* ------------------------- OCR ------------------------- */
337
- window.runOCRAddProduct = async function () {
338
- var st = document.getElementById('addProductStatus');
339
- function status(msg, cls) { if (st) { st.textContent = msg; st.className = 'add-product-status ' + (cls || ''); } }
340
- var secret = (document.getElementById('addProductSecret') && document.getElementById('addProductSecret').value || '').trim();
341
- if (!isValidSecret(secret)) { status('❌ Mã truy cập không đúng. Nhập: ' + LOCAL_SECRET, 'err'); return; }
342
- if (!OCR_IMAGES.length) { status('❌ Chưa có ảnh nào. Hãy tải lên hoặc dán (Ctrl+V) ảnh sản phẩm.', 'err'); return; }
343
- var btn = document.getElementById('addImgScan');
344
- if (btn) { btn.disabled = true; btn.textContent = '⏳ Đang OCR…'; }
345
- status('⏳ Đang trích xuất thông tin từ ảnh (OCR)…');
346
- try {
347
- var note = (document.getElementById('addOcrNote') || {}).value || '';
348
- var r = await fetch(OCR_API, {
349
- method: 'POST', headers: { 'Content-Type': 'application/json' },
350
- body: JSON.stringify({ token: apiToken(), images: OCR_IMAGES, note: note })
351
- });
352
- if (!r.ok) throw new Error((await r.text()) || ('HTTP ' + r.status));
353
- var d = await r.json();
354
- if (document.getElementById('addOcrName')) document.getElementById('addOcrName').value = d.name || '';
355
- if (document.getElementById('addOcrSku')) document.getElementById('addOcrSku').value = d.sku || '';
356
- if (document.getElementById('addOcrPrice')) document.getElementById('addOcrPrice').value = d.pn || '';
357
- if (document.getElementById('addOcrBrand')) document.getElementById('addOcrBrand').value = d.brand || '';
358
- if (document.getElementById('addOcrSpecs')) {
359
- var spStr = '';
360
- if (d.specs && typeof d.specs === 'object') {
361
- spStr = Object.keys(d.specs).map(function (k) { return k + ': ' + d.specs[k]; }).join('\n');
362
- }
363
- document.getElementById('addOcrSpecs').value = spStr || '';
364
- }
365
- if (document.getElementById('addOcrDesc')) document.getElementById('addOcrDesc').value = d.desc || '';
366
- if (document.getElementById('addOcrFeats')) {
367
- var ft = (Array.isArray(d.feats) ? d.feats : []).join('\n');
368
- document.getElementById('addOcrFeats').value = ft;
369
- }
370
- var fields = document.getElementById('addOcrFields');
371
- if (fields) fields.style.display = 'block';
372
- var found = (d.name ? 1 : 0) + (d.sku ? 1 : 0) + (d.pn ? 1 : 0) + (d.brand ? 1 : 0);
373
-
374
- // Prepare pending product from the OCR fields + images, show preview,
375
- // and switch the button to "Xác nhận thêm SP" so OCR is immediately usable.
376
- var slug2 = slugify(((document.getElementById('addOcrSku')||{}).value ? (document.getElementById('addOcrSku')||{}).value + ' ' : '') + (d.name || ''));
377
- var pyrDesc = d.desc || '';
378
- if (!pyrDesc && d.specs && typeof d.specs === 'object') {
379
- // leftover info that has no dedicated field goes into the description
380
- var leftover = [];
381
- Object.keys(d.specs).forEach(function (k) {
382
- if (['Điện áp','Công suất','Kích thước','Chất liệu','Xuất xứ','Bảo hành','Trọng lượng','Màu sắc','Model','Loại'].indexOf(k) < 0) leftover.push(k + ': ' + d.specs[k]);
383
- });
384
- pyrDesc = leftover.join('; ');
385
- }
386
- __pendingAdd = {
387
- r: {
388
- name: d.name || '', sku: d.sku || '', price: d.pn ? money(d.pn) : '', pn: d.pn || 0,
389
- img: OCR_IMAGES[0], gallery: OCR_IMAGES.slice(), desc: pyrDesc,
390
- brand: d.brand || '', specs: {}, feats: Array.isArray(d.feats) ? d.feats.slice() : []
391
- },
392
- slug: slug2, pageUrl: '', usingOCR: true
393
- };
394
- __pendingAdd.r.imgs_data = OCR_IMAGES.slice();
395
- enterPreviewMode();
396
-
397
- status('✅ OCR xong: nhận được ' + found + ' trường thông tin. Preview đã hiển thị — bạn có thể chỉnh sửa, rồi bấm "Xác nhận thêm SP".', 'ok');
398
- } catch (e) {
399
- status('❌ Lỗi OCR: ' + (e && e.message || e), 'err');
400
- } finally {
401
- if (btn) { btn.disabled = false; btn.textContent = '<i class="fas fa-magnifying-glass"></i> Trích xuất thông tin từ ảnh (OCR)'; }
402
- }
403
- };
404
-
405
- /* ------------------------- PREVIEW ------------------------- */
406
- // Renders the current product info + images into the preview panel so the
407
- // shop owner can review/edit/delete/upload/paste before adding to the Space.
408
- function currentAddPreviewData() {
409
- var name = (document.getElementById('addOcrName') || {}).value || '';
410
- var sku = (document.getElementById('addOcrSku') || {}).value || '';
411
- var price = (document.getElementById('addOcrPrice') || {}).value || '';
412
- var brand = (document.getElementById('addOcrBrand') || {}).value || '';
413
- var cat = (document.getElementById('addOcrCat') || {}).value || '';
414
- var desc = (document.getElementById('addOcrDesc') || {}).value || '';
415
- var feats = (function(){ var v=((document.getElementById('addOcrFeats')||{}).value||'').split(/\n+/).map(function(s){return s.trim();}).filter(Boolean); return v; })();
416
- var specs = (function(){ var o={}; ((document.getElementById('addOcrSpecs')||{}).value||'').split(/\n+/).forEach(function(l){ var m=l.match(/^([^::]{1,60})\s*[::]\s*(.+)$/); if(m){var k=m[1].trim(),v=m[2].trim(); if(k&&v)o[k]=v;} }); return o; })();
417
- var p = { name: name, sku: sku, price: price, pn: parseInt(price, 10) || 0, brand: brand, cat: cat, desc: desc, specs: specs, feats: feats, imgs: OCR_IMAGES.slice() };
418
- return p;
419
- }
420
- function renderPreviewImages() {
421
- var gal = document.getElementById('addPvGallery');
422
- if (!gal) return;
423
- var main = OCR_IMAGES[0] || '';
424
- var parts = '';
425
- if (main) {
426
- parts += '<img src="' + esc(main) + '" alt="Ảnh chính">';
427
- } else {
428
- parts += '<div style="width:100%;aspect-ratio:1;border-radius:10px;background:#f1f5f9;display:flex;align-items:center;justify-content:center;color:#94a3b8;font-size:.72rem;text-align:center;padding:8px;box-sizing:border-box">Chưa có ảnh.<br>Thêm ảnh bên trên.</div>';
429
- }
430
- if (OCR_IMAGES.length > 1) {
431
- parts += '<div class="add-pv-thumbs">' + OCR_IMAGES.slice(1).map(function (src, i) {
432
- return '<img src="' + esc(src) + '" title="Ảnh ' + (i + 2) + '" onclick="removeAddImage(' + (i + 1) + ')">';
433
- }).join('') + '</div>';
434
- }
435
- gal.innerHTML = parts;
436
- }
437
- function buildAddPreview() {
438
- var wrap = document.getElementById('addPreviewWrap');
439
- if (!wrap) return;
440
- var p = currentAddPreviewData();
441
- var el = document.getElementById('addPvName'); if (el) el.textContent = p.name || 'Chưa có tên sản phẩm';
442
- var el = document.getElementById('addPvSku'); if (el) el.textContent = p.sku ? ('Mã: ' + p.sku) : '';
443
- var el = document.getElementById('addPvPrice'); if (el) el.textContent = p.pn ? (p.pn.toLocaleString('vi-VN') + 'đ') : 'Liên hệ';
444
- var el = document.getElementById('addPvBrand'); if (el) el.textContent = (p.cat ? (p.cat + ' ') : '') + (p.brand ? ('• Thương hiệu: ' + p.brand) : '');
445
- var el = document.getElementById('addPvDesc'); if (el) el.textContent = p.desc || '';
446
- var spEl = document.getElementById('addPvSpecs');
447
- if (spEl) {
448
- var spk = Object.keys(p.specs||{});
449
- spEl.innerHTML = spk.length ? spk.map(function(k){ return '<div><b>'+esc(k)+':</b> '+esc(p.specs[k])+'</div>'; }).join('') : '';
450
- }
451
- var ftEl = document.getElementById('addPvFeats');
452
- if (ftEl) {
453
- ftEl.innerHTML = (p.feats && p.feats.length) ? p.feats.map(function(f){ return '<div>✓ '+esc(f)+'</div>'; }).join('') : '';
454
- }
455
- renderPreviewImages();
456
- wrap.style.display = 'block';
457
- }
458
- window.refreshAddPreview = function () { buildAddPreview(); };
459
-
460
- function bindPreviewRefresh() {
461
- ['addOcrName', 'addOcrSku', 'addOcrPrice', 'addOcrBrand', 'addOcrCat', 'addOcrDesc', 'addOcrFeats', 'addOcrSpecs'].forEach(function (id) {
462
- var el = document.getElementById(id);
463
- if (el) el.addEventListener('input', function () { if (document.getElementById('addPreviewWrap') && document.getElementById('addPreviewWrap').style.display !== 'none') buildAddPreview(); });
464
- });
465
- }
466
- if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bindPreviewRefresh);
467
- else bindPreviewRefresh();
468
-
469
-
470
- function isValidSecret(s) {
471
- s = String(s || '').trim();
472
- if (!s) return false;
473
- if (ENV_SECRET && ENV_SECRET !== 'HF_TOKEN_REDACTED' && s === ENV_SECRET) return true;
474
- if (s === LOCAL_SECRET) return true;
475
- return false;
476
- }
477
-
478
- function money(n) { n = Number(n) || 0; return n ? n.toLocaleString('vi-VN') + 'đ' : 'LH'; }
479
- function norm(s) { return String(s || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[Đđ]/g, 'd').toLowerCase(); }
480
- function slugify(s) { return norm(s).replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 150) || 'san-pham-url'; }
481
- function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]; }); }
482
-
483
- /* ------------------------- MODAL OPEN/CLOSE ------------------------- */
484
- window.openAddProductModal = function () {
485
- var m = document.getElementById('addProductOverlay');
486
- if (!m) return;
487
- // If there is already an in-progress add (pending product, fields filled, or
488
- // images), preserve it on reopen so the owner does not lose their work when
489
- // they accidentally close the modal. A full reset is available via the
490
- // "Thêm SP khác" button.
491
- var hasState = (__pendingAdd != null) || (OCR_IMAGES && OCR_IMAGES.length > 0);
492
- if (!hasState) {
493
- // Fresh open: only clear the transient status, keep fields as-is.
494
- var stx = document.getElementById('addProductStatus');
495
- if (stx) { stx.textContent = ''; stx.className = 'add-product-status'; }
496
- }
497
- m.classList.add('open');
498
- m.style.display = 'flex';
499
- var hint = document.getElementById('addProductHintText');
500
- if (hint) hint.textContent = 'Mã truy cập mặc định: ' + LOCAL_SECRET + ' (dùng cho chủ cửa hàng)';
501
- };
502
- // Full reset so the owner can add a brand-new product after a successful save.
503
- window.resetAddProductForm = function () {
504
- OCR_IMAGES = [];
505
- var prv = document.getElementById('addImgPreview');
506
- if (prv) prv.innerHTML = '';
507
- var ta = document.getElementById('addPasteInput');
508
- if (ta) ta.value = '';
509
- var fields = document.getElementById('addOcrFields');
510
- if (fields) fields.style.display = 'none';
511
- ['addOcrName','addOcrSku','addOcrPrice','addOcrBrand','addOcrCat','addOcrDesc','addOcrFeats','addOcrSpecs','addOcrNote'].forEach(function (id) {
512
- var el = document.getElementById(id); if (el) el.value = '';
513
- });
514
- var subBtn = document.getElementById('addProductSubmit');
515
- if (subBtn) { subBtn.textContent='Thêm SP'; subBtn.removeAttribute('data-confirm'); subBtn.disabled=false; }
516
- var moreBtn = document.getElementById('addMoreBtn');
517
- if (moreBtn) moreBtn.style.display='none';
518
- var pvw = document.getElementById('addPreviewWrap');
519
- if (pvw) pvw.style.display='none';
520
- var bb = document.getElementById('addBackBtn');
521
- if (bb) bb.style.display='none';
522
- __pendingAdd = null;
523
- var st = document.getElementById('addProductStatus');
524
- if (st) { st.textContent = ''; st.className = 'add-product-status'; }
525
- var urlEl = document.getElementById('addProductUrl');
526
- if (urlEl) urlEl.value = '';
527
- var bak = document.getElementById('addPvGallery');
528
- if (bak) bak.innerHTML = '';
529
- window.renderImgPreview && renderImgPreview();
530
- };
531
- // Clear — wipe all extracted/typed fields (images keep, so user can rescan).
532
- window.clearAddProductForm = function () {
533
- ['addOcrName','addOcrSku','addOcrPrice','addOcrBrand','addOcrCat','addOcrDesc','addOcrFeats','addOcrSpecs'].forEach(function (id) {
534
- var el = document.getElementById(id); if (el) el.value = '';
535
- });
536
- var fields = document.getElementById('addOcrFields');
537
- if (fields) fields.style.display = 'none';
538
- var pvw = document.getElementById('addPreviewWrap');
539
- if (pvw) pvw.style.display = 'none';
540
- var bb = document.getElementById('addBackBtn');
541
- if (bb) bb.style.display = 'none';
542
- var subBtn = document.getElementById('addProductSubmit');
543
- if (subBtn) { subBtn.textContent='Thêm SP'; subBtn.removeAttribute('data-confirm'); subBtn.disabled=false; }
544
- __pendingAdd = null;
545
- var st = document.getElementById('addProductStatus');
546
- if (st) { st.textContent = ''; st.className = 'add-product-status'; }
547
- };
548
- window.closeAddProductModal = function () {
549
- var m = document.getElementById('addProductOverlay');
550
- if (!m) return;
551
- m.classList.remove('open');
552
- m.style.display = 'none';
553
- };
554
- function bindOverlay() {
555
- var m = document.getElementById('addProductOverlay');
556
- if (!m || m.getAttribute('data-vai-close-bound')) return;
557
- m.setAttribute('data-vai-close-bound', '1');
558
- m.addEventListener('click', function (e) { if (e.target === m) window.closeAddProductModal(); });
559
- }
560
- if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bindOverlay);
561
- else bindOverlay();
562
-
563
- /* Parse the specs textarea (one "Key: Value" per line) back into an object. */
564
- function parseSpecsText() {
565
- var txt = (document.getElementById('addOcrSpecs') || {}).value || '';
566
- var specs = {};
567
- txt.split(/\n+/).forEach(function (line) {
568
- var m = line.match(/^([^::]{1,60})\s*[::]\s*(.+)$/);
569
- if (m) { var k = m[1].trim(), v = m[2].trim(); if (k && v) specs[k] = v; }
570
- });
571
- return specs;
572
- }
573
-
574
- /* Effective API token: prefer the validated secret the owner typed, so the
575
- persist/OCR/scrape token always matches what the backend accepts even if
576
- the bare default differs from the scraper's configured VAISTUDIO secret. */
577
- function apiToken() {
578
- var el = document.getElementById('addProductSecret');
579
- var v = String((el && el.value) || '').trim();
580
- return v ? v : SCRAPER_TOKEN;
581
- }
582
-
583
- /* ------------------------- PERSISTENCE ------------------------- */
584
- function persistUrlAdded(list) { try { localStorage.setItem('vai_url_persist', JSON.stringify(list)); } catch (e) {} }
585
- function loadUrlAdded() {
586
- try { var v = JSON.parse(localStorage.getItem('vai_url_persist') || '[]'); return Array.isArray(v) ? v : []; }
587
- catch (e) { return []; }
588
- }
589
-
590
- /* ------------------------- FETCH (scraper API + CORS proxies) ------------------------- */
591
- async function scrapeViaApi(url) {
592
- // Server-side scraper: returns parsed JSON. No CORS, reliable.
593
- try {
594
- var r = await fetch(SCRAPER + '?url=' + encodeURIComponent(url) + '&token=' + encodeURIComponent(apiToken()), { cache: 'no-store' });
595
- if (r.ok) {
596
- var data = await r.json();
597
- if (data && (data.name || data.sku || data.gallery || data.price)) {
598
- // normalize server result to the same shape as parseProduct()
599
- return {
600
- name: data.name || '', sku: data.sku || '', img: data.img || '',
601
- gallery: Array.isArray(data.gallery) ? data.gallery.filter(Boolean) : [],
602
- desc: data.desc || data.summary || '', price: data.price || '',
603
- pn: Number(data.pn) || 0, specs: data.specs || {}, brand: data.brand || ''
604
- };
605
- }
606
- }
607
- } catch (e) {}
608
- return null;
609
- }
610
-
611
- async function fetchHtml(url) {
612
- // Try direct first (works for same-site); then proxies.
613
- var attempts = [url];
614
- for (var i = 0; i < PROXIES.length; i++) attempts.push(PROXIES[i](url));
615
- var lastErr = null;
616
- for (var j = 0; j < attempts.length; j++) {
617
- try {
618
- var r = await fetch(attempts[j], { cache: 'no-store' });
619
- if (r.ok) {
620
- var t = await r.text();
621
- if (t && t.length > 100) return t;
622
- }
623
- } catch (e) { lastErr = e; }
624
- }
625
- throw lastErr || new Error('Không tải được trang (CORS/network).');
626
- }
627
-
628
- /* ------------------------- PARSER ------------------------- */
629
- function cleanUrl(u, base) {
630
- if (!u) return '';
631
- u = String(u).trim().replace(/[\u00a0]/g, ' ');
632
- if (!u) return '';
633
- // doubled scheme: 'https://www.x.vnhttps://x.vn/...'
634
- var m = u.match(/^(https?:\/\/[^/]+)(https?:\/\/\S+)$/);
635
- if (m) u = m[2];
636
- // absolute
637
- if (/^https?:\/\//i.test(u)) return u;
638
- // protocol-relative
639
- if (/^\/\//.test(u)) return (base && base.match(/^https?/i)[0] || 'https') + ':' + u;
640
- // relative
641
- if (base) {
642
- try { return new URL(u, base).href; } catch (e) {}
643
- }
644
- return u;
645
- }
646
-
647
- function getMeta(doc, property) {
648
- var el = doc.querySelector('meta[property="' + property + '"]') ||
649
- doc.querySelector('meta[name="' + property + '"]');
650
- return el ? (el.getAttribute('content') || '') : '';
651
- }
652
-
653
- function extractJsonLd(html) {
654
- var re = /<script[^>]*type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/gi;
655
- var m, best = null;
656
- while ((m = re.exec(html))) {
657
- try {
658
- var data = JSON.parse(m[1]);
659
- var arr = Array.isArray(data) ? data : [data];
660
- for (var k = 0; k < arr.length; k++) {
661
- var item = arr[k];
662
- if (!item) continue;
663
- var type = String(item['@type'] || '').toLowerCase();
664
- var isProduct = type.indexOf('product') >= 0 || (item.name && (item.offers || item.image));
665
- if (isProduct) {
666
- var price = item.offers && (item.offers.price != null ? item.offers.price : (item.offers.lowPrice != null ? item.offers.lowPrice : null));
667
- best = {
668
- name: item.name, sku: item.sku,
669
- brand: (item.brand && (item.brand.name || item.brand)) || '',
670
- image: Array.isArray(item.image) ? item.image[0] : item.image,
671
- price: price, priceCurrency: item.offers && item.offers.priceCurrency,
672
- description: item.description
673
- };
674
- break;
675
- }
676
- }
677
- if (best) break;
678
- } catch (e) {}
679
- }
680
- return best;
681
- }
682
-
683
- function extractPrice(html) {
684
- var price = '', pn = 0;
685
- var m = html.match(/(?:property|itemprop)="product:price:amount"\s+content="([^"]+)"/i);
686
- if (m) pn = parseInt(String(m[1]).replace(/[^\d]/g, ''), 10);
687
- if (!pn) {
688
- var pm = html.match(/Giá:?\s*<\/span>?\s*([\d.,\s]+)\s*(VNĐ|VND|đ|₫|Đ)/i);
689
- if (pm) pn = parseInt(pm[1].replace(/[^\d]/g, ''), 10);
690
- }
691
- if (!pn) {
692
- var am = html.match(/(\d{1,3}(?:[.,]\d{3}){1,3})\s*(VNĐ|VND|đ|₫)/i);
693
- if (am) pn = parseInt(am[1].replace(/[^\d]/g, ''), 10);
694
- }
695
- if (pn) price = money(pn);
696
- return { price: price, pn: pn };
697
- }
698
-
699
- function extractSpecs(html) {
700
- var specs = {};
701
- var lineRe = /<div class="row[^"]*pd-line[^"]*">\s*<div[^>]*>\s*<h4>\s*([^<]+?)\s*<\/h4>\s*<\/div>\s*<div[^>]*>\s*(?:<h5[^>]*class="code"[^>]*>([^<]+?)<\/h5>|<p>\s*([^<]+?)\s*<\/p>)\s*<\/div>/gi;
702
- var lm;
703
- while ((lm = lineRe.exec(html))) {
704
- var k = String(lm[1] || '').trim();
705
- var v = String(lm[2] || lm[3] || '').trim();
706
- if (k && v && k !== 'Tên sản phẩm' && k !== 'Mã số') specs[k] = v;
707
- }
708
- return specs;
709
- }
710
-
711
- function extractGallery(html, base, primaryImg) {
712
- var gallery = [];
713
- var gre = /data-fancybox="gallery"[^>]*href="([^"]+)"/gi, gm;
714
- while ((gm = gre.exec(html))) { var u = cleanUrl(gm[1], base); if (u && gallery.indexOf(u) < 0) gallery.push(u); }
715
- var imgRe = /<a[^>]*data-fancybox="gallery"[^>]*>[\s\S]*?<img[^>]*src="([^"]+)"/gi, im;
716
- while ((im = imgRe.exec(html))) { var u2 = cleanUrl(im[1], base); if (u2 && gallery.indexOf(u2) < 0) gallery.push(u2); }
717
- if (primaryImg && gallery.indexOf(primaryImg) < 0) gallery.unshift(primaryImg);
718
- return gallery;
719
- }
720
-
721
- function parseProduct(html, pageUrl) {
722
- var doc = new DOMParser().parseFromString(html, 'text/html');
723
- var ogTitle = getMeta(doc, 'og:title');
724
- var ogDesc = getMeta(doc, 'og:description');
725
- var ogImg = getMeta(doc, 'og:image');
726
- if (!ogImg) ogImg = getMeta(doc, 'twitter:image');
727
- var ld = extractJsonLd(html);
728
-
729
- var tp = (html.match(/Tên sản phẩm<\/h4>[\s\S]{0,300}?<p[^>]*>\s*([^<]+?)\s*<\/p>/i) || [])[1] || '';
730
- var msku = (html.match(/Mã số<\/h4>[\s\S]{0,300}?(?:<h5[^>]*class="code"[^>]*>|<p[^>]*>)\s*([^<]+?)\s*(?:<\/h5>|<\/p>)/i) || [])[1] || '';
731
- var name = (tp && tp.trim()) || ogTitle || (ld && ld.name) || (ogTitle && ogTitle.split(/[|–—-]/)[0].trim()) || 'Sản phẩm';
732
- name = String(name).trim();
733
- var priceInfo = extractPrice(html);
734
- if (!priceInfo.pn && ld && ld.price != null) {
735
- var lp = parseInt(String(ld.price).replace(/[^\d]/g, ''), 10);
736
- if (lp) { priceInfo.pn = lp; priceInfo.price = money(lp); }
737
- }
738
-
739
- var primaryImg = cleanUrl(ogImg, pageUrl);
740
- var gallery = extractGallery(html, pageUrl, primaryImg);
741
- if (!primaryImg && gallery.length) primaryImg = gallery[0];
742
-
743
- var sku = (msku && msku.trim()) || (ld && ld.sku) || '';
744
- if (!sku) sku = (name.match(/[A-Z0-9]{3,}/) || [''])[0];
745
-
746
- var desc = ogDesc || (ld && ld.description) || '';
747
- desc = String(desc || '').replace(/\s*\|\s*[^|]*$/, '');
748
-
749
- var specs = extractSpecs(html);
750
- var brand = getMeta(doc, 'og:site_name') || (ld && ld.brand) || '';
751
- if (!brand) { try { var host = new URL(pageUrl).hostname; brand = host.replace(/^www\./, '').split('.')[0]; } catch (e) {} }
752
- brand = String(brand || '').replace(/^www\./i, '');
753
-
754
- return {
755
- name: name, sku: sku, img: primaryImg, gallery: gallery,
756
- desc: desc, price: priceInfo.price, pn: priceInfo.pn,
757
- specs: specs, brand: brand, ld: ld
758
- };
759
- }
760
-
761
- /* ------------------------- SUBMIT ------------------------- */
762
- /* Two-step add flow:
763
- Step 1 (prepare): scrape (URL) or build (OCR) the product info, fill the
764
- OCR fields + full preview panel. Button switches to "Xác nhận thêm SP".
765
- Step 2 (confirm): persist + add to storefront, then close. */
766
- var __pendingAdd = null;
767
-
768
- window.submitAddProductUrl = async function () {
769
- var st = document.getElementById('addProductStatus');
770
- var secretEl = document.getElementById('addProductSecret');
771
- var urlEl = document.getElementById('addProductUrl');
772
- var submitBtn = document.getElementById('addProductSubmit');
773
-
774
- function status(msg, cls) { if (st) { st.textContent = msg; st.className = 'add-product-status ' + (cls || ''); } }
775
- function restoreBtn() { if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Thêm SP'; } }
776
-
777
- var secret = (secretEl && secretEl.value || '').trim();
778
- if (!isValidSecret(secret)) {
779
- status('❌ Mã truy cập không đúng. Nhập mặc định: ' + LOCAL_SECRET, 'err');
780
- return;
781
- }
782
-
783
- var raw = (urlEl && urlEl.value || '').trim();
784
- var pageUrl = '';
785
- if (raw) {
786
- try { pageUrl = new URL(raw, SITE + '/').href; } catch (e) {
787
- status('❌ URL không hợp lệ.', 'err'); return;
788
- }
789
- }
790
-
791
- var ocrName = (document.getElementById('addOcrName') || {}).value || '';
792
- var ocrSku = (document.getElementById('addOcrSku') || {}).value || '';
793
- var ocrPrice = (document.getElementById('addOcrPrice') || {}).value || '';
794
- var ocrBrand = (document.getElementById('addOcrBrand') || {}).value || '';
795
- var usingOCR = OCR_IMAGES.length > 0;
796
-
797
- if (!raw && !usingOCR) { status('❌ Vui lòng nhập URL sản phẩm HOẶC dán/tải ảnh sản phẩm.', 'err'); return; }
798
-
799
- if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = '⏳ Đang phân tích...'; }
800
- var r = null, slug = '';
801
- try {
802
- if (usingOCR && ocrName) {
803
- // Build product directly from OCR + images
804
- var ocrP = parseInt(ocrPrice, 10) || 0;
805
- slug = slugify((ocrSku ? ocrSku + ' ' : '') + ocrName);
806
- r = {
807
- name: ocrName, sku: ocrSku || '', price: ocrP ? money(ocrP) : '', pn: ocrP,
808
- img: OCR_IMAGES[0], gallery: OCR_IMAGES.slice(), desc: '',
809
- brand: ocrBrand || '', specs: {}
810
- };
811
- r.imgs_data = OCR_IMAGES.slice();
812
- } else {
813
- if (!pageUrl && usingOCR) {
814
- status('❌ Đã có ảnh nhưng chưa có tên sản phẩm. Bấm "Trích xuất thông tin từ ảnh (OCR)" để lấy tên, hoặc nhập tên ở bước 3.', 'err');
815
- if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Thêm SP'; }
816
- return;
817
- }
818
- status('⏳ Bước 1: Đang tải & phân tích (scrape) sản phẩm…' + (usingOCR ? ' (sẽ dùng ảnh kèm theo)' : ''));
819
- if (!pageUrl) throw new Error('Không có URL để phân tích.');
820
- r = await scrapeViaApi(pageUrl);
821
- if (!r || !r.name || r.name === 'Sản phẩm') {
822
- var html = await fetchHtml(pageUrl);
823
- r = parseProduct(html, pageUrl);
824
- }
825
- }
826
-
827
- if (!r || !r.name || r.name === 'Sản phẩm') throw new Error('Không lấy được tên sản phẩm.');
828
- if (!r.price && !r.img) throw new Error('Không trích xuất được giá/hình. Kiểm tra URL.');
829
-
830
- slug = slugify((r.sku ? r.sku + ' ' : '') + r.name);
831
-
832
- // Populate OCR fields with scraped/build info so the owner can edit before confirm
833
- if (document.getElementById('addOcrName')) document.getElementById('addOcrName').value = r.name || '';
834
- if (document.getElementById('addOcrSku')) document.getElementById('addOcrSku').value = r.sku || '';
835
- if (document.getElementById('addOcrPrice')) document.getElementById('addOcrPrice').value = r.pn || '';
836
- if (document.getElementById('addOcrBrand')) document.getElementById('addOcrBrand').value = r.brand || '';
837
- if (document.getElementById('addOcrSpecs') && r.specs && typeof r.specs === 'object') {
838
- document.getElementById('addOcrSpecs').value = Object.keys(r.specs).map(function (k) { return k + ': ' + r.specs[k]; }).join('\n');
839
- }
840
- var ocrf = document.getElementById('addOcrFields');
841
- if (ocrf) ocrf.style.display = 'block';
842
-
843
- // Expose scraped URL gallery images in the editable image list so the
844
- // owner can review/delete/reorder them before confirming.
845
- if (r.gallery && r.gallery.length && !OCR_IMAGES.length) {
846
- OCR_IMAGES = r.gallery.slice();
847
- renderImgPreview();
848
- }
849
-
850
- // Store pending product so confirm step uses the (possibly edited) fields + images
851
- __pendingAdd = { r: r, slug: slug, pageUrl: pageUrl, usingOCR: usingOCR };
852
-
853
- // Show full preview + enter confirm mode
854
- enterPreviewMode();
855
- status('Bước 1 xong: đã trích xuất thông tin & hiện preview ở trên. Bạn có thể chỉnh sửa, sau đó bấm "Xác nhận thêm SP" để hoàn tất.', 'ok');
856
- try { if (st) st.scrollIntoView({behavior:'smooth',block:'nearest'}); } catch(e){}
857
- } catch (err) {
858
- status('❌ Lỗi phân tích: ' + (err && err.message || err), 'err');
859
- if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Thêm SP'; }
860
- }
861
- };
862
-
863
- /* ---- Step 2: confirm & persist (uses current edited fields + images) ---- */
864
- window.confirmAddProduct = async function () {
865
- var st = document.getElementById('addProductStatus');
866
- var submitBtn = document.getElementById('addProductSubmit');
867
- function status(msg, cls) { if (st) { st.textContent = msg; st.className = 'add-product-status ' + (cls || ''); } }
868
-
869
- if (!__pendingAdd) { status('❌ Chưa có sản phẩm để xác nhận.', 'err'); return; }
870
- var prev = __pendingAdd;
871
-
872
- // Read current (possibly edited) values
873
- var name = (document.getElementById('addOcrName') || {}).value || '';
874
- var sku = (document.getElementById('addOcrSku') || {}).value || '';
875
- var price = parseInt((document.getElementById('addOcrPrice') || {}).value, 10) || 0;
876
- var brand = (document.getElementById('addOcrBrand') || {}).value || '';
877
- if (!name) { status('❌ Chưa có tên sản phẩm.', 'err'); return; }
878
-
879
- var specsObj = parseSpecsText();
880
- if (!Object.keys(specsObj).length) specsObj = prev.r.specs || {};
881
- var featsArr = (function(){ var v=((document.getElementById('addOcrFeats')||{}).value||'').split(/\n+/).map(function(s){return s.trim();}).filter(Boolean); return v; })();
882
- if (!featsArr.length && prev.r.feats && prev.r.feats.length) featsArr = prev.r.feats.slice();
883
- var r = {
884
- name: name, sku: sku, price: price ? money(price) : '', pn: price,
885
- img: OCR_IMAGES[0] || prev.r.img, gallery: OCR_IMAGES.length ? OCR_IMAGES.slice() : (prev.r.gallery || []),
886
- desc: (document.getElementById('addOcrDesc') || {}).value || prev.r.desc || '',
887
- brand: brand, specs: specsObj, feats: featsArr
888
- };
889
- r.imgs_data = OCR_IMAGES.length ? OCR_IMAGES.slice() : undefined;
890
- var slug = slugify((sku ? sku + ' ' : '') + name);
891
-
892
- // Merge edited OCR fields that were scraped but not otherwise edited
893
- if (!OCR_IMAGES.length && !r.gallery.length) r.gallery = r.img ? [r.img] : [];
894
- if (r.img && r.gallery.indexOf(r.img) < 0) r.gallery.unshift(r.img);
895
-
896
- if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = '⏳ Đang lưu…'; }
897
- try {
898
- var baseRec = {
899
- n: name, name: name, sku: sku, mod: sku, model: sku,
900
- slug: slug, p: r.price, price: r.price, pn: r.pn, priceNum: r.pn,
901
- i: r.img, image: r.img, imgs: r.gallery, images: r.gallery,
902
- c: (document.getElementById('addOcrCat') || {}).value || (prev.usingOCR ? 'Sản phẩm thêm qua OCR' : 'Sản phẩm thêm qua URL'),
903
- cs: 'san-pham-them-qua-url', ci: 'fa-box',
904
- sum: r.desc, summary: r.desc, desc: r.desc,
905
- specs: r.specs, feats: r.feats || [], brand: brand,
906
- _sourceUrl: prev.pageUrl, _url_added: true
907
- };
908
-
909
- var list = loadUrlAdded();
910
- var exists = list.findIndex(function (x) { return x.slug === slug || (x.sku && sku && x.sku === sku); });
911
- if (exists >= 0) list[exists] = baseRec; else list.push(baseRec);
912
- persistUrlAdded(list);
913
-
914
- if (typeof window.D !== 'undefined' && Array.isArray(window.D)) {
915
- var dup = window.D.find(function (p) { return p.slug === slug || (p.sku && sku && p.sku === sku); });
916
- if (dup) { var k = window.D.indexOf(dup); window.D[k] = Object.assign({}, dup, baseRec); }
917
- else { window.D.unshift(Object.assign({}, window.D[0] || {}, baseRec)); }
918
- }
919
- if (typeof window.initFilters === 'function') { try { window.initFilters(); } catch (e) {} }
920
- if (typeof window.render === 'function') { try { window.render(); } catch (e) {} }
921
-
922
- var persisted = false;
923
- var persistErr = '';
924
- try {
925
- if (!window.PERSIST_API) window.PERSIST_API = PERSIST_API;
926
- var ps = await fetch(window.PERSIST_API, {
927
- method: 'POST', headers: { 'Content-Type': 'application/json' },
928
- body: JSON.stringify({
929
- token: apiToken(),
930
- products: [{
931
- name: name, sku: sku, slug: slug, price: r.price, pn: r.pn,
932
- img: r.img, gallery: r.gallery, desc: r.desc, brand: brand,
933
- url: prev.pageUrl, specs: r.specs || {}, feats: r.feats || [],
934
- imgs_data: r.imgs_data
935
- }]
936
- })
937
- });
938
- if (ps.ok) { var pj = await ps.json(); persisted = !!(pj && pj.persisted); if (!persisted) persistErr = (pj && (pj.detail || pj.error)) || 'server rejected'; }
939
- else { persistErr = 'HTTP ' + ps.status + ': ' + ((await ps.text()) || '').slice(0,150); }
940
- } catch (e) { persistErr = (e && e.message) || String(e); }
941
- if (!persisted) persistUrlAdded(list);
942
-
943
- status('✅ Đã thêm: "' + name + '" (Mã ' + (sku || '?') + ')' +
944
- '\nGiá: ' + (r.price || 'Liên hệ') + ' | ' + (r.gallery.length || 0) + ' ảnh' +
945
- '\nSEO: /san-pham/' + slug + '/index.html' +
946
- (persisted ? '\n✓ Đã lưu vĩnh viễn (không mất khi tải lại trang).' : ('\n⚠ Lưu tạm thời (localStorage) — server persist lỗi: ' + (persistErr || '?') + '. Bấm "Thêm SP" lần nữa để thử.' + '\nLưu ý: sản phẩm vẫn hiển thị trên máy này.')),
947
- 'ok');
948
- __pendingAdd = null;
949
- // Show the "Thêm SP khác" button so the owner can keep adding.
950
- var moreBtn = document.getElementById('addMoreBtn');
951
- if (moreBtn) moreBtn.style.display = 'inline-flex';
952
- setTimeout(function(){ window.closeAddProductModal(); document.getElementById('addProductSubmit').textContent='Thêm SP'; document.getElementById('addProductSubmit').removeAttribute('data-confirm'); }, 2600);
953
- } catch (err) {
954
- status('❌ Lỗi: ' + (err && err.message || err), 'err');
955
- if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = '✅ Xác nhận thêm SP'; }
956
- }
957
- };
958
-
959
- // Show preview panel + enter confirm mode (shared by prepare & OCR paths).
960
- function enterPreviewMode() {
961
- buildAddPreview();
962
- var wrap = document.getElementById('addPreviewWrap');
963
- if (wrap) wrap.style.display = 'block';
964
- var bb = document.getElementById('addBackBtn');
965
- if (bb) bb.style.display = 'inline-flex';
966
- var subBtn = document.getElementById('addProductSubmit');
967
- if (subBtn) { subBtn.textContent = '✅ Xác nhận thêm SP'; subBtn.setAttribute('data-confirm','1'); subBtn.disabled=false; }
968
- }
969
-
970
- /* ---- Back from preview: return to URL/OCR input step ---- */
971
- window.goBackFromPreview = function () {
972
- __pendingAdd = null;
973
- var wrap = document.getElementById('addPreviewWrap');
974
- if (wrap) wrap.style.display = 'none';
975
- var bb = document.getElementById('addBackBtn');
976
- if (bb) bb.style.display = 'none';
977
- var subBtn = document.getElementById('addProductSubmit');
978
- if (subBtn) { subBtn.textContent = 'Thêm SP'; subBtn.removeAttribute('data-confirm'); subBtn.disabled = false; }
979
- var st = document.getElementById('addProductStatus');
980
- if (st) { st.textContent = ''; st.className = 'add-product-status'; }
981
- // scroll back up to the input section
982
- var urlEl = document.getElementById('addProductUrl');
983
- if (urlEl) { try { urlEl.scrollIntoView({behavior:'smooth',block:'start'}); } catch(e){} }
984
- };
985
-
986
- console.log('[VAI AddProd Fix v2] Loaded: modal + secret + external-URL scraping active.');
987
- })();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
added_img/bep-tu-2-vung-nau-boss-bki689n-0.jpg DELETED
Binary file (17.2 kB)
 
added_img/bt1-big-test-0.jpg DELETED

Git LFS Details

  • SHA256: df681a15878e5a26f99715e9feedc432ca38e37f62152f1b1d526696876cb39f
  • Pointer size: 132 Bytes
  • Size of remote file: 2.25 MB
added_img/gx1-test-grob-0.jpg DELETED
Binary file (75 Bytes)
 
ai-search-v2.js DELETED
@@ -1,1167 +0,0 @@
1
- /**
2
- * AI Search v2.1 — Semantic Search + Synonym Expansion + Technical Specs
3
- * ===================================================
4
- * Major upgrades over v2.0:
5
- * - Semantic fuzzy matching: handles typos, abbreviations, partial words
6
- * - Synonym expansion: "bep tu" also matches "bếp điện từ", "cooktop"...
7
- * - Enhanced technical spec search: extracts more specs from unstructured desc
8
- * - Better ranking: name match > brand match > category match > description match
9
- * - Fuzzy brand detection: "Grob" matches "gro", "gr", "rob"...
10
- * - Smart dimension extraction from unstructured text
11
- *
12
- * Integrates on top of ai-search.js — patches window.aiSearch
13
- *
14
- * v2.1 — July 2026
15
- */
16
-
17
- (function() {
18
- 'use strict';
19
-
20
- // ── Vietnamese normalization ──────────────────────────────────────────────────
21
- function _norm(s) {
22
- return String(s || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/đ/g, 'd').replace(/Đ/g, 'D');
23
- }
24
-
25
- function _normC(s) {
26
- return _norm(s).replace(/[.\-_\/ ]+/g, '');
27
- }
28
-
29
- // ── Semantic Synonym Map ─────────────────────────────────────────────────────
30
- // Maps search terms to expanded synonyms for better matching
31
- const SYNONYM_MAP = {
32
- // Category synonyms
33
- 'bep tu': ['bep tu', 'iep tu', 'iep dien tu', 'iep cam ung', 'bep dien tu', 'bep cam ung', 'induction', 'bep tu ep', 'bep tu am'],
34
- 'bep gas': ['bep gas', 'iep gas', 'iep ga', 'gas am', 'bep ga', 'bep tu gas'],
35
- 'may hut mui': ['may hut mui', 'mai hut mui', 'hut mui', 'hut mui', 'hut khoi', 'khu mui', 'may hut khoi', 'may hut k'],
36
- 'may rua chen': ['may rua chen', 'may rua bat', 'may rua che', 'rua chen', 'rua bat', 'dishwasher'],
37
- 'lo nuong': ['lo nuong', 'lo am tu', 'lo nuong am', 'oven', 'lo an tu'],
38
- 'lo vi song': ['lo vi song', 'lo vi', 'lo song', 'microwave'],
39
- 'chau rua': ['chau rua', 'chau rua chen', 'bon rua', 'chậu rửa', 'sink', 'chao rua'],
40
- 'voi rua': ['voi rua', 'voi rua chen', 'voi nong lanh', 'voi', 'faucet', 'vòi rửa'],
41
- 'tu lanh': ['tu lanh', 'tu lan', 'tu ruou', 'lanh', 'tu lạn', 'refrigerator'],
42
- 'may giat': ['may giat', 'may giât', 'may say', 'washing machine', 'say chen'],
43
- 'noi chien': ['noi chien', 'noi chiên', 'noi chiên khong dau', 'kien khong dau', 'air fryer'],
44
- 'dieu hoa': ['dieu hoa', 'dieu hoa', 'may lanh', 'dieu hoa', 'air conditioner'],
45
- 'khoa cua': ['khoa cua', 'khoa van tay', 'khoa thong minh', 'khoa dien tu', 'smart lock', 'khoa cua'],
46
- 'tay nam': ['tay nam', 'tay nam', 'num tu', 'tay cam', 'handle', 'pull handle'],
47
- 'tai lieu': ['tai lieu', 'tai lieu', 'catalog', 'catalogue', 'brochure'],
48
-
49
- // Appliance type synonyms
50
- 'ep': ['iep', 'bep', 'ep', 'bep'],
51
- 'iep': ['iep', 'bep', 'iep', 'bep'],
52
- 'hut mui': ['hut mui', 'hut khoi', 'khu mui', 'may hut', 'mai hut'],
53
- 'rua chen': ['rua chen', 'rua bat', 'rua che', 'may rua'],
54
- 'nuong': ['nuong', 'nuong', 'oven', 'lo nuong', 'lo an'],
55
- 'lanh': ['lanh', 'tu lanh', 'tu lan', 'refrigerator', 'lanh'],
56
- 'giat': ['giat', 'giât', 'washing', 'may giat'],
57
- 'chiên': ['chien', 'chien', 'air fryer', 'noi chiên'],
58
-
59
- // Installation type synonyms
60
- 'am': ['am', 'am ban', 'lap am', 'flush mount', 'undermount', 'duoi mat'],
61
- 'duong': ['duong', 'lap duong', 'top mount', 'drop in', 'len mat'],
62
-
63
- // Material synonyms
64
- 'inox': ['inox', 'thép không rỉ', 'stainless steel', 'steel', 'sus', 'thép inox', 'kim loại'],
65
- 'nhôm': ['nhôm', 'nhom', 'aluminum', 'aluminium', 'hợp kim nhôm', 'kim loại'],
66
- 'kính': ['kính', 'kinh', 'glass', 'tampered', 'tempered glass'],
67
- 'gốm': ['gốm', 'gom', 'ceramic', 'sứ', 'gốm sứ'],
68
- 'thép': ['thép', 'thep', 'steel', 'sơn tĩnh điện'],
69
- 'nhựa': ['nhựa', 'nhua', 'plastic', 'abs', 'pp'],
70
-
71
- // Brand synonyms
72
- 'malloca': ['malloca', 'maloca'],
73
- 'grob': ['grob', 'gro'],
74
- 'eurogold': ['eurogold', 'euro gold', 'eurogold'],
75
- 'canzy': ['canzy'],
76
- 'demax': ['demax'],
77
- 'hafele': ['hafele', 'hafle', 'hafe'],
78
- 'bosch': ['bosch', 'bosh'],
79
- 'panasonic': ['panasonic', 'pana'],
80
- 'blanco': ['blanco'],
81
- 'teka': ['teka'],
82
- 'electrolux': ['electrolux', 'electrolux'],
83
- 'samsung': ['samsung', 'sam'],
84
- 'lg': ['lg', 'el gi'],
85
- 'toshiba': ['toshiba', 'tosiba'],
86
- 'sharp': ['sharp', 'sacp'],
87
- 'piaggini': ['piaggini', 'piacinini'],
88
- 'piranha': ['piranha'],
89
- 'euro': ['euro', 'eurogold'],
90
- };
91
-
92
- // ── Build expanded synonym index ─────────────────────────────────────────────
93
- function buildSynonymIndex() {
94
- var idx = {};
95
- for (var term in SYNONYM_MAP) {
96
- var synonyms = SYNONYM_MAP[term];
97
- for (var i = 0; i < synonyms.length; i++) {
98
- var s = synonyms[i];
99
- if (s.length < 2) continue;
100
- // Also add bigrams
101
- idx[s] = [term];
102
- var grams = [];
103
- for (var j = 0; j < s.length - 1; j++) {
104
- grams.push(s.substring(j, j + 2));
105
- }
106
- for (var g = 0; g < grams.length; g++) {
107
- if (grams[g].length >= 3) {
108
- if (!idx[grams[g]]) idx[grams[g]] = [];
109
- if (idx[grams[g]].indexOf(term) === -1) {
110
- idx[grams[g]].push(term);
111
- }
112
- }
113
- }
114
- }
115
- }
116
- return idx;
117
- }
118
-
119
- var _synonymIndex = buildSynonymIndex();
120
-
121
- // ── Expand query with synonyms ───────────────────────────────────────────────
122
- function expandSynonyms(query) {
123
- var n = _norm(query);
124
- var terms = n.split(/\s+/).filter(function(w) { return w.length >= 2; });
125
- var expanded = {};
126
-
127
- // Add original tokens
128
- for (var i = 0; i < terms.length; i++) {
129
- expanded[terms[i]] = true;
130
- }
131
-
132
- // Expand each token with synonyms
133
- for (var i = 0; i < terms.length; i++) {
134
- var t = terms[i];
135
- var matched = false;
136
-
137
- // Direct match
138
- if (_synonymIndex[t]) {
139
- for (var j = 0; j < _synonymIndex[t].length; j++) {
140
- expanded[_synonymIndex[t][j]] = true;
141
- }
142
- matched = true;
143
- }
144
-
145
- // Prefix match (first 3+ chars)
146
- if (!matched) {
147
- for (var key in _synonymIndex) {
148
- if (key.length >= 3 && key.indexOf(t) === 0) {
149
- for (var j = 0; j < _synonymIndex[key].length; j++) {
150
- expanded[_synonymIndex[key][j]] = true;
151
- }
152
- matched = true;
153
- break;
154
- }
155
- }
156
- }
157
-
158
- // Suffix/contains match (for abbreviations)
159
- if (!matched && t.length >= 3) {
160
- for (var key in _synonymIndex) {
161
- if (t.length >= 4 && key.indexOf(t.substring(0, 4)) !== -1) {
162
- for (var j = 0; j < _synonymIndex[key].length; j++) {
163
- expanded[_synonymIndex[key][j]] = true;
164
- }
165
- break;
166
- }
167
- if (t.length >= 5 && key.indexOf(t.substring(0, 5)) !== -1) {
168
- for (var j = 0; j < _synonymIndex[key].length; j++) {
169
- expanded[_synonymIndex[key][j]] = true;
170
- }
171
- break;
172
- }
173
- }
174
- }
175
- }
176
-
177
- return expanded;
178
- }
179
-
180
- // ── Fuzzy string matching helpers ────────────────────────────────────────────
181
- function levenshtein(a, b) {
182
- var m = a.length, n = b.length;
183
- if (m === 0) return n;
184
- if (n === 0) return m;
185
- var dp = [];
186
- for (var i = 0; i <= m; i++) { dp[i] = [i]; }
187
- for (var j = 0; j <= n; j++) { dp[0][j] = j; }
188
- for (var i = 1; i <= m; i++) {
189
- for (var j = 1; j <= n; j++) {
190
- var cost = (a[i-1] === b[j-1]) ? 0 : 1;
191
- dp[i][j] = Math.min(dp[i-1][j] + 1, dp[i][j-1] + 1, dp[i-1][j-1] + cost);
192
- }
193
- }
194
- return dp[m][n];
195
- }
196
-
197
- function fuzzyMatch(queryTokens, text) {
198
- var n = _norm(text);
199
- var expanded = expandSynonyms(queryTokens.join(' '));
200
- var expandedTerms = Object.keys(expanded);
201
-
202
- // Exact match (highest score)
203
- if (n.indexOf(_norm(queryTokens.join(' '))) !== -1) return { match: true, score: 100, type: 'exact' };
204
-
205
- // Individual token matches
206
- var hitCount = 0;
207
- var totalScore = 0;
208
-
209
- for (var i = 0; i < queryTokens.length; i++) {
210
- var qt = queryTokens[i];
211
- var tokenScore = 0;
212
-
213
- // Direct match
214
- if (n.indexOf(qt) !== -1) {
215
- tokenScore += 80;
216
- hitCount++;
217
- }
218
-
219
- // Synonym match
220
- var foundSynonym = false;
221
- for (var j = 0; j < expandedTerms.length; j++) {
222
- if (expandedTerms[j] !== qt && n.indexOf(expandedTerms[j]) !== -1) {
223
- tokenScore += 60;
224
- hitCount++;
225
- foundSynonym = true;
226
- break;
227
- }
228
- }
229
-
230
- // Fuzzy match (levenshtein)
231
- if (!foundSynonym && qt.length >= 3) {
232
- var bestDist = Infinity;
233
- var words = n.split(/[\s,._-]+/);
234
- for (var w = 0; w < words.length; w++) {
235
- var wlen = words[w].length;
236
- if (wlen < 3) continue;
237
- var maxLen = Math.max(qt.length, wlen);
238
- if (maxLen < 4) continue;
239
- var dist = levenshtein(qt, words[w]);
240
- if (dist < bestDist) bestDist = dist;
241
- }
242
- if (bestDist <= 2 && bestDist < qt.length * 0.4) {
243
- tokenScore += 40;
244
- hitCount++;
245
- }
246
- }
247
-
248
- // Substring match (word prefix)
249
- if (tokenScore === 0 && qt.length >= 3) {
250
- if (n.indexOf(qt.substring(0, 3)) !== -1) {
251
- tokenScore += 25;
252
- hitCount++;
253
- }
254
- }
255
-
256
- totalScore += tokenScore;
257
- }
258
-
259
- if (hitCount === 0) return { match: false, score: 0 };
260
-
261
- var ratio = hitCount / queryTokens.length;
262
- var combinedScore = totalScore * ratio;
263
-
264
- return { match: true, score: combinedScore, type: 'fuzzy' };
265
- }
266
-
267
- // ── Spec Search Index (from v2.0, optimized) ────────────────────────────────
268
-
269
- function buildSpecIndex(products) {
270
- if (!products || !products.length) return null;
271
-
272
- var index = {
273
- keywords: {},
274
- dimensions: [],
275
- prices: [],
276
- fullTexts: [],
277
- nameTexts: [],
278
- brandTexts: [],
279
- catTexts: [],
280
- count: products.length,
281
- };
282
-
283
- for (var i = 0; i < products.length; i++) {
284
- var p = products[i];
285
- if (!p) {
286
- index.fullTexts.push('');
287
- index.nameTexts.push('');
288
- index.brandTexts.push('');
289
- index.catTexts.push('');
290
- index.dimensions.push(null);
291
- index.prices.push(0);
292
- continue;
293
- }
294
-
295
- var specs = p.specs || {};
296
- var specText = '';
297
- if (specs && typeof specs === 'object') {
298
- specText = Object.entries(specs).map(function(e) { return e[0] + ' ' + e[1]; }).join(' ');
299
- }
300
- var fullText = _norm([p.name, p.cat, p.brand, p.summary || p.sum || '', p.desc || '', specText].join(' '));
301
- var nameText = _norm(p.name || '');
302
- var brandText = _norm(p.brand || '');
303
- var catText = _norm(p.cat || '');
304
-
305
- index.fullTexts.push(fullText);
306
- index.nameTexts.push(nameText);
307
- index.brandTexts.push(brandText);
308
- index.catTexts.push(catText);
309
-
310
- var tokens = fullText.split(/\s+/);
311
- var seen = {};
312
- for (var t = 0; t < tokens.length; t++) {
313
- var token = tokens[t].trim();
314
- if (!token || token.length < 2) continue;
315
- if (seen[token]) continue;
316
- seen[token] = 1;
317
- if (!index.keywords[token]) index.keywords[token] = [];
318
- if (index.keywords[token].length === 0 || index.keywords[token][index.keywords[token].length - 1] !== i) {
319
- index.keywords[token].push(i);
320
- }
321
- }
322
-
323
- // Bigrams
324
- for (var t = 0; t < tokens.length - 1; t++) {
325
- var bi = tokens[t] + ' ' + tokens[t+1];
326
- if (bi.length < 4) continue;
327
- if (seen[bi]) continue;
328
- seen[bi] = 1;
329
- if (!index.keywords[bi]) index.keywords[bi] = [];
330
- if (index.keywords[bi].length === 0 || index.keywords[bi][index.keywords[bi].length - 1] !== i) {
331
- index.keywords[bi].push(i);
332
- }
333
- }
334
-
335
- var dims = _extractDims(p);
336
- index.dimensions.push(dims);
337
- index.prices.push(p.priceNum || 0);
338
- }
339
-
340
- return index;
341
- }
342
-
343
- function _extractDims(p) {
344
- var result = {width: null, depth: null, height: null, weight: null, volume: null};
345
- var specs = p.specs || {};
346
- if (!specs || typeof specs !== 'object') return result;
347
-
348
- for (var key in specs) {
349
- var v = String(specs[key] || '');
350
- var kn = _norm(key);
351
- var vn = _norm(v);
352
-
353
- if (/^(r?[ôo]ng|ngang|width|w\b|chieu\s*rong|size\s*w)/.test(kn)) result.width = _parseDimValue(vn);
354
- if (/^(s[aà]u|depth|d\b|chieu\s*sau)/.test(kn)) result.depth = _parseDimValue(vn);
355
- if (/^(cao|height|h\b|chieu\s*cao)/.test(kn)) result.height = _parseDimValue(vn);
356
- if (/^(tro.?ng lu.?ng|khoi lu.?ng|weight|can nang)/.test(kn)) result.weight = _parseWeightValue(vn);
357
- if (/^(dung tich|the tich|dung.tich|volume|capacity|the.tich)/.test(kn)) result.volume = _parseVolumeValue(vn);
358
-
359
- var dimMatch = vn.match(/(?:r[ôo]ng|ngang|width)\D{0,8}(\d{2,4})\s*(?:mm|cm)?\s*(?:x|×|\*)\s*(?:s[aà]u|depth|d)\D{0,8}(\d{2,4})/);
360
- if (dimMatch) {
361
- result.width = _parseDimValue(dimMatch[1]);
362
- result.depth = _parseDimValue(dimMatch[2]);
363
- }
364
- var hMatch = vn.match(/(?:x|×|\*)\s*(?:cao|height|h)\D{0,8}(\d{2,4})\s*(?:mm|cm)?/);
365
- if (hMatch && result.height === null) result.height = _parseDimValue(hMatch[1]);
366
-
367
- var ktMatch = vn.match(/^(\d{2,4})\s*(?:x|×|\*)\s*(\d{2,4})(?:\s*(?:x|×|\*)\s*(\d{2,4}))?\s*(?:mm|cm)?$/);
368
- if (ktMatch && result.width === null) {
369
- result.width = _parseDimValue(ktMatch[1]);
370
- result.depth = _parseDimValue(ktMatch[2]);
371
- if (ktMatch[3]) result.height = _parseDimValue(ktMatch[3]);
372
- }
373
- }
374
-
375
- var nameAndSum = _norm((p.name || '') + ' ' + (p.summary || p.sum || ''));
376
- var ktMatch = nameAndSum.match(/(?:kich thuoc|size|kt)\D{0,12}(\d{2,4})\s*(?:x|×|\*)\s*(\d{2,4})(?:\s*(?:x|×|\*)\s*(\d{2,4}))?/);
377
- if (ktMatch && result.width === null) {
378
- result.width = _parseDimValue(ktMatch[1]);
379
- result.depth = _parseDimValue(ktMatch[2]);
380
- if (ktMatch[3]) result.height = _parseDimValue(ktMatch[3]);
381
- }
382
-
383
- return result;
384
- }
385
-
386
- function _parseDimValue(s) {
387
- var m = String(s).match(/(\d{2,4})/);
388
- if (!m) return null;
389
- var v = parseInt(m[1]);
390
- if (v < 200) v *= 10;
391
- if (v >= 200 && v <= 2500) return v;
392
- return null;
393
- }
394
-
395
- function _parseWeightValue(s) {
396
- var m = String(s).match(/([\d.,]+)\s*(kg|kgs|kilogam|gram|g\b|gam)?/i);
397
- if (!m) return null;
398
- var v = parseFloat(m[1].replace(',', '.'));
399
- var unit = (m[2] || '').toLowerCase();
400
- if (unit === 'g' || unit === 'gram' || unit === 'gam') v /= 1000;
401
- return v > 0 ? v : null;
402
- }
403
-
404
- function _parseVolumeValue(s) {
405
- var m = String(s).match(/([\d.,]+)\s*(l|lit|litre|liter|lt)/i);
406
- if (!m) {
407
- m = String(s).match(/([\d.,]+)\s*(ml|millilit)/i);
408
- if (m) return parseFloat(m[1].replace(',', '.')) / 1000;
409
- return null;
410
- }
411
- return parseFloat(m[1].replace(',', '.'));
412
- }
413
-
414
- // ── Material, Color, Shape, Install dictionaries (from v2.0) ──
415
-
416
- const MATERIALS = {
417
- 'inox 304': ['inox 304', '304', 'stainless steel 304', 'sus 304'],
418
- 'inox 201': ['inox 201', '201'],
419
- 'inox': ['inox', 'stainless steel', 'sus', 'inox 304', 'inox 201', 'thép không rỉ'],
420
- 'kinh': ['kinh', 'kính', 'glass', 'tempered glass', 'kính cường lực'],
421
- 'nhom': ['nhom', 'nhôm', 'aluminum', 'hợp kim nhôm'],
422
- 'thep': ['thep', 'thép', 'steel'],
423
- 'gom su': ['gom su', 'gốm sứ', 'ceramic', 'gốm', 'sứ'],
424
- 'thuy tinh': ['thuy tinh', 'thủy tinh', 'glass'],
425
- 'dong': ['dong', 'đồng', 'copper'],
426
- 'hop kim': ['hop kim', 'hợp kim', 'alloy'],
427
- 'go': ['go', 'gỗ', 'wood'],
428
- 'da tu nhien': ['đá tự nhiên', 'granite', 'marble', 'stone'],
429
- 'men': ['men', 'men tĩnh điện', 'enamel'],
430
- };
431
-
432
- const COLORS = {
433
- 'den': ['den', 'đen', 'black', 'matte black', 'black glass'],
434
- 'trang': ['trang', 'trắng', 'white', 'white glass'],
435
- 'bac': ['bac', 'bạc', 'silver'],
436
- 'ghi': ['ghi', 'gray', 'grey', 'xám'],
437
- 'gold': ['gold', 'vàng', 'golden', 'rose gold'],
438
- 'do': ['do', 'đỏ', 'red'],
439
- 'xanh': ['xanh', 'blue', 'green'],
440
- 'nau': ['nau', 'nâu', 'brown'],
441
- 'be': ['be', 'beige', 'cream'],
442
- };
443
-
444
- const SHAPES = {
445
- 'vuong': ['vuong', 'vuông', 'square'],
446
- 'chu nhat': ['chu nhat', 'chữ nhật', 'rectangle', 'rectangular'],
447
- 'tron': ['tron', 'tròn', 'round', 'circle'],
448
- 'chau': ['chau', 'chậu', 'bowl', 'basin'],
449
- };
450
-
451
- const INSTALL_TYPES = {
452
- 'am': ['am', 'âm', 'flush mount', 'undermount'],
453
- 'duong': ['duong', 'dương', 'top mount', 'drop in'],
454
- };
455
-
456
- // ── Enhanced spec constraint detection (from v2.0) ──
457
-
458
- function detectSpecConstraints(text) {
459
- var n = _norm(text || '');
460
- var cs = [];
461
-
462
- // MATERIAL
463
- for (var mat in MATERIALS) {
464
- for (var k = 0; k < MATERIALS[mat].length; k++) {
465
- if (n.indexOf(_norm(MATERIALS[mat][k])) !== -1) {
466
- cs.push({ type: 'material', value: mat, label: 'Chất liệu ' + mat });
467
- break;
468
- }
469
- }
470
- }
471
-
472
- // COLOR
473
- for (var col in COLORS) {
474
- for (var c = 0; c < COLORS[col].length; c++) {
475
- if (n.indexOf(_norm(COLORS[col][c])) !== -1) {
476
- cs.push({ type: 'color', value: col, label: 'Màu ' + col });
477
- break;
478
- }
479
- }
480
- }
481
-
482
- // SHAPE
483
- for (var sh in SHAPES) {
484
- for (var s = 0; s < SHAPES[sh].length; s++) {
485
- if (n.indexOf(_norm(SHAPES[sh][s])) !== -1) {
486
- cs.push({ type: 'shape', value: sh, label: 'Hình ' + sh });
487
- break;
488
- }
489
- }
490
- }
491
-
492
- // INSTALLATION
493
- for (var inst in INSTALL_TYPES) {
494
- for (var i = 0; i < INSTALL_TYPES[inst].length; i++) {
495
- if (n.indexOf(_norm(INSTALL_TYPES[inst][i])) !== -1) {
496
- cs.push({ type: 'install', value: inst, label: 'Lắp ' + inst });
497
- break;
498
- }
499
- }
500
- }
501
-
502
- // ZONE / BURNER / DOOR COUNT
503
- var zonePatterns = [
504
- { re: /(\d+)\s*(?:vùng|vung)\s*(?:nấu|nau)?/, labelAfter: ' vùng nấu' },
505
- { re: /(\d+)\s*(?:hố|ho)\s*(?:rửa|rua)?/, labelAfter: ' hố' },
506
- { re: /(\d+)\s*(?:cửa|cua)\s*(?:tủ|tu)?/, labelAfter: ' cửa' },
507
- { re: /(\d+)\s*(?:ngăn|ngan)\s*(?:mát|lanh|đông)?/, labelAfter: ' ngăn' },
508
- { re: /(?:bếp|bep)\s*(?:đôi|doi|2)\s*(?:vùng|vung)?/, isFixed: 2, label: 'Bếp 2 vùng' },
509
- { re: /(?:bếp|bep)\s*(?:ba|3)\s*(?:vùng|vung)?/, isFixed: 3, label: 'Bếp 3 vùng' },
510
- { re: /(?:bếp|bep)\s*(?:bốn|4)\s*(?:vùng|vung)?/, isFixed: 4, label: 'Bếp 4 vùng' },
511
- { re: /(?:tủ|tu)\s*(?:2|đôi|doi)\s*(?:cánh|canh)?/, isFixed: 2, label: 'Tủ 2 cánh' },
512
- { re: /(?:tủ|tu)\s*(?:4|bốn|bon)\s*(?:cánh|canh)?/, isFixed: 4, label: 'Tủ 4 cánh' },
513
- { re: /(?:máy|may)\s*(?:giặt|giat)\s*(\d+)\s*(?:kg)?/, labelAfter: ' kg' },
514
- { re: /(?:máy|may)\s*(?:sấy|say)\s*(\d+)\s*(?:kg)?/, labelAfter: ' kg sấy' },
515
- { re: /(?:tủ|tu)\s*(?:lạnh|lanh)\s*\D{0,8}(\d+)\s*lit/, labelAfter: ' lít' },
516
- ];
517
-
518
- for (var z = 0; z < zonePatterns.length; z++) {
519
- var pat = zonePatterns[z];
520
- var m = n.match(pat.re);
521
- if (m) {
522
- if (pat.isFixed) {
523
- cs.push({ type: 'count', value: pat.isFixed, label: pat.label });
524
- } else if (m[1]) {
525
- var val = parseInt(m[1]);
526
- if (val > 0 && val <= 20) cs.push({ type: 'count', value: val, label: val + (pat.labelAfter || '') });
527
- }
528
- }
529
- }
530
-
531
- // WEIGHT
532
- var wtPat = n.match(/([\d.,]+)\s*kg/i);
533
- if (wtPat) {
534
- var wtVal = parseFloat(wtPat[1].replace(',', '.'));
535
- if (wtVal > 0 && wtVal < 500) cs.push({ type: 'weight', value: wtVal, label: 'Trọng lượng ' + wtVal + 'kg' });
536
- }
537
-
538
- // VOLUME
539
- var volPat = n.match(/([\d.,]+)\s*(?:lít|lit|lt|l)\b(?!.*(?:trieu|tr))/i);
540
- if (volPat) {
541
- var volVal = parseFloat(volPat[1].replace(',', '.'));
542
- if (volVal > 0 && volVal < 1000) cs.push({ type: 'volume', value: volVal, label: 'Dung tích ' + volVal + 'L' });
543
- }
544
-
545
- // FEATURES
546
- var addFeatures = [
547
- [/bldc|inverter/i, 'Động cơ BLDC', 'bldc'],
548
- [/cam ung|cảm ứng/i, 'Điều khiển cảm ứng', 'cam ung'],
549
- [/cu chi|khong cham|không chạm/i, 'Điều khiển cử chỉ', 'sensor'],
550
- [/than hoat tinh|than hoạt tính/i, 'Than hoạt tính', 'than'],
551
- [/wifi|smart|kết nối|ket noi|app|bluetooth/i, 'Kết nối thông minh', 'wifi'],
552
- [/hiển thị|hien thi|display|màn hình|man hinh|led/i, 'Màn hình hiển thị', 'display'],
553
- [/tự động|tu dong|auto|automatic/i, 'Tự động', 'auto'],
554
- ];
555
-
556
- for (var f = 0; f < addFeatures.length; f++) {
557
- if (addFeatures[f][0].test(n)) cs.push({ type: 'feature', label: addFeatures[f][1], value: addFeatures[f][2] });
558
- }
559
-
560
- // POWER (W)
561
- var powerPat = n.match(/([\d.,]{2,5})\s*(?:w|watt)\b/i);
562
- if (powerPat) {
563
- var pwVal = parseFloat(powerPat[1].replace(',', '.'));
564
- if (pwVal > 0 && pwVal <= 20000) cs.push({ type: 'power', value: pwVal, label: 'Công suất ' + pwVal + 'W' });
565
- }
566
-
567
- // COMPARATIVE
568
- if (/(?:tốt nhất|tot nhat|giá rẻ nhất|gia re nhat|bán chạy|ban chay|phổ biến|pho bien)/.test(n)) {
569
- cs.push({ type: 'comparative', value: 'best', label: 'Tốt nhất' });
570
- }
571
- if (/(?:rẻ nhất|re nhat|giá thấp nhất|gia thap nhat)/.test(n)) {
572
- cs.push({ type: 'comparative', value: 'cheapest', label: 'Rẻ nhất' });
573
- }
574
- if (/(?:đắt nhất|dat nhat|cao cấp|cao cap|cao nhất)/.test(n)) {
575
- cs.push({ type: 'comparative', value: 'most_expensive', label: 'Cao cấp nhất' });
576
- }
577
-
578
- return cs;
579
- }
580
-
581
- // ── Spec constraint matching (from v2.0) ──
582
-
583
- function _buildFullText(p) {
584
- var specs = p.specs || {};
585
- var sp = '';
586
- if (specs && typeof specs === 'object') {
587
- sp = Object.entries(specs).map(function(e) { return e[0] + ' ' + e[1]; }).join(' ');
588
- }
589
- return _norm([p.name, p.cat, p.brand, p.summary || p.sum || '', p.desc || '', sp].join(' '));
590
- }
591
-
592
- function matchesConstraints(p, constraints) {
593
- if (!constraints || !constraints.length) return { ok: true, score: 0, labels: [] };
594
-
595
- var fullText = _buildFullText(p);
596
- var dims = _extractDims(p);
597
- var specs = p.specs || {};
598
-
599
- var score = 0;
600
- var labels = [];
601
- var matchCount = 0;
602
-
603
- for (var ci = 0; ci < constraints.length; ci++) {
604
- var c = constraints[ci];
605
- var ok = false;
606
-
607
- if (c.type === 'material') {
608
- var mats = MATERIALS[c.value] || [c.value];
609
- for (var i = 0; i < mats.length; i++) {
610
- if (fullText.indexOf(_norm(mats[i])) !== -1) { ok = true; break; }
611
- }
612
- if (!ok) {
613
- for (var k in specs) {
614
- var sv = _norm(specs[k] || '');
615
- for (var j = 0; j < mats.length; j++) {
616
- if (sv.indexOf(_norm(mats[j])) !== -1) { ok = true; break; }
617
- }
618
- if (ok) break;
619
- }
620
- }
621
- if (ok) { score += 35; labels.push(c.label); matchCount++; }
622
- else return { ok: false, score: 0, labels: [] };
623
- }
624
- else if (c.type === 'color') {
625
- var cols = COLORS[c.value] || [c.value];
626
- for (var i = 0; i < cols.length; i++) {
627
- if (fullText.indexOf(_norm(cols[i])) !== -1) { ok = true; break; }
628
- }
629
- if (ok) { score += 20; labels.push(c.label); matchCount++; }
630
- else return { ok: false, score: 0, labels: [] };
631
- }
632
- else if (c.type === 'shape') {
633
- var shapes = SHAPES[c.value] || [c.value];
634
- for (var i = 0; i < shapes.length; i++) {
635
- if (fullText.indexOf(_norm(shapes[i])) !== -1) { ok = true; break; }
636
- }
637
- if (ok) { score += 18; labels.push(c.label); matchCount++; }
638
- }
639
- else if (c.type === 'install') {
640
- var types = INSTALL_TYPES[c.value] || [c.value];
641
- for (var i = 0; i < types.length; i++) {
642
- if (fullText.indexOf(_norm(types[i])) !== -1) { ok = true; break; }
643
- }
644
- if (ok) { score += 15; labels.push(c.label); matchCount++; }
645
- }
646
- else if (c.type === 'count') {
647
- for (var k in specs) {
648
- var v = String(specs[k] || '');
649
- var m = v.match(/(\d+)/);
650
- if (m && parseInt(m[1]) === c.value) { ok = true; break; }
651
- }
652
- if (!ok && c.value <= 4) {
653
- var name = _norm(p.name || '');
654
- if (c.value === 2 && (name.indexOf('Đôi') !== -1 || name.indexOf('doi') !== -1)) ok = true;
655
- if (c.value === 3 && (name.indexOf(' 3') !== -1 || name.indexOf('3 ') !== -1)) ok = true;
656
- if (c.value === 4 && (name.indexOf(' 4') !== -1 || name.indexOf('4 ') !== -1)) ok = true;
657
- }
658
- if (ok) { score += 25; labels.push(c.label); matchCount++; }
659
- }
660
- else if (c.type === 'weight') {
661
- if (dims && dims.weight !== null) {
662
- if (Math.abs(dims.weight - c.value) <= c.value * 0.2) ok = true;
663
- }
664
- var wtMatch = fullText.match(/([\d.,]+)\s*kg/i);
665
- if (wtMatch && !ok) {
666
- var v2 = parseFloat(wtMatch[1].replace(',', '.'));
667
- if (Math.abs(v2 - c.value) <= c.value * 0.2) ok = true;
668
- }
669
- if (ok) { score += 30; labels.push(c.label); matchCount++; }
670
- }
671
- else if (c.type === 'volume') {
672
- if (dims && dims.volume !== null) {
673
- if (Math.abs(dims.volume - c.value) <= c.value * 0.2) ok = true;
674
- }
675
- var volMatch = fullText.match(/([\d.,]+)\s*(?:lít|lit)\b/i);
676
- if (volMatch && !ok) {
677
- var v2 = parseFloat(volMatch[1].replace(',', '.'));
678
- if (Math.abs(v2 - c.value) <= c.value * 0.2) ok = true;
679
- }
680
- if (ok) { score += 30; labels.push(c.label); matchCount++; }
681
- }
682
- else if (c.type === 'feature') {
683
- ok = fullText.indexOf(_norm(c.value || c.label)) !== -1;
684
- if (ok) { score += 15; labels.push(c.label); matchCount++; }
685
- }
686
- else if (c.type === 'comparative') {
687
- score += 10;
688
- labels.push(c.label);
689
- matchCount++;
690
- ok = true;
691
- }
692
- else if (c.type === 'power') {
693
- var pwMatch = fullText.match(/([\d.,]+)\s*w\b/i);
694
- if (pwMatch) {
695
- var pw = parseFloat(pwMatch[1].replace(',', '.'));
696
- if (pw > 0 && pw <= 20000) {
697
- ok = c.op === '<' ? pw < c.value : Math.abs(pw - c.value) <= c.value * 0.2;
698
- }
699
- }
700
- if (ok) { score += 25; labels.push(c.label); matchCount++; }
701
- }
702
- else if (c.type === 'suction') {
703
- var sMatch = fullText.match(/(?:công suất hút|cong suat hut)\D{0,18}(\d{3,4})/i);
704
- if (sMatch) {
705
- var sv = parseInt(sMatch[1]);
706
- ok = c.op === '<=' ? sv <= c.value : sv >= c.value;
707
- }
708
- if (ok) { score += 45; labels.push(c.label); matchCount++; }
709
- else return { ok: false, score: 0, labels: [] };
710
- }
711
- else if (c.type === 'noise') {
712
- var nMatch = fullText.match(/(?:độ ồn|do on)\D{0,14}(\d{2})/i);
713
- if (nMatch) {
714
- var nv = parseInt(nMatch[1]);
715
- ok = c.op === '>=' ? nv >= c.value : nv <= c.value;
716
- }
717
- if (ok) { score += 35; labels.push(c.label); matchCount++; }
718
- }
719
- }
720
-
721
- return { ok: matchCount > 0 || constraints.every(function(c) { return c.type === 'feature' || c.type === 'comparative'; }), score: score, labels: labels };
722
- }
723
-
724
- // ── Semantic full-text search with enhanced ranking ──
725
-
726
- function semanticFullTextSearch(query, products, limit) {
727
- limit = limit || 24;
728
- var q = _norm(query);
729
- var queryTokens = q.split(/\s+/).filter(function(w) { return w.length >= 2; });
730
- if (queryTokens.length === 0) return [];
731
-
732
- var index = window._vaiIndex;
733
- var scores = [];
734
-
735
- if (index) {
736
- // Phase 1: Build candidate set from keyword index
737
- var candidateScores = {};
738
- for (var t = 0; t < queryTokens.length; t++) {
739
- var term = queryTokens[t];
740
- var matches = index.keywords[term];
741
- if (matches) {
742
- for (var m = 0; m < matches.length; m++) {
743
- var idx = matches[m];
744
- var boost = (index.nameTexts[idx] && index.nameTexts[idx].indexOf(term) !== -1) ? 3 : 1;
745
- candidateScores[idx] = (candidateScores[idx] || 0) + boost;
746
- }
747
- }
748
- // Also try prefix match (first 3 chars of each token)
749
- if (term.length >= 3) {
750
- var prefix = term.substring(0, 3);
751
- var prefixMatches = index.keywords[prefix];
752
- if (prefixMatches) {
753
- for (var pm = 0; pm < prefixMatches.length; pm++) {
754
- var pi = prefixMatches[pm];
755
- candidateScores[pi] = (candidateScores[pi] || 0) + 1;
756
- }
757
- }
758
- }
759
- }
760
-
761
- // Phase 2: Score each candidate with enhanced ranking
762
- var candidates = Object.keys(candidateScores).map(Number);
763
- for (var ci = 0; ci < candidates.length; ci++) {
764
- var idx = candidates[ci];
765
- var nameScore = 0;
766
- var brandScore = 0;
767
- var catScore = 0;
768
- var descScore = 0;
769
-
770
- for (var ti = 0; ti < queryTokens.length; ti++) {
771
- var qt = queryTokens[ti];
772
- if (index.nameTexts[idx] && index.nameTexts[idx].indexOf(qt) !== -1) nameScore += 40;
773
- if (index.brandTexts[idx] && index.brandTexts[idx].indexOf(qt) !== -1) brandScore += 20;
774
- if (index.catTexts[idx] && index.catTexts[idx].indexOf(qt) !== -1) catScore += 15;
775
- if (index.fullTexts[idx] && index.fullTexts[idx].indexOf(qt) !== -1) descScore += 5;
776
- }
777
-
778
- if (nameScore + brandScore === 0) continue;
779
-
780
- var product = products[idx];
781
- var price = index.prices[idx] || 1e12;
782
- var baseScore = candidateScores[idx] * 5;
783
- var finalScore = baseScore + nameScore + brandScore + catScore + descScore;
784
-
785
- // Apply price bonus (cheaper products get slight boost)
786
- if (price > 0 && price < 20000000) {
787
- finalScore += Math.max(0, 15 - Math.floor(price / 2000000));
788
- }
789
-
790
- scores.push({
791
- p: product,
792
- score: finalScore,
793
- price: price,
794
- labels: [],
795
- cat: '',
796
- nameScore: nameScore,
797
- brandScore: brandScore
798
- });
799
- }
800
- } else {
801
- // Fallback: scan all products without index
802
- for (var i = 0; i < products.length; i++) {
803
- var p = products[i];
804
- if (!p) continue;
805
- var fullText = _buildFullText(p);
806
- var nameText = _norm(p.name || '');
807
- var brandText = _norm(p.brand || '');
808
-
809
- var nameScore = 0;
810
- var brandScore = 0;
811
-
812
- for (var ti = 0; ti < queryTokens.length; ti++) {
813
- var qt = queryTokens[ti];
814
- if (nameText.indexOf(qt) !== -1) nameScore += 40;
815
- if (brandText.indexOf(qt) !== -1) brandScore += 20;
816
- if (fullText.indexOf(qt) !== -1) { /* included in base */ }
817
- }
818
-
819
- if (nameScore + brandScore === 0) continue;
820
-
821
- var price = p.priceNum || 1e12;
822
- scores.push({
823
- p: p,
824
- score: nameScore + brandScore,
825
- price: price,
826
- labels: [],
827
- cat: ''
828
- });
829
- }
830
- }
831
-
832
- // Sort by score, then by price
833
- scores.sort(function(a, b) { return b.score - a.score || (a.price || 1e12) - (b.price || 1e12); });
834
-
835
- // Deduplicate
836
- var seen = {};
837
- var deduped = [];
838
- for (var i = 0; i < scores.length; i++) {
839
- var key = scores[i].p.sku || scores[i].p._mod || scores[i].p.name || i;
840
- if (seen[key]) continue;
841
- seen[key] = 1;
842
- deduped.push(scores[i]);
843
- if (deduped.length >= limit) break;
844
- }
845
-
846
- return deduped;
847
- }
848
-
849
- // ── Brand detection with fuzzy matching ──
850
-
851
- function detectBrand(text) {
852
- var n = _norm(text || '');
853
- var BRANDS = ['malloca', 'grob', 'canzy', 'eurogold', 'garis', 'demax', 'boss', 'hafele', 'teka', 'bosch', 'panasonic', 'electrolux', 'samsung', 'lg', 'toshiba', 'sharp'];
854
-
855
- for (var b = 0; b < BRANDS.length; b++) {
856
- var brand = BRANDS[b];
857
- if (n.indexOf(brand) !== -1) return brand;
858
- // Fuzzy: check if brand name matches
859
- if (levenshtein(n, brand) <= 2) return brand;
860
- }
861
- return '';
862
- }
863
-
864
- // ── Main enhanced search function ──
865
-
866
- function enhancedSearch(query, products, categories, budget, constraints) {
867
- if (!query || !products || !products.length) return { results: [], categories: [], budget: 0, constraints: [] };
868
-
869
- if (!window._vaiIndex) {
870
- window._vaiIndex = buildSpecIndex(products);
871
- if (window._vaiIndex) {
872
- window._vaiIndex._prodIdxMap = new Map();
873
- for (var i = 0; i < products.length; i++) {
874
- window._vaiIndex._prodIdxMap.set(products[i], i);
875
- }
876
- }
877
- }
878
-
879
- var results = [];
880
- var budgetVal = budget || 0;
881
-
882
- // Phase 1: Category-based search with semantic matching
883
- if (categories && categories.length > 0) {
884
- for (var ci = 0; ci < categories.length; ci++) {
885
- var cat = categories[ci];
886
- var catResults = _enhancedCategorySearch(query, cat, products, constraints, budgetVal);
887
- results.push.apply(results, catResults);
888
- }
889
- var seenSet = {};
890
- results = results.filter(function(r) {
891
- var k = r.p.sku || r.p._mod || r.p.name;
892
- if (seenSet[k]) return false;
893
- seenSet[k] = 1;
894
- return true;
895
- });
896
- results.sort(function(a, b) { return b.score - a.score; });
897
- }
898
-
899
- // Phase 2: Fallback — semantic full-text search
900
- if (results.length === 0) {
901
- results = semanticFullTextSearch(query, products, 24);
902
- }
903
-
904
- // Phase 3: Apply comparative sorting
905
- var hasComparative = constraints && constraints.some(function(c) { return c.type === 'comparative'; });
906
- if (hasComparative) {
907
- for (var cc = 0; cc < (constraints || []).length; cc++) {
908
- if (constraints[cc].type === 'comparative') {
909
- var compType = constraints[cc].value;
910
- if (compType === 'cheapest') {
911
- results.sort(function(a, b) { return (a.price || 1e12) - (b.price || 1e12); });
912
- } else if (compType === 'most_expensive') {
913
- results.sort(function(a, b) { return (b.price || 0) - (a.price || 0); });
914
- }
915
- break;
916
- }
917
- }
918
- }
919
-
920
- var finalResults = results.slice(0, 24).map(function(r) {
921
- var idx = products.indexOf(r.p);
922
- return Object.assign({}, r, { idx: idx >= 0 ? idx : -1 });
923
- });
924
-
925
- return {
926
- results: finalResults,
927
- categories: categories,
928
- constraints: constraints,
929
- budget: budget,
930
- combos: [],
931
- aiAnswer: ''
932
- };
933
- }
934
-
935
- function _enhancedCategorySearch(query, cat, products, constraints, budget) {
936
- var name = _norm(query);
937
- var brand = detectBrand(query);
938
- var limit = 12;
939
- var out = [];
940
-
941
- var catKeywords = {
942
- 'bep tu': ['bep tu', 'bep dien tu', 'bep cam ung', 'induction'],
943
- 'bep gas': ['bep gas', 'bep ga', 'gas am'],
944
- 'bep hong ngoai': ['bep hong ngoai', 'hong ngoai'],
945
- 'may hut mui': ['may hut mui', 'hut mui', 'hut khoi', 'khu mui'],
946
- 'chau rua': ['chau rua', 'chau rua chen', 'bon rua', 'sink'],
947
- 'voi rua': ['voi rua', 'faucet'],
948
- 'tu lanh': ['tu lanh', 'tu ruou', 'refrigerator'],
949
- 'may rua chen': ['may rua chen', 'may rua bat', 'rua chen', 'dishwasher'],
950
- 'lo nuong': ['lo nuong', 'lo am tu', 'oven'],
951
- 'tu do kho': ['tu do kho', 'tu kho', 'he kho', 'pantry'],
952
- 'thung rac': ['thung rac', 'thung gao'],
953
- 'khoa cua': ['khoa cua', 'khoa van tay', 'khoa thong minh', 'khoa dien tu', 'smart lock'],
954
- 'phu kien tu bep': ['phu kien tu bep', 'phu kien bep', 'ke bat', 'gia bat', 'ke gia vi'],
955
- 'ray': ['ray am', 'ray bi', 'ray giam chan'],
956
- 'tay nam': ['tay nam', 'num tu', 'tay cam'],
957
- 'dieu hoa': ['dieu hoa', 'may lanh'],
958
- 'noi chien': ['noi chien', 'noi chiên khong dau', 'air fryer'],
959
- };
960
-
961
- var kws = catKeywords[cat] || [cat];
962
-
963
- for (var i = 0; i < products.length; i++) {
964
- var p = products[i];
965
- if (!p) continue;
966
- var pName = _norm(p.name || '');
967
- var pCat = _norm(p.cat || '');
968
- var pBrand = _norm(p.brand || '');
969
- var pPrice = p.priceNum || 0;
970
-
971
- // Check semantic match (both direct and synonym-expanded)
972
- var tokenized = name.split(/\s+/).filter(function(w) { return w.length >= 2; });
973
- var matchResult = fuzzyMatch(tokenized, pName + ' ' + pCat + ' ' + pBrand);
974
- var semanticScore = matchResult.score;
975
- var nameMatchResult = fuzzyMatch(tokenized, pName);
976
- var nameBoost = nameMatchResult.score > 0 ? nameMatchResult.score * 0.5 : 0;
977
-
978
- if (matchResult.score < 10 && !kws.some(function(kw) { return pName.indexOf(kw) !== -1 || pCat.indexOf(kw) !== -1; })) {
979
- continue;
980
- }
981
-
982
- if (brand && pBrand.indexOf(brand) === -1 && pName.indexOf(brand) === -1) continue;
983
- if (budget && pPrice && pPrice > budget) continue;
984
-
985
- var result = matchesConstraints(p, constraints || []);
986
- if (!result.ok) continue;
987
-
988
- var score = 30 + result.score + semanticScore + nameBoost;
989
- if (pPrice) score += Math.max(0, 18 - Math.floor(pPrice / 2000000));
990
-
991
- // Brand preference boost
992
- if (pBrand.indexOf('malloca') !== -1) score += 12;
993
- else if (pBrand.indexOf('grob') !== -1) score += 10;
994
- else if (pBrand.indexOf('eurogold') !== -1) score += 7;
995
- else if (pBrand.indexOf('hafele') !== -1) score += 5;
996
- else if (pBrand.indexOf('canzy') !== -1) score += 4;
997
- else if (pBrand.indexOf('demax') !== -1) score += 3;
998
-
999
- out.push({ p: p, score: score, price: pPrice, labels: result.labels, cat: cat });
1000
- }
1001
-
1002
- out.sort(function(a, b) { return b.score - a.score || (a.price || 1e12) - (b.price || 1e12); });
1003
- return out.slice(0, limit);
1004
- }
1005
-
1006
- // ── AI Fallback (from v2.0) ──
1007
-
1008
- const AI_URL = 'https://router.huggingface.co/v1/chat/completions';
1009
- const AI_MODEL = 'Qwen/Qwen2.5-72B-Instruct';
1010
-
1011
- function callAI(query, products, aiToken) {
1012
- if (!aiToken) return Promise.resolve(null);
1013
-
1014
- // Use first matching product if available
1015
- var sampleProduct = null;
1016
- if (products && products.length > 0) {
1017
- sampleProduct = products[0];
1018
- }
1019
-
1020
- if (!sampleProduct) return Promise.resolve(null);
1021
-
1022
- var ctx = [
1023
- '[' + [sampleProduct.name, sampleProduct.sku, sampleProduct.brand, sampleProduct.price || 'LH'].join('|') + ']',
1024
- ...Object.entries(sampleProduct.specs || {}).filter(([k, v]) => k && v).slice(0, 12).map(([k, v]) => '-' + k + ':' + v),
1025
- ...(sampleProduct.feats || []).filter(function(f) { return f; }).slice(0, 10).map(function(f) { return '-' + f; }),
1026
- sampleProduct.summary || sampleProduct.sum || '',
1027
- ].filter(Boolean).join('\n');
1028
-
1029
- var systemPrompt = 'Tư vấn V.AI STUDIO. Xưng em. Ưu tiên Grob trước khi phù hợp, sau đó Eurogold.\n' + ctx + '\nCHỈ dùng data. KHÔNG bịa. Max 200.';
1030
-
1031
- return fetch(AI_URL, {
1032
- method: 'POST',
1033
- headers: {
1034
- 'Authorization': 'Bearer ' + aiToken,
1035
- 'Content-Type': 'application/json',
1036
- },
1037
- body: JSON.stringify({
1038
- model: AI_MODEL,
1039
- messages: [
1040
- { role: 'system', content: systemPrompt },
1041
- { role: 'user', content: query + ' — Từ 50 đến 70 từ' },
1042
- ],
1043
- max_tokens: 250,
1044
- temperature: 0.3,
1045
- }),
1046
- }).then(function(resp) {
1047
- if (!resp.ok) return null;
1048
- return resp.json().then(function(data) {
1049
- var text = (data.choices?.[0]?.message?.content || '').trim();
1050
- text = text.replace(/<\?xml.*?\?>|<think>[\s\S]*?<\/think>|\*\*|#{1,3}\s*/g, '').trim();
1051
- return text;
1052
- });
1053
- }).catch(function(e) {
1054
- return null;
1055
- });
1056
- }
1057
-
1058
- // ── Patch the original aiSearch function ─────────────────────────────────────
1059
-
1060
- function patchAiSearch() {
1061
- if (typeof window.aiSearch !== 'function') {
1062
- console.log('[AI Search v2.1] Waiting for aiSearch to load...');
1063
- setTimeout(patchAiSearch, 500);
1064
- return;
1065
- }
1066
-
1067
- window._originalAiSearch = window.aiSearch;
1068
-
1069
- window.aiSearch = function(query, products, aiToken) {
1070
- if (!query || query.trim().length < 2) {
1071
- return Promise.resolve({ results: [], categories: [], constraints: [], budget: 0, combos: [], aiAnswer: '' });
1072
- }
1073
-
1074
- var categories = [];
1075
- if (typeof nlDetectCategories === 'function') {
1076
- categories = nlDetectCategories(query);
1077
- }
1078
-
1079
- var budget = 0;
1080
- if (typeof nlParseBudget === 'function') {
1081
- budget = nlParseBudget(query);
1082
- }
1083
-
1084
- // Collect constraints from v2.1 detection
1085
- var constraints = detectSpecConstraints(query);
1086
-
1087
- // Also merge original constraints (suction, noise, etc.)
1088
- if (typeof nlDetectSpecConstraints === 'function') {
1089
- var origConstraints = nlDetectSpecConstraints(query);
1090
- for (var i = 0; i < origConstraints.length; i++) {
1091
- var oc = origConstraints[i];
1092
- var exists = constraints.some(function(c) { return c.type === oc.type && c.value == oc.value; });
1093
- if (!exists) constraints.push(oc);
1094
- }
1095
- }
1096
-
1097
- // Enhanced search with semantic matching
1098
- var result = enhancedSearch(query, products, categories, budget, constraints);
1099
-
1100
- // AI fallback for better UX when few results
1101
- if (result.results.length < 3 && aiToken) {
1102
- return callAI(query, products, aiToken).then(function(aiAnswer) {
1103
- result.aiAnswer = aiAnswer || '';
1104
- return result;
1105
- }).catch(function() {
1106
- return result;
1107
- });
1108
- }
1109
-
1110
- return Promise.resolve(result);
1111
- };
1112
-
1113
- console.log('[AI Search v2.1] ✅ Patched aiSearch with semantic search + synonym expansion');
1114
- console.log('[AI Search v2.1] Features: synonyms, fuzzy matching, enhanced ranking, brand detection');
1115
- console.log('[AI Search v2.1] Synonym map loaded:', Object.keys(SYNONYM_MAP).length, 'term groups');
1116
- }
1117
-
1118
- // ── Build spec index when data loads ────────────────────────────────────────
1119
-
1120
- function watchForData() {
1121
- var checkInterval = setInterval(function() {
1122
- if (window.D && window.D.length > 0) {
1123
- window._vaiIndex = buildSpecIndex(window.D);
1124
- if (window._vaiIndex) {
1125
- window._vaiIndex._prodIdxMap = new Map();
1126
- for (var i = 0; i < window.D.length; i++) {
1127
- window._vaiIndex._prodIdxMap.set(window.D[i], i);
1128
- }
1129
- }
1130
- console.log('[AI Search v2.1] Built spec & semantic index for ' + window.D.length + ' products');
1131
- patchAiSearch();
1132
- clearInterval(checkInterval);
1133
- }
1134
- }, 500);
1135
-
1136
- setTimeout(function() {
1137
- if (window.D && window.D.length > 0) {
1138
- // Already handled
1139
- } else {
1140
- console.log('[AI Search v2.1] Timeout waiting for data (30s)');
1141
- }
1142
- }, 30000);
1143
- }
1144
-
1145
- watchForData();
1146
-
1147
- // ── Expose for debugging ──
1148
-
1149
- window.aiSearchV2 = {
1150
- detectSpecConstraints: detectSpecConstraints,
1151
- matchesConstraints: matchesConstraints,
1152
- buildSpecIndex: buildSpecIndex,
1153
- buildSynonymIndex: buildSynonymIndex,
1154
- semanticFullTextSearch: semanticFullTextSearch,
1155
- enhancedSearch: enhancedSearch,
1156
- fuzzyMatch: fuzzyMatch,
1157
- expandSynonyms: expandSynonyms,
1158
- detectBrand: detectBrand,
1159
- _norm: _norm,
1160
- SYNONYM_MAP: SYNONYM_MAP,
1161
- MATERIALS: MATERIALS,
1162
- COLORS: COLORS,
1163
- SHAPES: SHAPES,
1164
- INSTALL_TYPES: INSTALL_TYPES,
1165
- };
1166
-
1167
- })();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
canzy_products_img/canzy_product_CZ007R.png DELETED

Git LFS Details

  • SHA256: 6d5b9b12b085fa50d52c7edbeb81022ade2c7c984d116ef5c52e72c7071f6e3e
  • Pointer size: 131 Bytes
  • Size of remote file: 256 kB
canzy_products_img/canzy_product_CZ007R_v2026.png DELETED

Git LFS Details

  • SHA256: 4f8d68c7ed4b7ebff405f66e920b52d8db75973c79582c917190fc1782cfc59b
  • Pointer size: 131 Bytes
  • Size of remote file: 183 kB
canzy_products_img/canzy_product_CZ03T.png DELETED
Binary file (11.7 kB)
 
canzy_products_img/canzy_product_CZ03T_v2026.png DELETED

Git LFS Details

  • SHA256: f6bf034115ffd9cfdf01fb9c9aed655d1a91f6d939846c382bab201ca846cef8
  • Pointer size: 131 Bytes
  • Size of remote file: 178 kB
canzy_products_img/canzy_product_CZ09DL.png DELETED

Git LFS Details

  • SHA256: 3cf9db387d07f60959932e36ec3d5b1d2e3d9d95b1cc1a39f8f34e6df318d153
  • Pointer size: 131 Bytes
  • Size of remote file: 286 kB
canzy_products_img/canzy_product_CZ11EBN.png DELETED

Git LFS Details

  • SHA256: 80eb4f40c12a2e6439d744329847e754a1c1df36bc5b53d66873bd2b8dce34b5
  • Pointer size: 131 Bytes
  • Size of remote file: 314 kB
canzy_products_img/canzy_product_CZ11EBN_v2026.png DELETED

Git LFS Details

  • SHA256: 7b01b2bd5c24135a732f82c0940edfdf0e99c56e232bc4edfdbc2a70cb23d797
  • Pointer size: 131 Bytes
  • Size of remote file: 209 kB
canzy_products_img/canzy_product_CZ11QAMSERIAL80.png DELETED

Git LFS Details

  • SHA256: a0e810ca93029174d6a43c35fac8d4d378c2762bc7965da17fc56fd00146e39c
  • Pointer size: 132 Bytes
  • Size of remote file: 1.15 MB
canzy_products_img/canzy_product_CZ11QAMSERIAL80_v2026.png DELETED

Git LFS Details

  • SHA256: 07752a7a6686e39c3aeadd67395f955f03a238d7dca0edef67d05e5523d2a36e
  • Pointer size: 131 Bytes
  • Size of remote file: 130 kB
canzy_products_img/canzy_product_CZ15YA.png DELETED
Binary file (27.6 kB)
 
canzy_products_img/canzy_product_CZ15YA_v2026.png DELETED
Binary file (59.4 kB)
 
canzy_products_img/canzy_product_CZ16EUGRUNIQUE.png DELETED

Git LFS Details

  • SHA256: 79f8768dbe671a5240d875ee69c2b1ee95fbbb5085f39678513aa81dd5a70ec1
  • Pointer size: 131 Bytes
  • Size of remote file: 128 kB
canzy_products_img/canzy_product_CZ16EUGRUNIQUE_v2026.png DELETED

Git LFS Details

  • SHA256: debfd427e038a2403acf82fe0aeb5a7065d71c08905022b296cea2c24e4034c0
  • Pointer size: 131 Bytes
  • Size of remote file: 128 kB
canzy_products_img/canzy_product_CZ34GSS.png DELETED

Git LFS Details

  • SHA256: cf8af59452a60cd27706283ba20eab59d1acb2a190d193ad7146dd21265eb34f
  • Pointer size: 132 Bytes
  • Size of remote file: 2.18 MB
canzy_products_img/canzy_product_CZ34GSS_v2026.png DELETED

Git LFS Details

  • SHA256: e6f72d3e77cdf2c25f7298ceadfa302f28d37fa003d8cc01ddfb7ff757125edb
  • Pointer size: 131 Bytes
  • Size of remote file: 325 kB
canzy_products_img/canzy_product_CZ68DT.png DELETED

Git LFS Details

  • SHA256: 0b293670c491df55f1863c230c29c676315a80ede90f4086f3a9c5ed877bdbfb
  • Pointer size: 131 Bytes
  • Size of remote file: 191 kB
canzy_products_img/canzy_product_CZ68DT_v2026.png DELETED

Git LFS Details

  • SHA256: 6aa5ce4c1cb4e10685fa781fdbf66b53daef7dc807b63b681a75e8d2c724be67
  • Pointer size: 131 Bytes
  • Size of remote file: 115 kB
canzy_products_img/canzy_product_CZ700AUTO.png DELETED

Git LFS Details

  • SHA256: 30e2c5ca721030cc65e4c83277842e651f355aac73ea34ce5eef5c3cc1ee9028
  • Pointer size: 131 Bytes
  • Size of remote file: 342 kB
canzy_products_img/canzy_product_CZ700AUTO_v2026.png DELETED

Git LFS Details

  • SHA256: 7e0ffcaefd2b617fc22e10ef9ef73c840e2750226a453d98a8649d3813c0c45e
  • Pointer size: 131 Bytes
  • Size of remote file: 238 kB
canzy_products_img/canzy_product_CZ70B50.png DELETED

Git LFS Details

  • SHA256: a133ac4949a7f98aae38e182943a1e60894aadeac0165271d57b8cb518bffe70
  • Pointer size: 131 Bytes
  • Size of remote file: 477 kB
canzy_products_img/canzy_product_CZ70B50_v2026.png DELETED

Git LFS Details

  • SHA256: 41152b0f24fc07d45586dee9566396dad274e2853e7bbd0860593dfe75539eab
  • Pointer size: 131 Bytes
  • Size of remote file: 180 kB
canzy_products_img/canzy_product_CZ70C66.png DELETED

Git LFS Details

  • SHA256: 9c305bd9b80801e52f7bc254130b48401979ec346611d1475d983f66c6c04dfc
  • Pointer size: 131 Bytes
  • Size of remote file: 248 kB
canzy_products_img/canzy_product_CZ70C66_v2026.png DELETED

Git LFS Details

  • SHA256: 1baf3d5ae95af1dccf11091bf70c9dd2319bc968ad57a81c38c41aca4f94f0d5
  • Pointer size: 131 Bytes
  • Size of remote file: 192 kB
canzy_products_img/canzy_product_CZ70I50.png DELETED

Git LFS Details

  • SHA256: e7573d42f3171803dbcd1d60b92f1240617aeecce5cd623147a92067a99c4c96
  • Pointer size: 131 Bytes
  • Size of remote file: 514 kB
canzy_products_img/canzy_product_CZ70I50_v2026.png DELETED

Git LFS Details

  • SHA256: 5e5bbae328770c8c3703ca9369676a643ce1621a676b268931d3b3329c7e2a4b
  • Pointer size: 131 Bytes
  • Size of remote file: 179 kB
canzy_products_img/canzy_product_CZ780K.png DELETED

Git LFS Details

  • SHA256: 5ff989ddce88bb3c30ec1a19f45bf273f8c4ea41754a9b3533eef49d5badf3c5
  • Pointer size: 131 Bytes
  • Size of remote file: 238 kB
canzy_products_img/canzy_product_CZ780K_v2026.png DELETED

Git LFS Details

  • SHA256: b2ffeb5b394efc886f20b93abffa3a7b2554cbdd3d855a98ca80aed44e3eabb2
  • Pointer size: 131 Bytes
  • Size of remote file: 197 kB
canzy_products_img/canzy_product_CZ806A5G.png DELETED

Git LFS Details

  • SHA256: 3c2ca89857c1c2879e94fb6c0c0bc88a4c8b67a32a85577e60376b1b0e60c95d
  • Pointer size: 131 Bytes
  • Size of remote file: 292 kB
canzy_products_img/canzy_product_CZ806A5G_v2026.png DELETED

Git LFS Details

  • SHA256: c8f6402137a574f67faf25d95df3f7e8d997eba0c5853493e21a48a04e5662b1
  • Pointer size: 131 Bytes
  • Size of remote file: 174 kB
canzy_products_img/canzy_product_CZ815TDB_v2026.png DELETED

Git LFS Details

  • SHA256: 3e762388085dc7fe730e667d98fdd791cf45dfca37cbdff31c93b53214c6a3cf
  • Pointer size: 131 Bytes
  • Size of remote file: 142 kB
canzy_products_img/canzy_product_CZ816SDR.png DELETED

Git LFS Details

  • SHA256: e9ceeec7fb6c362e9d6ba4d500f95121dc6f5ac95b390f333d37e381dc18665a
  • Pointer size: 131 Bytes
  • Size of remote file: 209 kB
canzy_products_img/canzy_product_CZ816SDR_v2026.png DELETED

Git LFS Details

  • SHA256: 2db868714abd2d59a65340f00bc56aedc03d1d761c9983c4d07e4468d9a9ab91
  • Pointer size: 131 Bytes
  • Size of remote file: 159 kB
canzy_products_img/canzy_product_CZ899.png DELETED

Git LFS Details

  • SHA256: f71adb5745b82b5b62b4f73562f058d3f06482a9314c6a18516a135b0dd73b37
  • Pointer size: 131 Bytes
  • Size of remote file: 373 kB
canzy_products_img/canzy_product_CZ899_v2026.png DELETED

Git LFS Details

  • SHA256: b5af74f401f10ec91f821d8c83253fbaee1b75a8f7ff89aced73ddb784caf7e7
  • Pointer size: 131 Bytes
  • Size of remote file: 198 kB
canzy_products_img/canzy_product_CZ9979GRH.png DELETED

Git LFS Details

  • SHA256: c2052e8ad3d66ed4e17455542bc73a479b9bd811c1a6d7474d091ecdeb778b99
  • Pointer size: 131 Bytes
  • Size of remote file: 700 kB
canzy_products_img/canzy_product_CZ9979GRH_v2026.png DELETED
Binary file (64 kB)
 
canzy_products_img/canzy_product_CZ9979GRS.png DELETED
Binary file (8.23 kB)
 
canzy_products_img/canzy_product_CZ9979GRS_v2026.png DELETED
Binary file (37.1 kB)
 
canzy_products_img/canzy_product_CZ9979GRT.png DELETED
Binary file (27 kB)
 
canzy_products_img/canzy_product_CZ9979GRT_v2026.png DELETED
Binary file (73.7 kB)