Spaces:
Running
Running
Commit History
2 days ago
-
load timeList (newest-first interiors) and pass it to PhotoViewer for date-based navigation
Verified signaturebcd06d8 -
add timeList prop: page interiors by date (newest first) with prev/next arrows
Verified signature216548e -
add photo-date style
Verified signature290bf1c -
Upload src/App.jsx
Verified signature31e0336 -
Upload src/PhotoViewer.jsx
Verified signaturea9b7720
4 days ago
-
Fix semantic search: space-query product filtering, neon on top CLIP results, tighter layout, always show matched avatar interiors
- merge.js: pure-semantic queries (phòng khách, nhà phố, biệt thự...) never crowd-out interiors; space queries filter products whose name lacks a query-specific token (sofa phòng khách keeps sofa, khóa cửa phòng ngủ is dropped); matched avatar interiors always enter the field, not only on empty query - relevance.js: space queries ring the top CLIP cosine band (relative 0.85x top, floor 0.20) even without a verbatim caption match - layout.js: results spiral tighter (0.62/0.66) with reduced pill clearance so results sit near the search box - semantic-terms.js: SPACE_QUERY_TERMS + isSpaceQuery for nhà phố/biệt thự/phòng ngủ/nhà cấp 4/căn hộ... - index.html: bump cache-bust version to 20261110-17ce3b43
5 days ago
-
semantic-terms.js: core classification of semantic vs product-like tokens
Verified signature04b05ff -
Fix: products never ring (neon) on semantic queries (nhà phố, biệt thự, nội thất, bàn ghế)
Verified signature5c977f3 -
Fix products never ring (neon) on semantic queries (nhà phố, biệt thự, nội thất, bàn ghế)
Verified signature0f8c30b
6 days ago
-
Fix missing import: expandQueryTerms from semantic-terms.js in avatar-interiors.js
Verified signaturesearchAvatarInteriors() calls expandQueryTerms() but the module was never imported, so every avatar-interior text match threw ReferenceError. This is the semantic synonym expansion that lets a "nhà phố" query match a "Nhà ống" caption and "biệt thự" match "villa" -- the core of semantic interior search.ee6f311 -
Revert accidental 'ke' addition to SEMANTIC_TERMS (breaks crowd-out for "kệ xoong")
Verified signature'ke' must stay a product-like token: "kệ xoong" needs 2 product-like tokens (ke + xoong) to crowd out interiors. With ke classified as semantic, "kệ xoong" would have only 1 product-like token and the interiors would no longer be dropped -- breaking the product-search behaviour the catalog requires.1665f93 -
Expand semantic synonyms: nhà phố/nhà ống/nhà liền kề, biệt thự/villa, nội thất/trang trí, bàn/ghế, phòng khách/phòng ngủ/phòng tắm
Verified signature- nha: + lien (nhà liền kề), + khu (khu dân cư) - pho: + ong, + lien (phố ~ ống ~ liền kề) - biet/thu: + villa, + vuon (biệt thự vườn) - noi/that: + trang/tri (nội thất ~ trang trí) - ban: + ke (bàn ~ kệ cùng nhóm đồ nội thất) - phong: + khach/ngu/tam (phòng khách/ngủ/tắm) - True when a token (folded) is a general semantic keyword.f08ac5a -
Fix semantic search: keep interior photos for semantic queries (nhà phố, biệt thự, nội thất, bàn ghế) while preserving 100% exact SKU/model lookup
Verified signature- merge.js now imports productLikeTerms from semantic-terms.js (previously written but never wired in) and hasDecisiveWinner from relevance.js - crowd-out rule: interiors are dropped ONLY for (a) decisive exact matches (identity tier >= 700 with 2x margin -- SKU/model/title lookups, which must stay 100% precise) or (b) queries with >= 2 product-like tokens ALL contained in one product name - pure-semantic queries (nha pho, biet thu, noi that, ban ghe) no longer wipe the interior field - adaptive product floor: weak product hits (score < 120) no longer interleave into semantic-query fields; product-token queries keep the original floor of 2046320cf -
feat: avatar interiors search expands nhà phố/biệt thự/nội thất synonyms
Verified signaturea50226d -
feat: semantic keyword set v2 — narrowed synonyms to avoid false positives
Verified signaturec7f56b3 -
feat: merge.js — crowd-out only fires on true product queries (semantic fix)
Verified signature616e79f -
feat: semantic keyword set separating interior queries from product lookups
Verified signaturec5b12ab -
Upload src/merge.js
Verified signature08c971a -
FIX v1.20: SP mới nhất (Thêm SP) bị seen-marking ngay cả khi có query → biến mất khỏi kết quả tìm kiếm
Root cause: merge.js chạy vòng lặp recents (SP mới) trước khi tính showNewRow, đánh dấu seen cho mọi SP mới bất kể có query hay không. Khi user gõ query, SP mới (vd SMS6ZCI16E thêm qua Thêm SP) bị loại khỏi scoredHits → không bao giờ xuất hiện dù rank #1. Fix: chỉ chạy vòng recents khi showNewRow=true (query rỗng). Khi có query, SP mới được tự do vào kết quả tìm kiếm. Verified: E2E mergeResults với data live — 'máy rửa bát bosch' → SMS6 rank #1, 'SMS6ZCI16E' → 1 tile featured, query rỗng → SMS6 featured trong hàng mới.8e97d58
8 days ago
-
fix(build): restore assets.js exports so the Space can rebuild + ship rebuilt dist
The share-URL refactor (df9d078/90b5ae8) accidentally overwrote assets.js with only the shareURL section, dropping the DATASET/TOKENIZER/TEXT_MODEL/ TEXT_MODEL_REVISION/DIMENSIONS/imageURL/tileURL/cachedFile exports that worker.js and ImageField.jsx import. Every build since then failed with MISSING_EXPORT; the Space kept serving the last good dist/ from before that commit. Restored the full module (top half from 7c7b7e4, keeping the newer shareURL logic) and rebuilt dist/ so the live Space actually serves the image-restore, search-precision and crowd-out fixes.ef50ce7 -
fix: restore Eurogold images + kệ xoong search precision
- catalogue.js: SKU upsert prefers the row whose lead image is live, so a dead V.AISTUDIO /eurogold_img/ row can no longer clobber a live www.eurogold.vn photo (221 dead Eurogold leads -> 5). - catalogue.js: recoverDeadLeads maps dead-only SKUs through the dead filename itself (EPD4635VX->EPD4635.jpg, EUM101->EUM101L.jpg ...), restoring 16 more products deterministically. - product-search.js: kệ/giá name synonyms so 'kệ xoong' finds Eurogold 'Giá xoong nồi' racks; per-score-group round-robin tie-break so one brand family cannot swallow the field (Eurogold now visible in top-24). - merge.js: a strong product query (every term in a product name) drops CLIP interiors from the field -- kệ xoong shows ONLY racks; keywordFeatured defaults to productLimit so every relevant hit gets the neon ring+scale; leadImage refuses dead V.AISTUDIO dirs; PRODUCT_LIMIT 12->24. - App.jsx/worker.js: search-products limit 24->48. - tests: updated caps test (24), worker-shape score, added cro…aba5d22 -
fix: TL869IMAX neon row + image load speed + search precision (#7)
- fix: dist/assets/index.js (e255c14a557b4829b407d14928076b19acf314cd) - fix: src/merge.js (fc4900cea79a025aa17f876e52ca39f9554bfbc3) - fix: dist/assets/worker-nnOdWzlm.js (a3dab58978ef2496294b6fd6e7d9197a44a87428) - fix: src/image-loader.js (9aa3bfc4537d2efda4373fc85e7f5e61fac0ba72) - fix: src/product-search.js (aac38355f6de6db7e851e5661748a9709e631c1c) - fix: src/avatar-interiors.js (f23627d57042eac4e55c12f3aa5c85205c470670)76646ca -
fix: recent-product ring rows now reach the field + hide 'mới' row during queries (#6)
- fix(merge): recentProducts reach field on page entry (ordering: unshift into fresh BEFORE o.unshift) + hide "mới" row while query active (74db176cc7c7a06a741f8fd99f15267ead63b05b) - fix(merge): source mirror of bundle fix (freshProducts prepended before out.unshift + query gate) (d1ce9b53a5b136a5de9e019eb5f8857dd484986c)57833b7
9 days ago
-
feat: newest added products appear in 'mới' front row with neon ring (like newest interiors) (#5)
- src/worker.js: recentProductsOf + composite changed signature (36578e6ba682766e6bcb9e38c3b586c3ee9a8b39) - src/App.jsx: recentProducts state + handler + mergeResults option (a60dc2af7a78597a5906cc4432e5f9a77cb2b772) - src/merge.js: recentProducts ringed prepend (15f4e8989ef33830d9f50c984bd093dcd3237573) - dist bundle: recentProducts state/handler + merge ringed prepend (c82a18b21d6f385b1152325b477ed2ac06ad7278) - dist worker: recentProductsOf + composite changed (avatar-updated recentProducts) (1b1735130baf3afc4d92b550c64d66d72075459d)58b6cf1 -
fix: eurogold/gold false-hit + feature top keyword hits & fresh-added products larger/on-top (#4)
- fix: src/product-search.js (01be3a238bb3e19097f69fc0566ba50134f5b48f) - fix: src/merge.js (6dddbb3f46b39ca9065eb6486a6dbb48cb222b35) - fix: src/ImageField.jsx (17b566cbd95a5a4d47a062bc8903bc5eb1bd7f70) - fix: dist/assets/index.js (6e1111224c52dc7dac442e96524b30c3b0e20e10) - fix: src/catalogue.js (6f13fff078e7647a367f2251a2ea000fe5601dcb) - fix: dist/assets/worker-nnOdWzlm.js (81141956124585fc096c571dcb20cf31eadc1534)703f8d3 -
fix(neon): shrink ring stroke to 1px and halo to 2.5px (half width)
Verified signaturea6f73b7
10 days ago
-
refactor(css): drop the duplicated ringed-tile override; neon-frame.css owns it
Verified signature87f3556 -
refactor(neon): own the ringed-tile dim override here, so it has one definition
Verified signature558f1dd -
fix(share): an avatarimg record with no usable id gets no share URL, not the dead product route
Verified signature90b5ae8 -
fix(neon): run the neon light around the picture frame instead of rotating the mask
Verified signature0d446a1 -
refactor(css): import the neon frame from its own module so a rebuild keeps it
Verified signature703cbe5 -
Delete src/neon-frame.css
Verified signaturefdb0de1 -
fix(neon): run the neon light around the picture frame instead of rotating the mask
Verified signature8f04110 -
fix(share): route avatarimg photographs to /share/i/ instead of /share/p/
Verified signaturedf9d078 -
PhotoViewer: a vai-avatar photograph is not a product, even though it carries a sku
Verified signature`isProduct` drove the alt text, the gallery check, the price/spec caption and the detail title, and it was keyed on `Boolean(photo.sku)`. avatar-interiors.js gives every photograph published on vai-avatar a namespaced `sku` (`avatarimg:<id>`) so it travels the same code path as a catalogue product, so all of these photographs were announced as "Sản phẩm (avatarimg:b68dc779370dc70e)" and rendered with the product caption. `avatarImageId` distinguishes them: present only on records read from the vai-avatar interior index. detailOf() derives the same flag, and the two now agree because they read the same field.fbd38cf -
detailOf: a vai-avatar photograph is not a product, even though it carries a sku
Verified signatureavatar-interiors.js gives every photograph published on vai-avatar a namespaced sku (`avatarimg:<id>`) so it travels the same code path as a catalogue product: same viewer, same panel, same neon. That is worth keeping, but `Boolean(record.sku)` cannot tell the two apart, so every such photograph rendered as a product: the heading was the literal `avatarimg:b68dc779370dc70e`, the description came from an empty spec sheet, and no room, category or tag was shown even though the record carries all of them. `avatarImageId` is what actually distinguishes them -- it is present only on records read from the vai-avatar interior index. Keying off it renders the photograph as what it is, and leaves the shared viewer and shared `sku` untouched.4d8e264 -
shareURL: send a vai-avatar photograph to /share/i/, not /share/p/
Verified signatureavatar-interiors.js gives every photograph published on vai-avatar a namespaced sku (`avatarimg:<image_id>`) so it travels the same path as a catalogue product: same viewer, same detail panel, same neon, same share anchor. That sku is not an identity the product catalogue knows, and shareURL() preferred it, so every one of those photographs was sent to `/share/p/avatarimg%3A...` -- a route that resolves only against the product catalogue. It answered "Not Found" for the whole set, on the tile's share button and on the viewer's "Xem trang" alike. `avatarImageId` is present only on records read from the vai-avatar interior index and is the raw id, which is what `/share/i/` expects, so the interior identity is now resolved first and unconditionally. No prefix has to be stripped, and it cannot drift from whatever namespace merge.js happens to use for tiles. The namespaced sku stays on the record: it is what keeps the photograph on the product code path inside this app. The bug was only t…7c7b7e4 -
Neon: run the light around the frame's perimeter instead of spinning the ring layer
Verified signatureThe ring was a full-size ::before carrying BOTH the conic gradient and the 2px border mask, and the animation rotated that whole layer. The mask rotates with it, so on a rectangular tile the painted border no longer matched the tile's own edges -- the highlight appeared clipped, skewed and partly off the frame. On a near-square tile it happened to look right, which is why it read as "a static neon border" rather than as a bug. Rotate the gradient's angle instead, and leave the layer still. @property is required so the angle is interpolable; the animation is gated behind @supports at-rule(@property) because an unregistered custom property would jump discretely at 50% and read as a strobe.83cbb13 -
Restore viewer CSS, ring only the newest ten, and clear the search pill
ML Intern1. CSS. The previous commit stripped the size/position controls with a script that cut from .field-controls to the end of the file, taking 147 lines with it: every .photo-* rule, .search-status, .search-progress and .footer. It went unnoticed because an unstyled <dialog> still opens, still shows the photograph, and never throws -- it just renders a raw UA box with the share toolbar stacked as plain text. Restored verbatim from the last good revision; app.css goes 17.1kB -> 24.7kB. 2. Ten rings, not sixty. RECENT_LIMIT (shown) and FEATURED_LIMIT (ringed) are now separate constants, because "show more photographs" and "only the newest glow" pull in opposite directions if one list drives both. 24 rings on a dense field is decoration rather than emphasis. The rest still render -- as ordinary tiles, which is what the brief asks for and what makes the ten legible. 3. More photographs: fieldCount 96/56 -> 150/96. Raised less on phones because tile area is bo…f134575 -
Ring every exact hit with running neon, and sync vai-avatar's newest photos
ML InternFour changes, all of them visible at a glance: 1. The neon outline now RUNS. It was a static ring with a breathing glow; it is a conic gradient masked to a 2px border and rotated every frame, so the light travels around the edge. An animated border-color cannot do this (borders take no gradient) and an animated box-shadow only pulses in place. The stroke is on ::before and the halo on ::after, because one pseudo-element can hold only one background and the ring needs both. mask-composite turns the full-size gradient into an outline; without it the gradient would fill the tile and hide the photograph, so the test asserts the mask is present. 2. Interiors published on vai-avatar now appear here. src/avatar-interiors.js reads bep40/vai-avatar2-interior/index.json, which is the file carrying created_at -- 153 rows, newest 2026-10-05. metadata.json has 1,752 rows but no timestamps at all, so it cannot answer "newest". These are matched by TEXT, not by th…eb92bc7 -
V.AI STUDIO backdrop, vai-avatar's product search, size/position, neon
ML InternFour changes, all on the search field. Background. The same artwork vai-avatar shows on its body (public/avatars/vaistudio_center.png, copied byte-for-byte, md5 checked) at 30% opacity. It is a separate fixed layer rather than a background-image on .app because opacity on .app would fade the photographs and the search pill with it, and background-attachment:fixed cannot compose with the .vignette and .grain layers already sitting on top of the field. Product search. Ranking is now vai-avatar's own scoreProduct(), ported to product-search.js rather than extended in place, so the two modules can be diffed against each other when the original changes. The whole rule set carries over: exact SKU/model/title must win outright, millimetre sizes are compared numerically (the specs write "900 mm", "900mm" and "W900" for the same shelf), material words are matched against specs as well as title, and brand names get a flat bonus. 35 tests in scripts/test-product-search.mjs pin each rule. Both s…0df020e
12 days ago
-
keep the share button's label on a phone screen
ML InternChanging the control from <a target="_blank"> to <button> to reach navigator.share kept its class name, and the class carried a rule written for the anchor: at 560px and below, .photo-share span { display: none }. On a phone that left a 44px button with no visible label and no accessible name, since display:none removes the text from the accessibility tree too -- and the label is where the "Đã chia sẻ" confirmation appears, so the phone could not see the share succeed. Clipped out of sight instead: the span keeps a 1px box and clip-path: inset(50%), which is the standard visually-hidden pattern. The text stays in the accessibility tree, so the button is still named, and the confirmation still updates -- it just is not drawn on a screen with no room for it. Measured on the built app at 1280px and on a Pixel 7 viewport: button is 44x44 in both, the span computes to position:absolute with clip-path:inset(50%), and a click still reaches navigator.share and rewrites the label to "Đã chia …1d2e122 -
viewer: share through the platform's own sheet, and drop the origin
ML InternTwo changes to the same panel, one reason: the viewer was a place to look at a photograph and to leave the app, and it was doing neither job well. The share control was an <a target="_blank">, which opens a browser tab. That is not sharing. navigator.share is where the platform's share sheet lives, and on a phone it is the sheet with the reader's real apps on it -- Messages, Zalo, Telegram -- which no anchor can reach. It is now a <button> that calls navigator.share with the photograph's canonical URL and title, falling back to the clipboard where share is missing (desktop Firefox, older Safari) or the permission is refused, so the action never dead-ends. The confirmation goes into the button's own label for 1.8s rather than into an alert(). The viewer is full-screen; a modal dialog stacked on top of the photograph is a worse outcome than the share failing quietly. A dismissed sheet rejects with AbortError, which is a reader saying no rather than an error, so it is caught and ignored…cd00894 -
note why detail.description cannot gate the panel
ML InternMeasured rather than assumed: adding it to hasDetail takes ambient tiles from 0 to 180 shown, and every one of them would hold only the single fixed sentence describeInterior falls back to. All 1,222 real interiors already pass the gate on their own metadata, so nothing is gained.9c69ff8 -
Share link on every photograph, a real description, no source button
ML InternThree things the viewer was missing, all reported from using it. A share link on each photograph in the field. Every tile now carries one, pointing at the same public address the viewer's toolbar link does: /share/i/<id> for an interior photograph, /share/p/<slug> for a catalogue product. All 1,222 interiors and all 11,704 products resolve to one; the 180 ambient tiles in collection.json resolve too, because they carry ids. Hidden until hover, except the top three results, which show theirs outright -- 150 photographs each wearing a button turns the plane into a control panel. The button is aria-hidden and tabIndex -1, nested inside the tile's <button>. Nesting an <a> in a <button> is not valid HTML, so it is a pointer affordance only: the reachable share is the named link in the viewer. Clicking it opens a new tab -- navigating away would destroy the search, and the field has no route to come back to. Getting it clickable took three fixes, each measured rather than guessed: The sh…4641f85 -
viewer: name, describe and link every photo the search finds
ML InternOpening a photograph in the viewer showed nothing but the picture: no title, no room, no categories, no link back to the article it came from. The record carried all four -- worker.js spreads the whole image-embeddings.json row into each search hit, so title, categories and source arrive with the photograph. But nothing downstream ever kept them. ImageField creates one DOM node per photograph and reuses it across searches; the node's onclick closed over the record it was built from, and on first paint that record is the ambient entry from collection.json, which holds only id/url/ar/blur. A node's position was refreshed on every layout pass, its record was not. So any photograph that had been on screen before you searched -- 180 of the 1,222 -- kept opening with none of its metadata, no matter that it matched and now carried a title and a source. Layout now writes node.image alongside node.target, and the click handler reads it at click time. Adds src/detail.js, restored from ffe2656 …29ed8a3
13 days ago
-
feat: product share links + repair Malloca/Hafele misbranding
Verified signatureTwo things. Share links. vai-avatar served share pages for interior images and chat answers but nothing for products, so a product link had nowhere to point. A /share/p/<sku> route now exists there and resolves against VAIX_PRODUCTS, the 11,704-row catalogue that Space already loads. A "Xem thêm" button in the viewer toolbar links products to it and interiors to the existing /share/i/. The link is keyed on the photo, not the image, so paging a gallery still shares the product rather than one of its photographs. Misbranding. 99 rows are named "Malloca | ..." while their brand field says Hafele, and all 1,011 of their image URLs point at Hafele's CDN rather than the Malloca storefront. Hafele resells Malloca, so these are Malloca products catalogued under the wrong brand. 77 now show a real Malloca photograph and the remaining 22, which have no Malloca row anywhere in the file, keep their photo with the brand corrected. No product is dropped and no non-Malloca row is touched. Matching…9bf56b3 -
fix(catalogue): stop dropping 355 Malloca products that are not duplicates
Verified signature3fa0a11 -
feat(catalogue): load the full 8,162-product shop, not just pasted URLs
Verified signaturea301301 -
feat(field): merge Malloca products into the interior image field
Verified signaturee34a847