From 4ceabdffa07b1af8d99eb73622a4d549d99ec6d2 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 18 Sep 2025 10:59:01 -0400 Subject: wip: zen --- packages/cloud/app/src/component/icon.tsx | 82 --------- .../component/workspace/billing-section.module.css | 114 ------------ .../src/component/workspace/billing-section.tsx | 193 --------------------- .../cloud/app/src/component/workspace/common.tsx | 25 --- .../src/component/workspace/key-section.module.css | 172 ------------------ .../app/src/component/workspace/key-section.tsx | 182 ------------------- .../workspace/monthly-limit-section.module.css | 102 ----------- .../component/workspace/monthly-limit-section.tsx | 139 --------------- .../workspace/new-user-section.module.css | 163 ----------------- .../src/component/workspace/new-user-section.tsx | 97 ----------- .../component/workspace/payment-section.module.css | 72 -------- .../src/component/workspace/payment-section.tsx | 113 ------------ .../component/workspace/usage-section.module.css | 88 ---------- .../app/src/component/workspace/usage-section.tsx | 128 -------------- 14 files changed, 1670 deletions(-) delete mode 100644 packages/cloud/app/src/component/icon.tsx delete mode 100644 packages/cloud/app/src/component/workspace/billing-section.module.css delete mode 100644 packages/cloud/app/src/component/workspace/billing-section.tsx delete mode 100644 packages/cloud/app/src/component/workspace/common.tsx delete mode 100644 packages/cloud/app/src/component/workspace/key-section.module.css delete mode 100644 packages/cloud/app/src/component/workspace/key-section.tsx delete mode 100644 packages/cloud/app/src/component/workspace/monthly-limit-section.module.css delete mode 100644 packages/cloud/app/src/component/workspace/monthly-limit-section.tsx delete mode 100644 packages/cloud/app/src/component/workspace/new-user-section.module.css delete mode 100644 packages/cloud/app/src/component/workspace/new-user-section.tsx delete mode 100644 packages/cloud/app/src/component/workspace/payment-section.module.css delete mode 100644 packages/cloud/app/src/component/workspace/payment-section.tsx delete mode 100644 packages/cloud/app/src/component/workspace/usage-section.module.css delete mode 100644 packages/cloud/app/src/component/workspace/usage-section.tsx (limited to 'packages/cloud/app/src/component') diff --git a/packages/cloud/app/src/component/icon.tsx b/packages/cloud/app/src/component/icon.tsx deleted file mode 100644 index a82572e62..000000000 --- a/packages/cloud/app/src/component/icon.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { JSX } from "solid-js" - -export function IconLogo(props: JSX.SvgSVGAttributes) { - return ( - - - - - - - - - - - - - ) -} - -export function IconCopy(props: JSX.SvgSVGAttributes) { - return ( - - - - - ) -} - -export function IconCheck(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} - -export function IconCreditCard(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} diff --git a/packages/cloud/app/src/component/workspace/billing-section.module.css b/packages/cloud/app/src/component/workspace/billing-section.module.css deleted file mode 100644 index 0bb5709cb..000000000 --- a/packages/cloud/app/src/component/workspace/billing-section.module.css +++ /dev/null @@ -1,114 +0,0 @@ -.root { - [data-slot="section-content"] { - display: flex; - flex-direction: column; - gap: var(--space-3); - } - - [data-slot="reload-error"] { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--space-4); - padding: var(--space-4); - border: 1px solid var(--color-border); - border-radius: var(--border-radius-sm); - - p { - color: var(--color-danger); - font-size: var(--font-size-sm); - line-height: 1.4; - margin: 0; - flex: 1; - } - - [data-slot="create-form"] { - display: flex; - gap: var(--space-2); - margin: 0; - flex-shrink: 0; - } - } - [data-slot="payment"] { - display: flex; - flex-direction: column; - gap: var(--space-3); - padding: var(--space-4); - border: 1px solid var(--color-border); - border-radius: var(--border-radius-sm); - min-width: 14.5rem; - width: fit-content; - - @media (max-width: 30rem) { - width: 100%; - } - - [data-slot="credit-card"] { - padding: var(--space-3-5) var(--space-4); - background-color: var(--color-bg-surface); - border-radius: var(--border-radius-sm); - display: flex; - align-items: center; - justify-content: space-between; - - [data-slot="card-icon"] { - display: flex; - align-items: center; - color: var(--color-text-muted); - } - - [data-slot="card-details"] { - display: flex; - align-items: baseline; - gap: var(--space-1); - - [data-slot="secret"] { - position: relative; - bottom: 2px; - font-size: var(--font-size-lg); - color: var(--color-text-muted); - font-weight: 400; - } - - [data-slot="number"] { - font-size: var(--font-size-3xl); - font-weight: 500; - color: var(--color-text); - } - } - } - - [data-slot="button-row"] { - display: flex; - gap: var(--space-2); - align-items: center; - - @media (max-width: 30rem) { - flex-direction: column; - - > button { - width: 100%; - } - } - - [data-slot="create-form"] { - margin: 0; - } - - /* Make Enable Billing button full width when it's the only button */ - > button { - flex: 1; - } - } - } - [data-slot="usage"] { - p { - font-size: var(--font-size-sm); - line-height: 1.5; - color: var(--color-text-secondary); - b { - font-weight: 600; - } - } - } -} diff --git a/packages/cloud/app/src/component/workspace/billing-section.tsx b/packages/cloud/app/src/component/workspace/billing-section.tsx deleted file mode 100644 index ec314d9ef..000000000 --- a/packages/cloud/app/src/component/workspace/billing-section.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import { json, query, action, useParams, useAction, createAsync, useSubmission } from "@solidjs/router" -import { createMemo, Show } from "solid-js" -import { Billing } from "@opencode/cloud-core/billing.js" -import { withActor } from "~/context/auth.withActor" -import { IconCreditCard } from "~/component/icon" -import styles from "./billing-section.module.css" - -const createCheckoutUrl = action(async (workspaceID: string, successUrl: string, cancelUrl: string) => { - "use server" - return withActor(() => Billing.generateCheckoutUrl({ successUrl, cancelUrl }), workspaceID) -}, "checkoutUrl") - -const reload = action(async (form: FormData) => { - "use server" - const workspaceID = form.get("workspaceID")?.toString() - if (!workspaceID) return { error: "Workspace ID is required" } - return json(await withActor(() => Billing.reload(), workspaceID), { revalidate: getBillingInfo.key }) -}, "billing.reload") - -const disableReload = action(async (form: FormData) => { - "use server" - const workspaceID = form.get("workspaceID")?.toString() - if (!workspaceID) return { error: "Workspace ID is required" } - return json(await withActor(() => Billing.disableReload(), workspaceID), { revalidate: getBillingInfo.key }) -}, "billing.disableReload") - -const createSessionUrl = action(async (workspaceID: string, returnUrl: string) => { - "use server" - return withActor(() => Billing.generateSessionUrl({ returnUrl }), workspaceID) -}, "sessionUrl") - -const getBillingInfo = query(async (workspaceID: string) => { - "use server" - return withActor(async () => { - return await Billing.get() - }, workspaceID) -}, "billing.get") - -export function BillingSection() { - const params = useParams() - // ORIGINAL CODE - COMMENTED OUT FOR TESTING - const balanceInfo = createAsync(() => getBillingInfo(params.id)) - const createCheckoutUrlAction = useAction(createCheckoutUrl) - const createCheckoutUrlSubmission = useSubmission(createCheckoutUrl) - const createSessionUrlAction = useAction(createSessionUrl) - const createSessionUrlSubmission = useSubmission(createSessionUrl) - const disableReloadSubmission = useSubmission(disableReload) - const reloadSubmission = useSubmission(reload) - - // DUMMY DATA FOR TESTING - UNCOMMENT ONE OF THE SCENARIOS BELOW - - // Scenario 1: User has not added billing details and has no balance - // const balanceInfo = () => ({ - // balance: 0, - // paymentMethodLast4: null as string | null, - // reload: false, - // reloadError: null as string | null, - // timeReloadError: null as Date | null, - // }) - - // Scenario 2: User has not added billing details but has a balance - // const balanceInfo = () => ({ - // balance: 1500000000, // $15.00 - // paymentMethodLast4: null as string | null, - // reload: false, - // reloadError: null as string | null, - // timeReloadError: null as Date | null - // }) - - // Scenario 3: User has added billing details (reload enabled) - // const balanceInfo = () => ({ - // balance: 750000000, // $7.50 - // paymentMethodLast4: "4242", - // reload: true, - // reloadError: null as string | null, - // timeReloadError: null as Date | null - // }) - - // Scenario 4: User has billing details but reload failed - // const balanceInfo = () => ({ - // balance: 250000000, // $2.50 - // paymentMethodLast4: "4242", - // reload: true, - // reloadError: "Your card was declined." as string, - // timeReloadError: new Date(Date.now() - 3600000) as Date // 1 hour ago - // }) - - const balanceAmount = createMemo(() => { - return ((balanceInfo()?.balance ?? 0) / 100000000).toFixed(2) - }) - - return ( -
-
-

Billing

-

- Manage payments methods. Contact us if you have any questions. -

-
-
- -
-

- Reload failed at{" "} - {balanceInfo()?.timeReloadError!.toLocaleString("en-US", { - month: "short", - day: "numeric", - hour: "numeric", - minute: "2-digit", - second: "2-digit", - })} - . Reason: {balanceInfo()?.reloadError?.replace(/\.$/, "")}. Please update your payment method and try - again. -

-
- - -
-
-
-
-
-
- -
-
- ----}> - •••• - {balanceInfo()?.paymentMethodLast4} - -
-
-
- { - const baseUrl = window.location.href - const checkoutUrl = await createCheckoutUrlAction(params.id, baseUrl, baseUrl) - if (checkoutUrl) { - window.location.href = checkoutUrl - } - }} - > - {createCheckoutUrlSubmission.pending ? "Loading..." : "Enable Billing"} - - } - > - -
- - -
-
-
-
-
- -

- You have ${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()} remaining in - your account. You can continue using the API with your remaining balance. -

-
- -

- Your current balance is ${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()} - . We'll automatically reload $20 (+$1.23 processing fee) when it reaches $5. -

-
-
-
-
- ) -} diff --git a/packages/cloud/app/src/component/workspace/common.tsx b/packages/cloud/app/src/component/workspace/common.tsx deleted file mode 100644 index f85fd8423..000000000 --- a/packages/cloud/app/src/component/workspace/common.tsx +++ /dev/null @@ -1,25 +0,0 @@ -export 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(",", ",") -} - -export 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) -} diff --git a/packages/cloud/app/src/component/workspace/key-section.module.css b/packages/cloud/app/src/component/workspace/key-section.module.css deleted file mode 100644 index 6a1d0c85f..000000000 --- a/packages/cloud/app/src/component/workspace/key-section.module.css +++ /dev/null @@ -1,172 +0,0 @@ -.root { - [data-component="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 { - line-height: 1.5; - font-size: var(--font-size-sm); - color: var(--color-text-muted); - } - } - - [data-slot="create-form"] { - display: flex; - flex-direction: column; - gap: var(--space-3); - padding: var(--space-4); - border: 1px solid var(--color-border); - border-radius: var(--border-radius-sm); - - [data-slot="input-container"] { - display: flex; - flex-direction: column; - gap: var(--space-1); - } - - @media (max-width: 30rem) { - gap: var(--space-2); - } - - input { - flex: 1; - 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); - } - - [data-slot="form-error"] { - color: var(--color-danger); - font-size: var(--font-size-sm); - margin-top: var(--space-1); - line-height: 1.4; - } - } - - [data-slot="api-keys-table"] { - overflow-x: auto; - } - - [data-slot="api-keys-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: normal; - color: var(--color-text-muted); - text-transform: uppercase; - } - - 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="key-name"] { - color: var(--color-text); - font-family: var(--font-sans); - font-weight: 500; - } - - &[data-slot="key-value"] { - font-family: var(--font-mono); - - button { - display: flex; - align-items: center; - gap: var(--space-2); - padding: var(--space-2) var(--space-3); - font-size: var(--font-size-sm); - font-weight: 400; - border: none; - background-color: transparent; - color: var(--color-text-muted); - font-family: var(--font-mono); - border-radius: var(--border-radius-sm); - cursor: pointer; - transition: all 0.15s ease; - text-transform: none; - - &:hover:not(:disabled) { - background-color: var(--color-bg-surface); - color: var(--color-text); - } - - &:disabled { - cursor: default; - color: var(--color-text); - } - - span { - font-family: inherit; - } - } - } - - &[data-slot="key-date"] { - color: var(--color-text); - } - - &[data-slot="key-actions"] { - font-family: var(--font-sans); - } - } - - 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(3) /* Date */ { - display: none; - } - } - - td { - &:nth-child(3) /* Date */ { - display: none; - } - } - } - } -} diff --git a/packages/cloud/app/src/component/workspace/key-section.tsx b/packages/cloud/app/src/component/workspace/key-section.tsx deleted file mode 100644 index 4158ce793..000000000 --- a/packages/cloud/app/src/component/workspace/key-section.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { json, query, action, useParams, createAsync, useSubmission } from "@solidjs/router" -import { createEffect, createSignal, For, Show } from "solid-js" -import { IconCopy, IconCheck } from "~/component/icon" -import { Key } from "@opencode/cloud-core/key.js" -import { withActor } from "~/context/auth.withActor" -import { createStore } from "solid-js/store" -import { formatDateUTC, formatDateForTable } from "./common" -import styles from "./key-section.module.css" - -const removeKey = action(async (form: FormData) => { - "use server" - const id = form.get("id")?.toString() - if (!id) return { error: "ID is required" } - const workspaceID = form.get("workspaceID")?.toString() - if (!workspaceID) return { error: "Workspace ID is required" } - return json(await withActor(() => Key.remove({ id }), workspaceID), { revalidate: listKeys.key }) -}, "key.remove") - -const createKey = action(async (form: FormData) => { - "use server" - const name = form.get("name")?.toString().trim() - if (!name) return { error: "Name is required" } - const workspaceID = form.get("workspaceID")?.toString() - if (!workspaceID) return { error: "Workspace ID is required" } - return json( - await withActor( - () => - Key.create({ name }) - .then((data) => ({ error: undefined, data })) - .catch((e) => ({ error: e.message as string })), - workspaceID, - ), - { revalidate: listKeys.key }, - ) -}, "key.create") - -const listKeys = query(async (workspaceID: string) => { - "use server" - return withActor(() => Key.list(), workspaceID) -}, "key.list") - -export function KeyCreateForm() { - const params = useParams() - const submission = useSubmission(createKey) - const [store, setStore] = createStore({ show: false }) - - let input: HTMLInputElement - - createEffect(() => { - if (!submission.pending && submission.result && !submission.result.error) { - hide() - } - }) - - function show() { - // submission.clear() does not clear the result in some cases, ie. - // 1. Create key with empty name => error shows - // 2. Put in a key name and creates the key => form hides - // 3. Click add key button again => form shows with the same error if - // submission.clear() is called only once - while (true) { - submission.clear() - if (!submission.result) break - } - setStore("show", true) - input.focus() - } - - function hide() { - setStore("show", false) - } - - return ( - show()}> - Create API Key - - } - > -
-
- (input = r)} data-component="input" name="name" type="text" placeholder="Enter key name" /> - - {(err) =>
{err()}
} -
-
- -
- - -
-
-
- ) -} - -export function KeySection() { - const params = useParams() - const keys = createAsync(() => listKeys(params.id)) - - function formatKey(key: string) { - if (key.length <= 11) return key - return `${key.slice(0, 7)}...${key.slice(-4)}` - } - - return ( -
-
-

API Keys

-

Manage your API keys for accessing opencode services.

-
- -
- -

Create an opencode Gateway API key

-
- } - > - - - - - - - - - - - - {(key) => { - const [copied, setCopied] = createSignal(false) - // const submission = useSubmission(removeKey, ([fd]) => fd.get("id")?.toString() === key.id) - return ( - - - - - - - ) - }} - - -
NameKeyCreated
{key.name} - - - {formatDateForTable(key.timeCreated)} - -
- - - -
-
- - -
- ) -} diff --git a/packages/cloud/app/src/component/workspace/monthly-limit-section.module.css b/packages/cloud/app/src/component/workspace/monthly-limit-section.module.css deleted file mode 100644 index 02de058e4..000000000 --- a/packages/cloud/app/src/component/workspace/monthly-limit-section.module.css +++ /dev/null @@ -1,102 +0,0 @@ -.root { - [data-slot="section-content"] { - display: flex; - flex-direction: column; - gap: var(--space-3); - } - - [data-slot="balance"] { - display: flex; - flex-direction: column; - gap: var(--space-3); - padding: var(--space-4); - border: 1px solid var(--color-border); - border-radius: var(--border-radius-sm); - min-width: 15rem; - width: fit-content; - - @media (max-width: 30rem) { - width: 100%; - } - - [data-slot="amount"] { - padding: var(--space-3-5) var(--space-4); - background-color: var(--color-bg-surface); - border-radius: var(--border-radius-sm); - display: flex; - align-items: baseline; - gap: var(--space-1); - justify-content: flex-end; - - [data-slot="currency"] { - position: relative; - bottom: 2px; - font-size: var(--font-size-lg); - color: var(--color-text-muted); - font-weight: 400; - } - - [data-slot="value"] { - font-size: var(--font-size-3xl); - font-weight: 500; - color: var(--color-text); - } - } - - [data-slot="create-form"] { - display: flex; - flex-direction: column; - gap: var(--space-3); - margin-top: var(--space-1); - - [data-slot="input-container"] { - display: flex; - flex-direction: column; - gap: var(--space-1); - } - - @media (max-width: 30rem) { - gap: var(--space-2); - } - - input { - flex: 1; - 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="form-error"] { - color: var(--color-danger); - font-size: var(--font-size-sm); - line-height: 1.4; - } - } - } - - [data-slot="usage-status"] { - font-size: var(--font-size-sm); - color: var(--color-text-secondary); - margin: 0; - line-height: 1.4; - } -} diff --git a/packages/cloud/app/src/component/workspace/monthly-limit-section.tsx b/packages/cloud/app/src/component/workspace/monthly-limit-section.tsx deleted file mode 100644 index 5c1077ab1..000000000 --- a/packages/cloud/app/src/component/workspace/monthly-limit-section.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { json, query, action, useParams, createAsync, useSubmission } from "@solidjs/router" -import { createEffect, Show } from "solid-js" -import { createStore } from "solid-js/store" -import { withActor } from "~/context/auth.withActor" -import { Billing } from "@opencode/cloud-core/billing.js" -import styles from "./monthly-limit-section.module.css" - -const getBillingInfo = query(async (workspaceID: string) => { - "use server" - return withActor(async () => { - return await Billing.get() - }, workspaceID) -}, "billing.get") - -const setMonthlyLimit = action(async (form: FormData) => { - "use server" - const limit = form.get("limit")?.toString() - if (!limit) return { error: "Limit is required." } - const numericLimit = parseInt(limit) - if (numericLimit < 0) return { error: "Set a valid monthly limit." } - const workspaceID = form.get("workspaceID")?.toString() - if (!workspaceID) return { error: "Workspace ID is required." } - return json( - await withActor( - () => - Billing.setMonthlyLimit(numericLimit) - .then((data) => ({ error: undefined, data })) - .catch((e) => ({ error: e.message as string })), - workspaceID, - ), - { revalidate: getBillingInfo.key }, - ) -}, "billing.setMonthlyLimit") - -export function MonthlyLimitSection() { - const params = useParams() - const submission = useSubmission(setMonthlyLimit) - const [store, setStore] = createStore({ show: false }) - const balanceInfo = createAsync(() => getBillingInfo(params.id)) - - let input: HTMLInputElement - - createEffect(() => { - if (!submission.pending && submission.result && !submission.result.error) { - hide() - } - }) - - function show() { - // submission.clear() does not clear the result in some cases, ie. - // 1. Create key with empty name => error shows - // 2. Put in a key name and creates the key => form hides - // 3. Click add key button again => form shows with the same error if - // submission.clear() is called only once - while (true) { - submission.clear() - if (!submission.result) break - } - setStore("show", true) - input.focus() - } - - function hide() { - setStore("show", false) - } - - return ( -
-
-

Monthly Limit

-

Set a monthly spending limit for your account.

-
-
-
-
- {balanceInfo()?.monthlyLimit ? $ : null} - {balanceInfo()?.monthlyLimit ?? "-"} -
- -
- (input = r)} - data-component="input" - name="limit" - type="number" - placeholder="50" - /> - - {(err) =>
{err()}
} -
-
- -
- - -
- - } - > - -
-
- No spending limit set.

}> -

- Current usage for {new Date().toLocaleDateString("en-US", { month: "long", timeZone: "UTC" })} is $ - {(() => { - const dateLastUsed = balanceInfo()?.timeMonthlyUsageUpdated - if (!dateLastUsed) return "0" - - const current = new Date().toLocaleDateString("en-US", { - year: "numeric", - month: "long", - timeZone: "UTC", - }) - const lastUsed = dateLastUsed.toLocaleDateString("en-US", { - year: "numeric", - month: "long", - timeZone: "UTC", - }) - if (current !== lastUsed) return "0" - return ((balanceInfo()?.monthlyUsage ?? 0) / 100000000).toFixed(2) - })()} - . -

-
-
-
- ) -} diff --git a/packages/cloud/app/src/component/workspace/new-user-section.module.css b/packages/cloud/app/src/component/workspace/new-user-section.module.css deleted file mode 100644 index 2edc7cc14..000000000 --- a/packages/cloud/app/src/component/workspace/new-user-section.module.css +++ /dev/null @@ -1,163 +0,0 @@ -.root { - display: flex; - flex-direction: column; - gap: var(--space-8); - padding: var(--space-6); - background-color: var(--color-bg-surface); - border: 1px dashed var(--color-border); - border-radius: var(--border-radius-sm); - - @media (max-width: 30rem) { - gap: var(--space-8); - padding: var(--space-4); - } - - [data-component="feature-grid"] { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: var(--space-6); - - @media (max-width: 30rem) { - grid-template-columns: 1fr; - gap: var(--space-4); - } - - [data-slot="feature"] { - display: flex; - flex-direction: column; - gap: var(--space-2); - padding: var(--space-4); - border: 1px solid var(--color-border); - border-radius: var(--border-radius-sm); - - h3 { - font-size: var(--font-size-sm); - font-weight: 600; - margin: 0; - color: var(--color-text); - text-transform: uppercase; - letter-spacing: -0.025rem; - } - - p { - font-size: var(--font-size-sm); - line-height: 1.5; - margin: 0; - color: var(--color-text-muted); - } - } - } - - [data-component="api-key-highlight"] { - display: flex; - flex-direction: column; - gap: var(--space-6); - - [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); - } - } - } - - [data-slot="key-display"] { - display: flex; - flex-direction: column; - gap: var(--space-3); - - [data-slot="key-container"] { - display: flex; - gap: var(--space-3); - padding: var(--space-4); - border: 2px solid var(--color-accent); - border-radius: var(--border-radius-sm); - align-items: center; - - @media (max-width: 40rem) { - flex-direction: column; - gap: var(--space-3); - align-items: stretch; - } - - [data-slot="key-value"] { - flex: 1; - font-family: var(--font-mono); - font-size: var(--font-size-sm); - color: var(--color-text); - background-color: var(--color-bg); - padding: var(--space-3); - border-radius: var(--border-radius-sm); - border: 1px solid var(--color-border); - word-break: break-all; - line-height: 1.4; - - @media (max-width: 40rem) { - font-size: var(--font-size-xs); - padding: var(--space-2-5); - } - } - - button { - display: flex; - align-items: center; - gap: var(--space-2); - padding: var(--space-3) var(--space-4); - font-size: var(--font-size-sm); - font-weight: 500; - white-space: nowrap; - min-width: 130px; - - @media (max-width: 40rem) { - justify-content: center; - padding: var(--space-2-5) var(--space-3); - font-size: var(--font-size-xs); - min-width: 96px; - } - } - } - } - } - - [data-component="next-steps"] { - display: flex; - flex-direction: column; - gap: var(--space-6); - - ol { - margin: 0; - padding-left: 0; - display: flex; - flex-direction: column; - gap: var(--space-2); - list-style-position: inside; - - li { - font-size: var(--font-size-md); - line-height: 1.5; - color: var(--color-text-secondary); - - code { - font-family: var(--font-mono); - font-size: var(--font-size-sm); - padding: var(--space-1) var(--space-2); - border: 1px solid var(--color-border); - border-radius: var(--border-radius-sm); - color: var(--color-text); - } - } - } - } -} diff --git a/packages/cloud/app/src/component/workspace/new-user-section.tsx b/packages/cloud/app/src/component/workspace/new-user-section.tsx deleted file mode 100644 index 6e031e371..000000000 --- a/packages/cloud/app/src/component/workspace/new-user-section.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { query, useParams, createAsync } from "@solidjs/router" -import { createMemo, createSignal, Show } from "solid-js" -import { IconCopy, IconCheck } from "~/component/icon" -import { Key } from "@opencode/cloud-core/key.js" -import { Billing } from "@opencode/cloud-core/billing.js" -import { withActor } from "~/context/auth.withActor" -import styles from "./new-user-section.module.css" - -const getUsageInfo = query(async (workspaceID: string) => { - "use server" - return withActor(async () => { - return await Billing.usages() - }, workspaceID) -}, "usage.list") - -const listKeys = query(async (workspaceID: string) => { - "use server" - return withActor(() => Key.list(), workspaceID) -}, "key.list") - -export function NewUserSection() { - const params = useParams() - const [copiedKey, setCopiedKey] = createSignal(false) - const keys = createAsync(() => listKeys(params.id)) - const usage = createAsync(() => getUsageInfo(params.id)) - const isNew = createMemo(() => { - const keysList = keys() - const usageList = usage() - return keysList?.length === 1 && (!usageList || usageList.length === 0) - }) - const defaultKey = createMemo(() => keys()?.at(-1)?.key) - - return ( - -
-
-
-

Tested & Verified Models

-

We've benchmarked and tested models specifically for coding agents to ensure the best performance.

-
-
-

Highest Quality

-

Access models configured for optimal performance - no downgrades or routing to cheaper providers.

-
-
-

No Lock-in

-

Use Zen with any coding agent, and continue using other providers with opencode whenever you want.

-
-
- -
- -
-
- {defaultKey()} - -
-
-
-
- -
-
    -
  1. Enable billing
  2. -
  3. - Run opencode auth login and select opencode -
  4. -
  5. Paste your API key
  6. -
  7. - Start opencode and run /models to select a model -
  8. -
-
-
-
- ) -} diff --git a/packages/cloud/app/src/component/workspace/payment-section.module.css b/packages/cloud/app/src/component/workspace/payment-section.module.css deleted file mode 100644 index ea8e2ed42..000000000 --- a/packages/cloud/app/src/component/workspace/payment-section.module.css +++ /dev/null @@ -1,72 +0,0 @@ -.root { - [data-slot="payments-table"] { - overflow-x: auto; - } - - [data-slot="payments-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: normal; - color: var(--color-text-muted); - text-transform: uppercase; - } - - 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="payment-date"] { - color: var(--color-text); - } - - &[data-slot="payment-id"] { - font-family: var(--font-mono); - font-weight: 400; - color: var(--color-text-muted); - max-width: 200px; - word-break: break-word; - } - - &[data-slot="payment-amount"] { - 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) /* Payment ID */ { - display: none; - } - } - - td { - &:nth-child(2) /* Payment ID */ { - display: none; - } - } - } - } -} diff --git a/packages/cloud/app/src/component/workspace/payment-section.tsx b/packages/cloud/app/src/component/workspace/payment-section.tsx deleted file mode 100644 index 8cdceebc3..000000000 --- a/packages/cloud/app/src/component/workspace/payment-section.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { Billing } from "@opencode/cloud-core/billing.js" -import { query, action, useParams, createAsync, useAction } from "@solidjs/router" -import { For } from "solid-js" -import { withActor } from "~/context/auth.withActor" -import { formatDateUTC, formatDateForTable } from "./common" -import styles from "./payment-section.module.css" - -const getPaymentsInfo = query(async (workspaceID: string) => { - "use server" - return withActor(async () => { - return await Billing.payments() - }, workspaceID) -}, "payment.list") - -const downloadReceipt = action(async (workspaceID: string, paymentID: string) => { - "use server" - return withActor(() => Billing.generateReceiptUrl({ paymentID }), workspaceID) -}, "receipt.download") - -export function PaymentSection() { - const params = useParams() - // ORIGINAL CODE - COMMENTED OUT FOR TESTING - const payments = createAsync(() => getPaymentsInfo(params.id)) - const downloadReceiptAction = useAction(downloadReceipt) - - // DUMMY DATA FOR TESTING - // const payments = () => [ - // { - // id: "pi_3QK1x2FT9vXn4A6r1234567890", - // paymentID: "pi_3QK1x2FT9vXn4A6r1234567890", - // timeCreated: new Date(Date.now() - 86400000 * 1).toISOString(), // 1 day ago - // amount: 2100000000, // $21.00 ($20 + $1 fee) - // }, - // { - // id: "pi_3QJ8k7FT9vXn4A6r0987654321", - // paymentID: "pi_3QJ8k7FT9vXn4A6r0987654321", - // timeCreated: new Date(Date.now() - 86400000 * 15).toISOString(), // 15 days ago - // amount: 2100000000, // $21.00 - // }, - // { - // id: "pi_3QI5m1FT9vXn4A6r5678901234", - // paymentID: "pi_3QI5m1FT9vXn4A6r5678901234", - // timeCreated: new Date(Date.now() - 86400000 * 32).toISOString(), // 32 days ago - // amount: 2100000000, // $21.00 - // }, - // { - // id: "pi_3QH2n9FT9vXn4A6r3456789012", - // paymentID: "pi_3QH2n9FT9vXn4A6r3456789012", - // timeCreated: new Date(Date.now() - 86400000 * 47).toISOString(), // 47 days ago - // amount: 2100000000, // $21.00 - // }, - // { - // id: "pi_3QG7p4FT9vXn4A6r7890123456", - // paymentID: "pi_3QG7p4FT9vXn4A6r7890123456", - // timeCreated: new Date(Date.now() - 86400000 * 63).toISOString(), // 63 days ago - // amount: 2100000000, // $21.00 - // }, - // ] - - return ( - payments() && - payments()!.length > 0 && ( -
-
-

Payments History

-

Recent payment transactions.

-
-
- - - - - - - - - - - - {(payment) => { - const date = new Date(payment.timeCreated) - return ( - - - - - - - ) - }} - - -
DatePayment IDAmountReceipt
- {formatDateForTable(date)} - {payment.id}${((payment.amount ?? 0) / 100000000).toFixed(2)} - -
-
-
- ) - ) -} diff --git a/packages/cloud/app/src/component/workspace/usage-section.module.css b/packages/cloud/app/src/component/workspace/usage-section.module.css deleted file mode 100644 index 1a772ba87..000000000 --- a/packages/cloud/app/src/component/workspace/usage-section.module.css +++ /dev/null @@ -1,88 +0,0 @@ -.root { - [data-component="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 { - line-height: 1.5; - font-size: var(--font-size-sm); - color: var(--color-text-muted); - } - } - - [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: normal; - color: var(--color-text-muted); - text-transform: uppercase; - } - - 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/packages/cloud/app/src/component/workspace/usage-section.tsx b/packages/cloud/app/src/component/workspace/usage-section.tsx deleted file mode 100644 index 5d3d3b6c3..000000000 --- a/packages/cloud/app/src/component/workspace/usage-section.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { Billing } from "@opencode/cloud-core/billing.js" -import { query, useParams, createAsync } from "@solidjs/router" -import { createMemo, For, Show } from "solid-js" -import { formatDateUTC, formatDateForTable } from "./common" -import { withActor } from "~/context/auth.withActor" -import styles from "./usage-section.module.css" - -const getUsageInfo = query(async (workspaceID: string) => { - "use server" - return withActor(async () => { - return await Billing.usages() - }, workspaceID) -}, "usage.list") - -export function UsageSection() { - const params = useParams() - // ORIGINAL CODE - COMMENTED OUT FOR TESTING - const usage = createAsync(() => getUsageInfo(params.id)) - - // DUMMY DATA FOR TESTING - // const usage = () => [ - // { - // timeCreated: new Date(Date.now() - 86400000 * 0).toISOString(), // Today - // model: "claude-3-5-sonnet-20241022", - // inputTokens: 1247, - // outputTokens: 423, - // cost: 125400000, // $1.254 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 0.5).toISOString(), // 12 hours ago - // model: "claude-3-haiku-20240307", - // inputTokens: 892, - // outputTokens: 156, - // cost: 23500000, // $0.235 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 1).toISOString(), // Yesterday - // model: "claude-3-5-sonnet-20241022", - // inputTokens: 2134, - // outputTokens: 687, - // cost: 234700000, // $2.347 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 1.3).toISOString(), // 1.3 days ago - // model: "gpt-4o-mini", - // inputTokens: 567, - // outputTokens: 234, - // cost: 8900000, // $0.089 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 2).toISOString(), // 2 days ago - // model: "claude-3-opus-20240229", - // inputTokens: 1893, - // outputTokens: 945, - // cost: 445600000, // $4.456 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 2.7).toISOString(), // 2.7 days ago - // model: "gpt-4o", - // inputTokens: 1456, - // outputTokens: 532, - // cost: 156800000, // $1.568 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 3).toISOString(), // 3 days ago - // model: "claude-3-haiku-20240307", - // inputTokens: 634, - // outputTokens: 89, - // cost: 12300000, // $0.123 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 4).toISOString(), // 4 days ago - // model: "claude-3-5-sonnet-20241022", - // inputTokens: 3245, - // outputTokens: 1123, - // cost: 387200000, // $3.872 - // }, - // ] - - return ( -
-
-

Usage History

-

Recent API usage and costs.

-
-
- 0} - fallback={ -
-

Make your first API call to get started.

-
- } - > - - - - - - - - - - - - - {(usage) => { - const date = createMemo(() => new Date(usage.timeCreated)) - return ( - - - - - - - - ) - }} - - -
DateModelInputOutputCost
- {formatDateForTable(date())} - {usage.model}{usage.inputTokens}{usage.outputTokens}${((usage.cost ?? 0) / 100000000).toFixed(4)}
-
-
-
- ) -} -- cgit v1.2.3