update go referral component and add usage preview

This commit is contained in:
vimtor
2026-05-14 16:30:57 +02:00
parent 5564c1f8f6
commit aeab589b5f
10 changed files with 368 additions and 118 deletions

View File

@@ -745,7 +745,7 @@
"cross-spawn": "7.0.6",
"diff": "8.0.2",
"dompurify": "3.3.1",
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
"drizzle-kit": "1.0.0-beta.22",
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
"effect": "4.0.0-beta.65",
"fuzzysort": "3.1.0",
@@ -2972,7 +2972,7 @@
"dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="],
"drizzle-kit": ["drizzle-kit@1.0.0-beta.19-d95b7a4", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "get-tsconfig": "^4.13.6", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-M0sqc+42TYBod6kEZ3AsW6+JWe3+76gR1aDFbHH5DmuLKEwewmbzlhBG6qnvV6YA1cIIbkuam3dC7r6PREOCXw=="],
"drizzle-kit": ["drizzle-kit@1.0.0-beta.22", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "get-tsconfig": "^4.13.6", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-9HTZuQRljQKTgCx4UhiGn8KYYfHGk4+B/bRR1714W67kz0qgJvdrG527i8rQD8uUyET9UTGR1u8syySJD4znGw=="],
"drizzle-orm": ["drizzle-orm@1.0.0-beta.19-d95b7a4", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-bZZKKeoRKrMVU6zKTscjrSH0+WNb1WEi3N0Jl4wEyQ7aQpTgHzdYY6IJQ1P0M74HuSJVeX4UpkFB/S6dtqLEJg=="],

View File

@@ -53,7 +53,7 @@
"@tailwindcss/vite": "4.1.11",
"diff": "8.0.2",
"dompurify": "3.3.1",
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
"drizzle-kit": "1.0.0-beta.22",
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
"effect": "4.0.0-beta.65",
"ai": "6.0.168",

View File

@@ -54,6 +54,80 @@
line-height: 1.6;
}
[data-slot="usage-preview"] {
display: flex;
flex-direction: column;
gap: var(--space-5);
padding: var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
background-color: var(--color-bg-surface);
}
[data-slot="usage-preview-item"] {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
[data-slot="usage-preview-header"] {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--space-3);
}
[data-slot="usage-preview-label"] {
color: var(--color-text);
font-size: var(--font-size-sm);
font-weight: 500;
}
[data-slot="usage-preview-value"] {
display: inline-flex;
align-items: center;
gap: var(--space-1);
color: var(--color-text-muted);
font-family: var(--font-mono);
font-size: var(--font-size-xs);
white-space: nowrap;
}
[data-slot="usage-preview-after-value"] {
color: var(--color-accent);
font-weight: 600;
}
[data-slot="usage-preview-progress"] {
position: relative;
height: 8px;
overflow: hidden;
border-radius: var(--border-radius-sm);
background-color: var(--color-bg);
}
[data-slot="usage-preview-before"],
[data-slot="usage-preview-after"] {
position: absolute;
top: 0;
bottom: 0;
left: 0;
border-radius: var(--border-radius-sm);
}
[data-slot="usage-preview-before"] {
background-color: var(--color-border);
}
[data-slot="usage-preview-after"] {
background-color: var(--color-accent);
}
[data-slot="usage-preview-reset"] {
color: var(--color-text-muted);
font-size: var(--font-size-xs);
}
[data-slot="modal-actions"] {
display: flex;
justify-content: flex-end;
@@ -130,11 +204,6 @@
list-style-position: inside;
line-height: 1.5;
}
li::marker {
color: var(--color-text);
font-weight: 600;
}
}
[data-component="go-referral-section"] {
@@ -210,10 +279,8 @@
}
}
[data-slot="reward-list"] {
display: flex;
flex-direction: column;
gap: var(--space-2);
[data-slot="referrals-table"] {
overflow-x: auto;
}
[data-component="empty-state"] {
@@ -225,48 +292,80 @@
font-size: var(--font-size-sm);
}
[data-slot="reward-row"] {
display: grid;
grid-template-columns: minmax(9rem, 1fr) minmax(14rem, 2fr) auto;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
&[data-status="available"] {
background-color: var(--color-bg-surface);
}
@media (max-width: 48rem) {
grid-template-columns: 1fr;
align-items: stretch;
}
}
[data-slot="reward-main"],
[data-slot="reward-meta"] {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
[data-slot="reward-main"] {
strong {
color: var(--color-text);
font-size: var(--font-size-md);
font-weight: 600;
}
span {
color: var(--color-text-muted);
font-size: var(--font-size-sm);
}
}
[data-slot="reward-meta"] {
color: var(--color-text-muted);
[data-slot="referrals-table-element"] {
width: 100%;
border-collapse: collapse;
font-size: var(--font-size-sm);
line-height: 1.5;
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;
&:nth-child(1) {
width: 120px;
}
&:nth-child(3) {
width: 180px;
}
&:nth-child(4) {
width: 140px;
}
}
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="referral-amount"] {
color: var(--color-text);
font-weight: 500;
}
&[data-slot="referral-source"] {
color: var(--color-text-secondary);
font-family: var(--font-sans);
}
&[data-slot="referral-action"] {
text-align: right;
font-family: var(--font-sans);
white-space: nowrap;
button {
min-width: 96px;
}
}
}
tbody tr {
&[data-status="applied"] {
td:not([data-slot="referral-action"]) {
opacity: 0.68;
}
}
&:last-child td {
border-bottom: none;
}
}
@media (max-width: 40rem) {
th,
td {
padding: var(--space-2) var(--space-3);
font-size: var(--font-size-xs);
}
}
}
}

View File

@@ -1,13 +1,18 @@
import { action, json, query, useAction, useSubmission } from "@solidjs/router"
import { action, createAsync, json, query, useAction, useSubmission } from "@solidjs/router"
import { createMemo, createSignal, For, Show } from "solid-js"
import { getRequestEvent } from "solid-js/web"
import { Referral } from "@opencode-ai/console-core/referral.js"
import { Database, and, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js"
import { LiteData } from "@opencode-ai/console-core/lite.js"
import { LiteTable } from "@opencode-ai/console-core/schema/billing.sql.js"
import { ReferralRewardTable } from "@opencode-ai/console-core/schema/referral.sql.js"
import { Subscription } from "@opencode-ai/console-core/subscription.js"
import { withActor } from "~/context/auth.withActor"
import { Modal } from "~/component/modal"
import { IconCheck, IconCopy } from "~/component/icon"
import { useI18n } from "~/context/i18n"
import { useLanguage } from "~/context/language"
import { queryLiteSubscription } from "~/routes/workspace/[id]/go/lite-section"
import { formatResetTime, queryLiteSubscription } from "~/routes/workspace/[id]/go/lite-section"
import "./go-referral.css"
export type GoReferralReward = {
@@ -29,6 +34,24 @@ export type GoReferralSummary = {
rewards: GoReferralReward[]
}
type AnalyzedUsage = {
status: "ok" | "rate-limited"
resetInSec: number
usagePercent: number
}
type GoReferralUsagePreview = {
rollingUsage: GoReferralUsagePreviewItem
weeklyUsage: GoReferralUsagePreviewItem
monthlyUsage: GoReferralUsagePreviewItem
}
type GoReferralUsagePreviewItem = {
beforePercent: number
afterPercent: number
resetInSec: number
}
export const queryGoReferral = query(async (workspaceID: string) => {
"use server"
return withActor(async () => {
@@ -40,6 +63,81 @@ export const queryGoReferral = query(async (workspaceID: string) => {
}, workspaceID)
}, "go.referral.get")
export const queryGoReferralUsagePreview = query(async (workspaceID: string, rewardID?: string) => {
"use server"
if (!rewardID) return null
return withActor(async () => {
const row = await Database.use((tx) =>
tx
.select({
rewardAmount: ReferralRewardTable.amount,
rollingUsage: LiteTable.rollingUsage,
weeklyUsage: LiteTable.weeklyUsage,
monthlyUsage: LiteTable.monthlyUsage,
timeRollingUpdated: LiteTable.timeRollingUpdated,
timeWeeklyUpdated: LiteTable.timeWeeklyUpdated,
timeMonthlyUpdated: LiteTable.timeMonthlyUpdated,
timeCreated: LiteTable.timeCreated,
})
.from(ReferralRewardTable)
.innerJoin(LiteTable, eq(LiteTable.workspaceID, ReferralRewardTable.workspaceID))
.where(
and(
eq(ReferralRewardTable.workspaceID, workspaceID),
eq(ReferralRewardTable.id, rewardID),
isNull(ReferralRewardTable.timeApplied),
isNull(ReferralRewardTable.timeDeleted),
isNull(LiteTable.timeDeleted),
),
)
.then((rows) => rows[0]),
)
if (!row) return null
const limits = LiteData.getLimits()
const rollingBefore = Subscription.analyzeRollingUsage({
limit: limits.rollingLimit,
window: limits.rollingWindow,
usage: row.rollingUsage ?? 0,
timeUpdated: row.timeRollingUpdated ?? new Date(),
})
const rollingAfter = Subscription.analyzeRollingUsage({
limit: limits.rollingLimit,
window: limits.rollingWindow,
usage: Math.max(0, (row.rollingUsage ?? 0) - row.rewardAmount),
timeUpdated: row.timeRollingUpdated ?? new Date(),
})
const weeklyBefore = Subscription.analyzeWeeklyUsage({
limit: limits.weeklyLimit,
usage: row.weeklyUsage ?? 0,
timeUpdated: row.timeWeeklyUpdated ?? new Date(),
})
const weeklyAfter = Subscription.analyzeWeeklyUsage({
limit: limits.weeklyLimit,
usage: Math.max(0, (row.weeklyUsage ?? 0) - row.rewardAmount),
timeUpdated: row.timeWeeklyUpdated ?? new Date(),
})
const monthlyBefore = Subscription.analyzeMonthlyUsage({
limit: limits.monthlyLimit,
usage: row.monthlyUsage ?? 0,
timeUpdated: row.timeMonthlyUpdated ?? new Date(),
timeSubscribed: row.timeCreated,
})
const monthlyAfter = Subscription.analyzeMonthlyUsage({
limit: limits.monthlyLimit,
usage: Math.max(0, (row.monthlyUsage ?? 0) - row.rewardAmount),
timeUpdated: row.timeMonthlyUpdated ?? new Date(),
timeSubscribed: row.timeCreated,
})
return {
rollingUsage: usagePreview(rollingBefore, rollingAfter),
weeklyUsage: usagePreview(weeklyBefore, weeklyAfter),
monthlyUsage: usagePreview(monthlyBefore, monthlyAfter),
} satisfies GoReferralUsagePreview
}, workspaceID)
}, "go.referral.usagePreview")
export const applyGoReferralReward = action(async (workspaceID: string, rewardID: string) => {
"use server"
return json(
@@ -50,10 +148,18 @@ export const applyGoReferralReward = action(async (workspaceID: string, rewardID
.catch((e) => ({ error: e.message as string, data: undefined })),
workspaceID,
),
{ revalidate: [queryGoReferral.key, queryLiteSubscription.key] },
{ revalidate: [queryGoReferral.key, queryGoReferralUsagePreview.key, queryLiteSubscription.key] },
)
}, "go.referral.reward.apply")
function usagePreview(before: AnalyzedUsage, after: AnalyzedUsage) {
return {
beforePercent: before.usagePercent,
afterPercent: after.usagePercent,
resetInSec: after.resetInSec,
}
}
function formatCurrency(amount: number) {
if (amount % 100 === 0) return `$${amount / 100}`
return `$${(amount / 100).toFixed(2)}`
@@ -106,6 +212,7 @@ export function GoReferralSection(props: { workspaceID: string; summary: GoRefer
const apply = useAction(applyGoReferralReward)
const submission = useSubmission(applyGoReferralReward)
const [selected, setSelected] = createSignal<GoReferralReward>()
const preview = createAsync(() => queryGoReferralUsagePreview(props.workspaceID, selected()?.id))
const appliedCount = createMemo(() => props.summary.rewards.filter((reward) => reward.timeApplied).length)
async function onApply() {
@@ -128,7 +235,7 @@ export function GoReferralSection(props: { workspaceID: string; summary: GoRefer
<div data-component="go-referral-overview">
<div data-slot="referral-stats">
<div>
<span>{i18n.t("workspace.referral.stats.validInvites")}</span>
<span>{i18n.t("workspace.referral.stats.invites")}</span>
<strong>{props.summary.validInviteCount}</strong>
</div>
<div>
@@ -156,50 +263,47 @@ export function GoReferralSection(props: { workspaceID: string; summary: GoRefer
when={props.summary.rewards.length > 0}
fallback={<div data-component="empty-state">{i18n.t("workspace.referral.rewards.empty")}</div>}
>
<div data-slot="reward-list">
<For each={props.summary.rewards}>
{(reward) => {
const applied = createMemo(() => !!reward.timeApplied)
return (
<div data-slot="reward-row" data-status={applied() ? "applied" : "available"}>
<div data-slot="reward-main">
<strong>{formatCurrency(reward.amount)}</strong>
<span>{i18n.t(rewardSourceKey(reward.source))}</span>
</div>
<div data-slot="reward-meta">
<span>
{applied()
? i18n.t("workspace.referral.reward.status.applied")
: i18n.t("workspace.referral.reward.status.available")}
</span>
<span>
{applied() && reward.timeApplied
? i18n.t("workspace.referral.reward.appliedOn", {
date: formatDate(reward.timeApplied, language.tag(language.locale())),
})
: i18n.t("workspace.referral.reward.earnedOn", {
date: formatDate(reward.timeCreated, language.tag(language.locale())),
})}
</span>
</div>
<button
type="button"
disabled={applied() || !props.summary.hasActiveGo || submission.pending}
onClick={() => setSelected(reward)}
>
<Show
when={!applied()}
fallback={i18n.t("workspace.referral.reward.status.applied")}
>
{props.summary.hasActiveGo
? i18n.t("workspace.referral.apply.action")
: i18n.t("workspace.referral.apply.noGo")}
</Show>
</button>
</div>
)
}}
</For>
<div data-slot="referrals-table">
<table data-slot="referrals-table-element">
<thead>
<tr>
<th>{i18n.t("workspace.referral.table.reward")}</th>
<th>{i18n.t("workspace.referral.table.referral")}</th>
<th>{i18n.t("workspace.referral.table.earned")}</th>
<th></th>
</tr>
</thead>
<tbody>
<For each={props.summary.rewards}>
{(reward) => {
const applied = createMemo(() => !!reward.timeApplied)
const earnedAt = createMemo(() => formatDate(reward.timeCreated, language.tag(language.locale())))
return (
<tr data-status={applied() ? "applied" : "available"}>
<td data-slot="referral-amount">{formatCurrency(reward.amount)}</td>
<td data-slot="referral-source">{i18n.t(rewardSourceKey(reward.source))}</td>
<td data-slot="referral-date" title={earnedAt()}>
{earnedAt()}
</td>
<td data-slot="referral-action">
<button
type="button"
disabled={applied() || !props.summary.hasActiveGo || submission.pending}
onClick={() => setSelected(reward)}
>
<Show when={!applied()} fallback={i18n.t("workspace.referral.reward.status.applied")}>
{props.summary.hasActiveGo
? i18n.t("workspace.referral.apply.action")
: i18n.t("workspace.referral.apply.noGo")}
</Show>
</button>
</td>
</tr>
)
}}
</For>
</tbody>
</table>
</div>
</Show>
<Modal open={!!selected()} onClose={() => setSelected(undefined)} title={i18n.t("workspace.referral.apply.confirmTitle")}>
@@ -209,6 +313,9 @@ export function GoReferralSection(props: { workspaceID: string; summary: GoRefer
amount: formatCurrency(selected()?.amount ?? 0),
})}
</p>
<Show when={preview()} fallback={<p>{i18n.t("workspace.lite.loading")}</p>}>
{(usage) => <GoReferralUsagePreview preview={usage()} />}
</Show>
<div data-slot="modal-actions">
<button type="button" onClick={() => setSelected(undefined)}>
{i18n.t("common.cancel")}
@@ -223,6 +330,42 @@ export function GoReferralSection(props: { workspaceID: string; summary: GoRefer
)
}
function GoReferralUsagePreview(props: { preview: GoReferralUsagePreview }) {
const i18n = useI18n()
return (
<div data-slot="usage-preview">
<For
each={[
{ label: i18n.t("workspace.lite.subscription.rollingUsage"), usage: props.preview.rollingUsage },
{ label: i18n.t("workspace.lite.subscription.weeklyUsage"), usage: props.preview.weeklyUsage },
{ label: i18n.t("workspace.lite.subscription.monthlyUsage"), usage: props.preview.monthlyUsage },
]}
>
{(item) => (
<div data-slot="usage-preview-item">
<div data-slot="usage-preview-header">
<span data-slot="usage-preview-label">{item.label}</span>
<span data-slot="usage-preview-value">
<span>{item.usage.beforePercent}%</span>
<span aria-hidden="true">-&gt;</span>
<span data-slot="usage-preview-after-value">{item.usage.afterPercent}%</span>
</span>
</div>
<div data-slot="usage-preview-progress">
<div data-slot="usage-preview-before" style={{ width: `${item.usage.beforePercent}%` }} />
<div data-slot="usage-preview-after" style={{ width: `${item.usage.afterPercent}%` }} />
</div>
<span data-slot="usage-preview-reset">
{i18n.t("workspace.lite.subscription.resetsIn")} {formatResetTime(item.usage.resetInSec, i18n)}
</span>
</div>
)}
</For>
</div>
)
}
function InvitationInstructions(props: { rewardAmount: number }) {
const i18n = useI18n()

View File

@@ -667,23 +667,24 @@ export const dict = {
"workspace.referral.copyLink": "Copy Link",
"workspace.referral.copied": "Copied",
"workspace.referral.overview.title": "Invite friends to Go",
"workspace.referral.overview.subtitle": "Friends who subscribe with your link receive {{reward}} to apply to Go usage. You earn {{reward}} too.",
"workspace.referral.stats.validInvites": "Valid invites",
"workspace.referral.overview.subtitle": "You both get {{reward}} in Go usage when your friend subscribes through your link.",
"workspace.referral.stats.invites": "Invites",
"workspace.referral.stats.earned": "Earned",
"workspace.referral.stats.applied": "Applied",
"workspace.referral.instructions.share": "Share your invite link.",
"workspace.referral.instructions.subscribe": "Your friend subscribes to Go.",
"workspace.referral.instructions.claim": "Claim your rewards below.",
"workspace.referral.instructions.apply": "Apply {{amount}} to current Go usage.",
"workspace.referral.instructions.share": "Share your invite link",
"workspace.referral.instructions.subscribe": "Your friend subscribes to Go",
"workspace.referral.instructions.claim": "Claim your rewards below",
"workspace.referral.instructions.apply": "Apply {{amount}} to reduce current usage",
"workspace.referral.rewards.title": "Referral rewards",
"workspace.referral.rewards.subtitle": "{{applied}} / {{total}} rewards applied.",
"workspace.referral.rewards.empty": "No referral rewards yet.",
"workspace.referral.table.reward": "Reward",
"workspace.referral.table.referral": "Referral",
"workspace.referral.table.earned": "Earned",
"workspace.referral.reward.source.inviter": "Earned by inviting a friend",
"workspace.referral.reward.source.invitee": "Received from an invite",
"workspace.referral.reward.status.available": "Available",
"workspace.referral.reward.status.applied": "Applied",
"workspace.referral.reward.earnedOn": "Earned on {{date}}",
"workspace.referral.reward.appliedOn": "Applied on {{date}}",
"workspace.referral.reward.earnedOn": "{{date}}",
"workspace.referral.apply.noGo": "Subscribe to Go",
"workspace.referral.apply.action": "Apply",
"workspace.referral.apply.confirmTitle": "Apply Go reward",

View File

@@ -649,22 +649,23 @@ export const dict = {
"workspace.referral.copied": "已复制",
"workspace.referral.overview.title": "邀请好友使用 Go",
"workspace.referral.overview.subtitle": "通过您的链接订阅的好友可获得 {{reward}} Go 用量抵扣,您也可获得 {{reward}}。",
"workspace.referral.stats.validInvites": "有效邀请",
"workspace.referral.stats.invites": "邀请",
"workspace.referral.stats.earned": "已获得",
"workspace.referral.stats.applied": "已使用",
"workspace.referral.instructions.share": "分享您的邀请链接。",
"workspace.referral.instructions.subscribe": "好友订阅 Go。",
"workspace.referral.instructions.claim": "在下方领取您的奖励。",
"workspace.referral.instructions.apply": "使用 {{amount}} 抵扣当前 Go 用量。",
"workspace.referral.instructions.apply": "使用 {{amount}} 减少当前用量。",
"workspace.referral.rewards.title": "邀请奖励",
"workspace.referral.rewards.subtitle": "已使用 {{applied}} / {{total}} 个奖励。",
"workspace.referral.rewards.empty": "暂无邀请奖励。",
"workspace.referral.table.reward": "奖励",
"workspace.referral.table.referral": "邀请",
"workspace.referral.table.earned": "获得时间",
"workspace.referral.reward.source.inviter": "邀请好友获得",
"workspace.referral.reward.source.invitee": "通过邀请获得",
"workspace.referral.reward.status.available": "可用",
"workspace.referral.reward.status.applied": "已使用",
"workspace.referral.reward.earnedOn": "{{date}} 获得",
"workspace.referral.reward.appliedOn": "{{date}} 使用",
"workspace.referral.reward.earnedOn": "{{date}}",
"workspace.referral.apply.noGo": "订阅 Go",
"workspace.referral.apply.action": "使用",
"workspace.referral.apply.confirmTitle": "使用 Go 奖励",

View File

@@ -69,7 +69,7 @@ export const queryLiteSubscription = query(async (workspaceID: string) => {
}, workspaceID)
}, "lite.subscription.get")
function formatResetTime(seconds: number, i18n: ReturnType<typeof useI18n>) {
export function formatResetTime(seconds: number, i18n: ReturnType<typeof useI18n>) {
const days = Math.floor(seconds / 86400)
if (days >= 1) {
const hours = Math.floor((seconds % 86400) / 3600)

View File

@@ -382,6 +382,8 @@ export async function handler(
headers: resHeaders,
})
} catch (error: any) {
console.log('error')
console.log(error)
logger.metric({
"error.type": error.constructor.name,
"error.message": error.message,
@@ -700,6 +702,7 @@ export async function handler(
}
function validateBilling(authInfo: AuthInfo, modelInfo: ModelInfo): BillingSource {
return 'lite'
if (!authInfo) return "anonymous"
if (authInfo.provider?.credentials) return "byok"
if (authInfo.isFree) return "free"
@@ -949,6 +952,7 @@ export async function handler(
usageInfo: UsageInfo,
costInfo: CostInfo,
) {
console.log('tracking usage')
const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =
usageInfo
const { totalCostInCent, inputCost, outputCost, cacheReadCost, cacheWrite5mCost, cacheWrite1hCost } = costInfo

View File

@@ -1,6 +1,8 @@
import { Resource } from "sst"
import { defineConfig } from "drizzle-kit"
console.log(Resource.Database)
export default defineConfig({
out: "./migrations/",
strict: true,

View File

@@ -40,4 +40,4 @@ const newValues = Array.from({ length: PARTS }, (_, i) =>
const envFile = Bun.file(path.join(os.tmpdir(), `models-${Date.now()}.env`))
await envFile.write(newValues.map((v, i) => `ZEN_MODELS${i + 1}="${v.replace(/"/g, '\\"')}"`).join("\n"))
await $`bun sst secret load ${envFile.name} --stage frank`.cwd(root)
await $`bun sst secret load ${envFile.name} --stage vimtor`.cwd(root)