3c9b65d982
- Nuxt 4 + Nuxt UI 4 + AI SDK 7 + Drizzle/SQLite - 主编规划大纲与章节简报、选派作家;作家流式写入章节文件并实时推送前端 - 逐章确认模式(默认)与自动模式;摘要与故事记忆 - 酒馆角色卡(PNG/JSON,V1-V3)与世界书导入 - 自定义最高优先级系统提示词、审稿/修改/多版本、任务日志与提示词检视 - 模拟 LLM 服务与端到端测试脚本 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
127 lines
5.6 KiB
Vue
127 lines
5.6 KiB
Vue
<script setup lang="ts">
|
||
import type { ProjectMemory } from '#shared/types'
|
||
|
||
const store = useProjectStore()
|
||
const api = useApi()
|
||
const { chapters } = store
|
||
|
||
const mem = reactive<ProjectMemory>(structuredClone(toRaw(store.project.value!.memory)))
|
||
const snapshot = ref(JSON.stringify(mem))
|
||
const dirty = computed(() => JSON.stringify(mem) !== snapshot.value)
|
||
|
||
// 确认章节后服务端会更新记忆;没有本地修改时同步最新数据
|
||
watch(() => store.project.value?.memory, (m) => {
|
||
if (m && !dirty.value) {
|
||
Object.assign(mem, structuredClone(toRaw(m)))
|
||
snapshot.value = JSON.stringify(mem)
|
||
}
|
||
}, { deep: true })
|
||
|
||
async function save() {
|
||
await store.saveProject({ memory: JSON.parse(JSON.stringify(mem)) })
|
||
snapshot.value = JSON.stringify(mem)
|
||
api.toast.add({ title: '故事记忆已保存', color: 'success' })
|
||
}
|
||
|
||
const openForeshadowing = computed(() => mem.foreshadowing.filter(f => f.status === 'open'))
|
||
const resolvedForeshadowing = computed(() => mem.foreshadowing.filter(f => f.status === 'resolved'))
|
||
|
||
function addState() { mem.characterStates.push({ name: '', state: '' }) }
|
||
function addForeshadowing() { mem.foreshadowing.push({ text: '', status: 'open', chapter: chapters.value.filter(c => c.status === 'approved').at(-1)?.index ?? 0 }) }
|
||
function removeItem<T>(arr: T[], item: T) {
|
||
const i = arr.indexOf(item)
|
||
if (i >= 0) arr.splice(i, 1)
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<UDashboardPanel id="project-memory">
|
||
<template #header>
|
||
<UDashboardNavbar title="故事记忆">
|
||
<template #right>
|
||
<span v-if="dirty" class="text-xs text-warning">有未保存的修改</span>
|
||
<UButton icon="i-lucide-save" label="保存" :disabled="!dirty" @click="save" />
|
||
</template>
|
||
</UDashboardNavbar>
|
||
</template>
|
||
<template #body>
|
||
<div class="max-w-5xl mx-auto w-full space-y-8 pb-16">
|
||
<p class="text-sm text-muted">
|
||
每章确认后,摘要员会自动更新这里的内容。主编和作家写作时会参考这些信息来保持人物状态与情节的连贯。你可以随时手动修正。
|
||
</p>
|
||
|
||
<section class="space-y-3">
|
||
<div class="flex items-center gap-2">
|
||
<h2 class="font-semibold">
|
||
人物当前状态
|
||
</h2>
|
||
<UButton icon="i-lucide-plus" size="xs" variant="soft" label="添加" class="ms-auto" @click="addState" />
|
||
</div>
|
||
<div v-if="!mem.characterStates.length" class="text-sm text-muted">
|
||
暂无
|
||
</div>
|
||
<div v-for="(s, i) in mem.characterStates" :key="i" class="flex gap-2 items-start">
|
||
<UInput v-model="s.name" placeholder="人物" class="w-32 shrink-0" />
|
||
<UTextarea v-model="s.state" autoresize :rows="1" class="flex-1" />
|
||
<UButton icon="i-lucide-x" size="sm" color="neutral" variant="ghost" @click="removeItem(mem.characterStates, s)" />
|
||
</div>
|
||
</section>
|
||
|
||
<section class="space-y-3">
|
||
<div class="flex items-center gap-2">
|
||
<h2 class="font-semibold">
|
||
伏笔
|
||
</h2>
|
||
<UBadge variant="subtle" size="sm">
|
||
未回收 {{ openForeshadowing.length }}
|
||
</UBadge>
|
||
<UBadge variant="subtle" color="success" size="sm">
|
||
已回收 {{ resolvedForeshadowing.length }}
|
||
</UBadge>
|
||
<UButton icon="i-lucide-plus" size="xs" variant="soft" label="添加" class="ms-auto" @click="addForeshadowing" />
|
||
</div>
|
||
<div v-for="(f, i) in mem.foreshadowing" :key="i" class="flex gap-2 items-center">
|
||
<UBadge :color="f.status === 'open' ? 'warning' : 'success'" variant="subtle" class="shrink-0 cursor-pointer" @click="f.status = f.status === 'open' ? 'resolved' : 'open'">
|
||
{{ f.status === 'open' ? '未回收' : `已回收${f.resolvedChapter ? `·第${f.resolvedChapter}章` : ''}` }}
|
||
</UBadge>
|
||
<span class="text-xs text-muted shrink-0 w-16">第{{ f.chapter }}章</span>
|
||
<UInput v-model="f.text" class="flex-1" />
|
||
<UButton icon="i-lucide-x" size="sm" color="neutral" variant="ghost" @click="removeItem(mem.foreshadowing, f)" />
|
||
</div>
|
||
</section>
|
||
|
||
<section class="space-y-3">
|
||
<h2 class="font-semibold">
|
||
时间线
|
||
</h2>
|
||
<div v-if="!mem.timeline.length" class="text-sm text-muted">
|
||
暂无
|
||
</div>
|
||
<ol class="relative border-s border-default ms-2 space-y-2">
|
||
<li v-for="(t, i) in mem.timeline" :key="i" class="ms-4 flex gap-2 items-center">
|
||
<span class="absolute -start-1.5 size-3 rounded-full bg-primary/70" />
|
||
<span class="text-xs text-muted w-14 shrink-0">第{{ t.chapter }}章</span>
|
||
<UInput v-model="t.event" variant="ghost" class="flex-1" />
|
||
<UButton icon="i-lucide-x" size="xs" color="neutral" variant="ghost" @click="removeItem(mem.timeline, t)" />
|
||
</li>
|
||
</ol>
|
||
</section>
|
||
|
||
<section class="space-y-3">
|
||
<h2 class="font-semibold">
|
||
章节摘要一览
|
||
</h2>
|
||
<div v-for="c in chapters.filter(c => c.status === 'approved')" :key="c.id" class="text-sm">
|
||
<NuxtLink :to="`/projects/${store.projectId}?c=${c.id}`" class="font-medium hover:text-primary">
|
||
第{{ c.index }}章 {{ c.title }}
|
||
</NuxtLink>
|
||
</div>
|
||
<p class="text-xs text-muted">
|
||
点击章节可在写作台的「审稿与摘要」中查看和编辑摘要。
|
||
</p>
|
||
</section>
|
||
</div>
|
||
</template>
|
||
</UDashboardPanel>
|
||
</template>
|