mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 23:35:20 +00:00
Modo "Por tema" no acervo de falas: expansão determinística do tema via DeepSeek (generateObject, timeout, fallback null) alimenta o motor lexical existente (searchText/keywords OR), mantendo facetas AND, paginação por fala, o OR de cortes do C174 e o access control intacto. Resultado de tema mostra selo e "Por que apareceu" com trecho real (frase contínua), sem score; degradação honesta para termo exato com aviso e vazio honesto com saídas. - URL: mode=tema (default omitido; canonicaliza sem q), seletor otimista - ai/expandSpeechSearchTheme + lib/speechThemeTerms (sanitiza saída do modelo) - view model com themeMatchTerm + espelho speechMatchesSearchTerm - UI do card de tema (selos, proveniência, ações) e aviso degradado - sem migration/Consent/exposição pública; gate de leitura inalterado - testes unit/int/e2e; design hi-fi estendido e certificado pelo designer
103 lines
3.7 KiB
TypeScript
103 lines
3.7 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
buildHighlightedExcerpt,
|
|
findHighlightRanges,
|
|
findPhraseRanges,
|
|
matchesSearchTerms,
|
|
splitHighlightedParts,
|
|
} from '@/lib/speechHighlight'
|
|
|
|
const highlighted = (text: string, query: string): string[] =>
|
|
splitHighlightedParts(text, query)
|
|
.filter((part) => part.highlighted)
|
|
.map((part) => part.text)
|
|
|
|
describe('findHighlightRanges', () => {
|
|
it('maps an accent-free query back to the accented original spelling', () => {
|
|
const text = 'A defesa do SUS e da saúde pública'
|
|
expect(findHighlightRanges(text, 'saude')).toEqual([{ start: 21, end: 26 }])
|
|
expect(highlighted(text, 'saude')).toEqual(['saúde'])
|
|
})
|
|
|
|
it('finds every term of a multi-word query, accent-insensitively', () => {
|
|
const text = 'O Governo retomou a Farmácia Popular na Bahia'
|
|
expect(highlighted(text, 'farmacia popular')).toEqual(['Farmácia', 'Popular'])
|
|
expect(highlighted(text, 'FARMÁCIA')).toEqual(['Farmácia'])
|
|
})
|
|
|
|
it('merges overlapping occurrences', () => {
|
|
expect(findHighlightRanges('banana', 'ana')).toEqual([{ start: 1, end: 6 }])
|
|
})
|
|
|
|
it('handles whitespace and punctuation around the match', () => {
|
|
const text = ' saúde,\n educação '
|
|
expect(highlighted(text, 'saude educacao')).toEqual(['saúde', 'educação'])
|
|
})
|
|
|
|
it('returns nothing for an empty query or a miss', () => {
|
|
expect(findHighlightRanges('texto', '')).toEqual([])
|
|
expect(findHighlightRanges('texto', 'inexistente')).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('findPhraseRanges (C192)', () => {
|
|
it('highlights the whole phrase as one continuous range, keeping the accents', () => {
|
|
const text = 'Defendemos o atendimento público e acesso universal à saúde para todos'
|
|
const ranges = findPhraseRanges(text, 'acesso universal à saúde')
|
|
|
|
expect(ranges).toHaveLength(1)
|
|
expect(text.slice(ranges[0]!.start, ranges[0]!.end)).toBe('acesso universal à saúde')
|
|
})
|
|
|
|
it('finds overlapping occurrences of the same phrase', () => {
|
|
expect(findPhraseRanges('ana ana', 'ana')).toEqual([
|
|
{ start: 0, end: 3 },
|
|
{ start: 4, end: 7 },
|
|
])
|
|
})
|
|
|
|
it('falls back to the per-term ranges when the phrase is not contiguous', () => {
|
|
const ranges = findPhraseRanges('saúde pública e muito acesso', 'acesso universal')
|
|
expect(ranges).toEqual([{ start: 22, end: 28 }])
|
|
})
|
|
|
|
it('returns nothing for an empty phrase', () => {
|
|
expect(findPhraseRanges('texto', '')).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('matchesSearchTerms', () => {
|
|
it('requires every term, in any order', () => {
|
|
expect(matchesSearchTerms('A defesa do SUS e da saúde', 'saude sus')).toBe(true)
|
|
expect(matchesSearchTerms('A defesa do SUS', 'saude educacao')).toBe(false)
|
|
expect(matchesSearchTerms('A defesa do SUS', '')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('buildHighlightedExcerpt', () => {
|
|
it('windows around the first match and flags truncation', () => {
|
|
const text = `${'a'.repeat(300)} saúde ${'b'.repeat(300)}`
|
|
const excerpt = buildHighlightedExcerpt(text, 'saude', { radius: 20 })
|
|
|
|
expect(excerpt.truncatedStart).toBe(true)
|
|
expect(excerpt.truncatedEnd).toBe(true)
|
|
const parts = excerpt.parts
|
|
expect(parts.some((part) => part.highlighted && part.text === 'saúde')).toBe(true)
|
|
})
|
|
|
|
it('falls back to the text start without highlights when nothing matches', () => {
|
|
const excerpt = buildHighlightedExcerpt('Uma fala sem o termo buscado', 'inexistente', {
|
|
radius: 10,
|
|
})
|
|
expect(excerpt.parts.every((part) => !part.highlighted)).toBe(true)
|
|
expect(excerpt.truncatedStart).toBe(false)
|
|
expect(excerpt.truncatedEnd).toBe(true)
|
|
})
|
|
|
|
it('returns empty parts for empty text', () => {
|
|
expect(buildHighlightedExcerpt(' ', 'saude').parts).toEqual([])
|
|
})
|
|
})
|