feat(mini-program): ScriptView 多阶段状态机改造
This commit is contained in:
@@ -86,6 +86,56 @@
|
|||||||
</view>
|
</view>
|
||||||
</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">
|
<view v-else-if="viewState === 'generating'" class="generation-view">
|
||||||
<scroll-view
|
<scroll-view
|
||||||
class="generation-scroll"
|
class="generation-scroll"
|
||||||
@@ -267,9 +317,12 @@ import { useTypewriterStream } from '../../composables/useTypewriterStream.js'
|
|||||||
import { transcribeAudio } from '../../services/asr.js'
|
import { transcribeAudio } from '../../services/asr.js'
|
||||||
import { streamAiScene } from '../../services/aiRuntime.js'
|
import { streamAiScene } from '../../services/aiRuntime.js'
|
||||||
import MessageCard from '../../components/MessageCard.vue'
|
import MessageCard from '../../components/MessageCard.vue'
|
||||||
|
import ClarificationCard from '../../components/ClarificationCard.vue'
|
||||||
|
import {
|
||||||
|
startNovelStream,
|
||||||
|
followupStream
|
||||||
|
} from '../../services/shortNovel.js'
|
||||||
import {
|
import {
|
||||||
createScriptWithDialogue,
|
|
||||||
streamScriptChat,
|
|
||||||
listMessagesByConversation,
|
listMessagesByConversation,
|
||||||
listMessageVersions,
|
listMessageVersions,
|
||||||
switchVersion,
|
switchVersion,
|
||||||
@@ -326,6 +379,14 @@ const viewMode = ref('chat')
|
|||||||
const scriptId = ref('')
|
const scriptId = ref('')
|
||||||
const conversationId = ref('')
|
const conversationId = ref('')
|
||||||
const currentVersionMessageId = 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 messages = ref([])
|
||||||
const versions = ref([])
|
const versions = ref([])
|
||||||
const chatInput = ref('')
|
const chatInput = ref('')
|
||||||
@@ -1576,6 +1637,11 @@ const runGeneration = async ({ prompt, displayText, source = 'text', saveTheme }
|
|||||||
generationStartedAt.value = Date.now()
|
generationStartedAt.value = Date.now()
|
||||||
generating.value = true
|
generating.value = true
|
||||||
streamContent.value = ''
|
streamContent.value = ''
|
||||||
|
novelFullText.value = ''
|
||||||
|
novelOutline.value = null
|
||||||
|
clarificationCard.value = null
|
||||||
|
novelSessionId.value = ''
|
||||||
|
outlineFeedback.value = ''
|
||||||
generationScrollTarget.value = ''
|
generationScrollTarget.value = ''
|
||||||
generationScrollTop.value = 0
|
generationScrollTop.value = 0
|
||||||
generationScrollAnchor.value = 'generation-stream-anchor-a'
|
generationScrollAnchor.value = 'generation-stream-anchor-a'
|
||||||
@@ -1587,45 +1653,17 @@ const runGeneration = async ({ prompt, displayText, source = 'text', saveTheme }
|
|||||||
keepGenerationAtBottom()
|
keepGenerationAtBottom()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = {
|
currentStreamTask.value = startNovelStream({
|
||||||
theme: text,
|
query: text,
|
||||||
style: style.value || 'career',
|
onEvent: handleShortNovelEvent,
|
||||||
length: 'medium',
|
onError: (msg) => {
|
||||||
characterInfo: '',
|
markGenerationFailed(msg)
|
||||||
lifeEventsSummary: '',
|
analytics.track('script_generate_fail', {
|
||||||
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,
|
source,
|
||||||
style: currentResult.value.style || '',
|
error: msg
|
||||||
length: currentResult.value.length || '',
|
|
||||||
use_social_insights: useSocialInsights.value,
|
|
||||||
duration_ms: Date.now() - generationStartedAt.value
|
|
||||||
}, { eventType: 'script', pagePath })
|
}, { eventType: 'script', pagePath })
|
||||||
|
}
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
markGenerationFailed(error.message || '生成失败')
|
markGenerationFailed(error.message || '生成失败')
|
||||||
analytics.track('script_generate_fail', {
|
analytics.track('script_generate_fail', {
|
||||||
@@ -1634,10 +1672,82 @@ const runGeneration = async ({ prompt, displayText, source = 'text', saveTheme }
|
|||||||
}, { eventType: 'script', pagePath })
|
}, { eventType: 'script', pagePath })
|
||||||
} finally {
|
} finally {
|
||||||
generating.value = false
|
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 submitWish = async (source = 'text') => {
|
||||||
const text = wishText.value.trim()
|
const text = wishText.value.trim()
|
||||||
if (!text || generating.value) return
|
if (!text || generating.value) return
|
||||||
@@ -1962,6 +2072,162 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<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 {
|
.script-view {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
Reference in New Issue
Block a user