|
|
@@ -1,208 +1,95 @@
|
|
|
-import { Component, createMemo, Match, onCleanup, onMount, Show, Switch } from "solid-js"
|
|
|
+import { Component, createMemo, Show } from "solid-js"
|
|
|
import { useLocal } from "@/context/local"
|
|
|
import { useDialog } from "@/context/dialog"
|
|
|
-import { popularProviders, useProviders } from "@/hooks/use-providers"
|
|
|
-import { SelectDialog } from "@opencode-ai/ui/select-dialog"
|
|
|
+import { popularProviders } from "@/hooks/use-providers"
|
|
|
import { Button } from "@opencode-ai/ui/button"
|
|
|
import { Tag } from "@opencode-ai/ui/tag"
|
|
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
|
|
-import { List, ListRef } from "@opencode-ai/ui/list"
|
|
|
-import { iife } from "@opencode-ai/util/iife"
|
|
|
-import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
|
|
-import { IconName } from "@opencode-ai/ui/icons/provider"
|
|
|
+import { List } from "@opencode-ai/ui/list"
|
|
|
import { DialogSelectProvider } from "./dialog-select-provider"
|
|
|
-import { DialogConnect } from "./dialog-connect"
|
|
|
+import { DialogManageModels } from "./dialog-manage-models"
|
|
|
|
|
|
-export const DialogModel: Component<{ connectedProvider?: string }> = (props) => {
|
|
|
+export const DialogModel: Component<{ provider?: string }> = (props) => {
|
|
|
const local = useLocal()
|
|
|
const dialog = useDialog()
|
|
|
- const providers = useProviders()
|
|
|
|
|
|
- return (
|
|
|
- <Switch>
|
|
|
- <Match when={providers.paid().length > 0}>
|
|
|
- {iife(() => {
|
|
|
- const models = createMemo(() =>
|
|
|
- local.model
|
|
|
- .list()
|
|
|
- .filter((m) => m.visible)
|
|
|
- .filter((m) => (props.connectedProvider ? m.provider.id === props.connectedProvider : true)),
|
|
|
- )
|
|
|
- return (
|
|
|
- <SelectDialog
|
|
|
- defaultOpen
|
|
|
- onOpenChange={(open) => {
|
|
|
- if (!open) {
|
|
|
- dialog.clear()
|
|
|
- }
|
|
|
- }}
|
|
|
- title="Select model"
|
|
|
- placeholder="Search models"
|
|
|
- emptyMessage="No model results"
|
|
|
- key={(x) => `${x.provider.id}:${x.id}`}
|
|
|
- items={models}
|
|
|
- current={local.model.current()}
|
|
|
- filterKeys={["provider.name", "name", "id"]}
|
|
|
- sortBy={(a, b) => a.name.localeCompare(b.name)}
|
|
|
- groupBy={(x) => x.provider.name}
|
|
|
- sortGroupsBy={(a, b) => {
|
|
|
- if (a.category === "Recent" && b.category !== "Recent") return -1
|
|
|
- if (b.category === "Recent" && a.category !== "Recent") return 1
|
|
|
- const aProvider = a.items[0].provider.id
|
|
|
- const bProvider = b.items[0].provider.id
|
|
|
- if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
|
|
|
- if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
|
|
|
- return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
|
|
|
- }}
|
|
|
- onSelect={(x) =>
|
|
|
- local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
|
|
|
- recent: true,
|
|
|
- })
|
|
|
- }
|
|
|
- actions={
|
|
|
- <Button
|
|
|
- class="h-7 -my-1 text-14-medium"
|
|
|
- icon="plus-small"
|
|
|
- tabIndex={-1}
|
|
|
- onClick={() => dialog.replace(() => <DialogSelectProvider />)}
|
|
|
- >
|
|
|
- Connect provider
|
|
|
- </Button>
|
|
|
- }
|
|
|
- >
|
|
|
- {(i) => (
|
|
|
- <div class="w-full flex items-center gap-x-2.5">
|
|
|
- <span>{i.name}</span>
|
|
|
- <Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}>
|
|
|
- <Tag>Free</Tag>
|
|
|
- </Show>
|
|
|
- <Show when={i.latest}>
|
|
|
- <Tag>Latest</Tag>
|
|
|
- </Show>
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </SelectDialog>
|
|
|
- )
|
|
|
- })}
|
|
|
- </Match>
|
|
|
- <Match when={true}>
|
|
|
- {iife(() => {
|
|
|
- let listRef: ListRef | undefined
|
|
|
- const handleKey = (e: KeyboardEvent) => {
|
|
|
- if (e.key === "Escape") return
|
|
|
- listRef?.onKeyDown(e)
|
|
|
- }
|
|
|
+ let closeButton!: HTMLButtonElement
|
|
|
+ const models = createMemo(() =>
|
|
|
+ local.model
|
|
|
+ .list()
|
|
|
+ .filter((m) => m.visible)
|
|
|
+ .filter((m) => (props.provider ? m.provider.id === props.provider : true)),
|
|
|
+ )
|
|
|
|
|
|
- onMount(() => {
|
|
|
- document.addEventListener("keydown", handleKey)
|
|
|
- onCleanup(() => {
|
|
|
- document.removeEventListener("keydown", handleKey)
|
|
|
+ return (
|
|
|
+ <Dialog
|
|
|
+ modal
|
|
|
+ defaultOpen
|
|
|
+ onOpenChange={(open) => {
|
|
|
+ if (!open) {
|
|
|
+ dialog.clear()
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <Dialog.Header>
|
|
|
+ <Dialog.Title>Select model</Dialog.Title>
|
|
|
+ <Button
|
|
|
+ class="h-7 -my-1 text-14-medium"
|
|
|
+ icon="plus-small"
|
|
|
+ tabIndex={-1}
|
|
|
+ onClick={() => dialog.replace(() => <DialogSelectProvider />)}
|
|
|
+ >
|
|
|
+ Connect provider
|
|
|
+ </Button>
|
|
|
+ <Dialog.CloseButton ref={closeButton} tabIndex={-1} style={{ display: "none" }} />
|
|
|
+ </Dialog.Header>
|
|
|
+ <Dialog.Body>
|
|
|
+ <List
|
|
|
+ class="px-2.5"
|
|
|
+ search={{ placeholder: "Search models", autofocus: true }}
|
|
|
+ emptyMessage="No model results"
|
|
|
+ key={(x) => `${x.provider.id}:${x.id}`}
|
|
|
+ items={models}
|
|
|
+ current={local.model.current()}
|
|
|
+ filterKeys={["provider.name", "name", "id"]}
|
|
|
+ sortBy={(a, b) => a.name.localeCompare(b.name)}
|
|
|
+ groupBy={(x) => x.provider.name}
|
|
|
+ sortGroupsBy={(a, b) => {
|
|
|
+ if (a.category === "Recent" && b.category !== "Recent") return -1
|
|
|
+ if (b.category === "Recent" && a.category !== "Recent") return 1
|
|
|
+ const aProvider = a.items[0].provider.id
|
|
|
+ const bProvider = b.items[0].provider.id
|
|
|
+ if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
|
|
|
+ if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
|
|
|
+ return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
|
|
|
+ }}
|
|
|
+ onSelect={(x) => {
|
|
|
+ local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
|
|
|
+ recent: true,
|
|
|
})
|
|
|
- })
|
|
|
-
|
|
|
- return (
|
|
|
- <Dialog
|
|
|
- modal
|
|
|
- defaultOpen
|
|
|
- onOpenChange={(open) => {
|
|
|
- if (!open) {
|
|
|
- dialog.clear()
|
|
|
- }
|
|
|
- }}
|
|
|
- >
|
|
|
- <Dialog.Header>
|
|
|
- <Dialog.Title>Select model</Dialog.Title>
|
|
|
- <Dialog.CloseButton tabIndex={-1} />
|
|
|
- </Dialog.Header>
|
|
|
- <Dialog.Body>
|
|
|
- <div class="flex flex-col gap-3 px-2.5">
|
|
|
- <div class="text-14-medium text-text-base px-2.5">Free models provided by OpenCode</div>
|
|
|
- <List
|
|
|
- ref={(ref) => (listRef = ref)}
|
|
|
- items={local.model.list}
|
|
|
- current={local.model.current()}
|
|
|
- key={(x) => `${x.provider.id}:${x.id}`}
|
|
|
- onSelect={(x) => {
|
|
|
- local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
|
|
|
- recent: true,
|
|
|
- })
|
|
|
- dialog.clear()
|
|
|
- }}
|
|
|
- >
|
|
|
- {(i) => (
|
|
|
- <div class="w-full flex items-center gap-x-2.5">
|
|
|
- <span>{i.name}</span>
|
|
|
- <Tag>Free</Tag>
|
|
|
- <Show when={i.latest}>
|
|
|
- <Tag>Latest</Tag>
|
|
|
- </Show>
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </List>
|
|
|
- <div />
|
|
|
- <div />
|
|
|
- </div>
|
|
|
- <div class="px-1.5 pb-1.5">
|
|
|
- <div class="w-full rounded-sm border border-border-weak-base bg-surface-raised-base">
|
|
|
- <div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-4">
|
|
|
- <div class="px-2 text-14-medium text-text-base">Add more models from popular providers</div>
|
|
|
- <div class="w-full">
|
|
|
- <List
|
|
|
- class="w-full"
|
|
|
- key={(x) => x?.id}
|
|
|
- items={providers.popular}
|
|
|
- activeIcon="plus-small"
|
|
|
- sortBy={(a, b) => {
|
|
|
- if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
|
|
|
- return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
|
|
|
- return a.name.localeCompare(b.name)
|
|
|
- }}
|
|
|
- onSelect={(x) => {
|
|
|
- if (!x) return
|
|
|
- dialog.replace(() => <DialogConnect provider={x.id} />)
|
|
|
- }}
|
|
|
- >
|
|
|
- {(i) => (
|
|
|
- <div class="w-full flex items-center gap-x-4">
|
|
|
- <ProviderIcon
|
|
|
- data-slot="list-item-extra-icon"
|
|
|
- id={i.id as IconName}
|
|
|
- // TODO: clean this up after we update icon in models.dev
|
|
|
- classList={{
|
|
|
- "text-icon-weak-base": true,
|
|
|
- "size-4 mx-0.5": i.id === "opencode",
|
|
|
- "size-5": i.id !== "opencode",
|
|
|
- }}
|
|
|
- />
|
|
|
- <span>{i.name}</span>
|
|
|
- <Show when={i.id === "opencode"}>
|
|
|
- <Tag>Recommended</Tag>
|
|
|
- </Show>
|
|
|
- <Show when={i.id === "anthropic"}>
|
|
|
- <div class="text-14-regular text-text-weak">Connect with Claude Pro/Max or API key</div>
|
|
|
- </Show>
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </List>
|
|
|
- <Button
|
|
|
- variant="ghost"
|
|
|
- class="w-full justify-start px-[11px] py-3.5 gap-4.5 text-14-medium"
|
|
|
- icon="dot-grid"
|
|
|
- onClick={() => {
|
|
|
- dialog.replace(() => <DialogSelectProvider />)
|
|
|
- }}
|
|
|
- >
|
|
|
- View all providers
|
|
|
- </Button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </Dialog.Body>
|
|
|
- </Dialog>
|
|
|
- )
|
|
|
- })}
|
|
|
- </Match>
|
|
|
- </Switch>
|
|
|
+ closeButton.click()
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {(i) => (
|
|
|
+ <div class="w-full flex items-center gap-x-2.5">
|
|
|
+ <span>{i.name}</span>
|
|
|
+ <Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}>
|
|
|
+ <Tag>Free</Tag>
|
|
|
+ </Show>
|
|
|
+ <Show when={i.latest}>
|
|
|
+ <Tag>Latest</Tag>
|
|
|
+ </Show>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </List>
|
|
|
+ <Button
|
|
|
+ variant="ghost"
|
|
|
+ class="ml-2.5 mt-5 mb-6 text-text-base self-start"
|
|
|
+ onClick={() => dialog.replace(() => <DialogManageModels />)}
|
|
|
+ >
|
|
|
+ Manage models
|
|
|
+ </Button>
|
|
|
+ </Dialog.Body>
|
|
|
+ </Dialog>
|
|
|
)
|
|
|
}
|