From f89696509e1d97599cc041e0d1034c2933374837 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 11 Mar 2026 00:04:54 -0400 Subject: zen: update header --- packages/console/app/src/component/header.tsx | 16 ++++++---------- packages/console/app/src/routes/zen/index.tsx | 3 +-- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/console/app/src/component/header.tsx b/packages/console/app/src/component/header.tsx index 24d5a897c..1e129d590 100644 --- a/packages/console/app/src/component/header.tsx +++ b/packages/console/app/src/component/header.tsx @@ -161,16 +161,12 @@ export function Header(props: { zen?: boolean; go?: boolean; hideGetStarted?: bo
  • {i18n.t("nav.docs")}
  • - -
  • - {i18n.t("nav.zen")} -
  • -
    - -
  • - {i18n.t("nav.go")} -
  • -
    +
  • + {i18n.t("nav.zen")} +
  • +
  • + {i18n.t("nav.go")} +
  • {i18n.t("nav.enterprise")}
  • diff --git a/packages/console/app/src/routes/zen/index.tsx b/packages/console/app/src/routes/zen/index.tsx index 5b5581b53..62e8f5d37 100644 --- a/packages/console/app/src/routes/zen/index.tsx +++ b/packages/console/app/src/routes/zen/index.tsx @@ -24,8 +24,7 @@ import { LocaleLinks } from "~/component/locale-links" const checkLoggedIn = query(async () => { "use server" - const workspaceID = await getLastSeenWorkspaceID().catch(() => {}) - if (workspaceID) throw redirect(`/workspace/${workspaceID}`) + return await getLastSeenWorkspaceID().catch(() => {}) }, "checkLoggedIn.get") export default function Home() { -- cgit v1.2.3 From fac23a1afc6cfbb7767eef05bf6ea018837697ad Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 11 Mar 2026 00:05:08 -0400 Subject: zen: update usage graph on landing page --- packages/console/app/src/routes/go/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/console/app/src/routes/go/index.tsx b/packages/console/app/src/routes/go/index.tsx index 842256ca3..dcd909ab4 100644 --- a/packages/console/app/src/routes/go/index.tsx +++ b/packages/console/app/src/routes/go/index.tsx @@ -62,7 +62,7 @@ function LimitsGraph(props: { href: string }) { const rmax = Math.max(1, ...models.map((m) => ratio(m.req))) const log = (n: number) => Math.log10(Math.max(n, 1)) const base = 24 - const p = 2.2 + const p = 1.8 const x = (r: number) => left + base + Math.pow(log(r) / log(rmax), p) * (plot - base) const start = (x(1) / w) * 100 -- cgit v1.2.3 From ed3bb3ea8f18a3d2818eb17d77e2caad40eb1dee Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 11 Mar 2026 00:39:56 -0400 Subject: zen: add usage section --- packages/console/app/src/i18n/ar.ts | 1 + packages/console/app/src/i18n/br.ts | 1 + packages/console/app/src/i18n/da.ts | 1 + packages/console/app/src/i18n/de.ts | 1 + packages/console/app/src/i18n/en.ts | 1 + packages/console/app/src/i18n/es.ts | 1 + packages/console/app/src/i18n/fr.ts | 1 + packages/console/app/src/i18n/it.ts | 1 + packages/console/app/src/i18n/ja.ts | 1 + packages/console/app/src/i18n/ko.ts | 1 + packages/console/app/src/i18n/no.ts | 1 + packages/console/app/src/i18n/pl.ts | 1 + packages/console/app/src/i18n/ru.ts | 1 + packages/console/app/src/i18n/th.ts | 1 + packages/console/app/src/i18n/tr.ts | 1 + packages/console/app/src/i18n/zh.ts | 1 + packages/console/app/src/i18n/zht.ts | 1 + packages/console/app/src/routes/workspace/[id].tsx | 6 + .../routes/workspace/[id]/graph-section.module.css | 145 ------ .../src/routes/workspace/[id]/graph-section.tsx | 515 --------------------- .../app/src/routes/workspace/[id]/index.tsx | 6 - .../routes/workspace/[id]/usage-section.module.css | 185 -------- .../src/routes/workspace/[id]/usage-section.tsx | 217 --------- .../workspace/[id]/usage/graph-section.module.css | 145 ++++++ .../routes/workspace/[id]/usage/graph-section.tsx | 515 +++++++++++++++++++++ .../app/src/routes/workspace/[id]/usage/index.tsx | 21 + .../workspace/[id]/usage/usage-section.module.css | 185 ++++++++ .../routes/workspace/[id]/usage/usage-section.tsx | 217 +++++++++ 28 files changed, 1106 insertions(+), 1068 deletions(-) delete mode 100644 packages/console/app/src/routes/workspace/[id]/graph-section.module.css delete mode 100644 packages/console/app/src/routes/workspace/[id]/graph-section.tsx delete mode 100644 packages/console/app/src/routes/workspace/[id]/usage-section.module.css delete mode 100644 packages/console/app/src/routes/workspace/[id]/usage-section.tsx create mode 100644 packages/console/app/src/routes/workspace/[id]/usage/graph-section.module.css create mode 100644 packages/console/app/src/routes/workspace/[id]/usage/graph-section.tsx create mode 100644 packages/console/app/src/routes/workspace/[id]/usage/index.tsx create mode 100644 packages/console/app/src/routes/workspace/[id]/usage/usage-section.module.css create mode 100644 packages/console/app/src/routes/workspace/[id]/usage/usage-section.tsx diff --git a/packages/console/app/src/i18n/ar.ts b/packages/console/app/src/i18n/ar.ts index 86d51226a..a17530920 100644 --- a/packages/console/app/src/i18n/ar.ts +++ b/packages/console/app/src/i18n/ar.ts @@ -411,6 +411,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "سيتم خصم المبلغ من بطاقتك عند تفعيل اشتراكك", "workspace.nav.zen": "Zen", + "workspace.nav.usage": "الاستخدام", "workspace.nav.apiKeys": "مفاتيح API", "workspace.nav.members": "الأعضاء", "workspace.nav.billing": "الفوترة", diff --git a/packages/console/app/src/i18n/br.ts b/packages/console/app/src/i18n/br.ts index f14a69c85..f395db9f4 100644 --- a/packages/console/app/src/i18n/br.ts +++ b/packages/console/app/src/i18n/br.ts @@ -418,6 +418,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "Seu cartão será cobrado quando sua assinatura for ativada", "workspace.nav.zen": "Zen", + "workspace.nav.usage": "Uso", "workspace.nav.apiKeys": "Chaves de API", "workspace.nav.members": "Membros", "workspace.nav.billing": "Faturamento", diff --git a/packages/console/app/src/i18n/da.ts b/packages/console/app/src/i18n/da.ts index 775f029fb..6e2af12af 100644 --- a/packages/console/app/src/i18n/da.ts +++ b/packages/console/app/src/i18n/da.ts @@ -414,6 +414,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "Dit kort vil blive debiteret, når dit abonnement er aktiveret", "workspace.nav.zen": "Zen", + "workspace.nav.usage": "Brug", "workspace.nav.apiKeys": "API-nøgler", "workspace.nav.members": "Medlemmer", "workspace.nav.billing": "Fakturering", diff --git a/packages/console/app/src/i18n/de.ts b/packages/console/app/src/i18n/de.ts index 2d9be14ff..87094a28f 100644 --- a/packages/console/app/src/i18n/de.ts +++ b/packages/console/app/src/i18n/de.ts @@ -417,6 +417,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "Deine Karte wird belastet, sobald dein Abonnement aktiviert ist", "workspace.nav.zen": "Zen", + "workspace.nav.usage": "Nutzung", "workspace.nav.apiKeys": "API Keys", "workspace.nav.members": "Mitglieder", "workspace.nav.billing": "Abrechnung", diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts index 02d6126c4..1f1773dd4 100644 --- a/packages/console/app/src/i18n/en.ts +++ b/packages/console/app/src/i18n/en.ts @@ -411,6 +411,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "Your card will be charged when your subscription is activated", "workspace.nav.zen": "Zen", + "workspace.nav.usage": "Usage", "workspace.nav.apiKeys": "API Keys", "workspace.nav.members": "Members", "workspace.nav.billing": "Billing", diff --git a/packages/console/app/src/i18n/es.ts b/packages/console/app/src/i18n/es.ts index 25b8d37d7..19a9842d5 100644 --- a/packages/console/app/src/i18n/es.ts +++ b/packages/console/app/src/i18n/es.ts @@ -419,6 +419,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "Tu tarjeta se cargará cuando tu suscripción se active", "workspace.nav.zen": "Zen", + "workspace.nav.usage": "Uso", "workspace.nav.apiKeys": "Claves API", "workspace.nav.members": "Miembros", "workspace.nav.billing": "Facturación", diff --git a/packages/console/app/src/i18n/fr.ts b/packages/console/app/src/i18n/fr.ts index ddf33c0ec..cf6d5e95b 100644 --- a/packages/console/app/src/i18n/fr.ts +++ b/packages/console/app/src/i18n/fr.ts @@ -419,6 +419,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "Votre carte sera débitée lorsque votre abonnement sera activé", "workspace.nav.zen": "Zen", + "workspace.nav.usage": "Utilisation", "workspace.nav.apiKeys": "Clés API", "workspace.nav.members": "Membres", "workspace.nav.billing": "Facturation", diff --git a/packages/console/app/src/i18n/it.ts b/packages/console/app/src/i18n/it.ts index 770efde45..b967f078c 100644 --- a/packages/console/app/src/i18n/it.ts +++ b/packages/console/app/src/i18n/it.ts @@ -417,6 +417,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "La tua carta verrà addebitata quando il tuo abbonamento sarà attivato", "workspace.nav.zen": "Zen", + "workspace.nav.usage": "Utilizzo", "workspace.nav.apiKeys": "Chiavi API", "workspace.nav.members": "Membri", "workspace.nav.billing": "Fatturazione", diff --git a/packages/console/app/src/i18n/ja.ts b/packages/console/app/src/i18n/ja.ts index f2786ba8d..759d5e7c7 100644 --- a/packages/console/app/src/i18n/ja.ts +++ b/packages/console/app/src/i18n/ja.ts @@ -416,6 +416,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "サブスクリプションが有効化された時点でカードに請求されます", "workspace.nav.zen": "Zen", + "workspace.nav.usage": "利用", "workspace.nav.apiKeys": "APIキー", "workspace.nav.members": "メンバー", "workspace.nav.billing": "請求", diff --git a/packages/console/app/src/i18n/ko.ts b/packages/console/app/src/i18n/ko.ts index 169b56c0a..9a5237f33 100644 --- a/packages/console/app/src/i18n/ko.ts +++ b/packages/console/app/src/i18n/ko.ts @@ -410,6 +410,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "구독이 활성화되면 카드에 청구됩니다", "workspace.nav.zen": "Zen", + "workspace.nav.usage": "사용량", "workspace.nav.apiKeys": "API 키", "workspace.nav.members": "멤버", "workspace.nav.billing": "결제", diff --git a/packages/console/app/src/i18n/no.ts b/packages/console/app/src/i18n/no.ts index 0b6e76e0c..fa44e535e 100644 --- a/packages/console/app/src/i18n/no.ts +++ b/packages/console/app/src/i18n/no.ts @@ -415,6 +415,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "Kortet ditt vil bli belastet når abonnementet aktiveres", "workspace.nav.zen": "Zen", + "workspace.nav.usage": "Bruk", "workspace.nav.apiKeys": "API-nøkler", "workspace.nav.members": "Medlemmer", "workspace.nav.billing": "Fakturering", diff --git a/packages/console/app/src/i18n/pl.ts b/packages/console/app/src/i18n/pl.ts index b46280ae1..7c7300ab9 100644 --- a/packages/console/app/src/i18n/pl.ts +++ b/packages/console/app/src/i18n/pl.ts @@ -416,6 +416,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "Twoja karta zostanie obciążona po aktywacji subskrypcji", "workspace.nav.zen": "Zen", + "workspace.nav.usage": "Użycie", "workspace.nav.apiKeys": "Klucze API", "workspace.nav.members": "Członkowie", "workspace.nav.billing": "Rozliczenia", diff --git a/packages/console/app/src/i18n/ru.ts b/packages/console/app/src/i18n/ru.ts index 801c8fc7d..77e36bc99 100644 --- a/packages/console/app/src/i18n/ru.ts +++ b/packages/console/app/src/i18n/ru.ts @@ -421,6 +421,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "С вашей карты будет списана оплата при активации подписки", "workspace.nav.zen": "Zen", + "workspace.nav.usage": "Использование", "workspace.nav.apiKeys": "API Ключи", "workspace.nav.members": "Участники", "workspace.nav.billing": "Оплата", diff --git a/packages/console/app/src/i18n/th.ts b/packages/console/app/src/i18n/th.ts index d9d7d03d1..9d1e92fa8 100644 --- a/packages/console/app/src/i18n/th.ts +++ b/packages/console/app/src/i18n/th.ts @@ -413,6 +413,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "บัตรของคุณจะถูกเรียกเก็บเงินเมื่อการสมัครสมาชิกของคุณถูกเปิดใช้งาน", "workspace.nav.zen": "Zen", + "workspace.nav.usage": "การใช้งาน", "workspace.nav.apiKeys": "API Keys", "workspace.nav.members": "สมาชิก", "workspace.nav.billing": "การเรียกเก็บเงิน", diff --git a/packages/console/app/src/i18n/tr.ts b/packages/console/app/src/i18n/tr.ts index e28afe2b0..84ad9a7e3 100644 --- a/packages/console/app/src/i18n/tr.ts +++ b/packages/console/app/src/i18n/tr.ts @@ -418,6 +418,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "Aboneliğiniz aktive edildiğinde kartınızdan ödeme alınacaktır", "workspace.nav.zen": "Zen", + "workspace.nav.usage": "Kullanım", "workspace.nav.apiKeys": "API Anahtarları", "workspace.nav.members": "Üyeler", "workspace.nav.billing": "Faturalandırma", diff --git a/packages/console/app/src/i18n/zh.ts b/packages/console/app/src/i18n/zh.ts index e12fe7749..a3527ae3e 100644 --- a/packages/console/app/src/i18n/zh.ts +++ b/packages/console/app/src/i18n/zh.ts @@ -396,6 +396,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "您的卡将在订阅激活时扣费", "workspace.nav.zen": "Zen", + "workspace.nav.usage": "使用量", "workspace.nav.apiKeys": "API 密钥", "workspace.nav.members": "成员", "workspace.nav.billing": "计费", diff --git a/packages/console/app/src/i18n/zht.ts b/packages/console/app/src/i18n/zht.ts index b45f87156..1167d5a6c 100644 --- a/packages/console/app/src/i18n/zht.ts +++ b/packages/console/app/src/i18n/zht.ts @@ -397,6 +397,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "你的卡片將在訂閱啟用時扣款", "workspace.nav.zen": "Zen", + "workspace.nav.usage": "使用量", "workspace.nav.apiKeys": "API 金鑰", "workspace.nav.members": "成員", "workspace.nav.billing": "帳務", diff --git a/packages/console/app/src/routes/workspace/[id].tsx b/packages/console/app/src/routes/workspace/[id].tsx index fde074e5e..a36368fd0 100644 --- a/packages/console/app/src/routes/workspace/[id].tsx +++ b/packages/console/app/src/routes/workspace/[id].tsx @@ -19,6 +19,9 @@ export default function WorkspaceLayout(props: RouteSectionProps) { {i18n.t("workspace.nav.zen")} + + {i18n.t("workspace.nav.usage")} + {i18n.t("workspace.nav.apiKeys")} @@ -41,6 +44,9 @@ export default function WorkspaceLayout(props: RouteSectionProps) { {i18n.t("workspace.nav.zen")} + + {i18n.t("workspace.nav.usage")} + {i18n.t("workspace.nav.apiKeys")} diff --git a/packages/console/app/src/routes/workspace/[id]/graph-section.module.css b/packages/console/app/src/routes/workspace/[id]/graph-section.module.css deleted file mode 100644 index 24b85be74..000000000 --- a/packages/console/app/src/routes/workspace/[id]/graph-section.module.css +++ /dev/null @@ -1,145 +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); - height: 400px; - display: flex; - align-items: center; - justify-content: center; - - p { - font-size: var(--font-size-sm); - color: var(--color-text-muted); - } - } - - [data-slot="filter-container"] { - margin-bottom: 0; - display: flex; - align-items: center; - gap: var(--space-3); - - [data-component="dropdown"] { - [data-slot="trigger"] { - border: 1px solid var(--color-border); - background-color: var(--color-bg); - padding: var(--space-2) var(--space-3); - border-radius: var(--border-radius-sm); - color: var(--color-text); - font-size: var(--font-size-sm); - line-height: 1.5; - - &:hover { - border-color: var(--color-accent); - } - - &:focus { - outline: none; - border-color: var(--color-accent); - box-shadow: 0 0 0 3px var(--color-accent-alpha); - } - } - - [data-slot="chevron"] { - opacity: 0.6; - } - - [data-slot="dropdown"] { - min-width: 200px; - max-height: 300px; - overflow-y: auto; - padding: var(--space-1); - } - } - } - - [data-slot="month-picker"] { - display: flex; - align-items: center; - background-color: var(--color-bg); - border: 1px solid var(--color-border); - border-radius: var(--border-radius-sm); - padding: 0; - } - - [data-slot="month-button"] { - display: flex; - align-items: center; - justify-content: center; - background: none; - border: none !important; - color: var(--color-text); - cursor: pointer; - padding: var(--space-2) var(--space-3); - border-radius: var(--border-radius-xs); - transition: background-color 0.2s; - line-height: 1; - - &:hover { - background-color: var(--color-bg-hover); - } - - svg { - display: block; - width: 16px; - height: 16px; - stroke-width: 2; - } - } - - [data-slot="month-label"] { - font-size: var(--font-size-sm); - font-weight: 500; - color: var(--color-text); - line-height: 1.5; - min-width: 140px; - text-align: center; - white-space: nowrap; - } - - [data-slot="model-item"] { - display: flex; - align-items: center; - gap: var(--space-2); - padding: var(--space-2) var(--space-3); - cursor: pointer; - transition: background-color 0.2s; - font-size: var(--font-size-sm); - color: var(--color-text); - border: none !important; - background: none; - width: 100%; - text-align: left; - white-space: nowrap; - - &:hover { - background: var(--color-bg-hover); - } - - span { - flex: 1; - user-select: none; - } - } - - [data-slot="chart-container"] { - padding: var(--space-6); - background: var(--color-bg-secondary); - border: 1px solid var(--color-border); - border-radius: var(--border-radius-sm); - height: 400px; - } - - @media (max-width: 40rem) { - [data-slot="chart-container"] { - height: 300px; - padding: var(--space-4); - } - - [data-component="empty-state"] { - height: 300px; - } - } -} diff --git a/packages/console/app/src/routes/workspace/[id]/graph-section.tsx b/packages/console/app/src/routes/workspace/[id]/graph-section.tsx deleted file mode 100644 index bb4b4f4cf..000000000 --- a/packages/console/app/src/routes/workspace/[id]/graph-section.tsx +++ /dev/null @@ -1,515 +0,0 @@ -import { and, Database, eq, gte, inArray, isNull, lt, or, sql, sum } from "@opencode-ai/console-core/drizzle/index.js" -import { UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js" -import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js" -import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" -import { AuthTable } from "@opencode-ai/console-core/schema/auth.sql.js" -import { useParams } from "@solidjs/router" -import { createEffect, createMemo, onCleanup, Show, For } from "solid-js" -import { createStore } from "solid-js/store" -import { withActor } from "~/context/auth.withActor" -import { Dropdown } from "~/component/dropdown" -import { IconChevronLeft, IconChevronRight } from "~/component/icon" -import styles from "./graph-section.module.css" -import { - Chart, - BarController, - BarElement, - CategoryScale, - LinearScale, - Tooltip, - Legend, - type ChartConfiguration, -} from "chart.js" -import { useI18n } from "~/context/i18n" - -Chart.register(BarController, BarElement, CategoryScale, LinearScale, Tooltip, Legend) - -async function getCosts(workspaceID: string, year: number, month: number) { - "use server" - return withActor(async () => { - const startDate = new Date(year, month, 1) - const endDate = new Date(year, month + 1, 1) - const usageData = await Database.use((tx) => - tx - .select({ - date: sql`DATE(${UsageTable.timeCreated})`, - model: UsageTable.model, - totalCost: sum(UsageTable.cost), - keyId: UsageTable.keyID, - plan: sql`JSON_EXTRACT(${UsageTable.enrichment}, '$.plan')`, - }) - .from(UsageTable) - .where( - and( - eq(UsageTable.workspaceID, workspaceID), - gte(UsageTable.timeCreated, startDate), - lt(UsageTable.timeCreated, endDate), - ), - ) - .groupBy( - sql`DATE(${UsageTable.timeCreated})`, - UsageTable.model, - UsageTable.keyID, - sql`JSON_EXTRACT(${UsageTable.enrichment}, '$.plan')`, - ) - .then((x) => - x.map((r) => ({ - ...r, - totalCost: r.totalCost ? parseInt(r.totalCost) : 0, - plan: r.plan as "sub" | "lite" | "byok" | null, - })), - ), - ) - - // Get unique key IDs from usage - const usageKeyIds = new Set(usageData.map((r) => r.keyId).filter((id) => id !== null)) - - // Second query: get all existing keys plus any keys from usage - const keysData = await Database.use((tx) => - tx - .select({ - keyId: KeyTable.id, - keyName: KeyTable.name, - userEmail: AuthTable.subject, - timeDeleted: KeyTable.timeDeleted, - }) - .from(KeyTable) - .innerJoin(UserTable, and(eq(KeyTable.userID, UserTable.id), eq(KeyTable.workspaceID, UserTable.workspaceID))) - .innerJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email"))) - .where( - and( - eq(KeyTable.workspaceID, workspaceID), - usageKeyIds.size > 0 - ? or(inArray(KeyTable.id, Array.from(usageKeyIds)), isNull(KeyTable.timeDeleted)) - : isNull(KeyTable.timeDeleted), - ), - ) - .orderBy(AuthTable.subject, KeyTable.name), - ) - - return { - usage: usageData, - keys: keysData.map((key) => ({ - id: key.keyId, - displayName: `${key.userEmail} - ${key.keyName}`, - deleted: key.timeDeleted !== null, - })), - } - }, workspaceID) -} - -const MODEL_COLORS: Record = { - "claude-sonnet-4-5": "#D4745C", - "claude-sonnet-4": "#E8B4A4", - "claude-opus-4": "#C8A098", - "claude-haiku-4-5": "#F0D8D0", - "claude-3-5-haiku": "#F8E8E0", - "gpt-5.1": "#4A90E2", - "gpt-5.1-codex": "#6BA8F0", - "gpt-5": "#7DB8F8", - "gpt-5-codex": "#9FCAFF", - "gpt-5-nano": "#B8D8FF", - "grok-code": "#8B5CF6", - "big-pickle": "#10B981", - "kimi-k2": "#F59E0B", - "qwen3-coder": "#EC4899", - "glm-4.6": "#14B8A6", -} - -function getModelColor(model: string): string { - if (MODEL_COLORS[model]) return MODEL_COLORS[model] - - const hash = model.split("").reduce((acc, char) => char.charCodeAt(0) + ((acc << 5) - acc), 0) - const hue = Math.abs(hash) % 360 - return `hsl(${hue}, 50%, 65%)` -} - -function formatDateLabel(dateStr: string): string { - const date = new Date() - const [y, m, d] = dateStr.split("-").map(Number) - date.setFullYear(y) - date.setMonth(m - 1) - date.setDate(d) - date.setHours(0, 0, 0, 0) - const month = date.toLocaleDateString(undefined, { month: "short" }) - const day = date.getUTCDate().toString().padStart(2, "0") - return `${month} ${day}` -} - -function addOpacityToColor(color: string, opacity: number): string { - if (color.startsWith("#")) { - const r = parseInt(color.slice(1, 3), 16) - const g = parseInt(color.slice(3, 5), 16) - const b = parseInt(color.slice(5, 7), 16) - return `rgba(${r}, ${g}, ${b}, ${opacity})` - } - if (color.startsWith("hsl")) return color.replace(")", `, ${opacity})`).replace("hsl", "hsla") - return color -} - -export function GraphSection() { - let canvasRef: HTMLCanvasElement | undefined - let chartInstance: Chart | undefined - const params = useParams() - const i18n = useI18n() - const now = new Date() - const [store, setStore] = createStore({ - data: null as Awaited> | null, - year: now.getFullYear(), - month: now.getMonth(), - key: null as string | null, - model: null as string | null, - modelDropdownOpen: false, - keyDropdownOpen: false, - colorScheme: "light" as "light" | "dark", - }) - const onPreviousMonth = async () => { - const month = store.month === 0 ? 11 : store.month - 1 - const year = store.month === 0 ? store.year - 1 : store.year - setStore({ month, year }) - } - - const onNextMonth = async () => { - const month = store.month === 11 ? 0 : store.month + 1 - const year = store.month === 11 ? store.year + 1 : store.year - setStore({ month, year }) - } - - const onSelectModel = (model: string | null) => setStore({ model, modelDropdownOpen: false }) - - const onSelectKey = (keyID: string | null) => setStore({ key: keyID, keyDropdownOpen: false }) - - const getModels = createMemo(() => { - if (!store.data?.usage) return [] - return Array.from(new Set(store.data.usage.map((row) => row.model))).sort() - }) - - const getDates = createMemo(() => { - const daysInMonth = new Date(store.year, store.month + 1, 0).getDate() - return Array.from({ length: daysInMonth }, (_, i) => { - const date = new Date(store.year, store.month, i + 1) - return date.toISOString().split("T")[0] - }) - }) - - const getKeyName = (keyID: string | null): string => { - if (!keyID || !store.data?.keys) return i18n.t("workspace.cost.allKeys") - const found = store.data.keys.find((k) => k.id === keyID) - if (!found) return i18n.t("workspace.cost.allKeys") - return found.deleted ? `${found.displayName} ${i18n.t("workspace.cost.deletedSuffix")}` : found.displayName - } - - const formatMonthYear = () => - new Date(store.year, store.month, 1).toLocaleDateString(undefined, { month: "long", year: "numeric" }) - - const isCurrentMonth = () => store.year === now.getFullYear() && store.month === now.getMonth() - - const chartConfig = createMemo((): ChartConfiguration | null => { - const data = store.data - const dates = getDates() - if (!data?.usage?.length) return null - - store.colorScheme - const styles = getComputedStyle(document.documentElement) - const colorTextMuted = styles.getPropertyValue("--color-text-muted").trim() - const colorBorderMuted = styles.getPropertyValue("--color-border-muted").trim() - const colorBgElevated = styles.getPropertyValue("--color-bg-elevated").trim() - const colorText = styles.getPropertyValue("--color-text").trim() - const colorTextSecondary = styles.getPropertyValue("--color-text-secondary").trim() - const colorBorder = styles.getPropertyValue("--color-border").trim() - const subSuffix = ` (${i18n.t("workspace.cost.subscriptionShort")})` - const liteSuffix = " (go)" - - const dailyDataRegular = new Map>() - const dailyDataSub = new Map>() - const dailyDataLite = new Map>() - for (const dateKey of dates) { - dailyDataRegular.set(dateKey, new Map()) - dailyDataSub.set(dateKey, new Map()) - dailyDataLite.set(dateKey, new Map()) - } - - data.usage - .filter((row) => (store.key ? row.keyId === store.key : true)) - .forEach((row) => { - const targetMap = row.plan === "sub" ? dailyDataSub : row.plan === "lite" ? dailyDataLite : dailyDataRegular - const dayMap = targetMap.get(row.date) - if (!dayMap) return - dayMap.set(row.model, (dayMap.get(row.model) ?? 0) + row.totalCost) - }) - - const filteredModels = store.model === null ? getModels() : [store.model] - - // Create datasets: regular first, then subscription, then lite (with visual distinction via opacity) - const datasets = [ - ...filteredModels - .filter((model) => dates.some((date) => (dailyDataRegular.get(date)?.get(model) || 0) > 0)) - .map((model) => { - const color = getModelColor(model) - return { - label: model, - data: dates.map((date) => (dailyDataRegular.get(date)?.get(model) || 0) / 100_000_000), - backgroundColor: color, - hoverBackgroundColor: color, - borderWidth: 0, - stack: "usage", - } - }), - ...filteredModels - .filter((model) => dates.some((date) => (dailyDataSub.get(date)?.get(model) || 0) > 0)) - .map((model) => { - const color = getModelColor(model) - return { - label: `${model}${subSuffix}`, - data: dates.map((date) => (dailyDataSub.get(date)?.get(model) || 0) / 100_000_000), - backgroundColor: addOpacityToColor(color, 0.5), - hoverBackgroundColor: addOpacityToColor(color, 0.7), - borderWidth: 1, - borderColor: color, - stack: "subscription", - } - }), - ...filteredModels - .filter((model) => dates.some((date) => (dailyDataLite.get(date)?.get(model) || 0) > 0)) - .map((model) => { - const color = getModelColor(model) - return { - label: `${model}${liteSuffix}`, - data: dates.map((date) => (dailyDataLite.get(date)?.get(model) || 0) / 100_000_000), - backgroundColor: addOpacityToColor(color, 0.35), - hoverBackgroundColor: addOpacityToColor(color, 0.55), - borderWidth: 1, - borderColor: addOpacityToColor(color, 0.7), - borderDash: [4, 2], - stack: "lite", - } - }), - ] - - return { - type: "bar", - data: { - labels: dates.map(formatDateLabel), - datasets, - }, - options: { - responsive: true, - maintainAspectRatio: false, - scales: { - x: { - stacked: true, - grid: { - display: false, - }, - ticks: { - maxRotation: 0, - autoSkipPadding: 20, - color: colorTextMuted, - font: { - family: "monospace", - size: 11, - }, - }, - }, - y: { - stacked: true, - beginAtZero: true, - grid: { - color: colorBorderMuted, - }, - ticks: { - color: colorTextMuted, - font: { - family: "monospace", - size: 11, - }, - callback: (value) => { - const num = Number(value) - return num >= 1000 ? `$${(num / 1000).toFixed(1)}k` : `$${num.toFixed(0)}` - }, - }, - }, - }, - plugins: { - tooltip: { - mode: "index", - intersect: false, - backgroundColor: colorBgElevated, - titleColor: colorText, - bodyColor: colorTextSecondary, - borderColor: colorBorder, - borderWidth: 1, - padding: 12, - displayColors: true, - filter: (item) => (item.parsed.y ?? 0) > 0, - callbacks: { - label: (context) => `${context.dataset.label}: $${(context.parsed.y ?? 0).toFixed(2)}`, - }, - }, - legend: { - display: true, - position: "bottom", - labels: { - color: colorTextSecondary, - font: { - size: 12, - }, - padding: 16, - boxWidth: 16, - boxHeight: 16, - usePointStyle: false, - }, - onHover: (event, legendItem, legend) => { - const chart = legend.chart - chart.data.datasets?.forEach((dataset, i) => { - const meta = chart.getDatasetMeta(i) - const label = dataset.label || "" - const isSub = label.endsWith(subSuffix) - const isLite = label.endsWith(liteSuffix) - const model = isSub - ? label.slice(0, -subSuffix.length) - : isLite - ? label.slice(0, -liteSuffix.length) - : label - const baseColor = getModelColor(model) - const originalColor = isSub - ? addOpacityToColor(baseColor, 0.5) - : isLite - ? addOpacityToColor(baseColor, 0.35) - : baseColor - const color = i === legendItem.datasetIndex ? originalColor : addOpacityToColor(baseColor, 0.15) - meta.data.forEach((bar: any) => { - bar.options.backgroundColor = color - }) - }) - chart.update("none") - }, - onLeave: (event, legendItem, legend) => { - const chart = legend.chart - chart.data.datasets?.forEach((dataset, i) => { - const meta = chart.getDatasetMeta(i) - const label = dataset.label || "" - const isSub = label.endsWith(subSuffix) - const isLite = label.endsWith(liteSuffix) - const model = isSub - ? label.slice(0, -subSuffix.length) - : isLite - ? label.slice(0, -liteSuffix.length) - : label - const baseColor = getModelColor(model) - const color = isSub - ? addOpacityToColor(baseColor, 0.5) - : isLite - ? addOpacityToColor(baseColor, 0.35) - : baseColor - meta.data.forEach((bar: any) => { - bar.options.backgroundColor = color - }) - }) - chart.update("none") - }, - }, - }, - }, - } - }) - - createEffect(async () => { - const data = await getCosts(params.id!, store.year, store.month) - setStore({ data }) - }) - - createEffect(() => { - const config = chartConfig() - if (!config || !canvasRef) return - - if (chartInstance) chartInstance.destroy() - chartInstance = new Chart(canvasRef, config) - - onCleanup(() => chartInstance?.destroy()) - }) - - createEffect(() => { - const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)") - setStore({ colorScheme: mediaQuery.matches ? "dark" : "light" }) - - const handleColorSchemeChange = (e: MediaQueryListEvent) => { - setStore({ colorScheme: e.matches ? "dark" : "light" }) - } - - mediaQuery.addEventListener("change", handleColorSchemeChange) - onCleanup(() => mediaQuery.removeEventListener("change", handleColorSchemeChange)) - }) - - return ( -
    -
    -

    {i18n.t("workspace.cost.title")}

    -

    {i18n.t("workspace.cost.subtitle")}

    -
    - -
    -
    - - {formatMonthYear()} - -
    - setStore({ modelDropdownOpen: open })} - > - <> - - - {(model) => ( - - )} - - - - setStore({ keyDropdownOpen: open })} - > - <> - - - {(key) => ( - - )} - - - -
    - - -

    {i18n.t("workspace.cost.empty")}

    - - } - > -
    - -
    -
    -
    - ) -} diff --git a/packages/console/app/src/routes/workspace/[id]/index.tsx b/packages/console/app/src/routes/workspace/[id]/index.tsx index c91cfd2bc..ca3c2bc0c 100644 --- a/packages/console/app/src/routes/workspace/[id]/index.tsx +++ b/packages/console/app/src/routes/workspace/[id]/index.tsx @@ -2,10 +2,8 @@ import { Match, Show, Switch, createMemo } from "solid-js" import { createStore } from "solid-js/store" import { createAsync, useParams, useAction, useSubmission } from "@solidjs/router" import { NewUserSection } from "./new-user-section" -import { UsageSection } from "./usage-section" import { ModelSection } from "./model-section" import { ProviderSection } from "./provider-section" -import { GraphSection } from "./graph-section" import { IconLogo } from "~/component/icon" import { querySessionInfo, queryBillingInfo, createCheckoutUrl, formatBalance } from "../common" import { useI18n } from "~/context/i18n" @@ -73,14 +71,10 @@ export default function () {
    - - - -
    ) diff --git a/packages/console/app/src/routes/workspace/[id]/usage-section.module.css b/packages/console/app/src/routes/workspace/[id]/usage-section.module.css deleted file mode 100644 index 00232de88..000000000 --- a/packages/console/app/src/routes/workspace/[id]/usage-section.module.css +++ /dev/null @@ -1,185 +0,0 @@ -.root { - /* Empty state */ - [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); - - p { - font-size: var(--font-size-sm); - color: var(--color-text-muted); - } - } - - /* Table container */ - [data-slot="usage-table"] { - overflow-x: auto; - } - - /* Table element */ - [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-muted); - } - - &[data-slot="usage-model"] { - font-family: var(--font-sans); - color: var(--color-text-secondary); - max-width: 200px; - word-break: break-word; - } - - &[data-slot="usage-cost"] { - color: var(--color-text-muted); - } - - [data-slot="tokens-with-breakdown"] { - position: relative; - display: flex; - align-items: center; - gap: var(--space-2); - } - - [data-slot="breakdown-button"] { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0; - background: transparent; - border: none; - color: var(--color-text-muted); - cursor: pointer; - transition: color 0.15s ease; - - &:hover { - color: var(--color-text); - } - - svg { - width: 16px; - height: 16px; - } - } - - [data-slot="breakdown-popup"] { - position: absolute; - left: 0; - top: 100%; - margin-top: var(--space-2); - background: var(--color-bg); - border: 1px solid var(--color-border); - border-radius: var(--border-radius-sm); - padding: var(--space-2); - z-index: 10; - min-width: 180px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - font-size: var(--font-size-xs); - - @media (prefers-color-scheme: dark) { - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); - } - } - } - - tbody tr:last-child td { - border-bottom: none; - } - } - - /* Pagination */ - [data-slot="pagination"] { - display: flex; - justify-content: flex-end; - gap: var(--space-2); - padding: var(--space-4) 0; - border-top: 1px solid var(--color-border-muted); - margin-top: var(--space-2); - - button { - padding: var(--space-2) var(--space-4); - background: var(--color-bg-secondary); - border: 1px solid var(--color-border); - border-radius: var(--border-radius-sm); - color: var(--color-text); - font-size: var(--font-size-sm); - cursor: pointer; - transition: all 0.15s ease; - - svg { - width: 16px; - height: 16px; - stroke-width: 2; - } - - &:hover:not(:disabled) { - background: var(--color-bg-tertiary); - border-color: var(--color-border-hover); - } - - &:disabled { - opacity: 0.5; - cursor: not-allowed; - } - } - } - - /* Mobile responsive */ - @media (max-width: 40rem) { - [data-slot="usage-table-element"] { - th, - td { - padding: var(--space-2) var(--space-3); - font-size: var(--font-size-xs); - } - - /* Hide Model column on mobile */ - th:nth-child(2), - td:nth-child(2) { - display: none; - } - } - } - - /* Breakdown popup content */ - [data-slot="breakdown-row"] { - display: flex; - justify-content: space-between; - align-items: center; - gap: var(--space-4); - padding: var(--space-1) 0; - } - - [data-slot="breakdown-label"] { - color: var(--color-text-muted); - font-size: var(--font-size-xs); - } - - [data-slot="breakdown-value"] { - color: var(--color-text); - font-weight: 500; - font-size: var(--font-size-xs); - } -} diff --git a/packages/console/app/src/routes/workspace/[id]/usage-section.tsx b/packages/console/app/src/routes/workspace/[id]/usage-section.tsx deleted file mode 100644 index a20a5bf0d..000000000 --- a/packages/console/app/src/routes/workspace/[id]/usage-section.tsx +++ /dev/null @@ -1,217 +0,0 @@ -import { Billing } from "@opencode-ai/console-core/billing.js" -import { createAsync, query, useParams } from "@solidjs/router" -import { createMemo, For, Show, Switch, Match, createEffect, createSignal } from "solid-js" -import { formatDateUTC, formatDateForTable } from "../common" -import { withActor } from "~/context/auth.withActor" -import { IconChevronLeft, IconChevronRight, IconBreakdown } from "~/component/icon" -import styles from "./usage-section.module.css" -import { createStore } from "solid-js/store" -import { useI18n } from "~/context/i18n" - -const PAGE_SIZE = 50 - -async function getUsageInfo(workspaceID: string, page: number) { - "use server" - return withActor(async () => { - return await Billing.usages(page, PAGE_SIZE) - }, workspaceID) -} - -const queryUsageInfo = query(getUsageInfo, "usage.list") - -export function UsageSection() { - const params = useParams() - const i18n = useI18n() - const usage = createAsync(() => queryUsageInfo(params.id!, 0)) - const [store, setStore] = createStore({ page: 0, usage: [] as Awaited> }) - const [openBreakdownId, setOpenBreakdownId] = createSignal(null) - - createEffect(() => { - setStore({ usage: usage() }) - }, [usage]) - - createEffect(() => { - if (!openBreakdownId()) return - - const handleClickOutside = (e: MouseEvent) => { - const target = e.target as HTMLElement - if (!target.closest('[data-slot="tokens-with-breakdown"]')) { - setOpenBreakdownId(null) - } - } - - document.addEventListener("click", handleClickOutside) - return () => document.removeEventListener("click", handleClickOutside) - }) - - const hasResults = createMemo(() => store.usage && store.usage.length > 0) - const canGoPrev = createMemo(() => store.page > 0) - const canGoNext = createMemo(() => store.usage && store.usage.length === PAGE_SIZE) - - const calculateTotalInputTokens = (u: Awaited>[0]) => { - return u.inputTokens + (u.cacheReadTokens ?? 0) + (u.cacheWrite5mTokens ?? 0) + (u.cacheWrite1hTokens ?? 0) - } - - const calculateTotalOutputTokens = (u: Awaited>[0]) => { - return u.outputTokens + (u.reasoningTokens ?? 0) - } - - const goPrev = async () => { - const usage = await getUsageInfo(params.id!, store.page - 1) - setStore({ - page: store.page - 1, - usage, - }) - } - const goNext = async () => { - const usage = await getUsageInfo(params.id!, store.page + 1) - setStore({ - page: store.page + 1, - usage, - }) - } - - return ( -
    -
    -

    {i18n.t("workspace.usage.title")}

    -

    {i18n.t("workspace.usage.subtitle")}

    -
    -
    - -

    {i18n.t("workspace.usage.empty")}

    -
    - } - > - - - - - - - - - - - - - - {(usage, index) => { - const date = createMemo(() => new Date(usage.timeCreated)) - const totalInputTokens = createMemo(() => calculateTotalInputTokens(usage)) - const totalOutputTokens = createMemo(() => calculateTotalOutputTokens(usage)) - const inputBreakdownId = `input-breakdown-${index()}` - const outputBreakdownId = `output-breakdown-${index()}` - const isInputOpen = createMemo(() => openBreakdownId() === inputBreakdownId) - const isOutputOpen = createMemo(() => openBreakdownId() === outputBreakdownId) - const isClaude = usage.model.toLowerCase().includes("claude") - return ( - - - - - - - - - ) - }} - - -
    {i18n.t("workspace.usage.table.date")}{i18n.t("workspace.usage.table.model")}{i18n.t("workspace.usage.table.input")}{i18n.t("workspace.usage.table.output")}{i18n.t("workspace.usage.table.cost")}{i18n.t("workspace.usage.table.session")}
    - {formatDateForTable(date())} - {usage.model} -
    e.stopPropagation()}> - - setOpenBreakdownId(null)}>{totalInputTokens()} - -
    e.stopPropagation()}> -
    - {i18n.t("workspace.usage.breakdown.input")} - {usage.inputTokens} -
    -
    - {i18n.t("workspace.usage.breakdown.cacheRead")} - {usage.cacheReadTokens ?? 0} -
    - -
    - - {i18n.t("workspace.usage.breakdown.cacheWrite")} - - {usage.cacheWrite5mTokens ?? 0} -
    -
    -
    -
    -
    -
    -
    e.stopPropagation()}> - - setOpenBreakdownId(null)}>{totalOutputTokens()} - -
    e.stopPropagation()}> -
    - {i18n.t("workspace.usage.breakdown.output")} - {usage.outputTokens} -
    -
    - {i18n.t("workspace.usage.breakdown.reasoning")} - {usage.reasoningTokens ?? 0} -
    -
    -
    -
    -
    - ${((usage.cost ?? 0) / 100000000).toFixed(4)}}> - - {i18n.t("workspace.usage.subscription", { - amount: ((usage.cost ?? 0) / 100000000).toFixed(4), - })} - - - {i18n.t("workspace.usage.lite", { - amount: ((usage.cost ?? 0) / 100000000).toFixed(4), - })} - - - {i18n.t("workspace.usage.byok", { - amount: ((usage.cost ?? 0) / 100000000).toFixed(4), - })} - - - {usage.sessionID?.slice(-8) ?? "-"}
    - -
    - - -
    -
    - - -
    - ) -} diff --git a/packages/console/app/src/routes/workspace/[id]/usage/graph-section.module.css b/packages/console/app/src/routes/workspace/[id]/usage/graph-section.module.css new file mode 100644 index 000000000..24b85be74 --- /dev/null +++ b/packages/console/app/src/routes/workspace/[id]/usage/graph-section.module.css @@ -0,0 +1,145 @@ +.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); + height: 400px; + display: flex; + align-items: center; + justify-content: center; + + p { + font-size: var(--font-size-sm); + color: var(--color-text-muted); + } + } + + [data-slot="filter-container"] { + margin-bottom: 0; + display: flex; + align-items: center; + gap: var(--space-3); + + [data-component="dropdown"] { + [data-slot="trigger"] { + border: 1px solid var(--color-border); + background-color: var(--color-bg); + padding: var(--space-2) var(--space-3); + border-radius: var(--border-radius-sm); + color: var(--color-text); + font-size: var(--font-size-sm); + line-height: 1.5; + + &:hover { + border-color: var(--color-accent); + } + + &:focus { + outline: none; + border-color: var(--color-accent); + box-shadow: 0 0 0 3px var(--color-accent-alpha); + } + } + + [data-slot="chevron"] { + opacity: 0.6; + } + + [data-slot="dropdown"] { + min-width: 200px; + max-height: 300px; + overflow-y: auto; + padding: var(--space-1); + } + } + } + + [data-slot="month-picker"] { + display: flex; + align-items: center; + background-color: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + padding: 0; + } + + [data-slot="month-button"] { + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none !important; + color: var(--color-text); + cursor: pointer; + padding: var(--space-2) var(--space-3); + border-radius: var(--border-radius-xs); + transition: background-color 0.2s; + line-height: 1; + + &:hover { + background-color: var(--color-bg-hover); + } + + svg { + display: block; + width: 16px; + height: 16px; + stroke-width: 2; + } + } + + [data-slot="month-label"] { + font-size: var(--font-size-sm); + font-weight: 500; + color: var(--color-text); + line-height: 1.5; + min-width: 140px; + text-align: center; + white-space: nowrap; + } + + [data-slot="model-item"] { + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-2) var(--space-3); + cursor: pointer; + transition: background-color 0.2s; + font-size: var(--font-size-sm); + color: var(--color-text); + border: none !important; + background: none; + width: 100%; + text-align: left; + white-space: nowrap; + + &:hover { + background: var(--color-bg-hover); + } + + span { + flex: 1; + user-select: none; + } + } + + [data-slot="chart-container"] { + padding: var(--space-6); + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + height: 400px; + } + + @media (max-width: 40rem) { + [data-slot="chart-container"] { + height: 300px; + padding: var(--space-4); + } + + [data-component="empty-state"] { + height: 300px; + } + } +} diff --git a/packages/console/app/src/routes/workspace/[id]/usage/graph-section.tsx b/packages/console/app/src/routes/workspace/[id]/usage/graph-section.tsx new file mode 100644 index 000000000..bb4b4f4cf --- /dev/null +++ b/packages/console/app/src/routes/workspace/[id]/usage/graph-section.tsx @@ -0,0 +1,515 @@ +import { and, Database, eq, gte, inArray, isNull, lt, or, sql, sum } from "@opencode-ai/console-core/drizzle/index.js" +import { UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js" +import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js" +import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" +import { AuthTable } from "@opencode-ai/console-core/schema/auth.sql.js" +import { useParams } from "@solidjs/router" +import { createEffect, createMemo, onCleanup, Show, For } from "solid-js" +import { createStore } from "solid-js/store" +import { withActor } from "~/context/auth.withActor" +import { Dropdown } from "~/component/dropdown" +import { IconChevronLeft, IconChevronRight } from "~/component/icon" +import styles from "./graph-section.module.css" +import { + Chart, + BarController, + BarElement, + CategoryScale, + LinearScale, + Tooltip, + Legend, + type ChartConfiguration, +} from "chart.js" +import { useI18n } from "~/context/i18n" + +Chart.register(BarController, BarElement, CategoryScale, LinearScale, Tooltip, Legend) + +async function getCosts(workspaceID: string, year: number, month: number) { + "use server" + return withActor(async () => { + const startDate = new Date(year, month, 1) + const endDate = new Date(year, month + 1, 1) + const usageData = await Database.use((tx) => + tx + .select({ + date: sql`DATE(${UsageTable.timeCreated})`, + model: UsageTable.model, + totalCost: sum(UsageTable.cost), + keyId: UsageTable.keyID, + plan: sql`JSON_EXTRACT(${UsageTable.enrichment}, '$.plan')`, + }) + .from(UsageTable) + .where( + and( + eq(UsageTable.workspaceID, workspaceID), + gte(UsageTable.timeCreated, startDate), + lt(UsageTable.timeCreated, endDate), + ), + ) + .groupBy( + sql`DATE(${UsageTable.timeCreated})`, + UsageTable.model, + UsageTable.keyID, + sql`JSON_EXTRACT(${UsageTable.enrichment}, '$.plan')`, + ) + .then((x) => + x.map((r) => ({ + ...r, + totalCost: r.totalCost ? parseInt(r.totalCost) : 0, + plan: r.plan as "sub" | "lite" | "byok" | null, + })), + ), + ) + + // Get unique key IDs from usage + const usageKeyIds = new Set(usageData.map((r) => r.keyId).filter((id) => id !== null)) + + // Second query: get all existing keys plus any keys from usage + const keysData = await Database.use((tx) => + tx + .select({ + keyId: KeyTable.id, + keyName: KeyTable.name, + userEmail: AuthTable.subject, + timeDeleted: KeyTable.timeDeleted, + }) + .from(KeyTable) + .innerJoin(UserTable, and(eq(KeyTable.userID, UserTable.id), eq(KeyTable.workspaceID, UserTable.workspaceID))) + .innerJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email"))) + .where( + and( + eq(KeyTable.workspaceID, workspaceID), + usageKeyIds.size > 0 + ? or(inArray(KeyTable.id, Array.from(usageKeyIds)), isNull(KeyTable.timeDeleted)) + : isNull(KeyTable.timeDeleted), + ), + ) + .orderBy(AuthTable.subject, KeyTable.name), + ) + + return { + usage: usageData, + keys: keysData.map((key) => ({ + id: key.keyId, + displayName: `${key.userEmail} - ${key.keyName}`, + deleted: key.timeDeleted !== null, + })), + } + }, workspaceID) +} + +const MODEL_COLORS: Record = { + "claude-sonnet-4-5": "#D4745C", + "claude-sonnet-4": "#E8B4A4", + "claude-opus-4": "#C8A098", + "claude-haiku-4-5": "#F0D8D0", + "claude-3-5-haiku": "#F8E8E0", + "gpt-5.1": "#4A90E2", + "gpt-5.1-codex": "#6BA8F0", + "gpt-5": "#7DB8F8", + "gpt-5-codex": "#9FCAFF", + "gpt-5-nano": "#B8D8FF", + "grok-code": "#8B5CF6", + "big-pickle": "#10B981", + "kimi-k2": "#F59E0B", + "qwen3-coder": "#EC4899", + "glm-4.6": "#14B8A6", +} + +function getModelColor(model: string): string { + if (MODEL_COLORS[model]) return MODEL_COLORS[model] + + const hash = model.split("").reduce((acc, char) => char.charCodeAt(0) + ((acc << 5) - acc), 0) + const hue = Math.abs(hash) % 360 + return `hsl(${hue}, 50%, 65%)` +} + +function formatDateLabel(dateStr: string): string { + const date = new Date() + const [y, m, d] = dateStr.split("-").map(Number) + date.setFullYear(y) + date.setMonth(m - 1) + date.setDate(d) + date.setHours(0, 0, 0, 0) + const month = date.toLocaleDateString(undefined, { month: "short" }) + const day = date.getUTCDate().toString().padStart(2, "0") + return `${month} ${day}` +} + +function addOpacityToColor(color: string, opacity: number): string { + if (color.startsWith("#")) { + const r = parseInt(color.slice(1, 3), 16) + const g = parseInt(color.slice(3, 5), 16) + const b = parseInt(color.slice(5, 7), 16) + return `rgba(${r}, ${g}, ${b}, ${opacity})` + } + if (color.startsWith("hsl")) return color.replace(")", `, ${opacity})`).replace("hsl", "hsla") + return color +} + +export function GraphSection() { + let canvasRef: HTMLCanvasElement | undefined + let chartInstance: Chart | undefined + const params = useParams() + const i18n = useI18n() + const now = new Date() + const [store, setStore] = createStore({ + data: null as Awaited> | null, + year: now.getFullYear(), + month: now.getMonth(), + key: null as string | null, + model: null as string | null, + modelDropdownOpen: false, + keyDropdownOpen: false, + colorScheme: "light" as "light" | "dark", + }) + const onPreviousMonth = async () => { + const month = store.month === 0 ? 11 : store.month - 1 + const year = store.month === 0 ? store.year - 1 : store.year + setStore({ month, year }) + } + + const onNextMonth = async () => { + const month = store.month === 11 ? 0 : store.month + 1 + const year = store.month === 11 ? store.year + 1 : store.year + setStore({ month, year }) + } + + const onSelectModel = (model: string | null) => setStore({ model, modelDropdownOpen: false }) + + const onSelectKey = (keyID: string | null) => setStore({ key: keyID, keyDropdownOpen: false }) + + const getModels = createMemo(() => { + if (!store.data?.usage) return [] + return Array.from(new Set(store.data.usage.map((row) => row.model))).sort() + }) + + const getDates = createMemo(() => { + const daysInMonth = new Date(store.year, store.month + 1, 0).getDate() + return Array.from({ length: daysInMonth }, (_, i) => { + const date = new Date(store.year, store.month, i + 1) + return date.toISOString().split("T")[0] + }) + }) + + const getKeyName = (keyID: string | null): string => { + if (!keyID || !store.data?.keys) return i18n.t("workspace.cost.allKeys") + const found = store.data.keys.find((k) => k.id === keyID) + if (!found) return i18n.t("workspace.cost.allKeys") + return found.deleted ? `${found.displayName} ${i18n.t("workspace.cost.deletedSuffix")}` : found.displayName + } + + const formatMonthYear = () => + new Date(store.year, store.month, 1).toLocaleDateString(undefined, { month: "long", year: "numeric" }) + + const isCurrentMonth = () => store.year === now.getFullYear() && store.month === now.getMonth() + + const chartConfig = createMemo((): ChartConfiguration | null => { + const data = store.data + const dates = getDates() + if (!data?.usage?.length) return null + + store.colorScheme + const styles = getComputedStyle(document.documentElement) + const colorTextMuted = styles.getPropertyValue("--color-text-muted").trim() + const colorBorderMuted = styles.getPropertyValue("--color-border-muted").trim() + const colorBgElevated = styles.getPropertyValue("--color-bg-elevated").trim() + const colorText = styles.getPropertyValue("--color-text").trim() + const colorTextSecondary = styles.getPropertyValue("--color-text-secondary").trim() + const colorBorder = styles.getPropertyValue("--color-border").trim() + const subSuffix = ` (${i18n.t("workspace.cost.subscriptionShort")})` + const liteSuffix = " (go)" + + const dailyDataRegular = new Map>() + const dailyDataSub = new Map>() + const dailyDataLite = new Map>() + for (const dateKey of dates) { + dailyDataRegular.set(dateKey, new Map()) + dailyDataSub.set(dateKey, new Map()) + dailyDataLite.set(dateKey, new Map()) + } + + data.usage + .filter((row) => (store.key ? row.keyId === store.key : true)) + .forEach((row) => { + const targetMap = row.plan === "sub" ? dailyDataSub : row.plan === "lite" ? dailyDataLite : dailyDataRegular + const dayMap = targetMap.get(row.date) + if (!dayMap) return + dayMap.set(row.model, (dayMap.get(row.model) ?? 0) + row.totalCost) + }) + + const filteredModels = store.model === null ? getModels() : [store.model] + + // Create datasets: regular first, then subscription, then lite (with visual distinction via opacity) + const datasets = [ + ...filteredModels + .filter((model) => dates.some((date) => (dailyDataRegular.get(date)?.get(model) || 0) > 0)) + .map((model) => { + const color = getModelColor(model) + return { + label: model, + data: dates.map((date) => (dailyDataRegular.get(date)?.get(model) || 0) / 100_000_000), + backgroundColor: color, + hoverBackgroundColor: color, + borderWidth: 0, + stack: "usage", + } + }), + ...filteredModels + .filter((model) => dates.some((date) => (dailyDataSub.get(date)?.get(model) || 0) > 0)) + .map((model) => { + const color = getModelColor(model) + return { + label: `${model}${subSuffix}`, + data: dates.map((date) => (dailyDataSub.get(date)?.get(model) || 0) / 100_000_000), + backgroundColor: addOpacityToColor(color, 0.5), + hoverBackgroundColor: addOpacityToColor(color, 0.7), + borderWidth: 1, + borderColor: color, + stack: "subscription", + } + }), + ...filteredModels + .filter((model) => dates.some((date) => (dailyDataLite.get(date)?.get(model) || 0) > 0)) + .map((model) => { + const color = getModelColor(model) + return { + label: `${model}${liteSuffix}`, + data: dates.map((date) => (dailyDataLite.get(date)?.get(model) || 0) / 100_000_000), + backgroundColor: addOpacityToColor(color, 0.35), + hoverBackgroundColor: addOpacityToColor(color, 0.55), + borderWidth: 1, + borderColor: addOpacityToColor(color, 0.7), + borderDash: [4, 2], + stack: "lite", + } + }), + ] + + return { + type: "bar", + data: { + labels: dates.map(formatDateLabel), + datasets, + }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { + stacked: true, + grid: { + display: false, + }, + ticks: { + maxRotation: 0, + autoSkipPadding: 20, + color: colorTextMuted, + font: { + family: "monospace", + size: 11, + }, + }, + }, + y: { + stacked: true, + beginAtZero: true, + grid: { + color: colorBorderMuted, + }, + ticks: { + color: colorTextMuted, + font: { + family: "monospace", + size: 11, + }, + callback: (value) => { + const num = Number(value) + return num >= 1000 ? `$${(num / 1000).toFixed(1)}k` : `$${num.toFixed(0)}` + }, + }, + }, + }, + plugins: { + tooltip: { + mode: "index", + intersect: false, + backgroundColor: colorBgElevated, + titleColor: colorText, + bodyColor: colorTextSecondary, + borderColor: colorBorder, + borderWidth: 1, + padding: 12, + displayColors: true, + filter: (item) => (item.parsed.y ?? 0) > 0, + callbacks: { + label: (context) => `${context.dataset.label}: $${(context.parsed.y ?? 0).toFixed(2)}`, + }, + }, + legend: { + display: true, + position: "bottom", + labels: { + color: colorTextSecondary, + font: { + size: 12, + }, + padding: 16, + boxWidth: 16, + boxHeight: 16, + usePointStyle: false, + }, + onHover: (event, legendItem, legend) => { + const chart = legend.chart + chart.data.datasets?.forEach((dataset, i) => { + const meta = chart.getDatasetMeta(i) + const label = dataset.label || "" + const isSub = label.endsWith(subSuffix) + const isLite = label.endsWith(liteSuffix) + const model = isSub + ? label.slice(0, -subSuffix.length) + : isLite + ? label.slice(0, -liteSuffix.length) + : label + const baseColor = getModelColor(model) + const originalColor = isSub + ? addOpacityToColor(baseColor, 0.5) + : isLite + ? addOpacityToColor(baseColor, 0.35) + : baseColor + const color = i === legendItem.datasetIndex ? originalColor : addOpacityToColor(baseColor, 0.15) + meta.data.forEach((bar: any) => { + bar.options.backgroundColor = color + }) + }) + chart.update("none") + }, + onLeave: (event, legendItem, legend) => { + const chart = legend.chart + chart.data.datasets?.forEach((dataset, i) => { + const meta = chart.getDatasetMeta(i) + const label = dataset.label || "" + const isSub = label.endsWith(subSuffix) + const isLite = label.endsWith(liteSuffix) + const model = isSub + ? label.slice(0, -subSuffix.length) + : isLite + ? label.slice(0, -liteSuffix.length) + : label + const baseColor = getModelColor(model) + const color = isSub + ? addOpacityToColor(baseColor, 0.5) + : isLite + ? addOpacityToColor(baseColor, 0.35) + : baseColor + meta.data.forEach((bar: any) => { + bar.options.backgroundColor = color + }) + }) + chart.update("none") + }, + }, + }, + }, + } + }) + + createEffect(async () => { + const data = await getCosts(params.id!, store.year, store.month) + setStore({ data }) + }) + + createEffect(() => { + const config = chartConfig() + if (!config || !canvasRef) return + + if (chartInstance) chartInstance.destroy() + chartInstance = new Chart(canvasRef, config) + + onCleanup(() => chartInstance?.destroy()) + }) + + createEffect(() => { + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)") + setStore({ colorScheme: mediaQuery.matches ? "dark" : "light" }) + + const handleColorSchemeChange = (e: MediaQueryListEvent) => { + setStore({ colorScheme: e.matches ? "dark" : "light" }) + } + + mediaQuery.addEventListener("change", handleColorSchemeChange) + onCleanup(() => mediaQuery.removeEventListener("change", handleColorSchemeChange)) + }) + + return ( +
    +
    +

    {i18n.t("workspace.cost.title")}

    +

    {i18n.t("workspace.cost.subtitle")}

    +
    + +
    +
    + + {formatMonthYear()} + +
    + setStore({ modelDropdownOpen: open })} + > + <> + + + {(model) => ( + + )} + + + + setStore({ keyDropdownOpen: open })} + > + <> + + + {(key) => ( + + )} + + + +
    + + +

    {i18n.t("workspace.cost.empty")}

    + + } + > +
    + +
    +
    +
    + ) +} diff --git a/packages/console/app/src/routes/workspace/[id]/usage/index.tsx b/packages/console/app/src/routes/workspace/[id]/usage/index.tsx new file mode 100644 index 000000000..3a9c8db29 --- /dev/null +++ b/packages/console/app/src/routes/workspace/[id]/usage/index.tsx @@ -0,0 +1,21 @@ +import { Show } from "solid-js" +import { createAsync, useParams } from "@solidjs/router" +import { GraphSection } from "./graph-section" +import { UsageSection } from "./usage-section" +import { querySessionInfo } from "../../common" + +export default function () { + const params = useParams() + const user = createAsync(() => querySessionInfo(params.id!)) + + return ( +
    +
    + + + + +
    +
    + ) +} diff --git a/packages/console/app/src/routes/workspace/[id]/usage/usage-section.module.css b/packages/console/app/src/routes/workspace/[id]/usage/usage-section.module.css new file mode 100644 index 000000000..00232de88 --- /dev/null +++ b/packages/console/app/src/routes/workspace/[id]/usage/usage-section.module.css @@ -0,0 +1,185 @@ +.root { + /* Empty state */ + [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); + + p { + font-size: var(--font-size-sm); + color: var(--color-text-muted); + } + } + + /* Table container */ + [data-slot="usage-table"] { + overflow-x: auto; + } + + /* Table element */ + [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-muted); + } + + &[data-slot="usage-model"] { + font-family: var(--font-sans); + color: var(--color-text-secondary); + max-width: 200px; + word-break: break-word; + } + + &[data-slot="usage-cost"] { + color: var(--color-text-muted); + } + + [data-slot="tokens-with-breakdown"] { + position: relative; + display: flex; + align-items: center; + gap: var(--space-2); + } + + [data-slot="breakdown-button"] { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + background: transparent; + border: none; + color: var(--color-text-muted); + cursor: pointer; + transition: color 0.15s ease; + + &:hover { + color: var(--color-text); + } + + svg { + width: 16px; + height: 16px; + } + } + + [data-slot="breakdown-popup"] { + position: absolute; + left: 0; + top: 100%; + margin-top: var(--space-2); + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + padding: var(--space-2); + z-index: 10; + min-width: 180px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + font-size: var(--font-size-xs); + + @media (prefers-color-scheme: dark) { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + } + } + } + + tbody tr:last-child td { + border-bottom: none; + } + } + + /* Pagination */ + [data-slot="pagination"] { + display: flex; + justify-content: flex-end; + gap: var(--space-2); + padding: var(--space-4) 0; + border-top: 1px solid var(--color-border-muted); + margin-top: var(--space-2); + + button { + padding: var(--space-2) var(--space-4); + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + color: var(--color-text); + font-size: var(--font-size-sm); + cursor: pointer; + transition: all 0.15s ease; + + svg { + width: 16px; + height: 16px; + stroke-width: 2; + } + + &:hover:not(:disabled) { + background: var(--color-bg-tertiary); + border-color: var(--color-border-hover); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + } + } + + /* Mobile responsive */ + @media (max-width: 40rem) { + [data-slot="usage-table-element"] { + th, + td { + padding: var(--space-2) var(--space-3); + font-size: var(--font-size-xs); + } + + /* Hide Model column on mobile */ + th:nth-child(2), + td:nth-child(2) { + display: none; + } + } + } + + /* Breakdown popup content */ + [data-slot="breakdown-row"] { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--space-4); + padding: var(--space-1) 0; + } + + [data-slot="breakdown-label"] { + color: var(--color-text-muted); + font-size: var(--font-size-xs); + } + + [data-slot="breakdown-value"] { + color: var(--color-text); + font-weight: 500; + font-size: var(--font-size-xs); + } +} diff --git a/packages/console/app/src/routes/workspace/[id]/usage/usage-section.tsx b/packages/console/app/src/routes/workspace/[id]/usage/usage-section.tsx new file mode 100644 index 000000000..2cf8ef850 --- /dev/null +++ b/packages/console/app/src/routes/workspace/[id]/usage/usage-section.tsx @@ -0,0 +1,217 @@ +import { Billing } from "@opencode-ai/console-core/billing.js" +import { createAsync, query, useParams } from "@solidjs/router" +import { createMemo, For, Show, Switch, Match, createEffect, createSignal } from "solid-js" +import { formatDateUTC, formatDateForTable } from "../../common" +import { withActor } from "~/context/auth.withActor" +import { IconChevronLeft, IconChevronRight, IconBreakdown } from "~/component/icon" +import styles from "./usage-section.module.css" +import { createStore } from "solid-js/store" +import { useI18n } from "~/context/i18n" + +const PAGE_SIZE = 50 + +async function getUsageInfo(workspaceID: string, page: number) { + "use server" + return withActor(async () => { + return await Billing.usages(page, PAGE_SIZE) + }, workspaceID) +} + +const queryUsageInfo = query(getUsageInfo, "usage.list") + +export function UsageSection() { + const params = useParams() + const i18n = useI18n() + const usage = createAsync(() => queryUsageInfo(params.id!, 0)) + const [store, setStore] = createStore({ page: 0, usage: [] as Awaited> }) + const [openBreakdownId, setOpenBreakdownId] = createSignal(null) + + createEffect(() => { + setStore({ usage: usage() }) + }, [usage]) + + createEffect(() => { + if (!openBreakdownId()) return + + const handleClickOutside = (e: MouseEvent) => { + const target = e.target as HTMLElement + if (!target.closest('[data-slot="tokens-with-breakdown"]')) { + setOpenBreakdownId(null) + } + } + + document.addEventListener("click", handleClickOutside) + return () => document.removeEventListener("click", handleClickOutside) + }) + + const hasResults = createMemo(() => store.usage && store.usage.length > 0) + const canGoPrev = createMemo(() => store.page > 0) + const canGoNext = createMemo(() => store.usage && store.usage.length === PAGE_SIZE) + + const calculateTotalInputTokens = (u: Awaited>[0]) => { + return u.inputTokens + (u.cacheReadTokens ?? 0) + (u.cacheWrite5mTokens ?? 0) + (u.cacheWrite1hTokens ?? 0) + } + + const calculateTotalOutputTokens = (u: Awaited>[0]) => { + return u.outputTokens + (u.reasoningTokens ?? 0) + } + + const goPrev = async () => { + const usage = await getUsageInfo(params.id!, store.page - 1) + setStore({ + page: store.page - 1, + usage, + }) + } + const goNext = async () => { + const usage = await getUsageInfo(params.id!, store.page + 1) + setStore({ + page: store.page + 1, + usage, + }) + } + + return ( +
    +
    +

    {i18n.t("workspace.usage.title")}

    +

    {i18n.t("workspace.usage.subtitle")}

    +
    +
    + +

    {i18n.t("workspace.usage.empty")}

    +
    + } + > + + + + + + + + + + + + + + {(usage, index) => { + const date = createMemo(() => new Date(usage.timeCreated)) + const totalInputTokens = createMemo(() => calculateTotalInputTokens(usage)) + const totalOutputTokens = createMemo(() => calculateTotalOutputTokens(usage)) + const inputBreakdownId = `input-breakdown-${index()}` + const outputBreakdownId = `output-breakdown-${index()}` + const isInputOpen = createMemo(() => openBreakdownId() === inputBreakdownId) + const isOutputOpen = createMemo(() => openBreakdownId() === outputBreakdownId) + const isClaude = usage.model.toLowerCase().includes("claude") + return ( + + + + + + + + + ) + }} + + +
    {i18n.t("workspace.usage.table.date")}{i18n.t("workspace.usage.table.model")}{i18n.t("workspace.usage.table.input")}{i18n.t("workspace.usage.table.output")}{i18n.t("workspace.usage.table.cost")}{i18n.t("workspace.usage.table.session")}
    + {formatDateForTable(date())} + {usage.model} +
    e.stopPropagation()}> + + setOpenBreakdownId(null)}>{totalInputTokens()} + +
    e.stopPropagation()}> +
    + {i18n.t("workspace.usage.breakdown.input")} + {usage.inputTokens} +
    +
    + {i18n.t("workspace.usage.breakdown.cacheRead")} + {usage.cacheReadTokens ?? 0} +
    + +
    + + {i18n.t("workspace.usage.breakdown.cacheWrite")} + + {usage.cacheWrite5mTokens ?? 0} +
    +
    +
    +
    +
    +
    +
    e.stopPropagation()}> + + setOpenBreakdownId(null)}>{totalOutputTokens()} + +
    e.stopPropagation()}> +
    + {i18n.t("workspace.usage.breakdown.output")} + {usage.outputTokens} +
    +
    + {i18n.t("workspace.usage.breakdown.reasoning")} + {usage.reasoningTokens ?? 0} +
    +
    +
    +
    +
    + ${((usage.cost ?? 0) / 100000000).toFixed(4)}}> + + {i18n.t("workspace.usage.subscription", { + amount: ((usage.cost ?? 0) / 100000000).toFixed(4), + })} + + + {i18n.t("workspace.usage.lite", { + amount: ((usage.cost ?? 0) / 100000000).toFixed(4), + })} + + + {i18n.t("workspace.usage.byok", { + amount: ((usage.cost ?? 0) / 100000000).toFixed(4), + })} + + + {usage.sessionID?.slice(-8) ?? "-"}
    + +
    + + +
    +
    + + +
    + ) +} -- cgit v1.2.3 From 75cae81f75ad3058fbace67e0674aef30b9021c7 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 11 Mar 2026 03:12:11 -0400 Subject: zen: add Go page --- packages/console/app/src/i18n/ar.ts | 7 +- packages/console/app/src/i18n/br.ts | 7 +- packages/console/app/src/i18n/da.ts | 7 +- packages/console/app/src/i18n/de.ts | 7 +- packages/console/app/src/i18n/en.ts | 7 +- packages/console/app/src/i18n/es.ts | 7 +- packages/console/app/src/i18n/fr.ts | 7 +- packages/console/app/src/i18n/it.ts | 7 +- packages/console/app/src/i18n/ja.ts | 7 +- packages/console/app/src/i18n/ko.ts | 7 +- packages/console/app/src/i18n/no.ts | 7 +- packages/console/app/src/i18n/pl.ts | 7 +- packages/console/app/src/i18n/ru.ts | 7 +- packages/console/app/src/i18n/th.ts | 7 +- packages/console/app/src/i18n/tr.ts | 7 +- packages/console/app/src/i18n/zh.ts | 7 +- packages/console/app/src/i18n/zht.ts | 7 +- packages/console/app/src/routes/go/index.tsx | 2 +- packages/console/app/src/routes/workspace/[id].tsx | 6 + .../src/routes/workspace/[id]/billing/index.tsx | 4 - .../workspace/[id]/billing/lite-section.module.css | 190 ------------- .../routes/workspace/[id]/billing/lite-section.tsx | 286 -------------------- .../app/src/routes/workspace/[id]/go/index.tsx | 11 + .../workspace/[id]/go/lite-section.module.css | 190 +++++++++++++ .../src/routes/workspace/[id]/go/lite-section.tsx | 296 +++++++++++++++++++++ 25 files changed, 572 insertions(+), 532 deletions(-) delete mode 100644 packages/console/app/src/routes/workspace/[id]/billing/lite-section.module.css delete mode 100644 packages/console/app/src/routes/workspace/[id]/billing/lite-section.tsx create mode 100644 packages/console/app/src/routes/workspace/[id]/go/index.tsx create mode 100644 packages/console/app/src/routes/workspace/[id]/go/lite-section.module.css create mode 100644 packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx diff --git a/packages/console/app/src/i18n/ar.ts b/packages/console/app/src/i18n/ar.ts index a17530920..5a03eea09 100644 --- a/packages/console/app/src/i18n/ar.ts +++ b/packages/console/app/src/i18n/ar.ts @@ -411,6 +411,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "سيتم خصم المبلغ من بطاقتك عند تفعيل اشتراكك", "workspace.nav.zen": "Zen", + "workspace.nav.go": "Go", "workspace.nav.usage": "الاستخدام", "workspace.nav.apiKeys": "مفاتيح API", "workspace.nav.members": "الأعضاء", @@ -617,7 +618,7 @@ export const dict = { "workspace.lite.time.minute": "دقيقة", "workspace.lite.time.minutes": "دقائق", "workspace.lite.time.fewSeconds": "بضع ثوان", - "workspace.lite.subscription.title": "اشتراك Go", + "workspace.lite.title": "OpenCode Go", "workspace.lite.subscription.message": "أنت مشترك في OpenCode Go.", "workspace.lite.subscription.manage": "إدارة الاشتراك", "workspace.lite.subscription.rollingUsage": "الاستخدام المتجدد", @@ -627,10 +628,10 @@ export const dict = { "workspace.lite.subscription.useBalance": "استخدم رصيدك المتوفر بعد الوصول إلى حدود الاستخدام", "workspace.lite.subscription.selectProvider": 'اختر "OpenCode Go" كمزود في إعدادات opencode الخاصة بك لاستخدام نماذج Go.', - "workspace.lite.other.title": "اشتراك Go", + "workspace.lite.black.message": + 'أنت مشترك حاليًا في OpenCode Black أو في قائمة الانتظار. يرجى إلغاء الاشتراك أولاً إذا كنت ترغب في التبديل إلى Go.', "workspace.lite.other.message": "عضو آخر في مساحة العمل هذه مشترك بالفعل في OpenCode Go. يمكن لعضو واحد فقط لكل مساحة عمل الاشتراك.", - "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": "OpenCode Go هو اشتراك بسعر $10 شهريًا يوفر وصولاً موثوقًا إلى نماذج البرمجة المفتوحة الشائعة مع حدود استخدام سخية.", "workspace.lite.promo.modelsTitle": "ما يتضمنه", diff --git a/packages/console/app/src/i18n/br.ts b/packages/console/app/src/i18n/br.ts index f395db9f4..da79d2e66 100644 --- a/packages/console/app/src/i18n/br.ts +++ b/packages/console/app/src/i18n/br.ts @@ -418,6 +418,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "Seu cartão será cobrado quando sua assinatura for ativada", "workspace.nav.zen": "Zen", + "workspace.nav.go": "Go", "workspace.nav.usage": "Uso", "workspace.nav.apiKeys": "Chaves de API", "workspace.nav.members": "Membros", @@ -626,7 +627,7 @@ export const dict = { "workspace.lite.time.minute": "minuto", "workspace.lite.time.minutes": "minutos", "workspace.lite.time.fewSeconds": "alguns segundos", - "workspace.lite.subscription.title": "Assinatura Go", + "workspace.lite.title": "OpenCode Go", "workspace.lite.subscription.message": "Você assina o OpenCode Go.", "workspace.lite.subscription.manage": "Gerenciar Assinatura", "workspace.lite.subscription.rollingUsage": "Uso Contínuo", @@ -636,10 +637,10 @@ export const dict = { "workspace.lite.subscription.useBalance": "Use seu saldo disponível após atingir os limites de uso", "workspace.lite.subscription.selectProvider": 'Selecione "OpenCode Go" como provedor na sua configuração do opencode para usar os modelos Go.', - "workspace.lite.other.title": "Assinatura Go", + "workspace.lite.black.message": + 'Você está atualmente inscrito no OpenCode Black ou na lista de espera. Por favor, cancele a assinatura primeiro se desejar mudar para o Go.', "workspace.lite.other.message": "Outro membro neste workspace já assina o OpenCode Go. Apenas um membro por workspace pode assinar.", - "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": "O OpenCode Go é uma assinatura de $10 por mês que fornece acesso confiável a modelos abertos de codificação populares com limites de uso generosos.", "workspace.lite.promo.modelsTitle": "O que está incluído", diff --git a/packages/console/app/src/i18n/da.ts b/packages/console/app/src/i18n/da.ts index 6e2af12af..5fa9e2b8c 100644 --- a/packages/console/app/src/i18n/da.ts +++ b/packages/console/app/src/i18n/da.ts @@ -414,6 +414,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "Dit kort vil blive debiteret, når dit abonnement er aktiveret", "workspace.nav.zen": "Zen", + "workspace.nav.go": "Go", "workspace.nav.usage": "Brug", "workspace.nav.apiKeys": "API-nøgler", "workspace.nav.members": "Medlemmer", @@ -622,7 +623,7 @@ export const dict = { "workspace.lite.time.minute": "minut", "workspace.lite.time.minutes": "minutter", "workspace.lite.time.fewSeconds": "et par sekunder", - "workspace.lite.subscription.title": "Go-abonnement", + "workspace.lite.title": "OpenCode Go", "workspace.lite.subscription.message": "Du abonnerer på OpenCode Go.", "workspace.lite.subscription.manage": "Administrer abonnement", "workspace.lite.subscription.rollingUsage": "Løbende forbrug", @@ -632,10 +633,10 @@ export const dict = { "workspace.lite.subscription.useBalance": "Brug din tilgængelige saldo, når du har nået forbrugsgrænserne", "workspace.lite.subscription.selectProvider": 'Vælg "OpenCode Go" som udbyder i din opencode-konfiguration for at bruge Go-modeller.', - "workspace.lite.other.title": "Go-abonnement", + "workspace.lite.black.message": + 'Du abonnerer i øjeblikket på OpenCode Black eller er på venteliste. Afmeld venligst først, hvis du vil skifte til Go.', "workspace.lite.other.message": "Et andet medlem i dette workspace abonnerer allerede på OpenCode Go. Kun ét medlem pr. workspace kan abonnere.", - "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": "OpenCode Go er et abonnement til $10 om måneden, der giver pålidelig adgang til populære åbne kodningsmodeller med generøse forbrugsgrænser.", "workspace.lite.promo.modelsTitle": "Hvad er inkluderet", diff --git a/packages/console/app/src/i18n/de.ts b/packages/console/app/src/i18n/de.ts index 87094a28f..29bebc908 100644 --- a/packages/console/app/src/i18n/de.ts +++ b/packages/console/app/src/i18n/de.ts @@ -417,6 +417,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "Deine Karte wird belastet, sobald dein Abonnement aktiviert ist", "workspace.nav.zen": "Zen", + "workspace.nav.go": "Go", "workspace.nav.usage": "Nutzung", "workspace.nav.apiKeys": "API Keys", "workspace.nav.members": "Mitglieder", @@ -625,7 +626,7 @@ export const dict = { "workspace.lite.time.minute": "Minute", "workspace.lite.time.minutes": "Minuten", "workspace.lite.time.fewSeconds": "einige Sekunden", - "workspace.lite.subscription.title": "Go-Abonnement", + "workspace.lite.title": "OpenCode Go", "workspace.lite.subscription.message": "Du hast OpenCode Go abonniert.", "workspace.lite.subscription.manage": "Abo verwalten", "workspace.lite.subscription.rollingUsage": "Fortlaufende Nutzung", @@ -635,10 +636,10 @@ export const dict = { "workspace.lite.subscription.useBalance": "Nutze dein verfügbares Guthaben, nachdem die Nutzungslimits erreicht sind", "workspace.lite.subscription.selectProvider": 'Wähle "OpenCode Go" als Anbieter in deiner opencode-Konfiguration, um Go-Modelle zu verwenden.', - "workspace.lite.other.title": "Go-Abonnement", + "workspace.lite.black.message": + 'Du hast derzeit OpenCode Black abonniert oder stehst auf der Warteliste. Bitte kündige zuerst, wenn du zu Go wechseln möchtest.', "workspace.lite.other.message": "Ein anderes Mitglied in diesem Workspace hat OpenCode Go bereits abonniert. Nur ein Mitglied pro Workspace kann abonnieren.", - "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": "OpenCode Go ist ein Abonnement für $10 pro Monat, das zuverlässigen Zugriff auf beliebte offene Coding-Modelle mit großzügigen Nutzungslimits bietet.", "workspace.lite.promo.modelsTitle": "Was enthalten ist", diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts index 1f1773dd4..dca14bb87 100644 --- a/packages/console/app/src/i18n/en.ts +++ b/packages/console/app/src/i18n/en.ts @@ -411,6 +411,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "Your card will be charged when your subscription is activated", "workspace.nav.zen": "Zen", + "workspace.nav.go": "Go", "workspace.nav.usage": "Usage", "workspace.nav.apiKeys": "API Keys", "workspace.nav.members": "Members", @@ -619,7 +620,7 @@ export const dict = { "workspace.lite.time.minute": "minute", "workspace.lite.time.minutes": "minutes", "workspace.lite.time.fewSeconds": "a few seconds", - "workspace.lite.subscription.title": "Go Subscription", + "workspace.lite.title": "OpenCode Go", "workspace.lite.subscription.message": "You are subscribed to OpenCode Go.", "workspace.lite.subscription.manage": "Manage Subscription", "workspace.lite.subscription.rollingUsage": "Rolling Usage", @@ -629,10 +630,10 @@ export const dict = { "workspace.lite.subscription.useBalance": "Use your available balance after reaching the usage limits", "workspace.lite.subscription.selectProvider": 'Select "OpenCode Go" as the provider in your opencode configuration to use Go models.', - "workspace.lite.other.title": "Go Subscription", + "workspace.lite.black.message": + "You're currently subscribed to OpenCode Black or on the waitlist. Please unsubscribe first if you'd like to switch to Go.", "workspace.lite.other.message": "Another member in this workspace is already subscribed to OpenCode Go. Only one member per workspace can subscribe.", - "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": "OpenCode Go is a $10 per month subscription that provides reliable access to popular open coding models with generous usage limits.", "workspace.lite.promo.modelsTitle": "What's Included", diff --git a/packages/console/app/src/i18n/es.ts b/packages/console/app/src/i18n/es.ts index 19a9842d5..f1a95b2be 100644 --- a/packages/console/app/src/i18n/es.ts +++ b/packages/console/app/src/i18n/es.ts @@ -419,6 +419,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "Tu tarjeta se cargará cuando tu suscripción se active", "workspace.nav.zen": "Zen", + "workspace.nav.go": "Go", "workspace.nav.usage": "Uso", "workspace.nav.apiKeys": "Claves API", "workspace.nav.members": "Miembros", @@ -627,7 +628,7 @@ export const dict = { "workspace.lite.time.minute": "minuto", "workspace.lite.time.minutes": "minutos", "workspace.lite.time.fewSeconds": "unos pocos segundos", - "workspace.lite.subscription.title": "Suscripción Go", + "workspace.lite.title": "OpenCode Go", "workspace.lite.subscription.message": "Estás suscrito a OpenCode Go.", "workspace.lite.subscription.manage": "Gestionar Suscripción", "workspace.lite.subscription.rollingUsage": "Uso Continuo", @@ -637,10 +638,10 @@ export const dict = { "workspace.lite.subscription.useBalance": "Usa tu saldo disponible después de alcanzar los límites de uso", "workspace.lite.subscription.selectProvider": 'Selecciona "OpenCode Go" como proveedor en tu configuración de opencode para usar los modelos Go.', - "workspace.lite.other.title": "Suscripción Go", + "workspace.lite.black.message": + 'Actualmente estás suscrito a OpenCode Black o estás en la lista de espera. Por favor, cancela la suscripción primero si deseas cambiar a Go.', "workspace.lite.other.message": "Otro miembro de este espacio de trabajo ya está suscrito a OpenCode Go. Solo un miembro por espacio de trabajo puede suscribirse.", - "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": "OpenCode Go es una suscripción de $10 al mes que proporciona acceso confiable a modelos de codificación abiertos populares con generosos límites de uso.", "workspace.lite.promo.modelsTitle": "Qué incluye", diff --git a/packages/console/app/src/i18n/fr.ts b/packages/console/app/src/i18n/fr.ts index cf6d5e95b..7e2ff66db 100644 --- a/packages/console/app/src/i18n/fr.ts +++ b/packages/console/app/src/i18n/fr.ts @@ -419,6 +419,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "Votre carte sera débitée lorsque votre abonnement sera activé", "workspace.nav.zen": "Zen", + "workspace.nav.go": "Go", "workspace.nav.usage": "Utilisation", "workspace.nav.apiKeys": "Clés API", "workspace.nav.members": "Membres", @@ -631,7 +632,7 @@ export const dict = { "workspace.lite.time.minute": "minute", "workspace.lite.time.minutes": "minutes", "workspace.lite.time.fewSeconds": "quelques secondes", - "workspace.lite.subscription.title": "Abonnement Go", + "workspace.lite.title": "OpenCode Go", "workspace.lite.subscription.message": "Vous êtes abonné à OpenCode Go.", "workspace.lite.subscription.manage": "Gérer l'abonnement", "workspace.lite.subscription.rollingUsage": "Utilisation glissante", @@ -642,10 +643,10 @@ export const dict = { "Utilisez votre solde disponible après avoir atteint les limites d'utilisation", "workspace.lite.subscription.selectProvider": 'Sélectionnez "OpenCode Go" comme fournisseur dans votre configuration opencode pour utiliser les modèles Go.', - "workspace.lite.other.title": "Abonnement Go", + "workspace.lite.black.message": + "Vous êtes actuellement abonné à OpenCode Black ou sur liste d'attente. Veuillez d'abord vous désabonner si vous souhaitez passer à Go.", "workspace.lite.other.message": "Un autre membre de cet espace de travail est déjà abonné à OpenCode Go. Un seul membre par espace de travail peut s'abonner.", - "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": "OpenCode Go est un abonnement à 10 $ par mois qui offre un accès fiable aux modèles de codage ouverts populaires avec des limites d'utilisation généreuses.", "workspace.lite.promo.modelsTitle": "Ce qui est inclus", diff --git a/packages/console/app/src/i18n/it.ts b/packages/console/app/src/i18n/it.ts index b967f078c..c579a4863 100644 --- a/packages/console/app/src/i18n/it.ts +++ b/packages/console/app/src/i18n/it.ts @@ -417,6 +417,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "La tua carta verrà addebitata quando il tuo abbonamento sarà attivato", "workspace.nav.zen": "Zen", + "workspace.nav.go": "Go", "workspace.nav.usage": "Utilizzo", "workspace.nav.apiKeys": "Chiavi API", "workspace.nav.members": "Membri", @@ -625,7 +626,7 @@ export const dict = { "workspace.lite.time.minute": "minuto", "workspace.lite.time.minutes": "minuti", "workspace.lite.time.fewSeconds": "pochi secondi", - "workspace.lite.subscription.title": "Abbonamento Go", + "workspace.lite.title": "OpenCode Go", "workspace.lite.subscription.message": "Sei abbonato a OpenCode Go.", "workspace.lite.subscription.manage": "Gestisci Abbonamento", "workspace.lite.subscription.rollingUsage": "Utilizzo Continuativo", @@ -635,10 +636,10 @@ export const dict = { "workspace.lite.subscription.useBalance": "Usa il tuo saldo disponibile dopo aver raggiunto i limiti di utilizzo", "workspace.lite.subscription.selectProvider": 'Seleziona "OpenCode Go" come provider nella tua configurazione opencode per utilizzare i modelli Go.', - "workspace.lite.other.title": "Abbonamento Go", + "workspace.lite.black.message": + "Attualmente sei abbonato a OpenCode Black o sei in lista d'attesa. Annulla l'iscrizione prima se desideri passare a Go.", "workspace.lite.other.message": "Un altro membro in questo workspace è già abbonato a OpenCode Go. Solo un membro per workspace può abbonarsi.", - "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": "OpenCode Go è un abbonamento a $10 al mese che fornisce un accesso affidabile a popolari modelli di coding aperti con generosi limiti di utilizzo.", "workspace.lite.promo.modelsTitle": "Cosa è incluso", diff --git a/packages/console/app/src/i18n/ja.ts b/packages/console/app/src/i18n/ja.ts index 759d5e7c7..020f68005 100644 --- a/packages/console/app/src/i18n/ja.ts +++ b/packages/console/app/src/i18n/ja.ts @@ -416,6 +416,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "サブスクリプションが有効化された時点でカードに請求されます", "workspace.nav.zen": "Zen", + "workspace.nav.go": "Go", "workspace.nav.usage": "利用", "workspace.nav.apiKeys": "APIキー", "workspace.nav.members": "メンバー", @@ -625,7 +626,7 @@ export const dict = { "workspace.lite.time.minute": "分", "workspace.lite.time.minutes": "分", "workspace.lite.time.fewSeconds": "数秒", - "workspace.lite.subscription.title": "Goサブスクリプション", + "workspace.lite.title": "OpenCode Go", "workspace.lite.subscription.message": "あなたは OpenCode Go を購読しています。", "workspace.lite.subscription.manage": "サブスクリプションの管理", "workspace.lite.subscription.rollingUsage": "ローリング利用量", @@ -635,10 +636,10 @@ export const dict = { "workspace.lite.subscription.useBalance": "利用限度額に達したら利用可能な残高を使用する", "workspace.lite.subscription.selectProvider": "Go モデルを使用するには、opencode の設定で「OpenCode Go」をプロバイダーとして選択してください。", - "workspace.lite.other.title": "Goサブスクリプション", + "workspace.lite.black.message": + '現在 OpenCode Black を購読中、またはウェイティングリストに登録されています。Go に切り替える場合は、先に登録を解除してください。', "workspace.lite.other.message": "このワークスペースの別のメンバーが既に OpenCode Go を購読しています。ワークスペースにつき1人のメンバーのみが購読できます。", - "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": "OpenCode Goは月額$10のサブスクリプションプランで、人気のオープンコーディングモデルへの安定したアクセスを十分な利用枠で提供します。", "workspace.lite.promo.modelsTitle": "含まれるもの", diff --git a/packages/console/app/src/i18n/ko.ts b/packages/console/app/src/i18n/ko.ts index 9a5237f33..b5c6efc44 100644 --- a/packages/console/app/src/i18n/ko.ts +++ b/packages/console/app/src/i18n/ko.ts @@ -410,6 +410,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "구독이 활성화되면 카드에 청구됩니다", "workspace.nav.zen": "Zen", + "workspace.nav.go": "Go", "workspace.nav.usage": "사용량", "workspace.nav.apiKeys": "API 키", "workspace.nav.members": "멤버", @@ -617,7 +618,7 @@ export const dict = { "workspace.lite.time.minute": "분", "workspace.lite.time.minutes": "분", "workspace.lite.time.fewSeconds": "몇 초", - "workspace.lite.subscription.title": "Go 구독", + "workspace.lite.title": "OpenCode Go", "workspace.lite.subscription.message": "현재 OpenCode Go를 구독 중입니다.", "workspace.lite.subscription.manage": "구독 관리", "workspace.lite.subscription.rollingUsage": "롤링 사용량", @@ -627,10 +628,10 @@ export const dict = { "workspace.lite.subscription.useBalance": "사용 한도 도달 후에는 보유 잔액 사용", "workspace.lite.subscription.selectProvider": 'Go 모델을 사용하려면 opencode 설정에서 "OpenCode Go"를 공급자로 선택하세요.', - "workspace.lite.other.title": "Go 구독", + "workspace.lite.black.message": + '현재 OpenCode Black을 구독 중이거나 대기 명단에 등록되어 있습니다. Go로 전환하려면 먼저 구독을 취소해 주세요.', "workspace.lite.other.message": "이 워크스페이스의 다른 멤버가 이미 OpenCode Go를 구독 중입니다. 워크스페이스당 한 명의 멤버만 구독할 수 있습니다.", - "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": "OpenCode Go는 넉넉한 사용 한도와 함께 인기 있는 오픈 코딩 모델에 대한 안정적인 액세스를 제공하는 월 $10의 구독입니다.", "workspace.lite.promo.modelsTitle": "포함 내역", diff --git a/packages/console/app/src/i18n/no.ts b/packages/console/app/src/i18n/no.ts index fa44e535e..31dc8ee10 100644 --- a/packages/console/app/src/i18n/no.ts +++ b/packages/console/app/src/i18n/no.ts @@ -415,6 +415,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "Kortet ditt vil bli belastet når abonnementet aktiveres", "workspace.nav.zen": "Zen", + "workspace.nav.go": "Go", "workspace.nav.usage": "Bruk", "workspace.nav.apiKeys": "API-nøkler", "workspace.nav.members": "Medlemmer", @@ -623,7 +624,7 @@ export const dict = { "workspace.lite.time.minute": "minutt", "workspace.lite.time.minutes": "minutter", "workspace.lite.time.fewSeconds": "noen få sekunder", - "workspace.lite.subscription.title": "Go-abonnement", + "workspace.lite.title": "OpenCode Go", "workspace.lite.subscription.message": "Du abonnerer på OpenCode Go.", "workspace.lite.subscription.manage": "Administrer abonnement", "workspace.lite.subscription.rollingUsage": "Løpende bruk", @@ -633,10 +634,10 @@ export const dict = { "workspace.lite.subscription.useBalance": "Bruk din tilgjengelige saldo etter å ha nådd bruksgrensene", "workspace.lite.subscription.selectProvider": 'Velg "OpenCode Go" som leverandør i opencode-konfigurasjonen din for å bruke Go-modeller.', - "workspace.lite.other.title": "Go-abonnement", + "workspace.lite.black.message": + 'Du abonnerer for øyeblikket på OpenCode Black eller står på venteliste. Vennligst avslutt abonnementet først hvis du vil bytte til Go.', "workspace.lite.other.message": "Et annet medlem i dette arbeidsområdet abonnerer allerede på OpenCode Go. Kun ett medlem per arbeidsområde kan abonnere.", - "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": "OpenCode Go er et abonnement til $10 per måned som gir pålitelig tilgang til populære åpne kodemodeller med rause bruksgrenser.", "workspace.lite.promo.modelsTitle": "Hva som er inkludert", diff --git a/packages/console/app/src/i18n/pl.ts b/packages/console/app/src/i18n/pl.ts index 7c7300ab9..dde32158a 100644 --- a/packages/console/app/src/i18n/pl.ts +++ b/packages/console/app/src/i18n/pl.ts @@ -416,6 +416,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "Twoja karta zostanie obciążona po aktywacji subskrypcji", "workspace.nav.zen": "Zen", + "workspace.nav.go": "Go", "workspace.nav.usage": "Użycie", "workspace.nav.apiKeys": "Klucze API", "workspace.nav.members": "Członkowie", @@ -624,7 +625,7 @@ export const dict = { "workspace.lite.time.minute": "minuta", "workspace.lite.time.minutes": "minut(y)", "workspace.lite.time.fewSeconds": "kilka sekund", - "workspace.lite.subscription.title": "Subskrypcja Go", + "workspace.lite.title": "OpenCode Go", "workspace.lite.subscription.message": "Subskrybujesz OpenCode Go.", "workspace.lite.subscription.manage": "Zarządzaj subskrypcją", "workspace.lite.subscription.rollingUsage": "Użycie kroczące", @@ -634,10 +635,10 @@ export const dict = { "workspace.lite.subscription.useBalance": "Użyj dostępnego salda po osiągnięciu limitów użycia", "workspace.lite.subscription.selectProvider": 'Wybierz "OpenCode Go" jako dostawcę w konfiguracji opencode, aby używać modeli Go.', - "workspace.lite.other.title": "Subskrypcja Go", + "workspace.lite.black.message": + 'Obecnie subskrybujesz OpenCode Black lub jesteś na liście oczekujących. Jeśli chcesz przejść na Go, najpierw anuluj subskrypcję.', "workspace.lite.other.message": "Inny członek tego obszaru roboczego już subskrybuje OpenCode Go. Tylko jeden członek na obszar roboczy może subskrybować.", - "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": "OpenCode Go to subskrypcja za $10 miesięcznie, która zapewnia niezawodny dostęp do popularnych otwartych modeli do kodowania z hojnymi limitami użycia.", "workspace.lite.promo.modelsTitle": "Co zawiera", diff --git a/packages/console/app/src/i18n/ru.ts b/packages/console/app/src/i18n/ru.ts index 77e36bc99..4a84e91cc 100644 --- a/packages/console/app/src/i18n/ru.ts +++ b/packages/console/app/src/i18n/ru.ts @@ -421,6 +421,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "С вашей карты будет списана оплата при активации подписки", "workspace.nav.zen": "Zen", + "workspace.nav.go": "Go", "workspace.nav.usage": "Использование", "workspace.nav.apiKeys": "API Ключи", "workspace.nav.members": "Участники", @@ -630,7 +631,7 @@ export const dict = { "workspace.lite.time.minute": "минута", "workspace.lite.time.minutes": "минут", "workspace.lite.time.fewSeconds": "несколько секунд", - "workspace.lite.subscription.title": "Подписка Go", + "workspace.lite.title": "OpenCode Go", "workspace.lite.subscription.message": "Вы подписаны на OpenCode Go.", "workspace.lite.subscription.manage": "Управление подпиской", "workspace.lite.subscription.rollingUsage": "Скользящее использование", @@ -640,10 +641,10 @@ export const dict = { "workspace.lite.subscription.useBalance": "Использовать доступный баланс после достижения лимитов", "workspace.lite.subscription.selectProvider": 'Выберите "OpenCode Go" в качестве провайдера в настройках opencode для использования моделей Go.', - "workspace.lite.other.title": "Подписка Go", + "workspace.lite.black.message": + 'Вы подписаны на OpenCode Black или находитесь в списке ожидания. Пожалуйста, сначала отмените подписку, если хотите перейти на Go.', "workspace.lite.other.message": "Другой участник в этом рабочем пространстве уже подписан на OpenCode Go. Только один участник в рабочем пространстве может оформить подписку.", - "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": "OpenCode Go — это подписка за $10 в месяц, которая предоставляет надежный доступ к популярным открытым моделям для кодинга с щедрыми лимитами использования.", "workspace.lite.promo.modelsTitle": "Что включено", diff --git a/packages/console/app/src/i18n/th.ts b/packages/console/app/src/i18n/th.ts index 9d1e92fa8..aabfea257 100644 --- a/packages/console/app/src/i18n/th.ts +++ b/packages/console/app/src/i18n/th.ts @@ -413,6 +413,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "บัตรของคุณจะถูกเรียกเก็บเงินเมื่อการสมัครสมาชิกของคุณถูกเปิดใช้งาน", "workspace.nav.zen": "Zen", + "workspace.nav.go": "Go", "workspace.nav.usage": "การใช้งาน", "workspace.nav.apiKeys": "API Keys", "workspace.nav.members": "สมาชิก", @@ -621,7 +622,7 @@ export const dict = { "workspace.lite.time.minute": "นาที", "workspace.lite.time.minutes": "นาที", "workspace.lite.time.fewSeconds": "ไม่กี่วินาที", - "workspace.lite.subscription.title": "การสมัครสมาชิก Go", + "workspace.lite.title": "OpenCode Go", "workspace.lite.subscription.message": "คุณได้สมัครสมาชิก OpenCode Go แล้ว", "workspace.lite.subscription.manage": "จัดการการสมัครสมาชิก", "workspace.lite.subscription.rollingUsage": "การใช้งานแบบหมุนเวียน", @@ -631,10 +632,10 @@ export const dict = { "workspace.lite.subscription.useBalance": "ใช้ยอดคงเหลือของคุณหลังจากถึงขีดจำกัดการใช้งาน", "workspace.lite.subscription.selectProvider": 'เลือก "OpenCode Go" เป็นผู้ให้บริการในการตั้งค่า opencode ของคุณเพื่อใช้โมเดล Go', - "workspace.lite.other.title": "การสมัครสมาชิก Go", + "workspace.lite.black.message": + 'ขณะนี้คุณสมัครสมาชิก OpenCode Black หรืออยู่ในรายการรอ โปรดยกเลิกการสมัครก่อนหากต้องการเปลี่ยนไปใช้ Go', "workspace.lite.other.message": "สมาชิกคนอื่นใน Workspace นี้ได้สมัคร OpenCode Go แล้ว สามารถสมัครได้เพียงหนึ่งคนต่อหนึ่ง Workspace เท่านั้น", - "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": "OpenCode Go เป็นการสมัครสมาชิกราคา 10 ดอลลาร์ต่อเดือน ที่ให้การเข้าถึงโมเดลโอเพนโค้ดดิงยอดนิยมได้อย่างเสถียร ด้วยขีดจำกัดการใช้งานที่ครอบคลุม", "workspace.lite.promo.modelsTitle": "สิ่งที่รวมอยู่ด้วย", diff --git a/packages/console/app/src/i18n/tr.ts b/packages/console/app/src/i18n/tr.ts index 84ad9a7e3..6d6e414d1 100644 --- a/packages/console/app/src/i18n/tr.ts +++ b/packages/console/app/src/i18n/tr.ts @@ -418,6 +418,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "Aboneliğiniz aktive edildiğinde kartınızdan ödeme alınacaktır", "workspace.nav.zen": "Zen", + "workspace.nav.go": "Go", "workspace.nav.usage": "Kullanım", "workspace.nav.apiKeys": "API Anahtarları", "workspace.nav.members": "Üyeler", @@ -626,7 +627,7 @@ export const dict = { "workspace.lite.time.minute": "dakika", "workspace.lite.time.minutes": "dakika", "workspace.lite.time.fewSeconds": "birkaç saniye", - "workspace.lite.subscription.title": "Go Aboneliği", + "workspace.lite.title": "OpenCode Go", "workspace.lite.subscription.message": "OpenCode Go abonesisiniz.", "workspace.lite.subscription.manage": "Aboneliği Yönet", "workspace.lite.subscription.rollingUsage": "Devam Eden Kullanım", @@ -636,10 +637,10 @@ export const dict = { "workspace.lite.subscription.useBalance": "Kullanım limitlerine ulaştıktan sonra mevcut bakiyenizi kullanın", "workspace.lite.subscription.selectProvider": 'Go modellerini kullanmak için opencode yapılandırmanızda "OpenCode Go"\'yu sağlayıcı olarak seçin.', - "workspace.lite.other.title": "Go Aboneliği", + "workspace.lite.black.message": + "Şu anda OpenCode Black abonesisiniz veya bekleme listesindesiniz. Go'ya geçmek istiyorsanız lütfen önce aboneliğinizi iptal edin.", "workspace.lite.other.message": "Bu çalışma alanındaki başka bir üye zaten OpenCode Go abonesi. Çalışma alanı başına yalnızca bir üye abone olabilir.", - "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": "OpenCode Go, cömert kullanım limitleriyle popüler açık kodlama modellerine güvenilir erişim sağlayan aylık 10$'lık bir aboneliktir.", "workspace.lite.promo.modelsTitle": "Neler Dahil", diff --git a/packages/console/app/src/i18n/zh.ts b/packages/console/app/src/i18n/zh.ts index a3527ae3e..ccb3a554d 100644 --- a/packages/console/app/src/i18n/zh.ts +++ b/packages/console/app/src/i18n/zh.ts @@ -396,6 +396,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "您的卡将在订阅激活时扣费", "workspace.nav.zen": "Zen", + "workspace.nav.go": "Go", "workspace.nav.usage": "使用量", "workspace.nav.apiKeys": "API 密钥", "workspace.nav.members": "成员", @@ -601,7 +602,7 @@ export const dict = { "workspace.lite.time.minute": "分钟", "workspace.lite.time.minutes": "分钟", "workspace.lite.time.fewSeconds": "几秒钟", - "workspace.lite.subscription.title": "Go 订阅", + "workspace.lite.title": "OpenCode Go", "workspace.lite.subscription.message": "您已订阅 OpenCode Go。", "workspace.lite.subscription.manage": "管理订阅", "workspace.lite.subscription.rollingUsage": "滚动用量", @@ -611,9 +612,9 @@ export const dict = { "workspace.lite.subscription.useBalance": "达到使用限额后使用您的可用余额", "workspace.lite.subscription.selectProvider": "在你的 opencode 配置中选择「OpenCode Go」作为提供商,即可使用 Go 模型。", - "workspace.lite.other.title": "Go 订阅", + "workspace.lite.black.message": + '您当前已订阅 OpenCode Black 或在候补名单中。如需切换到 Go,请先取消订阅。', "workspace.lite.other.message": "此工作区中的另一位成员已经订阅了 OpenCode Go。每个工作区只有一名成员可以订阅。", - "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": "OpenCode Go 是一个每月 $10 的订阅计划,提供对主流开源编码模型的稳定访问,并配备充足的使用额度。", "workspace.lite.promo.modelsTitle": "包含模型", diff --git a/packages/console/app/src/i18n/zht.ts b/packages/console/app/src/i18n/zht.ts index 1167d5a6c..bd12783e2 100644 --- a/packages/console/app/src/i18n/zht.ts +++ b/packages/console/app/src/i18n/zht.ts @@ -397,6 +397,7 @@ export const dict = { "black.subscribe.success.chargeNotice": "你的卡片將在訂閱啟用時扣款", "workspace.nav.zen": "Zen", + "workspace.nav.go": "Go", "workspace.nav.usage": "使用量", "workspace.nav.apiKeys": "API 金鑰", "workspace.nav.members": "成員", @@ -602,7 +603,7 @@ export const dict = { "workspace.lite.time.minute": "分鐘", "workspace.lite.time.minutes": "分鐘", "workspace.lite.time.fewSeconds": "幾秒", - "workspace.lite.subscription.title": "Go 訂閱", + "workspace.lite.title": "OpenCode Go", "workspace.lite.subscription.message": "您已訂閱 OpenCode Go。", "workspace.lite.subscription.manage": "管理訂閱", "workspace.lite.subscription.rollingUsage": "滾動使用量", @@ -612,9 +613,9 @@ export const dict = { "workspace.lite.subscription.useBalance": "達到使用限制後使用您的可用餘額", "workspace.lite.subscription.selectProvider": "在您的 opencode 設定中選擇「OpenCode Go」作為提供商,即可使用 Go 模型。", - "workspace.lite.other.title": "Go 訂閱", + "workspace.lite.black.message": + '您目前已訂閱 OpenCode Black 或在候補名單中。若要切換至 Go,請先取消訂閱。', "workspace.lite.other.message": "此工作區中的另一位成員已訂閱 OpenCode Go。每個工作區只能有一位成員訂閱。", - "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": "OpenCode Go 是一個每月 $10 的訂閱方案,提供對主流開放原始碼編碼模型的穩定存取,並配備充足的使用額度。", "workspace.lite.promo.modelsTitle": "包含模型", diff --git a/packages/console/app/src/routes/go/index.tsx b/packages/console/app/src/routes/go/index.tsx index dcd909ab4..8f1930bd7 100644 --- a/packages/console/app/src/routes/go/index.tsx +++ b/packages/console/app/src/routes/go/index.tsx @@ -205,7 +205,7 @@ function LimitsGraph(props: { href: string }) { export default function Home() { const workspaceID = createAsync(() => checkLoggedIn()) - const subscribeUrl = createMemo(() => (workspaceID() ? `/workspace/${workspaceID()}/billing` : "/auth")) + const subscribeUrl = createMemo(() => (workspaceID() ? `/workspace/${workspaceID()}/go` : "/auth")) const i18n = useI18n() const language = useLanguage() return ( diff --git a/packages/console/app/src/routes/workspace/[id].tsx b/packages/console/app/src/routes/workspace/[id].tsx index a36368fd0..7a8e1616f 100644 --- a/packages/console/app/src/routes/workspace/[id].tsx +++ b/packages/console/app/src/routes/workspace/[id].tsx @@ -19,6 +19,9 @@ export default function WorkspaceLayout(props: RouteSectionProps) { {i18n.t("workspace.nav.zen")} + + {i18n.t("workspace.nav.go")} + {i18n.t("workspace.nav.usage")} @@ -44,6 +47,9 @@ export default function WorkspaceLayout(props: RouteSectionProps) { {i18n.t("workspace.nav.zen")} + + {i18n.t("workspace.nav.go")} + {i18n.t("workspace.nav.usage")} diff --git a/packages/console/app/src/routes/workspace/[id]/billing/index.tsx b/packages/console/app/src/routes/workspace/[id]/billing/index.tsx index e039a09ef..4a7dc2488 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/index.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/index.tsx @@ -3,7 +3,6 @@ import { BillingSection } from "./billing-section" import { ReloadSection } from "./reload-section" import { PaymentSection } from "./payment-section" import { BlackSection } from "./black-section" -import { LiteSection } from "./lite-section" import { createMemo, Show } from "solid-js" import { createAsync, useParams } from "@solidjs/router" import { queryBillingInfo, querySessionInfo } from "../../common" @@ -21,9 +20,6 @@ export default function () { - - - diff --git a/packages/console/app/src/routes/workspace/[id]/billing/lite-section.module.css b/packages/console/app/src/routes/workspace/[id]/billing/lite-section.module.css deleted file mode 100644 index 76d9bcfb0..000000000 --- a/packages/console/app/src/routes/workspace/[id]/billing/lite-section.module.css +++ /dev/null @@ -1,190 +0,0 @@ -.root { - [data-slot="title-row"] { - display: flex; - justify-content: space-between; - align-items: center; - gap: var(--space-4); - } - - [data-slot="usage"] { - display: flex; - gap: var(--space-6); - margin-top: var(--space-4); - - @media (max-width: 40rem) { - flex-direction: column; - gap: var(--space-4); - } - } - - [data-slot="usage-item"] { - flex: 1; - display: flex; - flex-direction: column; - gap: var(--space-2); - } - - [data-slot="usage-header"] { - display: flex; - justify-content: space-between; - align-items: baseline; - } - - [data-slot="usage-label"] { - font-size: var(--font-size-md); - font-weight: 500; - color: var(--color-text); - } - - [data-slot="usage-value"] { - font-size: var(--font-size-sm); - color: var(--color-text-muted); - } - - [data-slot="progress"] { - height: 8px; - background-color: var(--color-bg-surface); - border-radius: var(--border-radius-sm); - overflow: hidden; - } - - [data-slot="progress-bar"] { - height: 100%; - background-color: var(--color-accent); - border-radius: var(--border-radius-sm); - transition: width 0.3s ease; - } - - [data-slot="reset-time"] { - font-size: var(--font-size-sm); - color: var(--color-text-muted); - } - - [data-slot="setting-row"] { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--space-3); - margin-top: var(--space-4); - - p { - font-size: var(--font-size-sm); - line-height: 1.5; - color: var(--color-text-secondary); - margin: 0; - } - } - - [data-slot="toggle-label"] { - position: relative; - display: inline-block; - width: 2.5rem; - height: 1.5rem; - cursor: pointer; - flex-shrink: 0; - - input { - opacity: 0; - width: 0; - height: 0; - } - - span { - position: absolute; - inset: 0; - background-color: #ccc; - border: 1px solid #bbb; - border-radius: 1.5rem; - transition: all 0.3s ease; - cursor: pointer; - - &::before { - content: ""; - position: absolute; - top: 50%; - left: 0.125rem; - width: 1.25rem; - height: 1.25rem; - background-color: white; - border: 1px solid #ddd; - border-radius: 50%; - transform: translateY(-50%); - transition: all 0.3s ease; - } - } - - input:checked + span { - background-color: #21ad0e; - border-color: #148605; - - &::before { - transform: translateX(1rem) translateY(-50%); - } - } - - &:hover span { - box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2); - } - - input:checked:hover + span { - box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.3); - } - - &:has(input:disabled) { - cursor: not-allowed; - } - - input:disabled + span { - opacity: 0.5; - cursor: not-allowed; - } - } - - [data-slot="beta-notice"] { - padding: var(--space-3) var(--space-4); - border: 1px solid var(--color-border); - border-radius: var(--border-radius-sm); - background-color: var(--color-bg-surface); - font-size: var(--font-size-sm); - color: var(--color-text-secondary); - line-height: 1.5; - margin-top: var(--space-3); - - a { - color: var(--color-accent); - text-decoration: none; - } - } - - [data-slot="other-message"] { - font-size: var(--font-size-sm); - color: var(--color-text-muted); - line-height: 1.5; - } - - [data-slot="promo-description"] { - font-size: var(--font-size-md); - color: var(--color-text-secondary); - line-height: 1.5; - margin-top: var(--space-2); - } - - [data-slot="promo-models-title"] { - font-size: var(--font-size-md); - font-weight: 600; - margin-top: var(--space-4); - } - - [data-slot="promo-models"] { - margin: var(--space-2) 0 0 var(--space-4); - padding: 0; - font-size: var(--font-size-md); - color: var(--color-text-secondary); - line-height: 1.4; - } - - [data-slot="subscribe-button"] { - align-self: flex-start; - margin-top: var(--space-4); - } -} diff --git a/packages/console/app/src/routes/workspace/[id]/billing/lite-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/lite-section.tsx deleted file mode 100644 index f67775d79..000000000 --- a/packages/console/app/src/routes/workspace/[id]/billing/lite-section.tsx +++ /dev/null @@ -1,286 +0,0 @@ -import { action, useParams, useAction, useSubmission, json, query, createAsync } from "@solidjs/router" -import { createStore } from "solid-js/store" -import { Show } from "solid-js" -import { Billing } from "@opencode-ai/console-core/billing.js" -import { Database, eq, and, isNull } from "@opencode-ai/console-core/drizzle/index.js" -import { BillingTable, LiteTable } from "@opencode-ai/console-core/schema/billing.sql.js" -import { Actor } from "@opencode-ai/console-core/actor.js" -import { Subscription } from "@opencode-ai/console-core/subscription.js" -import { LiteData } from "@opencode-ai/console-core/lite.js" -import { withActor } from "~/context/auth.withActor" -import { queryBillingInfo } from "../../common" -import styles from "./lite-section.module.css" -import { useI18n } from "~/context/i18n" -import { useLanguage } from "~/context/language" -import { formError } from "~/lib/form-error" - -const queryLiteSubscription = query(async (workspaceID: string) => { - "use server" - return withActor(async () => { - const row = await Database.use((tx) => - tx - .select({ - userID: LiteTable.userID, - rollingUsage: LiteTable.rollingUsage, - weeklyUsage: LiteTable.weeklyUsage, - monthlyUsage: LiteTable.monthlyUsage, - timeRollingUpdated: LiteTable.timeRollingUpdated, - timeWeeklyUpdated: LiteTable.timeWeeklyUpdated, - timeMonthlyUpdated: LiteTable.timeMonthlyUpdated, - timeCreated: LiteTable.timeCreated, - lite: BillingTable.lite, - }) - .from(BillingTable) - .innerJoin(LiteTable, eq(LiteTable.workspaceID, BillingTable.workspaceID)) - .where(and(eq(LiteTable.workspaceID, Actor.workspace()), isNull(LiteTable.timeDeleted))) - .then((r) => r[0]), - ) - if (!row) return null - - const limits = LiteData.getLimits() - const mine = row.userID === Actor.userID() - - return { - mine, - useBalance: row.lite?.useBalance ?? false, - rollingUsage: Subscription.analyzeRollingUsage({ - limit: limits.rollingLimit, - window: limits.rollingWindow, - usage: row.rollingUsage ?? 0, - timeUpdated: row.timeRollingUpdated ?? new Date(), - }), - weeklyUsage: Subscription.analyzeWeeklyUsage({ - limit: limits.weeklyLimit, - usage: row.weeklyUsage ?? 0, - timeUpdated: row.timeWeeklyUpdated ?? new Date(), - }), - monthlyUsage: Subscription.analyzeMonthlyUsage({ - limit: limits.monthlyLimit, - usage: row.monthlyUsage ?? 0, - timeUpdated: row.timeMonthlyUpdated ?? new Date(), - timeSubscribed: row.timeCreated, - }), - } - }, workspaceID) -}, "lite.subscription.get") - -function formatResetTime(seconds: number, i18n: ReturnType) { - const days = Math.floor(seconds / 86400) - if (days >= 1) { - const hours = Math.floor((seconds % 86400) / 3600) - return `${days} ${days === 1 ? i18n.t("workspace.lite.time.day") : i18n.t("workspace.lite.time.days")} ${hours} ${hours === 1 ? i18n.t("workspace.lite.time.hour") : i18n.t("workspace.lite.time.hours")}` - } - const hours = Math.floor(seconds / 3600) - const minutes = Math.floor((seconds % 3600) / 60) - if (hours >= 1) - return `${hours} ${hours === 1 ? i18n.t("workspace.lite.time.hour") : i18n.t("workspace.lite.time.hours")} ${minutes} ${minutes === 1 ? i18n.t("workspace.lite.time.minute") : i18n.t("workspace.lite.time.minutes")}` - if (minutes === 0) return i18n.t("workspace.lite.time.fewSeconds") - return `${minutes} ${minutes === 1 ? i18n.t("workspace.lite.time.minute") : i18n.t("workspace.lite.time.minutes")}` -} - -const createLiteCheckoutUrl = action(async (workspaceID: string, successUrl: string, cancelUrl: string) => { - "use server" - return json( - await withActor( - () => - Billing.generateLiteCheckoutUrl({ successUrl, cancelUrl }) - .then((data) => ({ error: undefined, data })) - .catch((e) => ({ - error: e.message as string, - data: undefined, - })), - workspaceID, - ), - { revalidate: [queryBillingInfo.key, queryLiteSubscription.key] }, - ) -}, "liteCheckoutUrl") - -const createSessionUrl = action(async (workspaceID: string, returnUrl: string) => { - "use server" - return json( - await withActor( - () => - Billing.generateSessionUrl({ returnUrl }) - .then((data) => ({ error: undefined, data })) - .catch((e) => ({ - error: e.message as string, - data: undefined, - })), - workspaceID, - ), - { revalidate: [queryBillingInfo.key, queryLiteSubscription.key] }, - ) -}, "liteSessionUrl") - -const setLiteUseBalance = action(async (form: FormData) => { - "use server" - const workspaceID = form.get("workspaceID")?.toString() - if (!workspaceID) return { error: formError.workspaceRequired } - const useBalance = form.get("useBalance")?.toString() === "true" - - return json( - await withActor(async () => { - await Database.use((tx) => - tx - .update(BillingTable) - .set({ - lite: useBalance ? { useBalance: true } : {}, - }) - .where(eq(BillingTable.workspaceID, workspaceID)), - ) - return { error: undefined } - }, workspaceID).catch((e) => ({ error: e.message as string })), - { revalidate: [queryBillingInfo.key, queryLiteSubscription.key] }, - ) -}, "setLiteUseBalance") - -export function LiteSection() { - const params = useParams() - const i18n = useI18n() - const language = useLanguage() - const lite = createAsync(() => queryLiteSubscription(params.id!)) - const sessionAction = useAction(createSessionUrl) - const sessionSubmission = useSubmission(createSessionUrl) - const checkoutAction = useAction(createLiteCheckoutUrl) - const checkoutSubmission = useSubmission(createLiteCheckoutUrl) - const useBalanceSubmission = useSubmission(setLiteUseBalance) - const [store, setStore] = createStore({ - redirecting: false, - }) - - async function onClickSession() { - const result = await sessionAction(params.id!, window.location.href) - if (result.data) { - setStore("redirecting", true) - window.location.href = result.data - } - } - - async function onClickSubscribe() { - const result = await checkoutAction(params.id!, window.location.href, window.location.href) - if (result.data) { - setStore("redirecting", true) - window.location.href = result.data - } - } - - return ( - <> - - {(sub) => ( -
    -
    -

    {i18n.t("workspace.lite.subscription.title")}

    -
    -

    {i18n.t("workspace.lite.subscription.message")}

    - -
    -
    -
    - {i18n.t("workspace.lite.subscription.selectProvider")}{" "} - - {i18n.t("common.learnMore")} - - . -
    -
    -
    -
    - {i18n.t("workspace.lite.subscription.rollingUsage")} - {sub().rollingUsage.usagePercent}% -
    -
    -
    -
    - - {i18n.t("workspace.lite.subscription.resetsIn")}{" "} - {formatResetTime(sub().rollingUsage.resetInSec, i18n)} - -
    -
    -
    - {i18n.t("workspace.lite.subscription.weeklyUsage")} - {sub().weeklyUsage.usagePercent}% -
    -
    -
    -
    - - {i18n.t("workspace.lite.subscription.resetsIn")} {formatResetTime(sub().weeklyUsage.resetInSec, i18n)} - -
    -
    -
    - {i18n.t("workspace.lite.subscription.monthlyUsage")} - {sub().monthlyUsage.usagePercent}% -
    -
    -
    -
    - - {i18n.t("workspace.lite.subscription.resetsIn")}{" "} - {formatResetTime(sub().monthlyUsage.resetInSec, i18n)} - -
    -
    -
    -

    {i18n.t("workspace.lite.subscription.useBalance")}

    - - - -
    -
    - )} -
    - -
    -
    -

    {i18n.t("workspace.lite.other.title")}

    -
    -

    {i18n.t("workspace.lite.other.message")}

    -
    -
    - -
    -
    -

    {i18n.t("workspace.lite.promo.title")}

    -
    -

    {i18n.t("workspace.lite.promo.description")}

    -

    {i18n.t("workspace.lite.promo.modelsTitle")}

    -
      -
    • Kimi K2.5
    • -
    • GLM-5
    • -
    • MiniMax M2.5
    • -
    -

    {i18n.t("workspace.lite.promo.footer")}

    - -
    -
    - - ) -} diff --git a/packages/console/app/src/routes/workspace/[id]/go/index.tsx b/packages/console/app/src/routes/workspace/[id]/go/index.tsx new file mode 100644 index 000000000..116938a12 --- /dev/null +++ b/packages/console/app/src/routes/workspace/[id]/go/index.tsx @@ -0,0 +1,11 @@ +import { LiteSection } from "./lite-section" + +export default function () { + return ( +
    +
    + +
    +
    + ) +} diff --git a/packages/console/app/src/routes/workspace/[id]/go/lite-section.module.css b/packages/console/app/src/routes/workspace/[id]/go/lite-section.module.css new file mode 100644 index 000000000..76d9bcfb0 --- /dev/null +++ b/packages/console/app/src/routes/workspace/[id]/go/lite-section.module.css @@ -0,0 +1,190 @@ +.root { + [data-slot="title-row"] { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--space-4); + } + + [data-slot="usage"] { + display: flex; + gap: var(--space-6); + margin-top: var(--space-4); + + @media (max-width: 40rem) { + flex-direction: column; + gap: var(--space-4); + } + } + + [data-slot="usage-item"] { + flex: 1; + display: flex; + flex-direction: column; + gap: var(--space-2); + } + + [data-slot="usage-header"] { + display: flex; + justify-content: space-between; + align-items: baseline; + } + + [data-slot="usage-label"] { + font-size: var(--font-size-md); + font-weight: 500; + color: var(--color-text); + } + + [data-slot="usage-value"] { + font-size: var(--font-size-sm); + color: var(--color-text-muted); + } + + [data-slot="progress"] { + height: 8px; + background-color: var(--color-bg-surface); + border-radius: var(--border-radius-sm); + overflow: hidden; + } + + [data-slot="progress-bar"] { + height: 100%; + background-color: var(--color-accent); + border-radius: var(--border-radius-sm); + transition: width 0.3s ease; + } + + [data-slot="reset-time"] { + font-size: var(--font-size-sm); + color: var(--color-text-muted); + } + + [data-slot="setting-row"] { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); + margin-top: var(--space-4); + + p { + font-size: var(--font-size-sm); + line-height: 1.5; + color: var(--color-text-secondary); + margin: 0; + } + } + + [data-slot="toggle-label"] { + position: relative; + display: inline-block; + width: 2.5rem; + height: 1.5rem; + cursor: pointer; + flex-shrink: 0; + + input { + opacity: 0; + width: 0; + height: 0; + } + + span { + position: absolute; + inset: 0; + background-color: #ccc; + border: 1px solid #bbb; + border-radius: 1.5rem; + transition: all 0.3s ease; + cursor: pointer; + + &::before { + content: ""; + position: absolute; + top: 50%; + left: 0.125rem; + width: 1.25rem; + height: 1.25rem; + background-color: white; + border: 1px solid #ddd; + border-radius: 50%; + transform: translateY(-50%); + transition: all 0.3s ease; + } + } + + input:checked + span { + background-color: #21ad0e; + border-color: #148605; + + &::before { + transform: translateX(1rem) translateY(-50%); + } + } + + &:hover span { + box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2); + } + + input:checked:hover + span { + box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.3); + } + + &:has(input:disabled) { + cursor: not-allowed; + } + + input:disabled + span { + opacity: 0.5; + cursor: not-allowed; + } + } + + [data-slot="beta-notice"] { + padding: var(--space-3) var(--space-4); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + background-color: var(--color-bg-surface); + font-size: var(--font-size-sm); + color: var(--color-text-secondary); + line-height: 1.5; + margin-top: var(--space-3); + + a { + color: var(--color-accent); + text-decoration: none; + } + } + + [data-slot="other-message"] { + font-size: var(--font-size-sm); + color: var(--color-text-muted); + line-height: 1.5; + } + + [data-slot="promo-description"] { + font-size: var(--font-size-md); + color: var(--color-text-secondary); + line-height: 1.5; + margin-top: var(--space-2); + } + + [data-slot="promo-models-title"] { + font-size: var(--font-size-md); + font-weight: 600; + margin-top: var(--space-4); + } + + [data-slot="promo-models"] { + margin: var(--space-2) 0 0 var(--space-4); + padding: 0; + font-size: var(--font-size-md); + color: var(--color-text-secondary); + line-height: 1.4; + } + + [data-slot="subscribe-button"] { + align-self: flex-start; + margin-top: var(--space-4); + } +} diff --git a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx new file mode 100644 index 000000000..282411e8c --- /dev/null +++ b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx @@ -0,0 +1,296 @@ +import { action, useParams, useAction, useSubmission, json, query, createAsync } from "@solidjs/router" +import { createStore } from "solid-js/store" +import { createMemo, Show } from "solid-js" +import { Billing } from "@opencode-ai/console-core/billing.js" +import { Database, eq, and, isNull } from "@opencode-ai/console-core/drizzle/index.js" +import { BillingTable, LiteTable } from "@opencode-ai/console-core/schema/billing.sql.js" +import { Actor } from "@opencode-ai/console-core/actor.js" +import { Subscription } from "@opencode-ai/console-core/subscription.js" +import { LiteData } from "@opencode-ai/console-core/lite.js" +import { withActor } from "~/context/auth.withActor" +import { queryBillingInfo } from "../../common" +import styles from "./lite-section.module.css" +import { useI18n } from "~/context/i18n" +import { useLanguage } from "~/context/language" +import { formError } from "~/lib/form-error" + +const queryLiteSubscription = query(async (workspaceID: string) => { + "use server" + return withActor(async () => { + const row = await Database.use((tx) => + tx + .select({ + userID: LiteTable.userID, + rollingUsage: LiteTable.rollingUsage, + weeklyUsage: LiteTable.weeklyUsage, + monthlyUsage: LiteTable.monthlyUsage, + timeRollingUpdated: LiteTable.timeRollingUpdated, + timeWeeklyUpdated: LiteTable.timeWeeklyUpdated, + timeMonthlyUpdated: LiteTable.timeMonthlyUpdated, + timeCreated: LiteTable.timeCreated, + lite: BillingTable.lite, + }) + .from(BillingTable) + .innerJoin(LiteTable, eq(LiteTable.workspaceID, BillingTable.workspaceID)) + .where(and(eq(LiteTable.workspaceID, Actor.workspace()), isNull(LiteTable.timeDeleted))) + .then((r) => r[0]), + ) + if (!row) return null + + const limits = LiteData.getLimits() + const mine = row.userID === Actor.userID() + + return { + mine, + useBalance: row.lite?.useBalance ?? false, + rollingUsage: Subscription.analyzeRollingUsage({ + limit: limits.rollingLimit, + window: limits.rollingWindow, + usage: row.rollingUsage ?? 0, + timeUpdated: row.timeRollingUpdated ?? new Date(), + }), + weeklyUsage: Subscription.analyzeWeeklyUsage({ + limit: limits.weeklyLimit, + usage: row.weeklyUsage ?? 0, + timeUpdated: row.timeWeeklyUpdated ?? new Date(), + }), + monthlyUsage: Subscription.analyzeMonthlyUsage({ + limit: limits.monthlyLimit, + usage: row.monthlyUsage ?? 0, + timeUpdated: row.timeMonthlyUpdated ?? new Date(), + timeSubscribed: row.timeCreated, + }), + } + }, workspaceID) +}, "lite.subscription.get") + +function formatResetTime(seconds: number, i18n: ReturnType) { + const days = Math.floor(seconds / 86400) + if (days >= 1) { + const hours = Math.floor((seconds % 86400) / 3600) + return `${days} ${days === 1 ? i18n.t("workspace.lite.time.day") : i18n.t("workspace.lite.time.days")} ${hours} ${hours === 1 ? i18n.t("workspace.lite.time.hour") : i18n.t("workspace.lite.time.hours")}` + } + const hours = Math.floor(seconds / 3600) + const minutes = Math.floor((seconds % 3600) / 60) + if (hours >= 1) + return `${hours} ${hours === 1 ? i18n.t("workspace.lite.time.hour") : i18n.t("workspace.lite.time.hours")} ${minutes} ${minutes === 1 ? i18n.t("workspace.lite.time.minute") : i18n.t("workspace.lite.time.minutes")}` + if (minutes === 0) return i18n.t("workspace.lite.time.fewSeconds") + return `${minutes} ${minutes === 1 ? i18n.t("workspace.lite.time.minute") : i18n.t("workspace.lite.time.minutes")}` +} + +const createLiteCheckoutUrl = action(async (workspaceID: string, successUrl: string, cancelUrl: string) => { + "use server" + return json( + await withActor( + () => + Billing.generateLiteCheckoutUrl({ successUrl, cancelUrl }) + .then((data) => ({ error: undefined, data })) + .catch((e) => ({ + error: e.message as string, + data: undefined, + })), + workspaceID, + ), + { revalidate: [queryBillingInfo.key, queryLiteSubscription.key] }, + ) +}, "liteCheckoutUrl") + +const createSessionUrl = action(async (workspaceID: string, returnUrl: string) => { + "use server" + return json( + await withActor( + () => + Billing.generateSessionUrl({ returnUrl }) + .then((data) => ({ error: undefined, data })) + .catch((e) => ({ + error: e.message as string, + data: undefined, + })), + workspaceID, + ), + { revalidate: [queryBillingInfo.key, queryLiteSubscription.key] }, + ) +}, "liteSessionUrl") + +const setLiteUseBalance = action(async (form: FormData) => { + "use server" + const workspaceID = form.get("workspaceID")?.toString() + if (!workspaceID) return { error: formError.workspaceRequired } + const useBalance = form.get("useBalance")?.toString() === "true" + + return json( + await withActor(async () => { + await Database.use((tx) => + tx + .update(BillingTable) + .set({ + lite: useBalance ? { useBalance: true } : {}, + }) + .where(eq(BillingTable.workspaceID, workspaceID)), + ) + return { error: undefined } + }, workspaceID).catch((e) => ({ error: e.message as string })), + { revalidate: [queryBillingInfo.key, queryLiteSubscription.key] }, + ) +}, "setLiteUseBalance") + +export function LiteSection() { + const params = useParams() + const i18n = useI18n() + const language = useLanguage() + const billingInfo = createAsync(() => queryBillingInfo(params.id!)) + const isBlack = createMemo(() => billingInfo()?.subscriptionID || billingInfo()?.timeSubscriptionBooked) + const lite = createAsync(() => queryLiteSubscription(params.id!)) + const sessionAction = useAction(createSessionUrl) + const sessionSubmission = useSubmission(createSessionUrl) + const checkoutAction = useAction(createLiteCheckoutUrl) + const checkoutSubmission = useSubmission(createLiteCheckoutUrl) + const useBalanceSubmission = useSubmission(setLiteUseBalance) + const [store, setStore] = createStore({ + redirecting: false, + }) + + async function onClickSession() { + const result = await sessionAction(params.id!, window.location.href) + if (result.data) { + setStore("redirecting", true) + window.location.href = result.data + } + } + + async function onClickSubscribe() { + const result = await checkoutAction(params.id!, window.location.href, window.location.href) + if (result.data) { + setStore("redirecting", true) + window.location.href = result.data + } + } + + return ( + <> + +
    +
    +

    {i18n.t("workspace.lite.title")}

    +
    +

    {i18n.t("workspace.lite.black.message")}

    +
    +
    + + {(sub) => ( +
    +
    +

    {i18n.t("workspace.lite.title")}

    +
    +

    {i18n.t("workspace.lite.subscription.message")}

    + +
    +
    +
    + {i18n.t("workspace.lite.subscription.selectProvider")}{" "} + + {i18n.t("common.learnMore")} + + . +
    +
    +
    +
    + {i18n.t("workspace.lite.subscription.rollingUsage")} + {sub().rollingUsage.usagePercent}% +
    +
    +
    +
    + + {i18n.t("workspace.lite.subscription.resetsIn")}{" "} + {formatResetTime(sub().rollingUsage.resetInSec, i18n)} + +
    +
    +
    + {i18n.t("workspace.lite.subscription.weeklyUsage")} + {sub().weeklyUsage.usagePercent}% +
    +
    +
    +
    + + {i18n.t("workspace.lite.subscription.resetsIn")} {formatResetTime(sub().weeklyUsage.resetInSec, i18n)} + +
    +
    +
    + {i18n.t("workspace.lite.subscription.monthlyUsage")} + {sub().monthlyUsage.usagePercent}% +
    +
    +
    +
    + + {i18n.t("workspace.lite.subscription.resetsIn")}{" "} + {formatResetTime(sub().monthlyUsage.resetInSec, i18n)} + +
    +
    +
    +

    {i18n.t("workspace.lite.subscription.useBalance")}

    + + + +
    +
    + )} +
    + +
    +
    +

    {i18n.t("workspace.lite.title")}

    +
    +

    {i18n.t("workspace.lite.other.message")}

    +
    +
    + +
    +
    +

    {i18n.t("workspace.lite.title")}

    +
    +

    {i18n.t("workspace.lite.promo.description")}

    +

    {i18n.t("workspace.lite.promo.modelsTitle")}

    +
      +
    • Kimi K2.5
    • +
    • GLM-5
    • +
    • MiniMax M2.5
    • +
    +

    {i18n.t("workspace.lite.promo.footer")}

    + +
    +
    + + ) +} -- cgit v1.2.3 From 4a81df190c58c29418d8c32e9402cf71afa61bc8 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 11 Mar 2026 03:31:24 -0400 Subject: zen: add alipay for go sub --- packages/console/app/src/component/icon.tsx | 9 +++++++++ packages/console/app/src/i18n/ar.ts | 3 ++- packages/console/app/src/i18n/br.ts | 3 ++- packages/console/app/src/i18n/da.ts | 3 ++- packages/console/app/src/i18n/de.ts | 3 ++- packages/console/app/src/i18n/en.ts | 1 + packages/console/app/src/i18n/es.ts | 3 ++- packages/console/app/src/i18n/fr.ts | 1 + packages/console/app/src/i18n/it.ts | 1 + packages/console/app/src/i18n/ja.ts | 3 ++- packages/console/app/src/i18n/ko.ts | 3 ++- packages/console/app/src/i18n/no.ts | 3 ++- packages/console/app/src/i18n/pl.ts | 3 ++- packages/console/app/src/i18n/ru.ts | 3 ++- packages/console/app/src/i18n/th.ts | 3 ++- packages/console/app/src/i18n/tr.ts | 1 + packages/console/app/src/i18n/zh.ts | 4 ++-- packages/console/app/src/i18n/zht.ts | 4 ++-- .../app/src/routes/workspace/[id]/billing/billing-section.tsx | 8 +++++++- packages/console/core/src/billing.ts | 2 +- 20 files changed, 47 insertions(+), 17 deletions(-) diff --git a/packages/console/app/src/component/icon.tsx b/packages/console/app/src/component/icon.tsx index 1225aeb10..8d3c71656 100644 --- a/packages/console/app/src/component/icon.tsx +++ b/packages/console/app/src/component/icon.tsx @@ -111,6 +111,15 @@ export function IconStripe(props: JSX.SvgSVGAttributes) { ) } +export function IconAlipay(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} + export function IconChevron(props: JSX.SvgSVGAttributes) { return ( diff --git a/packages/console/app/src/i18n/ar.ts b/packages/console/app/src/i18n/ar.ts index 5a03eea09..081535a3a 100644 --- a/packages/console/app/src/i18n/ar.ts +++ b/packages/console/app/src/i18n/ar.ts @@ -537,6 +537,7 @@ export const dict = { "workspace.billing.loading": "جارٍ التحميل...", "workspace.billing.addAction": "إضافة", "workspace.billing.addBalance": "إضافة رصيد", + "workspace.billing.alipay": "Alipay", "workspace.billing.linkedToStripe": "مرتبط بـ Stripe", "workspace.billing.manage": "إدارة", "workspace.billing.enable": "تمكين الفوترة", @@ -629,7 +630,7 @@ export const dict = { "workspace.lite.subscription.selectProvider": 'اختر "OpenCode Go" كمزود في إعدادات opencode الخاصة بك لاستخدام نماذج Go.', "workspace.lite.black.message": - 'أنت مشترك حاليًا في OpenCode Black أو في قائمة الانتظار. يرجى إلغاء الاشتراك أولاً إذا كنت ترغب في التبديل إلى Go.', + "أنت مشترك حاليًا في OpenCode Black أو في قائمة الانتظار. يرجى إلغاء الاشتراك أولاً إذا كنت ترغب في التبديل إلى Go.", "workspace.lite.other.message": "عضو آخر في مساحة العمل هذه مشترك بالفعل في OpenCode Go. يمكن لعضو واحد فقط لكل مساحة عمل الاشتراك.", "workspace.lite.promo.description": diff --git a/packages/console/app/src/i18n/br.ts b/packages/console/app/src/i18n/br.ts index da79d2e66..96f40499e 100644 --- a/packages/console/app/src/i18n/br.ts +++ b/packages/console/app/src/i18n/br.ts @@ -545,6 +545,7 @@ export const dict = { "workspace.billing.loading": "Carregando...", "workspace.billing.addAction": "Adicionar", "workspace.billing.addBalance": "Adicionar Saldo", + "workspace.billing.alipay": "Alipay", "workspace.billing.linkedToStripe": "Vinculado ao Stripe", "workspace.billing.manage": "Gerenciar", "workspace.billing.enable": "Ativar Faturamento", @@ -638,7 +639,7 @@ export const dict = { "workspace.lite.subscription.selectProvider": 'Selecione "OpenCode Go" como provedor na sua configuração do opencode para usar os modelos Go.', "workspace.lite.black.message": - 'Você está atualmente inscrito no OpenCode Black ou na lista de espera. Por favor, cancele a assinatura primeiro se desejar mudar para o Go.', + "Você está atualmente inscrito no OpenCode Black ou na lista de espera. Por favor, cancele a assinatura primeiro se desejar mudar para o Go.", "workspace.lite.other.message": "Outro membro neste workspace já assina o OpenCode Go. Apenas um membro por workspace pode assinar.", "workspace.lite.promo.description": diff --git a/packages/console/app/src/i18n/da.ts b/packages/console/app/src/i18n/da.ts index 5fa9e2b8c..7ed111485 100644 --- a/packages/console/app/src/i18n/da.ts +++ b/packages/console/app/src/i18n/da.ts @@ -541,6 +541,7 @@ export const dict = { "workspace.billing.loading": "Indlæser...", "workspace.billing.addAction": "Tilføj", "workspace.billing.addBalance": "Tilføj saldo", + "workspace.billing.alipay": "Alipay", "workspace.billing.linkedToStripe": "Forbundet til Stripe", "workspace.billing.manage": "Administrer", "workspace.billing.enable": "Aktiver fakturering", @@ -634,7 +635,7 @@ export const dict = { "workspace.lite.subscription.selectProvider": 'Vælg "OpenCode Go" som udbyder i din opencode-konfiguration for at bruge Go-modeller.', "workspace.lite.black.message": - 'Du abonnerer i øjeblikket på OpenCode Black eller er på venteliste. Afmeld venligst først, hvis du vil skifte til Go.', + "Du abonnerer i øjeblikket på OpenCode Black eller er på venteliste. Afmeld venligst først, hvis du vil skifte til Go.", "workspace.lite.other.message": "Et andet medlem i dette workspace abonnerer allerede på OpenCode Go. Kun ét medlem pr. workspace kan abonnere.", "workspace.lite.promo.description": diff --git a/packages/console/app/src/i18n/de.ts b/packages/console/app/src/i18n/de.ts index 29bebc908..bd81c9bf8 100644 --- a/packages/console/app/src/i18n/de.ts +++ b/packages/console/app/src/i18n/de.ts @@ -544,6 +544,7 @@ export const dict = { "workspace.billing.loading": "Lade...", "workspace.billing.addAction": "Hinzufügen", "workspace.billing.addBalance": "Guthaben aufladen", + "workspace.billing.alipay": "Alipay", "workspace.billing.linkedToStripe": "Mit Stripe verbunden", "workspace.billing.manage": "Verwalten", "workspace.billing.enable": "Abrechnung aktivieren", @@ -637,7 +638,7 @@ export const dict = { "workspace.lite.subscription.selectProvider": 'Wähle "OpenCode Go" als Anbieter in deiner opencode-Konfiguration, um Go-Modelle zu verwenden.', "workspace.lite.black.message": - 'Du hast derzeit OpenCode Black abonniert oder stehst auf der Warteliste. Bitte kündige zuerst, wenn du zu Go wechseln möchtest.', + "Du hast derzeit OpenCode Black abonniert oder stehst auf der Warteliste. Bitte kündige zuerst, wenn du zu Go wechseln möchtest.", "workspace.lite.other.message": "Ein anderes Mitglied in diesem Workspace hat OpenCode Go bereits abonniert. Nur ein Mitglied pro Workspace kann abonnieren.", "workspace.lite.promo.description": diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts index dca14bb87..05c4643af 100644 --- a/packages/console/app/src/i18n/en.ts +++ b/packages/console/app/src/i18n/en.ts @@ -538,6 +538,7 @@ export const dict = { "workspace.billing.loading": "Loading...", "workspace.billing.addAction": "Add", "workspace.billing.addBalance": "Add Balance", + "workspace.billing.alipay": "Alipay", "workspace.billing.linkedToStripe": "Linked to Stripe", "workspace.billing.manage": "Manage", "workspace.billing.enable": "Enable Billing", diff --git a/packages/console/app/src/i18n/es.ts b/packages/console/app/src/i18n/es.ts index f1a95b2be..92486987b 100644 --- a/packages/console/app/src/i18n/es.ts +++ b/packages/console/app/src/i18n/es.ts @@ -546,6 +546,7 @@ export const dict = { "workspace.billing.loading": "Cargando...", "workspace.billing.addAction": "Añadir", "workspace.billing.addBalance": "Añadir Saldo", + "workspace.billing.alipay": "Alipay", "workspace.billing.linkedToStripe": "Vinculado con Stripe", "workspace.billing.manage": "Gestionar", "workspace.billing.enable": "Habilitar Facturación", @@ -639,7 +640,7 @@ export const dict = { "workspace.lite.subscription.selectProvider": 'Selecciona "OpenCode Go" como proveedor en tu configuración de opencode para usar los modelos Go.', "workspace.lite.black.message": - 'Actualmente estás suscrito a OpenCode Black o estás en la lista de espera. Por favor, cancela la suscripción primero si deseas cambiar a Go.', + "Actualmente estás suscrito a OpenCode Black o estás en la lista de espera. Por favor, cancela la suscripción primero si deseas cambiar a Go.", "workspace.lite.other.message": "Otro miembro de este espacio de trabajo ya está suscrito a OpenCode Go. Solo un miembro por espacio de trabajo puede suscribirse.", "workspace.lite.promo.description": diff --git a/packages/console/app/src/i18n/fr.ts b/packages/console/app/src/i18n/fr.ts index 7e2ff66db..df379fae9 100644 --- a/packages/console/app/src/i18n/fr.ts +++ b/packages/console/app/src/i18n/fr.ts @@ -547,6 +547,7 @@ export const dict = { "workspace.billing.loading": "Chargement...", "workspace.billing.addAction": "Ajouter", "workspace.billing.addBalance": "Ajouter un solde", + "workspace.billing.alipay": "Alipay", "workspace.billing.linkedToStripe": "Lié à Stripe", "workspace.billing.manage": "Gérer", "workspace.billing.enable": "Activer la facturation", diff --git a/packages/console/app/src/i18n/it.ts b/packages/console/app/src/i18n/it.ts index c579a4863..24f3aa2f8 100644 --- a/packages/console/app/src/i18n/it.ts +++ b/packages/console/app/src/i18n/it.ts @@ -544,6 +544,7 @@ export const dict = { "workspace.billing.loading": "Caricamento...", "workspace.billing.addAction": "Aggiungi", "workspace.billing.addBalance": "Aggiungi Saldo", + "workspace.billing.alipay": "Alipay", "workspace.billing.linkedToStripe": "Collegato a Stripe", "workspace.billing.manage": "Gestisci", "workspace.billing.enable": "Abilita Fatturazione", diff --git a/packages/console/app/src/i18n/ja.ts b/packages/console/app/src/i18n/ja.ts index 020f68005..f11f5052b 100644 --- a/packages/console/app/src/i18n/ja.ts +++ b/packages/console/app/src/i18n/ja.ts @@ -543,6 +543,7 @@ export const dict = { "workspace.billing.loading": "読み込み中...", "workspace.billing.addAction": "追加", "workspace.billing.addBalance": "残高を追加", + "workspace.billing.alipay": "Alipay", "workspace.billing.linkedToStripe": "Stripeと連携済み", "workspace.billing.manage": "管理", "workspace.billing.enable": "課金を有効にする", @@ -637,7 +638,7 @@ export const dict = { "workspace.lite.subscription.selectProvider": "Go モデルを使用するには、opencode の設定で「OpenCode Go」をプロバイダーとして選択してください。", "workspace.lite.black.message": - '現在 OpenCode Black を購読中、またはウェイティングリストに登録されています。Go に切り替える場合は、先に登録を解除してください。', + "現在 OpenCode Black を購読中、またはウェイティングリストに登録されています。Go に切り替える場合は、先に登録を解除してください。", "workspace.lite.other.message": "このワークスペースの別のメンバーが既に OpenCode Go を購読しています。ワークスペースにつき1人のメンバーのみが購読できます。", "workspace.lite.promo.description": diff --git a/packages/console/app/src/i18n/ko.ts b/packages/console/app/src/i18n/ko.ts index b5c6efc44..fe33bf545 100644 --- a/packages/console/app/src/i18n/ko.ts +++ b/packages/console/app/src/i18n/ko.ts @@ -537,6 +537,7 @@ export const dict = { "workspace.billing.loading": "로드 중...", "workspace.billing.addAction": "추가", "workspace.billing.addBalance": "잔액 추가", + "workspace.billing.alipay": "Alipay", "workspace.billing.linkedToStripe": "Stripe에 연결됨", "workspace.billing.manage": "관리", "workspace.billing.enable": "결제 활성화", @@ -629,7 +630,7 @@ export const dict = { "workspace.lite.subscription.selectProvider": 'Go 모델을 사용하려면 opencode 설정에서 "OpenCode Go"를 공급자로 선택하세요.', "workspace.lite.black.message": - '현재 OpenCode Black을 구독 중이거나 대기 명단에 등록되어 있습니다. Go로 전환하려면 먼저 구독을 취소해 주세요.', + "현재 OpenCode Black을 구독 중이거나 대기 명단에 등록되어 있습니다. Go로 전환하려면 먼저 구독을 취소해 주세요.", "workspace.lite.other.message": "이 워크스페이스의 다른 멤버가 이미 OpenCode Go를 구독 중입니다. 워크스페이스당 한 명의 멤버만 구독할 수 있습니다.", "workspace.lite.promo.description": diff --git a/packages/console/app/src/i18n/no.ts b/packages/console/app/src/i18n/no.ts index 31dc8ee10..af2a8d59f 100644 --- a/packages/console/app/src/i18n/no.ts +++ b/packages/console/app/src/i18n/no.ts @@ -542,6 +542,7 @@ export const dict = { "workspace.billing.loading": "Laster...", "workspace.billing.addAction": "Legg til", "workspace.billing.addBalance": "Legg til saldo", + "workspace.billing.alipay": "Alipay", "workspace.billing.linkedToStripe": "Koblet til Stripe", "workspace.billing.manage": "Administrer", "workspace.billing.enable": "Aktiver fakturering", @@ -635,7 +636,7 @@ export const dict = { "workspace.lite.subscription.selectProvider": 'Velg "OpenCode Go" som leverandør i opencode-konfigurasjonen din for å bruke Go-modeller.', "workspace.lite.black.message": - 'Du abonnerer for øyeblikket på OpenCode Black eller står på venteliste. Vennligst avslutt abonnementet først hvis du vil bytte til Go.', + "Du abonnerer for øyeblikket på OpenCode Black eller står på venteliste. Vennligst avslutt abonnementet først hvis du vil bytte til Go.", "workspace.lite.other.message": "Et annet medlem i dette arbeidsområdet abonnerer allerede på OpenCode Go. Kun ett medlem per arbeidsområde kan abonnere.", "workspace.lite.promo.description": diff --git a/packages/console/app/src/i18n/pl.ts b/packages/console/app/src/i18n/pl.ts index dde32158a..4ec3dbc64 100644 --- a/packages/console/app/src/i18n/pl.ts +++ b/packages/console/app/src/i18n/pl.ts @@ -543,6 +543,7 @@ export const dict = { "workspace.billing.loading": "Ładowanie...", "workspace.billing.addAction": "Dodaj", "workspace.billing.addBalance": "Doładuj saldo", + "workspace.billing.alipay": "Alipay", "workspace.billing.linkedToStripe": "Połączono ze Stripe", "workspace.billing.manage": "Zarządzaj", "workspace.billing.enable": "Włącz rozliczenia", @@ -636,7 +637,7 @@ export const dict = { "workspace.lite.subscription.selectProvider": 'Wybierz "OpenCode Go" jako dostawcę w konfiguracji opencode, aby używać modeli Go.', "workspace.lite.black.message": - 'Obecnie subskrybujesz OpenCode Black lub jesteś na liście oczekujących. Jeśli chcesz przejść na Go, najpierw anuluj subskrypcję.', + "Obecnie subskrybujesz OpenCode Black lub jesteś na liście oczekujących. Jeśli chcesz przejść na Go, najpierw anuluj subskrypcję.", "workspace.lite.other.message": "Inny członek tego obszaru roboczego już subskrybuje OpenCode Go. Tylko jeden członek na obszar roboczy może subskrybować.", "workspace.lite.promo.description": diff --git a/packages/console/app/src/i18n/ru.ts b/packages/console/app/src/i18n/ru.ts index 4a84e91cc..d114e188e 100644 --- a/packages/console/app/src/i18n/ru.ts +++ b/packages/console/app/src/i18n/ru.ts @@ -549,6 +549,7 @@ export const dict = { "workspace.billing.loading": "Загрузка...", "workspace.billing.addAction": "Пополнить", "workspace.billing.addBalance": "Пополнить баланс", + "workspace.billing.alipay": "Alipay", "workspace.billing.linkedToStripe": "Привязано к Stripe", "workspace.billing.manage": "Управление", "workspace.billing.enable": "Включить оплату", @@ -642,7 +643,7 @@ export const dict = { "workspace.lite.subscription.selectProvider": 'Выберите "OpenCode Go" в качестве провайдера в настройках opencode для использования моделей Go.', "workspace.lite.black.message": - 'Вы подписаны на OpenCode Black или находитесь в списке ожидания. Пожалуйста, сначала отмените подписку, если хотите перейти на Go.', + "Вы подписаны на OpenCode Black или находитесь в списке ожидания. Пожалуйста, сначала отмените подписку, если хотите перейти на Go.", "workspace.lite.other.message": "Другой участник в этом рабочем пространстве уже подписан на OpenCode Go. Только один участник в рабочем пространстве может оформить подписку.", "workspace.lite.promo.description": diff --git a/packages/console/app/src/i18n/th.ts b/packages/console/app/src/i18n/th.ts index aabfea257..f74c56323 100644 --- a/packages/console/app/src/i18n/th.ts +++ b/packages/console/app/src/i18n/th.ts @@ -540,6 +540,7 @@ export const dict = { "workspace.billing.loading": "กำลังโหลด...", "workspace.billing.addAction": "เพิ่ม", "workspace.billing.addBalance": "เพิ่มยอดคงเหลือ", + "workspace.billing.alipay": "Alipay", "workspace.billing.linkedToStripe": "เชื่อมโยงกับ Stripe", "workspace.billing.manage": "จัดการ", "workspace.billing.enable": "เปิดใช้งานการเรียกเก็บเงิน", @@ -633,7 +634,7 @@ export const dict = { "workspace.lite.subscription.selectProvider": 'เลือก "OpenCode Go" เป็นผู้ให้บริการในการตั้งค่า opencode ของคุณเพื่อใช้โมเดล Go', "workspace.lite.black.message": - 'ขณะนี้คุณสมัครสมาชิก OpenCode Black หรืออยู่ในรายการรอ โปรดยกเลิกการสมัครก่อนหากต้องการเปลี่ยนไปใช้ Go', + "ขณะนี้คุณสมัครสมาชิก OpenCode Black หรืออยู่ในรายการรอ โปรดยกเลิกการสมัครก่อนหากต้องการเปลี่ยนไปใช้ Go", "workspace.lite.other.message": "สมาชิกคนอื่นใน Workspace นี้ได้สมัคร OpenCode Go แล้ว สามารถสมัครได้เพียงหนึ่งคนต่อหนึ่ง Workspace เท่านั้น", "workspace.lite.promo.description": diff --git a/packages/console/app/src/i18n/tr.ts b/packages/console/app/src/i18n/tr.ts index 6d6e414d1..c685bf03d 100644 --- a/packages/console/app/src/i18n/tr.ts +++ b/packages/console/app/src/i18n/tr.ts @@ -545,6 +545,7 @@ export const dict = { "workspace.billing.loading": "Yükleniyor...", "workspace.billing.addAction": "Ekle", "workspace.billing.addBalance": "Bakiye Ekle", + "workspace.billing.alipay": "Alipay", "workspace.billing.linkedToStripe": "Stripe'a bağlı", "workspace.billing.manage": "Yönet", "workspace.billing.enable": "Faturalandırmayı Etkinleştir", diff --git a/packages/console/app/src/i18n/zh.ts b/packages/console/app/src/i18n/zh.ts index ccb3a554d..bbfc0df11 100644 --- a/packages/console/app/src/i18n/zh.ts +++ b/packages/console/app/src/i18n/zh.ts @@ -521,6 +521,7 @@ export const dict = { "workspace.billing.loading": "加载中...", "workspace.billing.addAction": "充值", "workspace.billing.addBalance": "充值余额", + "workspace.billing.alipay": "支付宝", "workspace.billing.linkedToStripe": "已关联 Stripe", "workspace.billing.manage": "管理", "workspace.billing.enable": "启用计费", @@ -612,8 +613,7 @@ export const dict = { "workspace.lite.subscription.useBalance": "达到使用限额后使用您的可用余额", "workspace.lite.subscription.selectProvider": "在你的 opencode 配置中选择「OpenCode Go」作为提供商,即可使用 Go 模型。", - "workspace.lite.black.message": - '您当前已订阅 OpenCode Black 或在候补名单中。如需切换到 Go,请先取消订阅。', + "workspace.lite.black.message": "您当前已订阅 OpenCode Black 或在候补名单中。如需切换到 Go,请先取消订阅。", "workspace.lite.other.message": "此工作区中的另一位成员已经订阅了 OpenCode Go。每个工作区只有一名成员可以订阅。", "workspace.lite.promo.description": "OpenCode Go 是一个每月 $10 的订阅计划,提供对主流开源编码模型的稳定访问,并配备充足的使用额度。", diff --git a/packages/console/app/src/i18n/zht.ts b/packages/console/app/src/i18n/zht.ts index bd12783e2..6a5ce0f8a 100644 --- a/packages/console/app/src/i18n/zht.ts +++ b/packages/console/app/src/i18n/zht.ts @@ -522,6 +522,7 @@ export const dict = { "workspace.billing.loading": "載入中...", "workspace.billing.addAction": "儲值", "workspace.billing.addBalance": "儲值餘額", + "workspace.billing.alipay": "支付寶", "workspace.billing.linkedToStripe": "已連結 Stripe", "workspace.billing.manage": "管理", "workspace.billing.enable": "啟用帳務", @@ -613,8 +614,7 @@ export const dict = { "workspace.lite.subscription.useBalance": "達到使用限制後使用您的可用餘額", "workspace.lite.subscription.selectProvider": "在您的 opencode 設定中選擇「OpenCode Go」作為提供商,即可使用 Go 模型。", - "workspace.lite.black.message": - '您目前已訂閱 OpenCode Black 或在候補名單中。若要切換至 Go,請先取消訂閱。', + "workspace.lite.black.message": "您目前已訂閱 OpenCode Black 或在候補名單中。若要切換至 Go,請先取消訂閱。", "workspace.lite.other.message": "此工作區中的另一位成員已訂閱 OpenCode Go。每個工作區只能有一位成員訂閱。", "workspace.lite.promo.description": "OpenCode Go 是一個每月 $10 的訂閱方案,提供對主流開放原始碼編碼模型的穩定存取,並配備充足的使用額度。", diff --git a/packages/console/app/src/routes/workspace/[id]/billing/billing-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/billing-section.tsx index db89a1c9e..d966f38d3 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/billing-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/billing-section.tsx @@ -3,7 +3,7 @@ import { createMemo, Match, Show, Switch, createEffect } from "solid-js" import { createStore } from "solid-js/store" import { Billing } from "@opencode-ai/console-core/billing.js" import { withActor } from "~/context/auth.withActor" -import { IconCreditCard, IconStripe } from "~/component/icon" +import { IconAlipay, IconCreditCard, IconStripe } from "~/component/icon" import styles from "./billing-section.module.css" import { createCheckoutUrl, formatBalance, queryBillingInfo } from "../../common" import { useI18n } from "~/context/i18n" @@ -205,6 +205,9 @@ export function BillingSection() { + + +
    @@ -218,6 +221,9 @@ export function BillingSection() { {i18n.t("workspace.billing.linkedToStripe")} + + {i18n.t("workspace.billing.alipay")} +