Jay V 5 месяцев назад
Родитель
Сommit
133ae42c55

+ 65 - 0
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;

+ 3 - 4
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>
   )
 }

+ 35 - 101
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;
     }

+ 389 - 305
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>
   )