<script setup>
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { useMessage, useDialog } from 'naive-ui'
import { NButton, NIcon, NInput, NSelect, NTag, NSpin, NBreadcrumb, NInputNumber } from 'naive-ui'
import {
  AddOutline, RefreshOutline, CloudUploadOutline, SearchOutline,
  TrashOutline, ReloadOutline, ChevronForwardOutline, DocumentTextOutline,
  LayersOutline, AlertCircleOutline, SparklesOutline, ListOutline
} from '@vicons/ionicons5'
import KbCategoryTree from '../../components/knowledge/KbCategoryTree.vue'
import DocumentUploader from '../../components/knowledge/DocumentUploader.vue'
import {
  getKbCategories, createKbCategory, updateKbCategory, deleteKbCategory,
  getKbDocuments, deleteKbDocument, revectorizeKbDocument, vectorizeKbDocument,
  getKbDependencies, searchKb, getKbDocumentChunks, vectorizeKbChunk
} from '../../api/knowledge'

const message = useMessage()
const dialog = useDialog()

// ============ 状态 ============
const loadingCategories = ref(false)
const categories = ref([])              // 树形结构
const flatCategories = ref([])          // 扁平结构（用于 uploader）
const selectedCategoryId = ref(null)    // null 表示「全部」
const expandedIds = ref(new Set())

const loadingDocs = ref(false)
const documents = ref([])
const keyword = ref('')
const page = ref(1)
const size = ref(20)
const total = ref(0)

const showUploader = ref(false)

// ============ 分块查看状态 ============
const showChunks = ref(false)
const chunksLoading = ref(false)
const chunksDoc = ref(null)
const chunks = ref([])

// ============ 分块完整内容查看状态 ============
const showFullContent = ref(false)
const fullChunk = ref(null)

// ============ 向量检索状态 ============
const showSearch = ref(false)
const searchQuery = ref('')
const searchTopK = ref(5)
const searchThreshold = ref(0.0)
const searchUseCategoryFilter = ref(false)  // 是否启用当前分类过滤
const searching = ref(false)
const searchResults = ref([])
const searchElapsed = ref(0)

// 编辑/新增分类的状态
const editingId = ref(null)
const editingName = ref('')
const addingParentId = ref(undefined)  // undefined=未在添加，null=添加根，number=添加某父级
const addingName = ref('')

// 子系统依赖状态
const deps = ref({ kbEnabled: true, embeddingAvailable: false, vectorStoreAvailable: false })
let pollTimer = null

// ============ 工具 ============
/** 把扁平列表构造成树 */
function buildTree(list) {
  const map = new Map()
  list.forEach(c => map.set(c.id, { ...c, children: [] }))
  const roots = []
  map.forEach(node => {
    if (node.parentId != null && map.has(node.parentId)) {
      map.get(node.parentId).children.push(node)
    } else {
      roots.push(node)
    }
  })
  return roots
}

function flatten(nodes, out = []) {
  for (const n of nodes) {
    out.push(n)
    if (n.children?.length) flatten(n.children, out)
  }
  return out
}

const statusMeta = {
  PENDING: { label: '排队中', type: 'default' },
  PARSING: { label: '解析中', type: 'info' },
  CHUNKING: { label: '分块中', type: 'info' },
  EMBEDDING: { label: '向量化', type: 'info' },
  VECTORIZING: { label: '入库中', type: 'info' },
  READY: { label: '就绪', type: 'success' },
  FAILED: { label: '失败', type: 'error' }
}

function formatSize(bytes) {
  if (!bytes) return '-'
  if (bytes < 1024) return bytes + ' B'
  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
  return (bytes / 1024 / 1024).toFixed(1) + ' MB'
}

// ============ 分类加载 ============
async function loadCategories() {
  loadingCategories.value = true
  try {
    const res = await getKbCategories()
    const list = res.data || []
    categories.value = buildTree(list)
    flatCategories.value = flatten(categories.value)
  } catch (e) {
    message.error('加载分类失败：' + e.message)
  } finally {
    loadingCategories.value = false
  }
}

// ============ 文档加载 ============
async function loadDocuments() {
  loadingDocs.value = true
  try {
    const params = {
      page: page.value,
      size: size.value
    }
    if (selectedCategoryId.value !== null) params.categoryId = selectedCategoryId.value
    if (keyword.value.trim()) params.keyword = keyword.value.trim()
    const res = await getKbDocuments(params)
    documents.value = res.data.items || []
    total.value = res.data.total || 0
  } catch (e) {
    message.error('加载文档失败：' + e.message)
  } finally {
    loadingDocs.value = false
  }
}

// ============ 依赖状态 ============
async function loadDeps() {
  try {
    const res = await getKbDependencies()
    deps.value = res.data
  } catch {
    // 静默
  }
}

// ============ 分类操作 ============
function selectCategory(id) {
  // id === null 表示「全部文档」虚拟节点
  selectedCategoryId.value = id
  page.value = 1
  loadDocuments()
}

function toggleExpand(id) {
  const s = new Set(expandedIds.value)
  if (s.has(id)) s.delete(id); else s.add(id)
  expandedIds.value = s
}

function startAdd(parentId) {
  addingParentId.value = parentId
  addingName.value = ''
  if (parentId !== undefined && parentId !== null && !expandedIds.value.has(parentId)) {
    toggleExpand(parentId)
  }
}

async function confirmAdd() {
  if (!addingName.value.trim()) {
    addingParentId.value = undefined
    return
  }
  try {
    await createKbCategory({ name: addingName.value.trim(), parentId: addingParentId.value === null ? null : addingParentId.value })
    message.success('已创建分类')
    addingParentId.value = undefined
    addingName.value = ''
    await loadCategories()
  } catch (e) {
    message.error('创建失败：' + e.message)
  }
}

function cancelAdd() {
  addingParentId.value = undefined
  addingName.value = ''
}

function startEdit({ id, name }) {
  editingId.value = id
  editingName.value = name
}

async function confirmEdit() {
  if (!editingName.value.trim()) {
    editingId.value = null
    return
  }
  try {
    await updateKbCategory(editingId.value, { name: editingName.value.trim() })
    editingId.value = null
    editingName.value = ''
    await loadCategories()
  } catch (e) {
    message.error('更新失败：' + e.message)
  }
}

function cancelEdit() {
  editingId.value = null
  editingName.value = ''
}

function confirmDeleteCategory({ id, name }) {
  dialog.warning({
    title: '确认删除',
    content: `确认删除分类「${name}」？删除前请确保该分类下没有子分类和文档。`,
    maskClosable: false,
    positiveText: '删除',
    negativeText: '取消',
    onPositiveClick: async () => {
      try {
        await deleteKbCategory(id)
        message.success('已删除')
        if (selectedCategoryId.value === id) selectedCategoryId.value = null
        await loadCategories()
      } catch (e) {
        message.error('删除失败：' + e.message)
      }
    }
  })
}

// ============ 文档操作 ============
async function refreshOne(doc) {
  // 简单刷新单个文档（轮询状态用）
  try {
    const res = await getKbDocuments({ page: 1, size: 1 })
    // 不精确，直接重新加载列表
    await loadDocuments()
  } catch { }
}

function confirmDelete(doc) {
  dialog.warning({
    title: '确认删除',
    content: `确认删除文档「${doc.name}」？该操作会同时删除已生成的向量记录，不可恢复。`,
    maskClosable: false,
    positiveText: '删除',
    negativeText: '取消',
    onPositiveClick: async () => {
      try {
        await deleteKbDocument(doc.id)
        message.success('已删除')
        await loadDocuments()
      } catch (e) {
        message.error('删除失败：' + e.message)
      }
    }
  })
}

async function revectorize(doc) {
  try {
    await revectorizeKbDocument(doc.id)
    message.success('已触发重新向量化')
    await loadDocuments()
  } catch (e) {
    message.error('重新向量化失败：' + e.message)
  }
}

async function vectorizeDocument(doc) {
  try {
    await vectorizeKbDocument(doc.id)
    message.success('已向量化')
    await loadDocuments()
    if (showChunks.value && chunksDoc.value?.id === doc.id) {
      await viewChunks(doc)
    }
  } catch (e) {
    message.error('向量化失败：' + e.message)
  }
}

async function vectorizeChunk(chunk) {
  try {
    await vectorizeKbChunk(chunk.id)
    message.success('分块已向量化')
    if (chunksDoc.value) await viewChunks(chunksDoc.value)
  } catch (e) {
    message.error('分块向量化失败：' + e.message)
  }
}

async function viewChunks(doc) {
  chunksDoc.value = doc
  showChunks.value = true
  chunksLoading.value = true
  chunks.value = []
  try {
    const res = await getKbDocumentChunks(doc.id)
    chunks.value = res.data || []
  } catch (e) {
    message.error('加载分块失败：' + e.message)
  } finally {
    chunksLoading.value = false
  }
}

function closeChunks() {
  showChunks.value = false
  chunksDoc.value = null
  chunks.value = []
}

function viewFullContent(chunk) {
  fullChunk.value = chunk
  showFullContent.value = true
}

function closeFullContent() {
  showFullContent.value = false
  fullChunk.value = null
}

function onUploaded() {
  loadDocuments()
}

// ============ 轮询进行中任务 ============
const hasPending = computed(() =>
  documents.value.some(d => ['PENDING', 'PARSING', 'CHUNKING', 'EMBEDDING', 'VECTORIZING'].includes(d.status))
)

function startPolling() {
  stopPolling()
  pollTimer = setInterval(() => {
    if (hasPending.value) loadDocuments()
  }, 3000)
}

function stopPolling() {
  if (pollTimer) clearInterval(pollTimer)
  pollTimer = null
}

// ============ 搜索 ============
let searchTimer = null
function onSearchInput() {
  if (searchTimer) clearTimeout(searchTimer)
  searchTimer = setTimeout(() => {
    page.value = 1
    loadDocuments()
  }, 300)
}

// ============ 向量检索 ============
async function runSearch() {
  const q = searchQuery.value.trim()
  if (!q) {
    message.warning('请输入查询内容')
    return
  }
  searching.value = true
  searchResults.value = []
  const startedAt = performance.now()
  try {
    const params = {
      query: q,
      topK: searchTopK.value,
      similarityThreshold: searchThreshold.value
    }
    if (searchUseCategoryFilter.value && selectedCategoryId.value !== null) {
      params.categoryId = selectedCategoryId.value
    }
    const res = await searchKb(params)
    searchResults.value = res.data || []
    searchElapsed.value = Math.round(performance.now() - startedAt)
  } catch (e) {
    message.error('检索失败：' + e.message)
  } finally {
    searching.value = false
  }
}

function formatScore(s) {
  if (s == null) return '-'
  return s.toFixed(4)
}

function truncate(text, max = 240) {
  if (!text) return ''
  return text.length > max ? text.slice(0, max) + '…' : text
}

// ============ 生命周期 ============
onMounted(async () => {
  await Promise.all([loadCategories(), loadDocuments(), loadDeps()])
  startPolling()
})

onUnmounted(() => {
  stopPolling()
})
</script>

<template>
  <div class="kb-docs-page">

    <div class="kb-layout">
      <!-- 左侧：分类树 -->
      <aside class="kb-sidebar">
        <div class="sidebar-header">
          <span class="sidebar-title">分类</span>
          <div class="sidebar-actions">
            <button class="icon-btn-sm" @click="startAdd(null)" title="新增根分类">
              <n-icon size="14">
                <AddOutline />
              </n-icon>
            </button>
            <button class="icon-btn-sm" @click="loadCategories" title="刷新">
              <n-icon size="14">
                <RefreshOutline />
              </n-icon>
            </button>
          </div>
        </div>

        <!-- 根「全部文档」入口 -->
        <div class="root-item" :class="{ active: selectedCategoryId === null }" @click="selectCategory(null)">
          <n-icon size="14" color="var(--color-accent)">
            <LayersOutline />
          </n-icon>
          <span class="root-name">全部文档</span>
        </div>

        <!-- 添加根分类输入框 -->
        <div v-if="addingParentId === null" class="category-item adding" style="paddingLeft: 16px">
          <input class="inline-input" v-model="addingName" placeholder="输入分类名称" @keyup.enter="confirmAdd"
            @keyup.escape="cancelAdd" @blur="cancelAdd" autofocus />
        </div>

        <div class="sidebar-tree" v-if="loadingCategories">
          <div class="loading"><n-spin size="small" /></div>
        </div>
        <div class="sidebar-tree" v-else-if="!categories.length">
          <div class="empty-text">暂无分类</div>
        </div>
        <div class="sidebar-tree" v-else>
          <KbCategoryTree :nodes="categories" :selected-id="selectedCategoryId" :expanded-ids="expandedIds"
            :editing-id="editingId" :editing-name="editingName" :adding-parent-id="addingParentId"
            :adding-name="addingName" @select="selectCategory" @toggle="toggleExpand" @add="startAdd"
            @delete="confirmDeleteCategory" @edit-start="startEdit" @edit-confirm="confirmEdit"
            @edit-cancel="cancelEdit" @add-confirm="confirmAdd" @add-cancel="cancelAdd"
            @update:editing-name="editingName = $event" @update:adding-name="addingName = $event" />
        </div>
      </aside>

      <!-- 右侧：文档列表 -->
      <main class="kb-main">
        <!-- 工具栏 -->
        <div class="toolbar">
          <div class="toolbar-left">
            <n-input v-model:value="keyword" placeholder="按文件名搜索..." size="small" clearable style="width: 240px"
              @input="onSearchInput">
              <template #prefix>
                <n-icon>
                  <SearchOutline />
                </n-icon>
              </template>
            </n-input>
            <n-button size="small" :type="showSearch ? 'primary' : 'default'" :ghost="showSearch"
              @click="showSearch = !showSearch" title="基于向量相似度的语义检索">
              <template #icon><n-icon>
                  <SparklesOutline />
                </n-icon></template>
              检索测试
            </n-button>
          </div>
          <div class="toolbar-right">
            <n-button size="small" @click="loadDocuments">
              <template #icon><n-icon>
                  <RefreshOutline />
                </n-icon></template>
              刷新
            </n-button>
            <n-button size="small" type="primary" @click="showUploader = true">
              <template #icon><n-icon>
                  <CloudUploadOutline />
                </n-icon></template>
              上传文档
            </n-button>
          </div>
        </div>

        <!-- 向量检索面板 -->
        <div class="search-panel" v-if="showSearch">
          <div class="search-form">
            <n-input v-model:value="searchQuery" placeholder="输入查询文本，语义召回相关分块..." type="textarea"
              :autosize="{ minRows: 2, maxRows: 4 }" @keydown.enter.exact.prevent="runSearch" />
            <div class="search-options">
              <span class="opt-label">TopK</span>
              <n-input-number v-model:value="searchTopK" :min="1" :max="50" size="small" style="width: 90px" />
              <span class="opt-label">相似度阈值</span>
              <n-input-number v-model:value="searchThreshold" :min="0" :max="1" :step="0.05" size="small"
                style="width: 100px" />
              <label class="opt-check">
                <input type="checkbox" v-model="searchUseCategoryFilter" />
                <span>限定当前分类</span>
              </label>
              <n-button size="small" type="primary" :loading="searching" @click="runSearch">
                <template #icon><n-icon>
                    <SparklesOutline />
                  </n-icon></template>
                检索
              </n-button>
            </div>
          </div>

          <div class="search-results">
            <div v-if="searching" class="loading-block">
              <n-spin size="medium" />
            </div>
            <div v-else-if="!searchResults.length" class="empty-block">
              <span class="empty-title">输入查询后点击「检索」查看命中分块</span>
            </div>
            <div v-else>
              <div class="results-meta">
                共 {{ searchResults.length }} 条结果 · 耗时 {{ searchElapsed }}ms
                <span v-if="searchUseCategoryFilter && selectedCategoryId !== null">
                  · 限定分类 #{{ selectedCategoryId }}
                </span>
              </div>
              <div class="hit-list">
                <div class="hit-item" v-for="(hit, idx) in searchResults" :key="idx">
                  <div class="hit-header">
                    <span class="hit-rank">#{{ idx + 1 }}</span>
                    <span class="hit-doc" :title="hit.docName">{{ hit.docName || '未知文档' }}</span>
                    <span class="hit-chunk" v-if="hit.chunkIndex != null">分块 #{{ hit.chunkIndex }}</span>
                    <span class="hit-score">score: {{ formatScore(hit.score) }}</span>
                  </div>
                  <div class="hit-content">{{ truncate(hit.content, 600) }}</div>
                </div>
              </div>
            </div>
          </div>
        </div>

        <!-- 文档表格 -->
        <div class="doc-list" v-loading="loadingDocs">
          <div v-if="loadingDocs && !documents.length" class="loading-block">
            <n-spin size="medium" />
          </div>
          <div v-else-if="!documents.length" class="empty-block">
            <n-icon size="36" color="var(--text-muted)">
              <DocumentTextOutline />
            </n-icon>
            <div class="empty-title">暂无文档</div>
            <n-button size="small" type="success" @click="showUploader = true">
              <template #icon><n-icon>
                  <CloudUploadOutline />
                </n-icon></template>
              上传第一个文档
            </n-button>
          </div>
          <div v-else class="doc-grid">
            <div class="doc-card" v-for="doc in documents" :key="doc.id">
              <div class="doc-card-header">
                <n-icon size="20" color="var(--color-accent)">
                  <DocumentTextOutline />
                </n-icon>
                <span class="doc-name" :title="doc.name">{{ doc.name }}</span>
              </div>
              <div class="doc-meta">
                <n-tag size="tiny" :type="statusMeta[doc.status]?.type || 'default'" round>
                  {{ statusMeta[doc.status]?.label || doc.status }}
                </n-tag>
                <span class="meta-item">{{ formatSize(doc.fileSize) }}</span>
                <span class="meta-item">{{ doc.chunkCount || 0 }} 块</span>
              </div>
              <div class="doc-error" v-if="doc.status === 'FAILED' && doc.errorMessage">
                <span class="error-text" :title="doc.errorMessage">
                  {{ doc.errorMessage }}
                </span>
              </div>
              <div class="doc-card-footer">
                <span class="doc-time">{{ doc.createdAt }}</span>
                <div class="footer-actions">
                  <button class="action-btn" title="查看分块" @click="viewChunks(doc)">
                    <n-icon size="14">
                      <ListOutline />
                    </n-icon>
                  </button>
                  <button class="action-btn primary" title="向量化" @click="vectorizeDocument(doc)"
                    :disabled="doc.status === 'PENDING' || doc.status === 'PARSING' || doc.status === 'CHUNKING' || doc.status === 'EMBEDDING' || doc.status === 'VECTORIZING' || doc.vectorCount === doc.chunkCount">
                    <n-icon size="14">
                      <SparklesOutline />
                    </n-icon>
                  </button>
                  <button class="action-btn" title="重新向量化" @click="revectorize(doc)"
                    :disabled="['PARSING', 'CHUNKING', 'EMBEDDING', 'VECTORIZING'].includes(doc.status)">
                    <n-icon size="14">
                      <ReloadOutline />
                    </n-icon>
                  </button>
                  <button class="action-btn danger" title="删除" @click="confirmDelete(doc)">
                    <n-icon size="14">
                      <TrashOutline />
                    </n-icon>
                  </button>
                </div>
              </div>
            </div>
          </div>

          <!-- 分页 -->
          <div class="pagination" v-if="total > size">
            <n-button size="tiny" :disabled="page <= 1" @click="page--; loadDocuments()">上一页</n-button>
            <span class="page-info">第 {{ page }} 页 / 共 {{ Math.ceil(total / size) }} 页（{{ total }} 条）</span>
            <n-button size="tiny" :disabled="page * size >= total" @click="page++; loadDocuments()">下一页</n-button>
          </div>
        </div>
      </main>
    </div>

    <!-- 分块查看弹窗 -->
    <div v-if="showChunks" class="chunks-modal-overlay" @click.self="closeChunks">
      <div class="chunks-modal">
        <div class="chunks-modal-header">
          <div class="chunks-title">
            <n-icon size="18" color="var(--color-accent)">
              <ListOutline />
            </n-icon>
            <span>「{{ chunksDoc?.name }}」分块详情</span>
          </div>
          <button class="chunks-close" @click="closeChunks">×</button>
        </div>
        <div class="chunks-modal-body">
          <div v-if="chunksLoading" class="loading-block"><n-spin size="small" /> 加载中…</div>
          <div v-else-if="chunks.length === 0" class="empty-block">暂无分块数据</div>
          <div v-else class="chunks-list">
            <div v-for="(chunk, idx) in chunks" :key="idx" class="chunk-item">
              <div class="chunk-header">
                <span class="chunk-index">#{{ chunk.chunkIndex + 1 }}</span>
                <span class="chunk-meta level">L{{ chunk.chunkLevel ?? 3 }}</span>
                <span class="chunk-meta">{{ chunk.charCount }} 字符</span>
                <span v-if="chunk.vectorId" class="chunk-meta vector">已向量化</span>
                <span v-else-if="(chunk.chunkLevel ?? 3) === 3" class="chunk-meta no-vector">未向量化</span>
                <span v-else class="chunk-meta no-vector">父块不入库</span>
                <button v-if="!chunk.vectorId && (chunk.chunkLevel ?? 3) === 3" class="chunk-vectorize-btn"
                  @click="vectorizeChunk({ id: chunk.id, chunkIndex: chunk.chunkIndex })">
                  <n-icon size="12">
                    <SparklesOutline />
                  </n-icon> 向量化
                </button>
              </div>
              <div class="chunk-preview">
                <pre>{{ chunk.contentPreview || chunk.preview || chunk.content }}</pre>
                <button
                  v-if="(chunk.contentPreview || chunk.preview || '').length >= 200 || (chunk.content && chunk.content.length > (chunk.contentPreview || chunk.preview || '').length)"
                  class="view-full-btn" @click="viewFullContent(chunk)">
                  查看完整内容
                </button>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- 分块完整内容弹窗 -->
    <div v-if="showFullContent" class="chunks-modal-overlay" @click.self="closeFullContent">
      <div class="chunks-modal full-content-modal">
        <div class="chunks-modal-header">
          <div class="chunks-title">
            <n-icon size="18" color="var(--color-accent)">
              <ListOutline />
            </n-icon>
            <span>#{{ fullChunk?.chunkIndex + 1 }} 完整内容（L{{ fullChunk?.chunkLevel ?? 3 }} · {{ fullChunk?.charCount }}
              字符）</span>
          </div>
          <button class="chunks-close" @click="closeFullContent">×</button>
        </div>
        <div class="chunks-modal-body">
          <pre class="full-chunk-content">{{ fullChunk?.content }}</pre>
        </div>
      </div>
    </div>

    <!-- 上传弹窗 -->
    <DocumentUploader v-model:show="showUploader" :category-id="selectedCategoryId" :categories="categories"
      @uploaded="onUploaded" />
  </div>
</template>

<style scoped>
.kb-docs-page {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
}

.deps-banner {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
  background: rgba(245, 158, 11, 0.08);
  border-bottom: 1px solid rgba(245, 158, 11, 0.2);
  color: #f59e0b;
  font-size: 12px;
}

.banner-text {
  line-height: 1.5;
}

.kb-layout {
  display: grid;
  grid-template-columns: 240px 1fr;
  flex: 1;
  min-height: 0;
}

/* 左侧分类树 */
.kb-sidebar {
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--border-color);
  background: var(--bg-secondary);
  overflow: hidden;
}

.sidebar-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 12px 8px;
}

.sidebar-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-secondary);
}

.sidebar-actions {
  display: flex;
  gap: 2px;
}

.sidebar-tree {
  flex: 1;
  overflow-y: auto;
  padding-bottom: 8px;
}

.loading,
.empty-text,
.loading-block,
.empty-block {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 32px;
  gap: 8px;
  color: var(--text-muted);
  font-size: 12px;
}

.root-item {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 16px;
  cursor: pointer;
  transition: all 0.15s;
  font-size: 13px;
  color: var(--text-secondary);
}

.root-item:hover {
  background: rgba(37, 99, 235, 0.06);
}

.root-item.active {
  background: rgba(37, 99, 235, 0.12);
  color: var(--color-accent);
  font-weight: 500;
}

.icon-btn-sm {
  background: none;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  padding: 4px;
  border-radius: 4px;
  display: flex;
  align-items: center;
  transition: all 0.15s;
}

.icon-btn-sm:hover {
  color: var(--color-accent);
  background: rgba(37, 99, 235, 0.1);
}

.inline-input {
  background: rgba(37, 99, 235, 0.08);
  border: 1px solid rgba(37, 99, 235, 0.2);
  border-radius: 4px;
  padding: 4px 8px;
  font-size: 13px;
  color: var(--text-primary);
  outline: none;
  width: calc(100% - 32px);
  font-family: inherit;
}

.inline-input:focus {
  border-color: var(--color-accent);
}

.category-item.adding {
  padding: 6px 16px 6px 0;
  cursor: default;
}

/* 右侧文档列表 */
.kb-main {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-width: 0;
  overflow: hidden;
}

.toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 20px;
  border-bottom: 1px solid var(--border-color);
  background: var(--bg-card);
}

.toolbar-right {
  display: flex;
  gap: 8px;
}

.toolbar-left {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* 向量检索面板 */
.search-panel {
  border-bottom: 1px solid var(--border-color);
  background: var(--bg-secondary);
  padding: 12px 20px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-height: 50vh;
  overflow-y: auto;
}

.search-form {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.search-options {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font-size: 12px;
  color: var(--text-secondary);
}

.opt-label {
  color: var(--text-muted);
}

.opt-check {
  display: flex;
  align-items: center;
  gap: 4px;
  cursor: pointer;
  user-select: none;
}

.opt-check input {
  cursor: pointer;
}

.search-results {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-height: 80px;
}

.results-meta {
  font-size: 11px;
  color: var(--text-muted);
  padding: 4px 0;
}

.hit-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.hit-item {
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: 6px;
  padding: 8px 10px;
}

.hit-header {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 11px;
  color: var(--text-muted);
  margin-bottom: 4px;
}

.hit-rank {
  background: rgba(37, 99, 235, 0.1);
  color: var(--color-accent);
  padding: 1px 6px;
  border-radius: 4px;
  font-weight: 600;
}

.hit-doc {
  color: var(--text-primary);
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 280px;
}

.hit-chunk {
  background: rgba(107, 114, 128, 0.1);
  color: var(--text-secondary);
  padding: 1px 6px;
  border-radius: 4px;
}

.hit-score {
  margin-left: auto;
  font-variant-numeric: tabular-nums;
  color: var(--color-accent);
}

.hit-content {
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-primary);
  white-space: pre-wrap;
  word-break: break-word;
}

.doc-list {
  flex: 1;
  overflow-y: auto;
  padding: 16px 20px;
}

.doc-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 12px;
}

.doc-card {
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: 8px;
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  transition: all 0.2s;
}

.doc-card:hover {
  border-color: var(--border-color-hover);
  box-shadow: var(--shadow-card-hover);
}

.doc-card-header {
  display: flex;
  align-items: center;
  gap: 8px;
}

.doc-name {
  font-size: 13px;
  font-weight: 500;
  color: var(--text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 1;
}

.doc-meta {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 11px;
  color: var(--text-muted);
}

.meta-item {
  font-variant-numeric: tabular-nums;
}

.doc-error {
  background: rgba(239, 68, 68, 0.08);
  border-radius: 4px;
  padding: 6px 8px;
}

.error-text {
  font-size: 11px;
  color: #ef4444;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.doc-card-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-top: 4px;
  border-top: 1px dashed var(--border-color);
}

.doc-time {
  font-size: 11px;
  color: var(--text-muted);
}

.footer-actions {
  display: flex;
  gap: 2px;
}

.action-btn {
  background: none;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  padding: 4px;
  border-radius: 4px;
  display: flex;
  align-items: center;
}

.action-btn:hover {
  color: var(--color-accent);
  background: rgba(37, 99, 235, 0.1);
}

.action-btn.primary {
  color: var(--color-accent);
}

.action-btn.primary:hover {
  color: #fff;
  background: var(--color-accent);
}

.action-btn.danger:hover {
  color: #ef4444;
  background: rgba(239, 68, 68, 0.1);
}

.action-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.chunk-vectorize-btn {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 11px;
  padding: 2px 8px;
  border-radius: 4px;
  border: 1px solid var(--color-accent);
  background: rgba(37, 99, 235, 0.1);
  color: var(--color-accent);
  cursor: pointer;
}

.chunk-vectorize-btn:hover {
  background: var(--color-accent);
  color: #fff;
}

.pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 16px 0;
  font-size: 12px;
  color: var(--text-muted);
}

.page-info {
  font-variant-numeric: tabular-nums;
}

/* 分块查看弹窗 */
.chunks-modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 2000;
}

.chunks-modal {
  width: 720px;
  max-width: 90vw;
  max-height: 80vh;
  background: var(--bg-primary);
  border: 1px solid var(--border-color);
  border-radius: 10px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.chunks-modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 16px;
  border-bottom: 1px solid var(--border-color);
}

.chunks-title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  font-weight: 600;
  color: var(--text-primary);
}

.chunks-close {
  background: none;
  border: none;
  color: var(--text-muted);
  font-size: 22px;
  cursor: pointer;
  line-height: 1;
}

.chunks-close:hover {
  color: var(--text-primary);
}

.chunks-modal-body {
  flex: 1;
  overflow-y: auto;
  padding: 12px 16px;
  min-height: 0;
}

.chunks-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.chunk-item {
  border: 1px solid var(--border-color);
  border-radius: 8px;
  background: var(--bg-secondary);
  overflow: hidden;
}

.chunk-header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  background: rgba(37, 99, 235, 0.08);
  border-bottom: 1px solid var(--border-color);
  font-size: 11px;
}

.chunk-index {
  font-weight: 600;
  color: var(--color-accent);
}

.chunk-meta {
  color: var(--text-muted);
}

.chunk-meta.vector {
  color: #10b981;
}

.chunk-meta.no-vector {
  color: #f59e0b;
}

.chunk-preview {
  padding: 10px 12px;
  font-family: 'Cascadia Code', 'Fira Code', monospace;
  font-size: 12px;
  line-height: 1.6;
  color: var(--text-secondary);
}

.chunk-preview pre {
  margin: 0;
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 240px;
  overflow-y: auto;
}

.view-full-btn {
  margin-top: 8px;
  padding: 4px 10px;
  border: 1px solid var(--color-accent);
  border-radius: 4px;
  background: transparent;
  color: var(--color-accent);
  font-size: 12px;
  cursor: pointer;
  transition: all 0.2s;
}

.view-full-btn:hover {
  background: var(--color-accent);
  color: #fff;
}

.full-content-modal {
  width: 80vw;
  max-width: 960px;
}

.full-chunk-content {
  margin: 0;
  padding: 12px;
  font-family: 'Cascadia Code', 'Fira Code', monospace;
  font-size: 13px;
  line-height: 1.7;
  color: var(--text-secondary);
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 70vh;
  overflow-y: auto;
  background: var(--bg-primary);
  border-radius: 6px;
}

.chunk-content {
  margin: 0;
  padding: 10px 12px;
  font-family: 'Cascadia Code', 'Fira Code', monospace;
  font-size: 12px;
  line-height: 1.6;
  color: var(--text-secondary);
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 240px;
  overflow-y: auto;
}
</style>
