<template>
  <div class="graph-explorer">
    <a-row :gutter="12" class="graph-layout">
      <!-- ── Left panel: search + filters + detail ── -->
      <a-col :span="6" class="left-panel">
        <!-- Search -->
        <a-card title="实体搜索" size="small" :body-style="{ padding: '12px' }">
          <a-input-search
            v-model:value="searchQuery"
            placeholder="输入实体名称搜索…"
            allow-clear
            :loading="searching"
            @search="handleSearch"
          />
          <!-- Label filter chips -->
          <div class="label-chips mt-8">
            <a-tag
              v-for="t in NODE_TYPE_LIST"
              :key="t.value"
              :color="selectedLabelFilter === t.value ? t.color : 'default'"
              style="cursor: pointer; margin: 2px"
              @click="toggleLabelFilter(t.value)"
            >
              {{ t.label }}
            </a-tag>
          </div>

          <!-- Search results -->
          <div v-if="searchResults.length" class="search-results mt-8">
            <div
              v-for="item in searchResults"
              :key="item.id"
              class="search-result-item"
              @click="loadEntityGraph(item.id, item.name)"
            >
              <a-tag :color="getLabelColor(item.labels[0])" class="mr-4">
                {{ getNodeTypeZh(item.labels[0]) }}
              </a-tag>
              <span>{{ item.name }}</span>
            </div>
          </div>
        </a-card>

        <!-- Node type legend / visibility -->
        <a-card title="节点类型" size="small" :body-style="{ padding: '12px' }" class="mt-12">
          <a-checkbox-group v-model:value="visibleTypes" @change="applyFilters">
            <div v-for="t in NODE_TYPE_LIST" :key="t.value" class="type-row">
              <a-checkbox :value="t.value">
                <a-badge :color="t.color" :text="t.label" />
              </a-checkbox>
            </div>
          </a-checkbox-group>
        </a-card>

        <!-- Selected node detail -->
        <a-card
          v-if="selectedNode"
          title="节点详情"
          size="small"
          :body-style="{ padding: '12px' }"
          class="mt-12"
        >
          <!-- ── Node basic info (all types) ── -->
          <a-descriptions :column="1" size="small">
            <a-descriptions-item label="名称">
              {{ selectedNode.properties?.name || selectedNode.properties?.title || '—' }}
            </a-descriptions-item>
            <a-descriptions-item label="类型">
              <a-tag :color="getLabelColor(selectedNodeType)">
                {{ getNodeTypeZh(selectedNodeType) }}
              </a-tag>
            </a-descriptions-item>
            <template v-if="selectedNode.properties?.doc_number">
              <a-descriptions-item label="文号">
                {{ selectedNode.properties.doc_number }}
              </a-descriptions-item>
            </template>
            <template v-if="selectedNode.properties?.publish_date">
              <a-descriptions-item label="日期">
                {{ selectedNode.properties.publish_date }}
              </a-descriptions-item>
            </template>
          </a-descriptions>

          <div class="node-actions mt-8">
            <a-button
              v-if="CARD_TYPES.has(selectedNodeType)"
              size="small"
              type="primary"
              :loading="cardLoading"
              @click="openCardModal"
            >
              查看知识卡
            </a-button>
            <a-button
              v-if="selectedNodeType === 'Document'"
              size="small"
              type="primary"
              ghost
              @click="openSelectedDocument"
            >
              查看文档
            </a-button>
            <a-button
              v-if="selectedNodeType === 'Document'"
              size="small"
              style="margin-left: 6px"
              @click="addSelectedDocumentToBasket"
            >
              加入资料篮
            </a-button>
            <a-button
              size="small"
              style="margin-left: 6px"
              @click="expandNode(selectedNode.id)"
            >
              展开邻居
            </a-button>
          </div>

          <!-- Related documents for non-Document nodes (non-card types) -->
          <template v-if="selectedNodeType !== 'Document' && !cardData && relatedDocs.length">
            <a-divider style="margin: 8px 0" />
            <div class="related-title">关联公文</div>
            <div
              v-for="doc in relatedDocs.slice(0, 5)"
              :key="doc.doc_id"
              class="related-doc-item"
            >
              <div class="related-doc-main" @click="openDocById(doc.doc_id)">
                <a-tag size="small">{{ getRelTypeZh(doc.rel_type) }}</a-tag>
                <span class="doc-title">{{ doc.title || doc.doc_number }}</span>
              </div>
              <a-button type="link" size="small" @click.stop="addRelatedDocToBasket(doc)">加入</a-button>
            </div>
          </template>
        </a-card>
      </a-col>

      <!-- ── Right panel: G6 canvas ── -->
      <a-col :span="18" class="canvas-col">
        <a-card :body-style="{ padding: 0, height: '100%' }" class="canvas-card">
          <!-- Toolbar -->
          <div class="canvas-toolbar">
            <a-space>
              <a-button size="small" @click="resetZoom">适应画布</a-button>
              <a-button size="small" @click="loadOverview">全局概览</a-button>
              <a-button
                v-if="hasMore"
                size="small"
                :loading="loadingMore"
                @click="loadMore"
              >
                加载更多
              </a-button>
              <a-button
                v-if="graphData.nodes.length"
                size="small"
                danger
                @click="clearGraph"
              >
                清空画布
              </a-button>
              <a-divider type="vertical" />
              <a-badge
                :color="graphData.nodes.length > 3000 ? 'red' : graphData.nodes.length > 1000 ? 'orange' : 'green'"
                :text="`节点 ${graphData.nodes.length} | 边 ${graphData.edges.length}${totalCount ? ` / 总计 ${totalCount}` : ''}`"
              />
            </a-space>
          </div>

          <!-- Canvas container -->
          <div ref="canvasRef" class="g6-canvas" />

          <!-- Empty state -->
          <div v-if="!graphData.nodes.length && !loading" class="empty-state">
            <a-empty description="搜索实体或点击「全局概览」加载图谱" />
          </div>

          <!-- Loading overlay -->
          <div v-if="loading" class="loading-overlay">
            <a-spin tip="加载图谱数据…" />
          </div>
        </a-card>
      </a-col>
    </a-row>

    <!-- ── Entity knowledge card modal ── -->
    <a-modal
      v-model:open="cardModalVisible"
      :title="cardModalTitle"
      :footer="null"
      width="720px"
      destroy-on-close
    >
      <a-spin :spinning="cardLoading">
        <PolicyCard
          v-if="selectedNodeType === 'Policy' && cardData"
          :card="(cardData as PolicyCardType)"
          :loading="false"
          @open-document="(id: string) => { cardModalVisible = false; openDocById(id) }"
        />
        <TaskCard
          v-else-if="selectedNodeType === 'Task' && cardData"
          :card="(cardData as TaskCardType)"
          :loading="false"
          @open-document="(id: string) => { cardModalVisible = false; openDocById(id) }"
        />
        <ProjectCard
          v-else-if="selectedNodeType === 'Project' && cardData"
          :card="(cardData as ProjectCardType)"
          :loading="false"
          @open-document="(id: string) => { cardModalVisible = false; openDocById(id) }"
        />
        <MatterCardComp
          v-else-if="selectedNodeType === 'Matter' && cardData"
          :matter="(cardData as MatterCardResponse)"
          :loading="false"
          @open-document="(id: string) => { cardModalVisible = false; openDocById(id) }"
        />
        <SystemCardComp
          v-else-if="selectedNodeType === 'System' && cardData"
          :card="(cardData as SystemCardType)"
          :loading="false"
          @open-document="(id: string) => { cardModalVisible = false; openDocById(id) }"
        />
        <DataResourceCardComp
          v-else-if="selectedNodeType === 'DataResource' && cardData"
          :card="(cardData as DataResourceCardType)"
          :loading="false"
          @open-document="(id: string) => { cardModalVisible = false; openDocById(id) }"
        />
        <BudgetCardComp
          v-else-if="selectedNodeType === 'Budget' && cardData"
          :card="(cardData as BudgetCardType)"
          :loading="false"
          @open-document="(id: string) => { cardModalVisible = false; openDocById(id) }"
        />
        <IndicatorCardComp
          v-else-if="selectedNodeType === 'Indicator' && cardData"
          :card="(cardData as IndicatorCardType)"
          :loading="false"
          @open-document="(id: string) => { cardModalVisible = false; openDocById(id) }"
        />
        <IndustryCardComp
          v-else-if="selectedNodeType === 'Industry' && cardData"
          :card="(cardData as IndustryCardType)"
          :loading="false"
          @open-document="(id: string) => { cardModalVisible = false; openDocById(id) }"
        />
        <a-empty v-else-if="!cardLoading" description="暂无卡片数据" />
      </a-spin>
    </a-modal>
  </div>
</template>

/**
 * 知识图谱探索视图
 * 基于 AntV G6 实现交互式知识图谱可视化。
 * 左侧面板提供实体搜索、节点类型筛选、选中节点详情和关联文档。
 * 右侧为 G6 画布，支持缩放、拖拽、节点展开邻居等交互。
 *
 * Sprint 5 改造：
 * - 响应式类型数据源（NODE_TYPE_LIST computed + loadTypesFromApi）
 * - 按标签分发实体卡片（Policy/Task/Project）
 * - 统一选中流程 hydrateSelectedNode
 * - 中文标签展示 + entity_key 主键定位
 */
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { message } from 'ant-design-vue'
import { useRouter } from 'vue-router'
import {
  getEntityDetail,
  getGraphOverview,
  getRelatedDocs,
  searchEntities,
  getPolicyCard,
  getTaskCard,
  getProjectCard,
  getSystemCard,
  getDataResourceCard,
  getBudgetCard,
  getIndicatorCard,
  getIndustryCard,
} from '@/api/graph'
import type {
  PolicyCard as PolicyCardType,
  TaskCard as TaskCardType,
  ProjectCard as ProjectCardType,
  SystemCard as SystemCardType,
  DataResourceCard as DataResourceCardType,
  BudgetCard as BudgetCardType,
  IndicatorCard as IndicatorCardType,
  IndustryCard as IndustryCardType,
} from '@/api/graph'
import { getMatterCard } from '@/api/matter'
import type { MatterCardResponse } from '@/types/matter'
import { useBasketStore } from '@/stores/basket'
import { escapeHtml } from '@/utils/sanitize'
import { useG6Graph } from '@/composables/useG6Graph'
import type { G6NodeData } from '@/types/graph'
import type { GraphData, GraphNode, EntitySearchResult, RelatedDocItem } from '@/types/graph'
import {
  getNodeColor,
  getNodeTypeZh,
  getRelTypeZh,
  loadTypesFromApi,
  NODE_TYPE_LIST,
  nodeTypeState,
} from '@/utils/graphLabels'

// Card components
import PolicyCard from '@/components/PolicyCard.vue'
import TaskCard from '@/components/TaskCard.vue'
import ProjectCard from '@/components/ProjectCard.vue'
import MatterCardComp from '@/components/MatterCard.vue'
import SystemCardComp from '@/components/SystemCard.vue'
import DataResourceCardComp from '@/components/DataResourceCard.vue'
import BudgetCardComp from '@/components/BudgetCard.vue'
import IndicatorCardComp from '@/components/IndicatorCard.vue'
import IndustryCardComp from '@/components/IndustryCard.vue'

// Types with card support — clicking these nodes loads a dedicated card
const CARD_TYPES = new Set([
  'Policy', 'Task', 'Project', 'Matter',
  'System', 'DataResource', 'Budget', 'Indicator', 'Industry',
])

// ---------------------------------------------------------------------------
// Reactive state
// ---------------------------------------------------------------------------

const router = useRouter()
const basketStore = useBasketStore()

const canvasRef = ref<HTMLDivElement>()
const loading = ref(false)
const searching = ref(false)

const searchQuery = ref('')
const searchResults = ref<EntitySearchResult[]>([])
const selectedLabelFilter = ref<string | null>(null)
const visibleTypes = ref<string[]>(NODE_TYPE_LIST.value.map((t) => t.value))

const graphData = ref<GraphData>({ nodes: [], edges: [] })
const selectedNode = ref<GraphNode | null>(null)
const relatedDocs = ref<RelatedDocItem[]>([])

// Card state for Policy/Task/Project
const cardData = ref<PolicyCardType | TaskCardType | ProjectCardType | MatterCardResponse | SystemCardType | DataResourceCardType | BudgetCardType | IndicatorCardType | IndustryCardType | null>(null)
const cardLoading = ref(false)
const cardModalVisible = ref(false)

const cardModalTitle = computed(() => {
  if (!selectedNode.value) return '知识卡'
  const zh = getNodeTypeZh(selectedNodeType.value)
  const name = selectedNode.value.properties?.name || ''
  return `${zh}：${name}`
})

// 渐进式加载状态
const currentCursor = ref<string | null>(null)
const hasMore = ref(false)
const loadingMore = ref(false)
const totalCount = ref(0)

// ---------------------------------------------------------------------------
// Computed
// ---------------------------------------------------------------------------

const selectedNodeType = computed(() => {
  if (!selectedNode.value) return ''
  return selectedNode.value._type || getPrimaryLabel(selectedNode.value)
})

// ---------------------------------------------------------------------------
// Watch: sync new types into visibleTypes when loadTypesFromApi adds them
// ---------------------------------------------------------------------------

watch(NODE_TYPE_LIST, (newList) => {
  const current = new Set(visibleTypes.value)
  for (const item of newList) {
    if (!current.has(item.value)) {
      visibleTypes.value.push(item.value)
    }
  }
})

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function getLabelColor(label: string): string {
  return getNodeColor(label)
}

function toggleLabelFilter(value: string) {
  selectedLabelFilter.value = selectedLabelFilter.value === value ? null : value
}

function getPrimaryLabel(node: GraphNode): string {
  return (node.labels || [])[0] || 'Unknown'
}

/** Resolve entity_key for a node based on its label's key_property */
function resolveEntityKey(node: GraphNode): string | undefined {
  const label = node._type || getPrimaryLabel(node)
  const keyProp = nodeTypeState.keyProperties[label] || 'name'
  return node.properties?.[keyProp]
}

function openDocById(docId: string) {
  router.push(`/doc/${docId}`)
}

// ---------------------------------------------------------------------------
// Unified node selection — all entry points flow through here
// ---------------------------------------------------------------------------

async function hydrateSelectedNode(node: GraphNode | undefined) {
  if (!node) return
  // Wait for type metadata (keyProperties) before resolving entity keys
  if (!typesReady.value) {
    try { await loadTypesFromApi() } catch { /* use defaults */ }
    typesReady.value = true
  }
  selectedNode.value = node
  relatedDocs.value = []
  cardData.value = null
  cardModalVisible.value = false

  const nodeType = node._type || getPrimaryLabel(node)
  if (nodeType === 'Document') return

  // Card types: don't auto-load card data; user clicks "查看知识卡" button
  if (CARD_TYPES.has(nodeType)) return

  // For other non-Document types, load related docs with entity_key
  try {
    const entityKey = resolveEntityKey(node)
    relatedDocs.value = await getRelatedDocs(
      node.properties?.name || '',
      nodeType,
      10,
      entityKey,
    )
  } catch {
    relatedDocs.value = []
  }
}

/** Open the knowledge card modal and fetch card data on demand */
async function openCardModal() {
  if (!selectedNode.value) return
  const node = selectedNode.value
  const nodeType = node._type || getPrimaryLabel(node)
  if (!CARD_TYPES.has(nodeType)) return

  cardModalVisible.value = true
  cardLoading.value = true
  cardData.value = null
  try {
    const entityKey = resolveEntityKey(node) || node.properties?.name || ''
    if (nodeType === 'Policy') {
      cardData.value = await getPolicyCard(entityKey)
    } else if (nodeType === 'Task') {
      cardData.value = await getTaskCard(entityKey)
    } else if (nodeType === 'Project') {
      cardData.value = await getProjectCard(entityKey)
    } else if (nodeType === 'Matter') {
      const matterId = node.properties?.matter_id || node.id
      cardData.value = await getMatterCard(matterId)
    } else if (nodeType === 'System') {
      cardData.value = await getSystemCard(entityKey)
    } else if (nodeType === 'DataResource') {
      cardData.value = await getDataResourceCard(entityKey)
    } else if (nodeType === 'Budget') {
      cardData.value = await getBudgetCard(entityKey)
    } else if (nodeType === 'Indicator') {
      cardData.value = await getIndicatorCard(entityKey)
    } else if (nodeType === 'Industry') {
      cardData.value = await getIndustryCard(entityKey)
    }
  } catch {
    cardData.value = null
  } finally {
    cardLoading.value = false
  }
}

// ---------------------------------------------------------------------------
// G6 graph composable
// ---------------------------------------------------------------------------

const { graphInstance, initGraph, renderGraph, addGraphData, fitView, destroy: destroyGraph } = useG6Graph({
  layoutType: 'preset',
  rendererMode: 'canvas',
  enableLOD: true,
  layoutOverrides: {},
  plugins: [{
    type: 'tooltip',
    getContent: (_e: unknown, items: G6NodeData[]) => {
      if (!items.length) return ''
      const d = items[0]
      return `<div style="padding:4px 8px">${escapeHtml(d._label || d.id)}</div>`
    },
  }],
  onNodeClick: async (nodeData: GraphNode) => {
    await hydrateSelectedNode(nodeData)
  },
})

async function doRenderGraph(data: GraphData) {
  await renderGraph(data.nodes, data.edges, visibleTypes.value)
}

function applyFilters() {
  if (graphData.value.nodes.length) {
    doRenderGraph(graphData.value)
  }
}

function resetZoom() {
  fitView()
}

function openSelectedDocument() {
  const docId = selectedNode.value?.properties?.doc_id
  if (docId) {
    router.push(`/doc/${docId}`)
  }
}

function addSelectedDocumentToBasket() {
  const props = selectedNode.value?.properties || {}
  const docId = props.doc_id
  if (!docId) return

  const added = basketStore.addItem({
    item_type: 'document',
    title: props.title || props.name || '未命名文档',
    selected_from: 'graph',
    importance: 'reference',
    doc_id: docId,
    content_hash: props.content_hash,
    doc_code: props.doc_code || props.doc_number,
  })

  if (added) {
    message.success('已加入资料篮')
  } else {
    message.warning('该资料已存在于资料篮中')
  }
}

function addRelatedDocToBasket(doc: RelatedDocItem) {
  const added = basketStore.addItem({
    item_type: 'document',
    title: doc.title || doc.doc_number || '未命名文档',
    selected_from: 'graph',
    importance: 'reference',
    doc_id: doc.doc_id,
    doc_code: doc.doc_number,
  })

  if (added) {
    message.success('已加入资料篮')
  } else {
    message.warning('该资料已存在于资料篮中')
  }
}

// ---------------------------------------------------------------------------
// Data loading
// ---------------------------------------------------------------------------

async function loadOverview() {
  loading.value = true
  currentCursor.value = null
  hasMore.value = false
  totalCount.value = 0
  try {
    const data = await getGraphOverview(undefined, undefined, 500)
    graphData.value = { nodes: data.nodes, edges: data.edges }
    currentCursor.value = data.next_cursor ?? null
    hasMore.value = !!data.next_cursor
    totalCount.value = data.total_count ?? 0
    await doRenderGraph(graphData.value)
  } catch {
    message.error('加载图谱概览失败')
  } finally {
    loading.value = false
  }
}

async function loadMore() {
  if (!currentCursor.value || loadingMore.value) return
  loadingMore.value = true
  try {
    const data = await getGraphOverview(undefined, undefined, 500, currentCursor.value)
    const existingIds = new Set(graphData.value.nodes.map((n) => n.id))
    const newNodes = data.nodes.filter((n) => !existingIds.has(n.id))
    const existingEdges = new Set(
      graphData.value.edges.map((e) => `${e.source}-${e.type}-${e.target}`),
    )
    const newEdges = data.edges.filter(
      (e) => !existingEdges.has(`${e.source}-${e.type}-${e.target}`),
    )
    graphData.value = {
      nodes: [...graphData.value.nodes, ...newNodes],
      edges: [...graphData.value.edges, ...newEdges],
    }
    currentCursor.value = data.next_cursor ?? null
    hasMore.value = !!data.next_cursor
    await addGraphData(newNodes, newEdges, visibleTypes.value)
  } catch {
    message.error('加载更多数据失败')
  } finally {
    loadingMore.value = false
  }
}

function clearGraph() {
  graphData.value = { nodes: [], edges: [] }
  currentCursor.value = null
  hasMore.value = false
  totalCount.value = 0
  selectedNode.value = null
  relatedDocs.value = []
  cardData.value = null
  doRenderGraph(graphData.value)
}

async function handleSearch(query: string) {
  if (!query.trim()) {
    searchResults.value = []
    return
  }
  searching.value = true
  try {
    searchResults.value = await searchEntities(
      query,
      selectedLabelFilter.value ?? undefined,
      15,
    )
  } catch {
    message.error('搜索失败')
    searchResults.value = []
  } finally {
    searching.value = false
  }
}

/**
 * 搜索结果点击：清空画布后加载该实体的子图（全新渲染）。
 */
async function loadEntityGraph(entityId: string, _name: string) {
  loading.value = true
  try {
    const resp = await getEntityDetail(entityId, 2, 300)
    const { graph } = resp

    // 清空旧数据，全量替换
    graphData.value = { nodes: graph.nodes, edges: graph.edges }
    selectedNode.value = null
    relatedDocs.value = []
    cardData.value = null

    await doRenderGraph(graphData.value)

    // Select the clicked node — go through unified hydration
    const found = graph.nodes.find((n) => n.id === entityId)
    if (found) {
      const hydrated = { ...found, _type: getPrimaryLabel(found) }
      await hydrateSelectedNode(hydrated)
    }
  } catch {
    message.error('加载实体图谱失败')
  } finally {
    loading.value = false
  }
}

/**
 * 展开邻居：增量合并到现有图谱中。
 */
async function expandNode(entityId: string) {
  loading.value = true
  try {
    const resp = await getEntityDetail(entityId, 2, 300)
    const { graph } = resp

    const existingIds = new Set(graphData.value.nodes.map((n) => n.id))
    const newNodes = graph.nodes.filter((n) => !existingIds.has(n.id))
    const mergedNodes = [...graphData.value.nodes, ...newNodes]

    const existingEdgeKeys = new Set(
      graphData.value.edges.map((e) => `${e.source}-${e.type}-${e.target}`),
    )
    const newEdges = graph.edges.filter(
      (e) => !existingEdgeKeys.has(`${e.source}-${e.type}-${e.target}`),
    )
    const mergedEdges = [...graphData.value.edges, ...newEdges]

    graphData.value = { nodes: mergedNodes, edges: mergedEdges }
    await doRenderGraph(graphData.value)

    const found = mergedNodes.find((n) => n.id === entityId)
    if (found) {
      const hydrated = { ...found, _type: getPrimaryLabel(found) }
      await hydrateSelectedNode(hydrated)
    }
  } catch {
    message.error('展开邻居失败')
  } finally {
    loading.value = false
  }
}

// ---------------------------------------------------------------------------
// Lifecycle
// ---------------------------------------------------------------------------

let resizeObserver: ResizeObserver | null = null

const typesReady = ref(false)

onMounted(async () => {
  // Load dynamic types from API (populates reactive nodeTypeState)
  // Must await so keyProperties are available before any node interaction
  try {
    await loadTypesFromApi()
  } catch {
    // Non-fatal: fallback defaults are already in place
  }
  typesReady.value = true

  if (!canvasRef.value) return
  await initGraph(canvasRef.value, canvasRef.value.clientWidth, canvasRef.value.clientHeight)
  await loadOverview()

  resizeObserver = new ResizeObserver(() => {
    if (canvasRef.value && graphInstance.value) {
      graphInstance.value.resize(canvasRef.value.clientWidth, canvasRef.value.clientHeight)
    }
  })
  resizeObserver.observe(canvasRef.value)
})

onUnmounted(() => {
  resizeObserver?.disconnect()
  resizeObserver = null
  destroyGraph()
})
</script>

<style scoped lang="less">
.graph-explorer {
  padding: 12px;
  height: calc(100vh - 64px);
  overflow: hidden;

  .graph-layout {
    height: 100%;
  }
}

.left-panel {
  height: 100%;
  overflow-y: auto;

  .mt-8 { margin-top: 8px; }
  .mt-12 { margin-top: 12px; }
  .mr-4 { margin-right: 4px; }

  .label-chips {
    display: flex;
    flex-wrap: wrap;
  }

  .search-results {
    max-height: 240px;
    overflow-y: auto;
  }

  .search-result-item {
    padding: 4px 6px;
    cursor: pointer;
    border-radius: 4px;
    font-size: 12px;
    display: flex;
    align-items: center;

    &:hover {
      background: #f5f5f5;
    }
  }

  .type-row {
    padding: 2px 0;
  }

  .node-actions {
    display: flex;
    align-items: center;
  }

  .related-title {
    font-size: 12px;
    font-weight: 600;
    color: #666;
    margin-bottom: 6px;
  }

  .related-doc-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 3px 0;
    font-size: 12px;

    .related-doc-main {
      display: flex;
      align-items: center;
      gap: 4px;
      cursor: pointer;
      flex: 1;
      min-width: 0;
    }

    .doc-title {
      flex: 1;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      color: #1677ff;

      &:hover {
        text-decoration: underline;
      }
    }
  }
}

.canvas-col {
  height: 100%;

  .canvas-card {
    height: 100%;
    position: relative;
    overflow: hidden;
  }
}

.canvas-toolbar {
  padding: 8px 12px;
  border-bottom: 1px solid #f0f0f0;
  background: #fafafa;

  .stats-text {
    font-size: 12px;
    color: #8c8c8c;
  }
}

.g6-canvas {
  width: 100%;
  height: calc(100% - 42px);
}

.empty-state {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}

.loading-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.7);
  z-index: 10;
}
</style>
