settings-models.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import { useFilteredList } from "@opencode-ai/ui/hooks"
  2. import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
  3. import { Switch } from "@opencode-ai/ui/switch"
  4. import { Icon } from "@opencode-ai/ui/icon"
  5. import { IconButton } from "@opencode-ai/ui/icon-button"
  6. import { TextField } from "@opencode-ai/ui/text-field"
  7. import { type Component, For, Show } from "solid-js"
  8. import { useLanguage } from "@/context/language"
  9. import { useModels } from "@/context/models"
  10. import { popularProviders } from "@/hooks/use-providers"
  11. import { SettingsList } from "./settings-list"
  12. import { SettingsServerPicker, SettingsServerScope } from "./settings-server-picker"
  13. type ModelItem = ReturnType<ReturnType<typeof useModels>["list"]>[number]
  14. const ListLoadingState: Component<{ label: string }> = (props) => {
  15. return (
  16. <div class="flex flex-col items-center justify-center py-12 text-center">
  17. <span class="text-14-regular text-text-weak">{props.label}</span>
  18. </div>
  19. )
  20. }
  21. const ListEmptyState: Component<{ message: string; filter: string }> = (props) => {
  22. return (
  23. <div class="flex flex-col items-center justify-center py-12 text-center">
  24. <span class="text-14-regular text-text-weak">{props.message}</span>
  25. <Show when={props.filter}>
  26. <span class="text-14-regular text-text-strong mt-1">&quot;{props.filter}&quot;</span>
  27. </Show>
  28. </div>
  29. )
  30. }
  31. export const SettingsModels: Component = () => {
  32. return (
  33. <SettingsServerScope>
  34. <SettingsModelsContent />
  35. </SettingsServerScope>
  36. )
  37. }
  38. const SettingsModelsContent: Component = () => {
  39. const language = useLanguage()
  40. const models = useModels()
  41. const list = useFilteredList<ModelItem>({
  42. items: (_filter) => models.list(),
  43. key: (x) => `${x.provider.id}:${x.id}`,
  44. filterKeys: ["provider.name", "name", "id"],
  45. sortBy: (a, b) => a.name.localeCompare(b.name),
  46. groupBy: (x) => x.provider.id,
  47. sortGroupsBy: (a, b) => {
  48. const aIndex = popularProviders.indexOf(a.category)
  49. const bIndex = popularProviders.indexOf(b.category)
  50. const aPopular = aIndex >= 0
  51. const bPopular = bIndex >= 0
  52. if (aPopular && !bPopular) return -1
  53. if (!aPopular && bPopular) return 1
  54. if (aPopular && bPopular) return aIndex - bIndex
  55. const aName = a.items[0].provider.name
  56. const bName = b.items[0].provider.name
  57. return aName.localeCompare(bName)
  58. },
  59. })
  60. return (
  61. <div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
  62. <div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
  63. <div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
  64. <div class="flex items-center justify-between gap-4">
  65. <h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
  66. <SettingsServerPicker />
  67. </div>
  68. <div class="flex items-center gap-2 px-3 h-9 rounded-lg bg-surface-base">
  69. <Icon name="magnifying-glass" class="text-icon-weak-base flex-shrink-0" />
  70. <TextField
  71. variant="ghost"
  72. type="text"
  73. value={list.filter()}
  74. onChange={list.onInput}
  75. placeholder={language.t("dialog.model.search.placeholder")}
  76. spellcheck={false}
  77. autocorrect="off"
  78. autocomplete="off"
  79. autocapitalize="off"
  80. class="flex-1"
  81. />
  82. <Show when={list.filter()}>
  83. <IconButton icon="circle-x" variant="ghost" onClick={list.clear} />
  84. </Show>
  85. </div>
  86. </div>
  87. </div>
  88. <div class="flex flex-col gap-8 max-w-[720px]">
  89. <Show
  90. when={!list.grouped.loading}
  91. fallback={
  92. <ListLoadingState label={`${language.t("common.loading")}${language.t("common.loading.ellipsis")}`} />
  93. }
  94. >
  95. <Show
  96. when={list.flat().length > 0}
  97. fallback={<ListEmptyState message={language.t("dialog.model.empty")} filter={list.filter()} />}
  98. >
  99. <For each={list.grouped.latest}>
  100. {(group) => (
  101. <div class="flex flex-col gap-1">
  102. <div class="flex items-center gap-2 pb-2">
  103. <ProviderIcon id={group.category} class="size-5 shrink-0 icon-strong-base" />
  104. <span class="text-14-medium text-text-strong">{group.items[0].provider.name}</span>
  105. </div>
  106. <SettingsList>
  107. <For each={group.items}>
  108. {(item) => {
  109. const key = { providerID: item.provider.id, modelID: item.id }
  110. return (
  111. <div class="flex flex-wrap items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
  112. <div class="min-w-0">
  113. <span class="text-14-regular text-text-strong truncate block">{item.name}</span>
  114. </div>
  115. <div class="flex-shrink-0">
  116. <Switch
  117. checked={models.visible(key)}
  118. onChange={(checked) => {
  119. models.setVisibility(key, checked)
  120. }}
  121. hideLabel
  122. >
  123. {item.name}
  124. </Switch>
  125. </div>
  126. </div>
  127. )
  128. }}
  129. </For>
  130. </SettingsList>
  131. </div>
  132. )}
  133. </For>
  134. </Show>
  135. </Show>
  136. </div>
  137. </div>
  138. )
  139. }