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>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user