dialog-model.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import { createMemo, createSignal } from "solid-js"
  2. import { useLocal } from "@tui/context/local"
  3. import { useSync } from "@tui/context/sync"
  4. import { map, pipe, flatMap, entries, filter, sortBy, take } from "remeda"
  5. import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select"
  6. import { useDialog } from "@tui/ui/dialog"
  7. import { createDialogProviderOptions, DialogProvider } from "./dialog-provider"
  8. import { Keybind } from "@/util/keybind"
  9. import * as fuzzysort from "fuzzysort"
  10. export function useConnected() {
  11. const sync = useSync()
  12. return createMemo(() =>
  13. sync.data.provider.some((x) => x.id !== "opencode" || Object.values(x.models).some((y) => y.cost?.input !== 0)),
  14. )
  15. }
  16. export function DialogModel(props: { providerID?: string }) {
  17. const local = useLocal()
  18. const sync = useSync()
  19. const dialog = useDialog()
  20. const [ref, setRef] = createSignal<DialogSelectRef<unknown>>()
  21. const [query, setQuery] = createSignal("")
  22. const connected = useConnected()
  23. const providers = createDialogProviderOptions()
  24. const showExtra = createMemo(() => {
  25. if (!connected()) return false
  26. if (props.providerID) return false
  27. return true
  28. })
  29. const options = createMemo(() => {
  30. const q = query()
  31. const needle = q.trim()
  32. const showSections = showExtra() && needle.length === 0
  33. const favorites = connected() ? local.model.favorite() : []
  34. const recents = local.model.recent()
  35. const recentList = showSections
  36. ? recents.filter(
  37. (item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID),
  38. )
  39. : []
  40. const favoriteOptions = showSections
  41. ? favorites.flatMap((item) => {
  42. const provider = sync.data.provider.find((x) => x.id === item.providerID)
  43. if (!provider) return []
  44. const model = provider.models[item.modelID]
  45. if (!model) return []
  46. return [
  47. {
  48. key: item,
  49. value: {
  50. providerID: provider.id,
  51. modelID: model.id,
  52. },
  53. title: model.name ?? item.modelID,
  54. description: provider.name,
  55. category: "Favorites",
  56. disabled: provider.id === "opencode" && model.id.includes("-nano"),
  57. footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
  58. onSelect: () => {
  59. dialog.clear()
  60. local.model.set(
  61. {
  62. providerID: provider.id,
  63. modelID: model.id,
  64. },
  65. { recent: true },
  66. )
  67. },
  68. },
  69. ]
  70. })
  71. : []
  72. const recentOptions = showSections
  73. ? recentList.flatMap((item) => {
  74. const provider = sync.data.provider.find((x) => x.id === item.providerID)
  75. if (!provider) return []
  76. const model = provider.models[item.modelID]
  77. if (!model) return []
  78. return [
  79. {
  80. key: item,
  81. value: {
  82. providerID: provider.id,
  83. modelID: model.id,
  84. },
  85. title: model.name ?? item.modelID,
  86. description: provider.name,
  87. category: "Recent",
  88. disabled: provider.id === "opencode" && model.id.includes("-nano"),
  89. footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
  90. onSelect: () => {
  91. dialog.clear()
  92. local.model.set(
  93. {
  94. providerID: provider.id,
  95. modelID: model.id,
  96. },
  97. { recent: true },
  98. )
  99. },
  100. },
  101. ]
  102. })
  103. : []
  104. const providerOptions = pipe(
  105. sync.data.provider,
  106. sortBy(
  107. (provider) => provider.id !== "opencode",
  108. (provider) => provider.name,
  109. ),
  110. flatMap((provider) =>
  111. pipe(
  112. provider.models,
  113. entries(),
  114. filter(([_, info]) => info.status !== "deprecated"),
  115. filter(([_, info]) => (props.providerID ? info.providerID === props.providerID : true)),
  116. map(([model, info]) => {
  117. const value = {
  118. providerID: provider.id,
  119. modelID: model,
  120. }
  121. return {
  122. value,
  123. title: info.name ?? model,
  124. description: favorites.some(
  125. (item) => item.providerID === value.providerID && item.modelID === value.modelID,
  126. )
  127. ? "(Favorite)"
  128. : undefined,
  129. category: connected() ? provider.name : undefined,
  130. disabled: provider.id === "opencode" && model.includes("-nano"),
  131. footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
  132. onSelect() {
  133. dialog.clear()
  134. local.model.set(
  135. {
  136. providerID: provider.id,
  137. modelID: model,
  138. },
  139. { recent: true },
  140. )
  141. },
  142. }
  143. }),
  144. filter((x) => {
  145. if (!showSections) return true
  146. const value = x.value
  147. const inFavorites = favorites.some(
  148. (item) => item.providerID === value.providerID && item.modelID === value.modelID,
  149. )
  150. if (inFavorites) return false
  151. const inRecents = recentList.some(
  152. (item) => item.providerID === value.providerID && item.modelID === value.modelID,
  153. )
  154. if (inRecents) return false
  155. return true
  156. }),
  157. sortBy(
  158. (x) => x.footer !== "Free",
  159. (x) => x.title,
  160. ),
  161. ),
  162. ),
  163. )
  164. const popularProviders = !connected()
  165. ? pipe(
  166. providers(),
  167. map((option) => {
  168. return {
  169. ...option,
  170. category: "Popular providers",
  171. }
  172. }),
  173. take(6),
  174. )
  175. : []
  176. // Search shows a single merged list (favorites inline)
  177. if (needle) {
  178. const filteredProviders = fuzzysort.go(needle, providerOptions, { keys: ["title", "category"] }).map((x) => x.obj)
  179. const filteredPopular = fuzzysort.go(needle, popularProviders, { keys: ["title"] }).map((x) => x.obj)
  180. return [...filteredProviders, ...filteredPopular]
  181. }
  182. return [...favoriteOptions, ...recentOptions, ...providerOptions, ...popularProviders]
  183. })
  184. const provider = createMemo(() =>
  185. props.providerID ? sync.data.provider.find((x) => x.id === props.providerID) : null,
  186. )
  187. const title = createMemo(() => {
  188. if (provider()) return provider()!.name
  189. return "Select model"
  190. })
  191. return (
  192. <DialogSelect
  193. keybind={[
  194. {
  195. keybind: Keybind.parse("ctrl+a")[0],
  196. title: connected() ? "Connect provider" : "View all providers",
  197. onTrigger() {
  198. dialog.replace(() => <DialogProvider />)
  199. },
  200. },
  201. {
  202. keybind: Keybind.parse("ctrl+f")[0],
  203. title: "Favorite",
  204. disabled: !connected(),
  205. onTrigger: (option) => {
  206. local.model.toggleFavorite(option.value as { providerID: string; modelID: string })
  207. },
  208. },
  209. ]}
  210. ref={setRef}
  211. onFilter={setQuery}
  212. skipFilter={true}
  213. title={title()}
  214. current={local.model.current()}
  215. options={options()}
  216. />
  217. )
  218. }