155 lines
6.6 KiB
TypeScript
155 lines
6.6 KiB
TypeScript
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 items = providers.popular().slice()
|
|
items.sort((a, b) => popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id))
|
|
return items
|
|
})
|
|
|
|
const source = (item: unknown) => (item as ProviderMeta).source
|
|
|
|
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"
|
|
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]">
|
|
<div class="flex items-center justify-between gap-4">
|
|
<h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
|
|
</div>
|
|
</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>
|
|
<Show when={source(item) === "env"}>
|
|
<Tag>{language.t("settings.providers.tag.environment")}</Tag>
|
|
</Show>
|
|
<Show when={source(item) === "api"}>
|
|
<Tag>{language.t("provider.connect.method.apiKey")}</Tag>
|
|
</Show>
|
|
</div>
|
|
<Show when={source(item) === "api"}>
|
|
<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="small"
|
|
variant="secondary"
|
|
icon="plus-small"
|
|
onClick={() => {
|
|
dialog.show(() => <DialogConnectProvider provider={item.id} />)
|
|
}}
|
|
>
|
|
{language.t("common.connect")}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</For>
|
|
</div>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
class="w-full justify-start px-[11px] py-3.5 gap-4.5 text-14-medium"
|
|
icon="dot-grid"
|
|
onClick={() => {
|
|
dialog.show(() => <DialogSelectProvider />)
|
|
}}
|
|
>
|
|
{language.t("dialog.provider.viewAll")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|