From 4c65f557f53ae50ddabcc34cb8472725bace8745 Mon Sep 17 00:00:00 2001
From: Johnny Zhang
Date: Mon, 24 Aug 2026 15:19:49 +0800
Subject: [PATCH 1/4] =?UTF-8?q?feat(project):=20=E7=94=BB=E9=A3=8E?=
=?UTF-8?q?=E6=94=B9=E4=B8=BA=E5=8F=AF=E9=80=89=E6=8B=A9=E7=9A=84=E5=8F=97?=
=?UTF-8?q?=E6=8E=A7=E6=A1=A3=E4=BD=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Closes #602
---
.../app/src/windup_app/web/api/project.py | 8 ++-
backend/tests/test_art_style.py | 23 ++++++--
frontend/src/entities/index.ts | 5 ++
frontend/src/entities/project/index.test.ts | 6 +--
frontend/src/entities/project/index.ts | 53 +++++++++++++++++--
.../export-package/character-export.test.ts | 2 +-
.../export-package/progressive-export.test.ts | 2 +-
.../features/quick-start-agent/production.ts | 4 +-
.../src/features/quick-start-agent/react.ts | 7 ++-
.../src/features/quick-start-agent/runtime.ts | 9 +++-
.../workflow-controller/controller.test.ts | 4 +-
.../workflow-controller/controller.ts | 15 +++++-
.../src/features/workflow-controller/index.ts | 1 +
.../src/pages/project-create/index.test.tsx | 8 +--
frontend/src/pages/project-create/index.tsx | 36 +++++++------
frontend/src/pages/projects/index.test.tsx | 2 +-
frontend/src/pages/projects/index.tsx | 3 +-
frontend/src/pages/quick-start/index.test.tsx | 3 ++
frontend/src/pages/quick-start/index.tsx | 25 ++++++++-
frontend/src/pages/quick-start/service.ts | 17 ++++--
.../src/pages/workflow-editor/index.test.tsx | 2 +-
frontend/src/pages/workflow-editor/index.tsx | 14 ++++-
.../src/pages/workflow-editor/runtime.test.ts | 2 +-
.../workflow-editor/workflow-editor-view.tsx | 21 +++++++-
frontend/src/test/project-assets-backend.ts | 4 +-
openapi.json | 20 +------
26 files changed, 225 insertions(+), 71 deletions(-)
diff --git a/backend/packages/app/src/windup_app/web/api/project.py b/backend/packages/app/src/windup_app/web/api/project.py
index e7bebbf6b..06276f5ee 100644
--- a/backend/packages/app/src/windup_app/web/api/project.py
+++ b/backend/packages/app/src/windup_app/web/api/project.py
@@ -97,11 +97,17 @@ class ProjectOut(BaseModel):
directional_movement: int
sprite_width: int
sprite_height: int
- game_style: str | None
+ game_style: ArtStyle
sprite_sample_url: str | None
create_at: datetime
update_at: datetime
+ @field_validator("game_style", mode="before")
+ @classmethod
+ def _normalize_style(cls, value: object) -> ArtStyle:
+ """库里既有枚举值也有存量自由文本;不归一的话前端会把「像素风格」显示成不指定。"""
+ return ArtStyle.from_stored(value if isinstance(value, str) else None)
+
class ProjectListOut(ProjectOut):
"""项目列表项;预览是读取投影,不写回 Project。"""
diff --git a/backend/tests/test_art_style.py b/backend/tests/test_art_style.py
index 17c69df9d..ab6e6dc25 100644
--- a/backend/tests/test_art_style.py
+++ b/backend/tests/test_art_style.py
@@ -80,10 +80,27 @@ def test_created_project_reports_the_chosen_style(auth_client):
assert created["game_style"] == "pixel"
-def test_unspecified_is_stored_as_null(auth_client):
- """现有前端把这一列原样显示,写字面量会让「不指定」四个字变成 unspecified。"""
+def test_unspecified_is_stored_as_null(auth_client, db_session):
+ """库里存 NULL 而不是字面量;响应侧再归一成枚举交给前端。"""
+ from windup_app.server.project.model import Project
+
+ created = auth_client.post("/projects", json=_payload()).json()["data"]
+ assert created["game_style"] == "unspecified"
+ assert db_session.get(Project, created["id"]).game_style is None
+
+
+def test_reading_a_legacy_row_reports_the_branch_it_takes(auth_client, db_session):
+ """存量存的是自由文本,不归一的话前端会把「像素风格」显示成不指定。"""
+ from windup_app.server.project.model import Project
+
created = auth_client.post("/projects", json=_payload()).json()["data"]
- assert created["game_style"] is None
+ project = db_session.get(Project, created["id"])
+ project.game_style = "像素风格"
+ db_session.commit()
+
+ body = auth_client.get(f"/projects/{created['id']}").json()
+ assert body["code"] == 200
+ assert body["data"]["game_style"] == "pixel"
# -- 改画风 ------------------------------------------------------------------
diff --git a/frontend/src/entities/index.ts b/frontend/src/entities/index.ts
index beb339bf4..29a5347d0 100644
--- a/frontend/src/entities/index.ts
+++ b/frontend/src/entities/index.ts
@@ -23,13 +23,18 @@ export type {
/* 项目 —— 全局约束:视角、朝向、精灵尺寸、画风 */
export {
+ ART_STYLE,
+ ART_STYLE_HINT,
+ ART_STYLE_OPTIONS,
CHARACTER_PERSPECTIVE,
+ isArtStyle,
DIRECTIONAL_MOVEMENT,
ProjectHasCharactersError,
ProjectNameConflictError,
} from './project'
export { projectApis } from './project'
export type {
+ ArtStyle,
CharacterPerspective,
CreateProjectInput,
DirectionalMovement,
diff --git a/frontend/src/entities/project/index.test.ts b/frontend/src/entities/project/index.test.ts
index 322ef23d8..38a227ab0 100644
--- a/frontend/src/entities/project/index.test.ts
+++ b/frontend/src/entities/project/index.test.ts
@@ -60,7 +60,7 @@ describe('projectApis', () => {
perspective: 'isometric',
directionalMovement: 'four-way',
spriteSize: { width: 64, height: 96 },
- gameStyle: null,
+ gameStyle: 'unspecified',
sampleImageUrl: 'https://cdn.windup.test/style.png',
previewUrl: 'https://cdn.windup.test/project-preview.png',
createdAt: '2026-08-01T08:00:00Z',
@@ -87,7 +87,7 @@ describe('projectApis', () => {
perspective: 'isometric',
directionalMovement: 'four-way',
spriteSize: { width: 64, height: 96 },
- gameStyle: null,
+ gameStyle: 'unspecified',
sampleImageUrl: 'https://cdn.windup.test/style.png',
})
@@ -100,7 +100,7 @@ describe('projectApis', () => {
directional_movement: 2,
sprite_width: 64,
sprite_height: 96,
- game_style: null,
+ game_style: 'unspecified',
sprite_sample_url: 'https://cdn.windup.test/style.png',
})
})
diff --git a/frontend/src/entities/project/index.ts b/frontend/src/entities/project/index.ts
index 0d3c06983..1e668a39b 100644
--- a/frontend/src/entities/project/index.ts
+++ b/frontend/src/entities/project/index.ts
@@ -9,7 +9,7 @@ export interface Project {
perspective: CharacterPerspective
directionalMovement: DirectionalMovement
spriteSize: { width: number; height: number }
- gameStyle: string | null
+ gameStyle: ArtStyle
sampleImageUrl: string | null
/** 项目列表已经解析好的卡片预览;详情响应不承诺提供。 */
previewUrl?: string | null
@@ -27,7 +27,7 @@ export interface CreateProjectInput {
perspective: CharacterPerspective
directionalMovement: DirectionalMovement
spriteSize: { width: number; height: number }
- gameStyle?: string | null
+ gameStyle?: ArtStyle
sampleImageUrl?: string | null
}
@@ -75,12 +75,41 @@ export const DIRECTIONAL_MOVEMENT: Record = {
'eight-way': '八向',
}
+/** 后端 game_style;只有 pixel 会改变出帧管线,其余只进提示词。 */
+export type ArtStyle = 'pixel' | 'cartoon' | 'hand_drawn' | 'realistic' | 'unspecified'
+
+export const ART_STYLE: Record = {
+ pixel: '像素',
+ cartoon: '卡通',
+ hand_drawn: '手绘',
+ realistic: '写实',
+ unspecified: '不指定',
+}
+
+/** 选项旁的一句说明;用户选画风时看不到管线,得把差别讲出来。 */
+export const ART_STYLE_HINT: Record = {
+ pixel: '出帧吸附母版像素网格,颜色吸回母版色板',
+ cartoon: '粗描线、平涂',
+ hand_drawn: '有笔触与纸纹',
+ realistic: '无描线,走渐变与体积光',
+ unspecified: '不给模型画风约束',
+}
+
+export const ART_STYLE_OPTIONS: ArtStyle[] = [
+ 'pixel',
+ 'cartoon',
+ 'hand_drawn',
+ 'realistic',
+ 'unspecified',
+]
+
/** Project 对应的一组后端接口。 */
export interface ProjectApis {
list(query?: ProjectPageQuery): Promise>
get(id: Project['id']): Promise
create(input: CreateProjectInput): Promise
rename(id: Project['id'], name: string): Promise
+ setGameStyle(id: Project['id'], gameStyle: ArtStyle): Promise
remove(id: Project['id']): Promise
}
@@ -99,6 +128,15 @@ interface ProjectDto {
update_at: string
}
+export function isArtStyle(value: unknown): value is ArtStyle {
+ return typeof value === 'string' && value in ART_STYLE
+}
+
+/** 后端已经把存量自由文本归一成枚举;这里只兜住 null 与未知取值。 */
+function artStyleFromDto(value: string | null): ArtStyle {
+ return isArtStyle(value) ? value : 'unspecified'
+}
+
const perspectiveFromDto: Record = {
1: 'side',
2: 'top-down',
@@ -154,7 +192,7 @@ function mapProject(dto: ProjectDto): Project {
'directional_movement',
),
spriteSize: { width: dto.sprite_width, height: dto.sprite_height },
- gameStyle: dto.game_style,
+ gameStyle: artStyleFromDto(dto.game_style),
sampleImageUrl: dto.sprite_sample_url,
previewUrl: dto.preview_url ?? dto.sprite_sample_url,
createdAt: dto.create_at,
@@ -240,6 +278,15 @@ export const projectApis: ProjectApis = {
}
},
+ async setGameStyle(id, gameStyle) {
+ return mapProject(
+ await getApiClient().request(`/projects/${encodeURIComponent(id)}`, {
+ method: 'PATCH',
+ json: { game_style: gameStyle },
+ }),
+ )
+ },
+
async remove(id) {
try {
await getApiClient().request(`/projects/${encodeURIComponent(id)}`, {
diff --git a/frontend/src/features/export-package/character-export.test.ts b/frontend/src/features/export-package/character-export.test.ts
index 80401c7b0..04492fe9d 100644
--- a/frontend/src/features/export-package/character-export.test.ts
+++ b/frontend/src/features/export-package/character-export.test.ts
@@ -11,7 +11,7 @@ const project: Project = {
perspective: 'side',
directionalMovement: 'single',
spriteSize: { width: 64, height: 80 },
- gameStyle: null,
+ gameStyle: 'unspecified',
sampleImageUrl: null,
createdAt: '2026-08-01T08:00:00Z',
updatedAt: '2026-08-01T08:00:00Z',
diff --git a/frontend/src/features/export-package/progressive-export.test.ts b/frontend/src/features/export-package/progressive-export.test.ts
index 2412ebd33..2cc6ed868 100644
--- a/frontend/src/features/export-package/progressive-export.test.ts
+++ b/frontend/src/features/export-package/progressive-export.test.ts
@@ -11,7 +11,7 @@ const project: Project = {
perspective: 'side',
directionalMovement: 'single',
spriteSize: { width: 32, height: 40 },
- gameStyle: null,
+ gameStyle: 'unspecified',
sampleImageUrl: null,
createdAt: '2026-08-01T08:00:00Z',
updatedAt: '2026-08-01T08:00:00Z',
diff --git a/frontend/src/features/quick-start-agent/production.ts b/frontend/src/features/quick-start-agent/production.ts
index ddd60a981..2da19d22b 100644
--- a/frontend/src/features/quick-start-agent/production.ts
+++ b/frontend/src/features/quick-start-agent/production.ts
@@ -1,4 +1,5 @@
import {
+ isArtStyle,
createAuthenticatedGenerationApis,
projectApis,
workflowRunApis,
@@ -133,6 +134,7 @@ export function createProductionQuickStartAgentDependencies(
return planner(input)
},
async startCharacterGeneration(input) {
+ const gameStyle = isArtStyle(input.gameStyle) ? input.gameStyle : undefined
generationApis ??= createAuthenticatedGenerationApis()
const controller = createController({
workflowRunApis: runApis,
@@ -141,7 +143,7 @@ export function createProductionQuickStartAgentDependencies(
onAsyncError: reportError,
})
try {
- return await controller.startCharacterGeneration(input)
+ return await controller.startCharacterGeneration({ prompt: input.prompt, gameStyle })
} finally {
controller.dispose()
}
diff --git a/frontend/src/features/quick-start-agent/react.ts b/frontend/src/features/quick-start-agent/react.ts
index f6651759b..e75342f6a 100644
--- a/frontend/src/features/quick-start-agent/react.ts
+++ b/frontend/src/features/quick-start-agent/react.ts
@@ -102,7 +102,10 @@ export function useQuickStartAgent(options: UseQuickStartAgentOptions) {
])
const submit = useCallback(
- async (input: string): Promise => {
+ async (
+ input: string,
+ { gameStyle }: { gameStyle?: string } = {},
+ ): Promise => {
if (running.current) throw new Error('Planner 正在处理上一条输入')
running.current = true
const controller = new AbortController()
@@ -113,7 +116,7 @@ export function useQuickStartAgent(options: UseQuickStartAgentOptions) {
const activeAgent = ensureAgent()
const runTurn = started.current ? activeAgent.continue : activeAgent.start
started.current = true
- const result = await runTurn(input, { signal: controller.signal })
+ const result = await runTurn(input, { signal: controller.signal, gameStyle })
if (mounted.current) {
if (result.kind === 'message') {
setState({
diff --git a/frontend/src/features/quick-start-agent/runtime.ts b/frontend/src/features/quick-start-agent/runtime.ts
index ec6eab9c4..941171c16 100644
--- a/frontend/src/features/quick-start-agent/runtime.ts
+++ b/frontend/src/features/quick-start-agent/runtime.ts
@@ -65,6 +65,8 @@ export type QuickStartAgentResult =
export interface QuickStartAgentTurnOptions {
signal?: AbortSignal
+ /** 入口处选的画风,原样透传给宿主;取值范围由宿主定义,本模块不认识业务对象。 */
+ gameStyle?: string
}
export interface QuickStartAgent {
@@ -82,6 +84,7 @@ export interface QuickStartAgent {
export type StartCharacterGenerationAction = (input: {
prompt: string
directionalMovement?: QuickStartDirectionalMovement
+ gameStyle?: string
}) => Promise<{ runId: string }>
export interface CreateQuickStartAgentOptions {
@@ -223,6 +226,8 @@ export function createQuickStartAgent({
let running = false
let messages: PlannerMessage[] = [...initialMessages]
let currentProposal = initialProposal
+ /** 画风在入口那一轮就选定了,而真正下单在用户确认提案之后,中间要存住。 */
+ let pendingGameStyle: string | undefined
function assertAuthorized() {
if (revoked) throw new Error('生成授权已失效')
@@ -231,9 +236,10 @@ export function createQuickStartAgent({
async function runTurn(
input: string,
- { signal }: QuickStartAgentTurnOptions = {},
+ { signal, gameStyle }: QuickStartAgentTurnOptions = {},
): Promise {
assertAuthorized()
+ if (gameStyle !== undefined) pendingGameStyle = gameStyle
if (running) throw new Error('Planner 正在处理上一条输入')
const normalizedInput = input.trim()
if (!normalizedInput) throw new Error('请先描述想要创建的角色')
@@ -302,6 +308,7 @@ export function createQuickStartAgent({
const { runId } = await startCharacterGeneration({
prompt: effectivePrompt,
directionalMovement,
+ gameStyle: pendingGameStyle,
})
return { kind: 'generated', runId, ...proposal, optimizedPrompt: effectivePrompt }
} finally {
diff --git a/frontend/src/features/workflow-controller/controller.test.ts b/frontend/src/features/workflow-controller/controller.test.ts
index 6703e1f10..ad3c03574 100644
--- a/frontend/src/features/workflow-controller/controller.test.ts
+++ b/frontend/src/features/workflow-controller/controller.test.ts
@@ -280,7 +280,9 @@ describe('WorkflowController', () => {
controller.startCharacterGeneration({ prompt: ' 银发像素骑士 ' }),
).resolves.toEqual({ runId: 'run-1' })
- expect(prepareProject).toHaveBeenCalledWith('银发像素骑士', 'single')
+ expect(prepareProject).toHaveBeenCalledWith('银发像素骑士', 'single', {
+ gameStyle: undefined,
+ })
expect(workflow.apis.create).toHaveBeenCalledWith({
projectId: 'project-agent',
nodes: [
diff --git a/frontend/src/features/workflow-controller/controller.ts b/frontend/src/features/workflow-controller/controller.ts
index 411468503..041c27af4 100644
--- a/frontend/src/features/workflow-controller/controller.ts
+++ b/frontend/src/features/workflow-controller/controller.ts
@@ -29,6 +29,7 @@ import type {
DirectionalMovement,
} from '@/entities'
import {
+ type ArtStyle,
getDirectionProfile,
isImageCandidateCount,
ProjectNameConflictError,
@@ -101,14 +102,20 @@ export interface ApplyGenerationResultInput {
generation: Generation
}
+export interface PrepareQuickStartProjectOptions {
+ gameStyle?: ArtStyle
+}
+
export type PrepareQuickStartProject = (
prompt: string,
directionalMovement?: DirectionalMovement,
+ options?: PrepareQuickStartProjectOptions,
) => Promise & Partial>>
export interface StartCharacterGenerationInput {
prompt: string
directionalMovement?: DirectionalMovement
+ gameStyle?: ArtStyle
}
export interface StartCharacterGenerationResult {
@@ -245,7 +252,7 @@ function boundedProjectName(value: string, maxLength: number): string {
export function createAutoPrepareProject(
projectApis: Pick,
): PrepareQuickStartProject {
- return async (prompt, directionalMovement = 'single') => {
+ return async (prompt, directionalMovement = 'single', options) => {
const normalizedPrompt = prompt.trim().replace(/\s+/gu, ' ') || '未命名项目'
let lastConflict: unknown
@@ -260,6 +267,7 @@ export function createAutoPrepareProject(
perspective: 'side',
directionalMovement,
spriteSize: { width: 256, height: 256 },
+ gameStyle: options?.gameStyle,
})
return {
id: project.id,
@@ -390,13 +398,16 @@ export function createWorkflowController({
async function startCharacterGeneration({
prompt,
directionalMovement: selectedDirectionalMovement = 'single',
+ gameStyle,
}: StartCharacterGenerationInput): Promise {
ensureRunning()
if (!prepareProject) {
throw new Error('WorkflowController 未配置 Quick Start 项目准备能力')
}
const normalizedPrompt = nonEmpty(prompt, '角色描述')
- const project = await prepareProject(normalizedPrompt, selectedDirectionalMovement)
+ const project = await prepareProject(normalizedPrompt, selectedDirectionalMovement, {
+ gameStyle,
+ })
generationDirections = getDirectionProfile(
project.directionalMovement ?? selectedDirectionalMovement,
).generationDirections
diff --git a/frontend/src/features/workflow-controller/index.ts b/frontend/src/features/workflow-controller/index.ts
index a2f1f5d08..801b0e056 100644
--- a/frontend/src/features/workflow-controller/index.ts
+++ b/frontend/src/features/workflow-controller/index.ts
@@ -6,6 +6,7 @@ export type {
GenerateActionOptions,
GenerateCharacterTemplateOptions,
PrepareQuickStartProject,
+ PrepareQuickStartProjectOptions,
StartCharacterGenerationInput,
StartCharacterGenerationResult,
WorkflowController,
diff --git a/frontend/src/pages/project-create/index.test.tsx b/frontend/src/pages/project-create/index.test.tsx
index 694932d66..8b4aafa93 100644
--- a/frontend/src/pages/project-create/index.test.tsx
+++ b/frontend/src/pages/project-create/index.test.tsx
@@ -173,7 +173,7 @@ describe('ProjectCreatePage', () => {
fireEvent.change(screen.getByLabelText('游戏视角'), { target: { value: 'top-down' } })
fireEvent.change(screen.getByLabelText('朝向'), { target: { value: 'eight-way' } })
fireEvent.click(screen.getByRole('button', { name: '512 × 512' }))
- fireEvent.change(screen.getByLabelText('画风约束'), { target: { value: '低饱和像素绘本' } })
+ fireEvent.change(screen.getByLabelText('画风'), { target: { value: 'pixel' } })
fireEvent.click(screen.getByRole('button', { name: '创建项目' }))
expect(await screen.findByRole('heading', { name: '雾港来信' })).toBeTruthy()
@@ -186,7 +186,7 @@ describe('ProjectCreatePage', () => {
directional_movement: 3,
sprite_width: 512,
sprite_height: 512,
- game_style: '低饱和像素绘本',
+ game_style: 'pixel',
})
// 归属由后端从 JWT 取;请求体再带 user_id 就等于宣称可以替别人建项目。
expect(Object.hasOwn(body, 'user_id')).toBe(false)
@@ -217,11 +217,11 @@ describe('ProjectCreatePage', () => {
await renderProjectCreate()
fireEvent.change(screen.getByLabelText('项目名称'), { target: { value: '点灯人 · MVP' } })
- fireEvent.change(screen.getByLabelText('画风约束'), { target: { value: '低饱和像素绘本' } })
+ fireEvent.change(screen.getByLabelText('画风'), { target: { value: 'pixel' } })
fireEvent.click(screen.getByRole('button', { name: '创建项目' }))
expect((await screen.findByRole('alert')).textContent).toBe('项目名称已存在')
- expect((screen.getByLabelText('画风约束') as HTMLTextAreaElement).value).toBe('低饱和像素绘本')
+ expect((screen.getByLabelText('画风') as HTMLSelectElement).value).toBe('pixel')
})
it('连续点击创建只发出一次请求', async () => {
diff --git a/frontend/src/pages/project-create/index.tsx b/frontend/src/pages/project-create/index.tsx
index cebb692c1..d8057603c 100644
--- a/frontend/src/pages/project-create/index.tsx
+++ b/frontend/src/pages/project-create/index.tsx
@@ -2,10 +2,14 @@ import { useRef, useState, type FormEvent } from 'react'
import { useNavigate, useSearchParams } from 'react-router'
import {
+ ART_STYLE,
+ ART_STYLE_HINT,
+ ART_STYLE_OPTIONS,
CHARACTER_PERSPECTIVE,
DIRECTIONAL_MOVEMENT,
projectApis,
workflowRunApis,
+ type ArtStyle,
type CharacterPerspective,
type DirectionalMovement,
type Project,
@@ -31,12 +35,6 @@ const SPRITE_MAX = 2048
/** 常用档位只是快捷填充,用户仍可以填这三档之外的任意合法宽高。 */
const SPRITE_PRESETS = [128, 256, 512]
-/**
- * 画风约束的长度上限只存在于前端:后端 `game_style` 是没有长度约束的 Text。
- * 定这个数是为了让它维持在「一句风格描述」的量级,不是契约要求。
- */
-const GAME_STYLE_MAX_LENGTH = 240
-
/** 创建真实项目;首页画布入口与项目中心的新建按钮共用这一页。 */
export function ProjectCreatePage() {
const navigate = useNavigate()
@@ -54,7 +52,7 @@ export function ProjectCreatePage() {
const [directionalMovement, setDirectionalMovement] = useState('single')
const [spriteWidth, setSpriteWidth] = useState('256')
const [spriteHeight, setSpriteHeight] = useState('256')
- const [gameStyle, setGameStyle] = useState('')
+ const [gameStyle, setGameStyle] = useState('unspecified')
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState(null)
const [createdProject, setCreatedProject] = useState(null)
@@ -88,7 +86,7 @@ export function ProjectCreatePage() {
perspective,
directionalMovement,
spriteSize: { width, height },
- gameStyle: gameStyle.trim() || null,
+ gameStyle,
})
if (opensWorkflowEditor) setCreatedProject(project)
}
@@ -264,18 +262,24 @@ export function ProjectCreatePage() {
-
{error ? (
diff --git a/frontend/src/pages/projects/index.test.tsx b/frontend/src/pages/projects/index.test.tsx
index a9b3633d7..fd1ce1125 100644
--- a/frontend/src/pages/projects/index.test.tsx
+++ b/frontend/src/pages/projects/index.test.tsx
@@ -183,7 +183,7 @@ describe('ProjectsPage', () => {
expect(previewProject.textContent).toContain('08/04')
expect(previewProject.textContent).toContain('横版视角 · 四向')
expect(previewProject.textContent).toContain('64 × 64 px')
- expect(previewProject.textContent).toContain('低饱和像素绘本')
+ expect(previewProject.textContent).toContain('像素')
expect(screen.queryByText('项目名称')).toBeNull()
expect(screen.queryByRole('link', { name: /查看角色/ })).toBeNull()
const gallery = screen.getByRole('heading', { name: '最近项目 · 02' }).closest('section')
diff --git a/frontend/src/pages/projects/index.tsx b/frontend/src/pages/projects/index.tsx
index b97e43112..b3053bbb6 100644
--- a/frontend/src/pages/projects/index.tsx
+++ b/frontend/src/pages/projects/index.tsx
@@ -4,6 +4,7 @@ import { Link } from 'react-router'
import assetLibraryArtwork from '@/assets/workspace/asset-library.png'
import {
+ ART_STYLE,
CHARACTER_PERSPECTIVE,
DIRECTIONAL_MOVEMENT,
projectApis,
@@ -313,7 +314,7 @@ function ProjectGalleryTile({
{project.spriteSize.height} px
- {project.gameStyle || '未设置游戏风格'}
+ {ART_STYLE[project.gameStyle]}
diff --git a/frontend/src/pages/quick-start/index.test.tsx b/frontend/src/pages/quick-start/index.test.tsx
index c5057b73f..39e1f0db5 100644
--- a/frontend/src/pages/quick-start/index.test.tsx
+++ b/frontend/src/pages/quick-start/index.test.tsx
@@ -966,6 +966,7 @@ describe('QuickStartPage', () => {
expect(startCharacterGeneration).toHaveBeenCalledWith({
prompt: '云端工坊的银发机械师,佩戴黄铜护目镜',
directionalMovement: 'single',
+ gameStyle: 'unspecified',
})
})
@@ -1596,6 +1597,7 @@ describe('QuickStartPage', () => {
expect(startCharacterGeneration).toHaveBeenCalledWith({
prompt: '16-bit 日式 RPG 像素风,清晰轮廓,明亮配色',
directionalMovement: 'four-way',
+ gameStyle: 'unspecified',
}),
)
expect(service.start).not.toHaveBeenCalled()
@@ -1615,6 +1617,7 @@ describe('QuickStartPage', () => {
'挥手',
expect.any(AbortSignal),
'eight-way',
+ { gameStyle: 'unspecified' },
),
)
})
diff --git a/frontend/src/pages/quick-start/index.tsx b/frontend/src/pages/quick-start/index.tsx
index d3f771c54..0a5611f36 100644
--- a/frontend/src/pages/quick-start/index.tsx
+++ b/frontend/src/pages/quick-start/index.tsx
@@ -25,8 +25,11 @@ import Markdown from 'markdown-to-jsx'
import { Link, useLocation, useNavigate, useParams, useSearchParams } from 'react-router'
import {
+ ART_STYLE,
+ ART_STYLE_OPTIONS,
DIRECTIONAL_MOVEMENT,
type ActionFirstFrameWorkflowNode,
+ type ArtStyle,
type CharacterTemplateWorkflowNode,
type DirectionalMovement,
type WorkflowRun,
@@ -524,6 +527,7 @@ function QuickStartInput({
const [prompt, setPrompt] = useState('')
const [directionalMovement, setDirectionalMovement] = useState('single')
const [templateFile, setTemplateFile] = useState(null)
+ const [gameStyle, setGameStyle] = useState('unspecified')
const [submitting, setSubmitting] = useState(false)
const [revealingFirstAgentTurn, setRevealingFirstAgentTurn] = useState(false)
const [entryTransition, setEntryTransition] = useState<'idle' | 'leaving'>('idle')
@@ -765,7 +769,7 @@ function QuickStartInput({
else await revealFirstAgentTurn(userTurn)
setPrompt('')
try {
- const result = await agentSession.submit(normalizedPrompt)
+ const result = await agentSession.submit(normalizedPrompt, { gameStyle })
setRevealingFirstAgentTurn(false)
if (result.kind === 'message') {
appendConversationTurn({
@@ -807,6 +811,7 @@ function QuickStartInput({
normalizedPrompt,
abortController.signal,
directionalMovement,
+ { gameStyle },
)
const handoffPromise = new Promise((resolve) => {
handoffTimer.current = setTimeout(() => {
@@ -1087,6 +1092,24 @@ function QuickStartInput({
添加母版
) : null}
+ {!hasConversation ? (
+
+ ) : null}