Spaces:
Running
Running
| // rebuild-trigger: 1778762495 | |
| /** | |
| * V.AI STUDIO — ALL-IN-ONE Boot v18 | |
| * - Purge Malloca from BOTH: DMX + Hafele VN (hafele-vn.com) | |
| * - showDetail(index) navigation | |
| * - AI hook + context search | |
| */ | |
| (function(){ | |
| var _origFetch = window.fetch; | |
| var _D = function(){ return (typeof D !== 'undefined' && D && D.length) ? D : []; | |
| window._vaiGetD=_D; }; | |
| function norm(s){return(s||'').normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[Đđ]/g,'d').toLowerCase();} | |
| function fmt(n){if(!n||isNaN(n))return'LH';return Number(n).toLocaleString('vi-VN')+'đ';} | |
| var PRIORITY_BRANDS=['malloca','grob','canzy','eurogold','garis','demax']; | |
| var STOPWORDS='tìm tim giup giúp cho toi tôi anh chi chị em xin muon muốn cần can co có nao nào loai loại mua kiểu kieu nên nen duoc được mấy may cai cái xem hay hoac hoặc va và với voi the thế nay này kia đó do ấy ay bao lam làm sao gi gì vay vậy hỏi hoi ơi la là một mot nha nhé nhe ạ a ơ ừ uk ok vâng dạ da rồi roi duoc không khong ko sp san pham sản phẩm cua của'.split(' '); | |
| var CATEGORY_MAP={ | |
| 'bep tu':['bep tu','bep dien tu','bep cam ung','induction','bep tu doi','bep tu 2','bep tu 3','bep tu don','bep tu ba'], | |
| 'bep gas':['bep gas','bep ga','gas am','gas doi'], | |
| 'bep hong ngoai':['bep hong ngoai','hong ngoai','infrared'], | |
| 'bep ket hop':['bep gas ket hop','gas ket hop dien','bep ket hop','ket hop tu gas'], | |
| 'may hut mui':['may hut mui','hut mui','hood','hut khoi','khu mui','may hut khoi'], | |
| 'may rua bat':['may rua bat','rua bat','rua chen','may rua chen','dishwasher'], | |
| 'lo nuong':['lo nuong','oven','lo am tu','lo nuong am','lo nuong dien'], | |
| 'lo vi song':['lo vi song','vi song','microwave'], | |
| 'chau rua':['chau rua','bon rua','sink','chau rua chen','chau da','chau inox'], | |
| 'voi rua':['voi rua','faucet','voi rua chen','voi nong lanh','voi chen'], | |
| 'tu lanh':['tu lanh','tu ruou','wine','wine cooler'], | |
| 'may say chen':['may say chen','say chen','say bat','may say bat'], | |
| 'phu kien tu bep':['phu kien tu bep','phu kien bep','ke bat','gia bat','gia dia','tu do kho','ke gia vi','gia gia vi','thung rac','ke xoong','mam xoay','phu kien'], | |
| 'gia bat':['gia bat','gia dia','ke bat','gia nang ha'], | |
| 'tu do kho':['tu do kho','tu kho','he kho'], | |
| 'ray':['ray am','ray bi','ray giam chan','ray hop'], | |
| 'thung rac':['thung rac','thung gao'], | |
| 'khoa cua':['khoa cua','khoa van tay','khoa thong minh','khoa dien tu','smart lock'], | |
| 'tay nam':['tay nam','num tu','tay cam'], | |
| 'may giat':['may giat','may say','washing'], | |
| 'may loc nuoc':['may loc nuoc','loc nuoc','water filter'], | |
| 'tivi':['tivi','tv','television'], | |
| 'dieu hoa':['dieu hoa','may lanh','air conditioner'], | |
| 'noi chien':['noi chien khong dau','air fryer','chien khong dau'], | |
| 'may hut am':['may hut am','hut am','dehumidifier'] | |
| }; | |
| function detectCategory(text){ | |
| var n=norm(text);var bestCat='',bestLen=0; | |
| for(var cat in CATEGORY_MAP){var kws=CATEGORY_MAP[cat];for(var i=0;i<kws.length;i++){if(n.indexOf(kws[i])!==-1&&kws[i].length>bestLen){bestCat=cat;bestLen=kws[i].length;}}} | |
| return bestCat; | |
| } | |
| function detectBrand(text){ | |
| var n=norm(text); | |
| var allBrands=['malloca','grob','canzy','eurogold','garis','demax','boss','hafele','teka','bosch','panasonic','electrolux','samsung','lg','toshiba','sharp']; | |
| for(var i=0;i<allBrands.length;i++){if(n.indexOf(allBrands[i])!==-1)return allBrands[i];} | |
| return ''; | |
| } | |
| function detectPriceRange(text){ | |
| var n=norm(text);var min=0,max=Infinity; | |
| var m1=n.match(/(duoi|<|thap hon|re hon)\s*([\d.,]+)\s*(trieu|tr|t|m)?/);if(m1){max=parsePrice(m1[2],m1[3]);} | |
| var m2=n.match(/(tren|>|tu|toi thieu|lon hon)\s*([\d.,]+)\s*(trieu|tr|t|m)?/);if(m2){min=parsePrice(m2[2],m2[3]);} | |
| var m3=n.match(/(tu|from)\s*([\d.,]+)\s*(trieu|tr|t|m)?\s*(den|toi|to|-)\s*([\d.,]+)\s*(trieu|tr|t|m)?/);if(m3){min=parsePrice(m3[2],m3[3]||m3[6]);max=parsePrice(m3[5],m3[6]);} | |
| var m4=n.match(/(tam|khoang|around|gang)\s*([\d.,]+)\s*(trieu|tr|t|m)?/);if(m4&&max===Infinity){var mid=parsePrice(m4[2],m4[3]);min=mid*0.7;max=mid*1.3;} | |
| if(max===Infinity&&min===0){var m5=n.match(/([\d.,]+)\s*(trieu|tr)\b/);if(m5){if(n.indexOf('duoi')!==-1||n.indexOf('thap')!==-1||n.indexOf('re')!==-1)max=parsePrice(m5[1],m5[2]);}} | |
| return{min:min,max:max}; | |
| } | |
| function parsePrice(numStr,unit){ | |
| var v=parseFloat((numStr||'').replace(/\./g,'').replace(',','.'));if(isNaN(v))return 0; | |
| unit=(unit||'').toLowerCase(); | |
| if(unit==='trieu'||unit==='tr'||unit==='t'||unit==='m')return v*1000000; | |
| if(v>1000)return v;if(v>0&&v<200)return v*1000000;return v; | |
| } | |
| function _vaiIsCabinetAccessory(p){ | |
| var brand=norm(p.brand||''); | |
| var cat=norm(p.cat||p.c||''); | |
| var name=norm(p.name||p.n||''); | |
| return (brand.indexOf('grob')!==-1||brand.indexOf('eurogold')!==-1||brand.indexOf('garis')!==-1) && (cat.indexOf('phu kien tu bep')!==-1||name.indexOf('gia ')!==-1||name.indexOf('ro ')!==-1||name.indexOf('dao')!==-1||name.indexOf('gia vi')!==-1||name.indexOf('thung')!==-1||name.indexOf('ke ')!==-1||name.indexOf('ray')!==-1); | |
| } | |
| function _vaiAccessoryDimQuery(text){ | |
| var n=norm(text||''); | |
| var hasDim=/(kich thuoc|phu bi|phù bì|lot long|lọt lòng|khoang tu|chieu rong tu|kt mat canh|mat canh|tu |tu bep|phu kien|gia vi|dao thot|thung rac|ke goc|ro)/i.test(n); | |
| var m=n.match(/(?:phu bi|lot long|khoang tu|chieu rong tu|tu|rong|r)?\s*(\d{3,4})\s*(?:mm)?/i); | |
| if(!hasDim||!m)return null; | |
| var target=parseInt(m[1],10);if(!target||target<100||target>1200)return null; | |
| var isOuter=/(phu bi|khoang tu|chieu rong tu|kt mat canh|mat canh|tu |tu bep|rong tu)/i.test(n) || !/(lot long|lọt lòng)/i.test(n); | |
| return {target:target,isOuter:isOuter,min:isOuter?target-50:target-8,max:isOuter?target:target+8}; | |
| } | |
| function _vaiAccessoryClearDims(p){ | |
| var vals=[];var specs=p.specs||{}; | |
| function addFrom(v){ | |
| String(v||'').replace(/(?:R|W|W=|R=)?\s*(\d{2,4})\s*(?:mm)?/gi,function(_,num){var x=parseInt(num,10);if(x>=100&&x<=1200&&[201,304].indexOf(x)===-1)vals.push(x);}); | |
| } | |
| if(specs&&typeof specs==='object'){ | |
| for(var k in specs){var kn=norm(k);if(kn.indexOf('kich thuoc san pham')!==-1||kn.indexOf('kich thuoc lot long')!==-1||kn.indexOf('lot long')!==-1){addFrom(specs[k]);}} | |
| } | |
| // Fallback only if no usable spec dimension exists. | |
| if(!vals.length){addFrom((p.name||p.n||'')+' '+(p.summary||p.sum||''));} | |
| var uniq=[];vals.forEach(function(v){if(uniq.indexOf(v)===-1)uniq.push(v);});return uniq; | |
| } | |
| function _vaiAccessoryTypeOk(p,text){ | |
| var n=norm(text||'');var pn=norm((p.name||p.n||'')+' '+(p.cat||p.c||'')); | |
| var groups=[ | |
| [['gia vi','chai lo','chai lọ'],['gia vi','chai lo','chai lọ']], | |
| [['dao thot','dao thớt'],['dao thot','dao thớt']], | |
| [['thung rac','thùng rác'],['thung rac','thùng rác']], | |
| [['thung gao','thùng gạo'],['thung gao','thùng gạo','gao']], | |
| [['gia bat','bát','bat dia','bát đĩa'],['gia bat','bát','bat dia','bát đĩa']], | |
| [['xoong noi','xoong nồi'],['xoong noi','xoong nồi']], | |
| [['khay chia','chia thia','thìa dĩa'],['khay chia','chia thia','thìa dĩa']], | |
| [['ke goc','góc','lien hoan','liên hoàn'],['ke goc','góc','lien hoan','liên hoàn']], | |
| [['tu kho','tủ kho','do kho','đồ khô'],['tu kho','tủ kho','do kho','đồ khô']] | |
| ]; | |
| for(var i=0;i<groups.length;i++){ | |
| var q=groups[i][0], names=groups[i][1], asked=false; | |
| for(var a=0;a<q.length;a++){if(n.indexOf(norm(q[a]))!==-1){asked=true;break;}} | |
| if(asked){for(var b=0;b<names.length;b++){if(pn.indexOf(norm(names[b]))!==-1)return true;}return false;} | |
| } | |
| return true; | |
| } | |
| function _vaiAccessoryNominalDims(p){ | |
| var vals=[];var specs=p.specs||{}; | |
| function addFrom(v){String(v||'').replace(/(?:R|W|KT\s*mặt\s*cánh|mat\s*canh|rộng|rong|khoang\s*tủ|khoang\s*tu|chiều\s*rộng\s*tủ|chieu\s*rong\s*tu)?\s*[≥>=]*\s*(\d{3,4})\s*(?:mm)?/gi,function(_,num){var x=parseInt(num,10);if(x>=100&&x<=1200&&[201,304].indexOf(x)===-1)vals.push(x);});} | |
| if(specs&&typeof specs==='object'){ | |
| for(var k in specs){var kn=norm(k);if(kn.indexOf('chieu rong tu')!==-1||kn.indexOf('khoang tu')!==-1||kn.indexOf('lot long tu')!==-1||kn.indexOf('mat canh')!==-1){addFrom(specs[k]);}} | |
| } | |
| addFrom((p.name||p.n||'')+' '+(p.summary||p.sum||'')); | |
| var uniq=[];vals.forEach(function(v){if(uniq.indexOf(v)===-1)uniq.push(v);});return uniq; | |
| } | |
| function _vaiAccessoryDimMatch(p,dq){ | |
| if(!dq||!_vaiIsCabinetAccessory(p))return null; | |
| var dims=_vaiAccessoryClearDims(p);var noms=_vaiAccessoryNominalDims(p); | |
| // Garis-style mapping: nominal/phủ bì/khoang tủ/mặt cánh can equal target; R/W/quy cách is clear/real accessory size. | |
| var nominalHit=noms.filter(function(v){return Math.abs(v-dq.target)<=3;}); | |
| var clearHits=dims.filter(function(v){return v>dq.min&&v<dq.max;}); | |
| var hits=clearHits.length?clearHits:[]; | |
| if(nominalHit.length&&!hits.length){hits=dims.filter(function(v){return v<dq.target&&v>dq.target-80;});} | |
| if(!hits.length)return null; | |
| hits.sort(function(a,b){return Math.abs(a-dq.target)-Math.abs(b-dq.target);}); | |
| return {dims:dims,nominal:noms,hits:hits,nominalHit:nominalHit}; | |
| } | |
| function extractKeywords(query){ | |
| var n=norm(query);var words=n.split(/[\s,;.!?]+/).filter(function(w){return w.length>=2;}); | |
| var filtered=words.filter(function(w){return STOPWORDS.indexOf(w)===-1;}); | |
| return filtered.length>0?filtered:words.filter(function(w){return w.length>=2;}); | |
| } | |
| function compactNorm(s){return norm(s||'').replace(/[.\-_\s\/]+/g,'');} | |
| function productSearchText(p){return norm((p.name||p.n||'')+' '+(p.sku||'')+' '+(p.model||p.mod||'')+' '+(p.brand||'')+' '+(p.cat||p.c||''));} | |
| function productCodeText(p){return compactNorm((p.sku||'')+' '+(p.model||p.mod||''));} | |
| function isSpecificProductQuery(query,keywords){ | |
| var raw=(query||'').trim(); | |
| if(!raw)return false; | |
| var cat=detectCategory(raw), brand=detectBrand(raw), pr=detectPriceRange(raw), dim=_vaiAccessoryDimQuery(raw); | |
| // Broad category/price queries should still use contextual ranking. | |
| if(dim||pr.min>0||pr.max<Infinity)return false; | |
| var toks=raw.split(/[\s,;:!?'"()\[\]{}]+/).filter(Boolean); | |
| for(var i=0;i<toks.length;i++){ | |
| var t=toks[i]; | |
| // Product codes usually contain digits or separators: K131BL, MH-02I, DRIVEF152B. | |
| if(/[A-Za-z]/.test(t)&&/\d/.test(t)&&compactNorm(t).length>=3)return true; | |
| if(/[.\-_\/]/.test(t)&&compactNorm(t).length>=4)return true; | |
| } | |
| // If user types a concrete name fragment with 2+ meaningful non-category words, treat as product search. | |
| var k=(keywords||[]).filter(function(w){return w.length>=3&&w!==brand;}); | |
| if(cat){ | |
| var catW=[];(CATEGORY_MAP[cat]||[]).forEach(function(kw){kw.split(' ').forEach(function(w){if(w.length>=2)catW.push(w);});}); | |
| k=k.filter(function(w){return catW.indexOf(w)===-1;}); | |
| } | |
| return k.length>=2; | |
| } | |
| function directProductScore(p,query,keywords){ | |
| var qn=norm(query||'').trim();var qc=compactNorm(query||''); | |
| if(!qn||!qc)return 0; | |
| var nameN=norm(p.name||p.n||'');var nameC=compactNorm(p.name||p.n||''); | |
| var skuC=compactNorm(p.sku||'');var modelC=compactNorm(p.model||p.mod||''); | |
| var codeC=(skuC+' '+modelC).trim(); | |
| var score=0; | |
| if(skuC&&qc===skuC)score+=100000; | |
| if(modelC&&qc===modelC)score+=100000; | |
| if(skuC&&skuC.length>=3&&(qc.indexOf(skuC)!==-1||skuC.indexOf(qc)!==-1))score+=80000; | |
| if(modelC&&modelC.length>=3&&(qc.indexOf(modelC)!==-1||modelC.indexOf(qc)!==-1))score+=80000; | |
| if(nameC&&qc.length>=5&&nameC.indexOf(qc)!==-1)score+=60000; | |
| var useful=(keywords||[]).filter(function(w){return w.length>=2;}); | |
| if(useful.length){ | |
| var inName=0,inCode=0; | |
| for(var i=0;i<useful.length;i++){ | |
| var kw=useful[i], kcw=compactNorm(kw); | |
| if(nameN.indexOf(kw)!==-1||nameC.indexOf(kcw)!==-1)inName++; | |
| if(codeC.indexOf(kcw)!==-1)inCode++; | |
| } | |
| if(inCode)score+=50000+inCode*5000; | |
| if(inName===useful.length)score+=40000+inName*3000; | |
| else if(inName>0)score+=12000+inName*1500; | |
| } | |
| return score; | |
| } | |
| function passesHardFilters(p,text,cat,brand,priceRange,dimQuery){ | |
| var nameN=norm(p.name||p.n||'');var brandN=norm(p.brand||'');var catN=norm(p.cat||p.c||''); | |
| var price=p.priceNum||p.pn||0;if(!price)price=parseInt((p.price||p.p||'').toString().replace(/[^\d]/g,''))||0; | |
| if(dimQuery){var dm=_vaiAccessoryDimMatch(p,dimQuery);if(['grob','eurogold','garis'].every(function(b){return brandN.indexOf(b)===-1;}))return false;if(!_vaiAccessoryTypeOk(p,text)||!dm)return false;} | |
| if(cat){var catKws=CATEGORY_MAP[cat]||[];var inCat=false;for(var ci=0;ci<catKws.length;ci++){if(catN.indexOf(catKws[ci])!==-1||nameN.indexOf(catKws[ci])!==-1){inCat=true;break;}}if(!inCat)return false;} | |
| if(brand){if(brandN.indexOf(brand)===-1&&nameN.indexOf(brand)===-1)return false;} | |
| if(price>0){if(price<priceRange.min||price>priceRange.max)return false;}else if(priceRange.max<Infinity||priceRange.min>0){return false;} | |
| return true; | |
| } | |
| function searchProductsByContext(text,limit){ | |
| limit=limit||24;var data=_D();if(!data.length)return[]; | |
| var cat=detectCategory(text);var brand=detectBrand(text);var priceRange=detectPriceRange(text);var dimQuery=_vaiAccessoryDimQuery(text);var keywords=extractKeywords(text); | |
| // v17: If the user types a concrete product code/name, return exact/name matches first. | |
| // This fixes dropdown/AI search showing Grob products just because Grob had a large brand boost. | |
| if(isSpecificProductQuery(text,keywords)){ | |
| var direct=[]; | |
| for(var di=0;di<data.length;di++){ | |
| var dp=data[di];if(!dp)continue; | |
| if(!passesHardFilters(dp,text,cat,brand,priceRange,dimQuery))continue; | |
| var ds=directProductScore(dp,text,keywords); | |
| if(ds>0){var dprice=dp.priceNum||dp.pn||0;direct.push({p:dp,score:ds,price:dprice,idx:di});} | |
| } | |
| if(direct.length){direct.sort(function(a,b){return b.score-a.score||(a.price-b.price);});return direct.slice(0,limit);} | |
| } | |
| if(cat&&CATEGORY_MAP[cat]){var catW=[];CATEGORY_MAP[cat].forEach(function(kw){kw.split(' ').forEach(function(w){if(w.length>=2)catW.push(w);});});keywords=keywords.filter(function(w){return catW.indexOf(w)===-1;});} | |
| if(brand)keywords=keywords.filter(function(w){return w!==brand;}); | |
| keywords=keywords.filter(function(w){return!w.match(/^\d+$/);}); | |
| keywords=keywords.filter(function(w){return['trieu','tr','duoi','tren','den','tam','khoang','gia','tot','nhat','re','dep','chat','luong'].indexOf(w)===-1;}); | |
| var results=[]; | |
| for(var i=0;i<data.length;i++){ | |
| var p=data[i];if(!p)continue; | |
| var nameN=norm(p.name||p.n||'');var brandN=norm(p.brand||'');var catN=norm(p.cat||p.c||''); | |
| var descN=norm(p.summary||p.sum||p.desc||'');var featsN=norm((p.feats||[]).join(' ')); | |
| var specsN='';if(p.specs&&typeof p.specs==='object'){try{specsN=norm(Object.values(p.specs).join(' '));}catch(e){}} | |
| var skuN=norm(p.sku||p.model||p.mod||''); | |
| var price=p.priceNum||p.pn||0;if(!price)price=parseInt((p.price||p.p||'').toString().replace(/[^\d]/g,''))||0; | |
| var img=p.image||p.img||p.i||'';var score=0;var dimMatch=_vaiAccessoryDimMatch(p,dimQuery);if(dimQuery){if(['grob','eurogold','garis'].every(function(b){return brandN.indexOf(b)===-1;}))continue;if(!_vaiAccessoryTypeOk(p,text)||!dimMatch)continue;}if(dimMatch)score+=80; | |
| if(cat){var catKws=CATEGORY_MAP[cat]||[];var inCat=false;for(var ci=0;ci<catKws.length;ci++){if(catN.indexOf(catKws[ci])!==-1||nameN.indexOf(catKws[ci])!==-1){inCat=true;break;}}if(!inCat)continue;score+=30;} | |
| if(brand){if(brandN.indexOf(brand)!==-1||nameN.indexOf(brand)!==-1)score+=25;else continue;} | |
| if(price>0){if(price<priceRange.min||price>priceRange.max)continue;score+=10;}else if(priceRange.max<Infinity){continue;} | |
| var kwMatched=0;var kwNameMatched=0;var kwCodeMatched=0; | |
| for(var w=0;w<keywords.length;w++){var kw=keywords[w];var kwc=compactNorm(kw);if(nameN.indexOf(kw)!==-1){score+=35;kwMatched++;kwNameMatched++;}else if(skuN.indexOf(kw)!==-1||productCodeText(p).indexOf(kwc)!==-1){score+=45;kwMatched++;kwCodeMatched++;}else if(descN.indexOf(kw)!==-1){score+=8;kwMatched++;}else if(featsN.indexOf(kw)!==-1){score+=8;kwMatched++;}else if(specsN.indexOf(kw)!==-1){score+=6;kwMatched++;}else if(brandN.indexOf(kw)!==-1){score+=5;kwMatched++;}} | |
| if(kwMatched>=2)score+=kwMatched*8; | |
| if(kwNameMatched===keywords.length&&keywords.length>0)score+=70; | |
| if(kwCodeMatched>0)score+=90; | |
| // Only apply Grob/Eurogold/Garis boost for broad accessory/category queries, not for concrete name/code searches. | |
| if(!keywords.length||dimQuery||cat){if(_vaiIsCabinetAccessory(p)){if(brandN.indexOf('grob')!==-1)score+=60;else if(brandN.indexOf('eurogold')!==-1)score+=35;else if(brandN.indexOf('garis')!==-1)score+=15;}} | |
| for(var bi=0;bi<PRIORITY_BRANDS.length;bi++){if(brandN.indexOf(PRIORITY_BRANDS[bi])!==-1){score+=5-bi;break;}} | |
| // Pure text query must match at least one keyword; brand/category boost alone must not flood dropdown with Grob. | |
| if(keywords.length>0&&!cat&&!brand&&priceRange.max===Infinity&&priceRange.min===0&&!dimQuery&&kwMatched===0)continue; | |
| if(score>0){results.push({p:p,score:score,price:price,idx:i});} | |
| } | |
| results.sort(function(a,b){return b.score-a.score||(a.price-b.price);}); | |
| return results.slice(0,limit); | |
| } | |
| function detectCategories(text){ | |
| var n=norm(text||'');var cats=[]; | |
| for(var cat in CATEGORY_MAP){ | |
| var kws=CATEGORY_MAP[cat]; | |
| for(var i=0;i<kws.length;i++){ | |
| if(n.indexOf(kws[i])!==-1){cats.push(cat);break;} | |
| } | |
| } | |
| // Avoid overly broad kitchen accessory category swallowing specific subtypes unless explicitly asked. | |
| if(cats.length>1&&cats.indexOf('phu kien tu bep')!==-1){ | |
| var explicit=/phu kien tu bep|phu kien bep/i.test(n); | |
| if(!explicit)cats=cats.filter(function(c){return c!=='phu kien tu bep';}); | |
| } | |
| return cats; | |
| } | |
| function productFullText(p){ | |
| var specs=''; | |
| if(p.specs&&typeof p.specs==='object'){ | |
| try{for(var k in p.specs){specs+=' '+k+' '+p.specs[k];}}catch(e){} | |
| } | |
| return norm((p.name||p.n||'')+' '+(p.cat||p.c||'')+' '+(p.brand||'')+' '+(p.summary||p.sum||'')+' '+(p.desc||'')+' '+(p.feats||[]).join(' ')+' '+specs); | |
| } | |
| function specPairsText(p){ | |
| var arr=[]; | |
| if(p.specs&&typeof p.specs==='object'){ | |
| try{for(var k in p.specs){arr.push({k:norm(k),v:norm(String(p.specs[k]||'')),raw:k+': '+p.specs[k]});}}catch(e){} | |
| } | |
| return arr; | |
| } | |
| function detectSpecConstraints(text){ | |
| var n=norm(text||'');var cs=[];var m; | |
| // Ống thoát / phi / Ø diameter, e.g. "ống thoát phi 150mm", "đường ống Ø150". | |
| m=n.match(/(?:ong\s*thoat|duong\s*ong|ong\s*khoi|phi|ø|d\s*=?)\D{0,25}(\d{2,3})\s*(?:mm)?/i) || n.match(/(\d{2,3})\s*(?:mm)?\D{0,20}(?:ong\s*thoat|duong\s*ong|phi|ø)/i); | |
| if(m)cs.push({type:'diameter',value:parseInt(m[1],10),label:'ống thoát Ø'+parseInt(m[1],10)}); | |
| // Công suất hút, supports >=/<= hints. | |
| m=n.match(/(?:cong\s*suat\s*hut|suc\s*hut|hut\s*manh|m3\/?h)\D{0,20}(\d{3,4})/i); | |
| if(m)cs.push({type:'suction',value:parseInt(m[1],10),op:(/duoi|nho hon|<=|toi da/i.test(n)?'<=':'>='),label:'công suất hút '+parseInt(m[1],10)+'m³/h'}); | |
| // Độ ồn. | |
| m=n.match(/(?:do\s*on|ồn|db)\D{0,16}(\d{2})/i); | |
| if(m)cs.push({type:'noise',value:parseInt(m[1],10),op:(/tren|lon hon|>=/i.test(n)?'>=':'<='),label:'độ ồn '+parseInt(m[1],10)+'dB'}); | |
| // Width in cm/mm: "rộng 70cm", "ngang 90cm". Do NOT add this approximate constraint for "dưới/trên 800mm" queries; maxdim handles those strictly. | |
| if(!/(duoi|nho hon|<|toi da|khong qua|tren|lon hon|>|toi thieu)\D{0,20}\d{2,4}\s*(cm|mm)?/i.test(n)){ | |
| m=n.match(/(?:rong|ngang|chieu\s*rong|kich\s*thuoc)\D{0,16}(\d{2,4})\s*(cm|mm)?/i); | |
| if(m){var val=parseInt(m[1],10);var unit=m[2]||'';if(unit==='cm'||val<200)val*=10; if(val>=300&&val<=1400)cs.push({type:'width',value:val,label:'rộng '+val+'mm'});} | |
| } | |
| // Generic product dimension: e.g. "chậu rửa dưới 800mm" means largest listed dimension must be < 800mm (790/780...), not equal 800. | |
| var dm=n.match(/(?:duoi|nho hon|<|toi da|khong qua)\s*(\d{2,4})\s*(cm|mm)?/i) || n.match(/(?:kich\s*thuoc|rong|ngang|dai|chieu\s*rong)\D{0,18}(?:duoi|nho hon|<|toi da|khong qua)\D{0,8}(\d{2,4})\s*(cm|mm)?/i); | |
| if(dm){var dv=parseInt(dm[1],10);var du=dm[2]||'';if(du==='cm'||dv<200)dv*=10;if(dv>=300&&dv<=2000)cs.push({type:'maxdim',op:'<',value:dv,role:(/dai|dài|rong|rộng|ngang|width/.test(n)?'width':(/sau|sâu|depth/.test(n)?'depth':(/cao|height/.test(n)?'height':(isAccessoryDimensionQuery(text)?'width':'max')))),label:((/dai|dài|rong|rộng|ngang|width/.test(n)?'chiều dài/ngang':(/sau|sâu|depth/.test(n)?'chiều sâu':(/cao|height/.test(n)?'chiều cao':'kích thước')))+' dưới '+dv+'mm')});} | |
| var dm2=n.match(/(?:tren|lon hon|>|toi thieu)\s*(\d{2,4})\s*(cm|mm)?/i); | |
| if(dm2){var dv2=parseInt(dm2[1],10);var du2=dm2[2]||'';if(du2==='cm'||dv2<200)dv2*=10;if(dv2>=300&&dv2<=2000)cs.push({type:'maxdim',op:'>',value:dv2,label:'kích thước trên '+dv2+'mm'});} | |
| var wm=n.match(/(?:duoi|nho hon|<|toi da|khong qua)\s*(\d{2,5})\s*w\b/i)||n.match(/(?:cong\s*suat|dien|dong\s*co|motor)\D{0,22}(?:duoi|nho hon|<|toi da|khong qua)\D{0,8}(\d{2,5})\s*w\b/i); | |
| if(wm){var wv=parseInt(wm[1],10);if(wv>0&&wv<=10000)cs.push({type:'powerw',op:'<',value:wv,label:'công suất dưới '+wv+'W'});} | |
| // Feature keywords as soft technical constraints. | |
| var featureMap=[ | |
| {re:/bldc|dc inverter|inverter/i,label:'động cơ BLDC/Inverter',terms:['bldc','inverter']}, | |
| {re:/cam ung|cảm ứng/i,label:'điều khiển cảm ứng',terms:['cam ung','cảm ứng']}, | |
| {re:/cu chi|vay tay|khong cham|không chạm/i,label:'điều khiển cử chỉ/không chạm',terms:['cu chi','khong cham','cử chỉ','không chạm']}, | |
| {re:/than hoat tinh|than hoạt tính/i,label:'than hoạt tính',terms:['than hoat tinh','than hoạt tính']}, | |
| {re:/say khi nong|sấy khí nóng|hot air/i,label:'sấy khí nóng',terms:['say khi nong','sấy khí nóng','hot air']}, | |
| {re:/uv|khang khuan|kháng khuẩn/i,label:'UV/kháng khuẩn',terms:['uv','khang khuan','kháng khuẩn']} | |
| ]; | |
| featureMap.forEach(function(f){if(f.re.test(n))cs.push({type:'feature',label:f.label,terms:f.terms});}); | |
| return cs; | |
| } | |
| function numberNearText(txt,patterns){ | |
| var hits=[]; | |
| patterns.forEach(function(re){var m;while((m=re.exec(txt))!==null){var v=parseFloat((m[1]||m[2]||'').replace(',','.'));if(!isNaN(v))hits.push(v);}}); | |
| return hits; | |
| } | |
| function productMatchesSpecConstraints(p,constraints){ | |
| if(!constraints||!constraints.length)return {ok:true,score:0,labels:[]}; | |
| var full=productFullText(p);var pairs=specPairsText(p);var score=0;var labels=[]; | |
| for(var i=0;i<constraints.length;i++){ | |
| var c=constraints[i];var ok=false; | |
| if(c.type==='diameter'){ | |
| var target=String(c.value); | |
| for(var j=0;j<pairs.length;j++){ | |
| var kv=pairs[j]; | |
| if((kv.k.indexOf('ong')!==-1||kv.k.indexOf('thoat')!==-1||kv.v.indexOf('ong')!==-1||kv.v.indexOf('thoat')!==-1||kv.k.indexOf('duong')!==-1) && kv.v.indexOf(target)!==-1){ok=true;break;} | |
| } | |
| if(!ok){ok=new RegExp('(?:ø|phi|ong\\s*thoat|duong\\s*ong)[^0-9]{0,12}'+target+'|'+target+'[^0-9]{0,12}(?:ø|phi|ong\\s*thoat|duong\\s*ong)','i').test(full);} | |
| if(ok){score+=80;labels.push(c.label);}else return {ok:false,score:0,labels:labels}; | |
| }else if(c.type==='suction'){ | |
| var nums=numberNearText(full,[/(?:cong\s*suat\s*hut|suc\s*hut|hut)\D{0,18}(\d{3,4})/ig,/(\d{3,4})\s*m3\/?h/ig]); | |
| ok=nums.some(function(v){return c.op==='<='?v<=c.value:v>=c.value;}); | |
| if(ok){score+=45;labels.push(c.label);}else return {ok:false,score:0,labels:labels}; | |
| }else if(c.type==='noise'){ | |
| var nms=numberNearText(full,[/(?:do\s*on|ồn)\D{0,14}(\d{2})/ig,/(\d{2})\s*db/ig]); | |
| ok=nms.some(function(v){return c.op==='>='?v>=c.value:v<=c.value;}); | |
| if(ok){score+=35;labels.push(c.label);}else return {ok:false,score:0,labels:labels}; | |
| }else if(c.type==='width'){ | |
| var widths=numberNearText(full,[/(?:chieu\s*rong\s*san\s*pham|rong|ngang)\D{0,18}(\d{2,4})/ig]); | |
| ok=widths.some(function(v){if(v<200)v*=10;return Math.abs(v-c.value)<=30;}); | |
| if(ok){score+=30;labels.push(c.label);}else return {ok:false,score:0,labels:labels}; | |
| }else if(c.type==='maxdim'){ | |
| var dimObjs=(typeof extractProductDimensions==='function')?extractProductDimensions(p):[];var role=c.role||'max';var vals=dimObjs.filter(function(d){return role==='max'||d.role===role||d.role==='dim';}).map(function(d){return d.v;}); | |
| if(vals.length){var chosen=(role==='max')?Math.max.apply(null,vals):Math.min.apply(null,vals);ok=c.op==='<'?chosen<c.value:chosen>c.value;} | |
| if(ok){score+=55;labels.push(c.label);}else return {ok:false,score:0,labels:labels}; | |
| }else if(c.type==='powerw'){ | |
| var wvals=[];full.replace(/(?:cong\s*suat|dien\s*nang|motor|dong\s*co)?\D{0,16}(\d{2,5})\s*w\b/ig,function(_,a){var v=parseInt(a,10);if(v>0&&v<=10000)wvals.push(v);}); | |
| ok=wvals.length?wvals.some(function(v){return c.op==='<'?v<c.value:v>c.value;}):true; | |
| if(ok){score+=25;labels.push(c.label);}else return {ok:false,score:0,labels:labels}; | |
| }else if(c.type==='feature'){ | |
| ok=c.terms.some(function(t){return full.indexOf(norm(t))!==-1;}); | |
| if(ok){score+=22;labels.push(c.label);}else return {ok:false,score:0,labels:labels}; | |
| } | |
| } | |
| return {ok:true,score:score,labels:labels}; | |
| } | |
| function isValidCategoryProduct(p,cat){ | |
| var name=norm(p.name||p.n||'');var catN=norm(p.cat||p.c||'');var full=name+' '+catN; | |
| // Exclude accessories/cleaning consumables that mention appliance names but are not the appliance. | |
| var rejectCommon=/tay\s*rua|tẩy\s*rửa|dung\s*dich|dung\s*dịch|ve\s*sinh|vệ\s*sinh|nuoc\s*rua|nước\s*rửa|chai|lo\s*nuoc|lọ\s*nước|bo\s*ve\s*sinh|bộ\s*vệ\s*sinh/.test(full); | |
| if(cat==='bep tu'){ | |
| var nameOnly=norm(p.name||p.n||''); | |
| if(rejectCommon||/chao|chảo|noi\s|nồi\s|vi\s|vỉ\s|khay|mat\s*kinh|mặt\s*kính|dao\s*cao|khăn|khan/.test(full))return false; | |
| // Query bếp từ must not pull bếp gas just because a wrong category contains generic "bếp từ". | |
| if(/bep\s*gas|bếp\s*gas|bep\s*ga|bếp\s*ga|gas\s*2|gas\s*am|gas\s*âm/.test(nameOnly))return false; | |
| // Require product NAME itself to indicate induction/electric hob, not only category text. | |
| return /bep\s*(tu|dien\s*tu|cam\s*ung)|bếp\s*(từ|điện\s*từ|cảm\s*ứng)|induction/.test(nameOnly); | |
| } | |
| if(cat==='may hut mui'){ | |
| if(rejectCommon||/than\s*hoat\s*tinh\s*(thay|bo|bộ)|ong\s*thoat\s*(roi|rời)|phu\s*kien|phụ\s*kiện/.test(full))return false; | |
| return /may\s*hut|máy\s*hút|hut\s*mui|hút\s*mùi|hut\s*khoi|hút\s*khói|khu\s*mui|khử\s*mùi/.test(full); | |
| } | |
| if(cat==='chau rua'){ | |
| if(rejectCommon||/bo\s*xa|bộ\s*xả|ro\s*loc|rổ\s*lọc/.test(full))return false; | |
| return /chau\s*rua|chậu\s*rửa|bon\s*rua|bồn\s*rửa|sink/.test(full); | |
| } | |
| if(cat==='voi rua'){ | |
| if(rejectCommon||/day\s*cap|dây\s*cấp|loi\s*tron|lõi\s*trộn/.test(full))return false; | |
| return /voi\s*rua|vòi\s*rửa|faucet/.test(full); | |
| } | |
| return true; | |
| } | |
| function productDedupeKey(p){ | |
| var m=compactNorm(p.model||p.mod||''); | |
| if(m)return m; | |
| return compactNorm((p.name||p.n||'').replace(/\|.*$/,'')); | |
| } | |
| function searchProductsForCategory(text,cat,limit,extraConstraints,priceMax){ | |
| var data=_D();var out=[];var catKws=CATEGORY_MAP[cat]||[];var brand=detectBrand(text);var keywords=extractKeywords(text); | |
| // Remove category words so query terms like "bếp từ" don't penalize hoods in combo mode. | |
| var catW=[];catKws.forEach(function(kw){kw.split(' ').forEach(function(w){if(w.length>=2)catW.push(w);});}); | |
| keywords=keywords.filter(function(w){return catW.indexOf(w)===-1&&['duoi','tren','trieu','tr','tong','cong','combo','va','và'].indexOf(w)===-1&&!/^\d+$/.test(w);}); | |
| for(var i=0;i<data.length;i++){ | |
| var p=data[i];if(!p)continue; | |
| var nameN=norm(p.name||p.n||'');var catN=norm(p.cat||p.c||'');var brandN=norm(p.brand||'');var price=p.priceNum||p.pn||0;if(!price)price=parseInt((p.price||p.p||'').toString().replace(/[^\d]/g,''))||0; | |
| var inCat=false;for(var ci=0;ci<catKws.length;ci++){if(catN.indexOf(catKws[ci])!==-1||nameN.indexOf(catKws[ci])!==-1){inCat=true;break;}} | |
| if(!inCat)continue; | |
| if(!productPassSubtypeFilter(p,text))continue; | |
| if(!isValidCategoryProduct(p,cat))continue; | |
| if(brand&&(brandN.indexOf(brand)===-1&&nameN.indexOf(brand)===-1))continue; | |
| if(priceMax&&price>priceMax)continue; | |
| var sm=productMatchesSpecConstraints(p,extraConstraints||[]);if(!sm.ok)continue; | |
| var score=30+sm.score; | |
| if(price>0)score+=Math.max(0,18-Math.floor(price/2000000)); | |
| if(cat==='bep tu'){ | |
| var explicitPortable=/de\s*ban|để\s*bàn|mini|don|đơn|portable/i.test(norm(text)); | |
| if(/de\s*ban|để\s*bàn|bep\s*tu\s*don|bếp\s*từ\s*đơn|mini/.test(nameN)&&!explicitPortable)score-=45; | |
| if(/am|âm|2\s*vung|2\s*vùng|doi|đôi|hai\s*vung|hai\s*vùng/.test(nameN))score+=28; | |
| if(/3\s*vung|3\s*vùng|4\s*vung|4\s*vùng/.test(nameN))score+=12; | |
| } | |
| if(cat==='may hut mui'){ | |
| if(/am\s*tu|âm\s*tủ|gan\s*tu|gắn\s*tủ|ap\s*tuong|áp\s*tường/.test(nameN))score+=15; | |
| if(/ecokitchen/.test(nameN))score+=4; | |
| } | |
| if(brandN.indexOf('malloca')!==-1)score+=12;else if(brandN.indexOf('grob')!==-1)score+=10;else if(brandN.indexOf('eurogold')!==-1)score+=7; | |
| keywords.forEach(function(kw){var ft=productFullText(p);if(nameN.indexOf(kw)!==-1)score+=12;else if(ft.indexOf(kw)!==-1)score+=5;}); | |
| out.push({p:p,score:score,price:price,labels:sm.labels,idx:i,cat:cat}); | |
| } | |
| out.sort(function(a,b){return b.score-a.score||(a.price-b.price);}); | |
| var dedup=[];var seen={}; | |
| for(var oi=0;oi<out.length;oi++){ | |
| var key=productDedupeKey(out[oi].p)||('idx'+out[oi].idx); | |
| if(seen[key])continue;seen[key]=1;dedup.push(out[oi]); | |
| } | |
| return dedup.slice(0,limit||12); | |
| } | |
| function buildBudgetCombos(groups,budget,limit){ | |
| if(!groups||groups.length<2||!budget||budget===Infinity)return[]; | |
| var combos=[]; | |
| function rec(pos,items,total,score){ | |
| if(pos===groups.length){if(total<=budget)combos.push({items:items.slice(),total:total,score:score});return;} | |
| var g=groups[pos].items.slice(0,10); | |
| for(var i=0;i<g.length;i++){ | |
| var r=g[i];var pr=r.price||0;if(!pr||total+pr>budget)continue; | |
| items.push(r);rec(pos+1,items,total+pr,score+r.score);items.pop(); | |
| } | |
| } | |
| rec(0,[],0,0); | |
| combos.forEach(function(c){ | |
| var util=budget?Math.min(1,c.total/budget):0; | |
| // Prefer useful combos that utilize the budget reasonably, not just the cheapest possible pair. | |
| c.rank=c.score+util*120-(util<0.45?35:0); | |
| }); | |
| combos.sort(function(a,b){return b.rank-a.rank||b.total-a.total;});return combos.slice(0,limit||6); | |
| } | |
| function renderNaturalResults(nr,res){ | |
| var fmt=function(n){return (!n||isNaN(n))?'LH':Number(n).toLocaleString('vi-VN')+'đ';}; | |
| var h=''; | |
| h+='<div style="margin-bottom:10px;display:flex;gap:6px;flex-wrap:wrap">'; | |
| h+='<span style="background:#ecfeff;color:#155e75;border:1px solid #a5f3fc;padding:3px 8px;border-radius:20px;font-size:11px;font-weight:700">🧠 Tìm kiếm tự nhiên</span>'; | |
| if(nr.categories&&nr.categories.length)h+='<span style="background:#eff6ff;color:#1e40af;border:1px solid #bfdbfe;padding:3px 8px;border-radius:20px;font-size:11px;font-weight:700">📂 '+nr.categories.join(' + ')+'</span>'; | |
| if(nr.budget&&nr.budget<Infinity)h+='<span style="background:#f0fdf4;color:#166534;border:1px solid #bbf7d0;padding:3px 8px;border-radius:20px;font-size:11px;font-weight:700">💰 Tổng ≤ '+fmt(nr.budget)+'</span>'; | |
| if(nr.constraints&&nr.constraints.length)h+=nr.constraints.map(function(c){return '<span style="background:#fff7ed;color:#9a3412;border:1px solid #fed7aa;padding:3px 8px;border-radius:20px;font-size:11px;font-weight:700">⚙️ '+c.label+'</span>';}).join(''); | |
| h+='</div>'; | |
| if(nr.combos&&nr.combos.length){ | |
| h+='<div style="font-size:13px;font-weight:800;color:#003f62;margin:8px 0">✅ Combo phù hợp ngân sách tổng</div>'; | |
| nr.combos.forEach(function(c,ci){ | |
| h+='<div style="background:#fff;border:1px solid #c7d2fe;border-radius:12px;padding:10px;margin-bottom:10px"><div style="display:flex;justify-content:space-between;gap:8px;align-items:center;margin-bottom:8px"><b style="color:#1e3a8a">Combo '+(ci+1)+'</b><b style="color:#059669">Tổng: '+fmt(c.total)+'</b></div>'; | |
| h+='<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(155px,1fr));gap:8px">'; | |
| c.items.forEach(function(r){var p=r.p;var idx=_D().indexOf(p);var img=p.image||p.img||p.i||'';h+='<div onclick="showDetail('+idx+')" style="cursor:pointer;border:1px solid #e2e8f0;border-radius:10px;overflow:hidden;background:#f8fafc">';if(img)h+='<div style="height:70px;display:flex;align-items:center;justify-content:center;background:#fff"><img src="'+img+'" style="max-height:66px;max-width:100%;object-fit:contain"></div>';h+='<div style="padding:7px"><div style="font-size:10px;color:#64748b;font-weight:700">'+r.cat+'</div><div style="font-size:11px;font-weight:700;color:#003f62;height:30px;overflow:hidden">'+(p.name||p.n||'').substring(0,56)+'</div><div style="font-size:12px;font-weight:900;color:#059669;margin-top:3px">'+(p.price||p.p||'LH')+'</div></div></div>';}); | |
| h+='</div></div>'; | |
| }); | |
| } | |
| if(nr.groups&&nr.groups.length){ | |
| nr.groups.forEach(function(g){ | |
| h+='<div style="font-size:13px;font-weight:800;color:#003f62;margin:10px 0 6px">📌 '+g.cat+' ('+g.items.length+' SP)</div>'; | |
| h+='<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:10px">'; | |
| g.items.slice(0,12).forEach(function(r){var p=r.p;var idx=_D().indexOf(p);var img=p.image||p.img||p.i||'';var specs=p.specs||{};var sk=Object.entries(specs).filter(function(kv){return /ống|ong|hút|hut|ồn|on|rộng|rong|đường|duong/i.test(kv[0]);}).slice(0,3);h+='<div style="background:#fff;border:1px solid #e2e8f0;border-radius:10px;overflow:hidden;cursor:pointer" onclick="showDetail('+idx+')">';if(img)h+='<div style="height:90px;background:#f8fafc;display:flex;align-items:center;justify-content:center"><img src="'+img+'" style="max-width:100%;max-height:85px;object-fit:contain"></div>';h+='<div style="padding:8px"><div style="font-size:11px;font-weight:700;color:#003f62;line-height:1.3;height:31px;overflow:hidden">'+(p.name||p.n||'').substring(0,58)+'</div><div style="font-size:9px;color:#64748b;margin-top:2px">'+(p.brand||'')+' | '+(p.sku||p.mod||'')+'</div>';if(r.labels&&r.labels.length)h+='<div style="font-size:9px;color:#9a3412;margin-top:2px">✓ '+r.labels.join(', ')+'</div>';if(sk.length){h+='<div style="font-size:8px;color:#555;border-top:1px solid #f1f5f9;margin-top:4px;padding-top:3px">';sk.forEach(function(kv){h+=kv[0]+': <b>'+kv[1]+'</b><br>';});h+='</div>';}h+='<div style="font-size:13px;font-weight:900;color:#059669;margin-top:4px">'+(p.price||p.p||'LH')+'</div></div><div style="display:flex;gap:4px;padding:0 6px 6px"><button onclick="event.stopPropagation();if(typeof addToCart===\'function\')addToCart('+idx+')" style="flex:1;padding:4px;background:#db9815;color:#fff;border:none;border-radius:5px;font-size:9px;font-weight:700;cursor:pointer">+ Giỏ</button><button onclick="event.stopPropagation();window._vaiAddOrder(\''+(p.slug||'')+'\',this)" style="flex:1;padding:4px;background:#003f62;color:#fff;border:none;border-radius:5px;font-size:9px;font-weight:700;cursor:pointer">+ Đơn</button></div></div>';}); | |
| h+='</div>'; | |
| }); | |
| } | |
| if(!h||(!(nr.combos&&nr.combos.length)&&!(nr.groups&&nr.groups.some(function(g){return g.items.length;}))))h='❌ Chưa tìm thấy sản phẩm khớp đúng thông số/ngân sách trong dữ liệu hiện có. Anh/chị thử nới điều kiện hoặc nhắn Zalo 0981873395.'; | |
| res.innerHTML=h; | |
| } | |
| function extractProductDimensions(p){ | |
| var dims=[];var specs=p.specs||{}; | |
| function add(v,role,source){ | |
| v=parseInt(v,10);if(!v)return;if(v<200)v*=10; | |
| if(v>=250&&v<=2500&&[201,202,304,316,430,220,240,50,60].indexOf(v)===-1)dims.push({v:v,role:role||'dim',source:source||''}); | |
| } | |
| function parseDimString(s,role,source){ | |
| s=String(s||'').toLowerCase(); | |
| // Prefer full dimension groups: 790 x 500 x 220 mm | |
| s.replace(/(\d{2,4})\s*(?:x|×|\*)\s*(\d{2,4})(?:\s*(?:x|×|\*)\s*(\d{2,4}))?/ig,function(_,a,b,c){add(a,'width',source);add(b,'depth',source);if(c)add(c,'height',source);}); | |
| // Contextual single dimensions only; avoid random material/thickness/power numbers. | |
| s.replace(/(?:rộng|rong|ngang|width|w)\D{0,12}(\d{2,4})\s*(?:mm|cm)?/ig,function(_,a){add(a,'width',source);}); | |
| s.replace(/(?:dài|dai|length|l)\D{0,12}(\d{2,4})\s*(?:mm|cm)?/ig,function(_,a){add(a,'length',source);}); | |
| s.replace(/(?:sâu|sau|depth|d)\D{0,12}(\d{2,4})\s*(?:mm|cm)?/ig,function(_,a){add(a,'depth',source);}); | |
| s.replace(/(?:cao|height|h)\D{0,12}(\d{2,4})\s*(?:mm|cm)?/ig,function(_,a){add(a,'height',source);}); | |
| } | |
| if(specs&&typeof specs==='object'){ | |
| for(var k in specs){var kn=norm(k);if(/kich thuoc|kích thước|rong|rộng|ngang|dai|dài|sau|sâu|cao|size|dimension/.test(kn)){parseDimString(String(specs[k]||''),/rong|rộng|ngang/.test(kn)?'width':'dim',k);}} | |
| } | |
| parseDimString((p.name||p.n||'')+' '+(p.summary||p.sum||''),'dim','name'); | |
| var seen={},out=[];dims.forEach(function(d){var key=d.v+'-'+d.role;if(!seen[key]){seen[key]=1;out.push(d);}});return out; | |
| } | |
| function genericNumbersFromProduct(p){return extractProductDimensions(p).map(function(d){return d.v;});} | |
| function isAccessoryDimensionQuery(text){var n=norm(text||'');return /(gia vi|gia gia vi|ke gia vi|chai lo|dao thot|thung rac|gia bat|bat dia|xoong noi|tu kho|phu kien|ray)/.test(n);} | |
| function detectGenericMaxDimQuery(text){ | |
| var n=norm(text||'');var m=n.match(/(?:duoi|nho hon|<|toi da|khong qua)\s*(\d{2,4})\s*(cm|mm)?/i)||n.match(/(?:kich\s*thuoc|rong|ngang|dai|chieu\s*rong)\D{0,22}(?:duoi|nho hon|<|toi da|khong qua)\D{0,8}(\d{2,4})\s*(cm|mm)?/i); | |
| if(!m)return null;var v=parseInt(m[1],10);var u=m[2]||'';if(u==='cm'||v<200)v*=10;if(v>=300&&v<=2000)return {op:'<',value:v,role:(isAccessoryDimensionQuery(text)?'width':'max'),label:(isAccessoryDimensionQuery(text)?'kích thước tủ/rộng dưới ':'kích thước dưới ')+v+'mm'};return null; | |
| } | |
| function detectDimensionRoleQuery(text){ | |
| var n=norm(text||''); | |
| var role='max'; | |
| if(/\b(dai|dài|length|ngang|rong|rộng|width)\b/.test(n))role='width'; | |
| else if(/\b(sau|sâu|depth)\b/.test(n))role='depth'; | |
| else if(/\b(cao|height)\b/.test(n))role='height'; | |
| var m=n.match(/(?:dai|dài|length|ngang|rong|rộng|width|sau|sâu|depth|cao|height)\D{0,20}(?:duoi|nho hon|<|toi da|khong qua)\D{0,8}(\d{2,4})\s*(cm|mm)?/i)||n.match(/(?:duoi|nho hon|<|toi da|khong qua)\D{0,8}(\d{2,4})\s*(cm|mm)?\D{0,20}(?:dai|dài|length|ngang|rong|rộng|width|sau|sâu|depth|cao|height)/i); | |
| if(!m)return null;var v=parseInt(m[1],10);var u=m[2]||'';if(u==='cm'||v<200)v*=10;if(v>=250&&v<=2500)return {op:'<',value:v,role:role,label:(role==='width'?'chiều dài/ngang':role==='depth'?'chiều sâu':role==='height'?'chiều cao':'kích thước')+' dưới '+v+'mm'};return null; | |
| } | |
| function detectPowerWQuery(text){ | |
| var n=norm(text||'');var m=n.match(/(?:duoi|nho hon|<|toi da|khong qua)\s*(\d{2,5})\s*w\b/i)||n.match(/(?:cong\s*suat|dien|dong\s*co|motor)\D{0,22}(?:duoi|nho hon|<|toi da|khong qua)\D{0,8}(\d{2,5})\s*w\b/i); | |
| if(!m)return null;var v=parseInt(m[1],10);if(v>0&&v<=10000)return {op:'<',value:v,label:'công suất dưới '+v+'W'};return null; | |
| } | |
| function productPowerWatts(p){ | |
| var full=productFullText(p);var vals=[];full.replace(/(?:cong\s*suat|dien\s*nang|motor|dong\s*co|động\s*cơ)?\D{0,16}(\d{2,5})\s*w\b/ig,function(_,a){var v=parseInt(a,10);if(v>0&&v<=10000)vals.push(v);});return vals; | |
| } | |
| function fallbackTechnicalSearch(text,limit){ | |
| limit=limit||24;var cats=detectCategories(text);if(!cats.length){var dc=detectCategory(text);if(dc)cats=[dc];} | |
| var roleDim=detectDimensionRoleQuery(text);var md=roleDim||detectGenericMaxDimQuery(text);var pw=detectPowerWQuery(text);if(!md&&!pw)return null; | |
| var out=[];var data=_D(); | |
| for(var i=0;i<data.length;i++){ | |
| var p=data[i];if(!p)continue;if(!productPassQueryHardFilters(p,text))continue;var okCat=!cats.length||cats.some(function(cat){return isValidCategoryProduct(p,cat);});if(!okCat)continue; | |
| var score=0, labels=[]; | |
| if(md){var dimObjs=extractProductDimensions(p);var role=md.role||(/dai|dài|rong|rộng|ngang|width/i.test(norm(text))?'width':(/sau|sâu|depth/i.test(norm(text))?'depth':(/cao|height/i.test(norm(text))?'height':'max')));var vals=dimObjs.filter(function(d){return role==='max'||d.role===role||d.role==='dim';}).map(function(d){return d.v;});if(!vals.length)continue;var chosen=(role==='max')?Math.max.apply(null,vals):Math.min.apply(null,vals);if(!(chosen<md.value))continue;score+=100+(md.value-chosen)/10;labels.push(md.label+' ('+chosen+'mm)');} | |
| if(pw){var ws=productPowerWatts(p);if(!ws.length){/* many hoods have no W data; don't fail if there is clear hood category */ if(cats.indexOf('may hut mui')===-1)continue;}else{var maxw=Math.max.apply(null,ws);if(!(maxw<pw.value))continue;score+=80+(pw.value-maxw)/50;labels.push(pw.label+' ('+maxw+'W)');}} | |
| var price=p.priceNum||p.pn||0; if(price)score+=Math.max(0,12-price/5000000); | |
| out.push({p:p,score:score,price:price,labels:labels,cat:cats[0]||detectCategory(text)||''}); | |
| } | |
| out.sort(function(a,b){return b.score-a.score||(a.price-b.price);}); | |
| var seen={},ded=[];out.forEach(function(r){var k=productDedupeKey(r.p)||r.idx;if(!seen[k]){seen[k]=1;ded.push(r);}}); | |
| if(!ded.length)return null; | |
| return {query:text,categories:cats,constraints:[md,pw].filter(Boolean),budget:Infinity,groups:[{cat:cats[0]||'Sản phẩm phù hợp',items:ded.slice(0,Math.max(limit,36))}],combos:[],isCombo:false,fallback:true}; | |
| } | |
| function naturalSearch(text,limit){ | |
| limit=limit||24;var cats=detectCategories(text);var constraints=detectSpecConstraints(text);var pr=detectPriceRange(text);var budget=pr.max;var isCombo=cats.length>=2&&(budget<Infinity||/combo|bo |bộ|tong|tổng|ca |cả |va |và |\+/.test(norm(text))); | |
| if(!cats.length&&constraints.length){var dc=detectCategory(text);cats=dc?[dc]:['may hut mui'];} | |
| if(!cats.length)return null; | |
| var groups=[]; | |
| cats.forEach(function(cat){var items=searchProductsForCategory(text,cat,limit,constraints,budget<Infinity&&!isCombo?budget:0);if(items.length)groups.push({cat:cat,items:items});}); | |
| var combos=isCombo?buildBudgetCombos(groups,budget,6):[]; | |
| if(!groups.length){var fb=fallbackTechnicalSearch(text,limit);if(fb)return fb;} | |
| if(!constraints.length&&!isCombo&&cats.length<2){var fb2=fallbackTechnicalSearch(text,limit);if(fb2)return fb2;return null;} | |
| return {query:text,categories:cats,constraints:constraints,budget:budget,groups:groups,combos:combos,isCombo:isCombo}; | |
| } | |
| function productPassSubtypeFilter(p,text){ | |
| var q=norm(text||'');var full=norm((p.name||p.n||'')+' '+(p.cat||p.c||'')+' '+(p.summary||p.sum||'')); | |
| var rules=[ | |
| {ask:['gia vi','gia gia vi','ke gia vi','kệ gia vị','chai lo','chai lọ'], must:['gia vi','gia gia vi','ke gia vi','chai lo','chai lọ'], reject:['tay rua','tẩy rửa','tay rua da nang','tẩy rửa đa năng','thung rac','thùng rác','dao thot','dao thớt','gia bat','giá bát','xoong noi','xoong nồi']}, | |
| {ask:['tay rua','tẩy rửa','ke tay rua','kệ tẩy rửa'], must:['tay rua','tẩy rửa'], reject:['gia vi','chai lo','chai lọ']}, | |
| {ask:['dao thot','dao thớt'], must:['dao thot','dao thớt'], reject:['gia vi','tay rua','tẩy rửa']}, | |
| {ask:['thung rac','thùng rác'], must:['thung rac','thùng rác'], reject:['gia vi','tay rua','tẩy rửa']}, | |
| {ask:['gia bat','giá bát','bat dia','bát đĩa'], must:['gia bat','giá bát','bat dia','bát đĩa'], reject:['gia vi','tay rua','tẩy rửa']}, | |
| {ask:['xoong noi','xoong nồi'], must:['xoong noi','xoong nồi'], reject:['gia vi','tay rua','tẩy rửa']} | |
| ]; | |
| for(var i=0;i<rules.length;i++){ | |
| var r=rules[i];var asked=r.ask.some(function(x){return q.indexOf(norm(x))!==-1;}); | |
| if(asked){var ok=r.must.some(function(x){return full.indexOf(norm(x))!==-1;});var bad=r.reject.some(function(x){return full.indexOf(norm(x))!==-1;});return ok&&!bad;} | |
| } | |
| return true; | |
| } | |
| function productPassQueryHardFilters(p,text){ | |
| var cat=detectCategory(text), brand=detectBrand(text);var nameN=norm(p.name||p.n||''), catN=norm(p.cat||p.c||''), brandN=norm(p.brand||''); | |
| if(!productPassSubtypeFilter(p,text))return false; | |
| if(cat){if(!isValidCategoryProduct(p,cat))return false;var kws=CATEGORY_MAP[cat]||[];var inCat=kws.some(function(kw){return nameN.indexOf(kw)!==-1||catN.indexOf(kw)!==-1;});if(!inCat)return false;} | |
| if(brand){if(brandN.indexOf(brand)===-1&&nameN.indexOf(brand)===-1)return false;} | |
| return true; | |
| } | |
| function findProductForQuestion(text){ | |
| var data=_D();var q=compactNorm(text||'');var best=null,bestScore=0; | |
| for(var i=0;i<data.length;i++){ | |
| var p=data[i];if(!p)continue;if(!productPassQueryHardFilters(p,text))continue;var sc=directProductScore(p,text,extractKeywords(text)); | |
| var sku=compactNorm(p.sku||p.model||p.mod||'');if(sku&&q.indexOf(sku)!==-1)sc+=120000; | |
| var brand=norm(p.brand||'');if(brand&&norm(text).indexOf(brand)!==-1)sc+=3000; | |
| if(sc>bestScore){best=p;bestScore=sc;} | |
| } | |
| return bestScore>=25000?best:null; | |
| } | |
| function technicalIntent(text){ | |
| var n=norm(text||'');var intents=[]; | |
| var map=[['kich_thuoc',/(kich thuoc|rộng|rong|ngang|dai|dài|sau|sâu|cao|lo da|lỗ đá|cat da|cắt đá|lap dat|lắp đặt)/],['cong_suat',/(cong suat|w\b|kw|dien nang|điện năng|motor|dong co|động cơ|hut m3|m3\/h|suc hut)/],['tinh_nang',/(tinh nang|tính năng|co gi|có gì|uu diem|ưu điểm|inverter|bldc|cam ung|cảm ứng|khu mui|khử mùi|say|sấy|uv)/],['bao_hanh',/(bao hanh|bảo hành)/],['gia',/(gia|giá|bao nhieu|bao nhiêu)/]]; | |
| map.forEach(function(x){if(x[1].test(n))intents.push(x[0]);}); | |
| return intents; | |
| } | |
| function specEntries(p){var arr=[];var specs=p.specs||{};if(specs&&typeof specs==='object'){for(var k in specs){if(k&&specs[k])arr.push({k:k,v:String(specs[k])});}}return arr;} | |
| function pickSpecs(p,intents){ | |
| var entries=specEntries(p);var out=[];var nints=intents.join(' '); | |
| function hit(k,v){var s=norm(k+' '+v);if(nints.indexOf('kich_thuoc')!==-1&&/(kich thuoc|rong|ngang|dai|sau|cao|lo da|cat da|size|dimension)/.test(s))return true;if(nints.indexOf('cong_suat')!==-1&&/(cong suat|w\b|kw|dien|motor|dong co|hut|m3|db|on|ồn)/.test(s))return true;if(nints.indexOf('bao_hanh')!==-1&&/bao hanh/.test(s))return true;if(nints.indexOf('tinh_nang')!==-1&&/(tinh nang|inverter|bldc|cam ung|uv|say|khu|filter|loc|điều khiển|dieu khien)/.test(s))return true;return false;} | |
| entries.forEach(function(e){if(hit(e.k,e.v))out.push(e);}); | |
| if(!out.length&&entries.length)out=entries.slice(0,8);return out.slice(0,12); | |
| } | |
| function vaiTechnicalAnswer(query){ | |
| var p=findProductForQuestion(query);if(!p)return null;var intents=technicalIntent(query);if(!intents.length&&detectCategory(query))return null; | |
| var specs=pickSpecs(p,intents);var dims=(typeof extractProductDimensions==='function')?extractProductDimensions(p):[];var feats=(p.feats||[]).slice(0,8);var html=''; | |
| html+='<div style="background:#fff;border:1px solid #bfdbfe;border-radius:12px;padding:12px;margin-bottom:12px">'; | |
| html+='<div style="font-weight:900;color:#003f62;margin-bottom:6px">🧑🔧 Tư vấn kỹ thuật theo dữ liệu sản phẩm</div>'; | |
| html+='<div style="font-size:13px"><b>'+(p.name||p.n||'')+'</b><br>🏷 '+(p.brand||'')+' | 🔖 '+(p.sku||p.mod||p.model||'')+' | 💰 '+(p.price||p.p||'LH')+'</div>'; | |
| if(specs.length){html+='<div style="margin-top:8px;font-size:12px"><b>Thông số liên quan câu hỏi:</b><ul style="margin:5px 0 0 18px">';specs.forEach(function(e){html+='<li><b>'+e.k+':</b> '+e.v+'</li>';});html+='</ul></div>';} | |
| if(dims.length){html+='<div style="margin-top:6px;font-size:11px;color:#0f766e">📐 Kích thước đã nhận diện: '+dims.slice(0,8).map(function(d){return d.role+' '+d.v+'mm';}).join(', ')+'</div>';} | |
| if(feats.length){html+='<div style="margin-top:6px;font-size:12px"><b>Tính năng:</b> '+feats.join(' • ')+'</div>';} | |
| html+='<div style="margin-top:8px;font-size:11px;color:#64748b">Em chỉ trả lời theo dữ liệu đang có; nếu thiếu kích thước lỗ đá/công suất cụ thể, nên xác nhận lại catalogue/hãng trước khi thi công.</div>'; | |
| html+='</div>'; | |
| var res={html:html,product:p};return res; | |
| } | |
| function vaiTechnicalTextAnswer(query){ | |
| var ans=vaiTechnicalAnswer(query);if(!ans||!ans.product)return ''; | |
| var p=ans.product;var specs=pickSpecs(p,technicalIntent(query));var lines=[]; | |
| lines.push('🧑🔧 Em kiểm tra theo dữ liệu sản phẩm: '+(p.name||p.n||'')); | |
| lines.push('🏷 '+(p.brand||'')+' | 🔖 '+(p.sku||p.mod||p.model||'')+' | 💰 '+(p.price||p.p||'LH')); | |
| if(specs.length){lines.push('📋 Thông số liên quan:');specs.slice(0,8).forEach(function(e){lines.push('• '+e.k+': '+e.v);});} | |
| var feats=(p.feats||[]).slice(0,6);if(feats.length){lines.push('✨ Tính năng: '+feats.join(' • '));} | |
| lines.push('Lưu ý: em chỉ dùng dữ liệu đang có, nếu thiếu thông số thi công cần xác nhận lại catalogue/hãng.'); | |
| return lines.join(' | |
| '); | |
| } | |
| window._vaiTechnicalTextAnswer=vaiTechnicalTextAnswer; | |
| window._vaiTechnicalAnswer=vaiTechnicalAnswer; | |
| window._vaiNaturalSearch=naturalSearch; | |
| window._vaiRenderNaturalResults=renderNaturalResults; | |
| window._vaiSearchContext=searchProductsByContext; | |
| window._vaiDetectCategory=detectCategory; | |
| window._vaiDetectBrand=detectBrand; | |
| window._vaiDetectPrice=detectPriceRange; | |
| // === NAVIGATE TO PRODUCT (showDetail takes INDEX) === | |
| window._vaiGoProduct=function(slug){ | |
| if(!slug)return; | |
| var data=_D();if(!data.length)return; | |
| var idx=-1; | |
| for(var i=0;i<data.length;i++){if(data[i]&&data[i].slug===slug){idx=i;break;}} | |
| if(idx>=0&&typeof showDetail==='function'){ | |
| try{showDetail(idx);window.scrollTo({top:0,behavior:'smooth'});return;}catch(e){} | |
| } | |
| window.location.href='/san-pham/'+slug+'/index.html'; | |
| }; | |
| // === HOOK FETCH === | |
| var _lastProduct=null; | |
| var _conversationHistory=[]; | |
| var VISION_MODEL='meta-llama/Llama-4-Scout-17B-16E-Instruct'; | |
| window.fetch=function(url,options){ | |
| if(url&&typeof url==='string'&&url.indexOf('router.huggingface.co')!==-1&&options&&options.body){ | |
| try{ | |
| var body=JSON.parse(options.body); | |
| if(body.messages&&Array.isArray(body.messages)){ | |
| var userMsg=body.messages.filter(function(m){return m.role==='user';}).pop(); | |
| if(!userMsg)return _origFetch.apply(this,arguments); | |
| var userText=typeof userMsg.content==='string'?userMsg.content:(userMsg.content&&userMsg.content[0]?userMsg.content[0].text:''); | |
| var data=_D();var product=null; | |
| if(data.length){ | |
| var q=norm(userText).replace(/[.\-_ ]/g,''); | |
| for(var i=0;i<data.length;i++){var p=data[i];var sku=(p.sku||p.model||p.mod||'').replace(/[.\-_ ]/g,'').toLowerCase();if(sku&&sku.length>3&&q.indexOf(sku)!==-1){product=p;break;}} | |
| if(!product){var qn=norm(userText);var qkw=extractKeywords(userText);var best=null,bestScore=0;for(var j=0;j<data.length;j++){var p2=data[j];var sc=directProductScore(p2,userText,qkw);if(sc>bestScore){best=p2;bestScore=sc;}}if(best&&bestScore>=40000){product=best;}} | |
| } | |
| if(product){_lastProduct=product;}else if(_lastProduct){product=_lastProduct;} | |
| var contextProducts=[]; | |
| var cat=detectCategory(userText);var brand=detectBrand(userText);var priceRange=detectPriceRange(userText);var dimQuery=_vaiAccessoryDimQuery(userText); | |
| var isContextQuery=dimQuery||cat||(brand&&!product)||(priceRange.max<Infinity||priceRange.min>0); | |
| if(isContextQuery||!product){contextProducts=searchProductsByContext(userText,8);} | |
| if(product&&/thiết kế|kiểu dáng|màu sắc|design|phong cách|chất liệu/i.test(userText)){ | |
| var imgV=product.image||product.img||product.i||''; | |
| if(imgV){body.model=VISION_MODEL;var lu=body.messages[body.messages.length-1];if(lu&&lu.role==='user'&&typeof lu.content==='string')lu.content=[{type:'text',text:lu.content},{type:'image_url',image_url:{url:imgV}}];} | |
| } | |
| var strict='\n\n[QUY TẮC TUYỆT ĐỐI]:\n1. CHỈ trả lời dựa trên [DỮ LIỆU] bên dưới. KHÔNG bịa thông số/giá.\n2. Nếu không có data → "Em chưa có thông tin SP này, anh/chị liên hệ Zalo 0981873395".\n3. LUÔN đề cập TÊN SẢN PHẨM + GIÁ cụ thể.\n4. Gạch đầu dòng, ngắn gọn.\n5. Khi có nhiều SP: so sánh ưu/nhược.\n6. Cuối câu: gợi ý SP phù hợp nhất.\n7. Với phụ kiện tủ bếp Garis/Grob/Eurogold: học theo cấu trúc Garis. Các mã/khoang tủ/chiều rộng tủ/phủ bì/KT mặt cánh là kích thước tủ danh nghĩa; quy cách R/W hoặc kích thước sản phẩm là kích thước lọt lòng/thực của phụ kiện. Nếu khách hỏi phủ bì/tủ 300mm thì chọn mã có tủ danh nghĩa 300mm hoặc lọt lòng nhỏ hơn 300mm, thường lớn hơn 250mm. Ưu tiên Grob trước, sau đó Eurogold; Garis chỉ dùng làm mẫu tham chiếu cấu trúc. Khi khách hỏi kích thước phụ kiện tủ bếp thì KHÔNG lấy Hafele nếu có Grob/Eurogold/Garis phù hợp.'; | |
| if(product&&!isContextQuery){ | |
| var specs=product.specs||{};var ss='';if(typeof specs==='object')for(var sk2 in specs)ss+=sk2+':'+specs[sk2]+'; '; | |
| var feats=(product.feats||[]).slice(0,10).join('; '); | |
| strict+='\n\n[DỮ LIỆU SP]:\nTên: '+(product.name||product.n)+'\nGiá: '+(product.price||product.p||'LH')+'\nBrand: '+(product.brand||'')+'\nMã: '+(product.sku||product.mod||''); | |
| if(ss)strict+='\nThông số: '+ss;if(feats)strict+='\nTính năng: '+feats; | |
| if(product.summary||product.sum)strict+='\nMô tả: '+(product.summary||product.sum||'').substring(0,500); | |
| } | |
| if(contextProducts.length>0){ | |
| strict+='\n\n[DANH SÁCH SP PHÙ HỢP'; | |
| if(cat)strict+=' | Loại:'+cat;if(brand)strict+=' | Brand:'+brand; | |
| if(priceRange.max<Infinity)strict+=' | Giá<'+fmt(priceRange.max);strict+=']:'; | |
| contextProducts.forEach(function(r,idx2){var cp=r.p;strict+='\n'+(idx2+1)+'. '+(cp.name||cp.n)+' | Giá:'+(cp.price||cp.p||'LH')+' | Mã:'+(cp.sku||cp.mod||'');if(cp.brand)strict+=' | '+cp.brand;if(dimQuery){var dm=_vaiAccessoryDimMatch(cp,dimQuery);if(dm)strict+=' | Tủ/phủ bì: '+((dm.nominalHit&&dm.nominalHit.length)?dm.nominalHit.join('/')+'mm; ':'')+'lọt lòng/thực: '+dm.hits.join('/')+'mm';}var f=(cp.feats||[]).slice(0,3).join(', ');if(f)strict+=' | '+f;}); | |
| strict+='\n\n→ Giới thiệu TẤT CẢ SP trên, nêu tên + giá + ưu điểm chính. Gợi ý SP phù hợp nhất.'; | |
| } | |
| // APPEND product data to existing system prompt (don't replace) | |
| var sys=body.messages.find(function(m){return m.role==='system';}); | |
| if(sys&&typeof sys.content==='string'){ | |
| // Inline code already has excellent sales consultant prompt | |
| // We just APPEND real product data so AI can reference it | |
| sys.content+=strict; | |
| }else{ | |
| // Fallback: no system prompt exists, create one | |
| var sysPrompt='Bạn là V.AI STUDIO — chuyên gia tư vấn thiết bị nhà bếp. Xưng em, gọi anh/chị. Tư vấn tự nhiên như người thật, phân tích nhu cầu, giải thích tại sao SP phù hợp. Dùng data SP bên dưới.'; | |
| body.messages.unshift({role:'system',content:sysPrompt+strict}); | |
| } | |
| options.body=JSON.stringify(body); | |
| } | |
| }catch(e){console.warn('[Boot v18] hookAI error:',e);} | |
| } | |
| return _origFetch.apply(this,arguments); | |
| }; | |
| console.log('[Boot v18] ✅ hookAI installed'); | |
| // ============================================================ | |
| // PHẦN 2: CARD INJECTION | |
| // ============================================================ | |
| var PROCESSED='data-vc'; | |
| function getCards(botText,userText,limit){ | |
| limit=limit||6; | |
| var contextResults=searchProductsByContext(userText,limit); | |
| if(contextResults.length>=2)return contextResults.map(function(r){var p=r.p;return{name:p.name||p.n||'',slug:p.slug||'',price:p.price||p.p||'',image:p.image||p.img||p.i||'',model:p.model||p.mod||p.sku||'',brand:p.brand||''};}); | |
| var data=_D();if(data.length){ | |
| var n=norm(botText);var found=[];var seen={}; | |
| for(var i=0;i<data.length&&found.length<limit;i++){var p=data[i];if(!p||!p.slug||seen[p.slug])continue;var sku=(p.sku||p.model||p.mod||'').replace(/[.\-_ ]/g,'').toLowerCase();if(sku&&sku.length>3&&n.indexOf(sku)!==-1){seen[p.slug]=true;found.push({name:p.name||p.n||'',slug:p.slug,price:p.price||p.p||'',image:p.image||p.img||p.i||'',model:p.model||p.mod||'',brand:p.brand||''});}} | |
| if(found.length)return found; | |
| var botResults=searchProductsByContext(botText,limit); | |
| if(botResults.length>=1)return botResults.map(function(r){var p=r.p;return{name:p.name||p.n||'',slug:p.slug||'',price:p.price||p.p||'',image:p.image||p.img||p.i||'',model:p.model||p.mod||p.sku||'',brand:p.brand||''};}); | |
| if(_conversationHistory.length){var histResults=searchProductsByContext(_conversationHistory.slice(-3).join(' '),limit);if(histResults.length>=1)return histResults.map(function(r){var p=r.p;return{name:p.name||p.n||'',slug:p.slug||'',price:p.price||p.p||'',image:p.image||p.img||p.i||'',model:p.model||p.mod||p.sku||'',brand:p.brand||''};});} | |
| } | |
| return[]; | |
| } | |
| function createCardsHTML(products){ | |
| if(!products||!products.length)return''; | |
| var h='<div style="display:flex;gap:8px;overflow-x:auto;padding:8px 0;margin-top:8px;scrollbar-width:thin">'; | |
| products.forEach(function(p){ | |
| h+='<div style="flex:0 0 130px;border:1px solid #e2e8f0;border-radius:10px;overflow:hidden;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.06);display:flex;flex-direction:column">'; | |
| h+='<div onclick="window._vaiGoProduct(\''+p.slug+'\')" style="cursor:pointer;flex:1">'; | |
| if(p.image)h+='<img src="'+p.image+'" style="width:100%;height:70px;object-fit:contain;background:#f8fafc;padding:3px" onerror="this.style.display=\'none\'">'; | |
| h+='<div style="padding:4px 6px"><div style="font-size:9.5px;font-weight:600;color:#003f62;line-height:1.2;height:22px;overflow:hidden">'+(p.name||'').substring(0,35)+'</div>'; | |
| if(p.model)h+='<div style="font-size:8px;color:#94a3b8;margin-top:1px">'+(p.model||'').substring(0,20)+'</div>'; | |
| h+='<div style="font-size:10px;font-weight:700;color:#059669;margin-top:2px">'+(p.price||'LH')+'</div></div></div>'; | |
| h+='<div style="display:flex;gap:2px;margin:2px 4px 4px"><button class="vai-card-cart" data-slug="'+p.slug+'" style="flex:1;padding:3px;background:#db9815;color:#fff;border:none;border-radius:4px;font-size:8px;font-weight:700;cursor:pointer">+ Giỏ</button><button class="vai-card-add" data-slug="'+p.slug+'" style="flex:1;padding:3px;background:#003f62;color:#fff;border:none;border-radius:4px;font-size:8px;font-weight:700;cursor:pointer">+ Đơn</button></div>'; | |
| }); | |
| h+='</div>';return h; | |
| } | |
| function injectLoop(){ | |
| var chatBody=document.querySelector('.chat-body');if(!chatBody)return; | |
| var lastUserMsg='';var userMsgs=chatBody.querySelectorAll('.chat-msg.user'); | |
| if(userMsgs.length)lastUserMsg=userMsgs[userMsgs.length-1].textContent||''; | |
| var botMsgs=chatBody.querySelectorAll('.chat-msg.bot'); | |
| for(var i=0;i<botMsgs.length;i++){ | |
| var msg=botMsgs[i];if(msg.getAttribute(PROCESSED))continue; | |
| var text=msg.textContent||''; | |
| if(msg.querySelector('.chat-typing'))continue; | |
| if(msg.classList.contains('streaming'))continue; | |
| if(text.trim().length<5)continue; | |
| msg.setAttribute(PROCESSED,'1'); | |
| if(lastUserMsg&&_conversationHistory[_conversationHistory.length-1]!==lastUserMsg){_conversationHistory.push(lastUserMsg);if(_conversationHistory.length>10)_conversationHistory.shift();} | |
| var cards=getCards(text,lastUserMsg,6); | |
| if(cards.length){var div=document.createElement('div');div.innerHTML=createCardsHTML(cards);msg.appendChild(div.firstChild);} | |
| } | |
| chatBody.querySelectorAll('.vai-card-add:not([data-b])').forEach(function(btn){ | |
| btn.setAttribute('data-b','1'); | |
| btn.addEventListener('click',function(e){e.preventDefault();e.stopPropagation();var slug=this.getAttribute('data-slug');if(!slug)return;var self=this;var data=_D();for(var i=0;i<data.length;i++){if(data[i]&&data[i].slug===slug){if(window._showOrderPicker){window._showOrderPicker(data[i],function(r){if(r==='ok'){self.textContent='✓';self.style.background='#059669';}else if(r==='dup'){self.textContent='Đã có';self.style.background='#94a3b8';}setTimeout(function(){self.textContent='+ Đơn';self.style.background='#003f62';},2500);});}break;}}}); | |
| // +Giỏ button handler | |
| chatBody.querySelectorAll('.vai-card-cart:not([data-bc])').forEach(function(btn){ | |
| btn.setAttribute('data-bc','1'); | |
| btn.addEventListener('click',function(e){e.preventDefault();e.stopPropagation();var slug=this.getAttribute('data-slug');if(!slug)return;var self=this;var data=_D();for(var i=0;i<data.length;i++){if(data[i]&&data[i].slug===slug){if(typeof addToCart==='function'){addToCart(i);self.textContent='✓';self.style.background='#059669';setTimeout(function(){self.textContent='+ Giỏ';self.style.background='#db9815';},2000);}break;}}}); | |
| }); | |
| }); | |
| } | |
| // ============================================================ | |
| // PHẦN 3: SEARCH DROPDOWN | |
| // ============================================================ | |
| function initSearchDropdown(){ | |
| var searchInput=document.getElementById('q');if(!searchInput||searchInput.getAttribute('data-dd'))return; | |
| searchInput.setAttribute('data-dd','1'); | |
| var dropdown=document.createElement('div');dropdown.id='vai-search-dropdown'; | |
| dropdown.style.cssText='position:absolute;left:0;right:0;top:100%;background:#fff;border:1px solid #e2e8f0;border-radius:0 0 12px 12px;box-shadow:0 8px 24px rgba(0,0,0,.12);max-height:520px;overflow-y:auto;z-index:9999;display:none'; | |
| var parent=searchInput.parentElement;if(parent){parent.style.position='relative';parent.appendChild(dropdown);} | |
| var timer=null; | |
| searchInput.addEventListener('input',function(){clearTimeout(timer);var q=this.value.trim();if(q.length<2){dropdown.style.display='none';return;}timer=setTimeout(function(){showDD(q);},200);}); | |
| searchInput.addEventListener('focus',function(){if(this.value.trim().length>=2)showDD(this.value.trim());}); | |
| document.addEventListener('click',function(e){if(!dropdown.contains(e.target)&&e.target!==searchInput)dropdown.style.display='none';}); | |
| function showDD(query){ | |
| var data=_D();if(!data.length){dropdown.style.display='none';return;} | |
| var results=searchProductsByContext(query,15); | |
| if(results.length<3){var kw=extractKeywords(query);var seen={};results.forEach(function(r){seen[r.p.slug]=true;});for(var i=0;i<data.length&&results.length<15;i++){var p=data[i];if(!p||seen[p.slug])continue;var nameN=norm(p.name||p.n||'');var score=0;for(var w=0;w<kw.length;w++){if(nameN.indexOf(kw[w])!==-1)score+=10;}if(score>0){seen[p.slug]=true;results.push({p:p,score:score});}}} | |
| if(!results.length){dropdown.style.display='none';return;} | |
| var html='';results.forEach(function(r){var p=r.p;var name=p.name||p.n||'';var price=p.price||p.p||'LH';var img=p.image||p.img||p.i||'';var slug=p.slug||'';var brand2=p.brand||''; | |
| html+='<div class="vai-dd-item" style="padding:8px 12px;border-bottom:1px solid #f1f5f9;display:flex;align-items:center;gap:10px;cursor:pointer" onmouseover="this.style.background=\'#f8fafc\'" onmouseout="this.style.background=\'\'" data-slug="'+slug+'">'; | |
| if(img)html+='<img src="'+img+'" style="width:36px;height:36px;object-fit:contain;border-radius:4px;background:#f1f5f9;flex-shrink:0" onerror="this.style.display=\'none\'">'; | |
| html+='<div style="flex:1;overflow:hidden"><div style="font-size:12px;font-weight:600;color:#003f62;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+name.substring(0,55)+'</div><div style="font-size:10px;color:#64748b">'+brand2+'</div></div>'; | |
| html+='<div style="font-size:11px;font-weight:700;color:#059669;white-space:nowrap">'+price+'</div></div>'; | |
| }); | |
| dropdown.innerHTML=html;dropdown.style.display='block'; | |
| dropdown.querySelectorAll('.vai-dd-item').forEach(function(item){item.addEventListener('click',function(){var sl=this.getAttribute('data-slug');if(sl)window._vaiGoProduct(sl);dropdown.style.display='none';});}); | |
| } | |
| } | |
| // ============================================================ | |
| // PHẦN 4: PURGE MALLOCA FROM ALL EXTERNAL SOURCES (DMX + Hafele VN) | |
| // ============================================================ | |
| function cleanUrl(u){try{return u.split('?')[0].split('#')[0]}catch(e){return u}} | |
| function fixImg(src){if(!src)return '';src=src.trim();if(src.indexOf('//')===0)src='https:'+src;return src.split('?')[0];} | |
| function mapProduct(p,defaultBrand){ | |
| var link=cleanUrl(p.l||'');var image=fixImg(p.i||''); | |
| var images=(p.imgs||[]).map(fixImg).filter(Boolean); | |
| if(image&&images.indexOf(image)===-1)images.unshift(image); | |
| if(!image&&images.length)image=images[0]; | |
| return{name:p.n,link:link,image:image,price:p.p,priceNum:p.pn,cat:p.c,catSlug:p.cs,catIcon:p.ci,subCat:p.sub_c||'',subCatSlug:p.sub_cs||'',images:images,summary:p.sum||'',desc:p.desc||'',specs:p.specs||{},feats:p.feats||[],sku:p.sku||'',video:p.vid||'',model:p.mod||'',slug:p.slug||'',brand:p.brand||defaultBrand,_src:defaultBrand,_idx:(p.sku||'')+' '+(p.brand||'').toLowerCase()+' '+(p.n||'').toLowerCase()}; | |
| } | |
| function isMallocaProduct(p){ | |
| var check=((p.n||p.name||'')+' '+(p.brand||'')+' '+(p.sku||'')+' '+(p.mod||'')+' '+(p.model||'')).toLowerCase(); | |
| return check.indexOf('malloca')!==-1; | |
| } | |
| // PURGE Malloca from DMX + Hafele VN sources | |
| function purgeMallocaExternal(){ | |
| if(typeof D==='undefined'||!D||!D.length)return; | |
| var before=D.length;var newD=[]; | |
| for(var i=0;i<D.length;i++){ | |
| var p=D[i]; | |
| // Detect if product is from external source (DMX or Hafele VN) | |
| var isDMX=(p._src==='Điện Máy Xanh')||(p.brand&&p.brand.toLowerCase().indexOf('điện máy xanh')!==-1)||(p.link&&p.link.indexOf('dienmayxanh')!==-1); | |
| var isHafeleVN=(p._source&&p._source.toLowerCase().indexOf('hafele')!==-1)||(p._sourceUrl&&p._sourceUrl.indexOf('hafele-vn')!==-1)||(p.link&&p.link.indexOf('hafele-vn')!==-1); | |
| if(isDMX||isHafeleVN){ | |
| var name=((p.name||p.n||'')+' '+(p.brand||'')+' '+(p.sku||p.model||p.mod||'')).toLowerCase(); | |
| if(name.indexOf('malloca')!==-1){ | |
| continue; // SKIP — loại bỏ Malloca từ nguồn ngoài | |
| } | |
| } | |
| newD.push(p); | |
| } | |
| D.length=0;for(var j=0;j<newD.length;j++)D.push(newD[j]); | |
| var removed=before-D.length; | |
| if(removed>0)console.log('[Boot v18] PURGED '+removed+' Malloca products from external sources (DMX+HafeleVN). Remaining:',D.length); | |
| } | |
| function updateStats(){ | |
| var st=document.getElementById('statTotal');if(st)st.textContent=D.length; | |
| var sc=document.getElementById('statCats');if(sc){var cats={};D.forEach(function(p){cats[p.catSlug]=true});sc.textContent=Object.keys(cats).length;} | |
| } | |
| function loadSource(jsonFile,brand,label,filterFn){ | |
| return _origFetch(jsonFile).then(function(r){return r.json()}).then(function(data){ | |
| var existingSlugs={};D.forEach(function(p){if(p.slug)existingSlugs[p.slug]=true;}); | |
| var added=0,skipped=0; | |
| data.forEach(function(p){ | |
| if(filterFn&&filterFn(p)){skipped++;return;} | |
| var mapped=mapProduct(p,brand); | |
| if(!existingSlugs[mapped.slug]){ | |
| if(typeof buildSearchIndex==='function')mapped._idx=buildSearchIndex(mapped); | |
| D.push(mapped);existingSlugs[mapped.slug]=true;added++; | |
| } | |
| }); | |
| console.log('['+label+'] Added '+added+' (skipped '+skipped+'). Total:'+D.length); | |
| return added; | |
| }); | |
| } | |
| // ============================================================ | |
| // PHẦN 5: BOOT | |
| // ============================================================ | |
| var _ts='?_='+Date.now(); | |
| var c=document.createElement('script');c.src='crawled-data.js'+_ts; | |
| c.onload=function(){console.log('[Boot v18] crawled-data loaded');}; | |
| document.head.appendChild(c); | |
| function loadHelper(src){ | |
| var s=document.createElement('script');s.src=src+_ts;s.defer=true; | |
| if(document.body)document.body.appendChild(s); | |
| else document.addEventListener('DOMContentLoaded',function(){document.body.appendChild(s);}); | |
| } | |
| loadHelper('qr-payment.js'); | |
| loadHelper('quote-ui.js'); | |
| loadHelper('order-store.js'); | |
| loadHelper('epo-image-fix.js'); | |
| loadHelper('order-button.js'); | |
| loadHelper('zalo-feed.js'); | |
| loadHelper('shorts-subtitles.js'); | |
| loadHelper('ancuong-shorts.js'); | |
| loadHelper('custom-shorts.js'); | |
| loadHelper('smart-kitchen-shorts.js'); | |
| var _checkInterval=setInterval(function(){ | |
| if(typeof D!=='undefined'&&D.length>0&&typeof init==='function'){ | |
| clearInterval(_checkInterval); | |
| console.log('[Boot v18] Main data:',D.length); | |
| // PURGE Malloca from crawled-data (Hafele VN source) FIRST | |
| purgeMallocaExternal(); | |
| // Then load DMX + Garis + Konox (also filtering Malloca during load) | |
| loadSource('products_dmx_slugs.json'+_ts,'Điện Máy Xanh','DMX',isMallocaProduct) | |
| .then(function(){return loadSource('products_garis_slugs.json'+_ts,'Garis','Garis');}) | |
| .then(function(){return loadSource('products_konox_slugs.json'+_ts,'Konox','Konox');}) | |
| .then(function(){ | |
| // Second purge pass — catch any Malloca that slipped through | |
| purgeMallocaExternal(); | |
| updateStats(); | |
| if(typeof init==='function')init(); | |
| }) | |
| .catch(function(e){console.warn('[Boot v18] Error:',e);purgeMallocaExternal();updateStats();if(typeof init==='function')init();}); | |
| } | |
| },300); | |
| setTimeout(function(){clearInterval(_checkInterval);},30000); | |
| function initChatAndSearch(){ | |
| setInterval(injectLoop,800); | |
| var chatBody=document.querySelector('.chat-body'); | |
| if(chatBody){var mo=new MutationObserver(function(){setTimeout(injectLoop,300);});mo.observe(chatBody,{childList:true,subtree:true});} | |
| else{var retryObs=setInterval(function(){var cb=document.querySelector('.chat-body');if(cb){clearInterval(retryObs);var mo2=new MutationObserver(function(){setTimeout(injectLoop,300);});mo2.observe(cb,{childList:true,subtree:true});}},2000);setTimeout(function(){clearInterval(retryObs);},60000);} | |
| var ddTimer=setInterval(function(){if(_D().length>100){clearInterval(ddTimer);initSearchDropdown();}},2000); | |
| setTimeout(function(){clearInterval(ddTimer);initSearchDropdown();},15000); | |
| } | |
| if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',initChatAndSearch);else initChatAndSearch(); | |
| console.log('✅ Boot v18: purge Malloca from DMX+HafeleVN + AI hook + navigation'); | |
| })(); | |
| // ===== AUTO-NOTIFY ZALO BOT ON NEW DEPLOY ===== | |
| // Auto-detects the current Space commit SHA from HF API. Any web commit becomes a new update_id. | |
| // deploy-notify.json can override message/enabled, but no longer needs manual version bumps. | |
| (function(){ | |
| var BOT='https://bep40-vaistudio-zalo-bot.hf.space'; | |
| var KEY='vai_notify_ver'; | |
| var CFG='deploy-notify.json?_=' + Date.now(); | |
| var API='https://huggingface.co/api/spaces/bep40/V.AISTUDIO?_=' + Date.now(); | |
| function keepAlive(){try{fetch(BOT+'/webhook').catch(function(){});}catch(e){}} | |
| function notify(ver,msg){ | |
| if(!ver){keepAlive();return;} | |
| ver=String(ver); | |
| var last=localStorage.getItem(KEY)||''; | |
| if(last===ver){keepAlive();return;} | |
| localStorage.setItem(KEY,ver); | |
| fetch(BOT+'/hf-webhook',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({update_id:ver,event:{action:'update',update_id:ver,version:ver,scope:msg},repo:{type:'space',name:'bep40/V.AISTUDIO'}})}).catch(function(){}); | |
| console.log('[Notify] Space update '+ver+' → notify requested'); | |
| } | |
| try{ | |
| Promise.all([ | |
| fetch(CFG,{cache:'no-store'}).then(function(r){return r.ok?r.json():{};}).catch(function(){return{};}), | |
| fetch(API,{cache:'no-store'}).then(function(r){return r.ok?r.json():{};}).catch(function(){return{};}) | |
| ]).then(function(arr){ | |
| var cfg=arr[0]||{}, api=arr[1]||{}; | |
| if(cfg.enabled===false){keepAlive();return;} | |
| var sha=api.sha||api.lastModified||cfg.version||('web-'+Date.now()); | |
| var ver='space-'+String(sha).slice(0,12); | |
| var msg=cfg.message||('🌐 V.AI STUDIO vừa được cập nhật trên web.\n✅ Nội dung, sản phẩm hoặc tính năng mới đã sẵn sàng.\n👉 Mở V.AI STUDIO để xem ngay!'); | |
| notify(ver,msg); | |
| }).catch(function(){keepAlive();}); | |
| }catch(e){keepAlive();} | |
| })(); | |