feat(mini-program): ScriptView 多阶段状态机改造
This commit is contained in:
@@ -86,6 +86,56 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="viewState === 'clarifying'" class="clarifying-view">
|
||||
<scroll-view class="clarifying-scroll" scroll-y>
|
||||
<view class="clarifying-content">
|
||||
<view class="clarifying-intro">
|
||||
<text class="intro-title">在开始创作前,我想再了解你一点</text>
|
||||
</view>
|
||||
<ClarificationCard
|
||||
v-if="clarificationCard"
|
||||
:card="clarificationCard"
|
||||
@submit="submitClarification"
|
||||
/>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="viewState === 'outlining'" class="outlining-view">
|
||||
<scroll-view class="outlining-scroll" scroll-y>
|
||||
<view class="outlining-content">
|
||||
<view v-if="novelOutline" class="outline-card">
|
||||
<view v-if="novelOutline.title" class="outline-title">
|
||||
<text>{{ novelOutline.title }}</text>
|
||||
</view>
|
||||
<view v-if="novelOutline.logline" class="outline-logline">
|
||||
<text>{{ novelOutline.logline }}</text>
|
||||
</view>
|
||||
<view v-if="Array.isArray(novelOutline.beats)" class="outline-beats">
|
||||
<view v-for="(beat, idx) in novelOutline.beats" :key="idx" class="beat-item">
|
||||
<text class="beat-order">{{ beat.order || idx + 1 }}</text>
|
||||
<view class="beat-content">
|
||||
<text class="beat-title">{{ beat.title || '' }}</text>
|
||||
<text v-if="beat.summary" class="beat-summary">{{ beat.summary }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="novelOutline.ending" class="outline-ending">
|
||||
<text class="ending-label">结局</text>
|
||||
<text class="ending-text">{{ novelOutline.ending }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="outline-actions">
|
||||
<input v-model="outlineFeedback" placeholder="如需修改请输入意见" class="outline-feedback" />
|
||||
<view class="outline-buttons">
|
||||
<view class="btn-secondary" @click="modifyOutline(outlineFeedback)">修改大纲</view>
|
||||
<view class="btn-primary" @click="confirmOutline">确认大纲</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="viewState === 'generating'" class="generation-view">
|
||||
<scroll-view
|
||||
class="generation-scroll"
|
||||
@@ -267,9 +317,12 @@ import { useTypewriterStream } from '../../composables/useTypewriterStream.js'
|
||||
import { transcribeAudio } from '../../services/asr.js'
|
||||
import { streamAiScene } from '../../services/aiRuntime.js'
|
||||
import MessageCard from '../../components/MessageCard.vue'
|
||||
import ClarificationCard from '../../components/ClarificationCard.vue'
|
||||
import {
|
||||
startNovelStream,
|
||||
followupStream
|
||||
} from '../../services/shortNovel.js'
|
||||
import {
|
||||
createScriptWithDialogue,
|
||||
streamScriptChat,
|
||||
listMessagesByConversation,
|
||||
listMessageVersions,
|
||||
switchVersion,
|
||||
@@ -326,6 +379,14 @@ const viewMode = ref('chat')
|
||||
const scriptId = ref('')
|
||||
const conversationId = ref('')
|
||||
const currentVersionMessageId = ref('')
|
||||
// 短篇小说 SSE 流式生成新增字段
|
||||
const novelSessionId = ref('')
|
||||
const novelFullText = ref('')
|
||||
const novelOutline = ref(null)
|
||||
const clarificationCard = ref(null)
|
||||
const currentStreamTask = ref(null)
|
||||
const answeringClarification = ref(false)
|
||||
const outlineFeedback = ref('')
|
||||
const messages = ref([])
|
||||
const versions = ref([])
|
||||
const chatInput = ref('')
|
||||
@@ -1576,6 +1637,11 @@ const runGeneration = async ({ prompt, displayText, source = 'text', saveTheme }
|
||||
generationStartedAt.value = Date.now()
|
||||
generating.value = true
|
||||
streamContent.value = ''
|
||||
novelFullText.value = ''
|
||||
novelOutline.value = null
|
||||
clarificationCard.value = null
|
||||
novelSessionId.value = ''
|
||||
outlineFeedback.value = ''
|
||||
generationScrollTarget.value = ''
|
||||
generationScrollTop.value = 0
|
||||
generationScrollAnchor.value = 'generation-stream-anchor-a'
|
||||
@@ -1587,45 +1653,17 @@ const runGeneration = async ({ prompt, displayText, source = 'text', saveTheme }
|
||||
keepGenerationAtBottom()
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
theme: text,
|
||||
style: style.value || 'career',
|
||||
length: 'medium',
|
||||
characterInfo: '',
|
||||
lifeEventsSummary: '',
|
||||
useSocialInsights: useSocialInsights.value
|
||||
}
|
||||
|
||||
const res = await createScriptWithDialogue(payload)
|
||||
const data = res.data
|
||||
|
||||
scriptId.value = data.scriptId
|
||||
conversationId.value = data.conversationId
|
||||
currentVersionMessageId.value = data.currentVersionMessageId
|
||||
|
||||
currentResult.value = {
|
||||
id: data.scriptId,
|
||||
title: data.title,
|
||||
theme: data.theme,
|
||||
style: data.style,
|
||||
length: data.length,
|
||||
plotIntro: data.plotIntro,
|
||||
plotTurning: data.plotTurning,
|
||||
plotClimax: data.plotClimax,
|
||||
plotEnding: data.plotEnding
|
||||
}
|
||||
|
||||
await loadMessages()
|
||||
viewMode.value = 'chat'
|
||||
viewState.value = 'result'
|
||||
|
||||
analytics.track('script_generate_success', {
|
||||
source,
|
||||
style: currentResult.value.style || '',
|
||||
length: currentResult.value.length || '',
|
||||
use_social_insights: useSocialInsights.value,
|
||||
duration_ms: Date.now() - generationStartedAt.value
|
||||
}, { eventType: 'script', pagePath })
|
||||
currentStreamTask.value = startNovelStream({
|
||||
query: text,
|
||||
onEvent: handleShortNovelEvent,
|
||||
onError: (msg) => {
|
||||
markGenerationFailed(msg)
|
||||
analytics.track('script_generate_fail', {
|
||||
source,
|
||||
error: msg
|
||||
}, { eventType: 'script', pagePath })
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
markGenerationFailed(error.message || '生成失败')
|
||||
analytics.track('script_generate_fail', {
|
||||
@@ -1634,10 +1672,82 @@ const runGeneration = async ({ prompt, displayText, source = 'text', saveTheme }
|
||||
}, { eventType: 'script', pagePath })
|
||||
} finally {
|
||||
generating.value = false
|
||||
generationStatus.value = 'idle'
|
||||
}
|
||||
}
|
||||
|
||||
const handleShortNovelEvent = (event) => {
|
||||
const { type, session_id, payload = {} } = event
|
||||
if (session_id) novelSessionId.value = session_id
|
||||
|
||||
switch (type) {
|
||||
case 'status':
|
||||
generationStatus.value = payload.stage || 'processing'
|
||||
break
|
||||
case 'clarification_card':
|
||||
clarificationCard.value = payload.card || null
|
||||
viewState.value = 'clarifying'
|
||||
break
|
||||
case 'outline_created':
|
||||
novelOutline.value = payload.outline || null
|
||||
viewState.value = 'outlining'
|
||||
break
|
||||
case 'novel_start':
|
||||
streamContent.value = ''
|
||||
novelFullText.value = ''
|
||||
viewState.value = 'novel-generating'
|
||||
break
|
||||
case 'novel_delta':
|
||||
streamContent.value += payload.delta || ''
|
||||
break
|
||||
case 'novel_done':
|
||||
novelFullText.value = payload.full_text || streamContent.value
|
||||
streamContent.value = novelFullText.value
|
||||
scriptId.value = payload.scriptId || ''
|
||||
conversationId.value = payload.conversationId || ''
|
||||
currentVersionMessageId.value = payload.currentVersionMessageId || ''
|
||||
viewState.value = 'result'
|
||||
viewMode.value = 'chat'
|
||||
break
|
||||
case 'error':
|
||||
markGenerationFailed(payload.message || '生成失败')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const submitClarification = (answer) => {
|
||||
if (answeringClarification.value) return
|
||||
answeringClarification.value = true
|
||||
currentStreamTask.value = followupStream({
|
||||
sessionId: novelSessionId.value,
|
||||
action: 'answer_clarification',
|
||||
payload: { answer },
|
||||
onEvent: handleShortNovelEvent,
|
||||
onError: (msg) => markGenerationFailed(msg)
|
||||
})
|
||||
setTimeout(() => { answeringClarification.value = false }, 200)
|
||||
}
|
||||
|
||||
const confirmOutline = () => {
|
||||
currentStreamTask.value = followupStream({
|
||||
sessionId: novelSessionId.value,
|
||||
action: 'confirm_outline',
|
||||
payload: null,
|
||||
onEvent: handleShortNovelEvent,
|
||||
onError: (msg) => markGenerationFailed(msg)
|
||||
})
|
||||
}
|
||||
|
||||
const modifyOutline = (feedback) => {
|
||||
if (!feedback || !feedback.trim()) return
|
||||
currentStreamTask.value = followupStream({
|
||||
sessionId: novelSessionId.value,
|
||||
action: 'modify_outline',
|
||||
payload: { feedback },
|
||||
onEvent: handleShortNovelEvent,
|
||||
onError: (msg) => markGenerationFailed(msg)
|
||||
})
|
||||
}
|
||||
|
||||
const submitWish = async (source = 'text') => {
|
||||
const text = wishText.value.trim()
|
||||
if (!text || generating.value) return
|
||||
@@ -1962,6 +2072,162 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.clarifying-view {
|
||||
min-height: 100vh;
|
||||
padding: 40rpx 32rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.clarifying-scroll {
|
||||
height: calc(100vh - 80rpx);
|
||||
}
|
||||
|
||||
.clarifying-content {
|
||||
padding: 40rpx 0;
|
||||
}
|
||||
|
||||
.clarifying-intro {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.intro-title {
|
||||
color: #ffffff;
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.outlining-view {
|
||||
min-height: 100vh;
|
||||
padding: 40rpx 32rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.outlining-scroll {
|
||||
height: calc(100vh - 200rpx);
|
||||
}
|
||||
|
||||
.outline-card {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx 28rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.outline-title {
|
||||
color: #ffffff;
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.outline-logline {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.outline-beats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.beat-item {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.beat-order {
|
||||
flex-shrink: 0;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
background: #087e8b;
|
||||
border-radius: 50%;
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.beat-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.beat-title {
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.beat-summary {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 26rpx;
|
||||
margin-top: 8rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.outline-ending {
|
||||
margin-top: 24rpx;
|
||||
padding-top: 24rpx;
|
||||
border-top: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.ending-label {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 24rpx;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.ending-text {
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.outline-actions {
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.outline-feedback {
|
||||
width: 100%;
|
||||
padding: 20rpx;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 12rpx;
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.outline-buttons {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 16rpx 32rpx;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 32rpx;
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
padding: 16rpx 32rpx;
|
||||
background: #087e8b;
|
||||
border-radius: 32rpx;
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.script-view {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
Reference in New Issue
Block a user