137 lines
5.4 KiB
TypeScript
137 lines
5.4 KiB
TypeScript
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 { 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 = () => {
|
|
const local = useLocal()
|
|
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 (
|
|
<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>
|
|
)
|
|
}
|