Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions src/components/beatcanvas/beatcanvas-front-layer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import {
truncatePromptToMaxChars,
} from '@/core/effects/validation';
import { VIDEO_ANALYSIS_MODEL_ID } from '@/core/effects/video-analysis';
import { findWorkspaceModelOption } from '@/core/effects/workspace-models';
import {
findWorkspaceModelOption,
getWorkspaceAspectRatioOptions,
} from '@/core/effects/workspace-models';
import { cn } from '@/lib/utils';
import { StudioStartHere } from '@/components/studio/studio-start-here';
import {
Expand Down Expand Up @@ -356,7 +359,18 @@ export function BeatCanvasFrontLayer() {
? (selectedModel?.supportedDurations ?? [])
: [];
const selectedLanguageOptions = selectedModel?.supportedLanguages ?? [];
const selectedAspectRatioOptions = selectedModel?.supportedAspectRatios ?? [];
const hasImageReferences =
activeDraftCard?.referenceCardIds.some(
(cardId) => cards[cardId]?.type === 'image'
) ?? false;
const selectedAspectRatioOptions = useMemo(
() =>
getWorkspaceAspectRatioOptions({
model: selectedModel,
hasImageReferences,
}),
[hasImageReferences, selectedModel]
);
const selectedOutputQualities = selectedModel?.supportedOutputQualities ?? [];
const selectedCharacterOrientationOptions =
selectedModel?.characterOrientationOptions ?? [];
Expand All @@ -371,6 +385,17 @@ export function BeatCanvasFrontLayer() {
return;
}

if (
selectedAspectRatioOptions.length > 0 &&
!selectedAspectRatioOptions.includes(activeDraftCard.aspectRatio)
) {
onDraftAspectRatioChange(
activeDraftCard.id,
selectedModel.defaultAspectRatio ?? selectedAspectRatioOptions[0]
);
return;
}

const nextAspectRatio = resolveReferenceDrivenDraftAspectRatio({
draftCard: activeDraftCard,
cards,
Expand All @@ -392,6 +417,7 @@ export function BeatCanvasFrontLayer() {
editor,
isDraftBusy,
onDraftAspectRatioChange,
selectedAspectRatioOptions,
selectedModel,
]);

Expand Down
16 changes: 16 additions & 0 deletions src/components/pricing/pricing-catalog.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
{
"groups": [
{
"model": "Grok Imagine Image 2.0",
"category": "image",
"specs": [
{ "id": "grok-imagine-image-2.0-default", "spec": "1 image · generation or edit", "specZh": "1 张图片 · 生成或编辑", "beatapi": 0.03, "competitor": 0.04 }
]
},
{
"model": "Grok Imagine Video 1.5",
"category": "video",
"specs": [
{ "id": "grok-imagine-video-1.5-480p", "spec": "8 sec example · 480p", "specZh": "8 秒示例 · 480p", "beatapi": 0.2, "competitor": 0.64, "billingUnit": "second", "beatapiUnitPrice": 0.025, "competitorUnitPrice": 0.08, "exampleQuantity": 8 },
{ "id": "grok-imagine-video-1.5-720p", "spec": "8 sec example · 720p", "specZh": "8 秒示例 · 720p", "beatapi": 0.4, "competitor": 1.12, "billingUnit": "second", "beatapiUnitPrice": 0.05, "competitorUnitPrice": 0.14, "exampleQuantity": 8 },
{ "id": "grok-imagine-video-1.5-1080p", "spec": "8 sec example · 1080p · max 1 image", "specZh": "8 秒示例 · 1080p · 最多 1 张图", "beatapi": 0.72, "competitor": 2, "billingUnit": "second", "beatapiUnitPrice": 0.09, "competitorUnitPrice": 0.25, "exampleQuantity": 8 }
]
},
{
"model": "MiniMax H3",
"category": "video",
Expand Down
37 changes: 33 additions & 4 deletions src/components/pricing/pricing-comparison-data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,45 @@ test('workflow and realtime exclusive cards stay off the public page for now', (
test('catalog keeps one card per model and groups the Seedance family together', () => {
const names = comparisonGroups.map((group) => group.model);
assert.deepEqual(
names.slice(1, 5),
names.slice(3, 7),
['Seedance 2.0', 'Seedance 2.0 Mini', 'Seedance 2.0 Fast', 'Seedance 2.5']
);
assert.equal(new Set(names).size, names.length);
});

test('pricing filters count selectable model families rather than variant cards', () => {
assert.equal(countPricingModelFamilies(), 11);
assert.equal(countPricingModelFamilies('video'), 7);
assert.equal(countPricingModelFamilies('image'), 4);
assert.equal(countPricingModelFamilies(), 13);
assert.equal(countPricingModelFamilies('video'), 8);
assert.equal(countPricingModelFamilies('image'), 5);
});

test('Grok pricing matches the official BeatAPI list prices', () => {
const image = comparisonGroups.find(
(group) => group.model === 'Grok Imagine Image 2.0'
);
const video = comparisonGroups.find(
(group) => group.model === 'Grok Imagine Video 1.5'
);

assert.equal(image?.specs[0]?.beatapi, 0.03);
assert.equal(image?.specs[0]?.competitor, 0.04);
const video480 = video?.specs.find((spec) => spec.id.endsWith('480p'));
const video720 = video?.specs.find((spec) => spec.id.endsWith('720p'));
const video1080 = video?.specs.find((spec) => spec.id.endsWith('1080p'));
assert.equal(video480?.beatapiUnitPrice, 0.025);
assert.equal(video480?.exampleQuantity, 8);
assert.equal(video480?.beatapi, 0.025 * 8);
assert.match(video480?.spec ?? '', /8 sec example/);
assert.equal(video720?.beatapiUnitPrice, 0.05);
assert.equal(video720?.exampleQuantity, 8);
assert.equal(video720?.beatapi, 0.05 * 8);
assert.match(video720?.specZh ?? '', /8 秒示例/);
assert.equal(video1080?.beatapiUnitPrice, 0.09);
assert.equal(video1080?.competitorUnitPrice, 0.25);
assert.equal(video1080?.exampleQuantity, 8);
assert.equal(video1080?.beatapi, 0.09 * 8);
assert.equal(video1080?.competitor, 0.25 * 8);
assert.match(video1080?.specZh ?? '', /最多 1 张图/);
});

test('Seedance 2.0 uses the official BeatAPI list prices', () => {
Expand Down
4 changes: 4 additions & 0 deletions src/components/pricing/pricing-comparison-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export type ComparisonSpec = {
beatapi: number;
competitor?: number;
higgsfieldCredits?: number;
billingUnit?: 'second';
beatapiUnitPrice?: number;
competitorUnitPrice?: number;
exampleQuantity?: number;
};

export type ComparisonGroup = {
Expand Down
31 changes: 27 additions & 4 deletions src/components/pricing/pricing-comparison-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,28 @@ export function PricingModelCard({
{locale === 'zh' ? spec.specZh : spec.spec}
</td>
<td className="bg-[#ff7a33]/[0.05] px-5 py-3.5 text-center text-[14px] font-semibold tabular-nums text-[#f6f6f4]">
{formatPrice(spec.beatapi)}
<div>{formatPrice(spec.beatapi)}</div>
{spec.billingUnit === 'second' &&
spec.beatapiUnitPrice !== undefined ? (
<div className="mt-0.5 text-[10px] font-medium text-white/35">
{formatPrice(spec.beatapiUnitPrice)}
/s
</div>
) : null}
</td>
<td className="px-5 py-3.5 text-center text-[14px] tabular-nums text-white/50">
{spec.competitor !== undefined
? formatCompetitorPrice(spec.competitor)
: '—'}
<div>
{spec.competitor !== undefined
? formatCompetitorPrice(spec.competitor)
: '—'}
</div>
{spec.billingUnit === 'second' &&
spec.competitorUnitPrice !== undefined ? (
<div className="mt-0.5 text-[10px] text-white/30">
{formatCompetitorPrice(spec.competitorUnitPrice)}
/s
</div>
) : null}
</td>
<td className="px-5 py-3.5 text-center text-[14px]">
<HiggsfieldCell spec={spec} />
Expand Down Expand Up @@ -153,6 +169,13 @@ export function PricingModelCard({
<p className="text-[15px] font-semibold tabular-nums text-[#f6f6f4]">
{formatPrice(spec.beatapi)}
</p>
{spec.billingUnit === 'second' &&
spec.beatapiUnitPrice !== undefined ? (
<p className="mt-0.5 text-[11px] tabular-nums text-white/35">
{formatPrice(spec.beatapiUnitPrice)}
/s
</p>
) : null}
<p className="mt-0.5 text-[11px] tabular-nums text-white/35">
{spec.competitor !== undefined
? formatCompetitorPrice(spec.competitor)
Expand Down
33 changes: 31 additions & 2 deletions src/components/studio/studio-composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
Sparkles,
Video,
} from 'lucide-react';
import { useRef, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';

import {
composerCardClassName,
Expand All @@ -28,6 +28,7 @@ import {
} from '@/core/effects/validation';
import {
findWorkspaceModelOption,
getWorkspaceAspectRatioOptions,
type WorkspaceModelOption,
} from '@/core/effects/workspace-models';
import { getModelIconPathByModelId } from '@/core/workspace-lib/model-icons';
Expand Down Expand Up @@ -91,7 +92,14 @@ export function StudioComposer({
const selectedDurationOptions =
media === 'video' ? (selectedModel?.supportedDurations ?? []) : [];
const selectedLanguageOptions = selectedModel?.supportedLanguages ?? [];
const selectedAspectRatioOptions = selectedModel?.supportedAspectRatios ?? [];
const selectedAspectRatioOptions = useMemo(
() =>
getWorkspaceAspectRatioOptions({
model: selectedModel,
hasImageReferences: referenceUrls.length > 0,
}),
[referenceUrls.length, selectedModel]
);
const selectedOutputQualities = selectedModel?.supportedOutputQualities ?? [];
const selectedCharacterOrientationOptions =
selectedModel?.characterOrientationOptions ?? [];
Expand Down Expand Up @@ -148,6 +156,27 @@ export function StudioComposer({
),
}));

useEffect(() => {
if (
isAnalysis ||
selectedAspectRatioOptions.length === 0 ||
selectedAspectRatioOptions.includes(draft.aspectRatio)
) {
return;
}
onDraftChange({
...draft,
aspectRatio:
selectedModel?.defaultAspectRatio ?? selectedAspectRatioOptions[0],
});
}, [
draft,
isAnalysis,
onDraftChange,
selectedAspectRatioOptions,
selectedModel?.defaultAspectRatio,
]);

return (
<div className="absolute inset-x-0 bottom-0 z-20 bg-gradient-to-t from-[#09090a] via-[#09090a]/96 to-transparent px-3 pb-3 pt-12 sm:px-6 sm:pb-5 sm:pt-14">
<div
Expand Down
Loading
Loading