release v0.4.0
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "waoowaoo",
|
||||
"version": "0.3.0",
|
||||
"version": "0.4.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=18.18.0",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -146,13 +146,15 @@ export default function StoryInputComposer({
|
||||
options={styleOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-[152px] flex-shrink-0">
|
||||
<StylePresetSelector
|
||||
value={stylePresetValue}
|
||||
onChange={onStylePresetChange}
|
||||
options={stylePresetOptions}
|
||||
/>
|
||||
</div>
|
||||
{stylePresetOptions.length > 0 ? (
|
||||
<div className="w-[152px] flex-shrink-0">
|
||||
<StylePresetSelector
|
||||
value={stylePresetValue}
|
||||
onChange={onStylePresetChange}
|
||||
options={stylePresetOptions}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="ml-auto flex min-w-max items-center gap-2">
|
||||
{secondaryActions}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
29
tests/unit/helpers/read-error-message.test.ts
Normal file
29
tests/unit/helpers/read-error-message.test.ts
Normal file
@@ -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('创建失败')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user