diff --git a/server/index.mjs b/server/index.mjs index 855085a..ad59447 100644 --- a/server/index.mjs +++ b/server/index.mjs @@ -2584,7 +2584,11 @@ const suggestLimiter = rateLimit({ standardHeaders: true, legacyHeaders: false, }); -app.get('/api/search/suggest', suggestLimiter, async (req, res) => { +// NOTE: montée sur le router `r` (et non `app`) pour être servie sous les +// DEUX préfixes `/api` et `/proxy/api` (cf. apiBase() côté front : en prod le +// front appelle `/proxy/api`, sinon fallback SPA -> index.html -> parse error +// et seules les suggestions locales s'affichaient). Même pattern que transcript. +r.get('/search/suggest', suggestLimiter, async (req, res) => { try { const rawQ = typeof req.query.q === 'string' ? req.query.q : ''; const q = rawQ.trim(); diff --git a/server/tests/suggest.test.mjs b/server/tests/suggest.test.mjs index c6ef8ee..2c08705 100644 --- a/server/tests/suggest.test.mjs +++ b/server/tests/suggest.test.mjs @@ -155,6 +155,15 @@ try { expect(JSON.stringify(withoutWeb) === JSON.stringify(['dm', 'od', 'pt', 'ru', 'tw', 'yt']), `full registry fallback (got ${keys})`); }); + await scenario('served under both /api and /proxy/api (front apiBase)', async () => { + for (const prefix of ['/api', '/proxy/api']) { + const res = await fetch(`${baseUrl}${prefix}/search/suggest?q=tutoriel&providers=yt&limit=3`); + expect(res.status === 200, `expected 200 on ${prefix}, got ${res.status}`); + const body = await res.json(); + expect(body.q === 'tutoriel' && Array.isArray(body.groups?.yt), `${prefix} returns suggest JSON`); + } + }); + await scenario('no duplicate occurrence across groups (web enrichment included)', async () => { const res = await fetch(`${baseUrl}/api/search/suggest?q=tutoriel&limit=10`); expect(res.status === 200, `expected 200, got ${res.status}`); diff --git a/src/components/search/search-box.component.ts b/src/components/search/search-box.component.ts index 929beb4..695c74a 100644 --- a/src/components/search/search-box.component.ts +++ b/src/components/search/search-box.component.ts @@ -98,6 +98,16 @@ export class SearchBoxComponent { seen.add(k); return true; }); + // Tri « style YouTube/Google » : les complétions qui commencent par la + // frappe d'abord, puis le reste (tri stable : l'ordre providers est gardé). + const nq = normSuggest(q); + if (nq) { + merged.sort((a, b) => { + const pa = normSuggest(a.text).startsWith(nq) ? 0 : 1; + const pb = normSuggest(b.text).startsWith(nq) ? 0 : 1; + return pa - pb; + }); + } const out: SuggestFlatItem[] = [...recents, ...merged]; // Tier 3 — suggestions locales générées à la frappe (sans réseau) : la // frappe propose toujours quelque chose, même sans provider suggest