From b168bfe40df1ac9c3185766cdcaed688572c1a8a Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 8 Oct 2025 13:31:12 -0400 Subject: wip: zen --- packages/console/app/src/routes/workspace/[id].tsx | 2 + .../routes/workspace/provider-section.module.css | 107 ++++++++++++++ .../app/src/routes/workspace/provider-section.tsx | 163 +++++++++++++++++++++ 3 files changed, 272 insertions(+) create mode 100644 packages/console/app/src/routes/workspace/provider-section.module.css create mode 100644 packages/console/app/src/routes/workspace/provider-section.tsx (limited to 'packages/console/app/src') diff --git a/packages/console/app/src/routes/workspace/[id].tsx b/packages/console/app/src/routes/workspace/[id].tsx index 952c1417d..a44ddd927 100644 --- a/packages/console/app/src/routes/workspace/[id].tsx +++ b/packages/console/app/src/routes/workspace/[id].tsx @@ -8,6 +8,7 @@ import { KeySection } from "./key-section" import { MemberSection } from "./member-section" import { SettingsSection } from "./settings-section" import { ModelSection } from "./model-section" +import { ProviderSection } from "./provider-section" import { Show } from "solid-js" import { createAsync, query, useParams } from "@solidjs/router" import { Actor } from "@opencode-ai/console-core/actor.js" @@ -52,6 +53,7 @@ export default function () { + diff --git a/packages/console/app/src/routes/workspace/provider-section.module.css b/packages/console/app/src/routes/workspace/provider-section.module.css new file mode 100644 index 000000000..5f18862f5 --- /dev/null +++ b/packages/console/app/src/routes/workspace/provider-section.module.css @@ -0,0 +1,107 @@ +.root { + [data-slot="providers-table"] { + overflow-x: auto; + } + + [data-slot="providers-table-element"] { + width: 100%; + border-collapse: collapse; + font-size: var(--font-size-sm); + + thead { + border-bottom: 1px solid var(--color-border); + } + + th { + padding: var(--space-3) var(--space-4); + text-align: left; + font-weight: normal; + color: var(--color-text-muted); + text-transform: uppercase; + } + + td { + padding: var(--space-3) var(--space-4); + border-bottom: 1px solid var(--color-border-muted); + color: var(--color-text-muted); + font-family: var(--font-mono); + + &[data-slot="provider-name"] { + color: var(--color-text); + font-family: var(--font-mono); + font-weight: 500; + } + + &[data-slot="provider-status"] { + text-align: left; + color: var(--color-text); + } + + &[data-slot="provider-toggle"] { + text-align: left; + font-family: var(--font-sans); + + [data-slot="edit-form"] { + display: flex; + flex-direction: column; + gap: var(--space-3); + + [data-slot="input-wrapper"] { + display: flex; + flex-direction: column; + gap: var(--space-1); + + input { + padding: var(--space-2) var(--space-3); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + background-color: var(--color-bg); + color: var(--color-text); + font-size: var(--font-size-sm); + font-family: var(--font-mono); + + &:focus { + outline: none; + border-color: var(--color-accent); + } + + &::placeholder { + color: var(--color-text-disabled); + } + } + + [data-slot="form-error"] { + color: var(--color-danger); + font-size: var(--font-size-sm); + line-height: 1.4; + } + } + + [data-slot="form-actions"] { + display: flex; + gap: var(--space-2); + } + } + } + } + + tbody tr { + &[data-enabled="false"] { + opacity: 0.6; + } + + &:last-child td { + border-bottom: none; + } + } + + @media (max-width: 40rem) { + + th, + td { + padding: var(--space-2) var(--space-3); + font-size: var(--font-size-xs); + } + } + } +} \ No newline at end of file diff --git a/packages/console/app/src/routes/workspace/provider-section.tsx b/packages/console/app/src/routes/workspace/provider-section.tsx new file mode 100644 index 000000000..856b3a6a2 --- /dev/null +++ b/packages/console/app/src/routes/workspace/provider-section.tsx @@ -0,0 +1,163 @@ +import { json, query, action, useParams, createAsync, useSubmission } from "@solidjs/router" +import { createEffect, For, Show } from "solid-js" +import { Provider } from "@opencode-ai/console-core/provider.js" +import { withActor } from "~/context/auth.withActor" +import { createStore } from "solid-js/store" +import styles from "./provider-section.module.css" + +const PROVIDERS = [ + { name: "OpenAI", key: "openai", prefix: "sk-" }, + { name: "Anthropic", key: "anthropic", prefix: "sk-ant-" }, +] as const + +type Provider = (typeof PROVIDERS)[number] + +const removeProvider = action(async (form: FormData) => { + "use server" + const provider = form.get("provider")?.toString() + if (!provider) return { error: "Provider is required" } + const workspaceID = form.get("workspaceID")?.toString() + if (!workspaceID) return { error: "Workspace ID is required" } + return json(await withActor(() => Provider.remove({ provider }), workspaceID), { revalidate: listProviders.key }) +}, "provider.remove") + +const saveProvider = action(async (form: FormData) => { + "use server" + const provider = form.get("provider")?.toString() + const credentials = form.get("credentials")?.toString() + if (!provider) return { error: "Provider is required" } + if (!credentials) return { error: "API key is required" } + const workspaceID = form.get("workspaceID")?.toString() + if (!workspaceID) return { error: "Workspace ID is required" } + return json( + await withActor( + () => + Provider.create({ provider, credentials }) + .then(() => ({ error: undefined })) + .catch((e) => ({ error: e.message as string })), + workspaceID, + ), + { revalidate: listProviders.key }, + ) +}, "provider.save") + +const listProviders = query(async (workspaceID: string) => { + "use server" + return withActor(() => Provider.list(), workspaceID) +}, "provider.list") + +function ProviderRow(props: { provider: Provider }) { + const params = useParams() + const providers = createAsync(() => listProviders(params.id)) + const saveSubmission = useSubmission(saveProvider, ([fd]) => fd.get("provider")?.toString() === props.provider.key) + const removeSubmission = useSubmission( + removeProvider, + ([fd]) => fd.get("provider")?.toString() === props.provider.key, + ) + const [store, setStore] = createStore({ editing: false }) + + let input: HTMLInputElement + + const isEnabled = () => providers()?.some((p) => p.provider === props.provider.key) + + createEffect(() => { + if (!saveSubmission.pending && saveSubmission.result && !saveSubmission.result.error) { + hide() + } + }) + + function show() { + while (true) { + saveSubmission.clear() + if (!saveSubmission.result) break + } + setStore("editing", true) + setTimeout(() => input?.focus(), 0) + } + + function hide() { + setStore("editing", false) + } + + return ( + + {props.provider.name} + {isEnabled() ? "Configured" : "Not Configured"} + + show()}> + Configure + + } + > +
+ + + +
+
+ } + > +
+
+ (input = r)} + name="credentials" + type="text" + placeholder={`Enter ${props.provider.name} API key (${props.provider.prefix}...)`} + autocomplete="off" + data-form-type="other" + data-lpignore="true" + /> + + {(err) =>
{err()}
} +
+
+ + +
+ + +
+
+ + + + ) +} + +export function ProviderSection() { + return ( +
+
+

Bring Your Own Key

+

Configure your own API keys from AI providers.

+
+
+ + + + + + + + + + {(provider) => } + +
ProviderStatusAction
+
+
+ ) +} -- cgit v1.2.3