<template>
  <div class="node-editor-wrapper">
    <div class="node-editor">
      <!-- 顶部工具栏 -->
    <div class="editor-toolbar">
      <h2 class="editor-title">{{ topSectionTitle }}</h2>
      <div class="toolbar-actions">
        <LanguageSwitcher />

        <button
          type="button"
          class="toolbar-btn"
          @click="goBack"
          :title="t('editor.backTitle')"
        >
          {{ t('editor.back') }}
        </button>
        <button
          v-if="showRunButton"
          class="toolbar-btn primary"
          @click="executeWorkflow"
          :disabled="isExecuting"
          :title="projectId ? '' : t('editor.missingProjectIdTitle')"
        >
          <svg v-if="!isExecuting" viewBox="0 0 24 24" fill="none">
            <polygon points="5,3 19,12 5,21" fill="currentColor"/>
          </svg>
          <svg v-else class="spinning" viewBox="0 0 24 24" fill="none">
            <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" stroke-dasharray="31.416" stroke-dashoffset="31.416">
              <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416;0 31.416" repeatCount="indefinite"/>
              <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416;-31.416" repeatCount="indefinite"/>
            </circle>
          </svg>
          {{ isExecuting ? t('editor.parsing') : t('editor.parse') }}
        </button>
        <button
          v-if="showDownloadButton"
          class="toolbar-btn"
          @click="downloadAnalysisExport"
          :disabled="isExecuting || isDownloading"
          :title="t('editor.downloadTitle')"
        >
          <svg v-if="!isDownloading" viewBox="0 0 24 24" fill="none">
            <path d="M12 3v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
            <path d="M8 11l4 4 4-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
            <path d="M5 21h14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
          </svg>
          <svg v-else class="spinning" viewBox="0 0 24 24" fill="none">
            <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" stroke-dasharray="31.416" stroke-dashoffset="31.416">
              <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416;0 31.416" repeatCount="indefinite"/>
              <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416;-31.416" repeatCount="indefinite"/>
            </circle>
          </svg>
          {{ isDownloading ? t('editor.downloading') : t('editor.download') }}
        </button>
      </div>
    </div>

    <!-- 主内容区域 -->
    <div class="editor-main">
      <!-- 左侧参数树（点击条目直接在画布生成节点） -->
      <aside class="node-palette">
        <div class="palette-header">
          <div class="palette-flow">
            <div class="palette-flow-title">{{ t('editor.flowTitle') }}</div>
            <div class="palette-flow-steps" role="tablist" :aria-label="t('editor.flowAria')">
              <button
                type="button"
                class="flow-step"
                role="tab"
                :class="{ active: topSection === 'preprocess' }"
                :aria-selected="topSection === 'preprocess'"
                @click="topSection = 'preprocess'"
              >
                <span class="flow-step-indicator" aria-hidden="true"></span>
                <span class="flow-step-num">1</span>
                <span class="flow-step-label">{{ t('editor.sectionPreprocess') }}</span>
              </button>
              <button
                type="button"
                class="flow-step"
                role="tab"
                :class="{ active: topSection === 'metaFree' }"
                :aria-selected="topSection === 'metaFree'"
                @click="topSection = 'metaFree'"
              >
                <span class="flow-step-indicator" aria-hidden="true"></span>
                <span class="flow-step-num">2</span>
                <span class="flow-step-label">{{ t('editor.sectionMetaFree') }}</span>
              </button>
              <button
                type="button"
                class="flow-step"
                role="tab"
                :class="{ active: topSection === 'meta' }"
                :aria-selected="topSection === 'meta'"
                @click="topSection = 'meta'"
              >
                <span class="flow-step-indicator" aria-hidden="true"></span>
                <span class="flow-step-num">3</span>
                <span class="flow-step-label">{{ t('editor.sectionMetaBased') }}</span>
              </button>
            </div>
          </div>
          <div class="palette-modules-head">
            <span class="palette-modules-title">{{ t('editor.modulesTitle') }}</span>
          </div>
        </div>

        <div class="palette-content" role="tree" :aria-label="t('editor.treeAria')">
          <!-- 2. 数据预处理 -->
          <template v-if="topSection === 'preprocess'">
          <button class="tree-sub-head" type="button" @click="toggleExpanded('qc')">
            <span class="tree-cat-icon" aria-hidden="true" v-html="paletteIconHtml('qc')"></span>
            <span class="caret" :class="{ open: expanded.qc }" aria-hidden="true">▸</span>
            <span class="tree-sub-head-label">{{ t('editor.palette.qc') }}</span>
          </button>
          <div v-if="expanded.qc" class="tree-leaves">
            <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('qc-euclidean')">{{ t('editor.palette.euclidean') }}</button>
            <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('qc-snr')">{{ t('editor.palette.snr') }}</button>
            <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('qc-icod')">{{ t('editor.palette.icod') }}</button>
            <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('qc-hotelling')">Hotelling T2</button>
            <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('qc-pcmcd')">{{ t('editor.palette.pcmcd') }}</button>
          </div>

          <button class="tree-sub-head" type="button" @click="toggleExpanded('peak')">
            <span class="tree-cat-icon" aria-hidden="true" v-html="paletteIconHtml('peak')"></span>
            <span class="caret" :class="{ open: expanded.peak }" aria-hidden="true">▸</span>
            <span class="tree-sub-head-label">{{ t('editor.palette.peakRemoval') }}</span>
          </button>
          <div v-if="expanded.peak" class="tree-leaves">
            <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('peak-removal')">{{ t('editor.palette.usePeakRemoval') }}</button>
          </div>

          <button class="tree-sub-head" type="button" @click="toggleExpanded('smooth')">
            <span class="tree-cat-icon" aria-hidden="true" v-html="paletteIconHtml('smooth')"></span>
            <span class="caret" :class="{ open: expanded.smooth }" aria-hidden="true">▸</span>
            <span class="tree-sub-head-label">{{ t('editor.palette.smoothSection') }}</span>
          </button>
          <div v-if="expanded.smooth" class="tree-leaves">
            <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('smooth-sg')">Savitzky-Golay</button>
            <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('smooth-snv')">SNV</button>
          </div>

          <button class="tree-sub-head" type="button" @click="toggleExpanded('base')">
            <span class="tree-cat-icon" aria-hidden="true" v-html="paletteIconHtml('base')"></span>
            <span class="caret" :class="{ open: expanded.base }" aria-hidden="true">▸</span>
            <span class="tree-sub-head-label">{{ t('editor.palette.baselineSection') }}</span>
          </button>
          <div v-if="expanded.base" class="tree-leaves">
            <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('base-polyfit')">{{ t('editor.palette.polyfit') }}</button>
            <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('base-bubble')">{{ t('editor.palette.bubble') }}</button>
          </div>

          <button class="tree-sub-head" type="button" @click="toggleExpanded('trunc')">
            <span class="tree-cat-icon" aria-hidden="true" v-html="paletteIconHtml('trunc')"></span>
            <span class="caret" :class="{ open: expanded.trunc }" aria-hidden="true">▸</span>
            <span class="tree-sub-head-label">{{ t('editor.palette.trunc') }}</span>
          </button>
          <div v-if="expanded.trunc" class="tree-leaves">
            <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('trunc-range')">{{ t('editor.palette.truncEnable') }}</button>
          </div>

          <button class="tree-sub-head" type="button" @click="toggleExpanded('norm')">
            <span class="tree-cat-icon" aria-hidden="true" v-html="paletteIconHtml('norm')"></span>
            <span class="caret" :class="{ open: expanded.norm }" aria-hidden="true">▸</span>
            <span class="tree-sub-head-label">{{ t('editor.palette.normSection') }}</span>
          </button>
          <div v-if="expanded.norm" class="tree-leaves">
            <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('norm-method')">{{ t('editor.palette.normMethod') }}</button>
          </div>
          </template>

          <!-- 3. 无元数据分析 -->
          <template v-else-if="topSection === 'metaFree'">
            <button class="tree-sub-head" type="button" @click="toggleExpanded('dim')">
              <span class="tree-cat-icon" aria-hidden="true" v-html="paletteIconHtml('dim')"></span>
              <span class="caret" :class="{ open: expanded.dim }" aria-hidden="true">▸</span>
              <span class="tree-sub-head-label">{{ t('editor.palette.dim') }}</span>
            </button>
            <div v-if="expanded.dim" class="tree-leaves">
              <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('dim-pca')">PCA</button>
              <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('dim-tsne')">t-SNE</button>
              <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('dim-umap')">UMAP</button>
              <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('dim-pcoa')">PCoA</button>
            </div>

            <button class="tree-sub-head" type="button" @click="toggleExpanded('phen')">
              <span class="tree-cat-icon" aria-hidden="true" v-html="paletteIconHtml('phen')"></span>
              <span class="caret" :class="{ open: expanded.phen }" aria-hidden="true">▸</span>
              <span class="tree-sub-head-label">{{ t('editor.palette.phenotype') }}</span>
            </button>
            <div v-if="expanded.phen" class="tree-leaves">
              <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('phen-louvain')">{{ t('editor.palette.louvain') }}</button>
              <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('phen-hca')">{{ t('editor.palette.hca') }}</button>
              <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('phen-kmeans')">K-means</button>
            </div>

            <button class="tree-sub-head" type="button" @click="toggleExpanded('decomp')">
              <span class="tree-cat-icon" aria-hidden="true" v-html="paletteIconHtml('decomp')"></span>
              <span class="caret" :class="{ open: expanded.decomp }" aria-hidden="true">▸</span>
              <span class="tree-sub-head-label">{{ t('editor.palette.spectral') }}</span>
            </button>
            <div v-if="expanded.decomp" class="tree-leaves">
              <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('decomp-mcrals')">MCR-ALS</button>
              <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('decomp-ica')">ICA</button>
              <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('decomp-nmf')">NMF</button>
            </div>

            <button class="tree-sub-head" type="button" @click="toggleExpanded('intra')">
              <span class="tree-cat-icon" aria-hidden="true" v-html="paletteIconHtml('intra')"></span>
              <span class="caret" :class="{ open: expanded.intra }" aria-hidden="true">▸</span>
              <span class="tree-sub-head-label">{{ t('editor.palette.intra') }}</span>
            </button>
            <div v-if="expanded.intra" class="tree-leaves">
              <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('intra-irca')">{{ t('editor.palette.ircaGlobal') }}</button>
              <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('intra-irca-local')">{{ t('editor.palette.ircaLocal') }}</button>
              <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('intra-2dcos')">{{ t('editor.palette.cos2d') }}</button>
            </div>
          </template>

          <!-- 4. 基于元数据分析 -->
          <template v-else>
            <button class="tree-sub-head" type="button" @click="toggleExpanded('cls')">
              <span class="tree-cat-icon" aria-hidden="true" v-html="paletteIconHtml('cls')"></span>
              <span class="caret" :class="{ open: expanded.cls }" aria-hidden="true">▸</span>
              <span class="tree-sub-head-label">{{ t('editor.palette.classification') }}</span>
            </button>
            <div v-if="expanded.cls" class="tree-leaves">
              <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('cls-lda')">LDA</button>
              <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('cls-rf')">{{ t('editor.palette.rf') }}</button>
              <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('cls-svm')">SVM</button>
            </div>

            <button class="tree-sub-head" type="button" @click="toggleExpanded('quant')">
              <span class="tree-cat-icon" aria-hidden="true" v-html="paletteIconHtml('quant')"></span>
              <span class="caret" :class="{ open: expanded.quant }" aria-hidden="true">▸</span>
              <span class="tree-sub-head-label">{{ t('editor.palette.regression') }}</span>
            </button>
            <div v-if="expanded.quant" class="tree-leaves">
              <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('quant-pls')">PLS</button>
              <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('quant-mlr')">MLR</button>
              <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('quant-glm')">GLM</button>
            </div>

            <button class="tree-sub-head" type="button" @click="toggleExpanded('markers')">
              <span class="tree-cat-icon" aria-hidden="true" v-html="paletteIconHtml('markers')"></span>
              <span class="caret" :class="{ open: expanded.markers }" aria-hidden="true">▸</span>
              <span class="tree-sub-head-label">{{ t('editor.palette.markers') }}</span>
            </button>
            <div v-if="expanded.markers" class="tree-leaves">
              <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('marker-rbcs')">RBCS</button>
              <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('marker-roc')">{{ t('editor.palette.roc') }}</button>
              <button class="tree-leaf-btn" type="button" @click="addNodeFromLibrary('marker-correlations')">{{ t('editor.palette.correlations') }}</button>
            </div>
          </template>
        </div>
      </aside>

      <!-- 节点编辑器画布 -->
      <div class="editor-container">
        <VueFlow
          v-model:nodes="nodes"
          v-model:edges="edges"
          :fit-view-on-init="true"
          :node-types="nodeTypes"
          :is-valid-connection="isValidConnection"
          :connection-line-style="{ stroke: '#1a1a1a', strokeWidth: 2, strokeDasharray: '5,5' }"
          :default-edge-options="{ 
            animated: true, 
            style: { stroke: '#1a1a1a', strokeWidth: 2, strokeDasharray: '5,5' },
            type: 'default'
          }"
          @connect="onConnect"
          @node-click="onNodeClick"
          @nodes-change="onNodesChange"
          @pane-click="onPaneClick"
          @ready="onVueFlowReady"
          @init="onVueFlowInit"
        >
          <Background 
            v-if="backgroundType !== 'none'"
            :pattern-color="backgroundType === 'dots' ? '#e2e8f0' : '#d1d5db'"
            :gap="backgroundType === 'dots' ? 16 : 20"
            :size="backgroundType === 'dots' ? 1 : 2"
          />
          <Controls />
          <MiniMap />
        </VueFlow>
      </div>

      <!-- 右侧分析结果侧边栏 -->
      <AnalysisResultSidebar
        v-if="analysisResult"
        :data="analysisResult"
        :open="showResultSidebar"
        @close="showResultSidebar = false"
        @toggle="showResultSidebar = !showResultSidebar"
      />
    </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, markRaw, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import LanguageSwitcher from './LanguageSwitcher.vue'
import { i18n } from '../i18n'
import { VueFlow, useVueFlow } from '@vue-flow/core'
import type { Connection } from '@vue-flow/core'
import { Background } from '@vue-flow/background'
import { Controls } from '@vue-flow/controls'
import { MiniMap } from '@vue-flow/minimap'
import '@vue-flow/core/dist/style.css'
import '@vue-flow/core/dist/theme-default.css'
import '@vue-flow/controls/dist/style.css'
import '@vue-flow/minimap/dist/style.css'
import WorkflowNodeComponent from './WorkflowNodeComponent.vue'
import AnalysisResultSidebar from './AnalysisResultSidebar.vue'

const { t } = useI18n()
const et = (key: string) => i18n.global.t(key) as string

// Vue 3 script setup 组件可以直接导入使用
import type { WorkflowConfig } from '../types/nodeTypes'
import { API_BASE_URL, ApiError, downloadProjectAnalysisExportFile, getProject, getProjectAnalysisResults, runProjectAnalysis } from '../api/http'
import { paletteIconSvg, getPaletteCategoryFromNodeType } from '../utils/paletteIcons'

const paletteIconHtml = (key: string) => paletteIconSvg(key)

const route = useRoute()
const router = useRouter()
const projectId = computed(() => String(route.query.projectId ?? ''))
const activeDataFileName = ref('')

const loadActiveDataFileName = async () => {
  const pid = projectId.value
  if (!pid) {
    activeDataFileName.value = ''
    return
  }
  try {
    const project = await getProject(pid)
    const active = project.active_data_file
    const files = project.data_files ?? project.files ?? []
    const file = active ?? files[0]
    activeDataFileName.value = String(file?.filename ?? file?.name ?? '').trim()
  } catch (e) {
    console.warn('load project file name failed', e)
    activeDataFileName.value = ''
  }
}

const goBack = () => {
  router.back()
}

const mediaBaseUrl = API_BASE_URL.replace(/\/api$/, '')

const leafPlotKeyMap: Record<string, string[]> = {
  'dim-pca': ['pca'],
  'dim-tsne': ['tsne'],
  'dim-umap': ['umap'],
  'dim-pcoa': ['pcoa'],
  'phen-louvain': ['louvain'],
  'phen-hca': ['hca'],
  'phen-kmeans': ['kmeans'],
  'decomp-mcrals': ['mcrals_spec', 'mcrals_violin'],
  'decomp-ica': ['ica_spec', 'ica_violin'],
  'decomp-nmf': ['nmf_spec', 'nmf_violin'],
  'intra-irca': ['irca_global'],
  'intra-irca-local': ['irca_local'],
  'intra-2dcos': ['2dcos'],
  'cls-lda': ['lda'],
  'cls-rf': ['rf'],
  'cls-svm': ['svm'],
  'quant-pls': ['pls'],
  'quant-mlr': ['mlr'],
  'quant-glm': ['glm'],
  'marker-rbcs': ['rbcs'],
  'marker-roc': ['roc'],
  'marker-correlations': ['correlations']
}

interface WorkflowNode {
  id: string
  type: string
  position: { x: number; y: number }
  data: {
    label: string
    nodeType?: 'local-csv' | 'missing-values' | 'distribution' | 'kmeans' | string
    content?: string
    properties?: Record<string, any>
    previewUrl?: string
    previewLoading?: boolean
    previewFileName?: string
    /** 与左侧树分类图标一致，用于节点 header 展示 */
    paletteCategory?: string
    schema?: Array<{
      key: string
      label: string
      type: 'number' | 'select' | 'checkbox' | 'table'
      hint?: string
      min?: number
      max?: number
      step?: number
      checkboxText?: string
      options?: Array<{ label: string; value: any }>
      columns?: Array<{ key: string; label: string; type: 'number' | 'text' }>
      minRows?: number
    }>
    headerColor?: string
    status?: 'ready' | 'pending' | 'error'
    showSettings?: boolean
    tabs?: Array<{ id: string; label: string }>
    inputs?: Array<{ id: string; label: string }>
    outputs?: Array<{ id: string; label?: string }>
  }
}


interface WorkflowEdge {
  id: string
  source: string
  target: string
  sourceHandle?: string
  targetHandle?: string
}

const nodes = ref<WorkflowNode[]>([])
const edges = ref<WorkflowEdge[]>([])
const vueFlowReady = ref(false)

const { screenToFlowCoordinate } = useVueFlow()
// 记录用户上一次在画布空白处点击的位置，新节点将添加到此处
const lastClickPosition = ref<{ x: number; y: number } | null>(null)

const onPaneClick = (event: MouseEvent) => {
  lastClickPosition.value = screenToFlowCoordinate({ x: event.clientX, y: event.clientY })
}

// 监听nodes变化，用于调试
watch(nodes, (newNodes) => {
  console.log('Nodes changed, count:', newNodes.length)
  newNodes.forEach((node, index) => {
    console.log(`Node ${index}:`, { id: node.id, type: node.type, position: node.position })
  })
}, { deep: true, immediate: true })

// 监听edges变化，自动清理无效连接
watch(edges, (newEdges) => {
  // 过滤掉无效的edge（缺少source或target，或自连接，或引用的节点不存在）
  const validEdges = newEdges.filter((edge: WorkflowEdge) => {
    // 检查edge是否有必需的字段
    if (!edge || !edge.id) {
      return false
    }
    if (!edge.source || !edge.target || edge.source === '' || edge.target === '') {
      console.log('移除无效edge（缺少source/target）:', edge)
      return false
    }
    if (edge.source === edge.target) {
      console.log('移除自连接:', edge)
      return false
    }
    // 检查source和target对应的节点是否存在
    const sourceNodeExists = nodes.value.some(node => node.id === edge.source)
    const targetNodeExists = nodes.value.some(node => node.id === edge.target)
    if (!sourceNodeExists || !targetNodeExists) {
      console.log('移除无效edge（节点不存在）:', edge, { sourceNodeExists, targetNodeExists })
      return false
    }
    return true
  })
  
  // 如果有无效edge被移除，更新edges（使用nextTick避免循环更新）
  if (validEdges.length !== newEdges.length) {
    nextTick(() => {
      edges.value = validEdges
    })
  }
}, { deep: true, immediate: false })

// 注册自定义节点类型（使用通用节点组件）
const nodeTypes: any = computed(() => {
  const types = {
    'default': markRaw(WorkflowNodeComponent)
  }
  console.log('Node types registered:', Object.keys(types), types)
  return types
})

const isExecuting = ref(false)
const nodeIdCounter = ref(0)
const backgroundType = ref<'dots' | 'lines' | 'none'>('dots')

const expanded = ref({
  qc: true,
  peak: true,
  smooth: true,
  base: true,
  trunc: true,
  norm: true,
  dim: true,
  phen: true,
  decomp: true,
  intra: true,
  cls: true,
  quant: true,
  markers: true
})

const topSection = ref<'preprocess' | 'metaFree' | 'meta'>('preprocess')
const topSectionTitle = computed(() => {
  if (topSection.value === 'preprocess') return t('editor.sectionPreprocess')
  if (topSection.value === 'metaFree') return t('editor.sectionMetaFree')
  return t('editor.sectionMetaBased')
})

const showRunButton = computed(() => {
  // 只要画布上存在任意节点就显示运行按钮（支持无元数据/基于元数据分析）
  return nodes.value.length > 0
})

interface AnalysisResultData {
  label?: string
  plots?: Record<string, string | string[]>
  analyses?: unknown
  message?: string
  exportId?: string
}

const analysisResult = ref<AnalysisResultData | null>(null)
const showResultSidebar = ref(false)

const buildPlotsFromResp = (resp: any): Record<string, string | string[]> => {
  const plots: Record<string, string | string[]> = {}
  const rawPlots = resp?.result?.plots ?? resp?.plots ?? {}
  const toAbsUrl = (v: unknown) => {
    const url = String(v ?? '')
    return url.startsWith('http') ? url : `${mediaBaseUrl}${url}`
  }
  Object.entries(rawPlots).forEach(([k, v]) => {
    plots[k] = Array.isArray(v) ? v.map(toAbsUrl) : toAbsUrl(v)
  })
  return plots
}

const pickLeafPreviewUrlFromPlots = (leafType: string, plots: Record<string, string | string[]>) => {
  const keys = leafPlotKeyMap[leafType] ?? []
  for (const k of keys) {
    const v = plots?.[k]
    const u = (Array.isArray(v) ? v[0] : v)
    const trimmed = String(u ?? '').trim()
    if (trimmed) return trimmed
  }
  return ''
}

// 把这次分析结果里的图，按节点类型分发回画布上对应的分析节点，作为节点内预览
// （节点在“运行/解析”之前只显示骨架占位，不再逐个自动请求后端）
const applyPreviewsToNodes = (plots: Record<string, string | string[]>) => {
  nodes.value = nodes.value.map(n => {
    const nodeType = String(n.data?.nodeType ?? '')
    if (!leafPlotKeyMap[nodeType]) return n
    const url = pickLeafPreviewUrlFromPlots(nodeType, plots)
    return {
      ...n,
      data: {
        ...n.data,
        previewUrl: url || undefined,
        previewFileName: url ? (activeDataFileName.value || undefined) : undefined
      }
    } as WorkflowNode
  })
}

// 有预览能力的节点在运行期间显示加载态骨架，运行结束后统一回填/清空
const setLeafNodesLoading = (loading: boolean) => {
  nodes.value = nodes.value.map(n => {
    const nodeType = String(n.data?.nodeType ?? '')
    if (!leafPlotKeyMap[nodeType]) return n
    return { ...n, data: { ...n.data, previewLoading: loading } } as WorkflowNode
  })
}

const updateResultNodeFromResp = (resp: any) => {
  const plots = buildPlotsFromResp(resp)

  analysisResult.value = {
    label: t('common.analysisResult'),
    plots,
    analyses: resp?.result?.analyses ?? resp?.analyses ?? null,
    message: resp?.message ?? resp?.result?.message ?? '',
    exportId: String(resp?.result?.artifacts?.export_id ?? resp?.result?.export_id ?? resp?.export_id ?? '').trim() || undefined
  }
  showResultSidebar.value = true
  applyPreviewsToNodes(plots)
}

const showDownloadButton = computed(() => {
  if (!projectId.value) return false
  const result = analysisResult.value
  if (!result) return false
  const plots = result.plots ?? {}
  const hasPlots = typeof plots === 'object' && plots && Object.keys(plots).length > 0
  const hasAnalyses = Boolean(result.analyses)
  const hasMessage = Boolean(String(result.message ?? '').trim())
  return hasPlots || hasAnalyses || hasMessage
})

const isDownloading = ref(false)

const downloadAnalysisExport = async () => {
  const pid = projectId.value
  if (!pid) {
    alert(t('common.errors.missingProjectId'))
    return
  }
  isDownloading.value = true
  try {
    const { blob, filename } = await downloadProjectAnalysisExportFile(pid)
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = (filename && filename.trim()) ? filename.trim() : `analysis-export-${pid}.zip`
    document.body.appendChild(a)
    a.click()
    a.remove()
    URL.revokeObjectURL(url)
  } catch (e: any) {
    alert(e instanceof ApiError ? e.message : (e?.message ?? t('common.errors.download')))
  } finally {
    isDownloading.value = false
  }
}

const toggleExpanded = (key: keyof typeof expanded.value) => {
  expanded.value[key] = !expanded.value[key]
}

const isNodeEnabled = (nodeType: string) =>
  nodes.value.some(n => (n.data?.nodeType ?? '') === nodeType)

const getLibraryConfig = (nodeType: string): Partial<WorkflowNode['data']> | null => {
  const configs: Record<string, Partial<WorkflowNode['data']>> = {
    'qc-euclidean': {
      label: et('editor.palette.euclidean'),
      nodeType: 'qc-euclidean',
      headerColor: '#0f172a',
      properties: { maxDistance: 1 },
      schema: [{ key: 'maxDistance', label: et('editor.fields.maxDistance'), type: 'number', min: 0, step: 0.1 }]
    },
    'qc-snr': {
      label: et('editor.palette.snr'),
      nodeType: 'qc-snr',
      headerColor: '#0f172a',
      properties: { strictness: '中' },
      schema: [{
        key: 'strictness',
        label: et('editor.fields.strictness'),
        type: 'select',
        options: [
          { label: et('common.strictness.low'), value: '低' },
          { label: et('common.strictness.medium'), value: '中' },
          { label: et('common.strictness.high'), value: '高' }
        ]
      }]
    },
    'qc-icod': {
      label: et('editor.palette.icod'),
      nodeType: 'qc-icod',
      headerColor: '#0f172a',
      properties: { peakTolerance: 0.5, maxIterations: 100 },
      schema: [
        { key: 'peakTolerance', label: et('editor.fields.peakTolerance'), type: 'number', min: 0, step: 0.1 },
        { key: 'maxIterations', label: et('editor.fields.maxIterations'), type: 'number', min: 1, step: 1 }
      ]
    },
    'qc-hotelling': {
      label: 'Hotelling T2',
      nodeType: 'qc-hotelling',
      headerColor: '#0f172a',
      properties: { alpha: 0.05 },
      schema: [{ key: 'alpha', label: et('editor.fields.alpha'), type: 'number', min: 0, max: 1, step: 0.01 }]
    },
    'qc-pcmcd': {
      label: 'PC-MCD',
      nodeType: 'qc-pcmcd',
      headerColor: '#0f172a',
      properties: { mcdRatio: 0.5, alpha: 0.01 },
      schema: [
        { key: 'mcdRatio', label: et('editor.fields.mcdRatio'), type: 'number', min: 0, max: 1, step: 0.01 },
        { key: 'alpha', label: et('editor.fields.alpha'), type: 'number', min: 0, max: 1, step: 0.01 }
      ]
    },
    'peak-removal': {
      label: et('editor.palette.peakRemoval'),
      nodeType: 'peak-removal',
      headerColor: '#0f172a',
      properties: { device: 'CPU' },
      schema: [{
        key: 'device',
        label: et('editor.fields.device'),
        type: 'select',
        options: [{ label: 'CPU', value: 'CPU' }, { label: 'GPU', value: 'GPU' }]
      }]
    },
    'smooth-sg': {
      label: et('editor.nodes.smoothSg'),
      nodeType: 'smooth-sg',
      headerColor: '#0f172a',
      properties: { differentiation_order: 0, polynomial_order: 5, window_size: 11 },
      schema: [
        { key: 'differentiation_order', label: et('editor.fields.differentiationOrder'), type: 'number', min: 0, max: 2, step: 1 },
        { key: 'polynomial_order', label: et('editor.fields.polynomialOrder'), type: 'number', min: 1, max: 9, step: 1 },
        { key: 'window_size', label: et('editor.fields.windowSize'), type: 'number', min: 3, step: 2 }
      ]
    },
    'smooth-snv': {
      label: et('editor.nodes.smoothSnv'),
      nodeType: 'smooth-snv',
      headerColor: '#0f172a',
      properties: {},
      schema: []
    },

    'base-polyfit': {
      label: et('editor.nodes.basePolyfit'),
      nodeType: 'base-polyfit',
      headerColor: '#0f172a',
      properties: { poly_order_fingerprint: 1, poly_order_others: 6 },
      schema: [
        { key: 'poly_order_fingerprint', label: et('editor.fields.polyOrderFingerprint'), type: 'number', min: 1, max: 10, step: 1 },
        { key: 'poly_order_others', label: et('editor.fields.polyOrderOthers'), type: 'number', min: 1, max: 10, step: 1 }
      ]
    },
    'base-bubble': {
      label: et('editor.nodes.baseBubble'),
      nodeType: 'base-bubble',
      headerColor: '#0f172a',
      properties: { min_bubble_width: 50 },
      schema: [{ key: 'min_bubble_width', label: et('editor.fields.minBubbleWidth'), type: 'number', min: 1, step: 1 }]
    },

    'trunc-range': {
      label: et('editor.nodes.truncRange'),
      nodeType: 'trunc-range',
      headerColor: '#0f172a',
      properties: { wavenumber_min: 550, wavenumber_max: 1800 },
      schema: [
        { key: 'wavenumber_min', label: et('editor.fields.wavenumberMin'), type: 'number', min: 0, step: 1 },
        { key: 'wavenumber_max', label: et('editor.fields.wavenumberMax'), type: 'number', min: 0, step: 1 }
      ]
    },

    'norm-method': {
      label: et('editor.nodes.normMethod'),
      nodeType: 'norm-method',
      headerColor: '#0f172a',
      properties: { method: 'max', peak_value: 1004 },
      schema: [
        {
          key: 'method',
          label: et('editor.fields.normMethod'),
          type: 'select',
          options: [
            { label: 'max', value: 'max' },
            { label: 'area', value: 'area' },
            { label: 'vector', value: 'vector' },
            { label: 'minmax', value: 'minmax' },
            { label: 'specific', value: 'specific' },
            { label: 'ch', value: 'ch' }
          ]
        },
        { key: 'peak_value', label: et('editor.fields.peakValue'), type: 'number', min: 0, step: 1 }
      ]
    },

    // 无元数据分析 - 降维
    'dim-pca': {
      label: 'PCA',
      nodeType: 'dim-pca',
      headerColor: '#0f172a',
      properties: { n_pc: 2 },
      schema: [{ key: 'n_pc', label: et('editor.fields.nPc'), type: 'number', min: 1, step: 1 }]
    },
    'dim-tsne': {
      label: 't-SNE',
      nodeType: 'dim-tsne',
      headerColor: '#0f172a',
      properties: { pca: 20, n_pc: 2, perplexity: 5, theta: 0.5, max_iter: 1000 },
      schema: [
        { key: 'pca', label: et('editor.fields.pcaPre'), type: 'number', min: 1, step: 1 },
        { key: 'n_pc', label: et('editor.fields.tsnePc'), type: 'number', min: 1, step: 1 },
        { key: 'perplexity', label: et('editor.fields.perplexity'), type: 'number', min: 1, step: 1 },
        { key: 'theta', label: et('editor.fields.theta'), type: 'number', min: 0, max: 1, step: 0.01 },
        { key: 'max_iter', label: et('editor.fields.maxIterations'), type: 'number', min: 1, step: 1 }
      ]
    },
    'dim-umap': {
      label: 'UMAP',
      nodeType: 'dim-umap',
      headerColor: '#0f172a',
      properties: { pca: 20, n_pc: 2, n_neighbors: 30, min_dist: 0.01, spread: 1 },
      schema: [
        { key: 'pca', label: et('editor.fields.pcaPre'), type: 'number', min: 1, step: 1 },
        { key: 'n_pc', label: et('editor.fields.umapPc'), type: 'number', min: 1, step: 1 },
        { key: 'n_neighbors', label: et('editor.fields.nNeighbors'), type: 'number', min: 2, step: 1 },
        { key: 'min_dist', label: et('editor.fields.minDist'), type: 'number', min: 0, max: 1, step: 0.001 },
        { key: 'spread', label: et('editor.fields.spread'), type: 'number', min: 0.1, step: 0.1 }
      ]
    },
    'dim-pcoa': {
      label: 'PCoA',
      nodeType: 'dim-pcoa',
      headerColor: '#0f172a',
      properties: { pca: 20, n_pc: 2, distance: 'euclidean' },
      schema: [
        { key: 'pca', label: et('editor.fields.pcaPre'), type: 'number', min: 1, step: 1 },
        { key: 'n_pc', label: et('editor.fields.pcoaPc'), type: 'number', min: 1, step: 1 },
        {
          key: 'distance',
          label: et('editor.fields.distanceMethod'),
          type: 'select',
          options: [
            { label: et('editor.distance.euclidean'), value: 'euclidean' },
            { label: et('editor.distance.maximum'), value: 'maximum' },
            { label: et('editor.distance.manhattan'), value: 'manhattan' },
            { label: et('editor.distance.canberra'), value: 'canberra' },
            { label: et('editor.distance.binary'), value: 'binary' },
            { label: et('editor.distance.minkowski'), value: 'minkowski' }
          ]
        }
      ]
    },

    // 无元数据分析 - 表型
    'phen-louvain': {
      label: et('editor.nodes.phenLouvain'),
      nodeType: 'phen-louvain',
      headerColor: '#0f172a',
      properties: { resolution: 0.4, n_pc: 10, threshold: 0.001, k: 30, n_tree: 50 },
      schema: [
        { key: 'resolution', label: et('editor.fields.resolution'), type: 'number', min: 0.01, step: 0.01 },
        { key: 'n_pc', label: et('editor.fields.pcaPre'), type: 'number', min: 1, step: 1 },
        { key: 'threshold', label: et('editor.fields.clusterThreshold'), type: 'number', min: 0, step: 0.0001 },
        { key: 'k', label: et('editor.fields.kNeighbors'), type: 'number', min: 1, step: 1 },
        { key: 'n_tree', label: et('editor.fields.nTree'), type: 'number', min: 1, step: 1 }
      ]
    },
    'phen-hca': {
      label: et('editor.nodes.phenHca'),
      nodeType: 'phen-hca',
      headerColor: '#0f172a',
      properties: {},
      schema: []
    },
    'phen-kmeans': {
      label: 'K-means',
      nodeType: 'phen-kmeans',
      headerColor: '#0f172a',
      properties: { k: 4, n_pc: 10 },
      schema: [
        { key: 'k', label: et('editor.fields.kClusters'), type: 'number', min: 2, step: 1 },
        { key: 'n_pc', label: et('editor.fields.pcaPre'), type: 'number', min: 1, step: 1 }
      ]
    },

    // 无元数据分析 - 谱图分解
    'decomp-mcrals': {
      label: 'MCR-ALS',
      nodeType: 'decomp-mcrals',
      headerColor: '#0f172a',
      properties: { n_comp: 3 },
      schema: [{ key: 'n_comp', label: et('editor.fields.nComp'), type: 'number', min: 1, step: 1 }]
    },
    'decomp-ica': {
      label: 'ICA',
      nodeType: 'decomp-ica',
      headerColor: '#0f172a',
      properties: { n_comp: 3 },
      schema: [{ key: 'n_comp', label: et('editor.fields.nComp'), type: 'number', min: 1, step: 1 }]
    },
    'decomp-nmf': {
      label: 'NMF',
      nodeType: 'decomp-nmf',
      headerColor: '#0f172a',
      properties: { n_comp: 3, max_iter: 100, tol: 0.0001 },
      schema: [
        { key: 'n_comp', label: et('editor.fields.nComp'), type: 'number', min: 1, step: 1 },
        { key: 'max_iter', label: et('editor.fields.maxIterations'), type: 'number', min: 1, step: 10 },
        { key: 'tol', label: et('editor.fields.tol'), type: 'number', min: 0, step: 0.0001 }
      ]
    },

    // 无元数据分析 - 拉曼组内分析
    'intra-irca': {
      label: et('editor.nodes.intraIrca'),
      nodeType: 'intra-irca',
      headerColor: '#0f172a',
      properties: { threshold: 0.6 },
      schema: [{ key: 'threshold', label: et('editor.fields.correlationThreshold'), type: 'number', min: 0, max: 1, step: 0.01 }]
    },
    'intra-irca-local': {
      label: et('editor.nodes.intraIrcaLocal'),
      nodeType: 'intra-irca-local',
      headerColor: '#0f172a',
      properties: {
        threshold: 0.6,
        bands_ann: [
          { wave_num: 742, group: 'Nucleic acid' },
          { wave_num: 850, group: 'Nucleic acid' },
          { wave_num: 872, group: 'Nucleic acid' },
          { wave_num: 971, group: 'Nucleic acid' },
          { wave_num: 997, group: 'Nucleic acid' },
          { wave_num: 1098, group: 'Nucleic acid' },
          { wave_num: 1293, group: 'Nucleic acid' },
          { wave_num: 1328, group: 'Nucleic acid' },
          { wave_num: 1426, group: 'Nucleic acid' },
          { wave_num: 1576, group: 'Nucleic acid' },
          { wave_num: 824, group: 'Protein' },
          { wave_num: 883, group: 'Protein' },
          { wave_num: 1005, group: 'Protein' },
          { wave_num: 1033, group: 'Protein' },
          { wave_num: 1051, group: 'Protein' },
          { wave_num: 1237, group: 'Protein' },
          { wave_num: 1559, group: 'Protein' },
          { wave_num: 1651, group: 'Protein' },
          { wave_num: 1076, group: 'Lipids' },
          { wave_num: 1119, group: 'Lipids' },
          { wave_num: 1370, group: 'Lipids' },
          { wave_num: 2834, group: 'Lipids' },
          { wave_num: 2866, group: 'Lipids' },
          { wave_num: 2912, group: 'Lipids' }
        ]
      },
      schema: [
        { key: 'threshold', label: et('editor.fields.correlationThreshold'), type: 'number', min: 0, max: 1, step: 0.01 },
        {
          key: 'bands_ann',
          label: et('editor.fields.bandsAnn'),
          type: 'table',
          hint: et('editor.fields.bandsAnnHint'),
          minRows: 2,
          columns: [
            { key: 'wave_num', label: et('editor.fields.bandWaveNum'), type: 'number' },
            { key: 'group', label: et('editor.fields.bandAnnotation'), type: 'text' }
          ]
        }
      ]
    },
    'intra-2dcos': {
      label: et('editor.nodes.intra2dcos'),
      nodeType: 'intra-2dcos',
      headerColor: '#0f172a',
      properties: {},
      schema: []
    },

    // 基于元数据分析 - 分类
    'cls-lda': {
      label: 'LDA',
      nodeType: 'cls-lda',
      headerColor: '#0f172a',
      properties: { n_pc: 20 },
      schema: [{ key: 'n_pc', label: '主成分数量', type: 'number', min: 1, step: 1 }]
    },
    'cls-rf': {
      label: et('editor.nodes.clsRf'),
      nodeType: 'cls-rf',
      headerColor: '#0f172a',
      properties: { n_estimators: 500, mtry: 2 },
      schema: [
        { key: 'n_estimators', label: et('editor.fields.nEstimators'), type: 'number', min: 10, step: 50 },
        { key: 'mtry', label: et('editor.fields.mtry'), type: 'number', min: 1, step: 1 }
      ]
    },
    'cls-svm': {
      label: 'SVM',
      nodeType: 'cls-svm',
      headerColor: '#0f172a',
      properties: {},
      schema: []
    },

    // 基于元数据分析 - 量化/回归
    'quant-pls': {
      label: 'PLS',
      nodeType: 'quant-pls',
      headerColor: '#0f172a',
      properties: { n_comp: 10 },
      schema: [{ key: 'n_comp', label: et('editor.fields.pcaPre'), type: 'number', min: 1, step: 1 }]
    },
    'quant-mlr': {
      label: 'MLR',
      nodeType: 'quant-mlr',
      headerColor: '#0f172a',
      properties: { n_pc: 10 },
      schema: [{ key: 'n_pc', label: et('editor.fields.pcaPre'), type: 'number', min: 1, step: 1 }]
    },
    'quant-glm': {
      label: 'GLM',
      nodeType: 'quant-glm',
      headerColor: '#0f172a',
      properties: { n_pc: 10 },
      schema: [{ key: 'n_pc', label: et('editor.fields.pcaPre'), type: 'number', min: 1, step: 1 }]
    },

    // 基于元数据分析 - 拉曼标记
    'marker-rbcs': {
      label: 'RBCS',
      nodeType: 'marker-rbcs',
      headerColor: '#0f172a',
      properties: { threshold: 0.002, n_estimators: 1000, nfolds: 3 },
      schema: [
        { key: 'threshold', label: et('editor.fields.importanceThreshold'), type: 'number', min: 0, step: 0.0001 },
        { key: 'n_estimators', label: et('editor.fields.nTree'), type: 'number', min: 10, step: 50 },
        { key: 'nfolds', label: et('editor.fields.nFolds'), type: 'number', min: 2, step: 1 }
      ]
    },
    'marker-roc': {
      label: et('editor.nodes.markerRoc'),
      nodeType: 'marker-roc',
      headerColor: '#0f172a',
      properties: { threshold: 0.75, batch_size: 1000 },
      schema: [
        { key: 'threshold', label: et('editor.fields.aucThreshold'), type: 'number', min: 0.5, max: 1, step: 0.01 },
        { key: 'batch_size', label: et('editor.fields.batchSize'), type: 'number', min: 1, step: 100 }
      ]
    },
    'marker-correlations': {
      label: et('editor.nodes.markerCorrelations'),
      nodeType: 'marker-correlations',
      headerColor: '#0f172a',
      properties: { min_cor: 0.8, min_range: 30, by_average: false },
      schema: [
        { key: 'min_cor', label: et('editor.fields.minCor'), type: 'number', min: 0, max: 1, step: 0.01 },
        { key: 'min_range', label: et('editor.fields.minRange'), type: 'number', min: 1, step: 1 },
        { key: 'by_average', label: et('editor.fields.byAverage'), type: 'checkbox', checkboxText: et('editor.checkbox.enabled') }
      ]
    }
  }
  return configs[nodeType] ?? null
}

const addNodeFromLibrary = (nodeType: string, position?: { x: number; y: number }) => {
  const config = getLibraryConfig(nodeType)
  if (!config) return ''
  const newNode: WorkflowNode = {
    id: `node-${++nodeIdCounter.value}`,
    type: 'default',
    position: position ?? lastClickPosition.value ?? { x: 420 + (nodes.value.length % 3) * 40, y: 220 + (nodes.value.length % 5) * 40 },
    data: {
      label: config.label || t('editor.defaultNodeName', { index: nodeIdCounter.value }),
      nodeType: config.nodeType || nodeType,
      content: '',
      properties: config.properties || {},
      schema: config.schema,
      headerColor: config.headerColor || '#0f172a',
      previewUrl: undefined,
      previewLoading: false,
      paletteCategory: getPaletteCategoryFromNodeType(String(config.nodeType || nodeType))
    }
  }
  nodes.value.push(newNode)
  return newNode.id
}

// 将当前画布序列化为可持久化的工作流配置，随分析请求一起交给后端保存
const serializeWorkflow = (): WorkflowConfig => ({
  nodes: nodes.value.map(n => ({
    id: n.id,
    type: String(n.data?.nodeType ?? ''),
    position: n.position,
    properties: n.data?.properties ?? {},
    previewUrl: n.data?.previewUrl,
    previewFileName: n.data?.previewFileName
  })),
  edges: edges.value.map(e => ({
    id: e.id,
    source: e.source,
    target: e.target,
    sourceHandle: e.sourceHandle,
    targetHandle: e.targetHandle
  }))
})

// 依据保存的工作流配置重建画布节点/连线（节点样式、schema 等元数据从节点库配置中恢复）
const restoreWorkflow = (config: WorkflowConfig) => {
  if (!config || !Array.isArray(config.nodes) || config.nodes.length === 0) return
  let maxCounter = nodeIdCounter.value
  const restoredNodes: WorkflowNode[] = []
  for (const n of config.nodes) {
    const libConfig = getLibraryConfig(n.type)
    if (!libConfig) continue
    restoredNodes.push({
      id: n.id,
      type: 'default',
      position: n.position,
      data: {
        label: libConfig.label || n.type,
        nodeType: libConfig.nodeType || n.type,
        content: '',
        properties: { ...(libConfig.properties || {}), ...(n.properties || {}) },
        schema: libConfig.schema,
        headerColor: libConfig.headerColor || '#0f172a',
        previewUrl: n.previewUrl || undefined,
        previewLoading: false,
        previewFileName: n.previewFileName || undefined,
        paletteCategory: getPaletteCategoryFromNodeType(String(libConfig.nodeType || n.type))
      }
    })
    const match = /^node-(\d+)$/.exec(n.id)
    if (match) maxCounter = Math.max(maxCounter, Number(match[1]))
  }
  if (restoredNodes.length === 0) return
  nodes.value = restoredNodes
  edges.value = Array.isArray(config.edges) ? config.edges.map(e => ({ ...e })) : []
  nodeIdCounter.value = maxCounter
}

const buildAnalysisPayload = () => {
  // 组装：quality_control / spike_removal / smoothing / baseline / truncation / normalization / dim_reduction / phenotype
  //       spectral_decomposition / intra_ramanome / classification / quantification / raman_markers
  const qcMethods: string[] = []
  const qcParams: Record<string, any> = {}

  const spikeRemoval = { enabled: false, parameters: {} as Record<string, any> }

  const smoothingMethods: string[] = []
  const smoothingParams: Record<string, any> = {}

  const baselineMethods: string[] = []
  const baselineParams: Record<string, any> = {}

  const truncation = { enabled: false, parameters: {} as Record<string, any> }

  const normalization = { method: '', parameters: {} as Record<string, any> }

  const dimMethods: string[] = []
  const dimParams: Record<string, any> = {}

  const phenMethods: string[] = []
  const phenParams: Record<string, any> = {}

  const decompMethods: string[] = []
  const decompParams: Record<string, any> = {}

  const intraMethods: string[] = []
  const intraParams: Record<string, any> = {}

  const clsMethods: string[] = []
  const clsParams: Record<string, any> = {}

  const quantMethods: string[] = []
  const quantParams: Record<string, any> = {}

  const markerMethods: string[] = []
  const markerParams: Record<string, any> = {}

  const byType = (t: string) => nodes.value.find(n => n.data?.nodeType === t)?.data?.properties ?? {}

  if (isNodeEnabled('qc-euclidean')) {
    qcMethods.push('euclidean')
    qcParams.max_distance = byType('qc-euclidean').maxDistance
  }
  if (isNodeEnabled('qc-snr')) {
    qcMethods.push('snr')
    const map: Record<string, string> = { '低': 'low', '中': 'medium', '高': 'high' }
    qcParams.snr_level = map[String(byType('qc-snr').strictness ?? '中')] ?? 'medium'
  }
  if (isNodeEnabled('qc-icod')) {
    qcMethods.push('icod')
    qcParams.var_tol = byType('qc-icod').peakTolerance
    qcParams.max_iterations = byType('qc-icod').maxIterations
  }
  if (isNodeEnabled('qc-hotelling')) {
    qcMethods.push('hotelling')
    qcParams.signif = byType('qc-hotelling').alpha
  }
  if (isNodeEnabled('qc-pcmcd')) {
    qcMethods.push('pcmcd')
    qcParams.fraction_mcd = byType('qc-pcmcd').mcdRatio
    qcParams.alpha_mcd = byType('qc-pcmcd').alpha
  }

  if (isNodeEnabled('peak-removal')) {
    spikeRemoval.enabled = true
    spikeRemoval.parameters.device = byType('peak-removal').device ?? 'CPU'
  }

  if (isNodeEnabled('smooth-sg')) {
    smoothingMethods.push('savitzky-golay')
    smoothingParams['savitzky-golay'] = { ...byType('smooth-sg') }
  }
  if (isNodeEnabled('smooth-snv')) {
    smoothingMethods.push('snv')
    smoothingParams.snv = {}
  }

  if (isNodeEnabled('base-polyfit')) {
    baselineMethods.push('polyfit')
    baselineParams.polyfit = { ...byType('base-polyfit') }
  }
  if (isNodeEnabled('base-bubble')) {
    baselineMethods.push('bubble')
    baselineParams.bubble = { ...byType('base-bubble') }
  }

  if (isNodeEnabled('trunc-range')) {
    truncation.enabled = true
    truncation.parameters = { ...byType('trunc-range') }
  }

  if (isNodeEnabled('norm-method')) {
    const p = byType('norm-method')
    normalization.method = String(p.method ?? '')
    if (normalization.method === 'specific') {
      normalization.parameters.peak_value = p.peak_value
    }
  }

  if (isNodeEnabled('dim-pca')) {
    dimMethods.push('pca')
    dimParams.pca = { n_pc: byType('dim-pca').n_pc }
  }
  if (isNodeEnabled('dim-tsne')) {
    dimMethods.push('tsne')
    dimParams.tsne = { ...byType('dim-tsne') }
  }
  if (isNodeEnabled('dim-umap')) {
    dimMethods.push('umap')
    dimParams.umap = { ...byType('dim-umap') }
  }
  if (isNodeEnabled('dim-pcoa')) {
    dimMethods.push('pcoa')
    dimParams.pcoa = { ...byType('dim-pcoa') }
  }

  if (isNodeEnabled('phen-louvain')) {
    phenMethods.push('louvain')
    phenParams.louvain = { ...byType('phen-louvain') }
  }
  if (isNodeEnabled('phen-hca')) {
    phenMethods.push('hca')
    phenParams.hca = {}
  }
  if (isNodeEnabled('phen-kmeans')) {
    phenMethods.push('kmeans')
    phenParams.kmeans = { ...byType('phen-kmeans') }
  }

  // 谱图分解
  if (isNodeEnabled('decomp-mcrals')) {
    decompMethods.push('mcrals')
    decompParams.mcrals = { ...byType('decomp-mcrals') }
  }
  if (isNodeEnabled('decomp-ica')) {
    decompMethods.push('ica')
    decompParams.ica = { ...byType('decomp-ica') }
  }
  if (isNodeEnabled('decomp-nmf')) {
    decompMethods.push('nmf')
    decompParams.nmf = { ...byType('decomp-nmf') }
  }

  // 拉曼组内分析
  if (isNodeEnabled('intra-irca')) {
    intraMethods.push('irca')
    intraParams.irca = { ...byType('intra-irca') }
  }
  if (isNodeEnabled('intra-irca-local')) {
    intraMethods.push('irca_local')
    intraParams.irca_local = { ...byType('intra-irca-local') }
  }
  if (isNodeEnabled('intra-2dcos')) {
    intraMethods.push('2dcos')
    intraParams['2dcos'] = {}
  }

  // 分类分析
  if (isNodeEnabled('cls-lda')) {
    clsMethods.push('lda')
    clsParams.lda = { ...byType('cls-lda') }
  }
  if (isNodeEnabled('cls-rf')) {
    clsMethods.push('rf')
    clsParams.rf = { ...byType('cls-rf') }
  }
  if (isNodeEnabled('cls-svm')) {
    clsMethods.push('svm')
    clsParams.svm = {}
  }

  // 回归与量化分析
  if (isNodeEnabled('quant-pls')) {
    quantMethods.push('pls')
    quantParams.pls = { ...byType('quant-pls') }
  }
  if (isNodeEnabled('quant-mlr')) {
    quantMethods.push('mlr')
    quantParams.mlr = { ...byType('quant-mlr') }
  }
  if (isNodeEnabled('quant-glm')) {
    quantMethods.push('glm')
    quantParams.glm = { ...byType('quant-glm') }
  }

  // 拉曼标记分析
  if (isNodeEnabled('marker-rbcs')) {
    markerMethods.push('rbcs')
    markerParams.rbcs = { ...byType('marker-rbcs') }
  }
  if (isNodeEnabled('marker-roc')) {
    markerMethods.push('roc')
    markerParams.roc = { ...byType('marker-roc') }
  }
  if (isNodeEnabled('marker-correlations')) {
    markerMethods.push('correlations')
    markerParams.correlations = { ...byType('marker-correlations') }
  }

  return {
    quality_control: { methods: qcMethods, parameters: qcParams },
    spike_removal: spikeRemoval,
    smoothing: { methods: smoothingMethods, parameters: smoothingParams },
    baseline: { methods: baselineMethods, parameters: baselineParams },
    truncation,
    normalization,
    dim_reduction: { methods: dimMethods, parameters: dimParams },
    phenotype: { methods: phenMethods, parameters: phenParams },
    spectral_decomposition: { methods: decompMethods, parameters: decompParams },
    intra_ramanome: { methods: intraMethods, parameters: intraParams },
    classification: { methods: clsMethods, parameters: clsParams },
    quantification: { methods: quantMethods, parameters: quantParams },
    raman_markers: { methods: markerMethods, parameters: markerParams }
  }
}

// 验证连接是否有效（禁止自连接）
const isValidConnection = (connection: any) => {
  console.log('isValidConnection 被调用:', connection)

  // VueFlow 在 edge 已经创建后也可能用同一校验函数回放校验；
  // 这时 connection 会带 id（edge 对象），应直接放行，避免把“自身”误判为重复连接
  if (connection && typeof connection === 'object' && 'id' in connection && connection.id) {
    return true
  }
  
  // 检查源节点和目标节点ID
  const source = connection.source || connection.sourceNode?.id
  const target = connection.target || connection.targetNode?.id
  const sourceHandle = connection.sourceHandle
  const targetHandle = connection.targetHandle
  
  console.log('连接参数:', { source, target, sourceHandle, targetHandle })
  
  // 拖拽连线过程中，target 可能还未命中某个 handle；此时不应直接判无效，否则会导致“怎么都连不上”
  if (!source || source === '') {
    console.log('❌ 连接验证失败: 缺少source', { connection, source })
    return false
  }
  if (!target || target === '') {
    return true
  }
  
  // 如果源和目标相同，禁止连接
  if (source === target) {
    console.log('❌ 连接验证失败: 自连接', { source, target })
    return false
  }
  
  // 检查节点是否存在
  const sourceNodeExists = nodes.value.some(node => node.id === source)
  const targetNodeExists = nodes.value.some(node => node.id === target)
  
  if (!sourceNodeExists || !targetNodeExists) {
    console.log('❌ 连接验证失败: 节点不存在', { 
      source, 
      target, 
      sourceNodeExists, 
      targetNodeExists,
      availableNodes: nodes.value.map(n => n.id)
    })
    return false
  }
  
  // 检查是否已经存在相同的连接（包括相同的handle）
  const existingEdge = edges.value.find(
    (edge: WorkflowEdge) => 
      edge.source === source && 
      edge.target === target &&
      edge.sourceHandle === sourceHandle &&
      edge.targetHandle === targetHandle
  )
  if (existingEdge) {
    console.log('❌ 连接验证失败: 连接已存在', { source, target, sourceHandle, targetHandle, existingEdge })
    return false
  }
  
  console.log('✅ 连接验证通过:', { source, target, sourceHandle, targetHandle })
  return true
}

const onConnect = (params: Connection) => {
  // 使用 v-model:edges 时，通常需要在 connect 里手动写入 edge
  const source = params?.source
  const target = params?.target
  
  if (!source || !target || source === '' || target === '') {
    console.warn('❌ 连接失败: 缺少source或target', params)
    return
  }
  
  console.log('✅ 连接事件触发:', { source, target, sourceHandle: params.sourceHandle, targetHandle: params.targetHandle })

  const sourceHandle = params.sourceHandle || 'output'
  const targetHandle = params.targetHandle || 'input'

  // 去重：同 source/target/handles 只保留一条
  const exists = edges.value.some(
    (e: WorkflowEdge) =>
      e.source === source &&
      e.target === target &&
      (e.sourceHandle || 'output') === sourceHandle &&
      (e.targetHandle || 'input') === targetHandle
  )
  if (exists) return

  const id = `e-${source}-${sourceHandle}-${target}-${targetHandle}-${Date.now()}`
  edges.value.push({
    id,
    source,
    target,
    sourceHandle,
    targetHandle
  })
}

const onNodeClick = (event: any) => {
  // 检查是否点击了删除按钮
  const target = event.event?.target as HTMLElement
  const deleteBtn = target?.closest('button[title="删除"]') || target?.closest('.header-delete-btn')
  if (deleteBtn) {
    handleDeleteNode(event.node.id)
    return
  }
}

const onNodesChange = (changes: any[]) => {
  // 处理节点变化（包括删除）
  console.log('onNodesChange called, changes:', changes)
  console.log('Current nodes:', nodes.value)
  changes.forEach(change => {
    if (change.type === 'remove') {
      // 节点被删除，清除相关边
      edges.value = edges.value.filter(
        edge => edge.source !== change.id && edge.target !== change.id
      )
    }
  })
}

const onVueFlowInit = () => {
  console.log('Vue Flow init event fired')
}

const onVueFlowReady = (flowInstance: any) => {
  // Vue Flow 已经准备好
  vueFlowReady.value = true
  console.log('Vue Flow ready, nodes:', nodes.value)
  console.log('Vue Flow instance:', flowInstance)
  console.log('Node types:', nodeTypes.value)
  
  console.log('节点数量:', nodes.value.length)
  
  // 尝试自动适应视图
  if (flowInstance && flowInstance.fitView) {
    nextTick(() => {
      flowInstance.fitView({ padding: 0.2 })
    })
  }
}

const handleDeleteNode = (nodeId: string) => {
  if (confirm(t('editor.deleteConfirm'))) {
    // 删除相关的边
    edges.value = edges.value.filter(
      edge => edge.source !== nodeId && edge.target !== nodeId
    )
    // 删除节点
    nodes.value = nodes.value.filter(node => node.id !== nodeId)
  }
}

// 旧工作流配置导出逻辑已不使用（当前以分析参数为主）

// 执行工作流（传给后端）
const executeWorkflow = async () => {
  if (!projectId.value) {
    alert(t('common.errors.missingProjectIdEditor'))
    return
  }

  isExecuting.value = true
  setLeafNodesLoading(true)

  try {
    const payload = buildAnalysisPayload()

    // 先拉取一次已有分析结果（用于复用旧图/旧状态），不阻塞后续触发分析
    try {
      const resultsResp: any = await getProjectAnalysisResults(projectId.value)
      updateResultNodeFromResp(resultsResp)
    } catch (e) {
      // results 接口可能在“尚无结果”时返回错误；此处忽略，继续触发解析
      console.warn('analysis/results 拉取失败，继续触发解析', e)
    }

    // 再触发解析接口，同时把当前画布的工作流结构一并保存，便于日后重新打开还原
    const resp: any = await runProjectAnalysis(projectId.value, payload, serializeWorkflow())
    updateResultNodeFromResp(resp)
  } catch (e) {
    alert(e instanceof ApiError ? e.message : t('common.errors.runFailed'))
  } finally {
    setLeafNodesLoading(false)
    isExecuting.value = false
  }
}


// 打开编辑器时尝试恢复该项目最近一次的分析结果与画布工作流，
// 避免退出面板后就再也看不到已完成的结果和当时搭建的流程
const loadExistingAnalysisState = async () => {
  const pid = projectId.value
  if (!pid) return
  try {
    const resp: any = await getProjectAnalysisResults(pid)
    const result = resp?.result ?? {}
    if (nodes.value.length === 0 && result?.workflow) {
      restoreWorkflow(result.workflow as WorkflowConfig)
    }
    const hasPlots = result?.plots && Object.keys(result.plots).length > 0
    const hasAnalyses = Boolean(result?.analyses && Object.keys(result.analyses).length > 0)
    if (hasPlots || hasAnalyses) {
      updateResultNodeFromResp(resp)
    }
  } catch (e) {
    console.warn('恢复历史分析结果失败', e)
  }
}

onMounted(() => {
  void loadActiveDataFileName()
  void loadExistingAnalysisState()
  console.log('onMounted - 节点数量:', nodes.value.length)
  console.log('onMounted - 节点列表:', nodes.value)
  console.log('onMounted - nodeTypes:', nodeTypes.value)
})

watch(projectId, () => {
  void loadActiveDataFileName()
})
</script>

<style scoped>
.node-editor-wrapper {
  width: 100%;
  height: 100%;
  min-height: 0;
  display: flex;
  flex-direction: column;
  position: relative;
  flex: 1;
  overflow: hidden;
}

.node-editor {
  width: 100%;
  height: 100%;
  min-height: 0;
  display: flex;
  flex-direction: column;
  background: #f8fafc;
  flex: 1;
  overflow: hidden;
}

.editor-toolbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem 1.5rem;
  background: white;
  border-bottom: 1px solid #e2e8f0;
  flex-shrink: 0;
}

.editor-title {
  font-size: 1.25rem;
  font-weight: 700;
  color: #2d3748;
  margin: 0;
}

.toolbar-actions {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.toolbar-btn {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 1rem;
  background: white;
  border: 1px solid #e2e8f0;
  border-radius: 2px;
  color: #4a5568;
  font-weight: 500;
  font-size: 0.9rem;
  cursor: pointer;
  transition: all 0.3s ease;
}

.toolbar-btn:hover {
  background: #f7fafc;
  border-color: #cbd5e0;
}

.toolbar-btn.primary {
  background: #1a1a1a;
  color: white;
  border: none;
}

.toolbar-btn.primary:hover:not(:disabled) {
  transform: translateY(-1px);
  background: #1d4ed8;
  box-shadow: 0 4px 12px rgba(37, 99, 235, 0.25);
}

.toolbar-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.toolbar-btn svg {
  width: 16px;
  height: 16px;
}

.spinning {
  animation: spin 1s linear infinite;
}

@keyframes spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

.editor-main {
  flex: 1;
  min-height: 0;
  display: flex;
  overflow: hidden;
  position: relative;
  width: 100%;
  height: 100%;
}

/* 左侧节点面板 */
.node-palette {
  width: 280px;
  background: #ffffff;
  border-right: 1px solid #e5e7eb;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  flex-shrink: 0;
}

.palette-header {
  padding: 0.85rem 1rem 0.65rem;
  border-bottom: 1px solid #e5e7eb;
  background: #f8fafc;
}

.palette-flow-title {
  font-size: 0.7rem;
  font-weight: 700;
  color: #64748b;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  margin-bottom: 0.45rem;
}

.palette-flow-steps {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  margin-bottom: 0.65rem;
}

.flow-step {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.45rem 0.55rem;
  border-radius: 6px;
  border: 1px solid #e2e8f0;
  background: #fff;
  color: #334155;
  font-size: 0.78rem;
  font-weight: 600;
  cursor: pointer;
  text-align: left;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.flow-step:hover {
  background: #f1f5f9;
  border-color: #cbd5e1;
}

.flow-step.active {
  background: #1a1a1a;
  border-color: #1d4ed8;
  color: #fff;
}

.flow-step-indicator {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  border: 2px solid #cbd5e1;
  flex-shrink: 0;
  position: relative;
}

.flow-step.active .flow-step-indicator {
  border-color: rgba(255, 255, 255, 0.95);
}

.flow-step.active .flow-step-indicator::after {
  content: '';
  position: absolute;
  inset: 3px;
  border-radius: 50%;
  background: #fff;
}

.flow-step-num {
  flex: 0 0 auto;
  min-width: 1rem;
  font-weight: 700;
  font-size: 0.72rem;
  opacity: 0.85;
}

.flow-step.active .flow-step-num {
  opacity: 1;
}

.flow-step-label {
  flex: 1;
  min-width: 0;
  line-height: 1.25;
}

.palette-modules-head {
  padding-top: 0.35rem;
  border-top: 1px solid #e2e8f0;
  margin-top: 0.15rem;
}

.palette-modules-title {
  font-size: 0.7rem;
  font-weight: 700;
  color: #64748b;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.palette-header-top {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  margin-bottom: 0.6rem;
}

.palette-back-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  border: 1px solid rgba(226, 232, 240, 1);
  background: #ffffff;
  color: #0f172a;
  padding: 0.35rem 0.5rem;
  border-radius: 2px;
  cursor: pointer;
  font-size: 0.75rem;
  line-height: 1;
  font-weight: 600;
}

.palette-back-btn:hover {
  background: #f8fafc;
}

.tree-empty {
  padding: 0.75rem 0.65rem;
  color: rgba(226, 232, 240, 0.75);
  font-size: 0.9rem;
  line-height: 1.5;
}

.palette-header h3 {
  font-size: 1rem;
  font-weight: 700;
  color: #111827;
  margin: 0 0 0.75rem 0;
}

.palette-search {
  width: 100%;
}

.search-input {
  width: 100%;
  padding: 0.5rem 0.75rem;
  border: 1px solid #d1d5db;
  border-radius: 2px;
  font-size: 0.875rem;
  color: #374151;
  background: #ffffff;
  transition: all 0.2s ease;
}

.search-input:focus {
  outline: none;
  border-color: #1a1a1a;
  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}

.tree-sub-head {
  width: 100%;
  background: transparent;
  border: none;
  color: inherit;
  padding: 0.5rem 0.45rem;
  text-align: left;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-weight: 650;
  color: #111827;
}

.tree-cat-icon {
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.tree-cat-icon :deep(svg) {
  display: block;
}

.tree-sub-head-label {
  flex: 1;
  min-width: 0;
  text-align: left;
}

.tree-sub-head:hover {
  background: rgba(15, 23, 42, 0.04);
}

.caret {
  display: inline-block;
  transform: rotate(0deg);
  transition: transform 0.12s ease;
  color: rgba(15, 23, 42, 0.55);
}

.caret.open {
  transform: rotate(90deg);
}

.tree-leaves {
  padding: 0.15rem 0.2rem 0.65rem 1.65rem;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

.tree-leaf-btn {
  width: 100%;
  text-align: left;
  background: transparent;
  border: none;
  color: #334155;
  padding: 0.28rem 0.2rem 0.28rem 0.15rem;
  border-radius: 2px;
  cursor: pointer;
  font-size: 0.72rem;
  line-height: 1.35;
  font-weight: 500;
  display: flex;
  align-items: center;
  gap: 0.35rem;
}


.tree-leaf-btn:hover {
  background: rgba(15, 23, 42, 0.05);
  color: #0f172a;
}


.palette-content {
  flex: 1;
  /* 强制常驻滚动条（避免 macOS 叠加滚动条看起来“消失”） */
  overflow-y: scroll;
  scrollbar-gutter: stable;
  padding: 0.75rem;
  scrollbar-width: thin;
  scrollbar-color: rgba(148, 163, 184, 0.9) rgba(241, 245, 249, 1);
}

/* WebKit 滚动条（Chrome/Safari） */
.palette-content::-webkit-scrollbar {
  width: 10px;
}

.palette-content::-webkit-scrollbar-track {
  background: rgba(241, 245, 249, 1);
}

.palette-content::-webkit-scrollbar-thumb {
  background: rgba(148, 163, 184, 0.9);
  border-radius: 2px;
  border: 2px solid rgba(241, 245, 249, 1);
}

.palette-content::-webkit-scrollbar-thumb:hover {
  background: rgba(100, 116, 139, 0.95);
}

.palette-category {
  margin-bottom: 1.5rem;
}

.palette-category:last-child {
  margin-bottom: 0;
}

.category-title {
  font-size: 0.75rem;
  font-weight: 700;
  color: #6b7280;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  margin-bottom: 0.75rem;
  padding: 0 0.5rem;
}

.category-nodes {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.palette-node-item {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.875rem 1rem;
  background: #ffffff;
  border: 1px solid #e5e7eb;
  border-radius: 2px;
  cursor: pointer;
  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
  position: relative;
  overflow: hidden;
}

.palette-node-item::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  background: var(--node-color, #1a1a1a);
  transform: scaleY(0);
  transition: transform 0.2s ease;
}

.palette-node-item:hover {
  background: #f9fafb;
  border-color: var(--node-color, #1a1a1a);
  transform: translateX(2px);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}

.palette-node-item:hover::before {
  transform: scaleY(1);
}

.node-item-icon {
  width: 40px;
  height: 40px;
  border-radius: 2px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-weight: 700;
  font-size: 1.1rem;
  flex-shrink: 0;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.node-item-info {
  flex: 1;
  min-width: 0;
}

.node-item-label {
  font-size: 0.9rem;
  font-weight: 600;
  color: #111827;
  margin-bottom: 0.25rem;
}

.node-item-desc {
  font-size: 0.75rem;
  color: #6b7280;
  line-height: 1.4;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.editor-container {
  flex: 1;
  min-height: 400px; /* 确保有最小高度 */
  position: relative;
  background: #f9fafb;
  width: 100%;
  height: 100%;
  overflow: hidden;
  min-width: 0;
}

/* 确保 VueFlow 根元素有高度 */
.editor-container > :deep(.vue-flow) {
  width: 100% !important;
  height: 100% !important;
  min-height: 0 !important;
  position: absolute !important;
  top: 0 !important;
  left: 0 !important;
  right: 0 !important;
  bottom: 0 !important;
}

.floating-toolbar {
  position: absolute;
  top: 20px;
  left: 20px;
  z-index: 1000;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.toolbar-item {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.75rem 1rem;
  background: #ffffff;
  border: 1px solid #e5e7eb;
  border-radius: 2px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06);
  cursor: pointer;
  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
  color: #374151;
  font-size: 0.875rem;
  font-weight: 500;
  min-width: 140px;
}

.toolbar-item:hover {
  background: #f9fafb;
  border-color: #1a1a1a;
  color: #1a1a1a;
  transform: translateY(-2px);
  box-shadow: 0 6px 16px rgba(102, 126, 234, 0.2), 0 4px 8px rgba(0, 0, 0, 0.08);
}

.toolbar-item svg {
  width: 20px;
  height: 20px;
  color: #1a1a1a;
  flex-shrink: 0;
}

.toolbar-item:hover svg {
  color: #1a1a1a;
}

.context-menu {
  position: absolute;
  background: #ffffff;
  border-radius: 4px;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.12), 0 4px 12px rgba(0, 0, 0, 0.08);
  padding: 0.5rem;
  z-index: 1000;
  min-width: 260px;
  max-height: 500px;
  overflow-y: auto;
  overflow-x: hidden;
  border: 1px solid #e5e7eb;
  backdrop-filter: blur(10px);
  /* 自定义滚动条样式 */
  scrollbar-width: thin;
  scrollbar-color: #cbd5e0 #f1f5f9;
}

/* Webkit浏览器滚动条样式 */
.context-menu::-webkit-scrollbar {
  width: 8px;
}

.context-menu::-webkit-scrollbar-track {
  background: #f1f5f9;
  border-radius: 4px;
}

.context-menu::-webkit-scrollbar-thumb {
  background: #cbd5e0;
  border-radius: 4px;
  transition: background 0.2s ease;
}

.context-menu::-webkit-scrollbar-thumb:hover {
  background: #94a3b8;
}

.menu-category {
  margin-bottom: 0.5rem;
}

.menu-category:last-child {
  margin-bottom: 0;
}

.category-header {
  padding: 0.625rem 0.875rem;
  font-size: 0.7rem;
  font-weight: 700;
  color: #9ca3af;
  text-transform: uppercase;
  letter-spacing: 0.8px;
  background: #f9fafb;
  margin: 0.25rem 0;
  border-radius: 2px;
}

.menu-item {
  display: flex;
  align-items: center;
  gap: 0.875rem;
  padding: 0.875rem 1rem;
  cursor: pointer;
  border-radius: 2px;
  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
  color: #374151;
  margin: 0.125rem 0;
}

.menu-item:hover {
  background: #f3f4f6;
  color: #1a1a1a;
  transform: translateX(2px);
}

.menu-item-icon {
  width: 36px;
  height: 36px;
  border-radius: 2px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-weight: 700;
  font-size: 0.95rem;
  flex-shrink: 0;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.menu-item-info {
  flex: 1;
  min-width: 0;
}

.menu-item-label {
  font-size: 0.9rem;
  font-weight: 500;
  color: #2d3748;
}

.menu-item-desc {
  font-size: 0.75rem;
  color: #718096;
  margin-top: 0.25rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}


</style>

<style>
/* 确保 VueFlow 容器有正确的高度 - 使用全局选择器 */
.editor-container .vue-flow,
.editor-container > div[class*="vue-flow"] {
  width: 100% !important;
  height: 100% !important;
  min-height: 0 !important;
  position: absolute !important;
  top: 0 !important;
  left: 0 !important;
  right: 0 !important;
  bottom: 0 !important;
}

.editor-container .vue-flow__viewport,
.editor-container [class*="viewport"] {
  width: 100% !important;
  height: 100% !important;
}

.editor-container .vue-flow__container,
.editor-container [class*="container"] {
  width: 100% !important;
  height: 100% !important;
}

/* 连接线 - 实线 */
.vue-flow__edge-path {
  stroke: #1a1a1a;
  stroke-width: 2;
  stroke-dasharray: none;
  transition: stroke-width 0.2s ease, stroke 0.2s ease;
}

.vue-flow__edge:hover .vue-flow__edge-path {
  stroke-width: 3;
  stroke: #5568d3;
}

.vue-flow__edge.selected .vue-flow__edge-path {
  stroke: #5568d3;
  stroke-width: 3;
}

/* 全局样式：确保节点按钮可见 */
.vue-flow__node .node-footer,
.vue-flow__node .footer-btn,
.vue-flow__node .node-actions,
.vue-flow__node .action-btn {
  display: flex !important;
  visibility: visible !important;
  opacity: 1 !important;
  pointer-events: auto !important;
}

.vue-flow__node .node-footer {
  display: flex !important;
  opacity: 1 !important;
  visibility: visible !important;
}

.vue-flow__node:hover .node-footer {
  opacity: 1 !important;
  display: flex !important;
}

.vue-flow__node.selected .node-footer {
  opacity: 1 !important;
  display: flex !important;
}

/* 确保按钮不被隐藏 */
.vue-flow__node button.footer-btn,
.vue-flow__node button.action-btn {
  display: flex !important;
  visibility: visible !important;
  opacity: 1 !important;
  pointer-events: auto !important;
}

/* 确保节点内容不被裁剪 */
.vue-flow__node {
  overflow: visible !important;
  outline: none !important;
  /* 去掉 VueFlow 默认节点外框（连接/选中时会出现黑框） */
  border: none !important;
  background: transparent !important;
}

/* 确保连接点可见且可交互 */
.vue-flow__handle {
  visibility: visible !important;
  opacity: 1 !important;
  pointer-events: auto !important;
  z-index: 1000 !important;
  width: 12px !important;
  height: 12px !important;
  border: 2px solid white !important;
  border-radius: 50% !important;
  cursor: pointer !important;
  position: absolute !important;
}

.vue-flow__pane {
  cursor: default !important;
}

.vue-flow__node {
  cursor: grab !important;
}

/* 输入连接点（绿色） */
.vue-flow__handle[data-handlepos="left"],
.vue-flow__handle.node-handle-input {
  background: #10b981 !important;
}

/* 输出连接点（橙色） */
.vue-flow__handle[data-handlepos="right"],
.vue-flow__handle.node-handle-output {
  background: #3b82f6 !important;
}

.vue-flow__handle:hover {
  transform: scale(1.3) !important;
  box-shadow: 0 0 0 6px rgba(102, 126, 234, 0.15) !important;
  z-index: 1001 !important;
}

.vue-flow__node:hover .vue-flow__handle {
  opacity: 1 !important;
  visibility: visible !important;
}

/* 确保节点容器不裁剪连接点 */
.vue-flow__node .workflow-node-component,
.vue-flow__node .result-node-component {
  overflow: visible !important;
}

/* 确保连接线可见 - 实线 */
.vue-flow__edge {
  stroke: #1a1a1a !important;
  stroke-width: 2 !important;
  stroke-dasharray: none !important;
  z-index: 100 !important;
}

.vue-flow__edge-path {
  stroke: #1a1a1a !important;
  stroke-width: 2 !important;
  stroke-dasharray: none !important;
}

/* 移除Vue Flow默认的选中边框 */
.vue-flow__node.selected {
  outline: none !important;
  box-shadow: none !important;
}

/* 某些主题会给 default 节点包一层边框 */
.vue-flow__node-default,
.vue-flow__node-input,
.vue-flow__node-output {
  border: none !important;
  background: transparent !important;
  box-shadow: none !important;
}

.vue-flow__node.selected .workflow-node-component {
  outline: none !important;
}

/* 强制显示操作按钮 */
.vue-flow__node .workflow-node-component .node-footer {
  display: flex !important;
  opacity: 1 !important;
  visibility: visible !important;
}

.vue-flow__node .workflow-node-component .node-actions {
  display: flex !important;
  opacity: 1 !important;
  visibility: visible !important;
}

/* 确保按钮可见 */
.vue-flow__node button.footer-btn,
.vue-flow__node button.action-btn {
  display: inline-flex !important;
  visibility: visible !important;
  opacity: 1 !important;
  pointer-events: auto !important;
}
</style>
