From 28c341ad321e106c690244681cc0bcd2c9926851 Mon Sep 17 00:00:00 2001 From: Jay V Date: Fri, 29 Aug 2025 17:30:48 -0400 Subject: [PATCH] ignore: cloud usage history --- cloud/app/src/routes/workspace.tsx | 4 +- cloud/app/src/routes/workspace/[id].tsx | 419 +++++++++------ cloud/app/src/routes/workspace/index.css | 518 ++++++++++++------- cloud/app/src/routes/workspace/workspace.css | 6 - 4 files changed, 580 insertions(+), 367 deletions(-) diff --git a/cloud/app/src/routes/workspace.tsx b/cloud/app/src/routes/workspace.tsx index 0724dd7f..865e7651 100644 --- a/cloud/app/src/routes/workspace.tsx +++ b/cloud/app/src/routes/workspace.tsx @@ -16,7 +16,9 @@ export default function WorkspaceLayout(props: RouteSectionProps) { Logout - {props.children} +
+ {props.children} +
) } diff --git a/cloud/app/src/routes/workspace/[id].tsx b/cloud/app/src/routes/workspace/[id].tsx index bfeb0688..6000228d 100644 --- a/cloud/app/src/routes/workspace/[id].tsx +++ b/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 (
- {/* Actor Section */} -
-
-

Actor

-

Current authenticated user information and session details.

-
-
{JSON.stringify(actor())}
+ {/* Title */} +
+

Gateway

+

+ Coding models optimized for use with opencode. Learn more. +

- {/* API Keys Section */} -
-
-

API Keys

-

Manage your API keys for accessing opencode services.

-
- - setKeyName(e.currentTarget.value)} - onKeyPress={(e) => e.key === "Enter" && handleCreateKey()} - /> -
- - -
-
- } - > - - -
- + {/* Actor Section */} +
+
+

Actor

+

Current authenticated user information and session details.

+
+
{JSON.stringify(actor())}
+
+ + {/* API Keys Section */} +
+
+

API Keys

+

Manage your API keys for accessing opencode services.

+
+ -

Create an API key to access opencode gateway

+
+ setKeyName(e.currentTarget.value)} + onKeyPress={(e) => e.key === "Enter" && handleCreateKey()} + /> +
+ + +
} > - {(key) => ( -
-
-
{key.name}
-
{formatKey(key.key)}
-
- Created: {formatDate(key.timeCreated)} - {key.timeUsed && ` • Last used: ${formatDate(key.timeUsed)}`} + + +
+ +

Create an API key to access opencode gateway

+
+ } + > + {(key) => ( +
+
+
{key.name}
+
{formatKey(key.key)}
+
+ Created: {formatDate(key.timeCreated)} + {key.timeUsed && ` • Last used: ${formatDate(key.timeUsed)}`} +
+
+
+ +
-
- - + )} + +
+
+ + {/* Balance Section */} +
+
+

Balance

+

Manage your billing and add credits to your account.

+
+
+

+ {(() => { + const balanceStr = ((billingInfo()?.billing?.balance ?? 0) / 100000000).toFixed(2) + return `$${balanceStr === "-0.00" ? "0.00" : balanceStr}` + })()} +

+ +
+
+ + {/* Payments Section */} + 0}> +
+
+

Payments History

+

Your recent payment transactions.

+
+
+ + {(payment) => ( +
+ {payment.id} + {" | "} + ${((payment.amount ?? 0) / 100000000).toFixed(2)} + {" | "} + {new Date(payment.timeCreated).toLocaleDateString()} +
+ )} +
+
+
+
+ + {/* Usage Section */} +
+
+

Usage History

+

Your recent API usage and costs.

+
+
+ 0} + fallback={ +
+

Make your first API call to get started.

-
- )} - -
- - - {/* Balance Section */} -
-
-

Balance

-

Manage your billing and add credits to your account.

-
-
-

- {(() => { - const balanceStr = ((billingInfo()?.billing?.balance ?? 0) / 100000000).toFixed(2) - return `$${balanceStr === "-0.00" ? "0.00" : balanceStr}` - })()} -

- -
-
- - {/* Payments Section */} -
-
-

Payments History

-

Your recent payment transactions.

-
-
- -

No payment history yet. Your payments will appear here after your first purchase.

-
- } - > - {(payment) => ( -
- {payment.id} - {" | "} - ${((payment.amount ?? 0) / 100000000).toFixed(2)} - {" | "} - {new Date(payment.timeCreated).toLocaleDateString()} -
- )} - - -
- - {/* Usage Section */} -
-
-

Usage History

-

Your recent API usage and costs.

-
-
- -

No API usage yet. Your usage history will appear here after your first API calls.

-
- } - > - {(usage) => ( -
- {usage.model} - {" | "} - {usage.inputTokens + usage.outputTokens} tokens - {" | "} - ${((usage.cost ?? 0) / 100000000).toFixed(4)} - {" | "} - {new Date(usage.timeCreated).toLocaleDateString()} -
- )} - - -
+ } + > + + + + + + + + + + + + {(usage) => { + const totalTokens = usage.inputTokens + usage.outputTokens + (usage.reasoningTokens || 0) + const date = new Date(usage.timeCreated) + return ( + + + + + + + ) + }} + + +
DateModelTokensCost
+ {formatDateForTable(date)} + {usage.model}{totalTokens.toLocaleString()}${((usage.cost ?? 0) / 100000000).toFixed(4)}
+ + + + ) } diff --git a/cloud/app/src/routes/workspace/index.css b/cloud/app/src/routes/workspace/index.css index 81bdff5b..90ef8cca 100644 --- a/cloud/app/src/routes/workspace/index.css +++ b/cloud/app/src/routes/workspace/index.css @@ -1,217 +1,31 @@ /* 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); -} + gap: var(--space-10); -/* Adjust header spacing */ -[data-component="workspace-header"] + div { - margin-top: var(--space-2); -} - -/* Section titles */ -[data-slot="section-title"] { - display: flex; - flex-direction: column; - gap: var(--space-0-5); -} - -[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; - } -} - -[data-slot="section-title"] p { - 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; -} - -/* Section containers */ -section { - display: flex; - flex-direction: column; - gap: var(--space-6); -} - -/* 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; - - 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"] { + [data-slot="sections"] { display: flex; - gap: var(--space-2); - justify-content: flex-end; - } -} - -[data-slot="key-list"], -[data-slot="payments-list"], -[data-slot="usage-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); + gap: var(--space-16); + + 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="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); -} - -[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); -} - -[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="empty-state"] { - padding: var(--space-8); - text-align: center; - border: 1px dashed var(--color-border); - border-radius: var(--border-radius-sm); - - 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; - - p { - font-size: var(--font-size-2xl); - 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); - - @media (max-width: 30rem) { - flex-direction: column; - align-items: flex-start; - 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); -} - -/* Buttons */ +/* Common elements */ button { padding: var(--space-2) var(--space-4); border: 1px solid var(--color-border); @@ -266,3 +80,305 @@ button { } } } + +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); + text-align: center; + border: 1px dashed var(--color-border); + border-radius: var(--border-radius-sm); + display: flex; + flex-direction: column; + gap: var(--space-2); + + p { + font-size: var(--font-size-sm); + color: var(--color-text-muted); + margin: 0; + } +} + +/* Title section */ +[data-slot="title-section"] { + display: flex; + flex-direction: column; + gap: var(--space-2); + padding-bottom: var(--space-8); + border-bottom: 1px solid var(--color-border); + + h1 { + font-size: var(--font-size-2xl); + font-weight: 500; + line-height: 1.2; + letter-spacing: -0.03125rem; + margin: 0; + text-transform: uppercase; + + @media (max-width: 30rem) { + font-size: var(--font-size-xl); + line-height: 1.25; + } + } + + p { + font-size: var(--font-size-md); + color: var(--color-text-muted); + + a { + color: var(--color-text-muted); + } + } +} + +/* 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-lg); + line-height: 1.25; + } + } + + p { + font-size: var(--font-size-sm); + color: var(--color-text-muted); + } +} + +/* 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; + + 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); + } + + [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); + } + + [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); + } +} + +/* 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); + } + } +} + +/* Payments Section */ +[data-slot="payments-section"] { + [data-slot="payments-list"] { + display: flex; + flex-direction: column; + gap: var(--space-2); + } + + [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); + } + } + + [data-slot="payment-id"], + [data-slot="payment-amount"], + [data-slot="payment-date"] { + color: var(--color-text-muted); + } +} + +/* Usage Section */ +[data-slot="usage-section"] { + [data-slot="usage-table"] { + overflow-x: auto; + } + + [data-slot="usage-table-element"] { + width: 100%; + border-collapse: collapse; + font-size: var(--font-size-sm); + + thead { + border-bottom: 1px solid var(--color-border); + } + + th { + padding: var(--space-3) var(--space-4); + text-align: left; + font-weight: 600; + color: var(--color-text-secondary); + } + + 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); + } + } + + tbody tr { + &:last-child td { + border-bottom: none; + } + } + + @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; + } + } + } + } +} diff --git a/cloud/app/src/routes/workspace/workspace.css b/cloud/app/src/routes/workspace/workspace.css index f4a8b2af..3390fbea 100644 --- a/cloud/app/src/routes/workspace/workspace.css +++ b/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);