Compare commits

..

5 Commits

Author SHA1 Message Date
peanut 9b496bb42a fix: 撤回 typewriter 改动,novel 改用直接累加 delta 保证流式输出
之前用 useTypewriterStream.writer.push + 同步读 visibleText 有时序问题:
writer 的 visibleText 由内部 setInterval 异步推进,push 后立即同步读得到空串,
气泡一直空白。

撤回 typewriter 改动,恢复 novel_delta 直接 lastNovel.content += delta,
SSE 本身就是流式推送,每次到达即显示。

保留 confirmOutline / modifyOutline 的 pendingNextResponse = true 修复(loading)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 20:43:11 +08:00
peanut 6afec090e4 fix: 大纲确认/修改时显示 loading + novel 消息使用 typewriter 逐字流式输出
- confirmOutline / modifyOutline 中设置 pendingNextResponse.value = true
  解决大纲确认/修改后等待响应期间无 loading 的问题
- novel_start 时为 novel 消息创建独立 typewriter writer 实例(novelWriters Map)
- novel_delta 时通过 writer.push() 推送增量,逐字渲染 visibleText
- novel_done 时 finish writer 并同步最终文本,清理 writer 实例

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 20:33:52 +08:00
peanut 7b53821cec fix: 澄清卡片 allow_custom 时允许纯自定义输入提交
- canSubmit 逻辑重构:纯文本卡片只需 customText 非空
- 有选项卡片:满足最小选择数,或(allow_custom 且 customText 非空)即可提交
- 修复其他,请简单描述输入自定义内容后提交按钮灰显的问题

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 20:25:24 +08:00
peanut 9a215a89e9 fix: 新生成短篇剧本刷新到历史列表 + 分类标签从实际数据动态生成
- novel_done 事件处理中调用 store.fetchScripts() 刷新剧本列表
- typeTabs 从硬编码改为 computed,根据实际 length 值动态生成
- activeType 默认值从 'long' 改为 'all'(显示全部)
- 过滤逻辑改为精确匹配 length 字段
- 移除无效的 '风格' 硬编码分类标签

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 08:39:14 +08:00
peanut 1f5ad3abe2 fix: 大纲修改意见输入框改 textarea,增高并完整显示 placeholder
- input 改为 textarea + auto-height,支持多行输入
- min-height 64rpx 保证 placeholder 完整显示(原来被压扁截断)
- max-height 120rpx 限制约 2 行,超出滚动显示最新底部内容
- line-height 1.5 比例协调,改动幅度小

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 23:52:46 +08:00
3 changed files with 36 additions and 15 deletions
@@ -63,10 +63,14 @@ const isSingle = computed(() => props.card.card_type === 'single_select')
const isMulti = computed(() => props.card.card_type === 'multi_select' || props.card.card_type === 'mixed')
const canSubmit = computed(() => {
// 纯文本输入卡片:只需 customText 非空
if (isTextInput.value) return customText.value.trim().length > 0
// 有选项的卡片:满足最小选择数,或(允许自定义且 customText 非空)
const minSel = props.card.min_selections || 1
if (selectedValues.value.length < minSel) return false
if (isTextInput.value && !customText.value.trim()) return false
return true
if (selectedValues.value.length >= minSel) return true
if (props.card.allow_custom && customText.value.trim()) return true
return false
})
function isSelected(value) {
@@ -152,7 +152,7 @@ import { useAppStore } from '../../stores/app.js'
import { toggleFavoriteScript, checkFavoriteScript } from '../../services/epicScript.js'
const store = useAppStore()
const activeType = ref('long')
const activeType = ref('all')
const activeStatus = ref('all')
const keyword = ref('')
const sortMode = ref('updated')
@@ -162,11 +162,18 @@ const activeMenuId = ref('')
const deleteTarget = ref(null)
const deletingScript = ref(false)
const typeTabs = [
{ label: '篇', value: 'long' },
{ label: '短篇', value: 'short' },
{ label: '风格', value: 'style' }
]
// length 字段值 → 显示标签映射
const LENGTH_LABELS = { short: '短篇', medium: '篇', long: '长篇' }
// 从实际剧本数据中提取分类标签,禁止硬编码
const typeTabs = computed(() => {
const lengths = new Set(scripts.value.map(s => s.length || 'medium'))
const tabs = [{ label: '全部', value: 'all' }]
lengths.forEach(l => {
tabs.push({ label: LENGTH_LABELS[l] || l, value: l })
})
return tabs
})
const statusFilters = [
{ label: '全部', value: 'all' },
@@ -187,8 +194,8 @@ const visibleScripts = computed(() => {
}
if (activeStatus.value === 'favorite') return isFavorite(script)
if (activeStatus.value !== 'all' && status !== activeStatus.value) return false
if (activeType.value === 'short') return script.length === 'short'
if (activeType.value === 'long') return script.length !== 'short'
// 精确匹配 length 字段,禁止硬编码分类
if (activeType.value !== 'all') return script.length === activeType.value
return true
})
return [...filtered].sort((a, b) => {
+14 -4
View File
@@ -157,7 +157,7 @@
</view>
</view>
<view v-if="!msg.confirmed" class="outline-actions">
<input v-model="msg.feedback" placeholder="如需修改请输入意见" class="outline-feedback" />
<textarea v-model="msg.feedback" placeholder="如需修改请输入意见" class="outline-feedback" auto-height />
<view class="outline-buttons">
<view class="btn-secondary" @click="modifyOutline(msg)">修改大纲</view>
<view class="btn-primary" @click="confirmOutline(msg)">确认大纲</view>
@@ -1711,7 +1711,7 @@ const handleShortNovelEvent = (event) => {
pendingNextResponse.value = false
break
case 'novel_delta': {
// 往最后一条 novel 消息加 delta
// 往最后一条 pending novel 消息直接累加 delta,保证流式显示
const lastNovel = [...resultMessages.value].reverse().find(m => m.kind === 'novel' && m.pending)
if (lastNovel) {
lastNovel.content += payload.delta || ''
@@ -1733,6 +1733,8 @@ const handleShortNovelEvent = (event) => {
generationStatus.value = 'idle'
pendingNextResponse.value = false
persistResultMessages()
// 从后端刷新剧本列表,确保新生成的短篇出现在历史中(非阻塞)
store.fetchScripts()
break
}
case 'error':
@@ -1773,6 +1775,7 @@ const submitClarification = (answer) => {
const confirmOutline = (msg) => {
if (msg) msg.confirmed = true
pendingNextResponse.value = true
addResultMessage({ role: 'user', kind: 'text', content: '确认大纲' })
currentStreamTask.value = followupStream({
sessionId: novelSessionId.value,
@@ -1790,6 +1793,7 @@ const modifyOutline = (msg) => {
return
}
if (msg) msg.confirmed = true
pendingNextResponse.value = true
addResultMessage({ role: 'user', kind: 'text', content: feedback })
currentStreamTask.value = followupStream({
sessionId: novelSessionId.value,
@@ -2285,11 +2289,14 @@ onUnmounted(() => {
.outline-feedback {
width: 100%;
padding: 20rpx;
min-height: 64rpx;
max-height: 120rpx;
padding: 16rpx 20rpx;
background: rgba(255, 255, 255, 0.05);
border-radius: 12rpx;
color: #ffffff;
font-size: 28rpx;
line-height: 1.5;
margin-bottom: 24rpx;
box-sizing: border-box;
}
@@ -3738,11 +3745,14 @@ onUnmounted(() => {
.outline-feedback {
width: 100%;
padding: 20rpx;
min-height: 64rpx;
max-height: 120rpx;
padding: 16rpx 20rpx;
background: rgba(255, 255, 255, 0.05);
border-radius: 12rpx;
color: #ffffff;
font-size: 28rpx;
line-height: 1.5;
margin-bottom: 16rpx;
box-sizing: border-box;
}