settings-models.tsx 5.4 KB

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