mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
fix(pricing): align Go plan launch experience (#7167)
* fix(pricing): align Go plan launch experience * test(e2e): dismiss Go launch campaign in UI specs * fix(go-plan): finish launch campaign polish * test(e2e): preserve Go campaign visual coverage --------- Co-authored-by: Amy <1184569493@qq.com>
This commit is contained in:
@@ -21,7 +21,7 @@ const apiOrigin =
|
||||
<aside
|
||||
class="go-banner"
|
||||
data-go-banner
|
||||
data-window-start="2026-08-20T20:00:00+08:00"
|
||||
data-window-start="2026-08-20T17:00:00+08:00"
|
||||
data-window-end="2026-09-03T20:00:00+08:00"
|
||||
data-api-origin={apiOrigin}
|
||||
aria-label={copy.ariaLabel}
|
||||
|
||||
@@ -38,27 +38,27 @@ const DEMO_ASSET_BASE = '/pricing-e-final/assets';
|
||||
const popularModels: ModelItem[] = [
|
||||
{ name: 'DeepSeek V4 Flash', icon: '/agents/deepseek.svg' },
|
||||
{ name: 'GLM-5.2', icon: `${DEMO_ASSET_BASE}/zai-logo-official-Byn-xbrp.png`, iconClass: 'model-logo-zhipu' },
|
||||
{ name: 'Kimi K2.7 Code', icon: '/agents/moonshot.svg', iconClass: 'model-logo-kimi' },
|
||||
{ name: 'Kimi K2.7 Code', icon: '/model-icons/kimi.svg', iconClass: 'model-logo-kimi' },
|
||||
{ name: 'DeepSeek V4 Pro', icon: '/agents/deepseek.svg' },
|
||||
{ name: 'MiniMax M2.7', icon: '/agents/minimax.svg' },
|
||||
{ name: 'Kimi K2.6', icon: '/agents/moonshot.svg', iconClass: 'model-logo-kimi' },
|
||||
{ name: 'MiniMax M2.7', icon: '/model-icons/minimax.svg' },
|
||||
{ name: 'Kimi K2.6', icon: '/model-icons/kimi.svg', iconClass: 'model-logo-kimi' },
|
||||
{ name: 'MiMo V2.5 Pro', icon: `${DEMO_ASSET_BASE}/mimo-logo-user-CWOWEwG5.png`, iconClass: 'model-logo-mimo' },
|
||||
{ name: 'GLM-5.1', icon: `${DEMO_ASSET_BASE}/zai-logo-official-Byn-xbrp.png`, iconClass: 'model-logo-zhipu' },
|
||||
];
|
||||
|
||||
const flagshipModels: ModelItem[] = [
|
||||
{ name: 'Claude Fable 5', icon: '/agents/anthropic.svg' },
|
||||
{ name: 'Kimi-K3', icon: '/agents/moonshot.svg', iconClass: 'model-logo-kimi' },
|
||||
{ name: 'Claude Fable 5', icon: '/model-icons/claude.svg' },
|
||||
{ name: 'Kimi-K3', icon: '/model-icons/kimi.svg', iconClass: 'model-logo-kimi' },
|
||||
{ name: 'GPT-5.6-Sol', icon: '/agents/openai.svg' },
|
||||
{ name: 'Claude Opus 5', icon: '/agents/anthropic.svg' },
|
||||
{ name: 'Claude Opus 5', icon: '/model-icons/claude.svg' },
|
||||
{ name: 'GPT-5.5-Pro', icon: '/agents/openai.svg' },
|
||||
{ name: 'Claude Opus 4.7', icon: '/agents/anthropic.svg' },
|
||||
{ name: 'Claude Opus 4.8', icon: '/agents/anthropic.svg' },
|
||||
{ name: 'Claude Sonnet 5', icon: '/agents/anthropic.svg' },
|
||||
{ name: 'Claude Opus 4.6', icon: '/agents/anthropic.svg' },
|
||||
{ name: 'Claude Sonnet 4.6', icon: '/agents/anthropic.svg' },
|
||||
{ name: 'Claude Opus 4.7', icon: '/model-icons/claude.svg' },
|
||||
{ name: 'Claude Opus 4.8', icon: '/model-icons/claude.svg' },
|
||||
{ name: 'Claude Sonnet 5', icon: '/model-icons/claude.svg' },
|
||||
{ name: 'Claude Opus 4.6', icon: '/model-icons/claude.svg' },
|
||||
{ name: 'Claude Sonnet 4.6', icon: '/model-icons/claude.svg' },
|
||||
{ name: 'GPT-5.5', icon: '/agents/openai.svg' },
|
||||
{ name: 'Grok-4.5', icon: '/agents/xai.svg' },
|
||||
{ name: 'Grok-4.5', icon: '/model-icons/grok.svg' },
|
||||
];
|
||||
|
||||
const plusFlagshipModels = flagshipModels.filter(
|
||||
@@ -66,8 +66,8 @@ const plusFlagshipModels = flagshipModels.filter(
|
||||
);
|
||||
|
||||
const imageModels: ModelItem[] = [
|
||||
{ name: 'Seedream 5 Lite / Pro', icon: '/model-icons/bytedance.svg' },
|
||||
{ name: 'Nano Banana 2', icon: '/agents/gemini.svg', iconClass: 'model-logo-nanobanana' },
|
||||
{ name: 'Seedream 5 Lite / Pro', icon: '/model-icons/jimeng.svg' },
|
||||
{ name: 'Nano Banana 2', icon: '/model-icons/nanobanana.svg', iconClass: 'model-logo-nanobanana' },
|
||||
{ name: 'GPT Image 2', icon: '/agents/openai.svg' },
|
||||
];
|
||||
|
||||
@@ -155,6 +155,15 @@ const tierBenefits: Record<TierId, string[]> = {
|
||||
max: paidTierBenefits('max'),
|
||||
};
|
||||
|
||||
const compactSavings = (amountUsd: number, interval: 'monthly' | 'yearly') => {
|
||||
const amount = locale === 'en'
|
||||
? formatUsd(amountUsd).replace('$', '')
|
||||
: formatUsd(amountUsd);
|
||||
return locale === 'en' && interval === 'monthly' ? `${amount}/mo` : amount;
|
||||
};
|
||||
const compactBillingTotal = (amountUsd: number) =>
|
||||
locale === 'en' ? formatUsd(amountUsd).replace('$', '') : formatUsd(amountUsd);
|
||||
|
||||
function individualView(tier: PlanTierConfig) {
|
||||
const yearlyEquivalent = Math.round(tier.yearly.priceUsd / 12);
|
||||
const yearlySave = tier.monthly.priceUsd * 12 - tier.yearly.priceUsd;
|
||||
@@ -166,7 +175,10 @@ function individualView(tier: PlanTierConfig) {
|
||||
amount: formatUsd(tier.monthly.introPriceUsd).replace('$', ''),
|
||||
strike: formatUsd(tier.monthly.priceUsd),
|
||||
discount: introDiscount,
|
||||
saving: formatUsd(tier.monthly.priceUsd - tier.monthly.introPriceUsd),
|
||||
saving: compactSavings(
|
||||
tier.monthly.priceUsd - tier.monthly.introPriceUsd,
|
||||
'monthly',
|
||||
),
|
||||
billed: fillTemplate(L.monthlyRenewal, {
|
||||
amountUsd: formatUsd(tier.monthly.priceUsd),
|
||||
}),
|
||||
@@ -175,9 +187,9 @@ function individualView(tier: PlanTierConfig) {
|
||||
amount: formatUsd(yearlyEquivalent).replace('$', ''),
|
||||
strike: formatUsd(tier.monthly.priceUsd),
|
||||
discount: tier.yearly.discountPct,
|
||||
saving: formatUsd(yearlySave),
|
||||
saving: compactSavings(yearlySave, 'yearly'),
|
||||
billed: fillTemplate(L.yearlySubline, {
|
||||
totalUsd: formatUsd(tier.yearly.priceUsd),
|
||||
totalUsd: compactBillingTotal(tier.yearly.priceUsd),
|
||||
savingsUsd: formatUsd(yearlySave),
|
||||
}),
|
||||
},
|
||||
@@ -197,7 +209,10 @@ const goView = {
|
||||
amount: formatUsd(GO_PLAN.monthly.introPriceUsd).replace('$', ''),
|
||||
strike: formatUsd(GO_PLAN.monthly.priceUsd),
|
||||
discount: goMonthlyDiscount,
|
||||
saving: formatUsd(GO_PLAN.monthly.priceUsd - GO_PLAN.monthly.introPriceUsd),
|
||||
saving: compactSavings(
|
||||
GO_PLAN.monthly.priceUsd - GO_PLAN.monthly.introPriceUsd,
|
||||
'monthly',
|
||||
),
|
||||
billed: fillTemplate(L.monthlyRenewal, {
|
||||
amountUsd: formatUsd(GO_PLAN.monthly.priceUsd),
|
||||
}),
|
||||
@@ -206,9 +221,9 @@ const goView = {
|
||||
amount: formatUsd(goYearlyEquivalent).replace('$', ''),
|
||||
strike: formatUsd(GO_PLAN.monthly.priceUsd),
|
||||
discount: goYearlyDiscount,
|
||||
saving: formatUsd(goYearlySave),
|
||||
saving: compactSavings(goYearlySave, 'yearly'),
|
||||
billed: fillTemplate(L.yearlySubline, {
|
||||
totalUsd: formatUsd(GO_PLAN.yearly.priceUsd),
|
||||
totalUsd: compactBillingTotal(GO_PLAN.yearly.priceUsd),
|
||||
savingsUsd: formatUsd(goYearlySave),
|
||||
}),
|
||||
},
|
||||
@@ -241,11 +256,11 @@ const usageRows = [
|
||||
|
||||
const comparisonPopular = [
|
||||
'DeepSeek V4 Flash',
|
||||
'Kimi K2.7 Code',
|
||||
'DeepSeek V4 Pro',
|
||||
'GLM-5.2',
|
||||
'Kimi K2.7 Code',
|
||||
'MiniMax M2.7',
|
||||
'MiMo V2.5 Pro',
|
||||
'GLM-5.2',
|
||||
'Kimi K2.6',
|
||||
'GLM-5.1',
|
||||
].map((name) => popularModels.find((model) => model.name === name)!);
|
||||
@@ -368,8 +383,7 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
|
||||
</div>
|
||||
|
||||
<a class="pricing-card-cta" href={cloudSubscribeUrl(tier, 'yearly')} data-pricing-cta data-tier={tier}>
|
||||
<span data-when="monthly">{tierCopy[tier].ctaLabel} · {L.monthly}</span>
|
||||
<span data-when="yearly">{tierCopy[tier].ctaLabel} · {L.yearly}</span>
|
||||
<span>{tierCopy[tier].ctaLabel}</span>
|
||||
</a>
|
||||
|
||||
<div class="plan-feature-details">
|
||||
@@ -385,7 +399,7 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
|
||||
<li class="model-with-status">
|
||||
<span class:list={['model-logo', model.iconClass]}><img src={model.icon} alt="" loading="lazy" /></span>
|
||||
<span>{model.name}</span>
|
||||
<em class={popularStatus.kind}>{popularStatus.text}</em>
|
||||
{popularStatus.kind === 'unlimited' && <em class={popularStatus.kind}>{popularStatus.text}</em>}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
@@ -397,7 +411,7 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
|
||||
|
||||
<section class:list={['plan-model-module', { 'unavailable-model-module': tier === 'go', 'recommended-model-module': tier === 'pro' || tier === 'max' }]} data-model-module>
|
||||
<header>
|
||||
<h4>{tier === 'go' ? P.flagshipModels : `${fillTemplate(P.modelCount, { count: `${flagship.length}+` })} · ${P.flagshipModels}`}</h4>
|
||||
<h4>{tier === 'go' ? P.flagshipModels : fillTemplate(P.flagshipModelCount, { count: String(flagship.length) })}</h4>
|
||||
{tier === 'go' && <span>{P.noAccess}</span>}
|
||||
</header>
|
||||
<ul>
|
||||
@@ -480,7 +494,6 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
|
||||
</div>
|
||||
<div class="individual-usage-note">
|
||||
<p>{P.usageAllowanceNote}</p>
|
||||
<p>{P.usagePeakNote}</p>
|
||||
<p>{P.usageEstimatesNote}</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -529,7 +542,11 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
|
||||
<td class="model-name-cell"><span class:list={['model-logo', model.iconClass]}><img src={model.icon} alt="" loading="lazy" /></span><span>{model.name}</span></td>
|
||||
{(['go', 'plus', 'pro', 'max'] as TierId[]).map((tier) => {
|
||||
const status = accessStatus(category, model.name, tier);
|
||||
return <td><span class={`model-access-status ${status.kind}`}><i class:list={['status-icon', { infinity: status.kind === 'unlimited', check: status.kind === 'more-ample' }]}>{status.kind === 'unlimited' ? '∞' : ''}</i>{status.text}</span></td>;
|
||||
return status.kind === 'more-ample' ? (
|
||||
<td><span class={`model-access-status ${status.kind}`} aria-label={status.text}><i class="status-icon check"></i></span></td>
|
||||
) : (
|
||||
<td><span class={`model-access-status ${status.kind}`}><i class:list={['status-icon', { infinity: status.kind === 'unlimited' }]}>{status.kind === 'unlimited' ? '∞' : ''}</i>{status.text}</span></td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
)),
|
||||
@@ -630,7 +647,7 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
|
||||
padding: 0 12px;
|
||||
overflow: hidden;
|
||||
color: #173111;
|
||||
font-size: 9px;
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.055em;
|
||||
@@ -640,12 +657,19 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
|
||||
}
|
||||
|
||||
.new-plan-ribbon span {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: #173111;
|
||||
background: linear-gradient(110deg, #173111 0% 39%, #397f2b 47%, #d8ff70 52%, #397f2b 57%, #173111 65% 100%) 100% 0 / 300% 100%;
|
||||
-webkit-text-fill-color: transparent;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
animation: pricing-ribbon-text-shimmer 4.2s linear infinite;
|
||||
}
|
||||
|
||||
.plan-top {
|
||||
display: block;
|
||||
min-height: 58px;
|
||||
min-height: 78px;
|
||||
}
|
||||
|
||||
.plan-title-row {
|
||||
@@ -708,12 +732,14 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
|
||||
|
||||
.plan-top p {
|
||||
max-width: none;
|
||||
min-height: 32px;
|
||||
margin: 8px 0 0;
|
||||
color: #60645c;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 1.35;
|
||||
white-space: nowrap;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.plan-max .plan-top p { color: #aeb5aa; }
|
||||
@@ -758,10 +784,11 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
|
||||
.plan-max .price del { color: #8f978b; }
|
||||
|
||||
.compact-price-detail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: 7px;
|
||||
min-height: 34px;
|
||||
min-height: 46px;
|
||||
color: #555a52;
|
||||
}
|
||||
|
||||
@@ -774,11 +801,14 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
|
||||
}
|
||||
|
||||
.compact-price-detail > small {
|
||||
margin-left: auto;
|
||||
min-width: 0;
|
||||
margin-left: 0;
|
||||
color: #51554d;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.plan-max .compact-price-detail > span.price-savings-note { color: #83d96c; }
|
||||
@@ -1531,8 +1561,11 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.model-access-status.unlimited,
|
||||
.model-access-status.more-ample { gap: 8px; }
|
||||
.model-access-status.unlimited { gap: 8px; }
|
||||
.model-access-status.more-ample {
|
||||
width: 23px;
|
||||
height: 23px;
|
||||
}
|
||||
.model-access-status.resolution { font-weight: 500; }
|
||||
|
||||
.status-icon {
|
||||
@@ -1636,6 +1669,10 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
|
||||
from { opacity: 0.4; transform: scaleX(0); }
|
||||
to { opacity: 1; transform: scaleX(1); }
|
||||
}
|
||||
@keyframes pricing-ribbon-text-shimmer {
|
||||
from { background-position: 100% 0; }
|
||||
to { background-position: -100% 0; }
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.four-tier-grid {
|
||||
@@ -1669,6 +1706,12 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
|
||||
animation: none;
|
||||
transform: none;
|
||||
}
|
||||
.new-plan-ribbon span {
|
||||
color: #173111;
|
||||
-webkit-text-fill-color: #173111;
|
||||
background: none;
|
||||
animation: none;
|
||||
}
|
||||
.pricing-card-cta { transition: none; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,6 +9,8 @@ export interface PricingCampaignContent {
|
||||
modelBenefits: readonly [string, string];
|
||||
paidBenefitNote: string;
|
||||
teamBenefitNote: string;
|
||||
teamOfferTitle: string;
|
||||
teamOfferModels: string;
|
||||
disclaimer: string;
|
||||
linkLabel: string;
|
||||
closeLabel: string;
|
||||
@@ -24,6 +26,8 @@ export const PRICING_CAMPAIGN_CONTENT_BY_LOCALE = {
|
||||
modelBenefits: ['Unlimited DeepSeek V4 Pro', 'Unlimited DeepSeek V4 Flash'],
|
||||
paidBenefitNote: 'Aug 13—Aug 27 · FREE for two weeks',
|
||||
teamBenefitNote: 'Aug 13—Aug 27 · FREE for two weeks',
|
||||
teamOfferTitle: 'Unlimited model access',
|
||||
teamOfferModels: 'DeepSeek V4 Flash and V4 Pro',
|
||||
disclaimer: 'Unlimited model quota and free generations included in a plan are available only in OpenDesign; they cannot be used through MCP/CLI/API or in other scenarios. Some models may require queuing during peak hours. The organizer reserves the right of final interpretation.',
|
||||
linkLabel: 'View campaign benefits', closeLabel: 'Dismiss campaign banner',
|
||||
},
|
||||
@@ -36,6 +40,8 @@ export const PRICING_CAMPAIGN_CONTENT_BY_LOCALE = {
|
||||
modelBenefits: ['DeepSeek V4 Pro 无限使用', 'DeepSeek V4 Flash 无限使用'],
|
||||
paidBenefitNote: '8月13日—8月27日 · 两周免费用',
|
||||
teamBenefitNote: '8月13日—8月27日 · 两周免费用',
|
||||
teamOfferTitle: '无限模型使用权益',
|
||||
teamOfferModels: 'DeepSeek V4 Flash 与 V4 Pro',
|
||||
disclaimer: '套餐内的无限制模型额度与免费生成次数,仅可通过OpenDesign使用;无法在MCP/CLI/API及其他场景使用。部分模型高峰期需要排队。解释权归官方所有。',
|
||||
linkLabel: '查看活动权益', closeLabel: '关闭活动横幅',
|
||||
},
|
||||
@@ -48,6 +54,8 @@ export const PRICING_CAMPAIGN_CONTENT_BY_LOCALE = {
|
||||
modelBenefits: ['DeepSeek V4 Pro 無限使用', 'DeepSeek V4 Flash 無限使用'],
|
||||
paidBenefitNote: '8月13日—8月27日 · 兩週免費用',
|
||||
teamBenefitNote: '8月13日—8月27日 · 兩週免費用',
|
||||
teamOfferTitle: '無限模型使用權益',
|
||||
teamOfferModels: 'DeepSeek V4 Flash 與 V4 Pro',
|
||||
disclaimer: '方案內的無限制模型額度與免費生成次數,僅可透過 OpenDesign 使用;無法在 MCP/CLI/API 及其他場景使用。部分模型在高峰時段可能需要排隊。最終解釋權歸官方所有。',
|
||||
linkLabel: '查看活動權益', closeLabel: '關閉活動橫幅',
|
||||
},
|
||||
@@ -60,6 +68,8 @@ export const PRICING_CAMPAIGN_CONTENT_BY_LOCALE = {
|
||||
modelBenefits: ['DeepSeek V4 Proを無制限で利用', 'DeepSeek V4 Flashを無制限で利用'],
|
||||
paidBenefitNote: '8月13日〜8月27日 · 2週間無料',
|
||||
teamBenefitNote: '8月13日〜8月27日 · 2週間無料',
|
||||
teamOfferTitle: 'モデルを無制限に利用',
|
||||
teamOfferModels: 'DeepSeek V4 Flash と V4 Pro',
|
||||
disclaimer: 'プランに含まれる無制限のモデル枠と無料生成回数は、OpenDesign内でのみ利用できます。MCP/CLI/APIなど、その他の環境では利用できません。一部のモデルはピーク時に待ち時間が発生する場合があります。最終的な解釈権は運営者に帰属します。',
|
||||
linkLabel: '特典を見る', closeLabel: 'キャンペーンバナーを閉じる',
|
||||
},
|
||||
@@ -72,6 +82,8 @@ export const PRICING_CAMPAIGN_CONTENT_BY_LOCALE = {
|
||||
modelBenefits: ['DeepSeek V4 Pro 무제한 사용', 'DeepSeek V4 Flash 무제한 사용'],
|
||||
paidBenefitNote: '8월 13일—8월 27일 · 2주 무료',
|
||||
teamBenefitNote: '8월 13일—8월 27일 · 2주 무료',
|
||||
teamOfferTitle: '모델 무제한 사용',
|
||||
teamOfferModels: 'DeepSeek V4 Flash 및 V4 Pro',
|
||||
disclaimer: '플랜에 포함된 무제한 모델 한도와 무료 생성 횟수는 OpenDesign에서만 사용할 수 있으며 MCP/CLI/API 또는 기타 환경에서는 사용할 수 없습니다. 일부 모델은 피크 시간대에 대기해야 할 수 있습니다. 최종 해석 권한은 운영사에 있습니다.',
|
||||
linkLabel: '이벤트 혜택 보기', closeLabel: '이벤트 배너 닫기',
|
||||
},
|
||||
@@ -84,6 +96,8 @@ export const PRICING_CAMPAIGN_CONTENT_BY_LOCALE = {
|
||||
modelBenefits: ['DeepSeek V4 Pro unbegrenzt nutzen', 'DeepSeek V4 Flash unbegrenzt nutzen'],
|
||||
paidBenefitNote: '13.—27. August · zwei Wochen kostenlos',
|
||||
teamBenefitNote: '13.—27. August · zwei Wochen kostenlos',
|
||||
teamOfferTitle: 'Unbegrenzter Modellzugang',
|
||||
teamOfferModels: 'DeepSeek V4 Flash und V4 Pro',
|
||||
disclaimer: 'Das im Tarif enthaltene unbegrenzte Modellkontingent und die kostenlosen Generierungen können nur in OpenDesign genutzt werden, nicht über MCP/CLI/API oder in anderen Umgebungen. Bei einigen Modellen kann es zu Spitzenzeiten zu Wartezeiten kommen. Der Veranstalter behält sich die endgültige Auslegung vor.',
|
||||
linkLabel: 'Aktionsvorteile ansehen', closeLabel: 'Aktionsbanner schließen',
|
||||
},
|
||||
@@ -96,6 +110,8 @@ export const PRICING_CAMPAIGN_CONTENT_BY_LOCALE = {
|
||||
modelBenefits: ['DeepSeek V4 Pro en illimité', 'DeepSeek V4 Flash en illimité'],
|
||||
paidBenefitNote: 'Du 13 au 27 août · gratuits pendant deux semaines',
|
||||
teamBenefitNote: 'Du 13 au 27 août · gratuits pendant deux semaines',
|
||||
teamOfferTitle: 'Accès illimité aux modèles',
|
||||
teamOfferModels: 'DeepSeek V4 Flash et V4 Pro',
|
||||
disclaimer: 'Le quota de modèles illimité et les générations gratuites inclus dans le forfait sont utilisables uniquement dans OpenDesign, et non via MCP/CLI/API ni dans d’autres contextes. Certains modèles peuvent nécessiter une mise en file d’attente aux heures de pointe. L’organisateur se réserve le droit d’interprétation finale.',
|
||||
linkLabel: 'Voir les avantages de la campagne', closeLabel: 'Fermer la bannière',
|
||||
},
|
||||
@@ -108,6 +124,8 @@ export const PRICING_CAMPAIGN_CONTENT_BY_LOCALE = {
|
||||
modelBenefits: ['DeepSeek V4 Pro без ограничений', 'DeepSeek V4 Flash без ограничений'],
|
||||
paidBenefitNote: '13—27 августа · две недели бесплатно',
|
||||
teamBenefitNote: '13—27 августа · две недели бесплатно',
|
||||
teamOfferTitle: 'Безлимитный доступ к моделям',
|
||||
teamOfferModels: 'DeepSeek V4 Flash и V4 Pro',
|
||||
disclaimer: 'Безлимитная квота моделей и бесплатные генерации, включённые в тариф, доступны только в OpenDesign. Они недоступны через MCP/CLI/API или в других сценариях. Для некоторых моделей в часы пик может потребоваться ожидание в очереди. Организатор оставляет за собой право окончательного толкования.',
|
||||
linkLabel: 'Посмотреть преимущества', closeLabel: 'Закрыть баннер',
|
||||
},
|
||||
@@ -120,6 +138,8 @@ export const PRICING_CAMPAIGN_CONTENT_BY_LOCALE = {
|
||||
modelBenefits: ['Uso ilimitado de DeepSeek V4 Pro', 'Uso ilimitado de DeepSeek V4 Flash'],
|
||||
paidBenefitNote: 'Del 13 al 27 de agosto · gratis durante dos semanas',
|
||||
teamBenefitNote: 'Del 13 al 27 de agosto · gratis durante dos semanas',
|
||||
teamOfferTitle: 'Acceso ilimitado a modelos',
|
||||
teamOfferModels: 'DeepSeek V4 Flash y V4 Pro',
|
||||
disclaimer: 'La cuota ilimitada de modelos y las generaciones gratuitas incluidas en el plan solo pueden utilizarse en OpenDesign, no mediante MCP/CLI/API ni en otros entornos. Algunos modelos pueden requerir espera en horas punta. El organizador se reserva el derecho de interpretación final.',
|
||||
linkLabel: 'Ver beneficios', closeLabel: 'Cerrar el banner',
|
||||
},
|
||||
@@ -132,6 +152,8 @@ export const PRICING_CAMPAIGN_CONTENT_BY_LOCALE = {
|
||||
modelBenefits: ['Uso ilimitado do DeepSeek V4 Pro', 'Uso ilimitado do DeepSeek V4 Flash'],
|
||||
paidBenefitNote: '13 a 27 de agosto · grátis por duas semanas',
|
||||
teamBenefitNote: '13 a 27 de agosto · grátis por duas semanas',
|
||||
teamOfferTitle: 'Acesso ilimitado aos modelos',
|
||||
teamOfferModels: 'DeepSeek V4 Flash e V4 Pro',
|
||||
disclaimer: 'A cota ilimitada de modelos e as gerações gratuitas incluídas no plano só podem ser usadas no OpenDesign, e não via MCP/CLI/API nem em outros cenários. Alguns modelos podem exigir espera em horários de pico. O organizador se reserva o direito de interpretação final.',
|
||||
linkLabel: 'Ver benefícios', closeLabel: 'Fechar banner',
|
||||
},
|
||||
@@ -144,6 +166,8 @@ export const PRICING_CAMPAIGN_CONTENT_BY_LOCALE = {
|
||||
modelBenefits: ['DeepSeek V4 Pro senza limiti', 'DeepSeek V4 Flash senza limiti'],
|
||||
paidBenefitNote: '13—27 agosto · gratis per due settimane',
|
||||
teamBenefitNote: '13—27 agosto · gratis per due settimane',
|
||||
teamOfferTitle: 'Accesso illimitato ai modelli',
|
||||
teamOfferModels: 'DeepSeek V4 Flash e V4 Pro',
|
||||
disclaimer: 'La quota modelli illimitata e le generazioni gratuite incluse nel piano sono utilizzabili solo in OpenDesign, non tramite MCP/CLI/API né in altri contesti. Alcuni modelli potrebbero richiedere attesa nelle ore di punta. L’organizzatore si riserva il diritto di interpretazione finale.',
|
||||
linkLabel: 'Scopri i vantaggi', closeLabel: 'Chiudi il banner',
|
||||
},
|
||||
@@ -156,6 +180,8 @@ export const PRICING_CAMPAIGN_CONTENT_BY_LOCALE = {
|
||||
modelBenefits: ['DeepSeek V4 Pro sınırsız kullanım', 'DeepSeek V4 Flash sınırsız kullanım'],
|
||||
paidBenefitNote: '13—27 Ağustos · iki hafta ücretsiz',
|
||||
teamBenefitNote: '13—27 Ağustos · iki hafta ücretsiz',
|
||||
teamOfferTitle: 'Sınırsız model erişimi',
|
||||
teamOfferModels: 'DeepSeek V4 Flash ve V4 Pro',
|
||||
disclaimer: 'Paket kapsamındaki sınırsız model kotası ve ücretsiz üretim hakları yalnızca OpenDesign içinde kullanılabilir; MCP/CLI/API veya diğer senaryolarda kullanılamaz. Bazı modeller yoğun saatlerde sıraya alınabilir. Nihai yorum hakkı organizatöre aittir.',
|
||||
linkLabel: 'Kampanya avantajlarını gör', closeLabel: 'Kampanya bandını kapat',
|
||||
},
|
||||
|
||||
@@ -105,6 +105,7 @@ export interface PersonalPricingCopy {
|
||||
creatorDeveloperSupport: string;
|
||||
popularModels: string;
|
||||
flagshipModels: string;
|
||||
flagshipModelCount: string;
|
||||
imageModels: string;
|
||||
modelCount: string;
|
||||
unlimited: string;
|
||||
@@ -122,7 +123,6 @@ export interface PersonalPricingCopy {
|
||||
estimatedRequests: string;
|
||||
estimatedRequestsAria: string;
|
||||
usageAllowanceNote: string;
|
||||
usagePeakNote: string;
|
||||
usageEstimatesNote: string;
|
||||
comparisonTitle: string;
|
||||
category: string;
|
||||
@@ -244,6 +244,7 @@ const PERSONAL_EN: PersonalPricingCopy = {
|
||||
creatorDeveloperSupport: 'Creator / developer support',
|
||||
popularModels: 'Popular models',
|
||||
flagshipModels: 'Flagship models',
|
||||
flagshipModelCount: '{count}+ flagship models',
|
||||
imageModels: 'Image models',
|
||||
modelCount: '{count} models',
|
||||
unlimited: 'Unlimited',
|
||||
@@ -261,7 +262,6 @@ const PERSONAL_EN: PersonalPricingCopy = {
|
||||
estimatedRequests: 'Estimated requests every 5 hours',
|
||||
estimatedRequestsAria: 'Estimated model requests every 5 hours',
|
||||
usageAllowanceNote: 'Popular models include ample allowance, roughly equivalent to $16 every 5 hours · $40 weekly · $80 monthly. Actual usage varies by model and context length.',
|
||||
usagePeakNote: 'DeepSeek V4 Flash / Pro estimates use off-peak pricing; unit usage counts ×2 during peak hours (Beijing time 09:00–12:00 and 14:00–18:00).',
|
||||
usageEstimatesNote: 'Estimated request counts by model may change with model pricing, usage patterns, and user feedback.',
|
||||
comparisonTitle: 'Compare every model across plans',
|
||||
category: 'Category',
|
||||
@@ -289,6 +289,7 @@ const PERSONAL_ZH_CN: PersonalPricingCopy = {
|
||||
creatorDeveloperSupport: '创作者 / 开发者支持',
|
||||
popularModels: '热门模型',
|
||||
flagshipModels: '旗舰模型',
|
||||
flagshipModelCount: '{count}+ 个旗舰模型',
|
||||
imageModels: '图片模型',
|
||||
modelCount: '{count} 个模型',
|
||||
unlimited: '无限量',
|
||||
@@ -306,7 +307,6 @@ const PERSONAL_ZH_CN: PersonalPricingCopy = {
|
||||
estimatedRequests: '每 5 小时预估可用次数',
|
||||
estimatedRequestsAria: '模型每 5 小时预估请求数',
|
||||
usageAllowanceNote: '热门模型提供充足额度,大致相当于每 5 小时 $16 · 每周 $40 · 每月 $80。实际用量因模型和上下文长度而异。',
|
||||
usagePeakNote: 'DeepSeek V4 Flash / Pro 按非高峰价格估算;高峰时段(北京时间 09:00–12:00、14:00–18:00)单位消耗 ×2。',
|
||||
usageEstimatesNote: '各模型的预估次数可能随模型价格、使用情况与用户反馈调整。',
|
||||
comparisonTitle: '对比各套餐的全部模型',
|
||||
category: '分类',
|
||||
@@ -334,6 +334,7 @@ const PERSONAL_ZH_TW: PersonalPricingCopy = {
|
||||
creatorDeveloperSupport: '創作者 / 開發者支援',
|
||||
popularModels: '熱門模型',
|
||||
flagshipModels: '旗艦模型',
|
||||
flagshipModelCount: '{count}+ 個旗艦模型',
|
||||
imageModels: '圖片模型',
|
||||
modelCount: '{count} 個模型',
|
||||
unlimited: '無限量',
|
||||
@@ -351,7 +352,6 @@ const PERSONAL_ZH_TW: PersonalPricingCopy = {
|
||||
estimatedRequests: '每 5 小時預估可用次數',
|
||||
estimatedRequestsAria: '模型每 5 小時預估請求數',
|
||||
usageAllowanceNote: '熱門模型提供充足額度,大致相當於每 5 小時 $16 · 每週 $40 · 每月 $80。實際用量依模型與上下文長度而異。',
|
||||
usagePeakNote: 'DeepSeek V4 Flash / Pro 依離峰價格估算;尖峰時段(北京時間 09:00–12:00、14:00–18:00)單位用量 ×2。',
|
||||
usageEstimatesNote: '各模型的預估次數可能依模型價格、使用情況與使用者回饋調整。',
|
||||
comparisonTitle: '比較各方案的全部模型',
|
||||
category: '分類',
|
||||
@@ -379,6 +379,7 @@ const PERSONAL_ES: PersonalPricingCopy = {
|
||||
creatorDeveloperSupport: 'Soporte para creadores / desarrolladores',
|
||||
popularModels: 'Modelos populares',
|
||||
flagshipModels: 'Modelos insignia',
|
||||
flagshipModelCount: '{count}+ modelos insignia',
|
||||
imageModels: 'Modelos de imagen',
|
||||
modelCount: '{count} modelos',
|
||||
unlimited: 'Ilimitado',
|
||||
@@ -396,7 +397,6 @@ const PERSONAL_ES: PersonalPricingCopy = {
|
||||
estimatedRequests: 'Solicitudes estimadas cada 5 horas',
|
||||
estimatedRequestsAria: 'Solicitudes de modelo estimadas cada 5 horas',
|
||||
usageAllowanceNote: 'Los modelos populares incluyen una capacidad amplia, equivalente aproximadamente a $16 cada 5 horas · $40 por semana · $80 al mes. El uso real varía según el modelo y la longitud del contexto.',
|
||||
usagePeakNote: 'Las estimaciones de DeepSeek V4 Flash / Pro usan precios fuera de hora punta; el consumo por unidad se duplica en hora punta (hora de Pekín 09:00–12:00 y 14:00–18:00).',
|
||||
usageEstimatesNote: 'Las solicitudes estimadas pueden cambiar según el precio del modelo, los patrones de uso y los comentarios de los usuarios.',
|
||||
comparisonTitle: 'Compara todos los modelos entre planes',
|
||||
category: 'Categoría',
|
||||
@@ -424,6 +424,7 @@ const PERSONAL_PT_BR: PersonalPricingCopy = {
|
||||
creatorDeveloperSupport: 'Suporte para criadores / desenvolvedores',
|
||||
popularModels: 'Modelos populares',
|
||||
flagshipModels: 'Modelos de ponta',
|
||||
flagshipModelCount: '{count}+ modelos de ponta',
|
||||
imageModels: 'Modelos de imagem',
|
||||
modelCount: '{count} modelos',
|
||||
unlimited: 'Ilimitado',
|
||||
@@ -441,7 +442,6 @@ const PERSONAL_PT_BR: PersonalPricingCopy = {
|
||||
estimatedRequests: 'Solicitações estimadas a cada 5 horas',
|
||||
estimatedRequestsAria: 'Solicitações de modelo estimadas a cada 5 horas',
|
||||
usageAllowanceNote: 'Os modelos populares incluem uma franquia ampla, aproximadamente equivalente a $16 a cada 5 horas · $40 por semana · $80 por mês. O uso real varia conforme o modelo e o tamanho do contexto.',
|
||||
usagePeakNote: 'As estimativas do DeepSeek V4 Flash / Pro usam preços fora do horário de pico; o consumo por unidade dobra no pico (horário de Pequim 09:00–12:00 e 14:00–18:00).',
|
||||
usageEstimatesNote: 'As solicitações estimadas podem mudar conforme o preço do modelo, os padrões de uso e o feedback dos usuários.',
|
||||
comparisonTitle: 'Compare todos os modelos entre os planos',
|
||||
category: 'Categoria',
|
||||
@@ -469,6 +469,7 @@ const PERSONAL_RU: PersonalPricingCopy = {
|
||||
creatorDeveloperSupport: 'Поддержка авторов / разработчиков',
|
||||
popularModels: 'Популярные модели',
|
||||
flagshipModels: 'Флагманские модели',
|
||||
flagshipModelCount: '{count}+ флагманских моделей',
|
||||
imageModels: 'Модели изображений',
|
||||
modelCount: '{count} моделей',
|
||||
unlimited: 'Без ограничений',
|
||||
@@ -486,7 +487,6 @@ const PERSONAL_RU: PersonalPricingCopy = {
|
||||
estimatedRequests: 'Оценка запросов каждые 5 часов',
|
||||
estimatedRequestsAria: 'Оценка запросов к моделям каждые 5 часов',
|
||||
usageAllowanceNote: 'Популярные модели включают большой лимит, примерно равный $16 каждые 5 часов · $40 в неделю · $80 в месяц. Фактический расход зависит от модели и длины контекста.',
|
||||
usagePeakNote: 'Оценки DeepSeek V4 Flash / Pro используют внепиковые цены; в часы пик (по пекинскому времени 09:00–12:00 и 14:00–18:00) расход единиц удваивается.',
|
||||
usageEstimatesNote: 'Оценка числа запросов может меняться вместе с ценой модели, характером использования и отзывами пользователей.',
|
||||
comparisonTitle: 'Сравните все модели во всех планах',
|
||||
category: 'Категория',
|
||||
@@ -514,6 +514,7 @@ const PERSONAL_FR: PersonalPricingCopy = {
|
||||
creatorDeveloperSupport: 'Support créateur / développeur',
|
||||
popularModels: 'Modèles populaires',
|
||||
flagshipModels: 'Modèles phares',
|
||||
flagshipModelCount: '{count}+ modèles phares',
|
||||
imageModels: 'Modèles d’image',
|
||||
modelCount: '{count} modèles',
|
||||
unlimited: 'Illimité',
|
||||
@@ -531,7 +532,6 @@ const PERSONAL_FR: PersonalPricingCopy = {
|
||||
estimatedRequests: 'Requêtes estimées toutes les 5 heures',
|
||||
estimatedRequestsAria: 'Requêtes de modèles estimées toutes les 5 heures',
|
||||
usageAllowanceNote: 'Les modèles populaires incluent un quota généreux, soit environ 16 $ toutes les 5 heures · 40 $ par semaine · 80 $ par mois. L’usage réel varie selon le modèle et la longueur du contexte.',
|
||||
usagePeakNote: 'Les estimations DeepSeek V4 Flash / Pro utilisent les tarifs hors pointe ; la consommation unitaire est doublée aux heures de pointe (heure de Pékin 09:00–12:00 et 14:00–18:00).',
|
||||
usageEstimatesNote: 'Le nombre estimé de requêtes peut évoluer avec le prix des modèles, les usages et les retours des utilisateurs.',
|
||||
comparisonTitle: 'Comparez tous les modèles selon les offres',
|
||||
category: 'Catégorie',
|
||||
@@ -559,6 +559,7 @@ const PERSONAL_KO: PersonalPricingCopy = {
|
||||
creatorDeveloperSupport: '크리에이터 / 개발자 지원',
|
||||
popularModels: '인기 모델',
|
||||
flagshipModels: '플래그십 모델',
|
||||
flagshipModelCount: '{count}+ 플래그십 모델',
|
||||
imageModels: '이미지 모델',
|
||||
modelCount: '모델 {count}개',
|
||||
unlimited: '무제한',
|
||||
@@ -576,7 +577,6 @@ const PERSONAL_KO: PersonalPricingCopy = {
|
||||
estimatedRequests: '5시간마다 예상 사용 횟수',
|
||||
estimatedRequestsAria: '5시간마다 예상 모델 요청 수',
|
||||
usageAllowanceNote: '인기 모델에는 넉넉한 한도가 포함되며, 대략 5시간마다 $16 · 주당 $40 · 월 $80에 해당합니다. 실제 사용량은 모델과 컨텍스트 길이에 따라 달라집니다.',
|
||||
usagePeakNote: 'DeepSeek V4 Flash / Pro 예상치는 비혼잡 시간대 요금을 기준으로 하며, 혼잡 시간대(베이징 시간 09:00–12:00, 14:00–18:00)에는 단위 사용량이 2배입니다.',
|
||||
usageEstimatesNote: '모델별 예상 요청 수는 모델 가격, 사용 패턴, 사용자 피드백에 따라 조정될 수 있습니다.',
|
||||
comparisonTitle: '요금제별 전체 모델 비교',
|
||||
category: '분류',
|
||||
@@ -604,6 +604,7 @@ const PERSONAL_DE: PersonalPricingCopy = {
|
||||
creatorDeveloperSupport: 'Support für Kreative / Entwickler',
|
||||
popularModels: 'Beliebte Modelle',
|
||||
flagshipModels: 'Flaggschiffmodelle',
|
||||
flagshipModelCount: '{count}+ Flaggschiffmodelle',
|
||||
imageModels: 'Bildmodelle',
|
||||
modelCount: '{count} Modelle',
|
||||
unlimited: 'Unbegrenzt',
|
||||
@@ -621,7 +622,6 @@ const PERSONAL_DE: PersonalPricingCopy = {
|
||||
estimatedRequests: 'Geschätzte Anfragen alle 5 Stunden',
|
||||
estimatedRequestsAria: 'Geschätzte Modellanfragen alle 5 Stunden',
|
||||
usageAllowanceNote: 'Beliebte Modelle enthalten ein großzügiges Kontingent, ungefähr entsprechend $16 alle 5 Stunden · $40 pro Woche · $80 pro Monat. Die tatsächliche Nutzung variiert je nach Modell und Kontextlänge.',
|
||||
usagePeakNote: 'Die Schätzungen für DeepSeek V4 Flash / Pro verwenden Nebenzeitenpreise; zu Spitzenzeiten (Peking-Zeit 09:00–12:00 und 14:00–18:00) zählt der Verbrauch doppelt.',
|
||||
usageEstimatesNote: 'Die geschätzten Anfragen pro Modell können sich mit Modellpreisen, Nutzungsmustern und Nutzerfeedback ändern.',
|
||||
comparisonTitle: 'Alle Modelle nach Tarif vergleichen',
|
||||
category: 'Kategorie',
|
||||
@@ -649,6 +649,7 @@ const PERSONAL_JA: PersonalPricingCopy = {
|
||||
creatorDeveloperSupport: 'クリエイター / 開発者サポート',
|
||||
popularModels: '人気モデル',
|
||||
flagshipModels: 'フラッグシップモデル',
|
||||
flagshipModelCount: 'フラッグシップモデル {count} 種以上',
|
||||
imageModels: '画像モデル',
|
||||
modelCount: '{count} モデル',
|
||||
unlimited: '無制限',
|
||||
@@ -666,7 +667,6 @@ const PERSONAL_JA: PersonalPricingCopy = {
|
||||
estimatedRequests: '5 時間ごとの推定リクエスト数',
|
||||
estimatedRequestsAria: '5 時間ごとのモデル推定リクエスト数',
|
||||
usageAllowanceNote: '人気モデルには十分な利用枠が含まれ、目安は 5 時間ごとに $16 · 週 $40 · 月 $80 相当です。実際の使用量はモデルとコンテキスト長によって異なります。',
|
||||
usagePeakNote: 'DeepSeek V4 Flash / Pro の推定値はオフピーク料金を使用します。ピーク時間(北京時間 09:00–12:00、14:00–18:00)は単位使用量が 2 倍になります。',
|
||||
usageEstimatesNote: 'モデル別の推定回数は、モデル価格、利用状況、ユーザーフィードバックに応じて変更される場合があります。',
|
||||
comparisonTitle: 'プラン別に全モデルを比較',
|
||||
category: 'カテゴリー',
|
||||
@@ -709,8 +709,8 @@ const EN: PricingContent = {
|
||||
freeTrialCreditLabel: 'Limited trial model credits (valid for 7 days)',
|
||||
firstMonthTag: '{pct}% off 1st month',
|
||||
yearlyDiscountTag: '{pct}% off',
|
||||
yearlySubline: 'Billed yearly · {totalUsd} / year (save {savingsUsd})',
|
||||
monthlyRenewal: 'Then {amountUsd} / mo',
|
||||
yearlySubline: 'Billed {totalUsd}/year',
|
||||
monthlyRenewal: 'First-month price',
|
||||
yearlySaveCta: 'Save {savingsUsd} yearly',
|
||||
},
|
||||
free: {
|
||||
@@ -720,8 +720,8 @@ const EN: PricingContent = {
|
||||
features: ['BYOK provider keys · Local coding agents', 'Community support'],
|
||||
},
|
||||
go: {
|
||||
tagline: 'Lightweight needs, effortless delivery · Zero-config',
|
||||
ctaLabel: 'Subscribe to Go',
|
||||
tagline: 'Light needs · Easy delivery',
|
||||
ctaLabel: 'Subscribe',
|
||||
allowance: '8 popular models · ample allowance',
|
||||
features: [
|
||||
'Full design and coding capabilities',
|
||||
@@ -731,8 +731,8 @@ const EN: PricingContent = {
|
||||
},
|
||||
plans: {
|
||||
plus: {
|
||||
tagline: 'Independent projects, solo delivery · Zero-config',
|
||||
ctaLabel: 'Upgrade to Plus',
|
||||
tagline: 'Everyday design · Continuous delivery',
|
||||
ctaLabel: 'Subscribe',
|
||||
concurrency: '2 concurrent tasks',
|
||||
features: [
|
||||
'Zero-config professional design agent',
|
||||
@@ -742,8 +742,8 @@ const EN: PricingContent = {
|
||||
],
|
||||
},
|
||||
pro: {
|
||||
tagline: 'One person, a whole design team · Zero-config',
|
||||
ctaLabel: 'Upgrade to Pro',
|
||||
tagline: 'Complex projects · Efficient production',
|
||||
ctaLabel: 'Subscribe',
|
||||
concurrency: '5 concurrent tasks',
|
||||
features: [
|
||||
'Zero-config professional design agent',
|
||||
@@ -753,8 +753,8 @@ const EN: PricingContent = {
|
||||
],
|
||||
},
|
||||
max: {
|
||||
tagline: 'Outsourced design costs, slashed · Zero-config',
|
||||
ctaLabel: 'Upgrade to Max',
|
||||
tagline: 'High-volume creation · Consistent output',
|
||||
ctaLabel: 'Subscribe',
|
||||
concurrency: '10 concurrent tasks',
|
||||
features: [
|
||||
'Zero-config professional design agent',
|
||||
|
||||
@@ -65,9 +65,9 @@ const EN: TeamPricingContent = {
|
||||
increaseSeats: 'Increase seats',
|
||||
minSeatsNote: 'Team plans require at least {count} seats.',
|
||||
perSeatMonth: '/ seat / month',
|
||||
monthlyTotal: 'First month only {amount}',
|
||||
yearlyTotal: 'First year only {amount}',
|
||||
checkout: 'Upgrade team',
|
||||
monthlyTotal: '{amount} / month for your team',
|
||||
yearlyTotal: '{amount} billed for the first year',
|
||||
checkout: 'Upgrade team · {amount}/year',
|
||||
teamFeatures: [
|
||||
'Share and manage projects, design systems, and plugins as a team',
|
||||
'Everyone can view and comment; only the project creator can edit',
|
||||
@@ -109,9 +109,9 @@ const ZH: TeamPricingContent = {
|
||||
increaseSeats: '增加席位',
|
||||
minSeatsNote: '团队版最少需要 {count} 个席位。',
|
||||
perSeatMonth: '/ 席 / 月',
|
||||
monthlyTotal: '首月仅需 {amount}',
|
||||
yearlyTotal: '首年仅需 {amount}',
|
||||
checkout: '升级团队版',
|
||||
monthlyTotal: '团队合计 {amount} / 月',
|
||||
yearlyTotal: '首年应付 {amount}',
|
||||
checkout: '升级团队版 · {amount}/年',
|
||||
teamFeatures: [
|
||||
'项目、设计系统与插件,团队统一共享与管理',
|
||||
'成员均可查看和评论项目,仅项目创建者可以编辑',
|
||||
@@ -153,9 +153,9 @@ const JA: TeamPricingContent = {
|
||||
increaseSeats: '席数を増やす',
|
||||
minSeatsNote: 'Team プランは最低 {count} 席から利用できます。',
|
||||
perSeatMonth: '/ 席 / 月',
|
||||
monthlyTotal: '初月は {amount} のみ',
|
||||
yearlyTotal: '初年度は {amount} のみ',
|
||||
checkout: 'Team にアップグレード',
|
||||
monthlyTotal: 'チーム合計 月額 {amount}',
|
||||
yearlyTotal: '初年度の請求額 {amount}',
|
||||
checkout: 'Team にアップグレード · 年額 {amount}',
|
||||
teamFeatures: [
|
||||
'プロジェクト、Design Systems、プラグインをチームで共有・管理',
|
||||
'全員が閲覧とコメント可能。編集はプロジェクト作成者のみ',
|
||||
@@ -197,9 +197,9 @@ const KO: TeamPricingContent = {
|
||||
increaseSeats: '좌석 늘리기',
|
||||
minSeatsNote: 'Team 요금제는 최소 {count}개 좌석이 필요합니다.',
|
||||
perSeatMonth: '/ 좌석 / 월',
|
||||
monthlyTotal: '첫 달은 {amount}',
|
||||
yearlyTotal: '첫해는 {amount}',
|
||||
checkout: 'Team으로 업그레이드',
|
||||
monthlyTotal: '팀 합계 월 {amount}',
|
||||
yearlyTotal: '첫해 청구 금액 {amount}',
|
||||
checkout: 'Team으로 업그레이드 · 연 {amount}',
|
||||
teamFeatures: [
|
||||
'프로젝트, Design Systems, 플러그인을 팀에서 공유하고 관리',
|
||||
'모든 구성원이 보고 댓글을 달 수 있으며 편집은 프로젝트 생성자만 가능',
|
||||
@@ -241,9 +241,9 @@ const DE: TeamPricingContent = {
|
||||
increaseSeats: 'Sitz hinzufügen',
|
||||
minSeatsNote: 'Team-Pläne erfordern mindestens {count} Sitze.',
|
||||
perSeatMonth: '/ Sitz / Monat',
|
||||
monthlyTotal: 'Im ersten Monat nur {amount}',
|
||||
yearlyTotal: 'Im ersten Jahr nur {amount}',
|
||||
checkout: 'Auf Team upgraden',
|
||||
monthlyTotal: 'Teamgesamtpreis {amount} / Monat',
|
||||
yearlyTotal: 'Im ersten Jahr werden {amount} berechnet',
|
||||
checkout: 'Auf Team upgraden · {amount}/Jahr',
|
||||
teamFeatures: [
|
||||
'Projekte, Design Systems und Plugins gemeinsam teilen und verwalten',
|
||||
'Alle können ansehen und kommentieren; nur Ersteller bearbeiten ihr Projekt',
|
||||
@@ -285,9 +285,9 @@ const FR: TeamPricingContent = {
|
||||
increaseSeats: 'Ajouter un siège',
|
||||
minSeatsNote: 'Les offres Team nécessitent au moins {count} sièges.',
|
||||
perSeatMonth: '/ siège / mois',
|
||||
monthlyTotal: 'Premier mois à seulement {amount}',
|
||||
yearlyTotal: 'Première année à seulement {amount}',
|
||||
checkout: 'Passer à Team',
|
||||
monthlyTotal: 'Total équipe : {amount} / mois',
|
||||
yearlyTotal: '{amount} facturés la première année',
|
||||
checkout: 'Passer à Team · {amount}/an',
|
||||
teamFeatures: [
|
||||
'Partager et gérer en équipe projets, Design Systems et plugins',
|
||||
'Tous peuvent consulter et commenter ; seul le créateur du projet le modifie',
|
||||
@@ -329,9 +329,9 @@ const RU: TeamPricingContent = {
|
||||
increaseSeats: 'Увеличить число мест',
|
||||
minSeatsNote: 'Для командного тарифа нужно минимум {count} места.',
|
||||
perSeatMonth: '/ место / месяц',
|
||||
monthlyTotal: 'Первый месяц — всего {amount}',
|
||||
yearlyTotal: 'Первый год — всего {amount}',
|
||||
checkout: 'Перейти на Team',
|
||||
monthlyTotal: '{amount} / месяц для команды',
|
||||
yearlyTotal: '{amount} за первый год',
|
||||
checkout: 'Перейти на Team · {amount}/год',
|
||||
teamFeatures: [
|
||||
'Общие проекты, Design Systems и плагины с управлением для команды',
|
||||
'Все могут смотреть и комментировать; редактирует только создатель проекта',
|
||||
@@ -373,9 +373,9 @@ const ES: TeamPricingContent = {
|
||||
increaseSeats: 'Añadir un asiento',
|
||||
minSeatsNote: 'Los planes Team requieren al menos {count} asientos.',
|
||||
perSeatMonth: '/ asiento / mes',
|
||||
monthlyTotal: 'Primer mes por solo {amount}',
|
||||
yearlyTotal: 'Primer año por solo {amount}',
|
||||
checkout: 'Mejorar a Team',
|
||||
monthlyTotal: '{amount} / mes para tu equipo',
|
||||
yearlyTotal: '{amount} facturados el primer año',
|
||||
checkout: 'Mejorar a Team · {amount}/año',
|
||||
teamFeatures: [
|
||||
'Compartir y gestionar proyectos, Design Systems y plugins en equipo',
|
||||
'Todos pueden ver y comentar; solo el creador del proyecto puede editarlo',
|
||||
@@ -417,9 +417,9 @@ const PT_BR: TeamPricingContent = {
|
||||
increaseSeats: 'Aumentar assentos',
|
||||
minSeatsNote: 'Os planos Team exigem pelo menos {count} assentos.',
|
||||
perSeatMonth: '/ assento / mês',
|
||||
monthlyTotal: 'Primeiro mês por apenas {amount}',
|
||||
yearlyTotal: 'Primeiro ano por apenas {amount}',
|
||||
checkout: 'Fazer upgrade para Team',
|
||||
monthlyTotal: '{amount} / mês para sua equipe',
|
||||
yearlyTotal: '{amount} cobrados no primeiro ano',
|
||||
checkout: 'Fazer upgrade para Team · {amount}/ano',
|
||||
teamFeatures: [
|
||||
'Compartilhe e gerencie projetos, Design Systems e plugins em equipe',
|
||||
'Todos podem ver e comentar; apenas o criador do projeto pode editar',
|
||||
@@ -461,9 +461,9 @@ const IT: TeamPricingContent = {
|
||||
increaseSeats: 'Aumenta le postazioni',
|
||||
minSeatsNote: 'I piani Team richiedono almeno {count} postazioni.',
|
||||
perSeatMonth: '/ postazione / mese',
|
||||
monthlyTotal: 'Primo mese a soli {amount}',
|
||||
yearlyTotal: 'Primo anno a soli {amount}',
|
||||
checkout: 'Passa a Team',
|
||||
monthlyTotal: '{amount} / mese per il tuo team',
|
||||
yearlyTotal: '{amount} fatturati il primo anno',
|
||||
checkout: 'Passa a Team · {amount}/anno',
|
||||
teamFeatures: [
|
||||
'Condividi e gestisci progetti, Design Systems e plugin come team',
|
||||
'Tutti possono vedere e commentare; modifica solo chi crea il progetto',
|
||||
@@ -505,9 +505,9 @@ const TR: TeamPricingContent = {
|
||||
increaseSeats: 'Koltuk sayısını artır',
|
||||
minSeatsNote: 'Team planları en az {count} koltuk gerektirir.',
|
||||
perSeatMonth: '/ koltuk / ay',
|
||||
monthlyTotal: 'İlk ay yalnızca {amount}',
|
||||
yearlyTotal: 'İlk yıl yalnızca {amount}',
|
||||
checkout: 'Team’e yükselt',
|
||||
monthlyTotal: 'Ekibiniz için aylık {amount}',
|
||||
yearlyTotal: 'İlk yıl için {amount} faturalandırılır',
|
||||
checkout: 'Team’e yükselt · {amount}/yıl',
|
||||
teamFeatures: [
|
||||
'Projeleri, Design Systems öğelerini ve eklentileri ekipçe paylaşın ve yönetin',
|
||||
'Herkes görüntüleyip yorum yapabilir; yalnızca proje sahibi düzenleyebilir',
|
||||
|
||||
@@ -116,6 +116,25 @@ export function cloudSubscribeUrl(
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Exact Team checkout handoff. Landing owns the comparison controls; Vela owns
|
||||
* authentication, live catalog validation, workspace permissions, and Stripe.
|
||||
* Carry the visitor's complete selection so Vela can prefill its existing
|
||||
* confirmation dialog instead of asking for the same choices a second time.
|
||||
*/
|
||||
export function cloudTeamSubscribeUrl(
|
||||
tier: TeamPlanTier,
|
||||
interval: BillingInterval,
|
||||
seats: number,
|
||||
): string {
|
||||
if (!Number.isSafeInteger(seats) || seats < 1) {
|
||||
throw new RangeError('Team checkout seats must be a positive integer');
|
||||
}
|
||||
const url = new URL(cloudSubscribeUrl(tier, interval));
|
||||
url.searchParams.set('seats', String(seats));
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Preserve an explicitly supplied workspace scope without inventing one from
|
||||
* local state. The browser enhancement calls this same URL contract when the
|
||||
|
||||
@@ -90,7 +90,8 @@ function teamView(tier: TeamPlanTierConfig, interval: 'monthly' | 'yearly') {
|
||||
amount: usd(introSeatMonth),
|
||||
strike: usd(regularSeatMonth),
|
||||
discountPct,
|
||||
intervalTotal: usd(intervalTotal),
|
||||
monthlyTeamTotal: usd(intervalTotal / months).replace(/^\$/, ''),
|
||||
intervalTotal: usd(intervalTotal).replace(/^\$/, ''),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -270,9 +271,10 @@ const jsonLd = [
|
||||
<div class="pr-team-grid">
|
||||
<section class="pr-team-card" aria-labelledby="pr-team-title">
|
||||
<svg class="pr-team-decoration" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09Z" />
|
||||
<path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.66 12.66 0 0 1 22 2c0 2.72-.78 7.5-6.05 11A22.35 22.35 0 0 1 12 15Z" />
|
||||
<path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" />
|
||||
<circle cx="9" cy="8" r="3" />
|
||||
<path d="M3.5 19c.35-3.45 2.1-5.2 5.5-5.2s5.15 1.75 5.5 5.2" />
|
||||
<circle cx="17.25" cy="9.25" r="2.25" />
|
||||
<path d="M15.3 14.7c3.2-.75 5.05.68 5.2 3.8" />
|
||||
</svg>
|
||||
<h2 id="pr-team-title" class="pr-team-card-title">{teamContent.teamTitle}</h2>
|
||||
<p class="pr-team-card-tagline">{teamContent.teamTagline}</p>
|
||||
@@ -285,11 +287,18 @@ const jsonLd = [
|
||||
{fillTemplate(L.yearlyDiscountTag, { pct: String(initialTeamView.discountPct) })}
|
||||
</span>
|
||||
</div>
|
||||
<p class="pr-team-yearly-summary" data-team-summary>
|
||||
{fillTemplate(teamContent.yearlyTotal, {
|
||||
amount: initialTeamView.intervalTotal,
|
||||
})}
|
||||
</p>
|
||||
<div class="pr-team-price-summary" aria-live="polite">
|
||||
<p data-team-monthly-summary>
|
||||
{fillTemplate(teamContent.monthlyTotal, {
|
||||
amount: initialTeamView.monthlyTeamTotal,
|
||||
})}
|
||||
</p>
|
||||
<strong data-team-yearly-summary>
|
||||
{fillTemplate(teamContent.yearlyTotal, {
|
||||
amount: initialTeamView.intervalTotal,
|
||||
})}
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
<div class="pr-team-controls">
|
||||
<div class="pr-team-field">
|
||||
@@ -355,15 +364,28 @@ const jsonLd = [
|
||||
{fillTemplate(teamContent.minSeatsNote, { count: String(initialTeamTier.minSeats) })}
|
||||
</p>
|
||||
|
||||
<div class="pr-team-model-offer" data-pricing-campaign-surface hidden>
|
||||
<span class="pr-team-model-offer-copy">
|
||||
<strong>{deepSeekCampaign.teamOfferTitle}</strong>
|
||||
<small>{deepSeekCampaign.teamOfferModels}</small>
|
||||
</span>
|
||||
<span
|
||||
class="pr-team-offer-countdown"
|
||||
data-team-campaign-countdown
|
||||
aria-label={deepSeekCampaign.windowLabel}
|
||||
>
|
||||
<b data-team-countdown-days data-day-unit={deepSeekCampaign.dayUnit}>
|
||||
0{deepSeekCampaign.dayUnit}
|
||||
</b>
|
||||
<b data-team-countdown-hours>00</b>
|
||||
<i aria-hidden="true">:</i>
|
||||
<b data-team-countdown-minutes>00</b>
|
||||
<i aria-hidden="true">:</i>
|
||||
<b data-team-countdown-seconds>00</b>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ul class="pr-team-feature-list">
|
||||
{deepSeekCampaign.modelBenefits.map((benefit) => (
|
||||
<li class="pr-campaign-model-benefit" data-pricing-campaign-surface hidden>
|
||||
<div>
|
||||
<strong>{benefit}</strong>
|
||||
<small>{deepSeekCampaign.teamBenefitNote}</small>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
{teamContent.teamFeatures.map((feature) => <li>{feature}</li>)}
|
||||
</ul>
|
||||
|
||||
@@ -372,14 +394,18 @@ const jsonLd = [
|
||||
href={CLOUD_CONSOLE_URL}
|
||||
data-pricing-cta
|
||||
data-tier="team"
|
||||
data-team-checkout-total
|
||||
>
|
||||
{teamContent.checkout}
|
||||
{fillTemplate(teamContent.checkout, { amount: initialTeamView.intervalTotal })}
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<section class="pr-team-card pr-enterprise-card" aria-labelledby="pr-enterprise-title">
|
||||
<svg class="pr-team-decoration" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6A19.79 19.79 0 0 1 2.12 4.18 2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.69 2.8a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.9.33 1.84.56 2.8.69A2 2 0 0 1 22 16.92Z" />
|
||||
<path d="M5 21V6.5L12 3v18" />
|
||||
<path d="M12 8h7v13" />
|
||||
<path d="M3 21h18" />
|
||||
<path d="M8 8.5h1M8 12h1M8 15.5h1M15 11h1M15 14h1" />
|
||||
</svg>
|
||||
<h2 id="pr-enterprise-title" class="pr-team-card-title">{teamContent.enterpriseTitle}</h2>
|
||||
<p class="pr-team-card-tagline">{teamContent.enterpriseTagline}</p>
|
||||
@@ -438,13 +464,34 @@ const jsonLd = [
|
||||
const campaignSurfaces = root.querySelectorAll('[data-pricing-campaign-surface]');
|
||||
const campaignStartAt = Date.parse(root.getAttribute('data-campaign-start-at') || '');
|
||||
const campaignEndAt = Date.parse(root.getAttribute('data-campaign-end-at') || '');
|
||||
const teamCampaignCountdown = root.querySelector('[data-team-campaign-countdown]');
|
||||
const teamCountdownDays = root.querySelector('[data-team-countdown-days]');
|
||||
const teamCountdownHours = root.querySelector('[data-team-countdown-hours]');
|
||||
const teamCountdownMinutes = root.querySelector('[data-team-countdown-minutes]');
|
||||
const teamCountdownSeconds = root.querySelector('[data-team-countdown-seconds]');
|
||||
let campaignEligible = false;
|
||||
let campaignVisible = false;
|
||||
const updateTeamCampaignCountdown = (now) => {
|
||||
if (!teamCampaignCountdown) return;
|
||||
const remaining = Math.max(0, campaignEndAt - now);
|
||||
const totalSeconds = Math.floor(remaining / 1000);
|
||||
const days = Math.floor(totalSeconds / 86400);
|
||||
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
if (teamCountdownDays) {
|
||||
teamCountdownDays.textContent = `${days}${teamCountdownDays.getAttribute('data-day-unit') || ''}`;
|
||||
}
|
||||
if (teamCountdownHours) teamCountdownHours.textContent = String(hours).padStart(2, '0');
|
||||
if (teamCountdownMinutes) teamCountdownMinutes.textContent = String(minutes).padStart(2, '0');
|
||||
if (teamCountdownSeconds) teamCountdownSeconds.textContent = String(seconds).padStart(2, '0');
|
||||
};
|
||||
const updateCampaignVisibility = () => {
|
||||
const now = Date.now();
|
||||
campaignEligible = now >= campaignStartAt && now < campaignEndAt;
|
||||
campaignVisible = campaignEligible;
|
||||
campaignSurfaces.forEach((surface) => { surface.hidden = !campaignVisible; });
|
||||
updateTeamCampaignCountdown(now);
|
||||
return campaignVisible;
|
||||
};
|
||||
updateCampaignVisibility();
|
||||
@@ -519,7 +566,28 @@ const jsonLd = [
|
||||
continue;
|
||||
}
|
||||
if (tier === 'team') {
|
||||
cta.setAttribute('href', scopedBillingPlanUrl());
|
||||
const selectedTier = teamTiers.find(
|
||||
(candidate) =>
|
||||
candidate.tier ===
|
||||
root.querySelector('[data-team-tier]')?.getAttribute('data-value'),
|
||||
);
|
||||
const selectedSeats = Number(
|
||||
root.querySelector('[data-team-seats]')?.textContent || '',
|
||||
);
|
||||
if (!selectedTier || !Number.isSafeInteger(selectedSeats)) {
|
||||
cta.setAttribute('href', scopedBillingPlanUrl());
|
||||
continue;
|
||||
}
|
||||
const target = new URL(scopedBillingPlanUrl());
|
||||
target.searchParams.set('billing', 'plan');
|
||||
target.searchParams.set('plan', selectedTier.tier);
|
||||
target.searchParams.set('interval', 'yearly');
|
||||
target.searchParams.set(
|
||||
'seats',
|
||||
String(Math.max(selectedTier.minSeats, selectedSeats)),
|
||||
);
|
||||
target.searchParams.set('checkout', 'auto');
|
||||
cta.setAttribute('href', target.toString());
|
||||
continue;
|
||||
}
|
||||
const target = new URL(scopedBillingPlanUrl());
|
||||
@@ -555,6 +623,8 @@ const jsonLd = [
|
||||
};
|
||||
const activateAudience = (audience, shouldTrackClick = true) => {
|
||||
root.setAttribute('data-audience', audience);
|
||||
const billingToggle = root.querySelector('.pr-toggle');
|
||||
if (billingToggle) billingToggle.hidden = audience === 'team';
|
||||
for (const button of root.querySelectorAll('[data-audience-btn]')) {
|
||||
const active = button.getAttribute('data-audience-btn') === audience;
|
||||
button.classList.toggle('is-active', active);
|
||||
@@ -705,18 +775,18 @@ const jsonLd = [
|
||||
if (teamSeatsOutput) teamSeatsOutput.textContent = String(teamSeats);
|
||||
if (teamSeatsDec) teamSeatsDec.disabled = teamSeats <= tier.minSeats;
|
||||
|
||||
const interval = root.getAttribute('data-interval') === 'monthly' ? 'monthly' : 'yearly';
|
||||
const interval = 'yearly';
|
||||
const selected = tier[interval];
|
||||
const months = interval === 'yearly' ? 12 : 1;
|
||||
const introSeatMonth = selected.introPriceUsd / months;
|
||||
const listSeatMonth = tier.monthly.priceUsd;
|
||||
const discountPct = Math.round((1 - introSeatMonth / listSeatMonth) * 100);
|
||||
const intervalTotal = selected.introPriceUsd * teamSeats;
|
||||
const monthlyTeamTotal = intervalTotal / months;
|
||||
const amount = usd(introSeatMonth);
|
||||
const strike = usd(listSeatMonth);
|
||||
const total = usd(intervalTotal);
|
||||
const introTotalTemplate =
|
||||
interval === 'yearly' ? teamCopy.yearlyTotal : teamCopy.monthlyTotal;
|
||||
const monthlyTotal = usd(monthlyTeamTotal).replace(/^\$/, '');
|
||||
const total = usd(intervalTotal).replace(/^\$/, '');
|
||||
|
||||
set('[data-audience-panel="team"]', '[data-team-amount]', amount);
|
||||
set('[data-audience-panel="team"]', '[data-team-strike]', strike);
|
||||
@@ -730,14 +800,25 @@ const jsonLd = [
|
||||
);
|
||||
set(
|
||||
'[data-audience-panel="team"]',
|
||||
'[data-team-summary]',
|
||||
fillTeam(introTotalTemplate, { amount: total }),
|
||||
'[data-team-monthly-summary]',
|
||||
fillTeam(teamCopy.monthlyTotal, { amount: monthlyTotal }),
|
||||
);
|
||||
set(
|
||||
'[data-audience-panel="team"]',
|
||||
'[data-team-yearly-summary]',
|
||||
fillTeam(teamCopy.yearlyTotal, { amount: total }),
|
||||
);
|
||||
set(
|
||||
'[data-audience-panel="team"]',
|
||||
'[data-team-checkout-total]',
|
||||
fillTeam(teamCopy.checkout, { amount: total }),
|
||||
);
|
||||
set(
|
||||
'[data-audience-panel="team"]',
|
||||
'[data-team-min]',
|
||||
fillTeam(teamCopy.minSeatsNote, { count: String(tier.minSeats) }),
|
||||
);
|
||||
syncCtas();
|
||||
};
|
||||
|
||||
teamTierTrigger?.addEventListener('click', () => {
|
||||
@@ -972,7 +1053,7 @@ const jsonLd = [
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: end;
|
||||
width: min(1280px, 100%);
|
||||
width: min(1200px, 100%);
|
||||
min-height: 42px;
|
||||
margin-top: 65px;
|
||||
}
|
||||
@@ -981,9 +1062,11 @@ const jsonLd = [
|
||||
justify-self: end;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-bottom: 11px;
|
||||
gap: 0;
|
||||
min-height: 42px;
|
||||
margin-right: 24px;
|
||||
}
|
||||
.pr-toggle[hidden] { display: none !important; }
|
||||
.pr-audience-toggle {
|
||||
grid-column: 2;
|
||||
position: relative;
|
||||
@@ -991,33 +1074,36 @@ const jsonLd = [
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 420px;
|
||||
}
|
||||
.pr-toggle-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 36px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 0.9rem;
|
||||
color: var(--ink-soft, #555);
|
||||
padding: 4px 2px;
|
||||
font-family: Albert Sans, Arial, sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: #858980;
|
||||
padding: 0 2px;
|
||||
transition: color 200ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
.pr-toggle-btn.is-active { color: var(--ink); font-weight: 700; }
|
||||
.pr-toggle-separator { color: var(--ink-soft, #aaa); }
|
||||
.pr-toggle-btn.is-active { color: #151712; }
|
||||
.pr-toggle-separator { margin: 0 9px; color: #c0c3bb; font-size: 14px; }
|
||||
.pr-audience-btn {
|
||||
position: relative;
|
||||
min-width: 60px;
|
||||
min-height: 38px;
|
||||
padding: 0 16px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
border-radius: 9px;
|
||||
background: transparent;
|
||||
color: var(--ink-soft, #777);
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
color: #74786f;
|
||||
font-family: Albert Sans, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
@@ -1025,9 +1111,8 @@ const jsonLd = [
|
||||
transition: color 200ms cubic-bezier(0.23, 1, 0.32, 1), background-color 200ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
.pr-audience-btn.is-active {
|
||||
color: var(--ink);
|
||||
font-weight: 650;
|
||||
background: color-mix(in srgb, var(--ink) 7%, transparent);
|
||||
color: #11120f;
|
||||
background: #e8e9e3;
|
||||
}
|
||||
.pr-toggle-save {
|
||||
margin-left: 7px;
|
||||
@@ -1043,7 +1128,8 @@ const jsonLd = [
|
||||
.pr-controls-row { grid-template-columns: 1fr; gap: 14px; }
|
||||
.pr-audience-toggle,
|
||||
.pr-toggle { grid-column: 1; justify-self: center; }
|
||||
.pr-toggle { padding-bottom: 0; }
|
||||
.pr-audience-toggle { width: 100%; }
|
||||
.pr-toggle { margin-right: 0; }
|
||||
}
|
||||
|
||||
/* ---- Interval-conditional visibility ---- */
|
||||
@@ -1053,14 +1139,17 @@ const jsonLd = [
|
||||
/* ---- Shared hosted multimodal benefit ---- */
|
||||
.pr-multimodal {
|
||||
position: relative;
|
||||
left: 50%;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.35fr) minmax(420px, 1fr);
|
||||
gap: 36px;
|
||||
align-items: center;
|
||||
max-width: 1280px;
|
||||
width: min(1160px, calc(100vw - 48px));
|
||||
max-width: none;
|
||||
margin: 28px auto 0;
|
||||
padding: 30px 34px;
|
||||
overflow: hidden;
|
||||
transform: translateX(-50%);
|
||||
border: 1px solid var(--border, rgba(0, 0, 0, 0.12));
|
||||
border-radius: 18px;
|
||||
background:
|
||||
@@ -1188,13 +1277,14 @@ const jsonLd = [
|
||||
|
||||
/* ---- Team / Enterprise plans ---- */
|
||||
.pr-team-panel {
|
||||
max-width: 900px;
|
||||
margin: 40px auto 0;
|
||||
width: min(1200px, 100%);
|
||||
max-width: none;
|
||||
margin: 23px auto 0;
|
||||
}
|
||||
.pr-team-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
.pr-team-card {
|
||||
@@ -1202,91 +1292,127 @@ const jsonLd = [
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
padding: 28px;
|
||||
min-height: 590px;
|
||||
padding: 24px 20px 22px;
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border, rgba(0, 0, 0, 0.12));
|
||||
border-radius: 18px;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.62));
|
||||
animation: pr-team-card-enter 0.65s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
.pr-team-card:nth-child(2) { animation-delay: 0.09s; }
|
||||
@keyframes pr-team-card-enter {
|
||||
from { opacity: 0; transform: translateY(12px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.pr-team-decoration {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 24px;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
color: color-mix(in srgb, var(--brand, #2bd200) 20%, transparent);
|
||||
right: 20px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
color: #55c94a;
|
||||
opacity: .2;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.4;
|
||||
stroke-width: 1.25;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.pr-team-card-title {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
margin: 0 0 8px;
|
||||
padding-right: 76px;
|
||||
color: #242622;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--ink);
|
||||
line-height: 1.1;
|
||||
letter-spacing: -.025em;
|
||||
}
|
||||
.pr-team-card-tagline {
|
||||
position: relative;
|
||||
margin: 5px 0 0;
|
||||
color: var(--ink-soft, #777);
|
||||
font-size: 0.86rem;
|
||||
min-height: 42px;
|
||||
margin: 0;
|
||||
padding-right: 76px;
|
||||
color: var(--pricing-muted, #676c64);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.pr-team-price-row {
|
||||
display: flex;
|
||||
min-height: 72px;
|
||||
align-items: baseline;
|
||||
align-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 28px;
|
||||
gap: 6px 8px;
|
||||
margin: 5px 0 0;
|
||||
color: #2c2e2a;
|
||||
}
|
||||
.pr-team-amount {
|
||||
font-family: var(--serif, Georgia, serif);
|
||||
font-size: 2.2rem;
|
||||
font-family: Albert Sans, Arial, sans-serif;
|
||||
font-size: 50px;
|
||||
line-height: 1;
|
||||
letter-spacing: -.045em;
|
||||
}
|
||||
.pr-team-strike { color: #858981; font-size: 16px; text-decoration-thickness: 1.5px; }
|
||||
.pr-team-per { color: #73776f; font-size: 12px; }
|
||||
.pr-team-discount {
|
||||
padding: 4px 7px;
|
||||
border-radius: 999px;
|
||||
background: #dff3d9;
|
||||
color: #208a19;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
.pr-team-strike { color: var(--ink-soft, #aaa); }
|
||||
.pr-team-per { color: var(--ink-soft, #777); font-size: 0.85rem; }
|
||||
.pr-team-discount {
|
||||
padding: 0.16rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--brand, #2bd200) 18%, transparent);
|
||||
color: var(--olive, #218c00);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.pr-team-yearly-summary,
|
||||
.pr-team-price-summary,
|
||||
.pr-team-min-seats {
|
||||
margin: 8px 0 0;
|
||||
color: var(--ink-soft, #777);
|
||||
font-size: 0.78rem;
|
||||
color: var(--pricing-muted, #676c64);
|
||||
font-weight: 400;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.pr-team-price-summary {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-height: 38px;
|
||||
margin: 2px 0 0;
|
||||
padding-bottom: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.pr-team-price-summary p,
|
||||
.pr-team-price-summary strong {
|
||||
margin: 0;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
}
|
||||
.pr-team-controls {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
grid-template-columns: minmax(0, 1.35fr) minmax(150px, .9fr);
|
||||
gap: 10px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
.pr-team-field {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
color: var(--ink-soft, #666);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: #454842;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.pr-team-select-trigger,
|
||||
.pr-team-stepper {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 42px;
|
||||
border: 1px solid var(--border, rgba(0, 0, 0, 0.14));
|
||||
height: 44px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
font: inherit;
|
||||
background: #f3f4f0;
|
||||
color: #2e302c;
|
||||
font: 400 13px/1 Albert Sans, Arial, sans-serif;
|
||||
}
|
||||
.pr-team-select {
|
||||
position: relative;
|
||||
@@ -1351,31 +1477,88 @@ const jsonLd = [
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
padding: 4px 6px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
.pr-team-stepper button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.pr-team-stepper button:disabled { cursor: not-allowed; opacity: 0.35; }
|
||||
.pr-team-stepper output { min-width: 2ch; text-align: center; color: var(--ink); }
|
||||
.pr-team-stepper output { min-width: 2ch; text-align: center; color: #2e302c; font-size: 13px; font-weight: 400; }
|
||||
.pr-team-min-seats { margin: 9px 0 0; font-size: 9px; }
|
||||
.pr-team-model-offer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
min-height: 68px;
|
||||
margin-top: 18px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 8px;
|
||||
background: #eef5e9;
|
||||
}
|
||||
.pr-team-model-offer-copy {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
}
|
||||
.pr-team-model-offer-copy strong {
|
||||
display: block;
|
||||
color: #292b27;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.pr-team-model-offer-copy small {
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
color: #565a53;
|
||||
font-size: 9px;
|
||||
font-weight: 400;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.pr-team-offer-countdown {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
.pr-team-offer-countdown b {
|
||||
display: grid;
|
||||
min-width: 34px;
|
||||
height: 38px;
|
||||
place-items: center;
|
||||
padding: 0 7px;
|
||||
border-radius: 8px;
|
||||
background: #dcf2d4;
|
||||
color: #188c10;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.pr-team-offer-countdown i {
|
||||
color: #188c10;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
}
|
||||
.pr-team-feature-list {
|
||||
list-style: none;
|
||||
margin: 20px 0 0;
|
||||
margin: 18px 0 22px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 11px;
|
||||
gap: 14px;
|
||||
}
|
||||
.pr-team-feature-list li {
|
||||
position: relative;
|
||||
padding-left: 22px;
|
||||
font-size: 0.86rem;
|
||||
padding-left: 23px;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
color: var(--ink, #2a2a2a);
|
||||
color: #30322e;
|
||||
}
|
||||
.pr-team-feature-list li::before {
|
||||
content: '✓';
|
||||
@@ -1383,7 +1566,9 @@ const jsonLd = [
|
||||
left: 0;
|
||||
top: 0;
|
||||
font-weight: 700;
|
||||
color: var(--olive, #218c00);
|
||||
color: #188c10;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
/* The campaign row extends 8px outside the list. Match that offset with a
|
||||
more specific rule so its checkmark stays on the exact same x-axis as
|
||||
@@ -1395,22 +1580,39 @@ const jsonLd = [
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.pr-team-checkout {
|
||||
width: 100%;
|
||||
width: calc(100% - 2px);
|
||||
min-height: 42px;
|
||||
margin-top: auto;
|
||||
padding-top: 11px;
|
||||
padding-bottom: 11px;
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
background: #10110f;
|
||||
color: #fff;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
font-family: Inter, PingFang SC, Microsoft YaHei, Arial, sans-serif;
|
||||
line-height: normal;
|
||||
}
|
||||
.pr-team-feature-list + .pr-team-checkout { margin-top: 28px; }
|
||||
@media (max-width: 820px) {
|
||||
.pr-team-feature-list + .pr-team-checkout { margin-top: auto; }
|
||||
.pr-enterprise-card .pr-team-feature-list { gap: 17px; margin-top: 22px; }
|
||||
@media (max-width: 980px) {
|
||||
.pr-team-panel { max-width: 760px; }
|
||||
.pr-team-grid { grid-template-columns: 1fr; }
|
||||
.pr-team-card { min-height: 0; }
|
||||
}
|
||||
@media (max-width: 520px) {
|
||||
.pr-team-card { padding: 22px; }
|
||||
.pr-team-controls { grid-template-columns: 1fr; }
|
||||
.pr-team-model-offer { align-items: flex-start; flex-direction: column; }
|
||||
.pr-audience-btn { min-width: 0; flex: 1; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.pr-team-card {
|
||||
animation: none;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- FAQ ---- */
|
||||
.pr-faq { max-width: 760px; margin: 72px auto 0; }
|
||||
|
||||
@@ -173,6 +173,9 @@ export default defineConfig({
|
||||
srcDir: './app',
|
||||
outDir: './out',
|
||||
trailingSlash: 'always',
|
||||
// The Astro audit toolbar observes the live countdown and re-audits every
|
||||
// image on each tick, creating a dev-only asset request storm.
|
||||
devToolbar: { enabled: false },
|
||||
vite: {
|
||||
define: {
|
||||
// Staging / PR-preview builds set OD_LANDING_NOINDEX=1. SeoHead reads
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Claude</title><path d="m4.709 15.955 4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 0 1-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Grok</title><path d="m9.27 15.29 7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 0 0-1.829-1A8.975 8.975 0 0 0 5.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815"/></svg>
|
||||
|
After Width: | Height: | Size: 697 B |
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Jimeng</title><path d="M5.257 1.808C8.768 6.74 14.431 9.45 20.064 8.403a7.398 7.398 0 0 0 1.281-.38c.855-.33 1.975 1.01 1.326 1.658-.43.428-.84.82-1.208 1.147a51.149 51.149 0 0 1-8.389 6.044 51.146 51.146 0 0 1-9.49 4.263c-.453.15-.98.304-1.546.457-.885.238-1.484-1.4-.77-1.975.378-.306.713-.617.978-.927C5.969 14.336 6.299 8.174 3.717 2.697a18.2 18.2 0 0 1-.364-.84C2.995.98 4.042.335 4.656 1.055c.243.284.453.545.601.753zM19.53 13.695c-2.324 2.863-2.816 5.944-2.378 8.583l.024.16c.117.79-.816 1.277-1.448.788a12.813 12.813 0 0 0-.093-.072c-2.12-1.614-4.62-2.828-7.948-2.446a27.95 27.95 0 0 0 6.086-2.627c2.21-1.275 4.147-2.774 5.756-4.386z"/></svg>
|
||||
|
After Width: | Height: | Size: 758 B |
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Kimi</title><path d="M21.846 0a1.923 1.923 0 1 1 0 3.846H20.15a.226.226 0 0 1-.227-.226V1.923C19.923.861 20.784 0 21.846 0z"/><path d="m11.065 11.199 7.257-7.2c.137-.136.06-.41-.116-.41H14.3a.164.164 0 0 0-.117.051l-7.82 7.756c-.122.12-.302.013-.302-.179V3.82c0-.127-.083-.23-.185-.23h-2.69c-.103 0-.186.103-.186.23v15.95c0 .128.083.23.186.23h2.69c.103 0 .186-.102.186-.23v-3.25c0-.069.025-.135.069-.178l2.424-2.406a.158.158 0 0 1 .205-.023l6.484 4.772a7.677 7.677 0 0 0 3.453 1.283c.108.012.2-.095.2-.23v-3.06c0-.117-.07-.212-.164-.227a5.028 5.028 0 0 1-2.027-.807l-5.613-4.064c-.117-.078-.132-.279-.028-.381z"/></svg>
|
||||
|
After Width: | Height: | Size: 727 B |
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>MiniMax</title><path d="M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 0 0 .744.709.74.74 0 0 0 .743-.709V9.099a2.06 2.06 0 0 1 2.071-2.049A2.06 2.06 0 0 1 24 9.1v6.561a.649.649 0 0 1-.652.645.649.649 0 0 1-.653-.645V9.1a.762.762 0 0 0-.766-.758.762.762 0 0 0-.766.758v7.472a2.037 2.037 0 0 1-2.048 2.026 2.037 2.037 0 0 1-2.048-2.026v-12.5a.785.785 0 0 0-.788-.753.785.785 0 0 0-.789.752l-.001 15.904A2.037 2.037 0 0 1 13.441 22a2.037 2.037 0 0 1-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 0 0 .372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 0 1-.652.645.648.648 0 0 1-.652-.645V4.07a.785.785 0 0 0-.789-.78.785.785 0 0 0-.789.78v14.013a2.06 2.06 0 0 1-2.07 2.048 2.06 2.06 0 0 1-2.071-2.048V9.1a.762.762 0 0 0-.766-.758.762.762 0 0 0-.766.758v3.8a2.06 2.06 0 0 1-2.071 2.049A2.06 2.06 0 0 1 0 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 0 1 2.07-2.048 2.06 2.06 0 0 1 2.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>NanoBanana</title><path d="M8.342 13.16H3.455c-.513 0-.772-.639-.408-1.012l1.608-1.653a3.166 3.166 0 0 1 4.565 0l.513.527.006-.015.038.017c.735-1.93.786-2.809.783-5.007v-.275c-.01-1.782-.02-3.935 1.965-4.22 2.603-.375 4.504 4.219 4.815 8.299a3.166 3.166 0 0 1 3.38.774l1.609 1.653c.365.375.106 1.012-.407 1.012H19.27c.072.264.11.542.11.828v5.664c0 .914-.994 1.292-1.602.576-.229-.27-1.067-1.25-2.155-2.52-2.92 4.183-10.266 6.462-12.34 3.006a.915.915 0 0 1-.05 0H2.49a.991.991 0 1 1 0-1.982h.246c.014-1.687 1.23-3.148 2.846-3.783a7.448 7.448 0 0 0 2.76-1.889zm7.543-2.145c0-.127-.001-.256-.005-.388a15.693 15.693 0 0 0-.632-3.939c-.38-1.282-.887-2.33-1.425-2.992-.545-.671-.906-.715-1.085-.69-.223.032-.292.098-.322.129-.05.052-.135.176-.209.45-.152.567-.15 1.286-.147 2.186v.244c.002 1.134-.009 2.028-.145 2.92a11.292 11.292 0 0 1-.537 2.08h4.507zm-3.468 3.056c-1.636 3.166-4.981 4.71-8.118 4.87a.562.562 0 0 0 .057 1.124c3.294-.169 6.921-1.749 8.845-5.081l-.784-.913z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -62,7 +62,7 @@ test('Go banner keeps its sticky height in flow without double-offsetting the mo
|
||||
});
|
||||
|
||||
test('Go banner classifies signed-out and unpaid visitors during the fixed window', () => {
|
||||
assert.match(banner, /2026-08-20T20:00:00\+08:00/);
|
||||
assert.match(banner, /2026-08-20T17:00:00\+08:00/);
|
||||
assert.match(banner, /2026-09-03T20:00:00\+08:00/);
|
||||
assert.match(banner, /\/api\/auth\/get-session/);
|
||||
assert.match(banner, /if \(!session\?\.user\) \{\s*show\(\)/);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
PLANS_JSON_URL,
|
||||
PRICING_SNAPSHOT,
|
||||
cloudSubscribeUrl,
|
||||
cloudTeamSubscribeUrl,
|
||||
formatUsd,
|
||||
scopedBillingPlanUrl,
|
||||
teamIntroTotalUsd,
|
||||
@@ -35,6 +36,10 @@ const PRICING_INDIVIDUAL_PATH = new URL(
|
||||
"../app/_components/pricing-individual-plans.astro",
|
||||
import.meta.url,
|
||||
);
|
||||
const PRICING_CONTENT_PATH = new URL(
|
||||
"../app/_lib/pricing-content.ts",
|
||||
import.meta.url,
|
||||
);
|
||||
const GO_PLAN_MIMO_LOGO_PATH = new URL(
|
||||
"../public/pricing-e-final/assets/mimo-logo-user-CWOWEwG5.png",
|
||||
import.meta.url,
|
||||
@@ -44,14 +49,14 @@ const GO_PLAN_ZHIPU_LOGO_PATH = new URL(
|
||||
import.meta.url,
|
||||
);
|
||||
const FIRST_PARTY_MODEL_ICON_PATHS = [
|
||||
"../public/agents/anthropic.svg",
|
||||
"../public/agents/deepseek.svg",
|
||||
"../public/agents/gemini.svg",
|
||||
"../public/agents/minimax.svg",
|
||||
"../public/agents/moonshot.svg",
|
||||
"../public/agents/openai.svg",
|
||||
"../public/agents/xai.svg",
|
||||
"../public/model-icons/bytedance.svg",
|
||||
"../public/model-icons/claude.svg",
|
||||
"../public/model-icons/grok.svg",
|
||||
"../public/model-icons/jimeng.svg",
|
||||
"../public/model-icons/kimi.svg",
|
||||
"../public/model-icons/minimax.svg",
|
||||
"../public/model-icons/nanobanana.svg",
|
||||
].map((path) => new URL(path, import.meta.url));
|
||||
const CAMPAIGN_PATH = new URL(
|
||||
"../app/_lib/pricing-campaign-content.ts",
|
||||
@@ -159,6 +164,101 @@ describe("pricing contract", () => {
|
||||
assert.doesNotMatch(plans, /--usage-label/);
|
||||
});
|
||||
|
||||
it("matches the demo's individual taglines and compact billing copy", async () => {
|
||||
const content = getPricingContent("en");
|
||||
const individualPlans = await readFile(PRICING_INDIVIDUAL_PATH, "utf8");
|
||||
|
||||
assert.deepEqual(
|
||||
[
|
||||
content.go.tagline,
|
||||
content.plans.plus.tagline,
|
||||
content.plans.pro.tagline,
|
||||
content.plans.max.tagline,
|
||||
],
|
||||
[
|
||||
"Light needs · Easy delivery",
|
||||
"Everyday design · Continuous delivery",
|
||||
"Complex projects · Efficient production",
|
||||
"High-volume creation · Consistent output",
|
||||
],
|
||||
);
|
||||
assert.equal(content.labels.monthlyRenewal, "First-month price");
|
||||
assert.equal(content.labels.yearlySubline, "Billed {totalUsd}/year");
|
||||
assert.deepEqual(
|
||||
[
|
||||
content.go.ctaLabel,
|
||||
content.plans.plus.ctaLabel,
|
||||
content.plans.pro.ctaLabel,
|
||||
content.plans.max.ctaLabel,
|
||||
],
|
||||
["Subscribe", "Subscribe", "Subscribe", "Subscribe"],
|
||||
);
|
||||
assert.match(
|
||||
individualPlans,
|
||||
/<span>\{tierCopy\[tier\]\.ctaLabel\}<\/span>/,
|
||||
);
|
||||
assert.doesNotMatch(individualPlans, /ctaLabel\} · \{L\.(?:monthly|yearly)\}/);
|
||||
assert.match(
|
||||
individualPlans,
|
||||
/const compactSavings = \(amountUsd: number, interval: 'monthly' \| 'yearly'\)/,
|
||||
);
|
||||
assert.match(
|
||||
individualPlans,
|
||||
/locale === 'en' && interval === 'monthly' \? `\$\{amount\}\/mo` : amount/,
|
||||
);
|
||||
assert.match(
|
||||
individualPlans,
|
||||
/const compactBillingTotal = \(amountUsd: number\) =>\s*locale === 'en'/,
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps recommendation ribbons legible, animated, and motion-safe", async () => {
|
||||
const individualPlans = await readFile(PRICING_INDIVIDUAL_PATH, "utf8");
|
||||
|
||||
assert.match(
|
||||
individualPlans,
|
||||
/\.new-plan-ribbon\s*\{[^}]*font-size:\s*11px;/s,
|
||||
);
|
||||
assert.match(
|
||||
individualPlans,
|
||||
/\.new-plan-ribbon span\s*\{[^}]*background:\s*linear-gradient\(110deg,[^}]*animation:\s*pricing-ribbon-text-shimmer 4\.2s linear infinite;/s,
|
||||
);
|
||||
assert.match(
|
||||
individualPlans,
|
||||
/\.new-plan-ribbon span\s*\{[^}]*-webkit-text-fill-color:\s*transparent;[^}]*background-clip:\s*text;/s,
|
||||
);
|
||||
assert.doesNotMatch(individualPlans, /\.new-plan-ribbon::after/);
|
||||
assert.match(
|
||||
individualPlans,
|
||||
/@keyframes pricing-ribbon-text-shimmer\s*\{[^}]*background-position:\s*100% 0;[^}]*\}[^}]*background-position:\s*-100% 0;/s,
|
||||
);
|
||||
assert.match(
|
||||
individualPlans,
|
||||
/@media \(prefers-reduced-motion: reduce\)\s*\{[\s\S]*?\.new-plan-ribbon span\s*\{[^}]*-webkit-text-fill-color:\s*#173111;[^}]*animation:\s*none;/,
|
||||
);
|
||||
});
|
||||
|
||||
it("lets localized plan and billing copy wrap inside each pricing card", async () => {
|
||||
const individualPlans = await readFile(PRICING_INDIVIDUAL_PATH, "utf8");
|
||||
|
||||
assert.match(
|
||||
individualPlans,
|
||||
/\.plan-top\s*\{[^}]*min-height:\s*78px;/s,
|
||||
);
|
||||
assert.match(
|
||||
individualPlans,
|
||||
/\.plan-top p\s*\{[^}]*white-space:\s*normal;[^}]*overflow-wrap:\s*anywhere;/s,
|
||||
);
|
||||
assert.match(
|
||||
individualPlans,
|
||||
/\.compact-price-detail\s*\{[^}]*display:\s*grid;[^}]*grid-template-columns:\s*auto minmax\(0, 1fr\);[^}]*min-height:\s*46px;/s,
|
||||
);
|
||||
assert.match(
|
||||
individualPlans,
|
||||
/\.compact-price-detail > small\s*\{[^}]*min-width:\s*0;[^}]*white-space:\s*normal;[^}]*overflow-wrap:\s*anywhere;/s,
|
||||
);
|
||||
});
|
||||
|
||||
it("serves the model comparison icons from first-party assets", async () => {
|
||||
const [individualPlans, ...icons] = await Promise.all([
|
||||
readFile(PRICING_INDIVIDUAL_PATH, "utf8"),
|
||||
@@ -168,16 +268,26 @@ describe("pricing contract", () => {
|
||||
for (const icon of icons) assert.ok(icon.byteLength > 0);
|
||||
assert.doesNotMatch(individualPlans, /unpkg\.com|@latest/);
|
||||
for (const asset of [
|
||||
"/agents/anthropic.svg",
|
||||
"/agents/deepseek.svg",
|
||||
"/agents/openai.svg",
|
||||
"/model-icons/claude.svg",
|
||||
"/model-icons/grok.svg",
|
||||
"/model-icons/jimeng.svg",
|
||||
"/model-icons/kimi.svg",
|
||||
"/model-icons/minimax.svg",
|
||||
"/model-icons/nanobanana.svg",
|
||||
]) {
|
||||
assert.ok(individualPlans.includes(asset), asset);
|
||||
}
|
||||
for (const superseded of [
|
||||
"/agents/anthropic.svg",
|
||||
"/agents/gemini.svg",
|
||||
"/agents/minimax.svg",
|
||||
"/agents/moonshot.svg",
|
||||
"/agents/openai.svg",
|
||||
"/agents/xai.svg",
|
||||
"/model-icons/bytedance.svg",
|
||||
]) {
|
||||
assert.ok(individualPlans.includes(asset), asset);
|
||||
assert.equal(individualPlans.includes(superseded), false, superseded);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -258,6 +368,14 @@ describe("pricing contract", () => {
|
||||
individualPlans,
|
||||
/const popularStatus = popularAccessStatus\(model\.name, tier\);/,
|
||||
);
|
||||
assert.match(
|
||||
individualPlans,
|
||||
/\{popularStatus\.kind === 'unlimited' && <em class=\{popularStatus\.kind\}>\{popularStatus\.text\}<\/em>\}/,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
individualPlans,
|
||||
/^\s*<em class=\{popularStatus\.kind\}>\{popularStatus\.text\}<\/em>$/m,
|
||||
);
|
||||
assert.match(
|
||||
individualPlans,
|
||||
/return popularAccessStatus\(modelName, tier\);/,
|
||||
@@ -268,6 +386,44 @@ describe("pricing contract", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("leads the comparison with Go unlimited models and keeps ample access check-only", async () => {
|
||||
const individualPlans = await readFile(PRICING_INDIVIDUAL_PATH, "utf8");
|
||||
const comparisonBlock = individualPlans.match(
|
||||
/const comparisonPopular = \[([\s\S]*?)\]\.map/,
|
||||
)?.[1];
|
||||
|
||||
assert.ok(comparisonBlock);
|
||||
assert.deepEqual(
|
||||
Array.from(comparisonBlock.matchAll(/'([^']+)'/g), (match) => match[1]).slice(0, 3),
|
||||
["DeepSeek V4 Flash", "DeepSeek V4 Pro", "GLM-5.2"],
|
||||
);
|
||||
assert.match(
|
||||
individualPlans,
|
||||
/status\.kind === 'more-ample'\s*\?\s*\(\s*<td><span class=\{`model-access-status \$\{status\.kind\}`\} aria-label=\{status\.text\}><i class="status-icon check"><\/i><\/span><\/td>/s,
|
||||
);
|
||||
});
|
||||
|
||||
it("renders paid flagship headings as one localized phrase", async () => {
|
||||
const individualPlans = await readFile(PRICING_INDIVIDUAL_PATH, "utf8");
|
||||
|
||||
for (const locale of PRICING_LOCALES) {
|
||||
assert.match(
|
||||
getPricingContent(locale).personal.flagshipModelCount,
|
||||
/\{count\}/,
|
||||
`${locale} must localize the complete flagship count heading`,
|
||||
);
|
||||
}
|
||||
assert.equal(
|
||||
getPricingContent("en").personal.flagshipModelCount,
|
||||
"{count}+ flagship models",
|
||||
);
|
||||
assert.match(
|
||||
individualPlans,
|
||||
/tier === 'go' \? P\.flagshipModels : fillTemplate\(P\.flagshipModelCount, \{ count: String\(flagship\.length\) \}\)/,
|
||||
);
|
||||
assert.doesNotMatch(individualPlans, /\} · \$\{P\.flagshipModels\}/);
|
||||
});
|
||||
|
||||
it("derives live Personal benefit totals from the catalog", async () => {
|
||||
const individualPlans = await readFile(PRICING_INDIVIDUAL_PATH, "utf8");
|
||||
|
||||
@@ -297,6 +453,15 @@ describe("pricing contract", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("omits the DeepSeek peak-pricing estimate note from Personal usage", async () => {
|
||||
const individualPlans = await readFile(PRICING_INDIVIDUAL_PATH, "utf8");
|
||||
const pricingContent = await readFile(PRICING_CONTENT_PATH, "utf8");
|
||||
|
||||
assert.doesNotMatch(individualPlans, /usagePeakNote/);
|
||||
assert.doesNotMatch(pricingContent, /usagePeakNote/);
|
||||
assert.doesNotMatch(pricingContent, /DeepSeek V4 Flash \/ Pro estimates use off-peak pricing/);
|
||||
});
|
||||
|
||||
it("keeps the DeepSeek plan benefits without a Pricing campaign banner", async () => {
|
||||
const page = await readFile(PRICING_PAGE_PATH, "utf8");
|
||||
const campaign = await readFile(CAMPAIGN_PATH, "utf8");
|
||||
@@ -389,20 +554,36 @@ describe("pricing contract", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("aligns the highlighted campaign checkmark with the Team benefit list", async () => {
|
||||
const page = await readFile(PRICING_PAGE_PATH, "utf8");
|
||||
it("renders one combined Team campaign offer with a live countdown", async () => {
|
||||
const [page, campaign] = await Promise.all([
|
||||
readFile(PRICING_PAGE_PATH, "utf8"),
|
||||
readFile(CAMPAIGN_PATH, "utf8"),
|
||||
]);
|
||||
const teamPanel = page.slice(
|
||||
page.indexOf('id="pr-team-panel"'),
|
||||
page.indexOf('<p class="pr-foot"'),
|
||||
);
|
||||
|
||||
assert.match(
|
||||
page,
|
||||
/\.pr-campaign-model-benefit > div\s*\{[\s\S]*display:\s*grid;[\s\S]*gap:\s*2px;/,
|
||||
"the campaign date note must render on its own line below the model benefit",
|
||||
);
|
||||
assert.match(page, /\.pr-campaign-model-benefit::before\s*\{\s*left:\s*8px;\s*\}/);
|
||||
assert.match(
|
||||
page,
|
||||
/\.pr-team-feature-list li\.pr-campaign-model-benefit::before\s*\{[\s\S]*left:\s*8px;[\s\S]*top:\s*18px;[\s\S]*transform:\s*translateY\(-50%\);/,
|
||||
"the team campaign checkmark must override the later base list rule",
|
||||
assert.match(campaign, /teamOfferTitle: 'Unlimited model access'/);
|
||||
assert.match(campaign, /teamOfferModels: 'DeepSeek V4 Flash and V4 Pro'/);
|
||||
assert.equal(
|
||||
teamPanel.match(/data-pricing-campaign-surface/g)?.length,
|
||||
1,
|
||||
"Team must expose one combined activity surface",
|
||||
);
|
||||
assert.doesNotMatch(teamPanel, /modelBenefits\.map/);
|
||||
assert.match(teamPanel, /class="pr-team-model-offer"/);
|
||||
assert.match(teamPanel, /data-team-campaign-countdown/);
|
||||
assert.match(teamPanel, /data-team-countdown-days/);
|
||||
assert.match(teamPanel, /data-team-countdown-hours/);
|
||||
assert.match(teamPanel, /data-team-countdown-minutes/);
|
||||
assert.match(teamPanel, /data-team-countdown-seconds/);
|
||||
assert.match(page, /Math\.max\(0, campaignEndAt - now\)/);
|
||||
const offerRule = page.match(/\.pr-team-model-offer\s*\{([^}]*)\}/)?.[1];
|
||||
assert.ok(offerRule);
|
||||
assert.match(offerRule, /background:\s*#eef5e9;/);
|
||||
assert.match(offerRule, /border-radius:\s*8px;/);
|
||||
assert.match(offerRule, /padding:\s*12px 14px;/);
|
||||
});
|
||||
|
||||
it("keeps the multimodal coming-soon note above the video label", async () => {
|
||||
@@ -444,6 +625,11 @@ describe("pricing contract", () => {
|
||||
1,
|
||||
"the retained Cloud capability section must render exactly once",
|
||||
);
|
||||
assert.match(
|
||||
page,
|
||||
/\.pr-multimodal\s*\{[\s\S]*?left:\s*50%;[\s\S]*?width:\s*min\(1160px, calc\(100vw - 48px\)\);[\s\S]*?max-width:\s*none;[\s\S]*?transform:\s*translateX\(-50%\);/,
|
||||
"the Cloud capability card must share the comparison table width",
|
||||
);
|
||||
});
|
||||
|
||||
it("points the public pricing URL at the landing-page JSON contract", () => {
|
||||
@@ -459,6 +645,14 @@ describe("pricing contract", () => {
|
||||
cloudSubscribeUrl("pro", "yearly"),
|
||||
"https://open-design.ai/cloud/dashboard?billing=plan&plan=pro&interval=yearly&checkout=auto",
|
||||
);
|
||||
assert.equal(
|
||||
cloudTeamSubscribeUrl("team_pro", "yearly", 4),
|
||||
"https://open-design.ai/cloud/dashboard?billing=plan&plan=team_pro&interval=yearly&checkout=auto&seats=4",
|
||||
);
|
||||
assert.throws(
|
||||
() => cloudTeamSubscribeUrl("team_pro", "yearly", 0),
|
||||
/positive integer/,
|
||||
);
|
||||
assert.equal(
|
||||
scopedBillingPlanUrl("workspace-a"),
|
||||
"https://open-design.ai/cloud/dashboard?billing=plan&workspaceId=workspace-a",
|
||||
@@ -593,55 +787,35 @@ describe("pricing contract", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("renders all 16 introductory Team totals for interval, tier, and seat changes", () => {
|
||||
const expected = {
|
||||
team_basic: {
|
||||
monthly: { 3: "First month only $12", 4: "First month only $16" },
|
||||
yearly: { 3: "First year only $126", 4: "First year only $168" },
|
||||
},
|
||||
team_plus: {
|
||||
monthly: { 3: "First month only $60", 4: "First month only $80" },
|
||||
yearly: { 3: "First year only $630", 4: "First year only $840" },
|
||||
},
|
||||
team_pro: {
|
||||
monthly: {
|
||||
3: "First month only $220.50",
|
||||
4: "First month only $294",
|
||||
},
|
||||
yearly: {
|
||||
3: "First year only $2,268",
|
||||
4: "First year only $3,024",
|
||||
},
|
||||
},
|
||||
team_max: {
|
||||
monthly: { 3: "First month only $369", 4: "First month only $492" },
|
||||
yearly: {
|
||||
3: "First year only $3,622.83",
|
||||
4: "First year only $4,830.44",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
it("renders the Team monthly total, first-year total, and priced checkout from the annual offer", async () => {
|
||||
const page = await readFile(PRICING_PAGE_PATH, "utf8");
|
||||
const english = TEAM_PRICING_CONTENT_BY_LOCALE.en;
|
||||
|
||||
for (const tier of PRICING_SNAPSHOT.teamTiers) {
|
||||
for (const interval of ["monthly", "yearly"] as const) {
|
||||
for (const seats of [3, 4] as const) {
|
||||
const template =
|
||||
interval === "monthly"
|
||||
? english.monthlyTotal
|
||||
: english.yearlyTotal;
|
||||
const rendered = template.replace(
|
||||
"{amount}",
|
||||
formatUsd(teamIntroTotalUsd(tier, interval, seats)),
|
||||
);
|
||||
assert.equal(
|
||||
rendered,
|
||||
expected[tier.tier][interval][seats],
|
||||
`${tier.tier} ${interval} ${seats} seats`,
|
||||
);
|
||||
}
|
||||
for (const seats of [3, 4] as const) {
|
||||
const annualTotal = teamIntroTotalUsd(tier, "yearly", seats);
|
||||
const monthlyTotal = annualTotal / 12;
|
||||
const amount = (value: number) => formatUsd(value).replace(/^\$/, "");
|
||||
|
||||
assert.equal(
|
||||
english.monthlyTotal.replace("{amount}", amount(monthlyTotal)),
|
||||
`${amount(monthlyTotal)} / month for your team`,
|
||||
);
|
||||
assert.equal(
|
||||
english.yearlyTotal.replace("{amount}", amount(annualTotal)),
|
||||
`${amount(annualTotal)} billed for the first year`,
|
||||
);
|
||||
assert.equal(
|
||||
english.checkout.replace("{amount}", amount(annualTotal)),
|
||||
`Upgrade team · ${amount(annualTotal)}/year`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
assert.match(page, /data-team-monthly-summary/);
|
||||
assert.match(page, /data-team-yearly-summary/);
|
||||
assert.match(page, /data-team-checkout-total/);
|
||||
assert.match(page, /const interval = 'yearly';/);
|
||||
});
|
||||
|
||||
it("removes the obsolete Team-coming-soon banner", async () => {
|
||||
@@ -665,10 +839,32 @@ describe("pricing contract", () => {
|
||||
// generic global section padding must be cancelled on the component root.
|
||||
assert.match(individualPlans, /\.demo-individual-pricing\s*\{[^}]*padding:\s*0 !important;/s);
|
||||
|
||||
// Creator/Team and billing interval share one compact row above the cards.
|
||||
// Creator/Team and billing interval share one compact row above the
|
||||
// Individual cards. The billing group is right-aligned and disappears
|
||||
// entirely when the Team tab is active, matching the approved demo.
|
||||
assert.match(page, /class="pr-audience-toggle"[^>]*role="tablist"/);
|
||||
assert.match(page, /\.pr-controls-row\s*\{[^}]*grid-template-columns:\s*1fr auto 1fr;/s);
|
||||
assert.match(page, /\.pr-audience-btn\.is-active\s*\{[^}]*background:/s);
|
||||
assert.match(page, /\.pr-toggle\s*\{[^}]*grid-column:\s*3;[^}]*justify-self:\s*end;/s);
|
||||
assert.match(
|
||||
page,
|
||||
/\.pr-toggle-btn\s*\{[^}]*min-height:\s*36px;[^}]*font-size:\s*13px;[^}]*font-weight:\s*600;[^}]*line-height:\s*1;/s,
|
||||
);
|
||||
assert.match(
|
||||
page,
|
||||
/\.pr-audience-btn\s*\{[^}]*border-radius:\s*9px;[^}]*font-size:\s*14px;[^}]*font-weight:\s*600;/s,
|
||||
);
|
||||
assert.match(
|
||||
page,
|
||||
/\.pr-audience-btn\.is-active\s*\{[^}]*background:\s*#e8e9e3;/s,
|
||||
);
|
||||
assert.match(
|
||||
page,
|
||||
/const billingToggle = root\.querySelector\('\.pr-toggle'\);[\s\S]*?billingToggle\.hidden = audience === 'team';/,
|
||||
);
|
||||
assert.match(
|
||||
page,
|
||||
/\.pr-toggle\[hidden\]\s*\{\s*display:\s*none !important;\s*\}/,
|
||||
);
|
||||
|
||||
// The visible Team tier control must never open the OS-native select popup.
|
||||
assert.doesNotMatch(page, /<select[^>]*data-team-tier/);
|
||||
@@ -681,6 +877,53 @@ describe("pricing contract", () => {
|
||||
assert.doesNotMatch(page, /data-team-total/);
|
||||
});
|
||||
|
||||
it("matches the approved Team card geometry and decoration artwork", async () => {
|
||||
const page = await readFile(PRICING_PAGE_PATH, "utf8");
|
||||
|
||||
assert.match(
|
||||
page,
|
||||
/\.pr-team-panel\s*\{[^}]*width:\s*min\(1200px,\s*100%\);[^}]*max-width:\s*none;/s,
|
||||
);
|
||||
assert.match(page, /\.pr-team-grid\s*\{[^}]*gap:\s*12px;/s);
|
||||
assert.match(
|
||||
page,
|
||||
/\.pr-team-card\s*\{[^}]*min-height:\s*590px;[^}]*padding:\s*24px 20px 22px;[^}]*border:\s*0;[^}]*border-radius:\s*12px;[^}]*background:\s*#fff;/s,
|
||||
);
|
||||
assert.match(
|
||||
page,
|
||||
/\.pr-team-decoration\s*\{[^}]*top:\s*20px;[^}]*right:\s*20px;[^}]*width:\s*60px;[^}]*height:\s*60px;[^}]*color:\s*#55c94a;[^}]*opacity:\s*\.2;/s,
|
||||
);
|
||||
assert.match(
|
||||
page,
|
||||
/<circle cx="9" cy="8" r="3"\s*\/>[\s\S]*?<path d="M3\.5 19c\.35-3\.45 2\.1-5\.2 5\.5-5\.2s5\.15 1\.75 5\.5 5\.2"\s*\/>/,
|
||||
);
|
||||
assert.match(
|
||||
page,
|
||||
/<path d="M5 21V6\.5L12 3v18"\s*\/>[\s\S]*?<path d="M12 8h7v13"\s*\/>[\s\S]*?<path d="M3 21h18"\s*\/>/,
|
||||
);
|
||||
});
|
||||
|
||||
it("reveals the Team cards with the demo's staggered entrance motion", async () => {
|
||||
const page = await readFile(PRICING_PAGE_PATH, "utf8");
|
||||
|
||||
assert.match(
|
||||
page,
|
||||
/\.pr-team-card\s*\{[^}]*animation:\s*pr-team-card-enter 0\.65s cubic-bezier\(0\.16,\s*1,\s*0\.3,\s*1\) both;/s,
|
||||
);
|
||||
assert.match(
|
||||
page,
|
||||
/\.pr-team-card:nth-child\(2\)\s*\{\s*animation-delay:\s*0\.09s;\s*\}/,
|
||||
);
|
||||
assert.match(
|
||||
page,
|
||||
/@keyframes pr-team-card-enter\s*\{\s*from\s*\{\s*opacity:\s*0;\s*transform:\s*translateY\(12px\);\s*\}\s*to\s*\{\s*opacity:\s*1;\s*transform:\s*translateY\(0\);\s*\}\s*\}/,
|
||||
);
|
||||
assert.match(
|
||||
page,
|
||||
/@media \(prefers-reduced-motion:\s*reduce\)\s*\{[\s\S]*?\.pr-team-card\s*\{[^}]*animation:\s*none;[^}]*transform:\s*none;/,
|
||||
);
|
||||
});
|
||||
|
||||
it("localizes the flagship Pricing structure for every active locale", () => {
|
||||
const activeLocales = LANDING_LOCALES.map((locale) => locale.code);
|
||||
|
||||
@@ -699,6 +942,7 @@ describe("pricing contract", () => {
|
||||
);
|
||||
assert.match(copy.monthlyTotal, /\{amount\}/);
|
||||
assert.match(copy.yearlyTotal, /\{amount\}/);
|
||||
assert.match(copy.checkout, /\{amount\}/);
|
||||
assert.doesNotMatch(copy.monthlyTotal, /\{count\}|\{savings\}/);
|
||||
assert.doesNotMatch(copy.yearlyTotal, /\{count\}|\{savings\}/);
|
||||
assert.equal("yearlySummary" in copy, false);
|
||||
@@ -706,23 +950,23 @@ describe("pricing contract", () => {
|
||||
|
||||
assert.equal(
|
||||
TEAM_PRICING_CONTENT_BY_LOCALE.zh.monthlyTotal,
|
||||
"首月仅需 {amount}",
|
||||
"团队合计 {amount} / 月",
|
||||
);
|
||||
assert.equal(
|
||||
TEAM_PRICING_CONTENT_BY_LOCALE.zh.yearlyTotal,
|
||||
"首年仅需 {amount}",
|
||||
"首年应付 {amount}",
|
||||
);
|
||||
assert.equal(
|
||||
TEAM_PRICING_CONTENT_BY_LOCALE.en.monthlyTotal,
|
||||
"First month only {amount}",
|
||||
"{amount} / month for your team",
|
||||
);
|
||||
assert.equal(
|
||||
TEAM_PRICING_CONTENT_BY_LOCALE.en.yearlyTotal,
|
||||
"First year only {amount}",
|
||||
"{amount} billed for the first year",
|
||||
);
|
||||
});
|
||||
|
||||
it("updates the Team intro-period total for period, tier, and seat controls", async () => {
|
||||
it("updates the annual Team quote for tier and seat controls", async () => {
|
||||
const page = await readFile(PRICING_PAGE_PATH, "utf8");
|
||||
const updateStart = page.indexOf("const updateTeamPlan = () =>");
|
||||
const updateEnd = page.indexOf(
|
||||
@@ -735,17 +979,19 @@ describe("pricing contract", () => {
|
||||
|
||||
assert.match(
|
||||
page,
|
||||
/fillTemplate\(teamContent\.yearlyTotal,\s*\{\s*amount:\s*initialTeamView\.intervalTotal,\s*\}\)/s,
|
||||
/fillTemplate\(teamContent\.monthlyTotal,\s*\{\s*amount:\s*initialTeamView\.monthlyTeamTotal,\s*\}\)/s,
|
||||
);
|
||||
assert.match(
|
||||
updateTeamPlan,
|
||||
/interval === 'yearly'\s*\?\s*teamCopy\.yearlyTotal\s*:\s*teamCopy\.monthlyTotal/,
|
||||
/const interval = 'yearly';/,
|
||||
);
|
||||
assert.match(
|
||||
updateTeamPlan,
|
||||
/const intervalTotal = selected\.introPriceUsd \* teamSeats/,
|
||||
);
|
||||
assert.match(page, /const activateInterval = \(interval, via\) => \{[\s\S]*?updateTeamPlan\(\)/);
|
||||
assert.match(updateTeamPlan, /fillTeam\(teamCopy\.monthlyTotal, \{ amount: monthlyTotal \}\)/);
|
||||
assert.match(updateTeamPlan, /fillTeam\(teamCopy\.yearlyTotal, \{ amount: total \}\)/);
|
||||
assert.match(updateTeamPlan, /fillTeam\(teamCopy\.checkout, \{ amount: total \}\)/);
|
||||
assert.match(page, /const selectTeamTier = \(option\) => \{[\s\S]*?updateTeamPlan\(\)/);
|
||||
assert.match(
|
||||
page,
|
||||
@@ -758,6 +1004,21 @@ describe("pricing contract", () => {
|
||||
assert.doesNotMatch(updateTeamPlan, /teamCopy\.yearlySummary/);
|
||||
assert.doesNotMatch(updateTeamPlan, /labels\.monthlyRenewal/);
|
||||
assert.doesNotMatch(updateTeamPlan, /savings|regularTotal/);
|
||||
assert.match(updateTeamPlan, /syncCtas\(\)/);
|
||||
});
|
||||
|
||||
it("hands the exact Team selection to Vela checkout", async () => {
|
||||
const page = await readFile(PRICING_PAGE_PATH, "utf8");
|
||||
const syncStart = page.indexOf("const syncCtas = () =>");
|
||||
const syncEnd = page.indexOf("syncCtas();", syncStart);
|
||||
assert.notEqual(syncStart, -1);
|
||||
assert.notEqual(syncEnd, -1);
|
||||
const syncCtas = page.slice(syncStart, syncEnd);
|
||||
|
||||
assert.match(syncCtas, /target\.searchParams\.set\('plan', selectedTier\.tier\)/);
|
||||
assert.match(syncCtas, /target\.searchParams\.set\('interval', 'yearly'\)/);
|
||||
assert.match(syncCtas, /'seats',[\s\S]*String\(Math\.max\(selectedTier\.minSeats, selectedSeats\)\)/);
|
||||
assert.match(syncCtas, /target\.searchParams\.set\('checkout', 'auto'\)/);
|
||||
});
|
||||
|
||||
it("removes the superseded Team total and annual-savings copy", async () => {
|
||||
|
||||
@@ -5421,6 +5421,14 @@ function AppInner() {
|
||||
? projectRouteWorkspaceContext.loading
|
||||
: undefined
|
||||
}
|
||||
amrLoggedIn={amrLoginStatus?.loggedIn ?? null}
|
||||
amrAccountPlan={
|
||||
amrLoginStatus?.account?.plan?.trim()
|
||||
|| amrLoginStatus?.user?.plan?.trim()
|
||||
|| null
|
||||
}
|
||||
metricsConsent={config.telemetry?.metrics === true}
|
||||
installationId={config.installationId}
|
||||
/>
|
||||
) : null}
|
||||
{route.kind === 'project'
|
||||
|
||||
@@ -18,18 +18,18 @@ export interface GoPlanCampaignCopy {
|
||||
|
||||
const english: GoPlanCampaignCopy = {
|
||||
eyebrow: 'NEW PLAN · LAUNCH OFFER',
|
||||
headline: 'Meet Go: more room to keep creating.',
|
||||
description: 'A lighter plan for everyday design and coding, with popular models ready to use.',
|
||||
headline: 'Low-cost design plan for everyone',
|
||||
description: 'Professional design intelligence at a lower cost—so every idea moves faster from prompt to finished work.',
|
||||
benefit: 'Go first month $5 · unlimited use',
|
||||
status: 'UNLIMITED',
|
||||
cta: 'View Go plan',
|
||||
cta: 'View Go plan · Limited-time 50% off',
|
||||
renewal: 'Then $10 / month',
|
||||
boundary: 'Allowance details and offer terms are shown on Pricing.',
|
||||
newBadge: 'NEW',
|
||||
closeAria: 'Close dialog',
|
||||
providersAria: 'Model providers available on Go',
|
||||
workbenchBadge: 'Go first month $5 · unlimited use',
|
||||
workbenchBadgeAria: 'Go first month $5, unlimited use — view Pricing',
|
||||
workbenchBadge: 'The new Go Plan · ¥5 for the first month · Unlimited model usage',
|
||||
workbenchBadgeAria: 'The new Go Plan, ¥5 for the first month, unlimited model usage — view Pricing',
|
||||
};
|
||||
|
||||
const localized: Record<Locale, GoPlanCampaignCopy> = {
|
||||
@@ -66,18 +66,18 @@ const localized: Record<Locale, GoPlanCampaignCopy> = {
|
||||
},
|
||||
'zh-CN': {
|
||||
eyebrow: '全新套餐 · 首发特惠',
|
||||
headline: 'Go,让灵感不断线。',
|
||||
description: '面向日常设计与编码的轻量套餐,热门模型开箱即用。',
|
||||
headline: '人人可用的低成本设计方案',
|
||||
description: '以更低成本使用专业设计模型,让每一个想法更快成为作品。',
|
||||
benefit: 'Go 首月 $5 · 无限用',
|
||||
status: '无限使用',
|
||||
cta: '查看 Go 套餐',
|
||||
cta: '查看 Go 套餐 · 限时 5 折',
|
||||
renewal: '之后 $10 / 月',
|
||||
boundary: '具体额度与活动规则以 Pricing 页面为准。',
|
||||
newBadge: 'NEW',
|
||||
closeAria: '关闭弹窗',
|
||||
providersAria: 'Go 套餐可用模型厂商',
|
||||
workbenchBadge: 'Go 首月 $5 · 无限用',
|
||||
workbenchBadgeAria: 'Go 首月 5 美元,无限用,查看 Pricing',
|
||||
workbenchBadge: '全新 Go 套餐 · 首月 ¥5 · 模型无限用',
|
||||
workbenchBadgeAria: '全新 Go 套餐,首月 ¥5,模型无限用,查看 Pricing',
|
||||
},
|
||||
'zh-TW': {
|
||||
eyebrow: '全新方案 · 首發優惠',
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { DeepSeekV4FlashCampaignAudience } from './deepseek-v4-flash';
|
||||
export const GO_PLAN_CAMPAIGN = {
|
||||
id: 'go-plan-launch-2026',
|
||||
window: {
|
||||
startAt: '2026-08-20T20:00:00+08:00',
|
||||
startAt: '2026-08-20T17:00:00+08:00',
|
||||
endAtExclusive: '2026-09-03T20:00:00+08:00',
|
||||
},
|
||||
} as const;
|
||||
@@ -24,7 +24,7 @@ export function goPlanCampaignNextBoundary(now: number): number | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Resolve subscription state without coupling the permanent Go entry to a campaign window. */
|
||||
/** Resolve subscription state independently from the campaign's launch window. */
|
||||
export function resolveSubscriptionAudience(input: {
|
||||
plan: string | null | undefined;
|
||||
loggedIn: boolean | null | undefined;
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
padding: 54px 42px 28px;
|
||||
padding: 64px 42px 28px 28px;
|
||||
}
|
||||
|
||||
.goWelcomeCopy h2 {
|
||||
@@ -251,7 +251,7 @@
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
object-fit: contain;
|
||||
filter: invert(1) brightness(2);
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.goWelcomeBenefitZhipu img {
|
||||
|
||||
@@ -74,6 +74,10 @@ import {
|
||||
import { canUpgradeFromPlanTier, resolvePlanLabelTier } from '../collab/team-plan';
|
||||
import { amrPlansUrlForProfile } from '../runtime/amr-guidance';
|
||||
import { useWorkspaceInvalidation } from '../collab/workspace-events';
|
||||
import { resolveDeepSeekV4FlashCampaignAudience } from '../campaigns/deepseek-v4-flash';
|
||||
import { useDeepSeekV4FlashCampaignVisibility } from '../campaigns/use-deepseek-v4-flash-campaign';
|
||||
import { resolveSubscriptionAudience } from '../campaigns/go-plan';
|
||||
import { useGoPlanCampaignVisibility } from '../campaigns/use-go-plan-campaign';
|
||||
import type { EntryHomeView } from '../router';
|
||||
import type {
|
||||
AccountMenuClickProps,
|
||||
@@ -92,6 +96,7 @@ import {
|
||||
stableAnalyticsErrorCode,
|
||||
workspaceAnalyticsDimensions,
|
||||
} from '../analytics/workspace';
|
||||
import { WorkbenchCampaignBadge } from './WorkbenchCampaignBadge';
|
||||
|
||||
const REPO_URL = 'https://github.com/nexu-io/open-design';
|
||||
const GITHUB_HELP_URL = `${REPO_URL}/issues/new`;
|
||||
@@ -1078,6 +1083,10 @@ export function WorkspaceTopRightAccountCluster({
|
||||
updaterSlot,
|
||||
workspaceContextOverride,
|
||||
workspaceContextLoading,
|
||||
amrLoggedIn = null,
|
||||
amrAccountPlan = null,
|
||||
metricsConsent = false,
|
||||
installationId,
|
||||
}: {
|
||||
onOpenSettings?: (section?: EntrySettingsSection) => void;
|
||||
onSignedOut?: () => void | Promise<void>;
|
||||
@@ -1085,29 +1094,69 @@ export function WorkspaceTopRightAccountCluster({
|
||||
updaterSlot?: ReactNode;
|
||||
workspaceContextOverride?: WorkspaceCollabContext | null;
|
||||
workspaceContextLoading?: boolean;
|
||||
amrLoggedIn?: boolean | null;
|
||||
amrAccountPlan?: string | null;
|
||||
metricsConsent?: boolean;
|
||||
installationId?: string | null;
|
||||
}) {
|
||||
const ambient = useWorkspaceContext();
|
||||
const hasExplicitWorkspaceContext = workspaceContextOverride !== undefined;
|
||||
const context = hasExplicitWorkspaceContext
|
||||
? workspaceContextOverride
|
||||
: ambient.context;
|
||||
const contextLoading = hasExplicitWorkspaceContext
|
||||
? workspaceContextLoading === true
|
||||
: ambient.loading;
|
||||
const billingResponse = useWorkspaceBillingResponse({
|
||||
context,
|
||||
loading: hasExplicitWorkspaceContext
|
||||
? workspaceContextLoading === true
|
||||
: ambient.loading,
|
||||
loading: contextLoading,
|
||||
});
|
||||
// Plan and money are both workspace-scoped questions, so both go through a
|
||||
// context-partitioned projection — `response.summary` on its own is an
|
||||
// ACCOUNT read (`workspaceId: null` by contract). Same rule as EntryShell.
|
||||
const billing = workspaceBillingSummaryForContext(billingResponse, context);
|
||||
const balanceUsd = workspaceBillingBalanceUsd(billingResponse, context);
|
||||
const deepSeekCampaignVisibility = useDeepSeekV4FlashCampaignVisibility();
|
||||
const goPlanCampaignVisibility = useGoPlanCampaignVisibility();
|
||||
const campaignPlan = resolvePlanLabelTier({
|
||||
billing,
|
||||
context,
|
||||
accountPlan:
|
||||
contextLoading || context?.workspaceType === 'team'
|
||||
? null
|
||||
: amrAccountPlan,
|
||||
});
|
||||
const deepSeekCampaignAudience = resolveDeepSeekV4FlashCampaignAudience({
|
||||
plan: campaignPlan,
|
||||
loggedIn: amrLoggedIn,
|
||||
now: deepSeekCampaignVisibility.now,
|
||||
});
|
||||
const subscriptionAudience = resolveSubscriptionAudience({
|
||||
plan: campaignPlan,
|
||||
loggedIn: amrLoggedIn,
|
||||
});
|
||||
const campaignKind =
|
||||
subscriptionAudience === 'unpaid'
|
||||
? goPlanCampaignVisibility.visible
|
||||
? 'go'
|
||||
: null
|
||||
: deepSeekCampaignAudience === 'paid'
|
||||
? 'deepseek'
|
||||
: null;
|
||||
return (
|
||||
<EntryTopRightCluster
|
||||
page="project"
|
||||
context={context}
|
||||
billing={billing}
|
||||
balanceUsd={balanceUsd}
|
||||
leadingSlot={campaignKind ? (
|
||||
<WorkbenchCampaignBadge
|
||||
kind={campaignKind}
|
||||
page="project"
|
||||
metricsConsent={metricsConsent}
|
||||
installationId={installationId}
|
||||
/>
|
||||
) : null}
|
||||
updaterSlot={updaterSlot}
|
||||
onOpenSettings={onOpenSettings}
|
||||
onSignedOut={onSignedOut}
|
||||
|
||||
@@ -39,12 +39,9 @@ import {
|
||||
trackOnboardingCompleteResult,
|
||||
trackOnboardingRuntimeScanResult,
|
||||
trackPageView,
|
||||
trackDeepSeekCampaignBadgeClick,
|
||||
trackDeepSeekCampaignBadgeSurfaceView,
|
||||
} from '../analytics/events';
|
||||
import {
|
||||
amrHandoffDeviceId,
|
||||
attributedAmrUrl,
|
||||
recordAmrEntry,
|
||||
type AmrEntryAttribution,
|
||||
} from '../analytics/amr-attribution';
|
||||
@@ -158,11 +155,10 @@ import { resolvePlanLabelTier } from '../collab/team-plan';
|
||||
import { resolveDeepSeekV4FlashCampaignAudience } from '../campaigns/deepseek-v4-flash';
|
||||
import { useDeepSeekV4FlashCampaignVisibility } from '../campaigns/use-deepseek-v4-flash-campaign';
|
||||
import {
|
||||
GO_PLAN_PRICING_URL,
|
||||
resolveSubscriptionAudience,
|
||||
} from '../campaigns/go-plan';
|
||||
import { useGoPlanCampaignVisibility } from '../campaigns/use-go-plan-campaign';
|
||||
import { getGoPlanCampaignCopy } from '../campaigns/go-plan-content';
|
||||
import { WorkbenchCampaignBadge } from './WorkbenchCampaignBadge';
|
||||
import {
|
||||
beginWorkspaceScopedRead,
|
||||
workspaceIdentityCacheKey,
|
||||
@@ -611,8 +607,7 @@ export function EntryShell({
|
||||
onAmrLoginStatusChange,
|
||||
artifactUpgradeSlot,
|
||||
}: Props) {
|
||||
const { locale, t } = useI18n();
|
||||
const goPlanCopy = getGoPlanCampaignCopy(locale);
|
||||
const { t } = useI18n();
|
||||
// Each entry sub-view (home / projects / design-systems) is its own
|
||||
// URL now, so the browser back/forward buttons work and a deep link
|
||||
// to /design-systems lands on that section. We derive the active
|
||||
@@ -704,7 +699,9 @@ export function EntryShell({
|
||||
: 'unknown';
|
||||
const topRightCampaignKind =
|
||||
subscriptionAudience === 'unpaid'
|
||||
? 'go'
|
||||
? goPlanCampaignVisibility.visible
|
||||
? 'go'
|
||||
: null
|
||||
: deepSeekV4FlashCampaignAudience === 'paid'
|
||||
? 'deepseek'
|
||||
: null;
|
||||
@@ -1189,62 +1186,6 @@ export function EntryShell({
|
||||
scrollContainer.scrollTop = 0;
|
||||
}, [view]);
|
||||
const analytics = useAnalytics();
|
||||
useEffect(() => {
|
||||
if (view !== 'home' || !topRightCampaignKind) return;
|
||||
const go = topRightCampaignKind === 'go';
|
||||
if (go) return;
|
||||
trackDeepSeekCampaignBadgeSurfaceView(analytics.track, {
|
||||
page_name: 'home',
|
||||
area: 'campaign_badge',
|
||||
element: 'deepseek_v4_pro',
|
||||
campaign_id: 'deepseek_v4_pro',
|
||||
user_state: 'paid',
|
||||
});
|
||||
}, [analytics.track, topRightCampaignKind, view]);
|
||||
const openCampaignPricing = useCallback(() => {
|
||||
if (!topRightCampaignKind) return;
|
||||
const go = topRightCampaignKind === 'go';
|
||||
if (go) {
|
||||
window.open(GO_PLAN_PRICING_URL, '_blank', 'noopener,noreferrer');
|
||||
return;
|
||||
}
|
||||
trackDeepSeekCampaignBadgeClick(analytics.track, {
|
||||
page_name: 'home',
|
||||
area: 'campaign_badge',
|
||||
element: 'open_pricing',
|
||||
campaign_id: 'deepseek_v4_pro',
|
||||
user_state: 'paid',
|
||||
});
|
||||
const attribution = recordAmrEntry(
|
||||
analytics.track,
|
||||
'deepseek_workbench_badge',
|
||||
new Date(),
|
||||
{
|
||||
metricsConsent: config.telemetry?.metrics === true,
|
||||
campaignId: 'deepseek_v4_pro',
|
||||
conversionSource: 'deepseek_workbench_badge',
|
||||
},
|
||||
);
|
||||
const deviceId = amrHandoffDeviceId({
|
||||
metricsConsent: config.telemetry?.metrics === true,
|
||||
resolvedDeviceId: getResolvedDeviceId(),
|
||||
installationId: config.installationId,
|
||||
});
|
||||
// Both campaign badges are lightweight discovery entries. Pricing owns
|
||||
// comparison; only a concrete Pricing card hands checkout to Cloud.
|
||||
const destination = GO_PLAN_PRICING_URL;
|
||||
window.open(
|
||||
attributedAmrUrl(destination, attribution, deviceId),
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
);
|
||||
}, [
|
||||
analytics.track,
|
||||
config.installationId,
|
||||
config.telemetry?.metrics,
|
||||
topRightCampaignKind,
|
||||
workspaceContext?.workspaceId,
|
||||
]);
|
||||
// 产品拍板 D5: the campaign modal's paid 立即使用 performs the REAL switch —
|
||||
// daemon execution mode + Cloud agent (amr) + DeepSeek V4 Flash — through
|
||||
// the same persistence callbacks the InlineModelSwitcher writes through.
|
||||
@@ -1681,21 +1622,13 @@ export function EntryShell({
|
||||
onOpenSearch={() => setProjectSearchOpen(true)}
|
||||
open={railOpen}
|
||||
topRightSlot={
|
||||
view === 'home' && topRightCampaignKind ? (
|
||||
<button
|
||||
type="button"
|
||||
className="entry-deepseek-campaign-badge"
|
||||
onClick={openCampaignPricing}
|
||||
aria-label={topRightCampaignKind === 'go'
|
||||
? goPlanCopy.workbenchBadgeAria
|
||||
: t('campaign.deepseekV4Flash.workbenchBadgeAria')}
|
||||
data-testid="deepseek-campaign-pricing-badge"
|
||||
>
|
||||
<span>{topRightCampaignKind === 'go'
|
||||
? goPlanCopy.workbenchBadge
|
||||
: t('campaign.deepseekV4Flash.workbenchBadge')}</span>
|
||||
<Icon name="arrow-right" size={13} />
|
||||
</button>
|
||||
topRightCampaignKind ? (
|
||||
<WorkbenchCampaignBadge
|
||||
kind={topRightCampaignKind}
|
||||
page="home"
|
||||
metricsConsent={config.telemetry?.metrics === true}
|
||||
installationId={config.installationId}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
context={railWorkspaceContext}
|
||||
@@ -1728,9 +1661,8 @@ export function EntryShell({
|
||||
lives in the rail footer, and everything below is fixed-position
|
||||
or portalled so it occupies no layout space here. */}
|
||||
<WhatsNewPopup active={view === 'home'} />
|
||||
{/* DeepSeek campaign badge moved into EntryNavRail's top-right
|
||||
cluster (topRightSlot above) so it sits beside the account
|
||||
module in one flex row. */}
|
||||
{/* The campaign badge lives in EntryNavRail's top-right cluster so it
|
||||
stays beside the account module across every entry tab. */}
|
||||
{amrBalanceGateBlock ? (
|
||||
<AmrBalanceDialog
|
||||
reason={amrBalanceGateBlock.reason}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import {
|
||||
trackDeepSeekCampaignBadgeClick,
|
||||
trackDeepSeekCampaignBadgeSurfaceView,
|
||||
} from '../analytics/events';
|
||||
import {
|
||||
amrHandoffDeviceId,
|
||||
attributedAmrUrl,
|
||||
recordAmrEntry,
|
||||
} from '../analytics/amr-attribution';
|
||||
import { getResolvedDeviceId } from '../analytics/client';
|
||||
import { useAnalytics } from '../analytics/provider';
|
||||
import { GO_PLAN_PRICING_URL } from '../campaigns/go-plan';
|
||||
import { getGoPlanCampaignCopy } from '../campaigns/go-plan-content';
|
||||
import { useI18n } from '../i18n';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
export type WorkbenchCampaignKind = 'go' | 'deepseek';
|
||||
|
||||
export function WorkbenchCampaignBadge({
|
||||
kind,
|
||||
page,
|
||||
metricsConsent,
|
||||
installationId,
|
||||
}: {
|
||||
kind: WorkbenchCampaignKind;
|
||||
page: 'home' | 'project';
|
||||
metricsConsent: boolean;
|
||||
installationId?: string | null;
|
||||
}) {
|
||||
const { locale, t } = useI18n();
|
||||
const analytics = useAnalytics();
|
||||
const goPlanCopy = getGoPlanCampaignCopy(locale);
|
||||
|
||||
useEffect(() => {
|
||||
// The current campaign analytics contract scopes badge impressions to
|
||||
// Home. Project-detail visibility is intentionally UI-only until that
|
||||
// contract gains a project page variant.
|
||||
if (kind !== 'deepseek' || page !== 'home') return;
|
||||
trackDeepSeekCampaignBadgeSurfaceView(analytics.track, {
|
||||
page_name: 'home',
|
||||
area: 'campaign_badge',
|
||||
element: 'deepseek_v4_pro',
|
||||
campaign_id: 'deepseek_v4_pro',
|
||||
user_state: 'paid',
|
||||
});
|
||||
}, [analytics.track, kind, page]);
|
||||
|
||||
const openCampaignPricing = useCallback(() => {
|
||||
if (kind === 'go') {
|
||||
window.open(GO_PLAN_PRICING_URL, '_blank', 'noopener,noreferrer');
|
||||
return;
|
||||
}
|
||||
if (page === 'home') {
|
||||
trackDeepSeekCampaignBadgeClick(analytics.track, {
|
||||
page_name: 'home',
|
||||
area: 'campaign_badge',
|
||||
element: 'open_pricing',
|
||||
campaign_id: 'deepseek_v4_pro',
|
||||
user_state: 'paid',
|
||||
});
|
||||
}
|
||||
const attribution = recordAmrEntry(
|
||||
analytics.track,
|
||||
'deepseek_workbench_badge',
|
||||
new Date(),
|
||||
{
|
||||
metricsConsent,
|
||||
campaignId: 'deepseek_v4_pro',
|
||||
conversionSource: 'deepseek_workbench_badge',
|
||||
},
|
||||
);
|
||||
const deviceId = amrHandoffDeviceId({
|
||||
metricsConsent,
|
||||
resolvedDeviceId: getResolvedDeviceId(),
|
||||
installationId,
|
||||
});
|
||||
window.open(
|
||||
attributedAmrUrl(GO_PLAN_PRICING_URL, attribution, deviceId),
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
);
|
||||
}, [analytics.track, installationId, kind, metricsConsent, page]);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="entry-deepseek-campaign-badge"
|
||||
onClick={openCampaignPricing}
|
||||
aria-label={kind === 'go'
|
||||
? goPlanCopy.workbenchBadgeAria
|
||||
: t('campaign.deepseekV4Flash.workbenchBadgeAria')}
|
||||
data-testid="deepseek-campaign-pricing-badge"
|
||||
>
|
||||
<span>{kind === 'go'
|
||||
? goPlanCopy.workbenchBadge
|
||||
: t('campaign.deepseekV4Flash.workbenchBadge')}</span>
|
||||
<Icon name="arrow-right" size={13} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,18 @@ const entryShellSource = readFileSync(
|
||||
resolve(process.cwd(), 'src/components/EntryShell.tsx'),
|
||||
'utf8',
|
||||
);
|
||||
const entryNavRailSource = readFileSync(
|
||||
resolve(process.cwd(), 'src/components/EntryNavRail.tsx'),
|
||||
'utf8',
|
||||
);
|
||||
const appSource = readFileSync(
|
||||
resolve(process.cwd(), 'src/App.tsx'),
|
||||
'utf8',
|
||||
);
|
||||
const workbenchCampaignBadgeSource = readFileSync(
|
||||
resolve(process.cwd(), 'src/components/WorkbenchCampaignBadge.tsx'),
|
||||
'utf8',
|
||||
);
|
||||
const entryLayoutStyles = readFileSync(
|
||||
resolve(process.cwd(), 'src/styles/home/entry-layout.css'),
|
||||
'utf8',
|
||||
@@ -59,6 +71,9 @@ describe('DeepSeek V4 Flash workbench campaign entry', () => {
|
||||
expect(campaignModalStyles).not.toContain(
|
||||
'.goWelcomeBenefitModel.goWelcomeBenefitZhipu img',
|
||||
);
|
||||
expect(campaignModalStyles).toMatch(
|
||||
/\.goWelcomeBenefitModel img\s*\{[\s\S]*?filter: brightness\(0\) invert\(1\);[\s\S]*?\}/,
|
||||
);
|
||||
});
|
||||
|
||||
it('collapses the Go modal before its fixed tracks overflow and scrolls short viewports', () => {
|
||||
@@ -78,22 +93,39 @@ describe('DeepSeek V4 Flash workbench campaign entry', () => {
|
||||
});
|
||||
|
||||
it('reuses the top-right campaign slot for Go and DeepSeek audiences', () => {
|
||||
expect(entryShellSource).toContain('deepseek-campaign-pricing-badge');
|
||||
expect(entryShellSource).toContain("topRightCampaignKind === 'go'");
|
||||
expect(entryShellSource).toContain('goPlanCopy.workbenchBadge');
|
||||
expect(entryShellSource).toContain("t('campaign.deepseekV4Flash.workbenchBadge')");
|
||||
expect(entryShellSource).toContain("t('campaign.deepseekV4Flash.workbenchBadgeAria')");
|
||||
expect(entryShellSource).toContain('<WorkbenchCampaignBadge');
|
||||
expect(workbenchCampaignBadgeSource).toContain('deepseek-campaign-pricing-badge');
|
||||
expect(workbenchCampaignBadgeSource).toContain("kind === 'go'");
|
||||
expect(workbenchCampaignBadgeSource).toContain('goPlanCopy.workbenchBadge');
|
||||
expect(workbenchCampaignBadgeSource).toContain("t('campaign.deepseekV4Flash.workbenchBadge')");
|
||||
expect(workbenchCampaignBadgeSource).toContain("t('campaign.deepseekV4Flash.workbenchBadgeAria')");
|
||||
expect(entryShellSource).toContain("subscriptionAudience === 'unpaid'");
|
||||
expect(entryShellSource).toContain('goPlanCampaignVisibility.visible');
|
||||
});
|
||||
|
||||
it('keeps the top-right campaign entry visible across entry tabs and project detail', () => {
|
||||
expect(entryShellSource).toMatch(
|
||||
/topRightSlot=\{\s*topRightCampaignKind \? \(/,
|
||||
);
|
||||
expect(entryShellSource).not.toMatch(
|
||||
/topRightSlot=\{\s*view === 'home'/,
|
||||
);
|
||||
expect(entryNavRailSource).toMatch(
|
||||
/export function WorkspaceTopRightAccountCluster[\s\S]*?leadingSlot=\{campaignKind \? \([\s\S]*?<WorkbenchCampaignBadge[\s\S]*?page="project"/,
|
||||
);
|
||||
expect(appSource).toMatch(
|
||||
/<WorkspaceTopRightAccountCluster[\s\S]*?amrLoggedIn=\{amrLoginStatus\?\.loggedIn \?\? null\}[\s\S]*?metricsConsent=\{config\.telemetry\?\.metrics === true\}/,
|
||||
);
|
||||
});
|
||||
|
||||
it('sends both Go and paid DeepSeek badges to public Pricing', () => {
|
||||
expect(entryShellSource).not.toContain('amrPlansUrlForWorkspace');
|
||||
expect(entryShellSource).toContain('GO_PLAN_PRICING_URL');
|
||||
expect(entryShellSource).toContain("'deepseek_workbench_badge'");
|
||||
expect(entryShellSource).toContain("'noopener,noreferrer'");
|
||||
expect(workbenchCampaignBadgeSource).toContain('GO_PLAN_PRICING_URL');
|
||||
expect(workbenchCampaignBadgeSource).toContain("'deepseek_workbench_badge'");
|
||||
expect(workbenchCampaignBadgeSource).toContain("'noopener,noreferrer'");
|
||||
// The public URL is locale-neutral; no language is pinned into a link
|
||||
// shown to every locale.
|
||||
expect(entryShellSource).not.toContain('open-design.ai/zh/pricing');
|
||||
expect(workbenchCampaignBadgeSource).not.toContain('open-design.ai/zh/pricing');
|
||||
});
|
||||
|
||||
it('reuses the existing DeepSeek badge treatment without Go-only chrome', () => {
|
||||
@@ -108,7 +140,7 @@ describe('DeepSeek V4 Flash workbench campaign entry', () => {
|
||||
expect(entryLayoutStyles).toContain('.entry-deepseek-campaign-badge::before');
|
||||
expect(entryLayoutStyles).toContain('background: var(--brand-text)');
|
||||
expect(entryLayoutStyles).toContain('.entry-deepseek-campaign-badge svg');
|
||||
expect(entryShellSource).toContain('className="entry-deepseek-campaign-badge"');
|
||||
expect(workbenchCampaignBadgeSource).toContain('className="entry-deepseek-campaign-badge"');
|
||||
expect(entryLayoutStyles).not.toContain('.entry-go-campaign-new');
|
||||
expect(entryLayoutStyles).not.toContain('.entry-go-campaign-badge');
|
||||
expect(badgeRule).not.toContain('color: var(--green)');
|
||||
@@ -180,9 +212,10 @@ describe('DeepSeek V4 Flash workbench campaign entry', () => {
|
||||
});
|
||||
|
||||
it('keeps existing DeepSeek analytics while the Go pass stays UI-only', () => {
|
||||
expect(entryShellSource).toContain('trackDeepSeekCampaignBadgeSurfaceView');
|
||||
expect(entryShellSource).toContain('trackDeepSeekCampaignBadgeClick');
|
||||
expect(entryShellSource).toContain("window.open(GO_PLAN_PRICING_URL, '_blank', 'noopener,noreferrer')");
|
||||
expect(workbenchCampaignBadgeSource).toContain('trackDeepSeekCampaignBadgeSurfaceView');
|
||||
expect(workbenchCampaignBadgeSource).toContain('trackDeepSeekCampaignBadgeClick');
|
||||
expect(workbenchCampaignBadgeSource).toContain("window.open(GO_PLAN_PRICING_URL, '_blank', 'noopener,noreferrer')");
|
||||
expect(workbenchCampaignBadgeSource).toContain("page !== 'home'");
|
||||
expect(modelSwitcherSource).toContain('trackDeepSeekCampaignModelBenefitSurfaceView');
|
||||
expect(modelSwitcherSource).toContain('trackExecutionSettingsPopoverClick');
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ describe('Go plan touchpoints', () => {
|
||||
const start = Date.parse(GO_PLAN_CAMPAIGN.window.startAt);
|
||||
const end = Date.parse(GO_PLAN_CAMPAIGN.window.endAtExclusive);
|
||||
|
||||
expect(GO_PLAN_CAMPAIGN.window.startAt).toBe('2026-08-20T17:00:00+08:00');
|
||||
expect(isGoPlanCampaignWindowOpen(start - 1)).toBe(false);
|
||||
expect(isGoPlanCampaignWindowOpen(start)).toBe(true);
|
||||
expect(isGoPlanCampaignWindowOpen(end - 1)).toBe(true);
|
||||
@@ -32,8 +33,23 @@ describe('Go plan touchpoints', () => {
|
||||
});
|
||||
|
||||
it('keeps the confirmed Chinese lightweight-entry copy', () => {
|
||||
expect(getGoPlanCampaignCopy('zh-CN').workbenchBadge).toBe('Go 首月 $5 · 无限用');
|
||||
expect(getGoPlanCampaignCopy('en').workbenchBadge).toBe('Go first month $5 · unlimited use');
|
||||
const chinese = getGoPlanCampaignCopy('zh-CN');
|
||||
const english = getGoPlanCampaignCopy('en');
|
||||
|
||||
expect(chinese.workbenchBadge).toBe('全新 Go 套餐 · 首月 ¥5 · 模型无限用');
|
||||
expect(chinese.headline).toBe('人人可用的低成本设计方案');
|
||||
expect(chinese.description).toBe(
|
||||
'以更低成本使用专业设计模型,让每一个想法更快成为作品。',
|
||||
);
|
||||
expect(chinese.cta).toBe('查看 Go 套餐 · 限时 5 折');
|
||||
expect(english.workbenchBadge).toBe(
|
||||
'The new Go Plan · ¥5 for the first month · Unlimited model usage',
|
||||
);
|
||||
expect(english.headline).toBe('Low-cost design plan for everyone');
|
||||
expect(english.description).toBe(
|
||||
'Professional design intelligence at a lower cost—so every idea moves faster from prompt to finished work.',
|
||||
);
|
||||
expect(english.cta).toBe('View Go plan · Limited-time 50% off');
|
||||
});
|
||||
|
||||
it('ships localized modal and workbench copy for every supported locale', () => {
|
||||
|
||||
@@ -49,6 +49,16 @@ function teamContext(): WorkspaceCollabContext {
|
||||
} as unknown as WorkspaceCollabContext;
|
||||
}
|
||||
|
||||
function freeContext(): WorkspaceCollabContext {
|
||||
return {
|
||||
...teamContext(),
|
||||
workspaceId: 'ws-free',
|
||||
workspaceType: 'personal',
|
||||
billingState: 'free',
|
||||
planId: null,
|
||||
} as unknown as WorkspaceCollabContext;
|
||||
}
|
||||
|
||||
function idleStatus(): OpenDesignHostUpdaterStatusSnapshot {
|
||||
return {
|
||||
arch: 'arm64',
|
||||
@@ -122,6 +132,7 @@ afterEach(() => {
|
||||
resetWorkspaceDirectoryCache();
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
async function renderWithDownloadedUpdate(context: WorkspaceCollabContext | null = teamContext()) {
|
||||
@@ -134,6 +145,26 @@ async function renderWithDownloadedUpdate(context: WorkspaceCollabContext | null
|
||||
}
|
||||
|
||||
describe('updater rocket placement after the account avatar', () => {
|
||||
it('shows the shared Go campaign badge on an unpaid project detail route', () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-08-20T10:00:00.000Z'));
|
||||
|
||||
render(
|
||||
<I18nProvider initial="zh-CN">
|
||||
<WorkspaceTopRightAccountCluster
|
||||
workspaceContextOverride={freeContext()}
|
||||
amrLoggedIn
|
||||
metricsConsent={false}
|
||||
installationId="test-installation"
|
||||
/>
|
||||
</I18nProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('deepseek-campaign-pricing-badge').textContent).toContain(
|
||||
'全新 Go 套餐 · 首月 ¥5 · 模型无限用',
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the project-detail updater slot in the shared account row', () => {
|
||||
render(
|
||||
<I18nProvider initial="zh-CN">
|
||||
|
||||
@@ -25,6 +25,8 @@ export const CAMPAIGN_DISMISSAL_STORAGE: Record<string, string> = {
|
||||
// id, so a new campaign needs its own entry here — otherwise its modal opens
|
||||
// over every spec on the first home render, exactly what this file prevents.
|
||||
'open-design:campaign-seen:deepseek-v4-dual-unlimited-2026': '1',
|
||||
// Live 8/20-9/3 Go plan launch window.
|
||||
'open-design:campaign-seen:go-plan-launch-2026': '1',
|
||||
};
|
||||
|
||||
export async function seedCampaignDismissals(context: BrowserContext): Promise<void> {
|
||||
|
||||
@@ -66,6 +66,20 @@ test('[P2] captures the Go campaign at narrow and short viewport boundaries', as
|
||||
await configureVisualPage(page, { projects: [] });
|
||||
await mockSignedInVelaAccount(page, { plan: 'free' });
|
||||
await gotoVisualHome(page);
|
||||
// Functional specs seed campaign dismissals globally so marketing surfaces
|
||||
// cannot interrupt unrelated flows. This visual contract deliberately opts
|
||||
// back into the Go modal after establishing the page's same-origin storage.
|
||||
await page.evaluate(() => {
|
||||
window.localStorage.removeItem('open-design:campaign-seen:go-plan-launch-2026');
|
||||
});
|
||||
await ensureRailOpen(page);
|
||||
await page.getByTestId('entry-nav-community').evaluate((element: HTMLButtonElement) => {
|
||||
element.click();
|
||||
});
|
||||
await expect(page.getByTestId('entry-view-home')).toHaveAttribute('data-active', 'false');
|
||||
await page.getByTestId('entry-nav-home').evaluate((element: HTMLButtonElement) => {
|
||||
element.click();
|
||||
});
|
||||
|
||||
const dialog = page.getByTestId('deepseek-v4-flash-campaign-dialog');
|
||||
const close = page.getByRole('button', { name: 'Close dialog' });
|
||||
|
||||
Reference in New Issue
Block a user