mattrguerra/cortexmainPrivate on GitHub, published here in full

ModelPicker.tsx

Language TypeScript5.6 KB157 lines

src/components/ModelPicker.tsx

  1. import { useEffect, useMemo, useRef, useState } from 'react'
  2. import type { ModelInfo } from '../ipc/gen/ModelInfo'
  3. import { isVision, priceLabel, tokens } from '../lib/format'
  4. import { useCurrentModel, useStore } from '../state/store'
  5. type Filter = 'all' | 'favorites' | 'vision' | 'reasoning' | 'local'
  6. const FILTERS: [Filter, string][] = [
  7. ['all', 'All'],
  8. ['favorites', 'Favorites'],
  9. ['vision', 'Vision'],
  10. ['reasoning', 'Reasoning'],
  11. ['local', 'Local'],
  12. ]
  13. export function ModelPicker({ onClose }: { onClose: () => void }) {
  14. const [q, setQ] = useState('')
  15. const [filter, setFilter] = useState<Filter>('all')
  16. const [cursor, setCursor] = useState(0)
  17. const input = useRef<HTMLInputElement>(null)
  18. const models = useStore((s) => s.models)
  19. const providers = useStore((s) => s.providers)
  20. const settings = useStore((s) => s.settings)
  21. const saveSettings = useStore((s) => s.saveSettings)
  22. const chooseModel = useStore((s) => s.chooseModel)
  23. const refreshModels = useStore((s) => s.refreshModels)
  24. const current = useCurrentModel()
  25. useEffect(() => input.current?.focus(), [])
  26. const favs = settings?.favorite_models ?? []
  27. const rows = useMemo(() => {
  28. const words = q.toLowerCase().split(/\s+/).filter(Boolean)
  29. const all: ModelInfo[] = providers.flatMap((p) => models[p.id]?.models ?? [])
  30. return all.filter((m) => {
  31. const hay = `${m.id} ${m.provider_id}`.toLowerCase()
  32. if (!words.every((w) => hay.includes(w))) return false
  33. if (filter === 'favorites') return favs.includes(m.id)
  34. if (filter === 'vision') return isVision(m)
  35. if (filter === 'reasoning') return m.tags.includes('reasoning')
  36. if (filter === 'local') return m.tags.includes('local')
  37. return true
  38. })
  39. }, [q, filter, models, providers, favs])
  40. const pick = (m: ModelInfo) => {
  41. void chooseModel(m.provider_id, m.id)
  42. onClose()
  43. }
  44. const toggleFav = (id: string) => {
  45. if (!settings) return
  46. const next = favs.includes(id) ? favs.filter((f) => f !== id) : [...favs, id]
  47. void saveSettings({ ...settings, favorite_models: next })
  48. }
  49. const di = models.deepinfra
  50. const source =
  51. di?.source === 'snapshot'
  52. ? `Prices as of ${di.snapshot_date ?? 'the bundled list'} (offline copy)`
  53. : di?.fetched_at
  54. ? `Prices from DeepInfra, updated ${new Date(di.fetched_at).toLocaleString()}`
  55. : ''
  56. return (
  57. <div
  58. className="popover model-picker glass-strong"
  59. role="dialog"
  60. aria-label="Choose a model"
  61. onKeyDown={(e) => {
  62. if (e.key === 'Escape') onClose()
  63. if (e.key === 'ArrowDown') {
  64. e.preventDefault()
  65. setCursor((c) => Math.min(c + 1, rows.length - 1))
  66. }
  67. if (e.key === 'ArrowUp') {
  68. e.preventDefault()
  69. setCursor((c) => Math.max(c - 1, 0))
  70. }
  71. if (e.key === 'Enter' && rows[cursor]) pick(rows[cursor])
  72. }}
  73. >
  74. <input
  75. ref={input}
  76. className="search-input"
  77. placeholder="Search models"
  78. value={q}
  79. onChange={(e) => {
  80. setQ(e.target.value)
  81. setCursor(0)
  82. }}
  83. aria-label="Search models"
  84. />
  85. <div className="filters" role="tablist">
  86. {FILTERS.map(([f, label]) => (
  87. <button
  88. key={f}
  89. role="tab"
  90. aria-selected={filter === f}
  91. className={`pill ${filter === f ? 'on' : ''}`}
  92. onClick={() => {
  93. setFilter(f)
  94. setCursor(0)
  95. }}
  96. >
  97. {label}
  98. </button>
  99. ))}
  100. </div>
  101. <div className="model-head">
  102. <span>Model</span>
  103. <span>In / out per 1M</span>
  104. <span>Context</span>
  105. </div>
  106. <ul className="model-list" role="listbox" aria-label="Models">
  107. {rows.map((m, i) => {
  108. const selected = m.id === current.modelId && m.provider_id === current.providerId
  109. return (
  110. <li
  111. key={`${m.provider_id}/${m.id}`}
  112. role="option"
  113. aria-selected={selected}
  114. className={`model-row ${i === cursor ? 'cursor' : ''} ${selected ? 'selected' : ''}`}
  115. onMouseEnter={() => setCursor(i)}
  116. onClick={() => pick(m)}
  117. >
  118. <span className="model-name">
  119. <button
  120. className={`star ${favs.includes(m.id) ? 'on' : ''}`}
  121. aria-label={favs.includes(m.id) ? 'Remove from favorites' : 'Add to favorites'}
  122. onClick={(e) => {
  123. e.stopPropagation()
  124. toggleFav(m.id)
  125. }}
  126. >
  127. ★
  128. </button>
  129. <span className="model-id">{m.id}</span>
  130. {m.tags.includes('vision') && <span className="tag">vision</span>}
  131. {m.tags.includes('reasoning') && <span className="tag">reasoning</span>}
  132. {m.provider_id !== 'deepinfra' && (
  133. <span className="tag local">{providers.find((p) => p.id === m.provider_id)?.name ?? 'local'}</span>
  134. )}
  135. </span>
  136. <span className="model-price">{priceLabel(m.prices)}</span>
  137. <span className="model-ctx">{m.context_length ? tokens(m.context_length) : ''}</span>
  138. </li>
  139. )
  140. })}
  141. {rows.length === 0 && <li className="empty-row">No models match.</li>}
  142. </ul>
  143. <div className="picker-foot">
  144. <span className="muted small">{source}</span>
  145. <button className="ghost small" onClick={() => providers.forEach((p) => void refreshModels(p.id, true))}>
  146. Refresh
  147. </button>
  148. </div>
  149. </div>
  150. )
  151. }