Spaces:
Running
Running
Reset main to 9e6dfe0
#21
by bep40 - opened
This view is limited to 50 files because it contains too many changes. See the raw diff here.
- .gitattributes +0 -217
- .github/workflows/auto-sync.yml +0 -30
- .ref_update +1 -0
- README.md +2 -11
- TRIGGER_REBUILD.txt +0 -1
- add-product-fix.js +0 -987
- added_img/bep-tu-2-vung-nau-boss-bki689n-0.jpg +0 -0
- added_img/bt1-big-test-0.jpg +0 -3
- added_img/gx1-test-grob-0.jpg +0 -0
- ai-search-v2.js +0 -1167
- canzy_products_img/canzy_product_CZ007R.png +0 -3
- canzy_products_img/canzy_product_CZ007R_v2026.png +0 -3
- canzy_products_img/canzy_product_CZ03T.png +0 -0
- canzy_products_img/canzy_product_CZ03T_v2026.png +0 -3
- canzy_products_img/canzy_product_CZ09DL.png +0 -3
- canzy_products_img/canzy_product_CZ11EBN.png +0 -3
- canzy_products_img/canzy_product_CZ11EBN_v2026.png +0 -3
- canzy_products_img/canzy_product_CZ11QAMSERIAL80.png +0 -3
- canzy_products_img/canzy_product_CZ11QAMSERIAL80_v2026.png +0 -3
- canzy_products_img/canzy_product_CZ15YA.png +0 -0
- canzy_products_img/canzy_product_CZ15YA_v2026.png +0 -0
- canzy_products_img/canzy_product_CZ16EUGRUNIQUE.png +0 -3
- canzy_products_img/canzy_product_CZ16EUGRUNIQUE_v2026.png +0 -3
- canzy_products_img/canzy_product_CZ34GSS.png +0 -3
- canzy_products_img/canzy_product_CZ34GSS_v2026.png +0 -3
- canzy_products_img/canzy_product_CZ68DT.png +0 -3
- canzy_products_img/canzy_product_CZ68DT_v2026.png +0 -3
- canzy_products_img/canzy_product_CZ700AUTO.png +0 -3
- canzy_products_img/canzy_product_CZ700AUTO_v2026.png +0 -3
- canzy_products_img/canzy_product_CZ70B50.png +0 -3
- canzy_products_img/canzy_product_CZ70B50_v2026.png +0 -3
- canzy_products_img/canzy_product_CZ70C66.png +0 -3
- canzy_products_img/canzy_product_CZ70C66_v2026.png +0 -3
- canzy_products_img/canzy_product_CZ70I50.png +0 -3
- canzy_products_img/canzy_product_CZ70I50_v2026.png +0 -3
- canzy_products_img/canzy_product_CZ780K.png +0 -3
- canzy_products_img/canzy_product_CZ780K_v2026.png +0 -3
- canzy_products_img/canzy_product_CZ806A5G.png +0 -3
- canzy_products_img/canzy_product_CZ806A5G_v2026.png +0 -3
- canzy_products_img/canzy_product_CZ815TDB_v2026.png +0 -3
- canzy_products_img/canzy_product_CZ816SDR.png +0 -3
- canzy_products_img/canzy_product_CZ816SDR_v2026.png +0 -3
- canzy_products_img/canzy_product_CZ899.png +0 -3
- canzy_products_img/canzy_product_CZ899_v2026.png +0 -3
- canzy_products_img/canzy_product_CZ9979GRH.png +0 -3
- canzy_products_img/canzy_product_CZ9979GRH_v2026.png +0 -0
- canzy_products_img/canzy_product_CZ9979GRS.png +0 -0
- canzy_products_img/canzy_product_CZ9979GRS_v2026.png +0 -0
- canzy_products_img/canzy_product_CZ9979GRT.png +0 -0
- 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 |
-
|
|
|
|
| 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 { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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
|
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
|
canzy_products_img/canzy_product_CZ007R_v2026.png
DELETED
Git LFS Details
|
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
|
canzy_products_img/canzy_product_CZ09DL.png
DELETED
Git LFS Details
|
canzy_products_img/canzy_product_CZ11EBN.png
DELETED
Git LFS Details
|
canzy_products_img/canzy_product_CZ11EBN_v2026.png
DELETED
Git LFS Details
|
canzy_products_img/canzy_product_CZ11QAMSERIAL80.png
DELETED
Git LFS Details
|
canzy_products_img/canzy_product_CZ11QAMSERIAL80_v2026.png
DELETED
Git LFS Details
|
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
|
canzy_products_img/canzy_product_CZ16EUGRUNIQUE_v2026.png
DELETED
Git LFS Details
|
canzy_products_img/canzy_product_CZ34GSS.png
DELETED
Git LFS Details
|
canzy_products_img/canzy_product_CZ34GSS_v2026.png
DELETED
Git LFS Details
|
canzy_products_img/canzy_product_CZ68DT.png
DELETED
Git LFS Details
|
canzy_products_img/canzy_product_CZ68DT_v2026.png
DELETED
Git LFS Details
|
canzy_products_img/canzy_product_CZ700AUTO.png
DELETED
Git LFS Details
|
canzy_products_img/canzy_product_CZ700AUTO_v2026.png
DELETED
Git LFS Details
|
canzy_products_img/canzy_product_CZ70B50.png
DELETED
Git LFS Details
|
canzy_products_img/canzy_product_CZ70B50_v2026.png
DELETED
Git LFS Details
|
canzy_products_img/canzy_product_CZ70C66.png
DELETED
Git LFS Details
|
canzy_products_img/canzy_product_CZ70C66_v2026.png
DELETED
Git LFS Details
|
canzy_products_img/canzy_product_CZ70I50.png
DELETED
Git LFS Details
|
canzy_products_img/canzy_product_CZ70I50_v2026.png
DELETED
Git LFS Details
|
canzy_products_img/canzy_product_CZ780K.png
DELETED
Git LFS Details
|
canzy_products_img/canzy_product_CZ780K_v2026.png
DELETED
Git LFS Details
|
canzy_products_img/canzy_product_CZ806A5G.png
DELETED
Git LFS Details
|
canzy_products_img/canzy_product_CZ806A5G_v2026.png
DELETED
Git LFS Details
|
canzy_products_img/canzy_product_CZ815TDB_v2026.png
DELETED
Git LFS Details
|
canzy_products_img/canzy_product_CZ816SDR.png
DELETED
Git LFS Details
|
canzy_products_img/canzy_product_CZ816SDR_v2026.png
DELETED
Git LFS Details
|
canzy_products_img/canzy_product_CZ899.png
DELETED
Git LFS Details
|
canzy_products_img/canzy_product_CZ899_v2026.png
DELETED
Git LFS Details
|
canzy_products_img/canzy_product_CZ9979GRH.png
DELETED
Git LFS Details
|
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)
|
|
|