|
@@ -1,14 +1,135 @@
|
|
|
-import { Component } from "solid-js"
|
|
|
|
|
|
|
+import { useFilteredList } from "@opencode-ai/ui/hooks"
|
|
|
|
|
+import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
|
|
|
|
+import { Switch } from "@opencode-ai/ui/switch"
|
|
|
|
|
+import { Icon } from "@opencode-ai/ui/icon"
|
|
|
|
|
+import { IconButton } from "@opencode-ai/ui/icon-button"
|
|
|
|
|
+import { TextField } from "@opencode-ai/ui/text-field"
|
|
|
|
|
+import type { IconName } from "@opencode-ai/ui/icons/provider"
|
|
|
|
|
+import { type Component, For, Show } from "solid-js"
|
|
|
import { useLanguage } from "@/context/language"
|
|
import { useLanguage } from "@/context/language"
|
|
|
|
|
+import { type ModelKey, useLocal } from "@/context/local"
|
|
|
|
|
+import { popularProviders } from "@/hooks/use-providers"
|
|
|
|
|
+
|
|
|
|
|
+type ModelItem = ReturnType<ReturnType<typeof useLocal>["model"]["list"]>[number]
|
|
|
|
|
|
|
|
export const SettingsModels: Component = () => {
|
|
export const SettingsModels: Component = () => {
|
|
|
|
|
+ const local = useLocal()
|
|
|
const language = useLanguage()
|
|
const language = useLanguage()
|
|
|
|
|
|
|
|
|
|
+ const list = useFilteredList<ModelItem>({
|
|
|
|
|
+ items: (_filter) => local.model.list(),
|
|
|
|
|
+ key: (x) => `${x.provider.id}:${x.id}`,
|
|
|
|
|
+ filterKeys: ["provider.name", "name", "id"],
|
|
|
|
|
+ sortBy: (a, b) => a.name.localeCompare(b.name),
|
|
|
|
|
+ groupBy: (x) => x.provider.id,
|
|
|
|
|
+ sortGroupsBy: (a, b) => {
|
|
|
|
|
+ const aIndex = popularProviders.indexOf(a.category)
|
|
|
|
|
+ const bIndex = popularProviders.indexOf(b.category)
|
|
|
|
|
+ const aPopular = aIndex >= 0
|
|
|
|
|
+ const bPopular = bIndex >= 0
|
|
|
|
|
+
|
|
|
|
|
+ if (aPopular && !bPopular) return -1
|
|
|
|
|
+ if (!aPopular && bPopular) return 1
|
|
|
|
|
+ if (aPopular && bPopular) return aIndex - bIndex
|
|
|
|
|
+
|
|
|
|
|
+ const aName = a.items[0].provider.name
|
|
|
|
|
+ const bName = b.items[0].provider.name
|
|
|
|
|
+ return aName.localeCompare(bName)
|
|
|
|
|
+ },
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
- <div class="flex flex-col h-full overflow-y-auto">
|
|
|
|
|
- <div class="flex flex-col gap-6 p-6 max-w-[600px]">
|
|
|
|
|
- <h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
|
|
|
|
|
- <p class="text-14-regular text-text-weak">{language.t("settings.models.description")}</p>
|
|
|
|
|
|
|
+ <div class="flex flex-col h-full overflow-y-auto no-scrollbar" style={{ padding: "0 40px 40px 40px" }}>
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="sticky top-0 z-10"
|
|
|
|
|
+ style={{
|
|
|
|
|
+ background:
|
|
|
|
|
+ "linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha) calc(100% - 24px), transparent)",
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
|
|
|
|
|
+ <h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
|
|
|
|
|
+ <div class="flex items-center gap-2 px-3 h-9 rounded-lg bg-surface-base">
|
|
|
|
|
+ <Icon name="magnifying-glass" class="text-icon-weak-base flex-shrink-0" />
|
|
|
|
|
+ <TextField
|
|
|
|
|
+ variant="ghost"
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ value={list.filter()}
|
|
|
|
|
+ onChange={list.onInput}
|
|
|
|
|
+ placeholder={language.t("dialog.model.search.placeholder")}
|
|
|
|
|
+ spellcheck={false}
|
|
|
|
|
+ autocorrect="off"
|
|
|
|
|
+ autocomplete="off"
|
|
|
|
|
+ autocapitalize="off"
|
|
|
|
|
+ class="flex-1"
|
|
|
|
|
+ />
|
|
|
|
|
+ <Show when={list.filter()}>
|
|
|
|
|
+ <IconButton icon="circle-x" variant="ghost" onClick={list.clear} />
|
|
|
|
|
+ </Show>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="flex flex-col gap-8 max-w-[720px]">
|
|
|
|
|
+ <Show
|
|
|
|
|
+ when={!list.grouped.loading}
|
|
|
|
|
+ fallback={
|
|
|
|
|
+ <div class="flex flex-col items-center justify-center py-12 text-center">
|
|
|
|
|
+ <span class="text-14-regular text-text-weak">
|
|
|
|
|
+ {language.t("common.loading")}
|
|
|
|
|
+ {language.t("common.loading.ellipsis")}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ }
|
|
|
|
|
+ >
|
|
|
|
|
+ <Show
|
|
|
|
|
+ when={list.flat().length > 0}
|
|
|
|
|
+ fallback={
|
|
|
|
|
+ <div class="flex flex-col items-center justify-center py-12 text-center">
|
|
|
|
|
+ <span class="text-14-regular text-text-weak">{language.t("dialog.model.empty")}</span>
|
|
|
|
|
+ <Show when={list.filter()}>
|
|
|
|
|
+ <span class="text-14-regular text-text-strong mt-1">"{list.filter()}"</span>
|
|
|
|
|
+ </Show>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ }
|
|
|
|
|
+ >
|
|
|
|
|
+ <For each={list.grouped.latest}>
|
|
|
|
|
+ {(group) => (
|
|
|
|
|
+ <div class="flex flex-col gap-1">
|
|
|
|
|
+ <div class="flex items-center gap-2 pb-2">
|
|
|
|
|
+ <ProviderIcon id={group.category as IconName} class="size-5 shrink-0 icon-strong-base" />
|
|
|
|
|
+ <span class="text-14-medium text-text-strong">{group.items[0].provider.name}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="bg-surface-raised-base px-4 rounded-lg">
|
|
|
|
|
+ <For each={group.items}>
|
|
|
|
|
+ {(item) => {
|
|
|
|
|
+ const key: ModelKey = { providerID: item.provider.id, modelID: item.id }
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
|
|
|
|
|
+ <div class="min-w-0">
|
|
|
|
|
+ <span class="text-14-regular text-text-strong truncate block">{item.name}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="flex-shrink-0">
|
|
|
|
|
+ <Switch
|
|
|
|
|
+ checked={!!local.model.visible(key)}
|
|
|
|
|
+ onChange={(checked) => {
|
|
|
|
|
+ local.model.setVisibility(key, checked)
|
|
|
|
|
+ }}
|
|
|
|
|
+ hideLabel
|
|
|
|
|
+ >
|
|
|
|
|
+ {item.name}
|
|
|
|
|
+ </Switch>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )
|
|
|
|
|
+ }}
|
|
|
|
|
+ </For>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </For>
|
|
|
|
|
+ </Show>
|
|
|
|
|
+ </Show>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
)
|
|
)
|