diff options
| author | Dax Raad <[email protected]> | 2025-09-11 18:20:22 -0400 |
|---|---|---|
| committer | Dax Raad <[email protected]> | 2025-09-11 18:20:37 -0400 |
| commit | 79c73267cf83b9d96af50ac60cbda948c26d8a49 (patch) | |
| tree | 1123736f726e1cdc629b797dd7e36d2d13e1c9b0 /cloud/app/src | |
| parent | 54f7fb5019cc46e8eef758959ca474556cc98c1c (diff) | |
| download | opencode-79c73267cf83b9d96af50ac60cbda948c26d8a49.tar.gz opencode-79c73267cf83b9d96af50ac60cbda948c26d8a49.zip | |
wip: zen
Diffstat (limited to 'cloud/app/src')
| -rw-r--r-- | cloud/app/src/routes/workspace/[id].tsx | 476 |
1 files changed, 187 insertions, 289 deletions
diff --git a/cloud/app/src/routes/workspace/[id].tsx b/cloud/app/src/routes/workspace/[id].tsx index b8f355507..222e51a15 100644 --- a/cloud/app/src/routes/workspace/[id].tsx +++ b/cloud/app/src/routes/workspace/[id].tsx @@ -2,9 +2,10 @@ import "./[id].css" import { Billing } from "@opencode/cloud-core/billing.js" import { Key } from "@opencode/cloud-core/key.js" import { json, query, action, useParams, useAction, createAsync, useSubmission } from "@solidjs/router" -import { createMemo, createSignal, For, Show } from "solid-js" +import { createEffect, createMemo, createSignal, For, Show } from "solid-js" import { withActor } from "~/context/auth.withActor" import { IconCopy, IconCheck } from "~/component/icon" +import { createStore } from "solid-js/store" function formatDateForTable(date: Date) { const options: Intl.DateTimeFormatOptions = { @@ -41,16 +42,24 @@ const listKeys = query(async (workspaceID: string) => { return withActor(() => Key.list(), workspaceID) }, "key.list") -const createKey = action(async (workspaceID: string, name: string) => { +const createKey = action(async (form: FormData) => { "use server" + const name = form.get("name")?.toString().trim() + if (!name) return { error: "Name is required" } + const workspaceID = form.get("workspaceID")?.toString() + if (!workspaceID) return { error: "Workspace ID is required" } return json( withActor(() => Key.create({ name }), workspaceID), { revalidate: listKeys.key }, ) }, "key.create") -const removeKey = action(async (workspaceID: string, id: string) => { +const removeKey = action(async (form: FormData) => { "use server" + const id = form.get("id")?.toString() + if (!id) return { error: "ID is required" } + const workspaceID = form.get("workspaceID")?.toString() + if (!workspaceID) return { error: "Workspace ID is required" } return json( withActor(() => Key.remove({ id }), workspaceID), { revalidate: listKeys.key }, @@ -92,127 +101,22 @@ const createCheckoutUrl = action(async (workspaceID: string, successUrl: string, // return withActor(() => Billing.generatePortalUrl({ returnUrl }), workspaceID) // }, "portalUrl") -function KeysSection() { - // Dummy data for testing - const dummyKeys = [ - { - id: "key_1", - name: "Development API Key", - key: "oc_dev_1234567890abcdef1234567890abcdef12345678", - timeCreated: new Date("2024-01-15T10:30:00Z"), - }, - { - id: "key_2", - name: "Production API Key", - key: "oc_prod_abcdef1234567890abcdef1234567890abcdef12", - timeCreated: new Date("2024-02-01T14:22:00Z"), - }, - { - id: "key_3", - name: "Testing Environment", - key: "oc_test_9876543210fedcba9876543210fedcba98765432", - timeCreated: new Date("2024-02-10T09:15:00Z"), - }, - ] - +function KeySection() { const params = useParams() const keys = createAsync(() => listKeys(params.id)) - // const keys = () => dummyKeys - const [showForm, setShowForm] = createSignal(false) - const [name, setName] = createSignal("") - const removeAction = useAction(removeKey) - const createAction = useAction(createKey) - const createSubmission = useSubmission(createKey) - const [copiedId, setCopiedId] = createSignal<string | null>(null) function formatKey(key: string) { if (key.length <= 11) return key return `${key.slice(0, 7)}...${key.slice(-4)}` } - async function handleCreateKey() { - if (!name().trim()) return - - try { - await createAction(params.id, name().trim()) - setName("") - setShowForm(false) - } catch (error) { - console.error("Failed to create API key:", error) - } - } - - async function copyKeyToClipboard(text: string, keyId: string) { - try { - await navigator.clipboard.writeText(text) - setCopiedId(keyId) - setTimeout(() => setCopiedId(null), 1500) - } catch (error) { - console.error("Failed to copy to clipboard:", error) - } - } - - async function handleDeleteKey(keyId: string) { - if (!confirm("Are you sure you want to delete this API key?")) { - return - } - - try { - await removeAction(params.id, keyId) - } catch (error) { - console.error("Failed to delete API key:", error) - } - } - return ( <section data-component="api-keys-section"> <div data-slot="section-title"> <h2>API Keys</h2> <p>Manage your API keys for accessing opencode services.</p> </div> - <Show - when={!showForm()} - fallback={ - <div data-slot="create-form"> - <input - data-component="input" - type="text" - placeholder="Enter key name" - value={name()} - onInput={(e) => setName(e.currentTarget.value)} - onKeyPress={(e) => e.key === "Enter" && handleCreateKey()} - /> - <div data-slot="form-actions"> - <button - data-color="ghost" - onClick={() => { - setShowForm(false) - setName("") - }} - > - Cancel - </button> - <button - data-color="primary" - disabled={createSubmission.pending || !name().trim()} - onClick={handleCreateKey} - > - {createSubmission.pending ? "Creating..." : "Create"} - </button> - </div> - </div> - } - > - <button - data-color="primary" - onClick={() => { - console.log("clicked") - setShowForm(true) - }} - > - Create API Key - </button> - </Show> + <KeyCreateForm /> <div data-slot="api-keys-table"> <Show when={keys()?.length} @@ -233,35 +137,42 @@ function KeysSection() { </thead> <tbody> <For each={keys()!}> - {(key) => ( - <tr> - <td data-slot="key-name">{key.name}</td> - <td data-slot="key-value"> - <button - data-color="ghost" - disabled={copiedId() === key.id} - onClick={() => copyKeyToClipboard(key.key, key.id)} - title="Copy API key" - > - <span>{formatKey(key.key)}</span> - <Show - when={copiedId() === key.id} - fallback={<IconCopy style={{ width: "14px", height: "14px" }} />} + {(key) => { + const [copied, setCopied] = createSignal(false) + // const submission = useSubmission(removeKey, ([fd]) => fd.get("id")?.toString() === key.id) + return ( + <tr> + <td data-slot="key-name">{key.name}</td> + <td data-slot="key-value"> + <button + data-color="ghost" + disabled={copied()} + onClick={async () => { + await navigator.clipboard.writeText(key.key) + setCopied(true) + setTimeout(() => setCopied(false), 1000) + }} + title="Copy API key" > - <IconCheck style={{ width: "14px", height: "14px" }} /> - </Show> - </button> - </td> - <td data-slot="key-date" title={formatDateUTC(key.timeCreated)}> - {formatDateForTable(key.timeCreated)} - </td> - <td data-slot="key-actions"> - <button data-color="ghost" onClick={() => handleDeleteKey(key.id)} title="Delete API key"> - Delete - </button> - </td> - </tr> - )} + <span>{formatKey(key.key)}</span> + <Show when={copied()} fallback={<IconCopy style={{ width: "14px", height: "14px" }} />}> + <IconCheck style={{ width: "14px", height: "14px" }} /> + </Show> + </button> + </td> + <td data-slot="key-date" title={formatDateUTC(key.timeCreated)}> + {formatDateForTable(key.timeCreated)} + </td> + <td data-slot="key-actions"> + <form action={removeKey} method="post"> + <input type="hidden" name="id" value={key.id} /> + <input type="hidden" name="workspaceID" value={params.id} /> + <button data-color="ghost">Delete</button> + </form> + </td> + </tr> + ) + }} </For> </tbody> </table> @@ -271,28 +182,62 @@ function KeysSection() { ) } -function BalanceSection() { +function KeyCreateForm() { const params = useParams() - const dummyBalanceInfo = { balance: 2500000000 } // $25.00 in cents + const submission = useSubmission(createKey) + const [store, setStore] = createStore({ + show: false, + }) - const balanceInfo = createAsync(() => getBalanceInfo(params.id)) - // const balanceInfo = () => dummyBalanceInfo - const createCheckoutUrlAction = useAction(createCheckoutUrl) - const createCheckoutUrlSubmission = useSubmission(createCheckoutUrl) + let input: HTMLInputElement - async function handleBuyCredits() { - try { - const baseUrl = window.location.href - const checkoutUrl = await createCheckoutUrlAction(params.id, baseUrl, baseUrl) - if (checkoutUrl) { - window.location.href = checkoutUrl - } - } catch (error) { - console.error("Failed to get checkout URL:", error) + createEffect(() => { + if (!submission.pending && submission.result) { + hide() } + }) + + function show() { + setStore("show", true) + input.focus() + } + + function hide() { + setStore("show", false) } return ( + <Show + when={store.show} + fallback={ + <button data-color="primary" onClick={() => show()}> + Create API Key + </button> + } + > + <form action={createKey} method="post" data-slot="create-form"> + <input ref={(r) => (input = r)} data-component="input" name="name" type="text" placeholder="Enter key name" /> + <input type="hidden" name="workspaceID" value={params.id} /> + <div data-slot="form-actions"> + <button type="reset" data-color="ghost" onClick={() => hide()}> + Cancel + </button> + <button type="submit" data-color="primary" disabled={submission.pending}> + {submission.pending ? "Creating..." : "Create"} + </button> + </div> + </form> + </Show> + ) +} + +function BalanceSection() { + const params = useParams() + const balanceInfo = createAsync(() => getBalanceInfo(params.id)) + const createCheckoutUrlAction = useAction(createCheckoutUrl) + const createCheckoutUrlSubmission = useSubmission(createCheckoutUrl) + + return ( <section data-component="balance-section"> <div data-slot="section-title"> <h2>Balance</h2> @@ -314,7 +259,17 @@ function BalanceSection() { })()} </span> </div> - <button data-color="primary" disabled={createCheckoutUrlSubmission.pending} onClick={handleBuyCredits}> + <button + data-color="primary" + disabled={createCheckoutUrlSubmission.pending} + onClick={async () => { + const baseUrl = window.location.href + const checkoutUrl = await createCheckoutUrlAction(params.id, baseUrl, baseUrl) + if (checkoutUrl) { + window.location.href = checkoutUrl + } + }} + > {createCheckoutUrlSubmission.pending ? "Loading..." : "Buy Credits"} </button> </div> @@ -324,43 +279,8 @@ function BalanceSection() { function UsageSection() { const params = useParams() - const dummyUsage = [ - { - id: "usage_1", - model: "claude-3-sonnet-20240229", - inputTokens: 1250, - outputTokens: 890, - cost: 125000000, // $1.25 in cents - timeCreated: "2024-02-10T15:30:00Z", - }, - { - id: "usage_2", - model: "gpt-4-turbo-preview", - inputTokens: 2100, - outputTokens: 1456, - cost: 340000000, // $3.40 in cents - timeCreated: "2024-02-09T09:45:00Z", - }, - { - id: "usage_3", - model: "claude-3-haiku-20240307", - inputTokens: 850, - outputTokens: 620, - cost: 45000000, // $0.45 in cents - timeCreated: "2024-02-08T13:22:00Z", - }, - { - id: "usage_4", - model: "gpt-3.5-turbo", - inputTokens: 1800, - outputTokens: 1200, - cost: 85000000, // $0.85 in cents - timeCreated: "2024-02-07T11:15:00Z", - }, - ] - const usage = createAsync(() => getUsageInfo(params.id)) - // const usage = () => dummyUsage + return ( <section data-component="usage-section"> <div data-slot="section-title"> @@ -411,23 +331,9 @@ function UsageSection() { ) } -function PaymentsSection() { +function PaymentSection() { const params = useParams() - const dummyPayments = [ - { - id: "pi_1234567890", - amount: 5000000000, // $50.00 in cents - timeCreated: "2024-02-01T10:00:00Z", - }, - { - id: "pi_0987654321", - amount: 2500000000, // $25.00 in cents - timeCreated: "2024-01-15T14:30:00Z", - }, - ] - const payments = createAsync(() => getPaymentsInfo(params.id)) - // const payments = () => dummyPayments return ( payments() && @@ -471,97 +377,90 @@ function PaymentsSection() { function NewUserSection() { const params = useParams() - const keys = createAsync(() => listKeys(params.id)) const [copiedKey, setCopiedKey] = createSignal(false) - - async function copyKeyToClipboard(text: string) { - try { - await navigator.clipboard.writeText(text) - setCopiedKey(true) - setTimeout(() => setCopiedKey(false), 2000) - } catch (error) { - console.error("Failed to copy to clipboard:", error) - } - } + const keys = createAsync(() => listKeys(params.id)) + const usage = createAsync(() => getUsageInfo(params.id)) + const isNew = createMemo(() => { + const keysList = keys() + const usageList = usage() + return keysList?.length === 1 && (!usageList || usageList.length === 0) + }) + const defaultKey = createMemo(() => keys()?.at(-1)?.key) return ( - <div data-slot="new-user-sections"> - <div data-component="feature-grid"> - <div data-slot="feature"> - <h3>Tested & Verified Models</h3> - <p>We've benchmarked and tested models specifically for coding agents to ensure the best performance.</p> - </div> - <div data-slot="feature"> - <h3>Highest Quality</h3> - <p>Access models configured for optimal performance - no downgrades or routing to cheaper providers.</p> - </div> - <div data-slot="feature"> - <h3>No Lock-in</h3> - <p>Use Zen with any coding agent, and continue using other providers with opencode whenever you want.</p> + <Show when={isNew()}> + <div data-slot="new-user-sections"> + <div data-component="feature-grid"> + <div data-slot="feature"> + <h3>Tested & Verified Models</h3> + <p>We've benchmarked and tested models specifically for coding agents to ensure the best performance.</p> + </div> + <div data-slot="feature"> + <h3>Highest Quality</h3> + <p>Access models configured for optimal performance - no downgrades or routing to cheaper providers.</p> + </div> + <div data-slot="feature"> + <h3>No Lock-in</h3> + <p>Use Zen with any coding agent, and continue using other providers with opencode whenever you want.</p> + </div> </div> - </div> - <div data-component="api-key-highlight"> - <div data-slot="section-title"> - <h2>Your API Key</h2> - </div> + <div data-component="api-key-highlight"> + <div data-slot="section-title"> + <h2>Your API Key</h2> + </div> - <Show when={keys()?.length}> - <div data-slot="key-display"> - <div data-slot="key-container"> - <code data-slot="key-value">{keys()![0].key}</code> - <button - data-color="primary" - disabled={copiedKey()} - onClick={() => copyKeyToClipboard(keys()![0].key)} - title="Copy API key" - > - <Show - when={copiedKey()} - fallback={ - <> - <IconCopy style={{ width: "16px", height: "16px" }} /> Copy Key - </> - } + <Show when={defaultKey()}> + <div data-slot="key-display"> + <div data-slot="key-container"> + <code data-slot="key-value">{defaultKey()}</code> + <button + data-color="primary" + disabled={copiedKey()} + onClick={async () => { + await navigator.clipboard.writeText(defaultKey() ?? "") + setCopiedKey(true) + setTimeout(() => setCopiedKey(false), 2000) + }} + title="Copy API key" > - <IconCheck style={{ width: "16px", height: "16px" }} /> Copied! - </Show> - </button> + <Show + when={copiedKey()} + fallback={ + <> + <IconCopy style={{ width: "16px", height: "16px" }} /> Copy Key + </> + } + > + <IconCheck style={{ width: "16px", height: "16px" }} /> Copied! + </Show> + </button> + </div> </div> - </div> - </Show> - </div> + </Show> + </div> - <div data-component="next-steps"> - <div data-slot="section-title"> - <h2>Next Steps</h2> + <div data-component="next-steps"> + <div data-slot="section-title"> + <h2>Next Steps</h2> + </div> + <ol> + <li>Copy your API key above</li> + <li> + Run <code>opencode auth login</code> and select opencode + </li> + <li>Paste your API key when prompted</li> + <li> + Run <code>/models</code> to see available models + </li> + </ol> </div> - <ol> - <li>Copy your API key above</li> - <li> - Run <code>opencode auth login</code> and select opencode - </li> - <li>Paste your API key when prompted</li> - <li> - Run <code>/models</code> to see available models - </li> - </ol> </div> - </div> + </Show> ) } export default function () { - const params = useParams() - const keys = createAsync(() => listKeys(params.id)) - const usage = createAsync(() => getUsageInfo(params.id)) - - const isNewUser = createMemo(() => { - const keysList = keys() - const usageList = usage() - return keysList?.length === 1 && (!usageList || usageList.length === 0) - }) - return ( <div data-page="workspace-[id]"> <section data-component="title-section"> @@ -575,14 +474,13 @@ export default function () { </p> </section> - <Show when={!isNewUser()} fallback={<NewUserSection />}> - <div data-slot="sections"> - <KeysSection /> - <BalanceSection /> - <UsageSection /> - <PaymentsSection /> - </div> - </Show> + <div data-slot="sections"> + <NewUserSection /> + <KeySection /> + <BalanceSection /> + <UsageSection /> + <PaymentSection /> + </div> </div> ) } |
