From 77a130351086cb6d5f9572ce4645e7790c64a46b Mon Sep 17 00:00:00 2001 From: saturn Date: Thu, 2 Apr 2026 21:44:46 +0800 Subject: [PATCH] release v0.4.0 --- package.json | 2 +- .../[locale]/workspace/[projectId]/page.tsx | 4 +-- .../story-input/StoryInputComposer.tsx | 16 +++++----- src/lib/style-presets.ts | 23 ++++++++++----- .../components/story-input-composer.test.ts | 27 +++++++++++++++++ tests/unit/helpers/read-error-message.test.ts | 29 +++++++++++++++++++ 6 files changed, 84 insertions(+), 17 deletions(-) create mode 100644 tests/unit/helpers/read-error-message.test.ts diff --git a/package.json b/package.json index c2d46b0..bff742e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "waoowaoo", - "version": "0.3.0", + "version": "0.4.0", "private": true, "engines": { "node": ">=18.18.0", diff --git a/src/app/[locale]/workspace/[projectId]/page.tsx b/src/app/[locale]/workspace/[projectId]/page.tsx index f8cc28b..9edc4bc 100644 --- a/src/app/[locale]/workspace/[projectId]/page.tsx +++ b/src/app/[locale]/workspace/[projectId]/page.tsx @@ -18,6 +18,7 @@ import { ModelCapabilityDropdown } from '@/components/ui/config-modals/ModelCapa import { AppIcon } from '@/components/ui/icons' import { readConfiguredAnalysisModel, shouldGuideToModelSetup } from '@/lib/workspace/model-setup' import { useRouter } from '@/i18n/navigation' +import { readApiErrorMessage } from '@/lib/api/read-error-message' // 有效的stage值 const VALID_STAGES = ['config', 'script', 'assets', 'text-storyboard', 'storyboard', 'videos', 'voice', 'editor'] as const @@ -209,8 +210,7 @@ export default function ProjectDetailPage() { }) if (!res.ok) { - const data = await res.json() - throw new Error(data.error || t('createFailed')) + throw new Error(await readApiErrorMessage(res, t('createFailed'))) } const data = await res.json() diff --git a/src/components/story-input/StoryInputComposer.tsx b/src/components/story-input/StoryInputComposer.tsx index 1071c6c..dde65d8 100644 --- a/src/components/story-input/StoryInputComposer.tsx +++ b/src/components/story-input/StoryInputComposer.tsx @@ -146,13 +146,15 @@ export default function StoryInputComposer({ options={styleOptions} /> -
- -
+ {stylePresetOptions.length > 0 ? ( +
+ +
+ ) : null}
{secondaryActions} diff --git a/src/lib/style-presets.ts b/src/lib/style-presets.ts index b216bd0..932b8cb 100644 --- a/src/lib/style-presets.ts +++ b/src/lib/style-presets.ts @@ -1,16 +1,25 @@ -export const STYLE_PRESETS = [ +export interface StylePresetOption { + value: string + label: string + description: string + enabled: boolean +} + +const ALL_STYLE_PRESETS: readonly StylePresetOption[] = [ { value: 'horror-suspense', label: '恐怖悬疑', description: '压迫氛围', + enabled: false, }, -] as const +] -export type StylePresetOption = (typeof STYLE_PRESETS)[number] -export type StylePresetValue = StylePresetOption['value'] +export const STYLE_PRESETS: readonly StylePresetOption[] = ALL_STYLE_PRESETS.filter( + (preset) => preset.enabled, +) -export const DEFAULT_STYLE_PRESET_VALUE: StylePresetValue = 'horror-suspense' +export const DEFAULT_STYLE_PRESET_VALUE = STYLE_PRESETS[0]?.value ?? '' -export function getStylePresetOption(value: string): StylePresetOption { - return STYLE_PRESETS.find((preset) => preset.value === value) ?? STYLE_PRESETS[0] +export function getStylePresetOption(value: string): StylePresetOption | null { + return STYLE_PRESETS.find((preset) => preset.value === value) ?? STYLE_PRESETS[0] ?? null } diff --git a/tests/unit/components/story-input-composer.test.ts b/tests/unit/components/story-input-composer.test.ts index 3783609..caf39cd 100644 --- a/tests/unit/components/story-input-composer.test.ts +++ b/tests/unit/components/story-input-composer.test.ts @@ -48,4 +48,31 @@ describe('StoryInputComposer', () => { expect(html).toContain('AI 帮我写') expect(html).toContain('开始创作') }) + + it('hides the style preset selector when no preset is enabled', () => { + Reflect.set(globalThis, 'React', React) + + const html = renderToStaticMarkup( + createElement(StoryInputComposer, { + value: '测试内容', + onValueChange: () => undefined, + placeholder: '请输入内容', + minRows: 8, + videoRatio: '9:16', + onVideoRatioChange: () => undefined, + ratioOptions: [{ value: '9:16', label: '9:16' }], + artStyle: 'realistic', + onArtStyleChange: () => undefined, + styleOptions: [{ value: 'realistic', label: '真人风格' }], + stylePresetValue: '', + onStylePresetChange: () => undefined, + stylePresetOptions: [], + primaryAction: createElement('button', { type: 'button' }, '开始创作'), + }), + ) + + expect(html).toContain('RatioSelector') + expect(html).toContain('StyleSelector') + expect(html).not.toContain('StylePresetSelector') + }) }) diff --git a/tests/unit/helpers/read-error-message.test.ts b/tests/unit/helpers/read-error-message.test.ts new file mode 100644 index 0000000..1f156f9 --- /dev/null +++ b/tests/unit/helpers/read-error-message.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest' +import { readApiErrorMessage } from '@/lib/api/read-error-message' + +function buildJsonResponse(body: unknown, status = 400): Response { + return new Response(JSON.stringify(body), { + status, + headers: { 'Content-Type': 'application/json' }, + }) +} + +describe('readApiErrorMessage', () => { + it('returns nested api error message instead of [object Object]', async () => { + const response = buildJsonResponse({ + error: { + code: 'INVALID_PARAMS', + message: 'Episode name is required', + }, + message: 'Invalid parameters', + }) + + await expect(readApiErrorMessage(response, '创建失败')).resolves.toBe('Episode name is required') + }) + + it('falls back when the response body is not json', async () => { + const response = new Response('bad gateway', { status: 502 }) + + await expect(readApiErrorMessage(response, '创建失败')).resolves.toBe('创建失败') + }) +})