Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
19b8a59
docs(docs): 更新文档子模块引用
honlnk Jun 16, 2026
b87270f
refactor(layout): 用 Activity Bar 和分类面板替换文件树
honlnk Jun 17, 2026
8feb3a4
feat(project): Agent 支持运行中停止并即时反馈
honlnk Jun 17, 2026
39a0899
refactor(app): 设置入口改为模态框并增强密码输入框
honlnk Jun 17, 2026
4b16168
feat(app): 内容面板新增编辑模式并统一 Tooltip 交互
honlnk Jun 17, 2026
82320ed
fix(app): 内容面板三态控件 Tooltip 增加 300ms 显示延迟
honlnk Jun 17, 2026
49fde65
feat(app): 选中内容引用支持独立引用块展示与模型注入
honlnk Jun 18, 2026
0a2fd93
docs(docs): 同步 UI 重构 R1/R2/R3/R5 文档状态
honlnk Jun 18, 2026
92ea786
fix(project): 修复项目列表章节数显示为 0 并新增要素数展示
honlnk Jun 18, 2026
5f04c2e
feat(layout): 内容面板支持拖拽改宽并持久化宽度
honlnk Jun 18, 2026
936358b
feat(ui): 输入框支持 @场景 指令切换激活场景
honlnk Jun 19, 2026
8e6c432
docs(docs): 同步文档子模块引用
honlnk Jun 19, 2026
483fae7
feat(ui): 实现 /提取要素 斜杠指令与命令菜单
honlnk Jun 19, 2026
b881cd6
docs(docs): 同步 UI 重构完成文档状态
honlnk Jun 19, 2026
949d540
refactor(store): 文件变更改为工具层结构化输出并移除文本推导
honlnk Jun 19, 2026
a6962bc
docs(docs): 同步 Agent 控制能力 Step 1 落地状态
honlnk Jun 19, 2026
0707809
feat(project): 写工具接入写入前确认与 diff 预览
honlnk Jun 19, 2026
b0651e7
refactor(store): 清理旧确认流程死代码
honlnk Jun 19, 2026
924a514
docs(docs): 同步 Agent 控制能力 Step 2/3 落地状态
honlnk Jun 19, 2026
e774d2c
feat(project): 用户即时工具约束支持指令解析与执行层强制
honlnk Jun 19, 2026
71c2d42
fix(project): 同会话内修改 system prompt 或场景后自动刷新
honlnk Jun 19, 2026
2dcaf25
feat(project): 实现对话历史会话的保存切换新建删除重命名
honlnk Jun 19, 2026
6a4a8e2
refactor(store): 优化会话列表刷新与删除切换的刷新开销
honlnk Jun 19, 2026
31dc592
fix(ui): 修复会话重命名输入框的重复提交
honlnk Jun 19, 2026
5b2a5f0
docs(docs): 更新 docs 子模块同步 Agent 控制能力 Step 5/6 状态
honlnk Jun 19, 2026
c575d46
feat(project): 补齐只读不写与只操作当前文件工具约束
honlnk Jun 20, 2026
53de64c
perf(project): 优化会话缓存 LRU 与历史列表扫描开销
honlnk Jun 20, 2026
1ee44d6
feat(ui): 优化输入指令弹层与要素提取详情展示
honlnk Jun 20, 2026
91ac1ce
feat(project): RAG 召回链路升级为 Orama 向量检索
honlnk Jun 20, 2026
9a0bdf0
docs(docs): 更新 docs 子模块修正 stopRun 过时描述
honlnk Jun 20, 2026
51fb981
feat(project): RAG 索引状态栏与 contentHash 增量重建
honlnk Jun 20, 2026
a58e6d5
docs(docs): 更新 docs 子模块同步 RAG 索引状态栏落地
honlnk Jun 20, 2026
f10dd02
docs(docs): 合并 Orama 与状态栏文档分支到 docs 子模块
honlnk Jun 20, 2026
e21f883
feat(project): 状态栏接入 runStatus 与场景提示词
honlnk Jun 22, 2026
f980e78
refactor(app): 删除无引用的 SceneChip 组件
honlnk Jun 22, 2026
a730025
feat(project): 引入 prompts/NovAI.md 项目总览与 /生成项目记忆 命令
honlnk Jun 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion INTERFACE.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ function resolvePreferredPath(changes: ChangedFileView[]) {
- `changedFiles` 仍是 service 层推导,不是工具层结构化输出。
- Agent 会话还没有持久化。
- `RagSearch` 已作为正式 Agent 工具接入,但真实创作任务中的触发率和召回质量仍需验证。
- 要素提取已能规则型提取并写入 `elements/`,但还不是 LLM 结构化提取,也缺少用户确认、去重合并和覆盖策略。
- 要素提取已收敛为 LLM 唯一路径(不再有正则降级)并写入 `elements/`,但还缺少用户确认、去重合并和覆盖策略。
- 旧的 `pendingFileChange / confirmPendingFileChange` 确认流程代码仍有残留,但当前主工作流没有接入。

## 协作规则
Expand Down
6 changes: 0 additions & 6 deletions packages/app/src/app/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { createRouter, createWebHistory } from 'vue-router'

import HomeView from '../views/HomeView.vue'
import ProjectView from '../views/ProjectView.vue'
import SettingsView from '../views/SettingsView.vue'

export const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
Expand All @@ -17,10 +16,5 @@ export const router = createRouter({
name: 'project',
component: ProjectView,
},
{
path: '/project/:id/settings',
name: 'settings',
component: SettingsView,
},
],
})
124 changes: 124 additions & 0 deletions packages/app/src/components/category/ChapterList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import type { ProjectFileNodeView } from '@novai/core/services/types'
import { pickDirectoryChildren } from '../../utils/file-tree'

const props = defineProps<{
files: ProjectFileNodeView[]
activeFilePath?: string
}>()

const emit = defineEmits<{
selectFile: [path: string]
}>()

/**
* chapters/ 目录的直接子项(保持层级,可能含子目录「卷/部」+ 章节文件)。
* 章节页本身展示这个平铺列表;子目录通过展开/折叠渲染其子项。
*/
const chapterNodes = computed(() => pickDirectoryChildren(props.files, 'chapters'))

const hasChapters = computed(() => chapterNodes.value.length > 0)

/** 每个目录节点的展开状态,key = 目录 path */
const expandedDirs = ref<Set<string>>(new Set())

function toggleDir(path: string) {
if (expandedDirs.value.has(path)) {
expandedDirs.value.delete(path)
} else {
expandedDirs.value.add(path)
}
}

function isDirExpanded(path: string) {
return expandedDirs.value.has(path)
}
</script>

<template>
<div class="flex h-full flex-col">
<div class="px-4 py-3">
<h2 class="text-sm font-semibold text-white">章节</h2>
</div>

<div class="flex-1 overflow-y-auto px-2 pb-2">
<!-- 空状态:chapters/ 目录为空或不存在 -->
<div v-if="!hasChapters" class="px-2 py-8 text-center">
<p class="mb-1 text-sm text-gray-400">还没有章节</p>
<p class="text-xs text-gray-500">在对话框输入指令,让 AI 开始第一章吧</p>
</div>

<!-- 章节列表:平铺渲染 chapters/ 直接子项 -->
<ul v-else class="space-y-0.5">
<li v-for="node in chapterNodes" :key="node.path">
<!-- 目录节点(卷/部):可折叠的分组标题 -->
<button
v-if="node.kind === 'directory'"
class="flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left text-sm text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-200"
@click="toggleDir(node.path)"
>
<svg
:class="['h-3.5 w-3.5 shrink-0 text-gray-500 transition-transform', isDirExpanded(node.path) ? 'rotate-90' : '']"
fill="none" stroke="currentColor" viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
<svg class="h-4 w-4 shrink-0 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
v-if="isDirExpanded(node.path)"
stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M5 19a2 2 0 01-2-2V7a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1M5 19h14a2 2 0 002-2v-5a2 2 0 00-2-2H9a2 2 0 00-2 2v5a2 2 0 01-2 2z"
/>
<path
v-else
stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"
/>
</svg>
<span class="flex-1 truncate">{{ node.name }}</span>
<span class="shrink-0 text-xs text-gray-500">{{ node.children?.length ?? 0 }}</span>
</button>

<!-- 目录展开后的子项 -->
<ul
v-if="node.kind === 'directory' && isDirExpanded(node.path) && node.children?.length"
class="mb-1 ml-3 border-l border-white/10 pl-2"
>
<li v-for="child in node.children" :key="child.path">
<button
v-if="child.kind === 'file'"
:class="[
'flex w-full items-center rounded-md px-2 py-1 text-left text-xs transition-colors',
child.path === activeFilePath
? 'bg-white/15 text-white'
: 'text-gray-400 hover:bg-white/10 hover:text-gray-200',
]"
@click="emit('selectFile', child.path)"
>
<span class="truncate">{{ child.name }}</span>
</button>
</li>
</ul>

<!-- 文件节点(章节):直接渲染 -->
<button
v-if="node.kind === 'file'"
:class="[
'flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left text-sm transition-colors',
node.path === activeFilePath
? 'bg-white/15 text-white'
: 'text-gray-400 hover:bg-white/10 hover:text-gray-200',
]"
@click="emit('selectFile', node.path)"
>
<svg class="h-4 w-4 shrink-0 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<span class="truncate">{{ node.name }}</span>
</button>
</li>
</ul>
</div>
</div>
</template>
163 changes: 163 additions & 0 deletions packages/app/src/components/category/ConversationList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<script setup lang="ts">
import { ref } from 'vue'
import type { ChatSessionSummaryView } from '@novai/core/services/types'

defineProps<{
sessions: ChatSessionSummaryView[]
activeSessionId: string | null
}>()

const emit = defineEmits<{
select: [sessionId: string]
create: []
rename: [sessionId: string, title: string]
remove: [sessionId: string]
}>()

/** 当前正在内联重命名的 sessionId;null 表示无 */
const renamingId = ref<string | null>(null)
const renameDraft = ref('')

function startRename(session: ChatSessionSummaryView) {
renamingId.value = session.sessionId
renameDraft.value = session.title
}

function commitRename(sessionId: string) {
// 防重入:Enter 会先触发 keydown 提交,随即 input 卸载又触发 blur。
// 第一次提交后 renamingId 已置 null,第二次进入直接返回,避免重复 emit。
if (renamingId.value === null) {
return
}

const title = renameDraft.value.trim()
renamingId.value = null
if (title) {
emit('rename', sessionId, title)
}
}

function cancelRename() {
renamingId.value = null
}

function confirmRemove(session: ChatSessionSummaryView) {
// 删除不可撤销,弹原生 confirm 二次确认
if (window.confirm(`确定删除对话「${session.title}」吗?此操作不可撤销。`)) {
emit('remove', session.sessionId)
}
}

/** 相对时间:今天显示 HH:mm,今年显示 MM-DD,更早显示完整日期 */
function formatRelativeTime(iso: string): string {
const date = new Date(iso)
const now = new Date()
const sameDay = date.toDateString() === now.toDateString()
if (sameDay) {
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
}
const sameYear = date.getFullYear() === now.getFullYear()
if (sameYear) {
return `${date.getMonth() + 1}月${date.getDate()}日`
}
return date.toLocaleDateString('zh-CN')
}
</script>

<template>
<div class="flex h-full flex-col">
<div class="flex items-center justify-between px-4 py-3">
<h2 class="text-sm font-semibold text-white">对话</h2>
<button
class="flex items-center gap-1 rounded-md px-2 py-1 text-xs text-gray-300 transition-colors hover:bg-white/10 hover:text-white"
title="开始新对话"
@click="emit('create')"
>
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
<span>新对话</span>
</button>
</div>

<div class="flex-1 overflow-y-auto px-2 pb-2">
<!-- 空状态 -->
<div v-if="sessions.length === 0" class="px-2 py-8 text-center">
<svg class="mx-auto mb-3 h-10 w-10 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
</svg>
<p class="mb-1 text-sm text-gray-400">还没有对话</p>
<p class="text-xs text-gray-500">点击「新对话」开始创作</p>
</div>

<!-- 会话列表 -->
<ul v-else class="space-y-0.5">
<li v-for="session in sessions" :key="session.sessionId" class="group relative">
<!-- 重命名态:内联输入框 -->
<div
v-if="renamingId === session.sessionId"
class="flex items-center rounded-md bg-white/10 px-2 py-1.5"
>
<input
v-model="renameDraft"
class="min-w-0 flex-1 rounded bg-transparent text-sm text-white outline-none placeholder:text-gray-500"
placeholder="会话标题"
@keydown.enter="commitRename(session.sessionId)"
@keydown.esc="cancelRename"
@blur="commitRename(session.sessionId)"
>
</div>

<!-- 展示态:会话卡片 -->
<button
v-else
:class="[
'flex w-full items-start gap-2 rounded-md px-2 py-2 text-left transition-colors',
session.sessionId === activeSessionId
? 'bg-white/15 text-white'
: 'text-gray-300 hover:bg-white/10 hover:text-white',
]"
@click="emit('select', session.sessionId)"
>
<svg class="mt-0.5 h-4 w-4 shrink-0 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2v10z" />
</svg>
<div class="min-w-0 flex-1">
<p class="truncate text-sm">{{ session.title }}</p>
<p class="mt-0.5 flex items-center gap-2 text-xs text-gray-500">
<span>{{ formatRelativeTime(session.updatedAt) }}</span>
<span>·</span>
<span>{{ session.messageCount }} 条</span>
</p>
</div>
</button>

<!-- hover 操作按钮(重命名/删除) -->
<div
v-if="renamingId !== session.sessionId"
class="absolute right-1 top-1.5 flex items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100"
>
<button
class="rounded p-1 text-gray-400 hover:bg-white/15 hover:text-white"
title="重命名"
@click.stop="startRename(session)"
>
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button
class="rounded p-1 text-gray-400 hover:bg-red-500/20 hover:text-red-300"
title="删除"
@click.stop="confirmRemove(session)"
>
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</li>
</ul>
</div>
</div>
</template>
Loading
Loading