Files
ai-novel-studio/app/pages/projects/[id]/memory.vue
T
jocay 3c9b65d982 AI 小说工坊:主编 + 作家多模型协作写作应用
- 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>
2026-10-18 02:04:40 +08:00

127 lines
5.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>