feat(home): add AI story expand button and modal
This commit is contained in:
@@ -13,11 +13,13 @@ import { RatioSelector, StyleSelector } from '@/components/selectors/RatioStyleS
|
||||
import { ART_STYLES, VIDEO_RATIOS } from '@/lib/constants'
|
||||
import { Link, useRouter } from '@/i18n/navigation'
|
||||
import { apiFetch } from '@/lib/api-fetch'
|
||||
import { expandHomeStory } from '@/lib/home/ai-story-expand'
|
||||
import { createHomeProjectLaunch } from '@/lib/home/create-project-launch'
|
||||
import {
|
||||
HOME_QUICK_START_MIN_ROWS,
|
||||
resolveTextareaTargetHeight,
|
||||
} from '@/lib/home/quick-start-textarea'
|
||||
import AiWriteModal from '@/components/home/AiWriteModal'
|
||||
|
||||
interface ProjectStats {
|
||||
episodes: number
|
||||
@@ -50,6 +52,8 @@ export default function HomePage() {
|
||||
const [videoRatio, setVideoRatio] = useState('9:16')
|
||||
const [artStyle, setArtStyle] = useState('american-comic')
|
||||
const [createLoading, setCreateLoading] = useState(false)
|
||||
const [aiWriteOpen, setAiWriteOpen] = useState(false)
|
||||
const [aiWriteLoading, setAiWriteLoading] = useState(false)
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const textareaMinHeightRef = useRef<number | null>(null)
|
||||
|
||||
@@ -148,6 +152,26 @@ export default function HomePage() {
|
||||
}
|
||||
}
|
||||
|
||||
// AI 帮我写 — 直接生成文本并回填首页输入框
|
||||
const handleAiWriteStart = async (prompt: string) => {
|
||||
if (aiWriteLoading) return
|
||||
setAiWriteLoading(true)
|
||||
try {
|
||||
const result = await expandHomeStory({
|
||||
apiFetch,
|
||||
prompt,
|
||||
})
|
||||
|
||||
setInputValue(result.expandedText)
|
||||
setAiWriteOpen(false)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed'
|
||||
window.alert(message)
|
||||
} finally {
|
||||
setAiWriteLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 比例选项(带推荐标签)
|
||||
const ratioOptions = useMemo(
|
||||
() => VIDEO_RATIOS.map((r) => ({ ...r, recommended: r.value === '9:16' })),
|
||||
@@ -251,7 +275,7 @@ export default function HomePage() {
|
||||
className="w-full bg-transparent border-none outline-none text-[var(--glass-text-primary)] placeholder:text-[var(--glass-text-tertiary)] text-base resize-none p-5 pb-3 custom-scrollbar"
|
||||
/>
|
||||
|
||||
{/* 底部工具栏:比例 + 风格 + 创建按钮 */}
|
||||
{/* 底部工具栏:比例 + 风格 + AI帮我写 + 创建按钮 */}
|
||||
<div className="flex items-end gap-3 px-5 pb-4">
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<div className="w-[160px] flex-shrink-0">
|
||||
@@ -269,6 +293,23 @@ export default function HomePage() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setAiWriteOpen(true)}
|
||||
disabled={createLoading}
|
||||
className="glass-btn-base px-4 py-2.5 text-sm flex-shrink-0 border border-[var(--glass-stroke-strong)] hover:border-[var(--glass-tone-info-fg)]/40 transition-all flex items-center gap-1.5"
|
||||
>
|
||||
<AppIcon name="sparkles" className="w-4 h-4 text-[#7c3aed]" />
|
||||
<span
|
||||
className="font-medium"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #3b82f6, #7c3aed)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
}}
|
||||
>
|
||||
{t('aiWrite.trigger')}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => void handleCreate()}
|
||||
disabled={!inputValue.trim() || createLoading}
|
||||
@@ -280,6 +321,14 @@ export default function HomePage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* AI 帮我写模态框 */}
|
||||
<AiWriteModal
|
||||
open={aiWriteOpen}
|
||||
loading={aiWriteLoading}
|
||||
onClose={() => setAiWriteOpen(false)}
|
||||
onStart={(prompt) => void handleAiWriteStart(prompt)}
|
||||
t={(key: string) => t(`aiWrite.${key}`)}
|
||||
/>
|
||||
</main>
|
||||
|
||||
{/* 最近项目 */}
|
||||
|
||||
48
src/app/api/user/ai-story-expand/route.ts
Normal file
48
src/app/api/user/ai-story-expand/route.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { createHash } from 'crypto'
|
||||
import { NextRequest } from 'next/server'
|
||||
import { requireUserAuth, isErrorResponse } from '@/lib/api-auth'
|
||||
import { apiHandler, ApiError } from '@/lib/api-errors'
|
||||
import { getUserModelConfig } from '@/lib/config-service'
|
||||
import { maybeSubmitLLMTask } from '@/lib/llm-observe/route-task'
|
||||
import { TASK_TYPE } from '@/lib/task/types'
|
||||
|
||||
export const POST = apiHandler(async (request: NextRequest) => {
|
||||
const authResult = await requireUserAuth()
|
||||
if (isErrorResponse(authResult)) return authResult
|
||||
const { session } = authResult
|
||||
|
||||
const body = (await request.json().catch(() => ({}))) as Record<string, unknown>
|
||||
const prompt = typeof body.prompt === 'string' ? body.prompt.trim() : ''
|
||||
if (!prompt) {
|
||||
throw new ApiError('INVALID_PARAMS')
|
||||
}
|
||||
|
||||
const userConfig = await getUserModelConfig(session.user.id)
|
||||
if (!userConfig.analysisModel) {
|
||||
throw new ApiError('MISSING_CONFIG')
|
||||
}
|
||||
|
||||
const dedupeDigest = createHash('sha1')
|
||||
.update(`${session.user.id}:home-story-expand:${prompt}`)
|
||||
.digest('hex')
|
||||
.slice(0, 16)
|
||||
|
||||
const asyncTaskResponse = await maybeSubmitLLMTask({
|
||||
request,
|
||||
userId: session.user.id,
|
||||
projectId: 'home-ai-write',
|
||||
type: TASK_TYPE.AI_STORY_EXPAND,
|
||||
targetType: 'HomeAiStoryExpand',
|
||||
targetId: session.user.id,
|
||||
routePath: '/api/user/ai-story-expand',
|
||||
body: {
|
||||
prompt,
|
||||
analysisModel: userConfig.analysisModel,
|
||||
},
|
||||
dedupeKey: `home_ai_story_expand:${dedupeDigest}`,
|
||||
priority: 1,
|
||||
})
|
||||
if (asyncTaskResponse) return asyncTaskResponse
|
||||
|
||||
throw new ApiError('INVALID_PARAMS')
|
||||
})
|
||||
126
src/components/home/AiWriteModal.tsx
Normal file
126
src/components/home/AiWriteModal.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* AI 帮我写 — 首页轻量模态框
|
||||
*
|
||||
* 用户输入创意/关键词/大纲,直接生成结果并回填首页主输入框
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react'
|
||||
import { AppIcon } from '@/components/ui/icons'
|
||||
|
||||
interface AiWriteModalProps {
|
||||
open: boolean
|
||||
loading: boolean
|
||||
onClose: () => void
|
||||
onStart: (prompt: string) => void
|
||||
t: (key: string) => string
|
||||
}
|
||||
|
||||
export default function AiWriteModal({
|
||||
open,
|
||||
loading,
|
||||
onClose,
|
||||
onStart,
|
||||
t,
|
||||
}: AiWriteModalProps) {
|
||||
const [promptText, setPromptText] = useState('')
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
if (loading) return
|
||||
setPromptText('')
|
||||
onClose()
|
||||
}, [loading, onClose])
|
||||
|
||||
const handleStart = useCallback(() => {
|
||||
if (!promptText.trim() || loading) return
|
||||
onStart(promptText.trim())
|
||||
}, [promptText, loading, onStart])
|
||||
|
||||
if (!open) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 glass-overlay flex items-center justify-center z-50 backdrop-blur-sm"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<div
|
||||
className="w-full max-w-lg mx-4 relative"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* 模态框容器 */}
|
||||
<div className="glass-surface-modal rounded-2xl p-6 space-y-5">
|
||||
{/* 头部 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0"
|
||||
style={{ background: 'linear-gradient(135deg, rgba(59,130,246,0.15), rgba(139,92,246,0.15))' }}
|
||||
>
|
||||
<AppIcon name="sparkles" className="w-5 h-5 text-[#7c3aed]" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-[var(--glass-text-primary)]">
|
||||
{t('modalTitle')}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--glass-text-tertiary)]">
|
||||
{t('modalSubtitle')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="glass-icon-btn-sm"
|
||||
disabled={loading}
|
||||
>
|
||||
<AppIcon name="close" className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 输入区域 */}
|
||||
<div>
|
||||
<label className="text-sm font-medium text-[var(--glass-text-secondary)] mb-2 block">
|
||||
{t('inputLabel')}
|
||||
</label>
|
||||
<textarea
|
||||
value={promptText}
|
||||
onChange={(e) => setPromptText(e.target.value)}
|
||||
placeholder={t('placeholder')}
|
||||
className="glass-textarea-base custom-scrollbar h-36 px-4 py-3 text-sm resize-none placeholder:text-[var(--glass-text-tertiary)]"
|
||||
disabled={loading}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 提示文案 */}
|
||||
<div
|
||||
className="px-3 py-2 rounded-lg text-xs text-[var(--glass-text-tertiary)] leading-relaxed"
|
||||
style={{ background: 'linear-gradient(135deg, rgba(59,130,246,0.06), rgba(139,92,246,0.06))' }}
|
||||
>
|
||||
{t('hint')}
|
||||
</div>
|
||||
|
||||
{/* 按钮区域 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={handleClose}
|
||||
disabled={loading}
|
||||
className="flex-1 py-2.5 text-sm text-[var(--glass-text-tertiary)] hover:text-[var(--glass-text-secondary)] transition-colors rounded-xl"
|
||||
>
|
||||
{t('cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleStart}
|
||||
disabled={!promptText.trim() || loading}
|
||||
className="flex-1 py-3 rounded-xl text-white font-semibold text-sm flex items-center justify-center gap-2 transition-all hover:opacity-90 active:scale-[0.98] disabled:opacity-50"
|
||||
style={{ background: 'linear-gradient(135deg, #3b82f6, #7c3aed)' }}
|
||||
>
|
||||
<AppIcon name="sparkles" className="w-4 h-4" />
|
||||
<span>{loading ? '...' : t('startAiWrite')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -31,6 +31,7 @@ const GENERATION_OPERATION_PATTERNS = [
|
||||
/\/story-to-script(?:-|\/|$)/,
|
||||
/\/script-to-storyboard(?:-|\/|$)/,
|
||||
/\/screenplay-conversion(?:\/|$)/,
|
||||
/\/ai-story-expand(?:\/|$)/,
|
||||
/\/voice-(?:analyze|design|generate)(?:\/|$)/,
|
||||
/\/ai-(?:create|modify)-/,
|
||||
/\/modify-(?:asset|storyboard)-image(?:\/|$)/,
|
||||
|
||||
@@ -36,6 +36,7 @@ const BILLABLE_TASK_TYPES = new Set<TaskType>([
|
||||
TASK_TYPE.SCREENPLAY_CONVERT,
|
||||
TASK_TYPE.VOICE_ANALYZE,
|
||||
TASK_TYPE.ANALYZE_GLOBAL,
|
||||
TASK_TYPE.AI_STORY_EXPAND,
|
||||
TASK_TYPE.AI_MODIFY_APPEARANCE,
|
||||
TASK_TYPE.AI_MODIFY_LOCATION,
|
||||
TASK_TYPE.AI_MODIFY_SHOT_PROMPT,
|
||||
@@ -277,6 +278,7 @@ export function buildDefaultTaskBillingInfo(taskType: TaskType, payload: AnyPayl
|
||||
case TASK_TYPE.SCREENPLAY_CONVERT:
|
||||
case TASK_TYPE.VOICE_ANALYZE:
|
||||
case TASK_TYPE.ANALYZE_GLOBAL:
|
||||
case TASK_TYPE.AI_STORY_EXPAND:
|
||||
case TASK_TYPE.AI_MODIFY_APPEARANCE:
|
||||
case TASK_TYPE.AI_MODIFY_LOCATION:
|
||||
case TASK_TYPE.AI_MODIFY_SHOT_PROMPT:
|
||||
|
||||
41
src/lib/home/ai-story-expand.ts
Normal file
41
src/lib/home/ai-story-expand.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { resolveTaskResponse } from '@/lib/task/client'
|
||||
|
||||
interface ApiFetchLike {
|
||||
(input: string, init?: RequestInit): Promise<Response>
|
||||
}
|
||||
|
||||
interface ExpandHomeStoryPayload {
|
||||
expandedText?: string
|
||||
}
|
||||
|
||||
export interface ExpandHomeStoryParams {
|
||||
apiFetch: ApiFetchLike
|
||||
prompt: string
|
||||
}
|
||||
|
||||
export interface ExpandHomeStoryResult {
|
||||
expandedText: string
|
||||
}
|
||||
|
||||
export async function expandHomeStory({
|
||||
apiFetch,
|
||||
prompt,
|
||||
}: ExpandHomeStoryParams): Promise<ExpandHomeStoryResult> {
|
||||
const response = await apiFetch('/api/user/ai-story-expand', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
prompt,
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await resolveTaskResponse<ExpandHomeStoryPayload>(response)
|
||||
const expandedText = typeof result.expandedText === 'string' ? result.expandedText.trim() : ''
|
||||
if (!expandedText) {
|
||||
throw new Error('AI story expand response missing expandedText')
|
||||
}
|
||||
|
||||
return {
|
||||
expandedText,
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ export interface HomeWorkspaceLaunchTarget {
|
||||
pathname: string
|
||||
query: {
|
||||
episode: string
|
||||
autoRun: 'storyToScript'
|
||||
autoRun?: 'storyToScript'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ const POLICY_BY_TASK_TYPE: Partial<Record<TaskType, LLMTaskPolicy>> = {
|
||||
[TASK_TYPE.SCREENPLAY_CONVERT]: LONG_FLOW_HIGH_POLICY,
|
||||
[TASK_TYPE.VOICE_ANALYZE]: LLM_STANDARD_POLICY,
|
||||
[TASK_TYPE.ANALYZE_GLOBAL]: LLM_STANDARD_POLICY,
|
||||
[TASK_TYPE.AI_STORY_EXPAND]: LLM_STANDARD_POLICY,
|
||||
[TASK_TYPE.AI_MODIFY_APPEARANCE]: LLM_STANDARD_POLICY,
|
||||
[TASK_TYPE.AI_MODIFY_LOCATION]: LLM_STANDARD_POLICY,
|
||||
[TASK_TYPE.AI_MODIFY_SHOT_PROMPT]: LLM_STANDARD_POLICY,
|
||||
|
||||
@@ -81,6 +81,10 @@ export const PROMPT_CATALOG: Record<PromptId, PromptCatalogEntry> = {
|
||||
'clip_content',
|
||||
],
|
||||
},
|
||||
[PROMPT_IDS.NP_AI_STORY_EXPAND]: {
|
||||
pathStem: 'novel-promotion/ai_story_expand',
|
||||
variableKeys: ['input'],
|
||||
},
|
||||
[PROMPT_IDS.NP_CHARACTER_CREATE]: {
|
||||
pathStem: 'novel-promotion/character_create',
|
||||
variableKeys: ['user_input'],
|
||||
|
||||
@@ -11,6 +11,7 @@ export const PROMPT_IDS = {
|
||||
NP_AGENT_STORYBOARD_DETAIL: 'np_agent_storyboard_detail',
|
||||
NP_AGENT_STORYBOARD_INSERT: 'np_agent_storyboard_insert',
|
||||
NP_AGENT_STORYBOARD_PLAN: 'np_agent_storyboard_plan',
|
||||
NP_AI_STORY_EXPAND: 'np_ai_story_expand',
|
||||
NP_CHARACTER_CREATE: 'np_character_create',
|
||||
NP_CHARACTER_DESCRIPTION_UPDATE: 'np_character_description_update',
|
||||
NP_CHARACTER_MODIFY: 'np_character_modify',
|
||||
|
||||
@@ -44,6 +44,7 @@ const TASK_INTENT_BY_TYPE: Record<TaskType, TaskIntent> = {
|
||||
[TASK_TYPE.SCREENPLAY_CONVERT]: 'convert',
|
||||
[TASK_TYPE.VOICE_ANALYZE]: 'analyze',
|
||||
[TASK_TYPE.ANALYZE_GLOBAL]: 'analyze',
|
||||
[TASK_TYPE.AI_STORY_EXPAND]: 'generate',
|
||||
[TASK_TYPE.AI_MODIFY_APPEARANCE]: 'modify',
|
||||
[TASK_TYPE.AI_MODIFY_LOCATION]: 'modify',
|
||||
[TASK_TYPE.AI_MODIFY_SHOT_PROMPT]: 'modify',
|
||||
|
||||
@@ -23,6 +23,7 @@ const TASK_TYPE_LABELS: Record<string, string> = {
|
||||
[TASK_TYPE.SCREENPLAY_CONVERT]: 'progress.taskType.screenplayConvert',
|
||||
[TASK_TYPE.VOICE_ANALYZE]: 'progress.taskType.voiceAnalyze',
|
||||
[TASK_TYPE.ANALYZE_GLOBAL]: 'progress.taskType.analyzeGlobal',
|
||||
[TASK_TYPE.AI_STORY_EXPAND]: 'progress.taskType.aiStoryExpand',
|
||||
[TASK_TYPE.AI_MODIFY_APPEARANCE]: 'progress.taskType.aiModifyAppearance',
|
||||
[TASK_TYPE.AI_MODIFY_LOCATION]: 'progress.taskType.aiModifyLocation',
|
||||
[TASK_TYPE.AI_MODIFY_SHOT_PROMPT]: 'progress.taskType.aiModifyShotPrompt',
|
||||
@@ -63,6 +64,8 @@ const STAGE_LABELS: Record<string, string> = {
|
||||
script_to_storyboard_step: 'progress.stage.scriptToStoryboardStep',
|
||||
script_to_storyboard_persist: 'progress.stage.scriptToStoryboardPersist',
|
||||
script_to_storyboard_persist_done: 'progress.stage.scriptToStoryboardPersistDone',
|
||||
ai_story_expand_prepare: 'progress.stage.aiStoryExpandPrepare',
|
||||
ai_story_expand_done: 'progress.stage.aiStoryExpandDone',
|
||||
insert_panel_generate_text: 'progress.stage.insertPanelGenerateText',
|
||||
insert_panel_persist: 'progress.stage.insertPanelPersist',
|
||||
polling_external: 'progress.stage.pollingExternal',
|
||||
|
||||
@@ -60,6 +60,7 @@ export const TASK_TYPE = {
|
||||
SCREENPLAY_CONVERT: 'screenplay_convert',
|
||||
VOICE_ANALYZE: 'voice_analyze',
|
||||
ANALYZE_GLOBAL: 'analyze_global',
|
||||
AI_STORY_EXPAND: 'ai_story_expand',
|
||||
AI_MODIFY_APPEARANCE: 'ai_modify_appearance',
|
||||
AI_MODIFY_LOCATION: 'ai_modify_location',
|
||||
AI_MODIFY_SHOT_PROMPT: 'ai_modify_shot_prompt',
|
||||
|
||||
79
src/lib/workers/handlers/ai-story-expand.ts
Normal file
79
src/lib/workers/handlers/ai-story-expand.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type { Job } from 'bullmq'
|
||||
import { executeAiTextStep } from '@/lib/ai-runtime'
|
||||
import { withInternalLLMStreamCallbacks } from '@/lib/llm-observe/internal-stream-context'
|
||||
import { buildPrompt, PROMPT_IDS } from '@/lib/prompt-i18n'
|
||||
import type { TaskJobData } from '@/lib/task/types'
|
||||
import { reportTaskProgress } from '@/lib/workers/shared'
|
||||
import { assertTaskActive } from '@/lib/workers/utils'
|
||||
import { createWorkerLLMStreamCallbacks, createWorkerLLMStreamContext } from './llm-stream'
|
||||
|
||||
function readText(value: unknown): string {
|
||||
return typeof value === 'string' ? value : ''
|
||||
}
|
||||
|
||||
export async function handleAiStoryExpandTask(job: Job<TaskJobData>) {
|
||||
const payload = (job.data.payload || {}) as Record<string, unknown>
|
||||
const promptInput = readText(payload.prompt).trim()
|
||||
const analysisModel = readText(payload.analysisModel).trim()
|
||||
|
||||
if (!promptInput) {
|
||||
throw new Error('prompt is required')
|
||||
}
|
||||
if (!analysisModel) {
|
||||
throw new Error('analysisModel is required')
|
||||
}
|
||||
|
||||
const prompt = buildPrompt({
|
||||
promptId: PROMPT_IDS.NP_AI_STORY_EXPAND,
|
||||
locale: job.data.locale,
|
||||
variables: {
|
||||
input: promptInput,
|
||||
},
|
||||
})
|
||||
|
||||
await reportTaskProgress(job, 25, {
|
||||
stage: 'ai_story_expand_prepare',
|
||||
stageLabel: '准备故事扩写参数',
|
||||
displayMode: 'loading',
|
||||
})
|
||||
await assertTaskActive(job, 'ai_story_expand_prepare')
|
||||
|
||||
const streamContext = createWorkerLLMStreamContext(job, 'ai_story_expand')
|
||||
const streamCallbacks = createWorkerLLMStreamCallbacks(job, streamContext)
|
||||
|
||||
const completion = await withInternalLLMStreamCallbacks(
|
||||
streamCallbacks,
|
||||
async () =>
|
||||
await executeAiTextStep({
|
||||
userId: job.data.userId,
|
||||
model: analysisModel,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
temperature: 0.7,
|
||||
projectId: job.data.projectId || 'home-ai-write',
|
||||
action: 'ai_story_expand',
|
||||
meta: {
|
||||
stepId: 'ai_story_expand',
|
||||
stepTitle: '故事扩写',
|
||||
stepIndex: 1,
|
||||
stepTotal: 1,
|
||||
},
|
||||
}),
|
||||
)
|
||||
await streamCallbacks.flush()
|
||||
await assertTaskActive(job, 'ai_story_expand_persist')
|
||||
|
||||
const expandedText = completion.text.trim()
|
||||
if (!expandedText) {
|
||||
throw new Error('AI story expand response is empty')
|
||||
}
|
||||
|
||||
await reportTaskProgress(job, 96, {
|
||||
stage: 'ai_story_expand_done',
|
||||
stageLabel: '故事扩写已完成',
|
||||
displayMode: 'loading',
|
||||
})
|
||||
|
||||
return {
|
||||
expandedText,
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import { handleStoryToScriptTask } from './handlers/story-to-script'
|
||||
import { handleScriptToStoryboardTask } from './handlers/script-to-storyboard'
|
||||
import { handleVoiceAnalyzeTask } from './handlers/voice-analyze'
|
||||
import { handleAssetHubAIDesignTask } from './handlers/asset-hub-ai-design'
|
||||
import { handleAiStoryExpandTask } from './handlers/ai-story-expand'
|
||||
import { handleClipsBuildTask } from './handlers/clips-build'
|
||||
import { handleAnalyzeNovelTask } from './handlers/analyze-novel'
|
||||
import { handleScreenplayConvertTask } from './handlers/screenplay-convert'
|
||||
@@ -663,6 +664,8 @@ async function processTextTask(job: Job<TaskJobData>) {
|
||||
return await handleVoiceAnalyzeTask(job)
|
||||
case TASK_TYPE.ANALYZE_NOVEL:
|
||||
return await handleAnalyzeNovelTask(job)
|
||||
case TASK_TYPE.AI_STORY_EXPAND:
|
||||
return await handleAiStoryExpandTask(job)
|
||||
case TASK_TYPE.CLIPS_BUILD:
|
||||
return await handleClipsBuildTask(job)
|
||||
case TASK_TYPE.SCREENPLAY_CONVERT:
|
||||
|
||||
Reference in New Issue
Block a user