summaryrefslogtreecommitdiffhomepage
path: root/packages/console/app/src/routes/workspace
diff options
context:
space:
mode:
Diffstat (limited to 'packages/console/app/src/routes/workspace')
-rw-r--r--packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx2
-rw-r--r--packages/console/app/src/routes/workspace/[id]/billing/index.tsx9
-rw-r--r--packages/console/app/src/routes/workspace/[id]/billing/lite-section.module.css160
-rw-r--r--packages/console/app/src/routes/workspace/[id]/billing/lite-section.tsx269
-rw-r--r--packages/console/app/src/routes/workspace/[id]/graph-section.tsx62
-rw-r--r--packages/console/app/src/routes/workspace/[id]/usage-section.tsx36
-rw-r--r--packages/console/app/src/routes/workspace/common.tsx2
7 files changed, 506 insertions, 34 deletions
diff --git a/packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx
index 5326306e2..b8f089864 100644
--- a/packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx
+++ b/packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx
@@ -90,7 +90,7 @@ const enroll = action(async (workspaceID: string) => {
"use server"
return json(
await withActor(async () => {
- await Billing.subscribe({ seats: 1 })
+ await Billing.subscribeBlack({ seats: 1 })
return { error: undefined }
}, workspaceID).catch((e) => ({ error: e.message as string })),
{ revalidate: [queryBillingInfo.key, querySubscription.key] },
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 a252a0234..9fbdad2ef 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,8 @@ import { BillingSection } from "./billing-section"
import { ReloadSection } from "./reload-section"
import { PaymentSection } from "./payment-section"
import { BlackSection } from "./black-section"
-import { Show } from "solid-js"
+import { LiteSection } from "./lite-section"
+import { createMemo, Show } from "solid-js"
import { createAsync, useParams } from "@solidjs/router"
import { queryBillingInfo, querySessionInfo } from "../../common"
@@ -11,14 +12,18 @@ export default function () {
const params = useParams()
const sessionInfo = createAsync(() => querySessionInfo(params.id!))
const billingInfo = createAsync(() => queryBillingInfo(params.id!))
+ const isBlack = createMemo(() => billingInfo()?.subscriptionID || billingInfo()?.timeSubscriptionBooked)
return (
<div data-page="workspace-[id]">
<div data-slot="sections">
<Show when={sessionInfo()?.isAdmin}>
- <Show when={billingInfo()?.subscriptionID || billingInfo()?.timeSubscriptionBooked}>
+ <Show when={isBlack()}>
<BlackSection />
</Show>
+ <Show when={!isBlack() && sessionInfo()?.isBeta}>
+ <LiteSection />
+ </Show>
<BillingSection />
<Show when={billingInfo()?.customerID}>
<ReloadSection />
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
new file mode 100644
index 000000000..20662ab61
--- /dev/null
+++ b/packages/console/app/src/routes/workspace/[id]/billing/lite-section.module.css
@@ -0,0 +1,160 @@
+.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="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-sm);
+ color: var(--color-text-secondary);
+ line-height: 1.5;
+ margin-top: var(--space-2);
+ }
+
+ [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
new file mode 100644
index 000000000..c9192fdcf
--- /dev/null
+++ b/packages/console/app/src/routes/workspace/[id]/billing/lite-section.tsx
@@ -0,0 +1,269 @@
+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"
+
+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<typeof useI18n>) {
+ 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: "Workspace ID is required" }
+ 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 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 (
+ <>
+ <Show when={lite() && lite()!.mine && lite()!}>
+ {(sub) => (
+ <section class={styles.root}>
+ <div data-slot="section-title">
+ <h2>{i18n.t("workspace.lite.subscription.title")}</h2>
+ <div data-slot="title-row">
+ <p>{i18n.t("workspace.lite.subscription.message")}</p>
+ <button
+ data-color="primary"
+ disabled={sessionSubmission.pending || store.redirecting}
+ onClick={onClickSession}
+ >
+ {sessionSubmission.pending || store.redirecting
+ ? i18n.t("workspace.lite.loading")
+ : i18n.t("workspace.lite.subscription.manage")}
+ </button>
+ </div>
+ </div>
+ <div data-slot="usage">
+ <div data-slot="usage-item">
+ <div data-slot="usage-header">
+ <span data-slot="usage-label">{i18n.t("workspace.lite.subscription.rollingUsage")}</span>
+ <span data-slot="usage-value">{sub().rollingUsage.usagePercent}%</span>
+ </div>
+ <div data-slot="progress">
+ <div data-slot="progress-bar" style={{ width: `${sub().rollingUsage.usagePercent}%` }} />
+ </div>
+ <span data-slot="reset-time">
+ {i18n.t("workspace.lite.subscription.resetsIn")}{" "}
+ {formatResetTime(sub().rollingUsage.resetInSec, i18n)}
+ </span>
+ </div>
+ <div data-slot="usage-item">
+ <div data-slot="usage-header">
+ <span data-slot="usage-label">{i18n.t("workspace.lite.subscription.weeklyUsage")}</span>
+ <span data-slot="usage-value">{sub().weeklyUsage.usagePercent}%</span>
+ </div>
+ <div data-slot="progress">
+ <div data-slot="progress-bar" style={{ width: `${sub().weeklyUsage.usagePercent}%` }} />
+ </div>
+ <span data-slot="reset-time">
+ {i18n.t("workspace.lite.subscription.resetsIn")} {formatResetTime(sub().weeklyUsage.resetInSec, i18n)}
+ </span>
+ </div>
+ <div data-slot="usage-item">
+ <div data-slot="usage-header">
+ <span data-slot="usage-label">{i18n.t("workspace.lite.subscription.monthlyUsage")}</span>
+ <span data-slot="usage-value">{sub().monthlyUsage.usagePercent}%</span>
+ </div>
+ <div data-slot="progress">
+ <div data-slot="progress-bar" style={{ width: `${sub().monthlyUsage.usagePercent}%` }} />
+ </div>
+ <span data-slot="reset-time">
+ {i18n.t("workspace.lite.subscription.resetsIn")}{" "}
+ {formatResetTime(sub().monthlyUsage.resetInSec, i18n)}
+ </span>
+ </div>
+ </div>
+ <form action={setLiteUseBalance} method="post" data-slot="setting-row">
+ <p>{i18n.t("workspace.lite.subscription.useBalance")}</p>
+ <input type="hidden" name="workspaceID" value={params.id} />
+ <input type="hidden" name="useBalance" value={sub().useBalance ? "false" : "true"} />
+ <label data-slot="toggle-label">
+ <input
+ type="checkbox"
+ checked={sub().useBalance}
+ disabled={useBalanceSubmission.pending}
+ onChange={(e) => e.currentTarget.form?.requestSubmit()}
+ />
+ <span></span>
+ </label>
+ </form>
+ </section>
+ )}
+ </Show>
+ <Show when={lite() && !lite()!.mine}>
+ <section class={styles.root}>
+ <div data-slot="section-title">
+ <h2>{i18n.t("workspace.lite.other.title")}</h2>
+ </div>
+ <p data-slot="other-message">{i18n.t("workspace.lite.other.message")}</p>
+ </section>
+ </Show>
+ <Show when={lite() === null}>
+ <section class={styles.root}>
+ <div data-slot="section-title">
+ <h2>{i18n.t("workspace.lite.promo.title")}</h2>
+ </div>
+ <p data-slot="promo-description">{i18n.t("workspace.lite.promo.description")}</p>
+ <button
+ data-slot="subscribe-button"
+ data-color="primary"
+ disabled={checkoutSubmission.pending || store.redirecting}
+ onClick={onClickSubscribe}
+ >
+ {checkoutSubmission.pending || store.redirecting
+ ? i18n.t("workspace.lite.promo.subscribing")
+ : i18n.t("workspace.lite.promo.subscribe")}
+ </button>
+ </section>
+ </Show>
+ </>
+ )
+}
diff --git a/packages/console/app/src/routes/workspace/[id]/graph-section.tsx b/packages/console/app/src/routes/workspace/[id]/graph-section.tsx
index f26c7291d..56a31cdd0 100644
--- a/packages/console/app/src/routes/workspace/[id]/graph-section.tsx
+++ b/packages/console/app/src/routes/workspace/[id]/graph-section.tsx
@@ -36,7 +36,7 @@ async function getCosts(workspaceID: string, year: number, month: number) {
model: UsageTable.model,
totalCost: sum(UsageTable.cost),
keyId: UsageTable.keyID,
- subscription: sql<boolean>`COALESCE(JSON_EXTRACT(${UsageTable.enrichment}, '$.plan') = 'sub', false)`,
+ plan: sql<string | null>`JSON_EXTRACT(${UsageTable.enrichment}, '$.plan')`,
})
.from(UsageTable)
.where(
@@ -50,13 +50,13 @@ async function getCosts(workspaceID: string, year: number, month: number) {
sql`DATE(${UsageTable.timeCreated})`,
UsageTable.model,
UsageTable.keyID,
- sql`COALESCE(JSON_EXTRACT(${UsageTable.enrichment}, '$.plan') = 'sub', false)`,
+ sql`JSON_EXTRACT(${UsageTable.enrichment}, '$.plan')`,
)
.then((x) =>
x.map((r) => ({
...r,
totalCost: r.totalCost ? parseInt(r.totalCost) : 0,
- subscription: Boolean(r.subscription),
+ plan: r.plan as "sub" | "lite" | "byok" | null,
})),
),
)
@@ -218,18 +218,21 @@ export function GraphSection() {
const colorTextSecondary = styles.getPropertyValue("--color-text-secondary").trim()
const colorBorder = styles.getPropertyValue("--color-border").trim()
const subSuffix = ` (${i18n.t("workspace.cost.subscriptionShort")})`
+ const liteSuffix = ` (${i18n.t("workspace.cost.liteShort")})`
+ const dailyDataRegular = new Map<string, Map<string, number>>()
const dailyDataSub = new Map<string, Map<string, number>>()
- const dailyDataNonSub = new Map<string, Map<string, number>>()
+ const dailyDataLite = new Map<string, Map<string, number>>()
for (const dateKey of dates) {
+ dailyDataRegular.set(dateKey, new Map())
dailyDataSub.set(dateKey, new Map())
- dailyDataNonSub.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.subscription ? dailyDataSub : dailyDataNonSub
+ 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)
@@ -237,15 +240,15 @@ export function GraphSection() {
const filteredModels = store.model === null ? getModels() : [store.model]
- // Create datasets: non-subscription first, then subscription (with hatched pattern effect via opacity)
+ // Create datasets: regular first, then subscription, then lite (with visual distinction via opacity)
const datasets = [
...filteredModels
- .filter((model) => dates.some((date) => (dailyDataNonSub.get(date)?.get(model) || 0) > 0))
+ .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) => (dailyDataNonSub.get(date)?.get(model) || 0) / 100_000_000),
+ data: dates.map((date) => (dailyDataRegular.get(date)?.get(model) || 0) / 100_000_000),
backgroundColor: color,
hoverBackgroundColor: color,
borderWidth: 0,
@@ -266,6 +269,21 @@ export function GraphSection() {
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 {
@@ -347,9 +365,18 @@ export function GraphSection() {
const meta = chart.getDatasetMeta(i)
const label = dataset.label || ""
const isSub = label.endsWith(subSuffix)
- const model = isSub ? label.slice(0, -subSuffix.length) : label
+ 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) : baseColor
+ 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
@@ -363,9 +390,18 @@ export function GraphSection() {
const meta = chart.getDatasetMeta(i)
const label = dataset.label || ""
const isSub = label.endsWith(subSuffix)
- const model = isSub ? label.slice(0, -subSuffix.length) : label
+ 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) : baseColor
+ const color = isSub
+ ? addOpacityToColor(baseColor, 0.5)
+ : isLite
+ ? addOpacityToColor(baseColor, 0.35)
+ : baseColor
meta.data.forEach((bar: any) => {
bar.options.backgroundColor = color
})
diff --git a/packages/console/app/src/routes/workspace/[id]/usage-section.tsx b/packages/console/app/src/routes/workspace/[id]/usage-section.tsx
index 3d6637610..a20a5bf0d 100644
--- a/packages/console/app/src/routes/workspace/[id]/usage-section.tsx
+++ b/packages/console/app/src/routes/workspace/[id]/usage-section.tsx
@@ -1,6 +1,6 @@
import { Billing } from "@opencode-ai/console-core/billing.js"
import { createAsync, query, useParams } from "@solidjs/router"
-import { createMemo, For, Show, createEffect, createSignal } from "solid-js"
+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"
@@ -175,23 +175,23 @@ export function UsageSection() {
</div>
</td>
<td data-slot="usage-cost">
- <Show
- when={usage.enrichment?.plan === "sub"}
- fallback={
- <Show
- when={usage.enrichment?.plan === "byok"}
- fallback={<>${((usage.cost ?? 0) / 100000000).toFixed(4)}</>}
- >
- {i18n.t("workspace.usage.byok", {
- amount: ((usage.cost ?? 0) / 100000000).toFixed(4),
- })}
- </Show>
- }
- >
- {i18n.t("workspace.usage.subscription", {
- amount: ((usage.cost ?? 0) / 100000000).toFixed(4),
- })}
- </Show>
+ <Switch fallback={<>${((usage.cost ?? 0) / 100000000).toFixed(4)}</>}>
+ <Match when={usage.enrichment?.plan === "sub"}>
+ {i18n.t("workspace.usage.subscription", {
+ amount: ((usage.cost ?? 0) / 100000000).toFixed(4),
+ })}
+ </Match>
+ <Match when={usage.enrichment?.plan === "lite"}>
+ {i18n.t("workspace.usage.lite", {
+ amount: ((usage.cost ?? 0) / 100000000).toFixed(4),
+ })}
+ </Match>
+ <Match when={usage.enrichment?.plan === "byok"}>
+ {i18n.t("workspace.usage.byok", {
+ amount: ((usage.cost ?? 0) / 100000000).toFixed(4),
+ })}
+ </Match>
+ </Switch>
</td>
<td data-slot="usage-session">{usage.sessionID?.slice(-8) ?? "-"}</td>
</tr>
diff --git a/packages/console/app/src/routes/workspace/common.tsx b/packages/console/app/src/routes/workspace/common.tsx
index 5cbd67183..d41793dd9 100644
--- a/packages/console/app/src/routes/workspace/common.tsx
+++ b/packages/console/app/src/routes/workspace/common.tsx
@@ -115,6 +115,8 @@ export const queryBillingInfo = query(async (workspaceID: string) => {
subscriptionPlan: billing.subscriptionPlan,
timeSubscriptionBooked: billing.timeSubscriptionBooked,
timeSubscriptionSelected: billing.timeSubscriptionSelected,
+ lite: billing.lite,
+ liteSubscriptionID: billing.liteSubscriptionID,
}
}, workspaceID)
}, "billing.get")