Files
zmo-cli/packages/console/app/src/routes/workspace/[id]/model-section.tsx
T

181 lines
7.1 KiB
TypeScript

import { Model } from "@opencode-ai/console-core/model.js"
import { query, action, useParams, createAsync, json } from "@solidjs/router"
import { createMemo, For, Show } from "solid-js"
import { withActor } from "~/context/auth.withActor"
import { ZenData } from "@opencode-ai/console-core/model.js"
import styles from "./model-section.module.css"
import { querySessionInfo } from "../common"
import {
IconAlibaba,
IconAnthropic,
IconGemini,
IconMiniMax,
IconMoonshotAI,
IconOpenAI,
IconStealth,
IconXai,
IconZai,
} from "~/component/icon"
import { useI18n } from "~/context/i18n"
import { useLanguage } from "~/context/language"
import { formError } from "~/lib/form-error"
const getModelLab = (modelId: string) => {
if (modelId.startsWith("claude")) return "Anthropic"
if (modelId.startsWith("gpt")) return "OpenAI"
if (modelId.startsWith("gemini")) return "Google"
if (modelId.startsWith("kimi")) return "Moonshot AI"
if (modelId.startsWith("glm")) return "Z.ai"
if (modelId.startsWith("qwen")) return "Alibaba"
if (modelId.startsWith("minimax")) return "MiniMax"
if (modelId.startsWith("grok")) return "xAI"
return "Stealth"
}
const getModelsInfo = query(async (workspaceID: string) => {
"use server"
return withActor(async () => {
return {
all: Object.entries(ZenData.list().models)
.filter(([id, _model]) => !["claude-3-5-haiku"].includes(id))
.filter(([id, _model]) => !id.startsWith("alpha-"))
.sort(([idA, modelA], [idB, modelB]) => {
const priority = ["big-pickle", "minimax", "grok", "claude", "gpt", "gemini"]
const getPriority = (id: string) => {
const index = priority.findIndex((p) => id.startsWith(p))
return index === -1 ? Infinity : index
}
const pA = getPriority(idA)
const pB = getPriority(idB)
if (pA !== pB) return pA - pB
const modelAName = Array.isArray(modelA) ? modelA[0].name : modelA.name
const modelBName = Array.isArray(modelB) ? modelB[0].name : modelB.name
return modelAName.localeCompare(modelBName)
})
.map(([id, model]) => ({ id, name: Array.isArray(model) ? model[0].name : model.name })),
disabled: await Model.listDisabled(),
}
}, workspaceID)
}, "model.info")
const updateModel = action(async (form: FormData) => {
"use server"
const model = form.get("model")?.toString()
if (!model) return { error: formError.modelRequired }
const workspaceID = form.get("workspaceID")?.toString()
if (!workspaceID) return { error: formError.workspaceRequired }
const enabled = form.get("enabled")?.toString() === "true"
return json(
withActor(async () => {
if (enabled) {
await Model.disable({ model })
} else {
await Model.enable({ model })
}
}, workspaceID),
{ revalidate: getModelsInfo.key },
)
}, "model.toggle")
export function ModelSection() {
const params = useParams()
const i18n = useI18n()
const language = useLanguage()
const modelsInfo = createAsync(() => getModelsInfo(params.id!))
const userInfo = createAsync(() => querySessionInfo(params.id!))
const modelsWithLab = createMemo(() => {
const info = modelsInfo()
if (!info) return []
return info.all.map((model) => ({
...model,
lab: getModelLab(model.id),
}))
})
return (
<section class={styles.root}>
<div data-slot="section-title">
<h2>{i18n.t("workspace.models.title")}</h2>
<p>
{i18n.t("workspace.models.subtitle.beforeLink")}{" "}
<a href={language.route("/docs/zen#pricing")}>{i18n.t("common.learnMore")}</a>.
</p>
</div>
<div data-slot="models-list">
<Show when={modelsInfo()}>
<div data-slot="models-table">
<table data-slot="models-table-element">
<thead>
<tr>
<th>{i18n.t("workspace.models.table.model")}</th>
<th></th>
<th>{i18n.t("workspace.models.table.enabled")}</th>
</tr>
</thead>
<tbody>
<For each={modelsWithLab()}>
{({ id, name, lab }) => {
const isEnabled = createMemo(() => !modelsInfo()!.disabled.includes(id))
return (
<tr data-slot="model-row" data-disabled={!isEnabled()}>
<td data-slot="model-name">
<div>
{(() => {
switch (lab) {
case "OpenAI":
return <IconOpenAI width={16} height={16} />
case "Anthropic":
return <IconAnthropic width={16} height={16} />
case "Google":
return <IconGemini width={16} height={16} />
case "Moonshot AI":
return <IconMoonshotAI width={16} height={16} />
case "Z.ai":
return <IconZai width={16} height={16} />
case "Alibaba":
return <IconAlibaba width={16} height={16} />
case "xAI":
return <IconXai width={16} height={16} />
case "MiniMax":
return <IconMiniMax width={16} height={16} />
default:
return <IconStealth width={16} height={16} />
}
})()}
<span>{name}</span>
</div>
</td>
<td data-slot="model-lab">{lab}</td>
<td data-slot="model-toggle">
<form action={updateModel} method="post">
<input type="hidden" name="model" value={id} />
<input type="hidden" name="workspaceID" value={params.id} />
<input type="hidden" name="enabled" value={isEnabled().toString()} />
<label data-slot="model-toggle-label">
<input
type="checkbox"
checked={isEnabled()}
disabled={!userInfo()?.isAdmin}
onChange={(e) => {
const form = e.currentTarget.closest("form")
if (form) form.requestSubmit()
}}
/>
<span></span>
</label>
</form>
</td>
</tr>
)
}}
</For>
</tbody>
</table>
</div>
</Show>
</div>
</section>
)
}