ignore: zen

This commit is contained in:
Jay V
2025-09-04 00:16:53 -07:00
parent e001af2709
commit 133ae42c55
4 changed files with 498 additions and 416 deletions

View File

@@ -1,6 +1,71 @@
[data-page="workspace"] { [data-page="workspace"] {
line-height: 1; 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 */ /* Workspace Header */
[data-component="workspace-header"] { [data-component="workspace-header"] {
position: sticky; position: sticky;

View File

@@ -11,8 +11,7 @@ const getUserInfo = query(async (workspaceID: string) => {
"use server" "use server"
return withActor(async () => { return withActor(async () => {
const actor = Actor.assert("user") const actor = Actor.assert("user")
const user = await User.fromID(actor.properties.userID) return await User.fromID(actor.properties.userID)
return { user }
}, workspaceID) }, workspaceID)
}, "userInfo") }, "userInfo")
@@ -44,7 +43,7 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
</A> </A>
</div> </div>
<div data-slot="header-actions"> <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"> <form action={logout} method="post">
<button type="submit" formaction={logout}> <button type="submit" formaction={logout}>
Logout Logout
@@ -52,7 +51,7 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
</form> </form>
</div> </div>
</header> </header>
<div data-slot="content">{props.children}</div> <div>{props.children}</div>
</main> </main>
) )
} }

View File

@@ -1,5 +1,4 @@
/* Root container */ [data-page="workspace-[id]"] {
[data-slot="root"] {
max-width: 64rem; max-width: 64rem;
padding: var(--space-10) var(--space-4); padding: var(--space-10) var(--space-4);
margin: 0 auto; margin: 0 auto;
@@ -28,6 +27,33 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--space-6); gap: var(--space-6);
/* 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);
}
}
} }
section:not(:last-child) { section:not(:last-child) {
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border);
@@ -39,72 +65,7 @@
} }
} }
/* Common elements */ [data-component="empty-state"] {
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;
}
[data-slot="empty-state"] {
padding: var(--space-20) var(--space-6); padding: var(--space-20) var(--space-6);
text-align: center; text-align: center;
border: 1px dashed var(--color-border); border: 1px dashed var(--color-border);
@@ -121,7 +82,7 @@
} }
/* Title section */ /* Title section */
[data-slot="title-section"] { [data-component="title-section"] {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--space-2); 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 */ /* API Keys Section */
[data-slot="api-keys-section"] { [data-component="api-keys-section"] {
[data-slot="create-form"] { [data-slot="create-form"] {
display: flex; display: flex;
gap: var(--space-3); gap: var(--space-3);
@@ -304,7 +238,7 @@
} }
/* Balance Section */ /* Balance Section */
[data-slot="balance-section"] { [data-component="balance-section"] {
[data-slot="balance"] { [data-slot="balance"] {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -324,7 +258,7 @@
gap: var(--space-1); gap: var(--space-1);
justify-content: flex-end; justify-content: flex-end;
&.danger { &[data-state="danger"] {
[data-slot="value"] { [data-slot="value"] {
color: var(--color-danger); color: var(--color-danger);
} }
@@ -348,7 +282,7 @@
} }
/* Payments Section */ /* Payments Section */
[data-slot="payments-section"] { [data-component="payments-section"] {
[data-slot="payments-table"] { [data-slot="payments-table"] {
overflow-x: auto; overflow-x: auto;
} }
@@ -422,7 +356,7 @@
} }
/* Usage Section */ /* Usage Section */
[data-slot="usage-section"] { [data-component="usage-section"] {
[data-slot="usage-table"] { [data-slot="usage-table"] {
overflow-x: auto; overflow-x: auto;
} }

View File

@@ -1,18 +1,49 @@
import "./[id].css" import "./[id].css"
import { Billing } from "@opencode/cloud-core/billing.js" import { Billing } from "@opencode/cloud-core/billing.js"
import { Key } from "@opencode/cloud-core/key.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 { createMemo, createSignal, For, Show } from "solid-js"
import { withActor } from "~/context/auth.withActor" import { withActor } from "~/context/auth.withActor"
import { IconCopy, IconCheck } from "~/component/icon" 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 // Keys related queries and actions
///////////////////////////////////// /////////////////////////////////////
const listKeys = query(async (workspaceID: string) => { const listKeys = query(async (workspaceID: string) => {
"use server" "use server"
return withActor(() => Key.list(), workspaceID) return withActor(() => Key.list(), workspaceID)
@@ -38,131 +69,221 @@ const removeKey = action(async (workspaceID: string, id: string) => {
// Billing related queries and actions // Billing related queries and actions
///////////////////////////////////// /////////////////////////////////////
const getBillingInfo = query(async (workspaceID: string) => { const getBalanceInfo = query(async (workspaceID: string) => {
"use server" "use server"
return withActor(async () => { return withActor(async () => {
const actor = Actor.assert("user") return await Billing.get()
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 }
}, workspaceID) }, 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) => { const createCheckoutUrl = action(async (workspaceID: string, successUrl: string, cancelUrl: string) => {
"use server" "use server"
return withActor(() => Billing.generateCheckoutUrl({ successUrl, cancelUrl }), workspaceID) return withActor(() => Billing.generateCheckoutUrl({ successUrl, cancelUrl }), workspaceID)
}, "checkoutUrl") }, "checkoutUrl")
const createPortalUrl = action(async (workspaceID: string, returnUrl: string) => { // const createPortalUrl = action(async (workspaceID: string, returnUrl: string) => {
"use server" // "use server"
return withActor(() => Billing.generatePortalUrl({ returnUrl }), workspaceID) // return withActor(() => Billing.generatePortalUrl({ returnUrl }), workspaceID)
}, "portalUrl") // }, "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"),
},
]
export default function() {
const params = useParams() const params = useParams()
/////////////////
// Keys section
/////////////////
const keys = createAsync(() => listKeys(params.id)) const keys = createAsync(() => listKeys(params.id))
const createKeyAction = useAction(createKey) // const keys = () => dummyKeys
const removeKeyAction = useAction(removeKey) const [showForm, setShowForm] = createSignal(false)
const createKeySubmission = useSubmission(createKey) const [name, setName] = createSignal("")
const [showCreateForm, setShowCreateForm] = createSignal(false) const removeAction = useAction(removeKey)
const [keyName, setKeyName] = createSignal("") const createAction = useAction(createKey)
const [copiedKeyId, setCopiedKeyId] = createSignal<string | null>(null) const createSubmission = useSubmission(createKey)
const [copiedId, setCopiedId] = createSignal<string | null>(null)
const formatDate = (date: Date) => { function formatKey(key: string) {
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 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) => {
if (key.length <= 11) return key if (key.length <= 11) return key
return `${key.slice(0, 7)}...${key.slice(-4)}` return `${key.slice(0, 7)}...${key.slice(-4)}`
} }
const copyToClipboard = async (text: string) => { async function handleCreateKey() {
try { if (!name().trim()) return
await navigator.clipboard.writeText(text)
} catch (error) {
console.error("Failed to copy to clipboard:", error)
}
}
const copyKeyToClipboard = async (text: string, keyId: string) => {
try {
await navigator.clipboard.writeText(text)
setCopiedKeyId(keyId)
setTimeout(() => setCopiedKeyId(null), 1500)
} catch (error) {
console.error("Failed to copy to clipboard:", error)
}
}
const handleCreateKey = async () => {
if (!keyName().trim()) return
try { try {
await createKeyAction(params.id, keyName().trim()) await createAction(params.id, name().trim())
setKeyName("") setName("")
setShowCreateForm(false) setShowForm(false)
} catch (error) { } catch (error) {
console.error("Failed to create API key:", error) console.error("Failed to create API key:", error)
} }
} }
const handleDeleteKey = async (keyId: string) => { 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?")) { if (!confirm("Are you sure you want to delete this API key?")) {
return return
} }
try { try {
await removeKeyAction(params.id, keyId) await removeAction(params.id, keyId)
} catch (error) { } catch (error) {
console.error("Failed to delete API key:", error) console.error("Failed to delete API key:", error)
} }
} }
///////////////// return (
// Billing section <section data-component="api-keys-section">
///////////////// <div data-slot="section-title">
const billingInfo = createAsync(() => getBillingInfo(params.id)) <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 createCheckoutUrlAction = useAction(createCheckoutUrl)
const createCheckoutUrlSubmission = useSubmission(createCheckoutUrl) const createCheckoutUrlSubmission = useSubmission(createCheckoutUrl)
const handleBuyCredits = async () => { async function handleBuyCredits() {
try { try {
const baseUrl = window.location.href const baseUrl = window.location.href
const checkoutUrl = await createCheckoutUrlAction(params.id, baseUrl, baseUrl) const checkoutUrl = await createCheckoutUrlAction(params.id, baseUrl, baseUrl)
@@ -175,231 +296,194 @@ export default function() {
} }
return ( return (
<div data-slot="root"> <section data-component="balance-section">
{/* Title */} <div data-slot="section-title">
<section data-slot="title-section"> <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>
)
}
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">
<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>
}
>
<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>
)
}
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>
<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>
)
}
export default function() {
return (
<div data-page="workspace-[id]">
<section data-component="title-section">
<h1>Zen</h1> <h1>Zen</h1>
<p> <p>
Curated list of models provided by opencode. <a href="/docs/zen">Learn more</a>. Curated list of models provided by opencode. <a target="_blank" href="/docs/zen">Learn more</a>.
</p> </p>
</section> </section>
<div data-slot="sections"> <div data-slot="sections">
{/* API Keys Section */} <KeysSection />
<section data-slot="api-keys-section"> <BalanceSection />
<div data-slot="section-title"> <UsageSection />
<h2>API Keys</h2> <PaymentsSection />
<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>
{/* 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>
</div>
<button data-color="primary" disabled={createCheckoutUrlSubmission.pending} onClick={handleBuyCredits}>
{createCheckoutUrlSubmission.pending ? "Loading..." : "Buy Credits"}
</button>
</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>
<tr>
<th>Date</th>
<th>Payment ID</th>
<th>Amount</th>
</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>
</div> </div>
</div> </div>
) )