Stabilize knowledge uploads in the UI

Keep folder selection stable across refreshes, surface upload failures
more clearly, and add focused composable tests for the knowledge page.
This keeps newly uploaded files visible and makes MinerU dependency
errors easier to understand from the frontend.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-22 13:43:00 +08:00
parent 3ee825aa90
commit e3691b01bb
5 changed files with 2106 additions and 22 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,8 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vue-tsc -b && vite build", "build": "vue-tsc -b && vite build",
"preview": "vite preview" "preview": "vite preview",
"test": "vitest run"
}, },
"dependencies": { "dependencies": {
"@vueuse/core": "^14.2.1", "@vueuse/core": "^14.2.1",
@@ -22,9 +23,12 @@
"devDependencies": { "devDependencies": {
"@types/node": "^25.5.0", "@types/node": "^25.5.0",
"@vitejs/plugin-vue": "^6.0.5", "@vitejs/plugin-vue": "^6.0.5",
"@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.9.0", "@vue/tsconfig": "^0.9.0",
"jsdom": "^29.0.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^8.0.1", "vite": "^8.0.1",
"vitest": "^4.1.0",
"vue-tsc": "^2.2.12" "vue-tsc": "^2.2.12"
} }
} }

View File

@@ -0,0 +1,332 @@
import { AxiosError } from 'axios'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const storage = new Map<string, string>()
const routeQuery = { folder_id: undefined as string | undefined }
const routerReplace = vi.fn(async ({ query }: { query: Record<string, unknown> }) => {
routeQuery.folder_id = typeof query.folder_id === 'string' ? query.folder_id : undefined
})
const mocks = vi.hoisted(() => ({
folderGetTree: vi.fn(),
folderCreate: vi.fn(),
documentList: vi.fn(),
documentUpload: vi.fn(),
documentGetContent: vi.fn(),
documentGetChunks: vi.fn(),
documentUpdateChunk: vi.fn(),
}))
vi.mock('vue-router', () => ({
useRoute: () => ({ query: routeQuery }),
useRouter: () => ({ replace: routerReplace }),
}))
vi.mock('@/api/folder', () => ({
folderApi: {
getTree: mocks.folderGetTree,
create: mocks.folderCreate,
},
}))
vi.mock('@/api/document', () => ({
documentApi: {
list: mocks.documentList,
upload: mocks.documentUpload,
getContent: mocks.documentGetContent,
getChunks: mocks.documentGetChunks,
updateChunk: mocks.documentUpdateChunk,
},
}))
import { useKnowledgeView } from './useKnowledgeView'
describe('useKnowledgeView chunk editing', () => {
beforeEach(() => {
vi.clearAllMocks()
routeQuery.folder_id = undefined
routerReplace.mockClear()
storage.clear()
vi.stubGlobal('localStorage', {
getItem: vi.fn((key: string) => storage.get(key) ?? null),
setItem: vi.fn((key: string, value: string) => {
storage.set(key, value)
}),
removeItem: vi.fn((key: string) => {
storage.delete(key)
}),
})
mocks.folderGetTree.mockResolvedValue({ data: [] })
mocks.documentList.mockResolvedValue({ data: [] })
mocks.documentUpload.mockResolvedValue({
data: {
id: 'doc-2',
title: 'New doc',
chunk_count: 1,
status: '上传成功,正在索引...',
},
})
mocks.documentGetContent.mockResolvedValue({ data: { content: 'document preview' } })
mocks.documentGetChunks.mockResolvedValue({
data: [
{
id: 'chunk-1',
chunk_index: 0,
content: 'original chunk content',
metadata_: '{"content_type":"paragraph","section_title":"Intro"}',
},
],
})
mocks.documentUpdateChunk.mockResolvedValue({
data: {
id: 'chunk-1',
chunk_index: 0,
content: 'edited chunk content',
metadata_: '{"content_type":"paragraph","section_title":"Intro"}',
},
})
})
it('loads chunks when opening a document and updates local chunk state after save', async () => {
const view = useKnowledgeView()
const document = {
id: 'doc-1',
title: 'Test doc',
filename: 'test.md',
file_type: 'md',
file_size: 128,
chunk_count: 1,
is_indexed: true,
created_at: '2026-03-22T00:00:00Z',
}
await view.openDocument(document)
expect(mocks.documentGetChunks).toHaveBeenCalledWith('doc-1')
expect(view.activeDocumentChunks.value).toHaveLength(1)
expect(view.activeDocumentChunks.value[0].content).toBe('original chunk content')
view.startChunkEdit(view.activeDocumentChunks.value[0])
view.chunkDrafts.value['chunk-1'] = 'edited chunk content'
await view.saveChunkEdit('chunk-1')
expect(mocks.documentUpdateChunk).toHaveBeenCalledWith('doc-1', 'chunk-1', { content: 'edited chunk content' })
expect(view.activeDocumentChunks.value[0].content).toBe('edited chunk content')
expect(view.chunkEditError.value['chunk-1']).toBe('')
})
it('shows a newly uploaded document immediately in the current folder list', async () => {
const view = useKnowledgeView()
const folder = {
id: 'folder-1',
name: '测试',
parent_id: null,
children: [],
created_at: '2026-03-22T00:00:00Z',
}
mocks.folderGetTree.mockResolvedValue({ data: [folder] })
mocks.documentList
.mockResolvedValueOnce({ data: [] })
.mockResolvedValueOnce({ data: [] })
const file = new File(['hello'], 'new-doc.md', { type: 'text/markdown' })
const event = { target: { files: [file], value: 'new-doc.md' } } as unknown as Event
await view.enterFolder(folder)
await view.handleUpload(event)
expect(view.documents.value).toHaveLength(1)
expect(view.documents.value[0].id).toBe('doc-2')
expect(view.documents.value[0].title).toBe('New doc')
expect(view.documents.value[0].filename).toBe('new-doc.md')
expect(view.documents.value[0].folder_id).toBe('folder-1')
expect(routeQuery.folder_id).toBe('folder-1')
expect(view.highlightedDocumentId.value).toBe('doc-2')
expect(view.uploadSuccess.value).toBe('已上传到 测试')
})
it('restores the selected folder and reloads its documents after refresh', async () => {
routeQuery.folder_id = 'folder-1'
storage.set('knowledge.currentFolderId', 'folder-1')
const folder = {
id: 'folder-1',
name: '测试',
parent_id: null,
children: [],
created_at: '2026-03-22T00:00:00Z',
}
const view = useKnowledgeView()
mocks.folderGetTree.mockResolvedValue({ data: [folder] })
mocks.documentList.mockResolvedValue({
data: [
{
id: 'doc-3',
title: 'Persisted doc',
filename: 'persisted.md',
file_type: 'md',
file_size: 512,
chunk_count: 2,
is_indexed: true,
folder_id: 'folder-1',
created_at: '2026-03-22T00:00:00Z',
},
],
})
await view.goToFolder('folder-1')
expect(view.currentFolderId.value).toBe('folder-1')
expect(mocks.documentList).toHaveBeenCalledWith('folder-1')
expect(view.documents.value).toHaveLength(1)
expect(view.documents.value[0].id).toBe('doc-3')
})
it('enters a newly created folder so refresh keeps uploaded documents visible', async () => {
const createdFolder = {
id: 'folder-new',
name: '新文件夹',
parent_id: null,
children: [],
created_at: '2026-03-22T00:00:00Z',
updated_at: '2026-03-22T00:00:00Z',
}
const uploadedDocument = {
id: 'doc-new',
title: 'Uploaded after create',
filename: 'uploaded.md',
file_type: 'md',
file_size: 256,
chunk_count: 1,
is_indexed: true,
folder_id: 'folder-new',
created_at: '2026-03-22T00:00:00Z',
}
mocks.folderCreate.mockResolvedValue({ data: createdFolder })
mocks.folderGetTree.mockResolvedValue({
data: [
{
id: 'folder-new',
name: '新文件夹',
parent_id: null,
children: [],
},
],
})
mocks.documentList
.mockResolvedValueOnce({ data: [] })
.mockResolvedValueOnce({ data: [uploadedDocument] })
.mockResolvedValue({ data: [uploadedDocument] })
mocks.documentUpload.mockResolvedValue({
data: {
id: 'doc-new',
title: 'Uploaded after create',
chunk_count: 1,
status: '上传成功,正在索引...',
ingestion_status: 'ready',
},
})
const view = useKnowledgeView()
view.newFolderName.value = '新文件夹'
await view.createFolder()
expect(view.currentFolderId.value).toBe('folder-new')
expect(routeQuery.folder_id).toBe('folder-new')
expect(storage.get('knowledge.currentFolderId')).toBe('folder-new')
const file = new File(['hello'], 'uploaded.md', { type: 'text/markdown' })
const event = { target: { files: [file], value: 'uploaded.md' } } as unknown as Event
await view.handleUpload(event)
expect(mocks.documentUpload).toHaveBeenCalledWith(file, 'folder-new')
expect(view.documents.value).toHaveLength(1)
expect(view.documents.value[0].folder_id).toBe('folder-new')
})
it('loads documents at the root view instead of clearing the list', async () => {
const view = useKnowledgeView()
mocks.documentList.mockResolvedValue({
data: [
{
id: 'doc-root',
title: 'Root doc',
filename: 'root.md',
file_type: 'md',
file_size: 128,
chunk_count: 1,
is_indexed: true,
folder_id: null,
created_at: '2026-03-22T00:00:00Z',
},
],
})
await view.goToFolder(null)
expect(mocks.documentList).toHaveBeenCalledWith(null)
expect(view.documents.value).toHaveLength(1)
expect(view.documents.value[0].id).toBe('doc-root')
})
it('resets a stale persisted folder before creating a new folder', async () => {
routeQuery.folder_id = 'stale-folder'
storage.set('knowledge.currentFolderId', 'stale-folder')
mocks.folderGetTree.mockResolvedValue({ data: [] })
mocks.folderCreate.mockResolvedValue({
data: {
id: 'folder-fresh',
name: '全新文件夹',
parent_id: null,
created_at: '2026-03-22T00:00:00Z',
updated_at: '2026-03-22T00:00:00Z',
},
})
const view = useKnowledgeView()
await view.goToFolder(null)
view.newFolderName.value = '全新文件夹'
await view.createFolder()
expect(mocks.folderCreate).toHaveBeenCalledWith({
name: '全新文件夹',
parent_id: null,
})
})
it('shows a clear upload message when mineru dependency is missing', async () => {
const folder = {
id: 'folder-1',
name: '测试',
parent_id: null,
children: [],
created_at: '2026-03-22T00:00:00Z',
}
mocks.folderGetTree.mockResolvedValue({ data: [folder] })
mocks.documentList.mockResolvedValue({ data: [] })
mocks.documentUpload.mockRejectedValue(new AxiosError(
'Request failed with status code 400',
'400',
undefined,
undefined,
{
data: { detail: 'PDF 解析依赖缺失: mineru' },
status: 400,
statusText: 'Bad Request',
headers: {},
config: {} as never,
},
))
const view = useKnowledgeView()
await view.enterFolder(folder)
const file = new File(['%PDF-1.4'], 'spec.pdf', { type: 'application/pdf' })
const event = { target: { files: [file], value: 'spec.pdf' } } as unknown as Event
await view.handleUpload(event)
expect(view.uploadError.value).toBe('PDF 解析依赖缺失: mineru')
})
})

View File

@@ -1,15 +1,34 @@
import { computed, onMounted, ref } from 'vue' import axios, { type AxiosError } from 'axios'
import { documentApi, type Document } from '@/api/document' import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { documentApi, type Document, type DocumentChunk } from '@/api/document'
import { folderApi, type FolderTree } from '@/api/folder' import { folderApi, type FolderTree } from '@/api/folder'
const KNOWLEDGE_FOLDER_STORAGE_KEY = 'knowledge.currentFolderId'
function getPersistedKnowledgeFolderId() {
return typeof window !== 'undefined' ? localStorage.getItem(KNOWLEDGE_FOLDER_STORAGE_KEY) : null
}
function normalizeFolderId(value: unknown) {
return typeof value === 'string' && value ? value : null
}
export function useKnowledgeView() { export function useKnowledgeView() {
const route = useRoute()
const router = useRouter()
const initialFolderId = normalizeFolderId(route.query.folder_id) ?? getPersistedKnowledgeFolderId()
const folders = ref<FolderTree[]>([]) const folders = ref<FolderTree[]>([])
const documents = ref<Document[]>([]) const documents = ref<Document[]>([])
const currentFolderId = ref<string | null>(null) const currentFolderId = ref<string | null>(initialFolderId)
const isUploading = ref(false) const isUploading = ref(false)
const isLoadingDocuments = ref(false) const isLoadingDocuments = ref(false)
const currentFolderName = ref('')
const uploadError = ref('') const uploadError = ref('')
const uploadSuccess = ref('')
const highlightedDocumentId = ref<string | null>(null)
const uploadInput = ref<HTMLInputElement | null>(null) const uploadInput = ref<HTMLInputElement | null>(null)
const statusPoller = ref<number | null>(null)
const showNewFolderDialog = ref(false) const showNewFolderDialog = ref(false)
const newFolderName = ref('') const newFolderName = ref('')
@@ -26,6 +45,12 @@ export function useKnowledgeView() {
const activeDocument = ref<Document | null>(null) const activeDocument = ref<Document | null>(null)
const activeDocumentContent = ref('') const activeDocumentContent = ref('')
const isLoadingDocumentContent = ref(false) const isLoadingDocumentContent = ref(false)
const activeDocumentChunks = ref<DocumentChunk[]>([])
const isLoadingDocumentChunks = ref(false)
const chunkDrafts = ref<Record<string, string>>({})
const chunkEditing = ref<Record<string, boolean>>({})
const chunkSaving = ref<Record<string, boolean>>({})
const chunkEditError = ref<Record<string, string>>({})
const folderMap = computed(() => { const folderMap = computed(() => {
const map = new Map<string, FolderTree>() const map = new Map<string, FolderTree>()
@@ -95,11 +120,6 @@ export function useKnowledgeView() {
} }
async function loadDocumentsByFolder(folderId: string | null) { async function loadDocumentsByFolder(folderId: string | null) {
if (!folderId) {
documents.value = []
return
}
isLoadingDocuments.value = true isLoadingDocuments.value = true
try { try {
const response = await documentApi.list(folderId) const response = await documentApi.list(folderId)
@@ -111,13 +131,34 @@ export function useKnowledgeView() {
} }
} }
async function persistCurrentFolder(folderId: string | null) {
if (folderId) {
localStorage.setItem(KNOWLEDGE_FOLDER_STORAGE_KEY, folderId)
} else {
localStorage.removeItem(KNOWLEDGE_FOLDER_STORAGE_KEY)
}
const nextQuery = { ...route.query }
if (folderId) {
nextQuery.folder_id = folderId
} else {
delete nextQuery.folder_id
}
await router.replace({ query: nextQuery })
}
async function enterFolder(folder: FolderTree) { async function enterFolder(folder: FolderTree) {
currentFolderId.value = folder.id currentFolderId.value = folder.id
currentFolderName.value = folder.name
await persistCurrentFolder(folder.id)
await loadDocumentsByFolder(folder.id) await loadDocumentsByFolder(folder.id)
} }
async function goToFolder(folderId: string | null) { async function goToFolder(folderId: string | null) {
currentFolderId.value = folderId currentFolderId.value = folderId
currentFolderName.value = folderId ? (folderMap.value.get(folderId)?.name ?? '') : ''
await persistCurrentFolder(folderId)
await loadDocumentsByFolder(folderId) await loadDocumentsByFolder(folderId)
} }
@@ -131,6 +172,44 @@ export function useKnowledgeView() {
uploadInput.value?.click() uploadInput.value?.click()
} }
function stopStatusPolling() {
if (statusPoller.value !== null) {
window.clearInterval(statusPoller.value)
statusPoller.value = null
}
}
function getStatusLabel(status?: Document['ingestion_status'], isIndexed?: boolean) {
if (status === 'failed') return 'FAILED'
if (status === 'warning') return 'WARNING'
if (status === 'ready' || isIndexed) return 'READY'
if (status === 'indexing') return 'INDEXING'
if (status === 'parsing') return 'PARSING'
return 'UPLOADED'
}
async function refreshActiveFolder() {
await loadDocumentsByFolder(currentFolderId.value)
}
function clearUploadFeedbackLater() {
window.setTimeout(() => {
uploadSuccess.value = ''
highlightedDocumentId.value = null
}, 4000)
}
function startStatusPolling() {
stopStatusPolling()
statusPoller.value = window.setInterval(async () => {
await refreshActiveFolder()
const hasPending = documents.value.some((doc) => !['ready', 'warning', 'failed'].includes(doc.ingestion_status ?? (doc.is_indexed ? 'ready' : 'uploaded')))
if (!hasPending) {
stopStatusPolling()
}
}, 2500)
}
async function handleUpload(event: Event) { async function handleUpload(event: Event) {
const target = event.target as HTMLInputElement const target = event.target as HTMLInputElement
const file = target.files?.[0] const file = target.files?.[0]
@@ -146,10 +225,49 @@ export function useKnowledgeView() {
} }
isUploading.value = true isUploading.value = true
uploadError.value = ''
uploadSuccess.value = ''
highlightedDocumentId.value = null
try { try {
await documentApi.upload(file, currentFolderId.value) const response = await documentApi.upload(file, currentFolderId.value)
await loadDocumentsByFolder(currentFolderId.value) const optimisticDocument: Document = {
id: response.data.id,
title: response.data.title,
filename: file.name,
file_type: file.name.split('.').pop()?.toLowerCase() ?? '',
file_size: file.size,
chunk_count: response.data.chunk_count,
is_indexed: false,
ingestion_status: response.data.ingestion_status ?? 'uploaded',
ingestion_error: response.data.ingestion_error ?? null,
indexed_at: response.data.indexed_at ?? null,
folder_id: currentFolderId.value,
created_at: new Date().toISOString(),
}
const hasDocument = documents.value.some((doc) => doc.id === optimisticDocument.id)
if (!hasDocument) {
documents.value = [optimisticDocument, ...documents.value]
}
await refreshActiveFolder()
if (!documents.value.some((doc) => doc.id === optimisticDocument.id)) {
documents.value = [optimisticDocument, ...documents.value]
}
highlightedDocumentId.value = response.data.id
const folderName = currentFolderName.value || folderMap.value.get(currentFolderId.value ?? '')?.name || currentFolder.value?.name || '当前文件夹'
uploadSuccess.value = `已上传到 ${folderName}`
if (response.data.ingestion_error) {
uploadError.value = response.data.ingestion_error
}
clearUploadFeedbackLater()
startStatusPolling()
} catch (error) { } catch (error) {
if (axios.isAxiosError(error)) {
const detail = error.response?.data?.detail
const requestId = error.response?.data?.request_id || (error as AxiosError & { requestId?: string }).requestId
uploadError.value = [detail, requestId ? `请求ID: ${requestId}` : ''].filter(Boolean).join(' · ') || '上传失败,请稍后重试'
} else {
uploadError.value = '上传失败,请稍后重试'
}
console.error('上传失败:', error) console.error('上传失败:', error)
} finally { } finally {
isUploading.value = false isUploading.value = false
@@ -180,12 +298,13 @@ export function useKnowledgeView() {
if (!newFolderName.value.trim()) return if (!newFolderName.value.trim()) return
try { try {
await folderApi.create({ const response = await folderApi.create({
name: newFolderName.value.trim(), name: newFolderName.value.trim(),
parent_id: newFolderParentId.value, parent_id: newFolderParentId.value,
}) })
await loadFolders() await loadFolders()
showNewFolderDialog.value = false showNewFolderDialog.value = false
await goToFolder(response.data.id)
} catch (error) { } catch (error) {
console.error('创建文件夹失败:', error) console.error('创建文件夹失败:', error)
} }
@@ -245,12 +364,70 @@ export function useKnowledgeView() {
} }
} }
async function loadDocumentChunks(documentId: string) {
isLoadingDocumentChunks.value = true
try {
const response = await documentApi.getChunks(documentId)
activeDocumentChunks.value = response.data
chunkDrafts.value = Object.fromEntries(response.data.map((chunk) => [chunk.id, chunk.content]))
chunkEditing.value = {}
chunkSaving.value = {}
chunkEditError.value = Object.fromEntries(response.data.map((chunk) => [chunk.id, '']))
} catch (error) {
console.error('加载文档切片失败:', error)
activeDocumentChunks.value = []
} finally {
isLoadingDocumentChunks.value = false
}
}
function startChunkEdit(chunk: DocumentChunk) {
chunkEditing.value[chunk.id] = true
chunkDrafts.value[chunk.id] = chunk.content
chunkEditError.value[chunk.id] = ''
}
function cancelChunkEdit(chunkId: string) {
const chunk = activeDocumentChunks.value.find((item) => item.id === chunkId)
if (!chunk) return
chunkEditing.value[chunkId] = false
chunkDrafts.value[chunkId] = chunk.content
chunkEditError.value[chunkId] = ''
}
async function saveChunkEdit(chunkId: string) {
if (!activeDocument.value) return
const nextContent = chunkDrafts.value[chunkId] ?? ''
chunkSaving.value[chunkId] = true
chunkEditError.value[chunkId] = ''
try {
const response = await documentApi.updateChunk(activeDocument.value.id, chunkId, { content: nextContent })
activeDocumentChunks.value = activeDocumentChunks.value.map((chunk) => (
chunk.id === chunkId ? response.data : chunk
))
chunkDrafts.value[chunkId] = response.data.content
chunkEditing.value[chunkId] = false
} catch (error) {
console.error('保存文档切片失败:', error)
chunkEditError.value[chunkId] = '切片保存失败,请稍后重试'
} finally {
chunkSaving.value[chunkId] = false
}
}
async function openDocument(doc: Document) { async function openDocument(doc: Document) {
activeDocument.value = doc activeDocument.value = doc
activeDocumentContent.value = '' activeDocumentContent.value = ''
activeDocumentChunks.value = []
chunkDrafts.value = {}
chunkEditing.value = {}
chunkSaving.value = {}
chunkEditError.value = {}
showDocumentDialog.value = true showDocumentDialog.value = true
isLoadingDocumentContent.value = true isLoadingDocumentContent.value = true
const contentPromise = (async () => {
try { try {
const response = await documentApi.getContent(doc.id) const response = await documentApi.getContent(doc.id)
const content = response.data as string | { content?: string } const content = response.data as string | { content?: string }
@@ -261,12 +438,21 @@ export function useKnowledgeView() {
} finally { } finally {
isLoadingDocumentContent.value = false isLoadingDocumentContent.value = false
} }
})()
const chunkPromise = loadDocumentChunks(doc.id)
await Promise.all([contentPromise, chunkPromise])
} }
function closeDocumentDialog() { function closeDocumentDialog() {
showDocumentDialog.value = false showDocumentDialog.value = false
activeDocument.value = null activeDocument.value = null
activeDocumentContent.value = '' activeDocumentContent.value = ''
activeDocumentChunks.value = []
chunkDrafts.value = {}
chunkEditing.value = {}
chunkSaving.value = {}
chunkEditError.value = {}
} }
function getFileTypeColor(type: string) { function getFileTypeColor(type: string) {
@@ -275,6 +461,9 @@ export function useKnowledgeView() {
md: '#60a5fa', md: '#60a5fa',
txt: '#34d399', txt: '#34d399',
docx: '#a78bfa', docx: '#a78bfa',
doc: '#c084fc',
csv: '#fbbf24',
xlsx: '#22c55e',
} }
return colors[type] || '#9ca3af' return colors[type] || '#9ca3af'
} }
@@ -295,9 +484,22 @@ export function useKnowledgeView() {
onMounted(async () => { onMounted(async () => {
await loadFolders() await loadFolders()
const persistedFolderId = currentFolderId.value
if (persistedFolderId && folderMap.value.has(persistedFolderId)) {
currentFolderName.value = folderMap.value.get(persistedFolderId)?.name ?? ''
await loadDocumentsByFolder(persistedFolderId)
return
}
currentFolderId.value = null
currentFolderName.value = ''
await persistCurrentFolder(null)
await loadDocumentsByFolder(null) await loadDocumentsByFolder(null)
}) })
onBeforeUnmount(() => {
stopStatusPolling()
})
return { return {
folders, folders,
documents, documents,
@@ -305,6 +507,8 @@ export function useKnowledgeView() {
isUploading, isUploading,
isLoadingDocuments, isLoadingDocuments,
uploadError, uploadError,
uploadSuccess,
highlightedDocumentId,
uploadInput, uploadInput,
showNewFolderDialog, showNewFolderDialog,
newFolderName, newFolderName,
@@ -318,6 +522,12 @@ export function useKnowledgeView() {
activeDocument, activeDocument,
activeDocumentContent, activeDocumentContent,
isLoadingDocumentContent, isLoadingDocumentContent,
activeDocumentChunks,
isLoadingDocumentChunks,
chunkDrafts,
chunkEditing,
chunkSaving,
chunkEditError,
currentFolder, currentFolder,
isRoot, isRoot,
visibleFolders, visibleFolders,
@@ -337,8 +547,12 @@ export function useKnowledgeView() {
deleteFolder, deleteFolder,
openDocument, openDocument,
closeDocumentDialog, closeDocumentDialog,
startChunkEdit,
cancelChunkEdit,
saveChunkEdit,
getFileTypeColor, getFileTypeColor,
formatFileSize, formatFileSize,
formatDate, formatDate,
getStatusLabel,
} }
} }

15
frontend/vitest.config.ts Normal file
View File

@@ -0,0 +1,15 @@
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
test: {
environment: 'jsdom',
},
})