|
|
@@ -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>
|
|
|
)
|