| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155 |
- import { Button } from "@opencode-ai/ui/button"
- import { useDialog } from "@opencode-ai/ui/context/dialog"
- import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
- import { Tag } from "@opencode-ai/ui/tag"
- import { showToast } from "@opencode-ai/ui/toast"
- import type { IconName } from "@opencode-ai/ui/icons/provider"
- import { popularProviders, useProviders } from "@/hooks/use-providers"
- import { createMemo, type Component, For, Show } from "solid-js"
- import { useLanguage } from "@/context/language"
- import { useGlobalSDK } from "@/context/global-sdk"
- import { DialogConnectProvider } from "./dialog-connect-provider"
- import { DialogSelectProvider } from "./dialog-select-provider"
- type ProviderSource = "env" | "api" | "config" | "custom"
- type ProviderMeta = { source?: ProviderSource }
- export const SettingsProviders: Component = () => {
- const dialog = useDialog()
- const language = useLanguage()
- const globalSDK = useGlobalSDK()
- const providers = useProviders()
- const connected = createMemo(() => providers.connected())
- const popular = createMemo(() => {
- const connectedIDs = new Set(connected().map((p) => p.id))
- const items = providers
- .popular()
- .filter((p) => !connectedIDs.has(p.id))
- .slice()
- items.sort((a, b) => popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id))
- return items
- })
- const source = (item: unknown) => (item as ProviderMeta).source
- const type = (item: unknown) => {
- const current = source(item)
- if (current === "env") return language.t("settings.providers.tag.environment")
- if (current === "api") return language.t("provider.connect.method.apiKey")
- if (current === "config") return language.t("settings.providers.tag.config")
- if (current === "custom") return language.t("settings.providers.tag.custom")
- return language.t("settings.providers.tag.other")
- }
- const canDisconnect = (item: unknown) => source(item) !== "env"
- const disconnect = async (providerID: string, name: string) => {
- await globalSDK.client.auth
- .remove({ providerID })
- .then(async () => {
- await globalSDK.client.global.dispose()
- showToast({
- variant: "success",
- icon: "circle-check",
- title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }),
- description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }),
- })
- })
- .catch((err: unknown) => {
- const message = err instanceof Error ? err.message : String(err)
- showToast({ title: language.t("common.requestFailed"), description: message })
- })
- }
- 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 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
- <div class="flex flex-col gap-1 pt-6 pb-8 max-w-[720px]">
- <h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
- </div>
- </div>
- <div class="flex flex-col gap-8 max-w-[720px]">
- <div class="flex flex-col gap-1">
- <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.connected")}</h3>
- <div class="bg-surface-raised-base px-4 rounded-lg">
- <Show
- when={connected().length > 0}
- fallback={
- <div class="py-4 text-14-regular text-text-weak">
- {language.t("settings.providers.connected.empty")}
- </div>
- }
- >
- <For each={connected()}>
- {(item) => (
- <div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
- <div class="flex items-center gap-3 min-w-0">
- <ProviderIcon id={item.id as IconName} class="size-5 shrink-0 icon-strong-base" />
- <span class="text-14-regular text-text-strong truncate">{item.name}</span>
- <Tag>{type(item)}</Tag>
- </div>
- <Show when={canDisconnect(item)}>
- <Button size="small" variant="ghost" onClick={() => void disconnect(item.id, item.name)}>
- {language.t("common.disconnect")}
- </Button>
- </Show>
- </div>
- )}
- </For>
- </Show>
- </div>
- </div>
- <div class="flex flex-col gap-1">
- <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.popular")}</h3>
- <div class="bg-surface-raised-base px-4 rounded-lg">
- <For each={popular()}>
- {(item) => (
- <div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
- <div class="flex items-center gap-x-3 min-w-0">
- <ProviderIcon id={item.id as IconName} class="size-5 shrink-0 icon-strong-base" />
- <span class="text-14-regular text-text-strong">{item.name}</span>
- <Show when={item.id === "opencode"}>
- <Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
- </Show>
- <Show when={item.id === "anthropic"}>
- <div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
- </Show>
- <Show when={item.id === "openai"}>
- <div class="text-14-regular text-text-weak">{language.t("dialog.provider.openai.note")}</div>
- </Show>
- <Show when={item.id.startsWith("github-copilot")}>
- <div class="text-14-regular text-text-weak">{language.t("dialog.provider.copilot.note")}</div>
- </Show>
- </div>
- <Button
- size="large"
- variant="secondary"
- icon="plus-small"
- onClick={() => {
- dialog.show(() => <DialogConnectProvider provider={item.id} />)
- }}
- >
- {language.t("common.connect")}
- </Button>
- </div>
- )}
- </For>
- </div>
- <Button
- variant="ghost"
- class="px-0 py-0 text-14-medium text-text-strong underline hover:bg-transparent active:bg-transparent"
- onClick={() => {
- dialog.show(() => <DialogSelectProvider />)
- }}
- >
- {language.t("dialog.provider.viewAll")}
- </Button>
- </div>
- </div>
- </div>
- )
- }
|