diff options
| author | Jay V <[email protected]> | 2025-09-04 00:16:53 -0700 |
|---|---|---|
| committer | Jay V <[email protected]> | 2025-09-04 00:17:06 -0700 |
| commit | 133ae42c55e88d6fb215516ef9cb3616517c299c (patch) | |
| tree | 90b076ba63c7b7996af407fb9d4b99e0625af0d9 | |
| parent | e001af27098635f8b0d92af341b1b2eb980936ca (diff) | |
| download | opencode-133ae42c55e88d6fb215516ef9cb3616517c299c.tar.gz opencode-133ae42c55e88d6fb215516ef9cb3616517c299c.zip | |
ignore: zen
| -rw-r--r-- | cloud/app/src/routes/workspace.css | 65 | ||||
| -rw-r--r-- | cloud/app/src/routes/workspace.tsx | 7 | ||||
| -rw-r--r-- | cloud/app/src/routes/workspace/[id].css | 136 | ||||
| -rw-r--r-- | cloud/app/src/routes/workspace/[id].tsx | 694 |
4 files changed, 492 insertions, 410 deletions
diff --git a/cloud/app/src/routes/workspace.css b/cloud/app/src/routes/workspace.css index 9faff4d57..2658ad7e5 100644 --- a/cloud/app/src/routes/workspace.css +++ b/cloud/app/src/routes/workspace.css @@ -1,6 +1,71 @@ [data-page="workspace"] { line-height: 1; + /* Common elements */ + button { + padding: var(--space-3) var(--space-4); + 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-sans); + font-weight: 500; + text-transform: uppercase; + cursor: pointer; + transition: all 0.15s ease; + + &:hover { + background-color: var(--color-surface-hover); + border-color: var(--color-accent); + } + + &:active { + transform: translateY(1px); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + + &:hover { + background-color: var(--color-bg); + border-color: var(--color-border); + transform: none; + } + } + + &[data-color="primary"] { + background-color: var(--color-primary); + border-color: var(--color-primary); + color: var(--color-primary-text); + + &:hover { + background-color: var(--color-primary-hover); + border-color: var(--color-primary-hover); + } + } + + &[data-color="ghost"] { + background-color: transparent; + border-color: transparent; + color: var(--color-text-muted); + + &:hover { + background-color: var(--color-surface-hover); + border-color: var(--color-border); + color: var(--color-text); + } + } + } + + a { + color: var(--color-text); + text-decoration: underline; + text-underline-offset: var(--space-0-75); + text-decoration-thickness: 1px; + } + /* Workspace Header */ [data-component="workspace-header"] { position: sticky; diff --git a/cloud/app/src/routes/workspace.tsx b/cloud/app/src/routes/workspace.tsx index 2fa097e22..746a3adcd 100644 --- a/cloud/app/src/routes/workspace.tsx +++ b/cloud/app/src/routes/workspace.tsx @@ -11,8 +11,7 @@ const getUserInfo = query(async (workspaceID: string) => { "use server" return withActor(async () => { const actor = Actor.assert("user") - const user = await User.fromID(actor.properties.userID) - return { user } + return await User.fromID(actor.properties.userID) }, workspaceID) }, "userInfo") @@ -44,7 +43,7 @@ export default function WorkspaceLayout(props: RouteSectionProps) { </A> </div> <div data-slot="header-actions"> - <span data-slot="user">{userInfo()?.user.email}</span> + <span data-slot="user">{userInfo()?.email}</span> <form action={logout} method="post"> <button type="submit" formaction={logout}> Logout @@ -52,7 +51,7 @@ export default function WorkspaceLayout(props: RouteSectionProps) { </form> </div> </header> - <div data-slot="content">{props.children}</div> + <div>{props.children}</div> </main> ) } diff --git a/cloud/app/src/routes/workspace/[id].css b/cloud/app/src/routes/workspace/[id].css index e79d35621..397ceec54 100644 --- a/cloud/app/src/routes/workspace/[id].css +++ b/cloud/app/src/routes/workspace/[id].css @@ -1,5 +1,4 @@ -/* Root container */ -[data-slot="root"] { +[data-page="workspace-[id]"] { max-width: 64rem; padding: var(--space-10) var(--space-4); margin: 0 auto; @@ -28,83 +27,45 @@ display: flex; flex-direction: column; gap: var(--space-6); - } - section:not(:last-child) { - border-bottom: 1px solid var(--color-border); - padding-bottom: var(--space-16); - - @media (max-width: 30rem) { - padding-bottom: var(--space-8); - } - } - } - /* Common elements */ - button { - padding: var(--space-3) var(--space-4); - 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-sans); - font-weight: 500; - text-transform: uppercase; - cursor: pointer; - transition: all 0.15s ease; - - &:hover { - background-color: var(--color-surface-hover); - border-color: var(--color-accent); - } - - &:active { - transform: translateY(1px); - } - - &:disabled { - opacity: 0.5; - cursor: not-allowed; + /* Section titles */ + [data-slot="section-title"] { + display: flex; + flex-direction: column; + gap: var(--space-1); - &:hover { - background-color: var(--color-bg); - border-color: var(--color-border); - transform: none; - } - } + h2 { + font-size: var(--font-size-md); + font-weight: 600; + line-height: 1.2; + letter-spacing: -0.03125rem; + margin: 0; + color: var(--color-text-secondary); + text-transform: uppercase; - &[data-color="primary"] { - background-color: var(--color-primary); - border-color: var(--color-primary); - color: var(--color-primary-text); + @media (max-width: 30rem) { + font-size: var(--font-size-md); + } + } - &:hover { - background-color: var(--color-primary-hover); - border-color: var(--color-primary-hover); + p { + line-height: 1.4; + font-size: var(--font-size-sm); + color: var(--color-text-muted); + } } } + section:not(:last-child) { + border-bottom: 1px solid var(--color-border); + padding-bottom: var(--space-16); - &[data-color="ghost"] { - background-color: transparent; - border-color: transparent; - color: var(--color-text-muted); - - &:hover { - background-color: var(--color-surface-hover); - border-color: var(--color-border); - color: var(--color-text); + @media (max-width: 30rem) { + padding-bottom: var(--space-8); } } } - a { - color: var(--color-text); - text-decoration: underline; - text-underline-offset: var(--space-0-75); - text-decoration-thickness: 1px; - } - - [data-slot="empty-state"] { + [data-component="empty-state"] { padding: var(--space-20) var(--space-6); text-align: center; border: 1px dashed var(--color-border); @@ -121,7 +82,7 @@ } /* Title section */ - [data-slot="title-section"] { + [data-component="title-section"] { display: flex; flex-direction: column; gap: var(--space-2); @@ -156,35 +117,8 @@ } } - /* Section titles */ - [data-slot="section-title"] { - display: flex; - flex-direction: column; - gap: var(--space-1); - - h2 { - font-size: var(--font-size-md); - font-weight: 600; - line-height: 1.2; - letter-spacing: -0.03125rem; - margin: 0; - color: var(--color-text-secondary); - text-transform: uppercase; - - @media (max-width: 30rem) { - font-size: var(--font-size-md); - } - } - - p { - line-height: 1.4; - font-size: var(--font-size-sm); - color: var(--color-text-muted); - } - } - /* API Keys Section */ - [data-slot="api-keys-section"] { + [data-component="api-keys-section"] { [data-slot="create-form"] { display: flex; gap: var(--space-3); @@ -304,7 +238,7 @@ } /* Balance Section */ - [data-slot="balance-section"] { + [data-component="balance-section"] { [data-slot="balance"] { display: flex; flex-direction: column; @@ -324,7 +258,7 @@ gap: var(--space-1); justify-content: flex-end; - &.danger { + &[data-state="danger"] { [data-slot="value"] { color: var(--color-danger); } @@ -348,7 +282,7 @@ } /* Payments Section */ - [data-slot="payments-section"] { + [data-component="payments-section"] { [data-slot="payments-table"] { overflow-x: auto; } @@ -422,7 +356,7 @@ } /* Usage Section */ - [data-slot="usage-section"] { + [data-component="usage-section"] { [data-slot="usage-table"] { overflow-x: auto; } diff --git a/cloud/app/src/routes/workspace/[id].tsx b/cloud/app/src/routes/workspace/[id].tsx index 29e672859..61e9c1db6 100644 --- a/cloud/app/src/routes/workspace/[id].tsx +++ b/cloud/app/src/routes/workspace/[id].tsx @@ -1,18 +1,49 @@ import "./[id].css" import { Billing } from "@opencode/cloud-core/billing.js" import { Key } from "@opencode/cloud-core/key.js" -import { action, createAsync, query, useAction, useSubmission, json, useParams } from "@solidjs/router" +import { + json, + query, + action, + useParams, + useAction, + createAsync, + useSubmission, +} from "@solidjs/router" import { createMemo, createSignal, For, Show } from "solid-js" import { withActor } from "~/context/auth.withActor" import { IconCopy, IconCheck } from "~/component/icon" -import { User } from "@opencode/cloud-core/user.js" -import { Actor } from "@opencode/cloud-core/actor.js" + +function formatDateForTable(date: Date) { + const options: Intl.DateTimeFormatOptions = { + day: "numeric", + month: "short", + hour: "numeric", + minute: "2-digit", + hour12: true, + } + return date.toLocaleDateString("en-GB", options).replace(",", ",") +} + +function formatDateUTC(date: Date) { + const options: Intl.DateTimeFormatOptions = { + weekday: "short", + year: "numeric", + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + second: "2-digit", + timeZoneName: "short", + timeZone: "UTC", + } + return date.toLocaleDateString("en-US", options) +} ///////////////////////////////////// // Keys related queries and actions ///////////////////////////////////// - const listKeys = query(async (workspaceID: string) => { "use server" return withActor(() => Key.list(), workspaceID) @@ -38,131 +69,221 @@ const removeKey = action(async (workspaceID: string, id: string) => { // Billing related queries and actions ///////////////////////////////////// -const getBillingInfo = query(async (workspaceID: string) => { +const getBalanceInfo = query(async (workspaceID: string) => { "use server" return withActor(async () => { - const actor = Actor.assert("user") - const now = Date.now() - const [user, billing, payments, usage] = await Promise.all([ - User.fromID(actor.properties.userID), - Billing.get(), - Billing.payments(), - Billing.usages(), - ]) - console.log("duration", Date.now() - now) - return { user, billing, payments, usage } + return await Billing.get() }, workspaceID) -}, "billingInfo") +}, "balanceInfo") + +const getUsageInfo = query(async (workspaceID: string) => { + "use server" + return withActor(async () => { + return await Billing.usages() + }, workspaceID) +}, "usageInfo") + +const getPaymentsInfo = query(async (workspaceID: string) => { + "use server" + return withActor(async () => { + return await Billing.payments() + }, workspaceID) +}, "paymentsInfo") const createCheckoutUrl = action(async (workspaceID: string, successUrl: string, cancelUrl: string) => { "use server" return withActor(() => Billing.generateCheckoutUrl({ successUrl, cancelUrl }), workspaceID) }, "checkoutUrl") -const createPortalUrl = action(async (workspaceID: string, returnUrl: string) => { - "use server" - return withActor(() => Billing.generatePortalUrl({ returnUrl }), workspaceID) -}, "portalUrl") +// const createPortalUrl = action(async (workspaceID: string, returnUrl: string) => { +// "use server" +// return withActor(() => Billing.generatePortalUrl({ returnUrl }), workspaceID) +// }, "portalUrl") -export default function() { - const params = useParams() +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"), + }, + ] - ///////////////// - // Keys section - ///////////////// + const params = useParams() const keys = createAsync(() => listKeys(params.id)) - const createKeyAction = useAction(createKey) - const removeKeyAction = useAction(removeKey) - const createKeySubmission = useSubmission(createKey) - const [showCreateForm, setShowCreateForm] = createSignal(false) - const [keyName, setKeyName] = createSignal("") - const [copiedKeyId, setCopiedKeyId] = createSignal<string | null>(null) - - const formatDate = (date: Date) => { - return date.toLocaleDateString() - } - - const formatDateForTable = (date: Date) => { - const options: Intl.DateTimeFormatOptions = { - day: "numeric", - month: "short", - hour: "numeric", - minute: "2-digit", - hour12: true, - } - return date.toLocaleDateString("en-GB", options).replace(",", ",") - } + // 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) - const formatDateUTC = (date: Date) => { - const options: Intl.DateTimeFormatOptions = { - weekday: "short", - year: "numeric", - month: "short", - day: "numeric", - hour: "numeric", - minute: "2-digit", - second: "2-digit", - timeZoneName: "short", - timeZone: "UTC", - } - return date.toLocaleDateString("en-US", options) - } - - const formatKey = (key: string) => { + function formatKey(key: string) { if (key.length <= 11) return key return `${key.slice(0, 7)}...${key.slice(-4)}` } - const copyToClipboard = async (text: string) => { + async function handleCreateKey() { + if (!name().trim()) return + try { - await navigator.clipboard.writeText(text) + await createAction(params.id, name().trim()) + setName("") + setShowForm(false) } catch (error) { - console.error("Failed to copy to clipboard:", error) + console.error("Failed to create API key:", error) } } - const copyKeyToClipboard = async (text: string, keyId: string) => { + async function copyKeyToClipboard(text: string, keyId: string) { try { await navigator.clipboard.writeText(text) - setCopiedKeyId(keyId) - setTimeout(() => setCopiedKeyId(null), 1500) + setCopiedId(keyId) + setTimeout(() => setCopiedId(null), 1500) } catch (error) { console.error("Failed to copy to clipboard:", error) } } - const handleCreateKey = async () => { - if (!keyName().trim()) return - - try { - await createKeyAction(params.id, keyName().trim()) - setKeyName("") - setShowCreateForm(false) - } catch (error) { - console.error("Failed to create API key:", error) - } - } - - const handleDeleteKey = async (keyId: string) => { + async function handleDeleteKey(keyId: string) { if (!confirm("Are you sure you want to delete this API key?")) { return } try { - await removeKeyAction(params.id, keyId) + await removeAction(params.id, keyId) } catch (error) { console.error("Failed to delete API key:", error) } } - ///////////////// - // Billing section - ///////////////// - const billingInfo = createAsync(() => getBillingInfo(params.id)) + 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> + <div data-slot="api-keys-table"> + <Show + when={keys()?.length} + fallback={ + <div data-component="empty-state"> + <p>Create an opencode Gateway API key</p> + </div> + } + > + <table data-slot="api-keys-table-element"> + <thead> + <tr> + <th>Name</th> + <th>Key</th> + <th>Created</th> + <th></th> + </tr> + </thead> + <tbody> + <For each={keys()!}> + {(key) => ( + <tr> + <td data-slot="key-name">{key.name}</td> + <td data-slot="key-value"> + <div onClick={() => copyKeyToClipboard(key.key, key.id)} title="Click to copy API key"> + <span>{formatKey(key.key)}</span> + <Show + when={copiedId() === key.id} + fallback={<IconCopy style={{ width: "14px", height: "14px" }} />} + > + <IconCheck style={{ width: "14px", height: "14px" }} /> + </Show> + </div> + </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> + )} + </For> + </tbody> + </table> + </Show> + </div> + </section> + ) +} + +function BalanceSection() { + const params = useParams() + const dummyBalanceInfo = { balance: 2500000000 } // $25.00 in cents + + const balanceInfo = createAsync(() => getBalanceInfo(params.id)) + // const balanceInfo = () => dummyBalanceInfo const createCheckoutUrlAction = useAction(createCheckoutUrl) const createCheckoutUrlSubmission = useSubmission(createCheckoutUrl) - const handleBuyCredits = async () => { + async function handleBuyCredits() { try { const baseUrl = window.location.href const checkoutUrl = await createCheckoutUrlAction(params.id, baseUrl, baseUrl) @@ -175,231 +296,194 @@ export default function() { } return ( - <div data-slot="root"> - {/* Title */} - <section data-slot="title-section"> - <h1>Zen</h1> - <p> - Curated list of models provided by opencode. <a href="/docs/zen">Learn more</a>. - </p> - </section> + <section data-component="balance-section"> + <div data-slot="section-title"> + <h2>Balance</h2> + <p>Add credits to your account.</p> + </div> + <div data-slot="balance"> + <div + data-slot="amount" + data-state={(() => { + const balanceStr = ((balanceInfo()?.balance ?? 0) / 100000000).toFixed(2) + return balanceStr === "0.00" || balanceStr === "-0.00" ? "danger" : undefined + })()} + > + <span data-slot="currency">$</span> + <span data-slot="value"> + {(() => { + const balanceStr = ((balanceInfo()?.balance ?? 0) / 100000000).toFixed(2) + return balanceStr === "-0.00" ? "0.00" : balanceStr + })()} + </span> + </div> + <button data-color="primary" disabled={createCheckoutUrlSubmission.pending} onClick={handleBuyCredits}> + {createCheckoutUrlSubmission.pending ? "Loading..." : "Buy Credits"} + </button> + </div> + </section> + ) +} - <div data-slot="sections"> - {/* API Keys Section */} - <section data-slot="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={!showCreateForm()} - fallback={ - <div data-slot="create-form"> - <input - data-component="input" - type="text" - placeholder="Enter key name" - value={keyName()} - onInput={(e) => setKeyName(e.currentTarget.value)} - onKeyPress={(e) => e.key === "Enter" && handleCreateKey()} - /> - <div data-slot="form-actions"> - <button - data-color="ghost" - onClick={() => { - setShowCreateForm(false) - setKeyName("") - }} - > - Cancel - </button> - <button - data-color="primary" - disabled={createKeySubmission.pending || !keyName().trim()} - onClick={handleCreateKey} - > - {createKeySubmission.pending ? "Creating..." : "Create"} - </button> - </div> - </div> - } - > - <button - data-color="primary" - onClick={() => { - console.log("clicked") - setShowCreateForm(true) - }} - > - Create API Key - </button> - </Show> - <div data-slot="api-keys-table"> - <Show - when={keys()?.length} - fallback={ - <div data-slot="empty-state"> - <p>Create an opencode Gateway API key</p> - </div> - } - > - <table data-slot="api-keys-table-element"> - <thead> - <tr> - <th>Name</th> - <th>Key</th> - <th>Created</th> - <th></th> - </tr> - </thead> - <tbody> - <For each={keys()!}> - {(key) => ( - <tr> - <td data-slot="key-name">{key.name}</td> - <td data-slot="key-value"> - <div onClick={() => copyKeyToClipboard(key.key, key.id)} title="Click to copy API key"> - <span>{formatKey(key.key)}</span> - <Show - when={copiedKeyId() === key.id} - fallback={<IconCopy style={{ width: "14px", height: "14px" }} />} - > - <IconCheck style={{ width: "14px", height: "14px" }} /> - </Show> - </div> - </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> - )} - </For> - </tbody> - </table> - </Show> - </div> - </section> +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", + }, + ] - {/* Balance Section */} - <section data-slot="balance-section"> - <div data-slot="section-title"> - <h2>Balance</h2> - <p>Add credits to your account.</p> - </div> - <div data-slot="balance"> - <div - data-slot="amount" - classList={{ - danger: (() => { - const balanceStr = ((billingInfo()?.billing?.balance ?? 0) / 100000000).toFixed(2) - return balanceStr === "0.00" || balanceStr === "-0.00" - })(), - }} - > - <span data-slot="currency">$</span> - <span data-slot="value"> - {(() => { - const balanceStr = ((billingInfo()?.billing?.balance ?? 0) / 100000000).toFixed(2) - return balanceStr === "-0.00" ? "0.00" : balanceStr - })()} - </span> + const usage = createAsync(() => getUsageInfo(params.id)) + // const usage = () => dummyUsage + return ( + <section data-component="usage-section"> + <div data-slot="section-title"> + <h2>Usage History</h2> + <p>Recent API usage and costs.</p> + </div> + <div data-slot="usage-table"> + <Show + when={usage() && usage()!.length > 0} + fallback={ + <div data-component="empty-state"> + <p>Make your first API call to get started.</p> </div> - <button data-color="primary" disabled={createCheckoutUrlSubmission.pending} onClick={handleBuyCredits}> - {createCheckoutUrlSubmission.pending ? "Loading..." : "Buy Credits"} - </button> - </div> - </section> + } + > + <table data-slot="usage-table-element"> + <thead> + <tr> + <th>Date</th> + <th>Model</th> + <th>Input</th> + <th>Output</th> + <th>Cost</th> + </tr> + </thead> + <tbody> + <For each={usage()!}> + {(usage) => { + const date = createMemo(() => new Date(usage.timeCreated)) + return ( + <tr> + <td data-slot="usage-date" title={formatDateUTC(date())}> + {formatDateForTable(date())} + </td> + <td data-slot="usage-model">{usage.model}</td> + <td data-slot="usage-tokens">{usage.inputTokens}</td> + <td data-slot="usage-tokens">{usage.outputTokens}</td> + <td data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</td> + </tr> + ) + }} + </For> + </tbody> + </table> + </Show> + </div> + </section> + ) +} - {/* Usage Section */} - <section data-slot="usage-section"> - <div data-slot="section-title"> - <h2>Usage History</h2> - <p>Recent API usage and costs.</p> - </div> - <div data-slot="usage-table"> - <Show - when={billingInfo() && billingInfo()!.usage.length > 0} - fallback={ - <div data-slot="empty-state"> - <p>Make your first API call to get started.</p> - </div> - } - > - <table data-slot="usage-table-element"> - <thead> - <tr> - <th>Date</th> - <th>Model</th> - <th>Input</th> - <th>Output</th> - <th>Cost</th> - </tr> - </thead> - <tbody> - <For each={billingInfo()!.usage}> - {(usage) => { - const date = createMemo(() => new Date(usage.timeCreated)) - return ( - <tr> - <td data-slot="usage-date" title={formatDateUTC(date())}> - {formatDateForTable(date())} - </td> - <td data-slot="usage-model">{usage.model}</td> - <td data-slot="usage-tokens">{usage.inputTokens}</td> - <td data-slot="usage-tokens">{usage.outputTokens}</td> - <td data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</td> - </tr> - ) - }} - </For> - </tbody> - </table> - </Show> - </div> - </section> - - {/* Payments Section */} - <Show when={billingInfo() && billingInfo()!.payments.length > 0}> - <section data-slot="payments-section"> - <div data-slot="section-title"> - <h2>Payments History</h2> - <p>Recent payment transactions.</p> - </div> - <div data-slot="payments-table"> - <table data-slot="payments-table-element"> - <thead> +function PaymentsSection() { + 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() && payments()!.length > 0 && ( + <section data-component="payments-section"> + <div data-slot="section-title"> + <h2>Payments History</h2> + <p>Recent payment transactions.</p> + </div> + <div data-slot="payments-table"> + <table data-slot="payments-table-element"> + <thead> + <tr> + <th>Date</th> + <th>Payment ID</th> + <th>Amount</th> + </tr> + </thead> + <tbody> + <For each={payments()!}> + {(payment) => { + const date = new Date(payment.timeCreated) + return ( <tr> - <th>Date</th> - <th>Payment ID</th> - <th>Amount</th> + <td data-slot="payment-date" title={formatDateUTC(date)}> + {formatDateForTable(date)} + </td> + <td data-slot="payment-id">{payment.id}</td> + <td data-slot="payment-amount">${((payment.amount ?? 0) / 100000000).toFixed(2)}</td> </tr> - </thead> - <tbody> - <For each={billingInfo()!.payments}> - {(payment) => { - const date = new Date(payment.timeCreated) - return ( - <tr> - <td data-slot="payment-date" title={formatDateUTC(date)}> - {formatDateForTable(date)} - </td> - <td data-slot="payment-id">{payment.id}</td> - <td data-slot="payment-amount">${((payment.amount ?? 0) / 100000000).toFixed(2)}</td> - </tr> - ) - }} - </For> - </tbody> - </table> - </div> - </section> - </Show> + ) + }} + </For> + </tbody> + </table> + </div> + </section> + ) +} + +export default function() { + return ( + <div data-page="workspace-[id]"> + <section data-component="title-section"> + <h1>Zen</h1> + <p> + Curated list of models provided by opencode. <a target="_blank" href="/docs/zen">Learn more</a>. + </p> + </section> + <div data-slot="sections"> + <KeysSection /> + <BalanceSection /> + <UsageSection /> + <PaymentsSection /> </div> </div> ) |
