mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-22 03:45:23 +00:00
update go referral component and add usage preview
This commit is contained in:
4
bun.lock
4
bun.lock
@@ -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=="],
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">-></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()
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 奖励",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Resource } from "sst"
|
||||
import { defineConfig } from "drizzle-kit"
|
||||
|
||||
console.log(Resource.Database)
|
||||
|
||||
export default defineConfig({
|
||||
out: "./migrations/",
|
||||
strict: true,
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user