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