summaryrefslogtreecommitdiffhomepage
path: root/cloud
diff options
context:
space:
mode:
Diffstat (limited to 'cloud')
-rw-r--r--cloud/app/src/routes/workspace.css65
-rw-r--r--cloud/app/src/routes/workspace.tsx7
-rw-r--r--cloud/app/src/routes/workspace/[id].css136
-rw-r--r--cloud/app/src/routes/workspace/[id].tsx694
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>
)