dialog-model.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  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 } from "@tui/ui/dialog-select"
  6. import { useDialog } from "@tui/ui/dialog"
  7. import { createDialogProviderOptions, DialogProvider } from "./dialog-provider"
  8. import { DialogVariant } from "./dialog-variant"
  9. import { useKeybind } from "../context/keybind"
  10. import * as fuzzysort from "fuzzysort"
  11. import { consoleManagedProviderLabel } from "@tui/util/provider-origin"
  12. export function useConnected() {
  13. const sync = useSync()
  14. return createMemo(() =>
  15. sync.data.provider.some((x) => x.id !== "opencode" || Object.values(x.models).some((y) => y.cost?.input !== 0)),
  16. )
  17. }
  18. export function DialogModel(props: { providerID?: string }) {
  19. const local = useLocal()
  20. const sync = useSync()
  21. const dialog = useDialog()
  22. const keybind = useKeybind()
  23. const [query, setQuery] = createSignal("")
  24. const connected = useConnected()
  25. const providers = createDialogProviderOptions()
  26. const showExtra = createMemo(() => connected() && !props.providerID)
  27. const options = createMemo(() => {
  28. const needle = query().trim()
  29. const showSections = showExtra() && needle.length === 0
  30. const favorites = connected() ? local.model.favorite() : []
  31. const recents = local.model.recent()
  32. function toOptions(items: typeof favorites, category: string) {
  33. if (!showSections) return []
  34. return items.flatMap((item) => {
  35. const provider = sync.data.provider.find((x) => x.id === item.providerID)
  36. if (!provider) return []
  37. const model = provider.models[item.modelID]
  38. if (!model) return []
  39. return [
  40. {
  41. key: item,
  42. value: { providerID: provider.id, modelID: model.id },
  43. title: model.name ?? item.modelID,
  44. description: consoleManagedProviderLabel(
  45. sync.data.console_state.consoleManagedProviders,
  46. provider.id,
  47. provider.name,
  48. ),
  49. category,
  50. disabled: provider.id === "opencode" && model.id.includes("-nano"),
  51. footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
  52. onSelect: () => {
  53. onSelect(provider.id, model.id)
  54. },
  55. },
  56. ]
  57. })
  58. }
  59. const favoriteOptions = toOptions(favorites, "Favorites")
  60. const recentOptions = toOptions(
  61. recents.filter(
  62. (item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID),
  63. ),
  64. "Recent",
  65. )
  66. const providerOptions = pipe(
  67. sync.data.provider,
  68. sortBy(
  69. (provider) => provider.id !== "opencode",
  70. (provider) => provider.name,
  71. ),
  72. flatMap((provider) =>
  73. pipe(
  74. provider.models,
  75. entries(),
  76. filter(([_, info]) => info.status !== "deprecated"),
  77. filter(([_, info]) => (props.providerID ? info.providerID === props.providerID : true)),
  78. map(([model, info]) => ({
  79. value: { providerID: provider.id, modelID: model },
  80. title: info.name ?? model,
  81. description: favorites.some((item) => item.providerID === provider.id && item.modelID === model)
  82. ? "(Favorite)"
  83. : undefined,
  84. category: connected()
  85. ? consoleManagedProviderLabel(sync.data.console_state.consoleManagedProviders, provider.id, provider.name)
  86. : undefined,
  87. disabled: provider.id === "opencode" && model.includes("-nano"),
  88. footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
  89. onSelect() {
  90. onSelect(provider.id, model)
  91. },
  92. })),
  93. filter((x) => {
  94. if (!showSections) return true
  95. if (favorites.some((item) => item.providerID === x.value.providerID && item.modelID === x.value.modelID))
  96. return false
  97. if (recents.some((item) => item.providerID === x.value.providerID && item.modelID === x.value.modelID))
  98. return false
  99. return true
  100. }),
  101. sortBy(
  102. (x) => x.footer !== "Free",
  103. (x) => x.title,
  104. ),
  105. ),
  106. ),
  107. )
  108. const popularProviders = !connected()
  109. ? pipe(
  110. providers(),
  111. map((option) => ({
  112. ...option,
  113. category: "Popular providers",
  114. })),
  115. take(6),
  116. )
  117. : []
  118. if (needle) {
  119. return [
  120. ...fuzzysort.go(needle, providerOptions, { keys: ["title", "category"] }).map((x) => x.obj),
  121. ...fuzzysort.go(needle, popularProviders, { keys: ["title"] }).map((x) => x.obj),
  122. ]
  123. }
  124. return [...favoriteOptions, ...recentOptions, ...providerOptions, ...popularProviders]
  125. })
  126. const provider = createMemo(() =>
  127. props.providerID ? sync.data.provider.find((x) => x.id === props.providerID) : null,
  128. )
  129. const title = createMemo(() => {
  130. const value = provider()
  131. if (!value) return "Select model"
  132. return consoleManagedProviderLabel(sync.data.console_state.consoleManagedProviders, value.id, value.name)
  133. })
  134. function onSelect(providerID: string, modelID: string) {
  135. local.model.set({ providerID, modelID }, { recent: true })
  136. const list = local.model.variant.list()
  137. const cur = local.model.variant.selected()
  138. if (cur === "default" || (cur && list.includes(cur))) {
  139. dialog.clear()
  140. return
  141. }
  142. if (list.length > 0) {
  143. dialog.replace(() => <DialogVariant />)
  144. return
  145. }
  146. dialog.clear()
  147. }
  148. return (
  149. <DialogSelect<ReturnType<typeof options>[number]["value"]>
  150. options={options()}
  151. keybind={[
  152. {
  153. keybind: keybind.all.model_provider_list?.[0],
  154. title: connected() ? "Connect provider" : "View all providers",
  155. onTrigger() {
  156. dialog.replace(() => <DialogProvider />)
  157. },
  158. },
  159. {
  160. keybind: keybind.all.model_favorite_toggle?.[0],
  161. title: "Favorite",
  162. disabled: !connected(),
  163. onTrigger: (option) => {
  164. local.model.toggleFavorite(option.value as { providerID: string; modelID: string })
  165. },
  166. },
  167. ]}
  168. onFilter={setQuery}
  169. flat={true}
  170. skipFilter={true}
  171. title={title()}
  172. current={local.model.current()}
  173. />
  174. )
  175. }