fix: TL869IMAX neon row + image load speed + search precision

#7
by bep40 - opened
dist/assets/index.js CHANGED
The diff for this file is too large to render. See raw diff
 
dist/assets/worker-nnOdWzlm.js CHANGED
The diff for this file is too large to render. See raw diff
 
src/avatar-interiors.js CHANGED
@@ -198,6 +198,11 @@ export function searchAvatarInteriors(text, items, limit = RECENT_LIMIT) {
198
  if (!hits) continue;
199
  // Covering the whole query beats matching one rare word hard.
200
  score += hits * 10;
 
 
 
 
 
201
  scored.push({ ...item, score });
202
  }
203
  scored.sort((a, b) => b.score - a.score || (a.addedAt < b.addedAt ? 1 : -1));
 
198
  if (!hits) continue;
199
  // Covering the whole query beats matching one rare word hard.
200
  score += hits * 10;
201
+ // A single accidental 25-point term (e.g. "tu" matching one caption) is
202
+ // noise: unrelated interiors must not appear at all. Keep only interiors
203
+ // with a real match — either the whole-phrase 900 identity hit, or enough
204
+ // term coverage to reach a meaningful 60 points.
205
+ if (score < 900 && score < 60) continue;
206
  scored.push({ ...item, score });
207
  }
208
  scored.sort((a, b) => b.score - a.score || (a.addedAt < b.addedAt ? 1 : -1));
src/image-loader.js CHANGED
@@ -1,5 +1,5 @@
1
  /** Bounded download/decode queue, with at most two image insertions per frame. */
2
- export function createImageLoader({ concurrency = 6, cacheSize = 64 } = {}) {
3
  const queue = [], completed = [];
4
  const active = new Set(), cache = new Map();
5
  let disposed = false, frame = 0, scheduled = false;
@@ -14,7 +14,7 @@ export function createImageLoader({ concurrency = 6, cacheSize = 64 } = {}) {
14
 
15
  function flush() {
16
  frame = 0;
17
- for (let i = 0; i < 2 && completed.length; i++) {
18
  const job = completed.shift();
19
  if (job.cancelled) remember(job.image);
20
  else job.receive(job.image);
@@ -36,7 +36,7 @@ export function createImageLoader({ concurrency = 6, cacheSize = 64 } = {}) {
36
  active.add(job);
37
  const photo = job.image = new Image();
38
  photo.decoding = 'async';
39
- photo.fetchPriority = job.priority < 12 ? 'high' : 'low';
40
  photo.src = job.url;
41
  // Decoding completes before this element is attached to the animated field.
42
  photo.decode().then(() => {
 
1
  /** Bounded download/decode queue, with at most two image insertions per frame. */
2
+ export function createImageLoader({ concurrency = 12, cacheSize = 128 } = {}) {
3
  const queue = [], completed = [];
4
  const active = new Set(), cache = new Map();
5
  let disposed = false, frame = 0, scheduled = false;
 
14
 
15
  function flush() {
16
  frame = 0;
17
+ for (let i = 0; i < 4 && completed.length; i++) {
18
  const job = completed.shift();
19
  if (job.cancelled) remember(job.image);
20
  else job.receive(job.image);
 
36
  active.add(job);
37
  const photo = job.image = new Image();
38
  photo.decoding = 'async';
39
+ photo.fetchPriority = job.priority < 24 ? 'high' : 'low';
40
  photo.src = job.url;
41
  // Decoding completes before this element is attached to the animated field.
42
  photo.decode().then(() => {
src/merge.js CHANGED
@@ -249,7 +249,7 @@ export function mergeResults(interiors, products, options = {}) {
249
  // are enlarged and ringed exactly like the newest interiors, so a product
250
  // added on vai-avatar appears in this Space's "mới" front row with a neon
251
  // frame, no query needed.
252
- const RING_LIMIT = options.recentProductRing ?? 6;
253
  const recents = (options.recentProducts ?? [])
254
  .filter(item => item && typeof item === 'object' && item.sku && !seen.has(productKey(item.sku)))
255
  .sort((a, b) => (String(a.addedAt || '') < String(b.addedAt || '') ? 1 : String(a.addedAt || '') > String(b.addedAt || '') ? -1 : 0));
@@ -288,8 +288,24 @@ export function mergeResults(interiors, products, options = {}) {
288
  const showNewRow = String(options.query ?? "").trim().length === 0;
289
  out.unshift(...(showNewRow ? fresh : []));
290
 
 
 
 
 
 
 
 
 
 
 
291
  for (const image of interiors ?? []) {
292
- if (!image?.id || seen.has(interiorKey(image.id))) continue;
 
 
 
 
 
 
293
  seen.add(interiorKey(image.id));
294
  out.push(image);
295
  if (out.length >= interiorLimit) break;
@@ -322,7 +338,12 @@ export function mergeResults(interiors, products, options = {}) {
322
  if (seen.has(key)) continue;
323
  seen.add(key);
324
  const isExact = exact.has(key);
325
- scoredHits.push({ product, src, key, isExact, score: Number.isFinite(item?.score) ? item.score : 0 });
 
 
 
 
 
326
  if (scoredHits.length >= productLimit) break;
327
  }
328
  // Products arrive already ranked by the worker; the score field is the
 
249
  // are enlarged and ringed exactly like the newest interiors, so a product
250
  // added on vai-avatar appears in this Space's "mới" front row with a neon
251
  // frame, no query needed.
252
+ const RING_LIMIT = options.recentProductRing ?? 12;
253
  const recents = (options.recentProducts ?? [])
254
  .filter(item => item && typeof item === 'object' && item.sku && !seen.has(productKey(item.sku)))
255
  .sort((a, b) => (String(a.addedAt || '') < String(b.addedAt || '') ? 1 : String(a.addedAt || '') > String(b.addedAt || '') ? -1 : 0));
 
288
  const showNewRow = String(options.query ?? "").trim().length === 0;
289
  out.unshift(...(showNewRow ? fresh : []));
290
 
291
+ // A relevance floor, so a search never paints tiles that only look like the
292
+ // query by chance. CLIP cosine answers "how close in meaning", and the lowest
293
+ // results in a top-60 pull are semantically unrelated to anything typed. The
294
+ // floor is RELATIVE to the query's own top match, so it adapts to however
295
+ // decisive the query is: "bếp đá" finds a clear winner and keeps nothing
296
+ // below half its strength, while a fuzzy query keeps a slightly broader tail.
297
+ // Tiles without a score (browse mode) are unaffected.
298
+ const cosineFloor = options.cosFloor ?? 0.18;
299
+ const cosineRel = options.cosRel ?? 0.45;
300
+ let topCosine = 0;
301
  for (const image of interiors ?? []) {
302
+ if (Number.isFinite(image?.score) && image.score > topCosine) topCosine = image.score;
303
+ }
304
+ const effectiveFloor = Math.max(cosineFloor, topCosine * cosineRel);
305
+ for (const image of interiors ?? []) {
306
+ const score = image?.score;
307
+ const filteredScore = score === undefined || !Number.isFinite(score) || score >= effectiveFloor;
308
+ if (!image?.id || !filteredScore || seen.has(interiorKey(image.id))) continue;
309
  seen.add(interiorKey(image.id));
310
  out.push(image);
311
  if (out.length >= interiorLimit) break;
 
338
  if (seen.has(key)) continue;
339
  seen.add(key);
340
  const isExact = exact.has(key);
341
+ const productFloor = options.productFloor ?? 20;
342
+ const score = Number.isFinite(item?.score) ? item.score : 0;
343
+ // A product that only scraped a 3-point token overlap is noise, not an
344
+ // answer: it would paint an unrelated photograph next to the query typed.
345
+ if (score < productFloor) continue;
346
+ scoredHits.push({ product, src, key, isExact, score });
347
  if (scoredHits.length >= productLimit) break;
348
  }
349
  // Products arrive already ranked by the worker; the score field is the
src/product-search.js CHANGED
@@ -223,7 +223,11 @@ export function searchLikeAvatar(text, items, limit = 24) {
223
  const scored = [];
224
  for (const product of items.values()) {
225
  const score = scoreProduct(product, String(text || '').trim(), terms);
226
- if (score > 0) scored.push({ sku: product.sku, score, product });
 
 
 
 
227
  }
228
  scored.sort((a, b) => b.score - a.score || a.sku.localeCompare(b.sku));
229
  return scored.slice(0, limit);
 
223
  const scored = [];
224
  for (const product of items.values()) {
225
  const score = scoreProduct(product, String(text || '').trim(), terms);
226
+ // A score below 24 is a single weak token scrape, not an answer: it would
227
+ // paint an unrelated photograph next to the query typed. Kept in sync with
228
+ // the merge.js productFloor default (20) plus the two strongest weak tiers
229
+ // (16 title + 3+10 hit bonus) — anything under it is noise.
230
+ if (score >= 24) scored.push({ sku: product.sku, score, product });
231
  }
232
  scored.sort((a, b) => b.score - a.score || a.sku.localeCompare(b.sku));
233
  return scored.slice(0, limit);