feat(prompt): AI 提示词优化双栏交互
新增 prompt API 模块调用后端 /api/v1/prompt/optimize,PromptEditor 重构为左右双栏布局支持原始/优化提示词对比编辑,默认开启 AI 优化, 用户可关闭开关或手动编辑优化结果后再提交生成。
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
.promptPanels {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.panelSingle {
|
||||
composes: panel;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.panelLabel {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.optimizedBadge {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
background: var(--accent-dim);
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
resize: vertical;
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.toggleRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 22px;
|
||||
border-radius: 11px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toggleOn {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.toggleOff {
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.toggleKnob {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
transition: left 0.2s;
|
||||
}
|
||||
|
||||
.toggleKnobOn {
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
.toggleKnobOff {
|
||||
left: 2px;
|
||||
}
|
||||
|
||||
.toggleLabel {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.optimizeBtn {
|
||||
padding: 6px 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.promptPanels {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ const ASSET_TYPES: { value: AssetType; label: string }[] = [
|
||||
]
|
||||
|
||||
interface GenerateFormProps {
|
||||
onSubmit: () => void
|
||||
onSubmit: (prompt: string) => void
|
||||
submitting: boolean
|
||||
}
|
||||
|
||||
@@ -24,13 +24,32 @@ export default function GenerateForm({ onSubmit, submitting }: GenerateFormProps
|
||||
assetType,
|
||||
taskStyle,
|
||||
params,
|
||||
enableAI,
|
||||
optimizedPrompt,
|
||||
isOptimizing,
|
||||
setPrompt,
|
||||
setAssetType,
|
||||
toggleTaskStyle,
|
||||
setParams,
|
||||
setEnableAI,
|
||||
setOptimizedPrompt,
|
||||
runOptimize,
|
||||
} = useTaskStore()
|
||||
|
||||
const mergedStyle = mergeStyles(projectStyle, taskStyle)
|
||||
const finalPrompt = enableAI && optimizedPrompt ? optimizedPrompt : prompt
|
||||
|
||||
const handleOptimize = async () => {
|
||||
try {
|
||||
await runOptimize(projectStyle)
|
||||
} catch {
|
||||
// error handled in store
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit(finalPrompt)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||
@@ -62,16 +81,41 @@ export default function GenerateForm({ onSubmit, submitting }: GenerateFormProps
|
||||
<StyleSelector value={taskStyle} onChange={toggleTaskStyle} compact />
|
||||
</div>
|
||||
|
||||
{/* 提示词 */}
|
||||
{/* 提示词(含 AI 优化) */}
|
||||
<div>
|
||||
<h3 style={{ marginBottom: 12, fontSize: 14, color: 'var(--text-secondary)' }}>
|
||||
提示词
|
||||
</h3>
|
||||
<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>
|
||||
<PromptEditor
|
||||
prompt={prompt}
|
||||
onPromptChange={setPrompt}
|
||||
style={mergedStyle}
|
||||
params={{ resolution: params.resolution, format: params.format }}
|
||||
enableAI={enableAI}
|
||||
onEnableAIToggle={setEnableAI}
|
||||
optimizedPrompt={optimizedPrompt}
|
||||
isOptimizing={isOptimizing}
|
||||
onOptimizedPromptChange={setOptimizedPrompt}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -113,8 +157,8 @@ export default function GenerateForm({ onSubmit, submitting }: GenerateFormProps
|
||||
<button
|
||||
type="button"
|
||||
className="btn-primary"
|
||||
onClick={onSubmit}
|
||||
disabled={submitting || !prompt.trim()}
|
||||
onClick={handleSubmit}
|
||||
disabled={submitting || !finalPrompt.trim()}
|
||||
style={{ alignSelf: 'flex-start', padding: '12px 32px', fontSize: 16 }}
|
||||
>
|
||||
{submitting ? '提交中...' : '开始生成'}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { STYLE_CATEGORIES } from '../utils/style'
|
||||
import styles from './GenerateForm.module.css'
|
||||
|
||||
interface PromptEditorProps {
|
||||
prompt: string
|
||||
@@ -8,6 +9,11 @@ interface PromptEditorProps {
|
||||
resolution: number
|
||||
format: string
|
||||
}
|
||||
enableAI: boolean
|
||||
onEnableAIToggle: (enable: boolean) => void
|
||||
optimizedPrompt: string | null
|
||||
isOptimizing: boolean
|
||||
onOptimizedPromptChange: (text: string) => void
|
||||
}
|
||||
|
||||
function styleToDescription(kvPairs: Record<string, string>): string {
|
||||
@@ -26,18 +32,75 @@ export default function PromptEditor({
|
||||
onPromptChange,
|
||||
style,
|
||||
params,
|
||||
enableAI,
|
||||
onEnableAIToggle,
|
||||
optimizedPrompt,
|
||||
isOptimizing,
|
||||
onOptimizedPromptChange,
|
||||
}: PromptEditorProps) {
|
||||
const styleDesc = styleToDescription(style)
|
||||
const displayPrompt = enableAI && optimizedPrompt ? optimizedPrompt : prompt
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<textarea
|
||||
value={prompt}
|
||||
onChange={e => onPromptChange(e.target.value)}
|
||||
placeholder="描述你想要生成的游戏素材..."
|
||||
rows={4}
|
||||
style={{ resize: 'vertical', width: '100%' }}
|
||||
/>
|
||||
{/* AI 开关 */}
|
||||
<div className={styles.toggleRow}>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.toggle} ${enableAI ? styles.toggleOn : styles.toggleOff}`}
|
||||
onClick={() => onEnableAIToggle(!enableAI)}
|
||||
role="switch"
|
||||
aria-checked={enableAI}
|
||||
>
|
||||
<span
|
||||
className={`${styles.toggleKnob} ${enableAI ? styles.toggleKnobOn : styles.toggleKnobOff}`}
|
||||
/>
|
||||
</button>
|
||||
<span className={styles.toggleLabel}>启用 AI 提示词优化</span>
|
||||
</div>
|
||||
|
||||
{/* 双栏文本框 */}
|
||||
<div className={enableAI ? styles.promptPanels : ''}>
|
||||
<div className={enableAI ? styles.panel : styles.panelSingle}>
|
||||
<span className={styles.panelLabel}>
|
||||
{enableAI ? '原始提示词' : '提示词'}
|
||||
</span>
|
||||
<textarea
|
||||
value={prompt}
|
||||
onChange={e => onPromptChange(e.target.value)}
|
||||
placeholder="描述你想要生成的游戏素材..."
|
||||
rows={4}
|
||||
className={styles.textarea}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{enableAI && (
|
||||
<div className={styles.panel}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span className={styles.panelLabel}>优化后提示词</span>
|
||||
{optimizedPrompt && (
|
||||
<span className={styles.optimizedBadge}>AI 优化</span>
|
||||
)}
|
||||
{isOptimizing && (
|
||||
<span style={{ fontSize: 12, color: 'var(--text-muted)' }}>
|
||||
优化中...
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<textarea
|
||||
value={optimizedPrompt ?? ''}
|
||||
onChange={e => onOptimizedPromptChange(e.target.value)}
|
||||
placeholder={isOptimizing ? 'AI 正在优化提示词...' : '点击「优化提示词」按钮生成'}
|
||||
rows={4}
|
||||
className={styles.textarea}
|
||||
disabled={isOptimizing}
|
||||
style={{ opacity: isOptimizing ? 0.6 : 1 }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 预览面板 */}
|
||||
<div
|
||||
style={{
|
||||
background: 'var(--bg-input)',
|
||||
@@ -55,7 +118,7 @@ export default function PromptEditor({
|
||||
</div>
|
||||
<div>
|
||||
<strong style={{ color: 'var(--accent)' }}>[主体]</strong>{' '}
|
||||
{prompt || '未输入'}
|
||||
{displayPrompt || '未输入'}
|
||||
</div>
|
||||
<div>
|
||||
<strong style={{ color: 'var(--accent)' }}>[参数]</strong> 分辨率=
|
||||
|
||||
Reference in New Issue
Block a user