<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { message, Modal } from 'ant-design-vue'
import {
  FolderOutlined,
  FileTextOutlined,
  UploadOutlined,
  ReloadOutlined,
  SearchOutlined,
  ClockCircleOutlined,
  TagsOutlined
} from '@ant-design/icons-vue'
import {
  categories as fetchCategories,
  stats as fetchStats
} from '@/api/knowledge'
import {
  list as fetchDocuments,
  detail as fetchDocumentDetail,
  remove as deleteDocument,
  reingest as reingestDocument,
  ingest as ingestDocument,
  upload as uploadDocument,
  uploadBatch
} from '@/api/documents'
import { useIngestPolling } from '@/composables/useIngestPolling'
import { useTasksDrawer } from '@/composables/useTasksDrawer'
import { INGEST_STATUS_TEXT, INGEST_STATUS_COLOR } from '@/constants/ingest'

const tasksDrawer = useTasksDrawer()

/* ---------- 树形目录 ---------- */
const isLoading = ref(false)
const treeData = ref([])
const categoriesData = ref([])
const categoryDesc = ref(null)
const selectedCategory = ref(null)

const allDocs = ref([])
const searchKeyword = ref('')

const UNCATEGORIZED = 'uncategorized'

function formatSize(bytes) {
  if (bytes == null) return ''
  if (bytes < 1024) return `${bytes} B`
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
  return `${(bytes / 1024 / 1024).toFixed(1)} MB`
}

function buildTree() {
  const docs = allDocs.value
  const filtered = searchKeyword.value
    ? docs.filter((d) => {
        const kw = searchKeyword.value.toLowerCase()
        return (
          (d.title || '').toLowerCase().includes(kw) ||
          (d.summary || '').toLowerCase().includes(kw) ||
          (d.category || '').toLowerCase().includes(kw)
        )
      })
    : docs

  // 类目 → 文档映射
  const byCat = {}
  filtered.forEach((d) => {
    const cat = d.category || UNCATEGORIZED
    if (!byCat[cat]) byCat[cat] = []
    byCat[cat].push(d)
  })

  const nodes = []
  // 定义类目顺序：taxonomy 中定义的类目在前，未定义但存在的类目追加
  const defined = categoriesData.value.map((c) => c.name)
  const known = new Set(defined)
  Object.keys(byCat).forEach((cat) => {
    if (!known.has(cat)) known.add(cat)
  })
  defined.forEach((cat) => {
    if (byCat[cat]) {
      nodes.push(makeCategoryNode(cat, byCat[cat]))
    }
  })
  // 未在 taxonomy 中但实际存在的类目
  Object.keys(byCat).forEach((cat) => {
    if (!defined.includes(cat)) {
      nodes.push(makeCategoryNode(cat, byCat[cat]))
    }
  })
  return nodes
}

function makeCategoryNode(cat, docs) {
  return {
    key: `cat:${cat}`,
    title: cat,
    icon: () => h(FolderOutlined),
    isLeaf: docs.length === 0,
    category: cat,
    count: docs.length,
    children: docs.map((d) => ({
      key: `doc:${d.doc_id}`,
      title: d.title || '(无标题)',
      icon: () => h(FileTextOutlined),
      isLeaf: true,
      doc: d
    }))
  }
}

function h(type) {
  return { render: () => _h(type) }
}

function _h(type) {
  return type
}

const treeDataComputed = computed(() => buildTree())

function handleTreeSelect(keys, info) {
  const node = info.selectedNodes?.[0]
  if (!node) return
  const key = node.key
  if (key.startsWith('cat:')) {
    selectedCategory.value = node.category
    categoryDesc.value = categoriesData.value.find((c) => c.name === node.category)?.description || ''
    closeDocDetail()
  } else if (key.startsWith('doc:')) {
    selectedCategory.value = null
    categoryDesc.value = null
    openDocDetail(node.doc)
  }
}

async function loadAll() {
  isLoading.value = true
  try {
    const [cats, stats] = await Promise.all([fetchCategories(), fetchStats()])
    categoriesData.value = cats.categories || []
    // 加载全部文档（分页直到取完）
    const docs = []
    let offset = null
    for (;;) {
      const page = await fetchDocuments(100, offset)
      docs.push(...(page.items || []))
      offset = page.next_offset ?? null
      if (!offset) break
    }
    allDocs.value = docs
    // 更新类目计数（可选）
  } catch (err) {
    message.error(err?.message || '加载知识库失败')
  } finally {
    isLoading.value = false
  }
}

function handleRefresh() {
  loadAll()
}

onMounted(() => {
  loadAll()
})

/* ---------- 文档详情 ---------- */
const docDetailVisible = ref(false)
const docDetailData = ref(null)
const isLoadingDetail = ref(false)
const isDeleting = ref(false)
const isReingesting = ref(false)

function openDocDetail(doc) {
  docDetailVisible.value = true
  docDetailData.value = null
  loadDocDetail(doc.doc_id)
}

function closeDocDetail() {
  docDetailVisible.value = false
  docDetailData.value = null
}

async function loadDocDetail(docId) {
  isLoadingDetail.value = true
  try {
    docDetailData.value = await fetchDocumentDetail(docId)
  } catch (err) {
    message.error(err?.message || '加载文档详情失败')
  } finally {
    isLoadingDetail.value = false
  }
}

function handleDelete(doc) {
  const docId = doc.doc_id
  const title = doc.title || docId
  Modal.confirm({
    title: '确认删除',
    content: `确定删除文档「${title}」(${docId}) 吗？该操作将删除四层集合中的全部数据，不可恢复。`,
    okText: '删除',
    okType: 'danger',
    cancelText: '取消',
    async onOk() {
      isDeleting.value = true
      try {
        await deleteDocument(docId)
        message.success('已删除')
        closeDocDetail()
        await loadAll()
      } catch (err) {
        message.error(err?.message || '删除文档失败')
      } finally {
        isDeleting.value = false
      }
    }
  })
}

function handleReingest(doc) {
  const docId = doc.doc_id
  const title = doc.title || docId
  Modal.confirm({
    title: '重新摘要入库',
    content: `将重新读取文档「${title}」(${docId}) 的原始文件，删除旧数据后重新生成三级总结并入库。生成新的 doc_id。是否继续？`,
    okText: '重新入库',
    cancelText: '取消',
    async onOk() {
      isReingesting.value = true
      try {
        const data = await reingestDocument(docId)
        message.success(`已提交重新入库任务：${data.task_id}`)
      } catch (err) {
        message.error(err?.message || '重新入库失败')
      } finally {
        isReingesting.value = false
      }
    }
  })
}

/* ---------- 入库抽屉 ---------- */
const ingestVisible = ref(false)
const activeTab = ref('text')
const { state: pollState, startPolling, stopPolling } = useIngestPolling()

const textFormRef = ref(null)
const textForm = reactive({ title: '', source: '', text: '' })
const textRules = {
  title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
  text: [{ required: true, message: '请输入正文', trigger: 'blur' }]
}
const isSubmittingText = ref(false)
const isSubmittingFile = ref(false)
const isSubmittingBatch = ref(false)

const fileForm = reactive({ title: '', source: '' })
const fileList = ref([])
const rawFile = ref(null)
const batchResult = ref(null)
const ACCEPTED_EXTENSIONS = '.txt,.md,.html,.htm,.pdf,.docx'

const statusBadgeColor = computed(() => INGEST_STATUS_COLOR[pollState.status] || 'default')
const statusBadgeText = computed(() => INGEST_STATUS_TEXT[pollState.status] || pollState.status || '-')
const isTerminal = computed(() => ['done', 'failed'].includes(pollState.status))
const resultData = computed(() => pollState.task?.result || null)
const errorData = computed(() => pollState.task?.error || pollState.error || null)
const summaryData = computed(() => resultData.value?.summary || {})
const partialSummary = computed(() => errorData.value?.partial_summary || null)

function openIngest() {
  ingestVisible.value = true
}

function handleFileChange(file) {
  rawFile.value = file
  fileList.value = [file]
  return false
}

function handleFileRemove() {
  rawFile.value = null
  fileList.value = []
}

function handleBatchFileChange(_file, fileListArg) {
  fileList.value = fileListArg.map((f) => f.originFileObj || f)
  batchResult.value = null
  return false
}

function handleBatchRemove(file) {
  fileList.value = fileList.value.filter((f) => f !== file && f.name !== file.name)
  batchResult.value = null
}

function handleBatchClear() {
  fileList.value = []
  batchResult.value = null
}

async function handleSubmitText() {
  try {
    await textFormRef.value.validate()
  } catch {
    return
  }
  isSubmittingText.value = true
  try {
    const payload = { title: textForm.title, text: textForm.text }
    if (textForm.source) payload.source = textForm.source
    const data = await ingestDocument(payload)
    message.success('任务已提交')
    startPolling(data.task_id)
  } catch (err) {
    message.error(err?.message || '提交入库失败')
  } finally {
    isSubmittingText.value = false
  }
}

async function handleSubmitFile() {
  if (!rawFile.value) {
    message.warning('请选择文件')
    return
  }
  isSubmittingFile.value = true
  try {
    const formData = new FormData()
    formData.append('file', rawFile.value)
    if (fileForm.title) formData.append('title', fileForm.title)
    if (fileForm.source) formData.append('source', fileForm.source)
    const data = await uploadDocument(formData)
    message.success('任务已提交')
    startPolling(data.task_id)
  } catch (err) {
    message.error(err?.message || '上传入库失败')
  } finally {
    isSubmittingFile.value = false
  }
}

async function handleSubmitBatch() {
  if (!fileList.value.length) {
    message.warning('请选择文件')
    return
  }
  isSubmittingBatch.value = true
  try {
    const data = await uploadBatch(fileList.value)
    batchResult.value = data
    const okCount = data.tasks?.length || 0
    const failCount = data.failed?.length || 0
    if (okCount > 0) {
      message.success(`已提交 ${okCount} 个任务${failCount > 0 ? `，${failCount} 个失败` : ''}`)
    } else if (failCount > 0) {
      message.error(`全部 ${failCount} 个文件上传失败`)
    }
    if (data.tasks?.length === 1 && failCount === 0) {
      startPolling(data.tasks[0].task_id)
    }
  } catch (err) {
    message.error(err?.message || '批量上传失败')
  } finally {
    isSubmittingBatch.value = false
  }
}

function handleCancelPolling() {
  stopPolling()
  message.info('已停止轮询')
}

const batchColumns = [
  { title: '文件名', dataIndex: 'filename', key: 'filename', ellipsis: true },
  { title: 'task_id', dataIndex: 'task_id', key: 'task_id', width: 240 }
]
const failedColumns = [
  { title: '文件名', dataIndex: 'filename', key: 'filename', ellipsis: true },
  { title: '错误', dataIndex: 'error', key: 'error' }
]
</script>

<template>
  <div class="library page-section">
    <div class="library__header">
      <h2 class="library__title">知识库</h2>
      <div class="library__actions">
        <a-input
          v-model:value="searchKeyword"
          placeholder="搜索标题 / 摘要 / 类目"
          allow-clear
          style="width: 260px"
        >
          <template #prefix><SearchOutlined /></template>
        </a-input>
        <a-button :loading="isLoading" @click="handleRefresh">
          <template #icon><ReloadOutlined /></template>
          刷新
        </a-button>
        <a-button @click="tasksDrawer.open()">
          <template #icon><ClockCircleOutlined /></template>
          入库进度
        </a-button>
        <a-button type="primary" @click="openIngest">
          <template #icon><UploadOutlined /></template>
          入库
        </a-button>
      </div>
    </div>

    <div class="library__body">
      <div class="library__tree">
        <a-spin :spinning="isLoading">
          <a-tree
            :tree-data="treeDataComputed"
            :default-expand-all="true"
            :show-icon="true"
            block-node
            @select="handleTreeSelect"
          >
            <template #title="{ title, count, category }">
              <span v-if="category !== undefined" class="library__node-cat">
                <span>{{ title }}</span>
                <span class="text-muted library__node-count">{{ count }}</span>
              </span>
              <span v-else>{{ title }}</span>
            </template>
          </a-tree>
        </a-spin>
      </div>

      <div class="library__detail">
        <!-- 类目说明 -->
        <div v-if="selectedCategory" class="library__panel">
          <h3 class="library__panel-title">
            <TagsOutlined /> {{ selectedCategory }}
          </h3>
          <div class="text-muted">{{ categoryDesc || '（无描述）' }}</div>
        </div>

        <!-- 未选中提示 -->
        <div v-else-if="!docDetailVisible" class="library__placeholder text-muted">
          从左侧选择类目或文档查看详情
        </div>
      </div>
    </div>

    <!-- 文档详情抽屉 -->
    <a-drawer
      :open="docDetailVisible"
      title="文档详情"
      placement="right"
      width="620"
      :destroy-on-close="true"
      @close="closeDocDetail"
    >
      <a-spin :spinning="isLoadingDetail">
        <div v-if="docDetailData">
          <a-descriptions :column="1" size="small" bordered>
            <a-descriptions-item label="doc_id">{{ docDetailData.l1?.doc_id || '-' }}</a-descriptions-item>
            <a-descriptions-item label="标题">{{ docDetailData.l1?.title || '-' }}</a-descriptions-item>
            <a-descriptions-item label="类目">{{ docDetailData.l1?.category || '-' }}</a-descriptions-item>
            <a-descriptions-item label="标签">
              <template v-if="docDetailData.l1?.tags && docDetailData.l1.tags.length">
                <a-tag v-for="tag in docDetailData.l1.tags" :key="tag">{{ tag }}</a-tag>
              </template>
              <span v-else class="text-muted">-</span>
            </a-descriptions-item>
            <a-descriptions-item label="chunks_count">{{ docDetailData.chunks_count ?? 0 }}</a-descriptions-item>
            <a-descriptions-item v-if="docDetailData.file" label="原文文件">
              <a :href="docDetailData.file.url" target="_blank" rel="noopener">
                {{ docDetailData.file.filename }}
                <span v-if="docDetailData.file.size_bytes" class="text-muted">
                  （{{ formatSize(docDetailData.file.size_bytes) }}）
                </span>
              </a>
            </a-descriptions-item>
          </a-descriptions>

          <div class="library__detail-actions">
            <a-button size="small" :loading="isReingesting" @click="handleReingest(docDetailData.l1)">
              重新摘要
            </a-button>
            <a-button size="small" danger :loading="isDeleting" @click="handleDelete(docDetailData.l1)">
              删除
            </a-button>
          </div>

          <h3 class="library__section-title">L1 全文</h3>
          <pre class="library__pre">{{ docDetailData.l1?.text || '' }}</pre>

          <h3 class="library__section-title">L2 节点（{{ (docDetailData.l2_nodes || []).length }}）</h3>
          <div v-if="(docDetailData.l2_nodes || []).length === 0" class="text-muted">无</div>
          <div v-for="(node, idx) in docDetailData.l2_nodes || []" :key="`l2-${idx}`" class="library__node">
            <div class="library__node-path">{{ node.section_path || '' }}</div>
            <div class="text-break">{{ node.text || '' }}</div>
          </div>

          <h3 class="library__section-title">L3 节点（{{ (docDetailData.l3_nodes || []).length }}）</h3>
          <div v-if="(docDetailData.l3_nodes || []).length === 0" class="text-muted">无</div>
          <div v-for="(node, idx) in docDetailData.l3_nodes || []" :key="`l3-${idx}`" class="library__node">
            <div class="library__node-path">{{ node.section_path || '' }}</div>
            <div class="text-break">{{ node.text || '' }}</div>
          </div>
        </div>
        <div v-else-if="!isLoadingDetail" class="text-muted">暂无数据</div>
      </a-spin>
    </a-drawer>

    <!-- 入库抽屉 -->
    <a-drawer
      :open="ingestVisible"
      title="文档入库"
      placement="right"
      width="560"
      :destroy-on-close="true"
      @close="() => (ingestVisible = false)"
    >
      <a-tabs v-model:activeKey="activeTab">
        <a-tab-pane key="text" tab="文本入库">
          <a-form ref="textFormRef" :model="textForm" :rules="textRules" layout="vertical">
            <a-form-item label="标题" name="title">
              <a-input v-model:value="textForm.title" placeholder="请输入标题" allow-clear />
            </a-form-item>
            <a-form-item label="来源" name="source">
              <a-input v-model:value="textForm.source" placeholder="例如：manual / web / file" allow-clear />
            </a-form-item>
            <a-form-item label="正文" name="text">
              <a-textarea v-model:value="textForm.text" placeholder="请输入文档正文" :auto-size="{ minRows: 8, maxRows: 18 }" />
            </a-form-item>
            <a-form-item>
              <a-button type="primary" :loading="isSubmittingText || pollState.isPolling" @click="handleSubmitText">
                提交入库
              </a-button>
            </a-form-item>
          </a-form>
        </a-tab-pane>

        <a-tab-pane key="file" tab="文件上传">
          <a-form :model="fileForm" layout="vertical">
            <a-form-item label="选择文件">
              <a-upload :file-list="fileList" :accept="ACCEPTED_EXTENSIONS" :max-count="1" :before-upload="handleFileChange" @remove="handleFileRemove">
                <a-button :disabled="fileList.length >= 1">选择文件</a-button>
              </a-upload>
              <div class="text-muted" style="margin-top: 4px">支持 .txt/.md/.html/.htm/.pdf/.docx</div>
            </a-form-item>
            <a-form-item label="标题（可选，默认取文件名）" name="title">
              <a-input v-model:value="fileForm.title" placeholder="留空则使用文件名去扩展" allow-clear />
            </a-form-item>
            <a-form-item label="来源（可选，默认 file:原文件名）" name="source">
              <a-input v-model:value="fileForm.source" placeholder="例如：manual / web" allow-clear />
            </a-form-item>
            <a-form-item>
              <a-button type="primary" :loading="isSubmittingFile || pollState.isPolling" @click="handleSubmitFile">
                上传入库
              </a-button>
            </a-form-item>
          </a-form>
        </a-tab-pane>

        <a-tab-pane key="batch" tab="批量上传">
          <a-form :model="fileForm" layout="vertical">
            <a-form-item label="选择文件（可多选）">
              <a-upload multiple :file-list="fileList" :accept="ACCEPTED_EXTENSIONS" :before-upload="handleBatchFileChange" @remove="handleBatchRemove">
                <a-button>选择文件</a-button>
              </a-upload>
              <div class="text-muted" style="margin-top: 4px">支持 .txt/.md/.html/.htm/.pdf/.docx，可多选，逐文件异步入库</div>
            </a-form-item>
            <a-form-item v-if="fileList.length">
              <a-button type="primary" :loading="isSubmittingBatch" @click="handleSubmitBatch">
                批量上传入库（{{ fileList.length }} 个文件）
              </a-button>
              <a-button style="margin-left: 8px" :disabled="isSubmittingBatch" @click="handleBatchClear">清空</a-button>
            </a-form-item>
          </a-form>

          <div v-if="batchResult" class="library__batch">
            <a-alert
              v-if="batchResult.failed && batchResult.failed.length"
              class="library__alert"
              type="error"
              show-icon
              :message="`${batchResult.failed.length} 个文件上传失败`"
            />
            <a-table
              v-if="batchResult.tasks && batchResult.tasks.length"
              :data-source="batchResult.tasks"
              :columns="batchColumns"
              :pagination="false"
              size="small"
              row-key="task_id"
            >
              <template #bodyCell="{ column, record }">
                <template v-if="column.key === 'task_id'">
                  <a-typography-text copyable :style="{ fontSize: '12px' }">{{ record.task_id }}</a-typography-text>
                </template>
              </template>
            </a-table>
            <a-table
              v-if="batchResult.failed && batchResult.failed.length"
              :data-source="batchResult.failed"
              :columns="failedColumns"
              :pagination="false"
              size="small"
              row-key="filename"
              style="margin-top: 12px"
            >
              <template #bodyCell="{ column, record }">
                <template v-if="column.key === 'error'">
                  <span class="text-break">{{ record.error }}</span>
                </template>
              </template>
            </a-table>
          </div>
        </a-tab-pane>
      </a-tabs>

      <div v-if="pollState.taskId" class="library__result">
        <div class="library__result-header">
          <span>任务已提交：{{ pollState.taskId }}</span>
          <a-tag :color="statusBadgeColor">{{ statusBadgeText }}</a-tag>
          <a-button v-if="pollState.isPolling" type="link" size="small" @click="handleCancelPolling">停止轮询</a-button>
        </div>
        <a-alert
          v-if="pollState.isTimeout"
          class="library__alert"
          type="warning"
          show-icon
          :message="`任务仍在进行，可稍后凭 task_id 查询：${pollState.taskId}`"
        />
        <div v-if="isTerminal && pollState.status === 'done' && resultData" class="library__done">
          <a-descriptions :column="1" size="small" bordered>
            <a-descriptions-item label="document_id">{{ resultData.document_id || '-' }}</a-descriptions-item>
            <a-descriptions-item label="类目">{{ resultData.category || '-' }}（置信度 {{ resultData.category_confidence ?? '-' }}）</a-descriptions-item>
            <a-descriptions-item label="标签">
              <template v-if="resultData.tags && resultData.tags.length">
                <a-tag v-for="tag in resultData.tags" :key="tag">{{ tag }}</a-tag>
              </template>
              <span v-else class="text-muted">-</span>
            </a-descriptions-item>
            <a-descriptions-item label="总结层级">{{ summaryData.level ?? '-' }}</a-descriptions-item>
            <a-descriptions-item label="chunks_count">{{ resultData.chunks_count ?? 0 }}</a-descriptions-item>
          </a-descriptions>
          <h3 class="library__section-title">L1 总结</h3>
          <pre class="library__pre">{{ summaryData.l1_summary || '' }}</pre>
        </div>
        <div v-if="isTerminal && pollState.status === 'failed'" class="library__failed">
          <a-descriptions :column="1" size="small" bordered>
            <a-descriptions-item label="失败阶段">{{ errorData?.stage || '-' }}</a-descriptions-item>
            <a-descriptions-item label="错误信息"><span class="text-break">{{ errorData?.message || '-' }}</span></a-descriptions-item>
          </a-descriptions>
          <template v-if="partialSummary && partialSummary.l1_summary">
            <h3 class="library__section-title">L1 总结</h3>
            <pre class="library__pre">{{ partialSummary.l1_summary }}</pre>
          </template>
        </div>
      </div>
    </a-drawer>
  </div>
</template>

<style scoped>
.library__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
  flex-wrap: wrap;
  gap: 8px;
}

.library__title {
  font-size: 16px;
  margin: 0;
}

.library__actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.library__body {
  display: flex;
  gap: 16px;
  min-height: 480px;
}

.library__tree {
  width: 320px;
  flex: 0 0 auto;
  background: #fff;
  border: 1px solid #f0f0f0;
  border-radius: 8px;
  padding: 12px;
  max-height: 70vh;
  overflow: auto;
}

.library__node-cat {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.library__node-count {
  font-size: 12px;
}

.library__detail {
  flex: 1;
  min-width: 0;
}

.library__panel {
  background: #fff;
  border: 1px solid #f0f0f0;
  border-radius: 8px;
  padding: 16px;
}

.library__panel-title {
  font-size: 15px;
  margin: 0 0 8px;
  display: flex;
  align-items: center;
  gap: 6px;
}

.library__placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  min-height: 300px;
  border: 1px dashed #d9d9d9;
  border-radius: 8px;
}

.library__detail-actions {
  display: flex;
  gap: 8px;
  margin: 12px 0;
}

.library__section-title {
  font-size: 14px;
  margin: 16px 0 8px;
  color: #374151;
}

.library__pre {
  background: #f9fafb;
  border: 1px solid #e5e7eb;
  border-radius: 4px;
  padding: 8px;
  white-space: pre-wrap;
  word-break: break-word;
  font-size: 12px;
  max-height: 260px;
  overflow: auto;
  margin: 0;
}

.library__node {
  border-left: 3px solid #93c5fd;
  padding: 6px 10px;
  margin-bottom: 6px;
  background: #f8fafc;
  border-radius: 0 4px 4px 0;
}

.library__node-path {
  font-size: 12px;
  color: #6b7280;
  margin-bottom: 4px;
}

.library__result {
  margin-top: 16px;
  border: 1px solid #e5e7eb;
  border-radius: 6px;
  background: #fafafa;
  padding: 12px 16px;
}

.library__result-header {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font-size: 13px;
  margin-bottom: 8px;
}

.library__alert {
  margin: 8px 0;
}

.library__batch {
  margin-top: 8px;
}

@media (max-width: 768px) {
  .library__body {
    flex-direction: column;
  }
  .library__tree {
    width: 100%;
  }
}
</style>