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('创建失败')
+ })
+})