2026-05-24 14:18:39 +08:00
|
|
|
import type { AssetType } from '../api/types'
|
|
|
|
|
import { useProjectStore } from '../stores/project'
|
|
|
|
|
import { useTaskStore } from '../stores/task'
|
|
|
|
|
import { mergeStyles } from '../utils/style'
|
|
|
|
|
import StyleSelector from './StyleSelector'
|
|
|
|
|
import PromptEditor from './PromptEditor'
|
|
|
|
|
|
|
|
|
|
const ASSET_TYPES: { value: AssetType; label: string }[] = [
|
|
|
|
|
{ value: 'sprite', label: '精灵' },
|
|
|
|
|
{ value: 'background', label: '背景' },
|
|
|
|
|
{ value: 'ui', label: 'UI' },
|
|
|
|
|
{ value: 'animation', label: '动画' },
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
interface GenerateFormProps {
|
2026-05-24 21:16:15 +08:00
|
|
|
onSubmit: (prompt: string) => void
|
2026-05-24 14:18:39 +08:00
|
|
|
submitting: boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function GenerateForm({ onSubmit, submitting }: GenerateFormProps) {
|
|
|
|
|
const { style: projectStyle } = useProjectStore()
|
|
|
|
|
const {
|
|
|
|
|
prompt,
|
|
|
|
|
assetType,
|
|
|
|
|
taskStyle,
|
|
|
|
|
params,
|
2026-05-24 21:16:15 +08:00
|
|
|
enableAI,
|
|
|
|
|
optimizedPrompt,
|
|
|
|
|
isOptimizing,
|
2026-05-24 14:18:39 +08:00
|
|
|
setPrompt,
|
|
|
|
|
setAssetType,
|
|
|
|
|
toggleTaskStyle,
|
|
|
|
|
setParams,
|
2026-05-24 21:16:15 +08:00
|
|
|
setEnableAI,
|
|
|
|
|
setOptimizedPrompt,
|
|
|
|
|
runOptimize,
|
2026-05-24 14:18:39 +08:00
|
|
|
} = useTaskStore()
|
|
|
|
|
|
|
|
|
|
const mergedStyle = mergeStyles(projectStyle, taskStyle)
|
2026-05-24 21:16:15 +08:00
|
|
|
const finalPrompt = enableAI && optimizedPrompt ? optimizedPrompt : prompt
|
|
|
|
|
|
|
|
|
|
const handleOptimize = async () => {
|
|
|
|
|
try {
|
|
|
|
|
await runOptimize(projectStyle)
|
|
|
|
|
} catch {
|
|
|
|
|
// error handled in store
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleSubmit = () => {
|
|
|
|
|
onSubmit(finalPrompt)
|
|
|
|
|
}
|
2026-05-24 14:18:39 +08:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
|
|
|
|
{/* 素材类型 */}
|
|
|
|
|
<div>
|
|
|
|
|
<h3 style={{ marginBottom: 12, fontSize: 14, color: 'var(--text-secondary)' }}>
|
|
|
|
|
素材类型
|
|
|
|
|
</h3>
|
|
|
|
|
<div style={{ display: 'flex', gap: 8 }}>
|
|
|
|
|
{ASSET_TYPES.map(t => (
|
|
|
|
|
<button
|
|
|
|
|
key={t.value}
|
|
|
|
|
type="button"
|
|
|
|
|
className={assetType === t.value ? 'btn-primary' : 'btn-secondary'}
|
|
|
|
|
onClick={() => setAssetType(t.value)}
|
|
|
|
|
style={{ padding: '8px 20px' }}
|
|
|
|
|
>
|
|
|
|
|
{t.label}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 任务风格覆盖 */}
|
|
|
|
|
<div>
|
|
|
|
|
<h3 style={{ marginBottom: 12, fontSize: 14, color: 'var(--text-secondary)' }}>
|
|
|
|
|
风格覆盖(可选)
|
|
|
|
|
</h3>
|
|
|
|
|
<StyleSelector value={taskStyle} onChange={toggleTaskStyle} compact />
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-05-24 21:16:15 +08:00
|
|
|
{/* 提示词(含 AI 优化) */}
|
2026-05-24 14:18:39 +08:00
|
|
|
<div>
|
2026-05-24 21:16:15 +08:00
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
marginBottom: 12,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<h3 style={{ fontSize: 14, color: 'var(--text-secondary)' }}>
|
|
|
|
|
提示词
|
|
|
|
|
</h3>
|
|
|
|
|
{enableAI && (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="btn-secondary"
|
|
|
|
|
onClick={handleOptimize}
|
|
|
|
|
disabled={isOptimizing || !prompt.trim()}
|
|
|
|
|
style={{ padding: '6px 16px', fontSize: 13 }}
|
|
|
|
|
>
|
|
|
|
|
{isOptimizing ? '优化中...' : '优化提示词'}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-05-24 14:18:39 +08:00
|
|
|
<PromptEditor
|
|
|
|
|
prompt={prompt}
|
|
|
|
|
onPromptChange={setPrompt}
|
|
|
|
|
style={mergedStyle}
|
|
|
|
|
params={{ resolution: params.resolution, format: params.format }}
|
2026-05-24 21:16:15 +08:00
|
|
|
enableAI={enableAI}
|
|
|
|
|
onEnableAIToggle={setEnableAI}
|
|
|
|
|
optimizedPrompt={optimizedPrompt}
|
|
|
|
|
isOptimizing={isOptimizing}
|
|
|
|
|
onOptimizedPromptChange={setOptimizedPrompt}
|
2026-05-24 14:18:39 +08:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 参数 */}
|
|
|
|
|
<div>
|
|
|
|
|
<h3 style={{ marginBottom: 12, fontSize: 14, color: 'var(--text-secondary)' }}>
|
|
|
|
|
参数
|
|
|
|
|
</h3>
|
|
|
|
|
<div style={{ display: 'flex', gap: 24, alignItems: 'center' }}>
|
|
|
|
|
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
|
|
|
<span style={{ fontSize: 13, color: 'var(--text-secondary)' }}>分辨率</span>
|
|
|
|
|
<input
|
|
|
|
|
type="number"
|
|
|
|
|
value={params.resolution}
|
|
|
|
|
onChange={e => setParams({ resolution: Number(e.target.value) })}
|
|
|
|
|
min={32}
|
|
|
|
|
max={1024}
|
|
|
|
|
step={32}
|
|
|
|
|
style={{ width: 100 }}
|
|
|
|
|
/>
|
|
|
|
|
<span style={{ fontSize: 13, color: 'var(--text-muted)' }}>px</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
|
|
|
<span style={{ fontSize: 13, color: 'var(--text-secondary)' }}>格式</span>
|
|
|
|
|
<select
|
|
|
|
|
value={params.format}
|
|
|
|
|
onChange={e =>
|
|
|
|
|
setParams({ format: e.target.value as 'spritesheet' | 'individual' })
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<option value="spritesheet">Spritesheet</option>
|
|
|
|
|
<option value="individual">单帧</option>
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 提交 */}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="btn-primary"
|
2026-05-24 21:16:15 +08:00
|
|
|
onClick={handleSubmit}
|
|
|
|
|
disabled={submitting || !finalPrompt.trim()}
|
2026-05-24 14:18:39 +08:00
|
|
|
style={{ alignSelf: 'flex-start', padding: '12px 32px', fontSize: 16 }}
|
|
|
|
|
>
|
|
|
|
|
{submitting ? '提交中...' : '开始生成'}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|