fix(13-11): content classifier — plural films, specific-before-generic, no bare "show"
Three defects in the two query classifiers that pick the content tab and
its header label. Found while writing the regression test for the
"Podcast recommendations" mislabel the operator reported on-device.
- "recommend me 10 scifi films" matched NOTHING: the film rule listed
film|movie|movies but not the plural `films` — the operator's own
phrasing. It opened no content tab at all.
- "listen to a podcast" classified as `song`: the song rule's bare
`listen` was checked before the podcast rule. Specific terms now win —
podcast is matched first, and `listen to` is no longer a podcast token
(so "listen to music" stays a song query).
- A bare `show` counted as a podcast word, which is how an operator
phrases nearly everything ("show me my files", "show the logs"), so
unrelated queries rendered "Podcast recommendations".
Both classifiers are fixed identically and the reason they must agree is
now stated in each — they label the same panel. 16/16 content tests, 56/56
composable tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
82d1b60891
commit
08356b9e18
@@ -0,0 +1,34 @@
|
||||
// Regression: operator phrasing must not be mistaken for content queries.
|
||||
//
|
||||
// Reported on-device 2026-08-06 — the content panel showed "Podcast
|
||||
// recommendations" for a query that had nothing to do with podcasts. Both
|
||||
// classifiers (this one and ChatPage's loader label) matched a bare `show`,
|
||||
// which is how an operator phrases most requests to a node assistant:
|
||||
// "show me my files", "show the logs", "show installed apps".
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { preferredFirstTab } from '../contentFiltering'
|
||||
|
||||
describe('preferredFirstTab — operator phrasing is not a content query', () => {
|
||||
it('does not classify "show me ..." as a podcast query', () => {
|
||||
expect(preferredFirstTab('show me my files')).not.toBe('podcast')
|
||||
expect(preferredFirstTab('show the filebrowser logs')).not.toBe('podcast')
|
||||
expect(preferredFirstTab('show installed apps')).not.toBe('podcast')
|
||||
})
|
||||
|
||||
it('still classifies genuine podcast queries', () => {
|
||||
expect(preferredFirstTab('recommend me a podcast')).toBe('podcast')
|
||||
expect(preferredFirstTab('any good episode about bitcoin?')).toBe('podcast')
|
||||
// NB: a bare "listen" belongs to the song rule, which is checked
|
||||
// first — only the explicit "listen to a podcast" phrasing lands here.
|
||||
expect(preferredFirstTab('listen to a podcast about bitcoin')).toBe('podcast')
|
||||
})
|
||||
|
||||
it('keeps tv-show detection, which needs the two-word form', () => {
|
||||
expect(preferredFirstTab('best tv show to binge')).toBe('tvshow')
|
||||
})
|
||||
|
||||
it('still classifies films and music', () => {
|
||||
expect(preferredFirstTab('recommend me 10 scifi films')).toBe('film')
|
||||
expect(preferredFirstTab('good album for working')).toBe('song')
|
||||
})
|
||||
})
|
||||
@@ -167,9 +167,17 @@ export function extractQueryContext(q: string): string {
|
||||
|
||||
export function preferredFirstTab(userQuery: string): ContentTab | null {
|
||||
const q = userQuery.toLowerCase().trim()
|
||||
if (/\b(film|movie|movies)\b/.test(q)) return 'film'
|
||||
// `films` (plural) was missing: "recommend me 10 scifi films" — the
|
||||
// operator's own words — matched nothing and opened no content tab.
|
||||
if (/\b(film|films|movie|movies)\b/.test(q)) return 'film'
|
||||
// Podcast is checked BEFORE song because its words are the specific
|
||||
// ones: "listen to a podcast" must not be swallowed by the song rule's
|
||||
// bare `listen`. Conversely `show` is NOT a podcast word — it is how
|
||||
// operators phrase almost every request ("show me my files"), and it
|
||||
// made unrelated queries render "Podcast recommendations". `tv show`
|
||||
// keeps its own two-word form in the tvshow rule below.
|
||||
if (/\b(podcast|episode)\b/.test(q)) return 'podcast'
|
||||
if (/\b(song|music|track|album|band|artist|listen)\b/.test(q)) return 'song'
|
||||
if (/\b(podcast|episode|show|listen to)\b/.test(q)) return 'podcast'
|
||||
if (/\b(book|books|read|reading|novel|author|nonfiction|non-fiction)\b/.test(q)) return 'book'
|
||||
if (/\b(tv\b|tv show|tv series|series|television|streaming|binge|watch)\b/.test(q)) return 'tvshow'
|
||||
if (/\b(image|images|photo|photos|picture|pictures|screenshot|gallery|artwork|illustration)\b/.test(q)) return 'image'
|
||||
|
||||
@@ -641,15 +641,16 @@ const loaderContextType = computed(() => {
|
||||
if (panelOpen.value) return contentType.value
|
||||
const lastUser = [...chatStore.messages].reverse().find((m) => m.role === 'user')
|
||||
const q = (lastUser?.content ?? '').toLowerCase()
|
||||
// Podcast before song: its words are the specific ones, so "listen to a
|
||||
// podcast" must not be swallowed by the song rule's bare `listen`.
|
||||
// Mirrors contentFiltering.ts's preferredFirstTab ordering — these two
|
||||
// classifiers label the same panel and must not disagree.
|
||||
if (/\b(podcast|episode)\b/.test(q)) return 'podcast'
|
||||
if (/\b(song|music|track|album|band|artist|listen)\b/.test(q)) return 'song'
|
||||
if (/\b(book|novel|read|author|fiction|nonfiction|memoir)\b/.test(q)) return 'book'
|
||||
if (/\b(tv show|tv series|series|television|binge|season)\b/.test(q)) return 'tvshow'
|
||||
if (/\b(image|images|photo|photos|picture|pictures|screenshot|gallery|artwork|illustration)\b/.test(q)) return 'image'
|
||||
if (/\b(restaurant|restaurants|place|places|food|eat|dining|cafe|bar|pub|brunch|lunch|dinner)\b/.test(q)) return 'film'
|
||||
// NOT a bare `show`: "show me my files" / "show the logs" is the most
|
||||
// common phrasing in an operator chat, and it was labelling every such
|
||||
// query "Podcast recommendations" (reported on-device 2026-08-06).
|
||||
if (/\b(podcast|episode|listen to)\b/.test(q)) return 'podcast'
|
||||
if (/\b(news|latest|recent|current|what'?s happening|what are people saying)\b/.test(q)) return 'news'
|
||||
if (/\b(bip|protocol|debate|sentiment|bearish|bull case|macro)\b/.test(q)) return 'magazine'
|
||||
if (/\b(website|websites|where to check|best places|check online|resources?|sources?)\b/.test(q)) return 'websites'
|
||||
|
||||
Reference in New Issue
Block a user