|
| 1 | +/** |
| 2 | + * @file tui-filters.js |
| 3 | + * @description TUI filter logic — tier, origin, verdict, health, text, and usability filters. |
| 4 | + * |
| 5 | + * @details |
| 6 | + * All the filtering that determines which model rows are visible in the TUI table |
| 7 | + * lives here. The main app.js calls applyTierFilter() on each frame and after every |
| 8 | + * keypress that changes a filter mode, so the visible set is always consistent. |
| 9 | + * |
| 10 | + * 🎯 Filter precedence (first failing check hides the row): |
| 11 | + * 1. Configure-only mode (E key) — hide models with no API key or bad health |
| 12 | + * 2. Usable-only mode (E key, second cycle) — only Health UP + verdict Normal/Perfect/Slow |
| 13 | + * 3. Sticky favorites (Y key) — pinned favorites bypass tier/origin/text filters |
| 14 | + * 4. Tier filter (T key) — only show models in the selected tier family |
| 15 | + * 5. Origin filter (O key) — only show models from the selected provider |
| 16 | + * 6. Verdict filter (V key) — only show models with the selected verdict |
| 17 | + * 7. Health filter (H key) — only show models with the selected health status |
| 18 | + * 8. Custom text filter (Ctrl+P) — case-insensitive match on name, ctx, provider |
| 19 | + * |
| 20 | + * @functions |
| 21 | + * → createTuiFilters(state, deps) — Returns { applyTierFilter, buildOriginCycle } |
| 22 | + * |
| 23 | + * @exports createTuiFilters |
| 24 | + * |
| 25 | + * @see src/app.js — calls applyTierFilter() on each render frame |
| 26 | + * @see src/tui-state.js — state shape for filter mode indices |
| 27 | + */ |
| 28 | + |
| 29 | +import { TIER_CYCLE, VERDICT_CYCLE, HEALTH_CYCLE } from './constants.js' |
| 30 | +import { TIER_LETTER_MAP } from './utils.js' |
| 31 | +import { getVerdict } from './utils.js' |
| 32 | + |
| 33 | +/** |
| 34 | + * 📖 createTuiFilters: Build the filter functions for a given TUI state + dependencies. |
| 35 | + * |
| 36 | + * @param {object} state — The TUI state object (from createTuiState) |
| 37 | + * @param {{ |
| 38 | + * sources: object, |
| 39 | + * getApiKey: function, |
| 40 | + * PROVIDER_METADATA: object, |
| 41 | + * }} deps — External dependencies needed by filter logic |
| 42 | + * @returns {{ applyTierFilter: () => Array, buildOriginCycle: () => Array }} |
| 43 | + */ |
| 44 | +export function createTuiFilters(state, { sources, getApiKey, PROVIDER_METADATA }) { |
| 45 | + /** |
| 46 | + * 📖 applyTierFilter: Apply all active filters to the model results. |
| 47 | + * 📖 Mutates r.hidden in-place for each result row. |
| 48 | + * 📖 Returns the results array for chaining. |
| 49 | + */ |
| 50 | + function applyTierFilter() { |
| 51 | + const activeTier = TIER_CYCLE[state.tierFilterMode] |
| 52 | + const activeOrigin = buildOriginCycle()[state.originFilterMode] |
| 53 | + const activeVerdict = VERDICT_CYCLE[state.verdictFilterMode] |
| 54 | + const activeHealth = HEALTH_CYCLE[state.healthFilterMode] |
| 55 | + |
| 56 | + state.results.forEach(r => { |
| 57 | + const stickyFavorite = state.favoritesPinnedAndSticky && r.isFavorite |
| 58 | + // 📖 CLI-only tools (rovo, gemini) and Zen models don't need traditional API keys — |
| 59 | + // 📖 they authenticate via their own CLI login flow, so "configured only" should never hide them. |
| 60 | + const providerMeta = PROVIDER_METADATA[r.providerKey] |
| 61 | + const noKeyNeeded = providerMeta?.cliOnly || providerMeta?.zenOnly |
| 62 | + // 📖 E toggles "Show only configured & working models": |
| 63 | + // 📖 hide models where provider has no key, or where the health status is noauth/auth_error (but keep timeout and 429) |
| 64 | + const badHealth = r.status === 'noauth' || r.status === 'auth_error' |
| 65 | + const unconfiguredHide = state.hideUnconfiguredModels && !noKeyNeeded && (!getApiKey(state.config, r.providerKey) || badHealth) |
| 66 | + if (unconfiguredHide) { |
| 67 | + r.hidden = true |
| 68 | + return |
| 69 | + } |
| 70 | + // 📖 Usable only: only show models with Health UP and Verdict Perfect/Normal/Slow |
| 71 | + if (state.bestModeOnly) { |
| 72 | + const bmVerdict = getVerdict(r) |
| 73 | + const bmVerdictOk = ['Perfect', 'Normal', 'Slow'].includes(bmVerdict) |
| 74 | + const bmHealthOk = r.status === 'up' |
| 75 | + if (!bmHealthOk || !bmVerdictOk) { |
| 76 | + r.hidden = true |
| 77 | + return |
| 78 | + } |
| 79 | + } |
| 80 | + // 📖 Sticky-favorites mode keeps usable favorites visible regardless of |
| 81 | + // 📖 tier/provider/text filters, but "Usable only" health still wins above. |
| 82 | + if (stickyFavorite) { |
| 83 | + r.hidden = false |
| 84 | + return |
| 85 | + } |
| 86 | + // 📖 Apply tier, origin, verdict, and health filters — model is hidden if it fails any |
| 87 | + const allowedTiers = (activeTier && TIER_LETTER_MAP[activeTier]) ? TIER_LETTER_MAP[activeTier] : [activeTier] |
| 88 | + const tierHide = activeTier !== null && !allowedTiers.includes(r.tier) |
| 89 | + const originHide = activeOrigin !== null && r.providerKey !== activeOrigin |
| 90 | + // 📖 Verdict filter: match against getVerdict(r) when active |
| 91 | + const rVerdict = getVerdict(r) |
| 92 | + const verdictHide = activeVerdict !== null && rVerdict !== activeVerdict |
| 93 | + // 📖 Health filter: match against r.status when active |
| 94 | + const healthHide = activeHealth !== null && r.status !== activeHealth |
| 95 | + if (tierHide || originHide || verdictHide || healthHide) { |
| 96 | + r.hidden = true |
| 97 | + return |
| 98 | + } |
| 99 | + // 📖 Custom text filter — case-insensitive includes match against model name, ctx, provider key, and provider display name. |
| 100 | + if (state.customTextFilter) { |
| 101 | + const q = state.customTextFilter.toLowerCase() |
| 102 | + const providerName = (sources[r.providerKey]?.name || '').toLowerCase() |
| 103 | + const match = (r.label || '').toLowerCase().includes(q) |
| 104 | + || (r.ctx || '').toLowerCase().includes(q) |
| 105 | + || (r.providerKey || '').toLowerCase().includes(q) |
| 106 | + || providerName.includes(q) |
| 107 | + r.hidden = !match |
| 108 | + return |
| 109 | + } |
| 110 | + r.hidden = false |
| 111 | + }) |
| 112 | + return state.results |
| 113 | + } |
| 114 | + |
| 115 | + /** |
| 116 | + * 📖 buildOriginCycle: Build the origin filter cycle array on demand. |
| 117 | + * 📖 [null, ...providerKeys] — null = "All", then each provider key in sources.js order. |
| 118 | + * @returns {Array<string|null>} |
| 119 | + */ |
| 120 | + function buildOriginCycle() { |
| 121 | + return [null, ...Object.keys(sources)] |
| 122 | + } |
| 123 | + |
| 124 | + return { |
| 125 | + applyTierFilter, |
| 126 | + buildOriginCycle, |
| 127 | + } |
| 128 | +} |
0 commit comments