feat(title): AI 自动选择标题开关可用 — 自动生成+只读显示+样式可调+校验放行 #1236

Merged
xiaoxia merged 13 commits from fix/ai-auto-select-title into develop 2026-08-04 17:24:59 +08:00
4 changed files with 145 additions and 51 deletions
@@ -21,34 +21,71 @@ const Step4TitleSettings: React.FC<Step4TitleSettingsProps> = (props) => {
<div className="xx-form-section">
<h3>📝 </h3>
<AiTitleGenerator
inputValue={t.aiTitleInput}
onInputChange={t.setAiTitleInput}
generating={t.aiTitleGenerating}
onGenerate={t.handleGenerateAiTitles}
results={t.aiTitleResults}
hasGenerated={t.hasGeneratedTitles}
onSelect={t.handleSelectAiTitle}
selectedTitle={t.titleSettings.title}
onRefresh={t.handleRefreshAiTitles}
/>
{/* AI 自动选择模式 */}
{t.titleSettings.aiAutoSelect && (
<>
<div className="xx-title-ai-toggle">
<span className="xx-toggle-label">AI </span>
<div className="xx-switch active" onClick={t.toggleAiAutoSelect}>
<div className="xx-switch-knob" />
</div>
</div>
<div className="xx-divider">
<span></span>
</div>
<div className="xx-title-ai-toggle">
<span className="xx-toggle-label">AI </span>
<div
className={`xx-switch ${t.titleSettings.aiAutoSelect ? "active" : ""}`}
onClick={t.toggleAiAutoSelect}
>
<div className="xx-switch-knob" />
</div>
</div>
{/* 显示当前 AI 选中的标题(只读)+ 换一个按钮 */}
<div className="xx-form-field">
<label> AI </label>
<div
style={{
display: "flex",
alignItems: "center",
gap: 10,
padding: "8px 12px",
background: "var(--bg-secondary, rgba(0,0,0,0.04))",
borderRadius: 8,
fontSize: 14,
color: "var(--text-primary, #333)",
}}
>
<span style={{ flex: 1 }}>{t.titleSettings.title || "AI 将自动为你选择标题"}</span>
<button
type="button"
className="xx-btn xx-btn-primary"
style={{ flexShrink: 0, fontSize: 13, padding: "4px 12px" }}
onClick={t.autoGenerateTitle}
>
🔄
</button>
</div>
</div>
</>
)}
{/* 手动选择模式 */}
{!t.titleSettings.aiAutoSelect && (
<>
<AiTitleGenerator
inputValue={t.aiTitleInput}
onInputChange={t.setAiTitleInput}
generating={t.aiTitleGenerating}
onGenerate={t.handleGenerateAiTitles}
results={t.aiTitleResults}
hasGenerated={t.hasGeneratedTitles}
onSelect={t.handleSelectAiTitle}
selectedTitle={t.titleSettings.title}
onRefresh={t.handleRefreshAiTitles}
/>
<div className="xx-divider">
<span></span>
</div>
<div className="xx-title-ai-toggle">
<span className="xx-toggle-label">AI </span>
<div className="xx-switch" onClick={t.toggleAiAutoSelect}>
<div className="xx-switch-knob" />
</div>
</div>
<div className="xx-form-field">
<label></label>
<Select
@@ -83,24 +120,25 @@ const Step4TitleSettings: React.FC<Step4TitleSettingsProps> = (props) => {
maxLength={50}
/>
</div>
<TitleStylePanel
settings={t.titleSettings}
onUpdatePosition={t.updatePosition}
onUpdateFont={t.updateFont}
onUpdateSize={t.updateSize}
onToggleBold={t.toggleBold}
onToggleItalic={t.toggleItalic}
onToggleStroke={t.toggleStroke}
onToggleShadow={t.toggleShadow}
onApplyPreset={t.applyPreset}
activePreset={t.activePreset}
titlePresets={t.titlePresets}
POSITION_OPTIONS={POSITION_OPTIONS}
FONT_OPTIONS={FONT_OPTIONS}
/>
</>
)}
{/* 样式面板始终可见,两种模式下都可调整标题展示样式 */}
<TitleStylePanel
settings={t.titleSettings}
onUpdatePosition={t.updatePosition}
onUpdateFont={t.updateFont}
onUpdateSize={t.updateSize}
onToggleBold={t.toggleBold}
onToggleItalic={t.toggleItalic}
onToggleStroke={t.toggleStroke}
onToggleShadow={t.toggleShadow}
onApplyPreset={t.applyPreset}
activePreset={t.activePreset}
titlePresets={t.titlePresets}
POSITION_OPTIONS={POSITION_OPTIONS}
FONT_OPTIONS={FONT_OPTIONS}
/>
</div>
)
}
@@ -63,7 +63,8 @@ export const buildEditPlanPayload = (props: UseGenerateVideoProps) => {
export const validateGenerateInputs = (props: UseGenerateVideoProps): string | null => {
const { titleSettings, materialMode, selectedMaterials, voiceMode, selectedClonedVoice } = props
if (!titleSettings.title.trim()) {
// AI 自动选择模式下,标题可以为空(后端会自行生成)
if (!titleSettings.aiAutoSelect && !titleSettings.title?.trim()) {
return "请先选择或输入标题"
}
if (materialMode === "manual" && selectedMaterials.length === 0) {
@@ -1,3 +1,4 @@
import { useEffect, useRef } from "react"
import { useQuery } from "@tanstack/react-query"
import { getTitles } from "@/api/titles"
import type { TitleSettings } from "../../types"
@@ -22,27 +23,59 @@ export function useStep4Title({ titleSettings, onTitleSettingsChange }: UseStep4
})
// AI 标题生成
const aiGenerator = useAiTitleGenerator({ titleSettings, onTitleSettingsChange })
const {
aiTitleInput,
setAiTitleInput,
aiTitleGenerating,
aiTitleResults,
hasGeneratedTitles,
handleGenerateAiTitles,
handleSelectAiTitle,
handleRefreshAiTitles,
autoGenerateTitle,
} = useAiTitleGenerator({ titleSettings, onTitleSettingsChange })
// 样式更新
const styleUpdaters = useTitleStyleUpdaters({ titleSettings, onTitleSettingsChange })
// 追踪 AI 自动选择开关的上一次值 & 是否首次挂载
const prevAiAutoSelect = useRef(titleSettings.aiAutoSelect)
const isFirstMount = useRef(true)
// 当 AI 自动选择开关打开时,自动生成/选择一个标题填入
// 首次挂载时如果开关已经是 true 且无标题,也需要触发
useEffect(() => {
if (isFirstMount.current) {
isFirstMount.current = false
if (titleSettings.aiAutoSelect && !titleSettings.title) {
autoGenerateTitle()
}
prevAiAutoSelect.current = titleSettings.aiAutoSelect
return
}
if (titleSettings.aiAutoSelect && !prevAiAutoSelect.current && !titleSettings.title) {
autoGenerateTitle()
}
prevAiAutoSelect.current = titleSettings.aiAutoSelect
}, [titleSettings.aiAutoSelect, titleSettings.title, autoGenerateTitle])
return {
// 数据
userTitles,
titleSettings,
// AI 标题状态
aiTitleInput: aiGenerator.aiTitleInput,
setAiTitleInput: aiGenerator.setAiTitleInput,
aiTitleGenerating: aiGenerator.aiTitleGenerating,
aiTitleResults: aiGenerator.aiTitleResults,
hasGeneratedTitles: aiGenerator.hasGeneratedTitles,
aiTitleInput,
setAiTitleInput,
aiTitleGenerating,
aiTitleResults,
hasGeneratedTitles,
activePreset: styleUpdaters.activePreset,
titlePresets: styleUpdaters.titlePresets,
// AI 标题操作
handleGenerateAiTitles: aiGenerator.handleGenerateAiTitles,
handleSelectAiTitle: aiGenerator.handleSelectAiTitle,
handleRefreshAiTitles: aiGenerator.handleRefreshAiTitles,
handleGenerateAiTitles,
handleSelectAiTitle,
handleRefreshAiTitles,
autoGenerateTitle,
// 标题设置操作
updateTitle: styleUpdaters.updateTitle,
toggleAiAutoSelect: styleUpdaters.toggleAiAutoSelect,
@@ -72,9 +72,30 @@ export function useAiTitleGenerator({
setAiTitleGenerating(false)
}, [aiTitleInput])
/**
* 自动生成标题(供 AI 自动选择开关使用)
* 如果已有生成结果,直接从中选一个;否则用默认关键词生成
*/
const autoGenerateTitle = useCallback((): string => {
if (aiTitleResults.length > 0) {
const picked = aiTitleResults[Math.floor(Math.random() * aiTitleResults.length)]
if (!picked) return ""
onTitleSettingsChange({ ...titleSettings, title: picked.title })
return picked.title
}
// 没有已有结果,用默认关键词生成
const results = generateTitlesFromTopic("短视频")
setAiTitleResults(results)
setHasGeneratedTitles(true)
const picked = results[Math.floor(Math.random() * results.length)]
if (!picked) return ""
onTitleSettingsChange({ ...titleSettings, title: picked.title })
return picked.title
}, [aiTitleResults, titleSettings, onTitleSettingsChange])
const handleSelectAiTitle = useCallback(
(title: string) => {
onTitleSettingsChange({ ...titleSettings, title, aiAutoSelect: false })
onTitleSettingsChange({ ...titleSettings, title })
message.success("已选用此标题")
},
[titleSettings, onTitleSettingsChange],
@@ -98,5 +119,6 @@ export function useAiTitleGenerator({
handleGenerateAiTitles,
handleSelectAiTitle,
handleRefreshAiTitles,
autoGenerateTitle,
}
}