Parcourir la source

ignore: cloud usage history

Jay V il y a 5 mois
Parent
commit
28c341ad32

+ 3 - 1
cloud/app/src/routes/workspace.tsx

@@ -16,7 +16,9 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
           <a href="/logout">Logout</a>
         </div>
       </header>
-      {props.children}
+      <div data-slot="content">
+        {props.children}
+      </div>
     </main>
   )
 }

+ 258 - 157
cloud/app/src/routes/workspace/[id].tsx

@@ -49,7 +49,60 @@ const createPortalUrl = action(async (returnUrl: string) => {
   return withActor(() => Billing.generatePortalUrl({ returnUrl }))
 }, "portalUrl")
 
-export default function () {
+const dummyUsageData = [
+  {
+    model: "claude-3-5-sonnet-20241022",
+    inputTokens: 1250,
+    outputTokens: 890,
+    reasoningTokens: 150,
+    cacheReadTokens: 0,
+    cacheWriteTokens: 45,
+    cost: 12340000,
+    timeCreated: new Date("2025-01-28T10:30:00Z"),
+  },
+  {
+    model: "claude-3-haiku-20240307",
+    inputTokens: 2100,
+    outputTokens: 450,
+    reasoningTokens: null,
+    cacheReadTokens: 120,
+    cacheWriteTokens: 0,
+    cost: 5670000,
+    timeCreated: new Date("2025-01-27T15:22:00Z"),
+  },
+  {
+    model: "claude-3-5-sonnet-20241022",
+    inputTokens: 850,
+    outputTokens: 1200,
+    reasoningTokens: 220,
+    cacheReadTokens: 30,
+    cacheWriteTokens: 15,
+    cost: 18990000,
+    timeCreated: new Date("2025-01-27T09:15:00Z"),
+  },
+  {
+    model: "claude-3-opus-20240229",
+    inputTokens: 3200,
+    outputTokens: 1800,
+    reasoningTokens: 400,
+    cacheReadTokens: 0,
+    cacheWriteTokens: 100,
+    cost: 45670000,
+    timeCreated: new Date("2025-01-26T14:45:00Z"),
+  },
+  {
+    model: "claude-3-haiku-20240307",
+    inputTokens: 650,
+    outputTokens: 280,
+    reasoningTokens: null,
+    cacheReadTokens: 200,
+    cacheWriteTokens: 0,
+    cost: 2340000,
+    timeCreated: new Date("2025-01-25T16:18:00Z"),
+  },
+]
+
+export default function() {
   const actor = createAsync(() => getActor())
   onMount(() => {
     console.log("MOUNTED", actor())
@@ -69,6 +122,32 @@ export default function () {
     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
     return `${key.slice(0, 7)}...${key.slice(-4)}`
@@ -155,173 +234,195 @@ export default function () {
 
   return (
     <div data-slot="root">
-      {/* Actor Section */}
-      <section data-slot="actor-section">
-        <div data-slot="section-title">
-          <h1>Actor</h1>
-          <p>Current authenticated user information and session details.</p>
-        </div>
-        <div>{JSON.stringify(actor())}</div>
+      {/* Title */}
+      <section data-slot="title-section">
+        <h1>Gateway</h1>
+        <p>
+          Coding models optimized for use with opencode. <a href="/docs">Learn more</a>.
+        </p>
       </section>
 
-      {/* API Keys Section */}
-      <section data-slot="keys-section">
-        <div data-slot="section-title">
-          <h1>API Keys</h1>
-          <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
-                  color="primary"
-                  disabled={createKeySubmission.pending || !keyName().trim()}
-                  onClick={handleCreateKey}
-                >
-                  {createKeySubmission.pending ? "Creating..." : "Create"}
-                </button>
-                <button
-                  color="ghost"
-                  onClick={() => {
-                    setShowCreateForm(false)
-                    setKeyName("")
-                  }}
-                >
-                  Cancel
-                </button>
-              </div>
-            </div>
-          }
-        >
-          <button
-            color="primary"
-            onClick={() => {
-              console.log("clicked")
-              setShowCreateForm(true)
-            }}
-          >
-            Create API Key
-          </button>
-        </Show>
-        <div data-slot="key-list">
-          <For
-            each={keys()}
+      <div data-slot="sections">
+        {/* Actor Section */}
+        <section data-slot="actor-section">
+          <div data-slot="section-title">
+            <h2>Actor</h2>
+            <p>Current authenticated user information and session details.</p>
+          </div>
+          <div>{JSON.stringify(actor())}</div>
+        </section>
+
+        {/* API Keys Section */}
+        <section data-slot="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="empty-state">
-                <p>Create an API key to access opencode gateway</p>
+              <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
+                    color="primary"
+                    disabled={createKeySubmission.pending || !keyName().trim()}
+                    onClick={handleCreateKey}
+                  >
+                    {createKeySubmission.pending ? "Creating..." : "Create"}
+                  </button>
+                  <button
+                    color="ghost"
+                    onClick={() => {
+                      setShowCreateForm(false)
+                      setKeyName("")
+                    }}
+                  >
+                    Cancel
+                  </button>
+                </div>
               </div>
             }
           >
-            {(key) => (
-              <div data-slot="key-item">
-                <div data-slot="key-info">
-                  <div data-slot="key-name">{key.name}</div>
-                  <div data-slot="key-value">{formatKey(key.key)}</div>
-                  <div data-slot="key-meta">
-                    Created: {formatDate(key.timeCreated)}
-                    {key.timeUsed && ` • Last used: ${formatDate(key.timeUsed)}`}
-                  </div>
+            <button
+              color="primary"
+              onClick={() => {
+                console.log("clicked")
+                setShowCreateForm(true)
+              }}
+            >
+              Create API Key
+            </button>
+          </Show>
+          <div data-slot="key-list">
+            <For
+              each={keys()}
+              fallback={
+                <div data-slot="empty-state">
+                  <p>Create an API key to access opencode gateway</p>
                 </div>
-                <div data-slot="key-actions">
-                  <button color="ghost" onClick={() => copyToClipboard(key.key)} title="Copy API key">
-                    Copy
-                  </button>
-                  <button color="ghost" onClick={() => handleDeleteKey(key.id)} title="Delete API key">
-                    Delete
-                  </button>
+              }
+            >
+              {(key) => (
+                <div data-slot="key-item">
+                  <div data-slot="key-info">
+                    <div data-slot="key-name">{key.name}</div>
+                    <div data-slot="key-value">{formatKey(key.key)}</div>
+                    <div data-slot="key-meta">
+                      Created: {formatDate(key.timeCreated)}
+                      {key.timeUsed && ` • Last used: ${formatDate(key.timeUsed)}`}
+                    </div>
+                  </div>
+                  <div data-slot="key-actions">
+                    <button color="ghost" onClick={() => copyToClipboard(key.key)} title="Copy API key">
+                      Copy
+                    </button>
+                    <button color="ghost" onClick={() => handleDeleteKey(key.id)} title="Delete API key">
+                      Delete
+                    </button>
+                  </div>
                 </div>
-              </div>
-            )}
-          </For>
-        </div>
-      </section>
+              )}
+            </For>
+          </div>
+        </section>
 
-      {/* Balance Section */}
-      <section data-slot="balance-section">
-        <div data-slot="section-title">
-          <h1>Balance</h1>
-          <p>Manage your billing and add credits to your account.</p>
-        </div>
-        <div data-slot="balance">
-          <p>
-            {(() => {
-              const balanceStr = ((billingInfo()?.billing?.balance ?? 0) / 100000000).toFixed(2)
-              return `$${balanceStr === "-0.00" ? "0.00" : balanceStr}`
-            })()}
-          </p>
-          <button color="primary" disabled={isLoading()} onClick={handleBuyCredits}>
-            {isLoading() ? "Loading..." : "Buy Credits"}
-          </button>
-        </div>
-      </section>
+        {/* Balance Section */}
+        <section data-slot="balance-section">
+          <div data-slot="section-title">
+            <h2>Balance</h2>
+            <p>Manage your billing and add credits to your account.</p>
+          </div>
+          <div data-slot="balance">
+            <p>
+              {(() => {
+                const balanceStr = ((billingInfo()?.billing?.balance ?? 0) / 100000000).toFixed(2)
+                return `$${balanceStr === "-0.00" ? "0.00" : balanceStr}`
+              })()}
+            </p>
+            <button color="primary" disabled={isLoading()} onClick={handleBuyCredits}>
+              {isLoading() ? "Loading..." : "Buy Credits"}
+            </button>
+          </div>
+        </section>
 
-      {/* Payments Section */}
-      <section data-slot="payments-section">
-        <div data-slot="section-title">
-          <h1>Payments History</h1>
-          <p>Your recent payment transactions.</p>
-        </div>
-        <div data-slot="payments-list">
-          <For
-            each={billingInfo()?.payments}
-            fallback={
-              <div data-slot="empty-state">
-                <p>No payment history yet. Your payments will appear here after your first purchase.</p>
-              </div>
-            }
-          >
-            {(payment) => (
-              <div data-slot="payment-item">
-                <span data-slot="payment-id">{payment.id}</span>
-                {"  |  "}
-                <span data-slot="payment-amount">${((payment.amount ?? 0) / 100000000).toFixed(2)}</span>
-                {"  |  "}
-                <span data-slot="payment-date">{new Date(payment.timeCreated).toLocaleDateString()}</span>
-              </div>
-            )}
-          </For>
-        </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>Your recent payment transactions.</p>
+            </div>
+            <div data-slot="payments-list">
+              <For each={billingInfo()?.payments}>
+                {(payment) => (
+                  <div data-slot="payment-item">
+                    <span data-slot="payment-id">{payment.id}</span>
+                    {"  |  "}
+                    <span data-slot="payment-amount">${((payment.amount ?? 0) / 100000000).toFixed(2)}</span>
+                    {"  |  "}
+                    <span data-slot="payment-date">{new Date(payment.timeCreated).toLocaleDateString()}</span>
+                  </div>
+                )}
+              </For>
+            </div>
+          </section>
+        </Show>
 
-      {/* Usage Section */}
-      <section data-slot="usage-section">
-        <div data-slot="section-title">
-          <h1>Usage History</h1>
-          <p>Your recent API usage and costs.</p>
-        </div>
-        <div data-slot="usage-list">
-          <For
-            each={billingInfo()?.usage}
-            fallback={
-              <div data-slot="empty-state">
-                <p>No API usage yet. Your usage history will appear here after your first API calls.</p>
-              </div>
-            }
-          >
-            {(usage) => (
-              <div data-slot="usage-item">
-                <span data-slot="usage-model">{usage.model}</span>
-                {"  |  "}
-                <span data-slot="usage-tokens">{usage.inputTokens + usage.outputTokens} tokens</span>
-                {"  |  "}
-                <span data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</span>
-                {"  |  "}
-                <span data-slot="usage-date">{new Date(usage.timeCreated).toLocaleDateString()}</span>
-              </div>
-            )}
-          </For>
-        </div>
-      </section>
+        {/* Usage Section */}
+        <section data-slot="usage-section">
+          <div data-slot="section-title">
+            <h2>Usage History</h2>
+            <p>Your recent API usage and costs.</p>
+          </div>
+          <div data-slot="usage-table">
+            <Show
+              when={dummyUsageData.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>Tokens</th>
+                    <th>Cost</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <For each={dummyUsageData}>
+                    {(usage) => {
+                      const totalTokens = usage.inputTokens + usage.outputTokens + (usage.reasoningTokens || 0)
+                      const date = 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">{totalTokens.toLocaleString()}</td>
+                          <td data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</td>
+                        </tr>
+                      )
+                    }}
+                  </For>
+                </tbody>
+              </table>
+            </Show>
+          </div>
+        </section>
+      </div>
     </div>
   )
 }

+ 309 - 193
cloud/app/src/routes/workspace/index.css

@@ -1,268 +1,384 @@
 /* Root container */
 [data-slot="root"] {
   max-width: 64rem;
+  padding: var(--space-10) var(--space-4);
   margin: 0 auto;
   width: 100%;
   display: flex;
   flex-direction: column;
-  gap: var(--space-6);
-}
-
-/* Adjust header spacing */
-[data-component="workspace-header"] + div {
-  margin-top: var(--space-2);
-}
+  gap: var(--space-10);
 
-/* Section titles */
-[data-slot="section-title"] {
-  display: flex;
-  flex-direction: column;
-  gap: var(--space-0-5);
-}
+  [data-slot="sections"] {
+    display: flex;
+    flex-direction: column;
+    gap: var(--space-16);
 
-[data-slot="section-title"] h1 {
-  font-size: var(--font-size-lg);
-  font-weight: 500;
-  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-lg);
-    line-height: 1.25;
+    section {
+      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);
+    }
   }
 }
 
-[data-slot="section-title"] p {
+/* Common elements */
+button {
+  padding: var(--space-2) 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);
-  color: var(--color-text-muted);
-}
-
-/* Section descriptions */
-p {
-  margin: 0;
-  color: var(--color-text-secondary);
-  font-size: var(--font-size-md);
-  line-height: 1.5;
-}
+  font-family: var(--font-sans);
+  cursor: pointer;
+  transition: all 0.15s ease;
 
-/* Section containers */
-section {
-  display: flex;
-  flex-direction: column;
-  gap: var(--space-6);
-}
+  &:hover {
+    background-color: var(--color-surface-hover);
+    border-color: var(--color-accent);
+  }
 
-/* API Keys Section */
-[data-slot="create-form"] {
-  display: flex;
-  flex-direction: column;
-  gap: var(--space-3);
-  padding: var(--space-4);
-  background-color: var(--color-bg-surface);
-  border: 1px solid var(--color-border);
-  border-radius: var(--border-radius-sm);
-  max-width: 32rem;
+  &:active {
+    transform: translateY(1px);
+  }
 
-  input {
-    padding: var(--space-2) var(--space-3);
-    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-mono);
+  &:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
 
-    &:focus {
-      outline: none;
-      border-color: var(--color-accent);
+    &:hover {
+      background-color: var(--color-bg);
+      border-color: var(--color-border);
+      transform: none;
     }
+  }
 
-    &::placeholder {
-      color: var(--color-text-disabled);
+  &[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-slot="form-actions"] {
-    display: flex;
-    gap: var(--space-2);
-    justify-content: flex-end;
+  &[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);
+    }
   }
 }
 
-[data-slot="key-list"],
-[data-slot="payments-list"],
-[data-slot="usage-list"] {
-  display: flex;
-  flex-direction: column;
-  gap: var(--space-2);
+a {
+  color: var(--color-text);
+  text-decoration: underline;
+  text-underline-offset: var(--space-0-75);
+  text-decoration-thickness: 1px;
 }
 
-[data-slot="key-item"] {
-  display: flex;
-  justify-content: space-between;
-  align-items: flex-start;
-  padding: var(--space-4);
-  background-color: var(--color-bg-surface);
-  border: 1px solid var(--color-border);
+[data-slot="empty-state"] {
+  padding: var(--space-20) var(--space-6);
+  text-align: center;
+  border: 1px dashed var(--color-border);
   border-radius: var(--border-radius-sm);
-  gap: var(--space-4);
+  display: flex;
+  flex-direction: column;
+  gap: var(--space-2);
 
-  @media (max-width: 30rem) {
-    flex-direction: column;
-    gap: var(--space-3);
+  p {
+    font-size: var(--font-size-sm);
+    color: var(--color-text-muted);
+    margin: 0;
   }
 }
 
-[data-slot="key-info"] {
+/* Title section */
+[data-slot="title-section"] {
   display: flex;
   flex-direction: column;
-  gap: var(--space-1);
-  flex: 1;
-}
+  gap: var(--space-2);
+  padding-bottom: var(--space-8);
+  border-bottom: 1px solid var(--color-border);
 
-[data-slot="key-name"] {
-  font-size: var(--font-size-md);
-  font-weight: 500;
-  color: var(--color-text);
-}
+  h1 {
+    font-size: var(--font-size-2xl);
+    font-weight: 500;
+    line-height: 1.2;
+    letter-spacing: -0.03125rem;
+    margin: 0;
+    text-transform: uppercase;
 
-[data-slot="key-value"] {
-  font-size: var(--font-size-xs);
-  font-family: var(--font-mono);
-  color: var(--color-text-secondary);
-  background-color: var(--color-bg);
-  padding: var(--space-1) var(--space-2);
-  border-radius: var(--border-radius-sm);
-  border: 1px solid var(--color-border-muted);
-}
+    @media (max-width: 30rem) {
+      font-size: var(--font-size-xl);
+      line-height: 1.25;
+    }
+  }
 
-[data-slot="key-meta"] {
-  font-size: var(--font-size-xs);
-  color: var(--color-text-disabled);
+  p {
+    font-size: var(--font-size-md);
+    color: var(--color-text-muted);
+
+    a {
+      color: var(--color-text-muted);
+    }
+  }
 }
 
-[data-slot="key-actions"] {
+/* Section titles */
+[data-slot="section-title"] {
   display: flex;
-  gap: var(--space-2);
-}
+  flex-direction: column;
+  gap: var(--space-1);
 
-[data-slot="empty-state"] {
-  padding: var(--space-8);
-  text-align: center;
-  border: 1px dashed var(--color-border);
-  border-radius: var(--border-radius-sm);
+  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-lg);
+      line-height: 1.25;
+    }
+  }
 
   p {
-    margin: 0;
     font-size: var(--font-size-sm);
     color: var(--color-text-muted);
   }
 }
 
-/* Balance Section */
-[data-slot="balance"] {
-  display: flex;
-  flex-direction: column;
-  gap: var(--space-3);
-  padding: var(--space-4);
-  background-color: var(--color-bg-surface);
-  border: 1px solid var(--color-border);
-  border-radius: var(--border-radius-sm);
-  max-width: 32rem;
+/* API Keys Section */
+[data-slot="api-keys-section"] {
+  [data-slot="create-form"] {
+    display: flex;
+    flex-direction: column;
+    gap: var(--space-3);
+    padding: var(--space-4);
+    background-color: var(--color-bg-surface);
+    border: 1px solid var(--color-border);
+    border-radius: var(--border-radius-sm);
+    max-width: 32rem;
 
-  p {
-    font-size: var(--font-size-2xl);
+    input {
+      padding: var(--space-2) var(--space-3);
+      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-mono);
+
+      &:focus {
+        outline: none;
+        border-color: var(--color-accent);
+      }
+
+      &::placeholder {
+        color: var(--color-text-disabled);
+      }
+    }
+
+    [data-slot="form-actions"] {
+      display: flex;
+      gap: var(--space-2);
+      justify-content: flex-end;
+    }
+  }
+
+  [data-slot="key-list"] {
+    display: flex;
+    flex-direction: column;
+    gap: var(--space-2);
+  }
+
+  [data-slot="key-item"] {
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-start;
+    padding: var(--space-4);
+    background-color: var(--color-bg-surface);
+    border: 1px solid var(--color-border);
+    border-radius: var(--border-radius-sm);
+    gap: var(--space-4);
+
+    @media (max-width: 30rem) {
+      flex-direction: column;
+      gap: var(--space-3);
+    }
+  }
+
+  [data-slot="key-info"] {
+    display: flex;
+    flex-direction: column;
+    gap: var(--space-1);
+    flex: 1;
+  }
+
+  [data-slot="key-name"] {
+    font-size: var(--font-size-md);
     font-weight: 500;
     color: var(--color-text);
-    margin: 0;
   }
-}
 
-/* Payment and Usage Items */
-[data-slot="payment-item"],
-[data-slot="usage-item"] {
-  display: flex;
-  align-items: center;
-  gap: var(--space-4);
-  padding: var(--space-3);
-  background-color: var(--color-bg-surface);
-  border: 1px solid var(--color-border);
-  border-radius: var(--border-radius-sm);
-  font-size: var(--font-size-sm);
-  font-family: var(--font-mono);
+  [data-slot="key-value"] {
+    font-size: var(--font-size-xs);
+    font-family: var(--font-mono);
+    color: var(--color-text-secondary);
+    background-color: var(--color-bg);
+    padding: var(--space-1) var(--space-2);
+    border-radius: var(--border-radius-sm);
+    border: 1px solid var(--color-border-muted);
+  }
 
-  @media (max-width: 30rem) {
-    flex-direction: column;
-    align-items: flex-start;
+  [data-slot="key-meta"] {
+    font-size: var(--font-size-xs);
+    color: var(--color-text-disabled);
+  }
+
+  [data-slot="key-actions"] {
+    display: flex;
     gap: var(--space-2);
   }
 }
 
-[data-slot="payment-id"],
-[data-slot="payment-amount"],
-[data-slot="payment-date"],
-[data-slot="usage-model"],
-[data-slot="usage-tokens"],
-[data-slot="usage-cost"],
-[data-slot="usage-date"] {
-  color: var(--color-text-muted);
+/* Balance Section */
+[data-slot="balance-section"] {
+  [data-slot="balance"] {
+    display: flex;
+    flex-direction: column;
+    gap: var(--space-3);
+    padding: var(--space-4);
+    background-color: var(--color-bg-surface);
+    border: 1px solid var(--color-border);
+    border-radius: var(--border-radius-sm);
+    max-width: 32rem;
+
+    p {
+      font-size: var(--font-size-2xl);
+      font-weight: 500;
+      color: var(--color-text);
+    }
+  }
 }
 
-/* Buttons */
-button {
-  padding: var(--space-2) 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);
-  cursor: pointer;
-  transition: all 0.15s ease;
+/* Payments Section */
+[data-slot="payments-section"] {
+  [data-slot="payments-list"] {
+    display: flex;
+    flex-direction: column;
+    gap: var(--space-2);
+  }
 
-  &:hover {
-    background-color: var(--color-surface-hover);
-    border-color: var(--color-accent);
+  [data-slot="payment-item"] {
+    display: flex;
+    align-items: center;
+    gap: var(--space-4);
+    padding: var(--space-3);
+    background-color: var(--color-bg-surface);
+    border: 1px solid var(--color-border);
+    border-radius: var(--border-radius-sm);
+    font-size: var(--font-size-sm);
+    font-family: var(--font-mono);
+
+    @media (max-width: 30rem) {
+      flex-direction: column;
+      align-items: flex-start;
+      gap: var(--space-2);
+    }
   }
 
-  &:active {
-    transform: translateY(1px);
+  [data-slot="payment-id"],
+  [data-slot="payment-amount"],
+  [data-slot="payment-date"] {
+    color: var(--color-text-muted);
   }
+}
 
-  &:disabled {
-    opacity: 0.5;
-    cursor: not-allowed;
+/* Usage Section */
+[data-slot="usage-section"] {
+  [data-slot="usage-table"] {
+    overflow-x: auto;
+  }
 
-    &:hover {
-      background-color: var(--color-bg);
-      border-color: var(--color-border);
-      transform: none;
+  [data-slot="usage-table-element"] {
+    width: 100%;
+    border-collapse: collapse;
+    font-size: var(--font-size-sm);
+
+    thead {
+      border-bottom: 1px solid var(--color-border);
     }
-  }
 
-  &[color="primary"] {
-    background-color: var(--color-primary);
-    border-color: var(--color-primary);
-    color: var(--color-primary-text);
+    th {
+      padding: var(--space-3) var(--space-4);
+      text-align: left;
+      font-weight: 600;
+      color: var(--color-text-secondary);
+    }
 
-    &:hover {
-      background-color: var(--color-primary-hover);
-      border-color: var(--color-primary-hover);
+    td {
+      padding: var(--space-3) var(--space-4);
+      border-bottom: 1px solid var(--color-border-muted);
+      color: var(--color-text-muted);
+      font-family: var(--font-mono);
+
+      &[data-slot="usage-date"] {
+        color: var(--color-text);
+      }
+
+      &[data-slot="usage-model"] {
+        font-family: var(--font-sans);
+        font-weight: 400;
+        color: var(--color-text-secondary);
+        max-width: 200px;
+        word-break: break-word;
+      }
+
+      &[data-slot="usage-cost"] {
+        color: var(--color-text);
+      }
     }
-  }
 
-  &[color="ghost"] {
-    background-color: transparent;
-    border-color: transparent;
-    color: var(--color-text-muted);
+    tbody tr {
+      &:last-child td {
+        border-bottom: none;
+      }
+    }
 
-    &:hover {
-      background-color: var(--color-surface-hover);
-      border-color: var(--color-border);
-      color: var(--color-text);
+    @media (max-width: 40rem) {
+      th,
+      td {
+        padding: var(--space-2) var(--space-3);
+        font-size: var(--font-size-xs);
+      }
+
+      th {
+        &:nth-child(2) /* Model */ {
+          display: none;
+        }
+      }
+
+      td {
+        &:nth-child(2) /* Model */ {
+          display: none;
+        }
+      }
     }
   }
 }

+ 0 - 6
cloud/app/src/routes/workspace/workspace.css

@@ -1,9 +1,5 @@
 [data-page="workspace"] {
-  display: flex;
-  flex-direction: column;
-  gap: var(--space-6);
   line-height: 1;
-  padding: var(--space-6);
 
   @media (max-width: 30rem) {
     padding: var(--space-4);
@@ -19,8 +15,6 @@
     justify-content: space-between;
     align-items: center;
     padding: var(--space-4) var(--space-4);
-    margin: calc(-1 * var(--space-6));
-    margin-bottom: var(--space-6);
     border-bottom: 1px solid var(--color-border);
     background-color: var(--color-bg);