<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import KnowledgeWorkspace from './components/KnowledgeWorkspace.vue'
import SimulationWorkspace from './components/SimulationWorkspace.vue'

type AppMode = 'teaching' | 'testing' | 'diagnostic' | 'simulation' | 'knowledge'
type StudyScope = 'overall' | 'part'
type DiagnosticTargetKind = 'part' | 'assembly' | 'drawing-part' | 'drawing-assembly'

type Standard = {
  standard_id: string
  title: string
  kind: string
  source_type: string
  summary: string
  steps: unknown[]
}

type LessonStep = {
  step_id: string
  order: number
  title: string
  voice_text: string
  expected_skill: string
  expected_arguments: Record<string, unknown>
  teaching_intent?: string
  common_mistake?: string
}

type Session = {
  session_id: string
  mode: AppMode
  title: string
  current_step_index: number
  lesson_steps: LessonStep[]
  status: string
}

type TeachingScript = {
  standard_id: string
  title: string
  role: string
  summary: string
  steps: TeachingScriptStep[]
}

type TeachingScriptStep = {
  order: number
  skill: string
  source_feature: string
  title: string
  voice_text: string
  teaching_intent: string
  solid_works_command_hint: string
  common_mistake: string
  key_parameters: Record<string, unknown>
}

type TeachingPollResponse = {
  observed_step_count: number
  result: {
    ok: boolean
    feedback_text: string
  }
  session_status: string
  current_step_index: number
}

type TeachingIntroduction = {
  standard_id: string
  title: string
  standard_part_path: string
  voice_text: string
  overview: string
  reducer_position: string
  functional_role: string
  key_features: string
  modeling_focus: string
  source_notes: string[]
  knowledge_snippets: string[]
  highlight_targets: HighlightTarget[]
  knowledge_coverage: KnowledgeCoverage
}

type HighlightTarget = {
  id: string
  kind: string
  role: string
  label: string
  surface_type: string
  raw_surface_type: string
  source_feature: string
  description: string
  highlight_status: string
}

type HighlightResult = {
  ok: boolean
  level: string
  matched_name: string
  message: string
  score: number
  voice_text: string
}

type KnowledgeCoverage = {
  has_standard_part: boolean
  has_framework_category: boolean
  has_functional_faces: boolean
  functional_face_count: number
  highlight_support: string
  missing_items: string[]
}

type MechanicalArtifact = {
  name: string
  file_path: string
  exists: boolean
  length: number
  last_modified?: string
}

type MechanicalDiagnosticRunResult = {
  ok: boolean
  stage: string
  model_path: string
  output_dir: string
  command: {
    project_path: string
    arguments: string[]
    exit_code: number
    standard_output: string
    standard_error: string
    started_at: string
    completed_at: string
  }
  artifacts: MechanicalArtifact[]
  next_hint: string
}

type MechanicalEvidenceImage = {
  name: string
  url: string
  image_kind?: string
  evidence_family?: string
  evidence_purpose?: string
  position_visibility?: string
  source_unit_key?: string
  source_local_id?: string
  source_anchor_id?: string
}

type MechanicalRetrievedIssue = {
  unit_key: string
  component_id?: string
  instance_name?: string
  local_id: string
  retrieval_sentence: string
  anchor_type?: string
  anchor_id?: string
  pattern_type?: string
  rule_id: string
  rule_title: string
  primary_view: string
  score: number
  image_evidence_names?: string[]
  evidence_images: MechanicalEvidenceImage[]
}

type MechanicalFinalIssue = {
  judgement: string
  rule_id: string
  rule_title: string
  location: string
  reason: string
  suggestion: string
  report_block_markdown: string
  related_unit_keys: string[]
  related_anchor_ids: string[]
  evidence_images: MechanicalEvidenceImage[]
}

type DiagnosticIssueCard = MechanicalRetrievedIssue & {
  judgement: string
  final_issue?: boolean
  related_unit_keys?: string[]
  related_anchor_ids?: string[]
}

type MechanicalAiDiagnosisResult = {
  ok: boolean
  model_path: string
  output_dir: string
  final_status: string
  final_message: string
  final_report_markdown: string
  semantic_graph_path: string
  retrieval_path: string
  final_report_path: string
  final_response_path: string
  retrieved_candidate_count: number
  retrieved_issues: MechanicalRetrievedIssue[]
  final_issues?: MechanicalFinalIssue[]
  artifacts: MechanicalArtifact[]
  bridge: MechanicalDiagnosticRunResult
}

type DiagnosticResultTab = 'report' | 'issues' | 'evidence' | 'artifacts'

type DiagnosticReportBlock = {
  type: 'heading' | 'paragraph' | 'list'
  level?: number
  text?: string
  items?: string[]
}

type MechanicalDiagnosisUnitProgress = {
  local_id: string
  display_name: string
  retrieval_sentence: string
  primary_view: string
  candidate_count: number
}

type MechanicalSubgraphComponentProgress = {
  component_id: string
  instance_name: string
  display_name: string
  evidence_path: string
  current_index: number
  total_components: number
  processed_components: number
  iteration: number
  status: string
  message: string
  feature_count: number
  face_count: number
  contact_count: number
  mate_count: number
  image_count: number
  adjacent_component_ids: string[]
}

type MechanicalDiagnosisJobStatus = {
  job_id: string
  status: 'running' | 'done' | 'failed' | string
  phase: string
  message: string
  model_path: string
  output_dir: string
  total_units: number
  processed_units: number
  remaining_units: number
  retrieved_candidate_count: number
  total_subgraph_components: number
  processed_subgraph_components: number
  current_subgraph_component?: MechanicalSubgraphComponentProgress | null
  current_unit?: MechanicalDiagnosisUnitProgress | null
  retrieved_issues: MechanicalRetrievedIssue[]
  result?: MechanicalAiDiagnosisResult | null
  error?: string
}

type MechanicalDiagnosticUsage = {
  usage_doc_path: string
  usage_markdown: string
  knowledge_index_root: string
  knowledge_files: string[]
  supported_stages: string[]
  ai_configured: boolean
  ai_model: string
  ai_text_model: string
  ai_base_url: string
  ai_api_mode: string
  backend_role: string
}

type DiagnosticProgressStep = {
  key: string
  title: string
  detail: string
  thresholdSeconds: number
}

type DwgDraftCategoryFile = {
  category: string
  file: string
  full_path: string
  item_count: number
  exists: boolean
}

type DwgDraftExtractionResult = {
  ok: boolean
  source: string
  prog_id: string
  started: boolean
  drawing_path: string
  export_dir: string
  manifest_path: string
  entity_count: number
  type_counts: Record<string, number>
  category_files: DwgDraftCategoryFile[]
  message: string
}

type DwgDraftBridgeStatus = {
  ok: boolean
  alive: boolean
  bridge_dir: string
  heartbeat_path: string
  last_write_time_utc?: string
  message: string
}

type DwgDraftLatestExtractionResult = {
  ok: boolean
  found: boolean
  export_dir: string
  manifest_path: string
  last_write_time_utc?: string
  category_files: DwgDraftCategoryFile[]
  message: string
}

type DrawingAssemblyArtifact = {
  name: string
  file_path: string
  exists: boolean
  length: number
  last_modified?: string
}

type DrawingIssueBounds = {
  min_x: number
  min_y: number
  max_x: number
  max_y: number
}

type DrawingAssemblyFormatIssue = {
  id: string
  line_class: string
  label: string
  expected_style: string
  current_style: string
  reason: string
  confidence: string
  bounds: DrawingIssueBounds
  evidence_image_path: string
  evidence_image_url: string
}

type DrawingAssemblyAnalysisResult = {
  ok: boolean
  stage: string
  message: string
  output_dir: string
  view_id: string
  student_dwg_path?: string
  model_path?: string
  student_view_image: string
  ownership_image: string
  probe_path: string
  supplemental_facts_path: string
  candidate_issues_path: string
  retrieved_rules_path: string
  final_report_path: string
  final_report_markdown?: string
  pipeline_manifest_path?: string
  artifacts: DrawingAssemblyArtifact[]
  format_issues?: DrawingAssemblyFormatIssue[]
}

type DrawingInterfaceContextCandidate = {
  candidate_id: string
  binding_status: string
  dwg_annotation?: {
    text?: string
    fit_code?: string
    nominal_size_mm?: number
    interface_diameter_mm?: number
    drawing_geometry_binding?: {
      status?: string
      point_bindings?: unknown[]
    }
  }
  related_3d_interfaces?: unknown[]
  drawing_boundary_to_3d_interface_matches?: Array<{
    interface_group_id?: string
    brep_binding_status?: string
    brep_face_pairs?: unknown[]
  }>
}

type DrawingInterfaceContextResult = {
  schema: string
  drawing_ownership_context?: {
    available?: boolean
    source_path?: string
    sample_count?: number
    component_count?: number
  }
  assembly_function_context?: {
    component_function_profiles?: unknown[]
    interface_function_profiles?: {
      cylindrical?: unknown[]
      planar?: unknown[]
    }
  }
  tolerance_assessment_context?: {
    tolerance_candidates?: DrawingInterfaceContextCandidate[]
    unannotated_interface_candidates?: unknown[]
  }
  roughness_assessment_context?: {
    roughness_candidates?: unknown[]
    unannotated_interface_surface_candidates?: unknown[]
  }
}

const apiBase = String(import.meta.env.VITE_API_BASE || 'http://127.0.0.1:7200').replace(/\/$/, '')
const overallStandardId = 'part-misc-two-stage-reducer'
const designTargetStandardId = 'part-gear-gear-1-high-speed-shaft'

const status = ref('未连接')
const standards = ref<Standard[]>([])
const mode = ref<AppMode | null>(null)
const projectSelected = ref(false)
const scope = ref<StudyScope | null>(null)
const selected = ref<Standard | null>(null)
const session = ref<Session | null>(null)
const feedback = ref('')
const pollingMessage = ref('')
const pollingBusy = ref(false)
const autoListening = ref(false)
const startingSession = ref(false)
const teachingScript = ref<TeachingScript | null>(null)
const loadingTeachingScript = ref(false)
const introduction = ref<TeachingIntroduction | null>(null)
const loadingIntroduction = ref(false)
const openingStandardPart = ref(false)
const highlightingTargetId = ref('')
const introPlaying = ref(false)
const observedSkill = ref('create_new_part')
const observedArgs = ref('{}')
const diagnosticTargetKind = ref<DiagnosticTargetKind | null>(null)
const diagnosticModelPath = ref('')
const diagnosticOutputDir = ref('')
const diagnosticProductDescription = ref('')
const diagnosticPartFunctionProfile = ref('')
const diagnosticPurchasedComponentHints = ref('')
const drawingPath = ref('')
const drawingOutputDir = ref('')
const drawingProgId = ref('')
const drawingVisible = ref(true)
const drawingUseBridge = ref(true)
const drawingCloseAfterExtract = ref(false)
const drawingMaxEntities = ref(100000)
const drawingExtractionResult = ref<DwgDraftExtractionResult | null>(null)
const drawingBridgeStatus = ref<DwgDraftBridgeStatus | null>(null)
const drawingStudentViewImage = ref('')
const drawingOwnershipImage = ref('')
const drawingDwgExtractionDir = ref('')
const drawingMechanicalContextDir = ref('')
const drawingOwnershipContextDir = ref('')
const drawingAnalysisOutputDir = ref('')
const drawingAssemblyAnalysisResult = ref<DrawingAssemblyAnalysisResult | null>(null)
const drawingInterfaceContextResult = ref<DrawingInterfaceContextResult | null>(null)
const diagnosticBusy = ref(false)
const diagnosticMessage = ref('')
const diagnosticResult = ref<MechanicalAiDiagnosisResult | null>(null)
const diagnosticJob = ref<MechanicalDiagnosisJobStatus | null>(null)
const diagnosticUsage = ref<MechanicalDiagnosticUsage | null>(null)
const diagnosticResultTab = ref<DiagnosticResultTab>('report')
const diagnosticActiveStep = ref(0)
const diagnosticElapsedSeconds = ref(0)
const diagnosticProgressState = ref<'idle' | 'running' | 'done' | 'failed'>('idle')
let pollTimer: number | undefined
let diagnosticProgressTimer: number | undefined
let diagnosticStatusTimer: number | undefined
let lastSpokenFeedbackKey = ''
let introPlaybackRun = 0

const assemblyDiagnosticProgressSteps: DiagnosticProgressStep[] = [
  {
    key: 'extract',
    title: '提取零件 B-rep 与装配关系',
    detail: '在装配体坐标系中读取各零件实例的 B-rep、包围盒、接触、配合、同轴和平面贴合等装配关系信息。',
    thresholdSeconds: 0,
  },
  {
    key: 'component_images',
    title: '提取零件高亮图',
    detail: '按零件实例导出装配体中的零件高亮图；对被遮挡零件保留直接相连零件作为上下文。',
    thresholdSeconds: 0,
  },
  {
    key: 'component_functions',
    title: 'LLM 生成各零件功能介绍',
    detail: '让 LLM 结合产品描述、B-rep、图片、接触和配合关系，先判断每个零件在整机中的功能和接口角色。',
    thresholdSeconds: 0,
  },
  {
    key: 'functional_groups',
    title: '形成多个功能组',
    detail: '按照功能关系、直接连接关系和内部遮挡关系，把零件划分为若干功能组，后续按组分析。',
    thresholdSeconds: 0,
  },
  {
    key: 'group_part_checks',
    title: '按功能组走零件查错过程',
    detail: '依次对每个功能组提取组内零件 B-rep、图片和局部结构证据，并对组内非标零件执行零件查错流程。',
    thresholdSeconds: 0,
  },
  {
    key: 'retrieval',
    title: '检索功能组相关规则',
    detail: '针对功能组内的配合关系问题和各零件自身结构问题，检索对应动态知识表和禁忌规则。',
    thresholdSeconds: 0,
  },
  {
    key: 'judge',
    title: '判定配合关系和零件自身问题',
    detail: '结合功能组事实、配合关系、零件局部证据和检索规则，判定组内装配问题及各零件自身结构问题。',
    thresholdSeconds: 0,
  },
]

const partDiagnosticProgressSteps: DiagnosticProgressStep[] = [
  {
    key: 'extract',
    title: '提取 B-rep 信息与特征分组',
    detail: '读取零件的面、边、孔、平面、圆柱面、包围盒等 B-rep 信息，并先把孔组、平面组、圆柱面组和未知曲面候选分组。',
    thresholdSeconds: 0,
  },
  {
    key: 'feature_images',
    title: '提取特征高亮图片',
    detail: '按特征组导出对应高亮图片；对于内部有结构的回转体零件，补充中心线剖视图作为辅助证据。',
    thresholdSeconds: 0,
  },
  {
    key: 'coarse_scan',
    title: '获取粗扫描信息',
    detail: '让 LLM 结合静态知识表、B-rep 信息和特征图片，先生成面向知识错误类型的粗扫描信息。',
    thresholdSeconds: 0,
  },
  {
    key: 'dynamic_retrieval',
    title: '检索动态知识表',
    detail: '只用粗扫描信息检索动态侦查块和相关规则，不把粗扫描结论直接作为判定依据。',
    thresholdSeconds: 0,
  },
  {
    key: 'local_subgraphs',
    title: '生成局部子图',
    detail: '结合动态侦查信息、B-rep 和特征图片，生成用于判定的局部语义子图。',
    thresholdSeconds: 0,
  },
  {
    key: 'judge',
    title: '规则判定',
    detail: '把完整规则条款与局部子图、原始 B-rep 和图片证据逐条对比，输出问题位置、原因和修改建议。',
    thresholdSeconds: 0,
  },
]

const modeText = computed(() => (
  mode.value === 'testing'
    ? '测试'
    : mode.value === 'diagnostic'
      ? '查错'
      : mode.value === 'simulation'
        ? '仿真'
        : mode.value === 'knowledge'
          ? '知识库'
          : '学习'
))
const overallStandard = computed(() => standards.value.find(item => item.standard_id === overallStandardId) ?? null)
const partStandards = computed(() =>
  standards.value
    .filter(item => item.source_type === 'manifest')
    .filter(item => item.standard_id !== overallStandardId)
    .filter(item => item.steps.length > 0)
    .sort((a, b) => a.title.localeCompare(b.title, 'zh-CN')),
)
const currentStep = computed(() => {
  if (!session.value) return null
  return session.value.lesson_steps[session.value.current_step_index] ?? null
})
const progressText = computed(() => {
  if (!session.value) return ''
  const total = session.value.lesson_steps.length
  const current = Math.min(session.value.current_step_index + 1, total)
  return `${current}/${total}`
})
const visibleDiagnosticIssues = computed(() => diagnosticResult.value?.retrieved_issues ?? diagnosticJob.value?.retrieved_issues ?? [])
const diagnosticRetrievedCandidates = computed(() =>
  visibleDiagnosticIssues.value
    .filter((issue, index, issues) => issues.findIndex(item => diagnosticIssueIdentity(item) === diagnosticIssueIdentity(issue)) === index)
    .sort((a, b) => b.score - a.score || a.rule_id.localeCompare(b.rule_id, 'zh-CN')),
)
const diagnosticIssues = computed(() =>
  visibleDiagnosticIssues.value
    .filter(issue => issue.rule_id.startsWith('18.'))
    .filter((issue, index, issues) => issues.findIndex(item => diagnosticIssueIdentity(item) === diagnosticIssueIdentity(issue)) === index),
)
const diagnosticIssueCards = computed<DiagnosticIssueCard[]>(() => {
  const finalIssues = diagnosticResult.value?.final_issues ?? []
  if (finalIssues.length) {
    return finalIssues
      .map((issue, index) => ({
        unit_key: issue.related_unit_keys.join('|') || `final_issue_${index}`,
        component_id: '',
        instance_name: '',
        local_id: issue.related_anchor_ids.join('、') || `final_issue_${index}`,
        retrieval_sentence: issue.location || issue.related_anchor_ids.join('、'),
        anchor_type: 'final_issue',
        anchor_id: issue.related_anchor_ids.join('、'),
        pattern_type: '',
        rule_id: issue.rule_id,
        rule_title: issue.rule_title,
        primary_view: '',
        score: 0,
        image_evidence_names: issue.evidence_images.map(image => image.name),
        evidence_images: issue.evidence_images,
        judgement: issue.report_block_markdown || formatFinalIssueJudgement(issue),
        final_issue: true,
        related_unit_keys: issue.related_unit_keys,
        related_anchor_ids: issue.related_anchor_ids,
      }))
      .filter(issue => isDisplayedFinalIssue(issue.judgement))
  }

  return dedupeDiagnosticIssueCards(diagnosticIssues.value
    .map(issue => ({
      ...issue,
      judgement: extractIssueJudgement(issue, diagnosticResult.value?.final_report_markdown ?? diagnosticResult.value?.final_message ?? ''),
    }))
    .filter(issue => !diagnosticResult.value || isDisplayedFinalIssue(issue.judgement)))
})
const diagnosticIssueGroups = computed(() => {
  const groups = new Map<string, {
    rule_id: string
    rule_title: string
    issues: DiagnosticIssueCard[]
  }>()

  for (const issue of diagnosticIssueCards.value) {
    const key = issue.rule_id
    if (!groups.has(key)) {
      groups.set(key, {
        rule_id: issue.rule_id,
        rule_title: issue.rule_title,
        issues: [],
      })
    }
    groups.get(key)!.issues.push(issue)
  }

  return Array.from(groups.values()).sort((a, b) => a.rule_id.localeCompare(b.rule_id, 'zh-CN'))
})
const diagnosticEvidenceImageCount = computed(() => new Set(
  diagnosticRetrievedCandidates.value.flatMap(issue => issue.evidence_images.map(image => image.url)),
).size)
const diagnosticArtifacts = computed(() => {
  const artifacts = [
    ...(diagnosticResult.value?.artifacts ?? []),
    ...(diagnosticResult.value?.bridge?.artifacts ?? []),
  ]
  return artifacts.filter((artifact, index) =>
    artifacts.findIndex(item => item.file_path === artifact.file_path) === index,
  )
})
const diagnosticReportBlocks = computed(() => parseDiagnosticMarkdown(diagnosticResult.value?.final_report_markdown ?? ''))
const diagnosticProgressSteps = computed(() =>
  diagnosticTargetKind.value === 'part' ? partDiagnosticProgressSteps : assemblyDiagnosticProgressSteps,
)
const isDrawingDiagnostic = computed(() =>
  diagnosticTargetKind.value === 'drawing-part' || diagnosticTargetKind.value === 'drawing-assembly',
)
const isDrawingPartDiagnostic = computed(() => diagnosticTargetKind.value === 'drawing-part')
const isDrawingAssemblyDiagnostic = computed(() => diagnosticTargetKind.value === 'drawing-assembly')
const drawingAssemblyFormatIssues = computed(() => drawingAssemblyAnalysisResult.value?.format_issues ?? [])
const drawingAssemblyAnnotationReport = computed(() =>
  extractDrawingReportSection(drawingAssemblyAnalysisResult.value?.final_report_markdown ?? '', '2. 标注错误', '3. 绘制错误'),
)
const drawingAssemblyRepresentationReport = computed(() =>
  extractDrawingReportSection(drawingAssemblyAnalysisResult.value?.final_report_markdown ?? '', '3. 绘制错误'),
)
const diagnosticTitle = computed(() => {
  if (diagnosticTargetKind.value === 'part') return '三维零件图查错'
  if (diagnosticTargetKind.value === 'assembly') return '三维装配体查错'
  if (diagnosticTargetKind.value === 'drawing-part') return '二维零件图查错'
  return '二维装配图查错'
})
const diagnosticModelPlaceholder = computed(() =>
  diagnosticTargetKind.value === 'part' ? 'D:\\path\\model.SLDPRT' : 'D:\\path\\model.SLDASM',
)
const diagnosticDescriptionText = computed(() => {
  if (diagnosticTargetKind.value === 'drawing-part') {
    return '二维零件图查错读取学生 DWG，格式错误由后端程序化线型/线宽规则直接判定；标注错误和绘制错误仍保留给 AI 分析模块。'
  }
  if (diagnosticTargetKind.value === 'drawing-assembly') {
    return '二维装配图查错读取装配 DWG 工程图，覆盖提取归属信息、绘制工程图导出 DWG、图纸实体抽取、后续上色和装配图分析等流程。'
  }
  if (diagnosticTargetKind.value === 'part') {
    return '零件查错只分析单个零件本身：先提取 B-rep 并做特征分组，再导出特征高亮图，随后由 LLM 结合静态知识表生成粗扫描信息，用粗扫描检索动态知识表，最后生成局部子图并判定。'
  }
  return '装配体查错先提取各零件 B-rep 和装配关系，再导出零件高亮图，让 LLM 理解每个零件功能并形成多个功能组；之后按功能组走零件查错过程，并同时判定组内配合关系问题和零件自身问题。'
})
const diagnosticStartDisabledReason = computed(() => {
  if (isDrawingDiagnostic.value) {
    return ''
  }
  if (!diagnosticModelPath.value.trim()) return '请先填写 SolidWorks 模型路径。'
  if (diagnosticTargetKind.value === 'assembly' && !diagnosticPurchasedComponentHints.value.trim()) {
    return '装配体查错需要填写外购件/不分析零件的完整名称。'
  }
  if (diagnosticTargetKind.value === 'part' && !diagnosticPartFunctionProfile.value.trim()) {
    return '零件查错需要先填写该零件的功能画像。'
  }
  return ''
})
const canRunMechanicalDiagnosis = computed(() => !diagnosticBusy.value && !diagnosticStartDisabledReason.value)
const canRunDrawingExtraction = computed(() =>
  isDrawingDiagnostic.value && !diagnosticBusy.value && !!drawingPath.value.trim(),
)
const drawingExtractionDisabledReason = computed(() =>
  isDrawingDiagnostic.value && !drawingPath.value.trim() ? '如需重新抽取 DWG 结构化信息，请填写 DWG 图纸路径。' : '',
)
const drawingPartAnalysisDisabledReason = computed(() => {
  if (!isDrawingPartDiagnostic.value) return ''
  if (!drawingPath.value.trim()) return '请先填写学生零件 DWG 路径。'
  return ''
})
const drawingAssemblyAnalysisDisabledReason = computed(() => {
  if (!isDrawingAssemblyDiagnostic.value) return ''
  if (!drawingPath.value.trim()) return '请先填写学生原始 DWG 路径。'
  if (!diagnosticModelPath.value.trim()) return '请先填写三维装配体模型路径。'
  return ''
})
const canRunDrawingPartAnalysis = computed(() =>
  isDrawingPartDiagnostic.value && !diagnosticBusy.value && !drawingPartAnalysisDisabledReason.value,
)
const canRunDrawingAssemblyAnalysis = computed(() =>
  isDrawingAssemblyDiagnostic.value && !diagnosticBusy.value && !drawingAssemblyAnalysisDisabledReason.value,
)
const drawingInterfaceContextDisabledReason = computed(() => {
  if (!isDrawingAssemblyDiagnostic.value) return ''
  if (!drawingDwgExtractionDir.value.trim() && !drawingExtractionResult.value?.export_dir) return '请先填写 DWG 提取目录，或先读取/抽取一次 DWG。'
  if (!drawingMechanicalContextDir.value.trim()) return '请先填写三维接触分析目录。'
  if (!drawingOwnershipContextDir.value.trim()) return '请先填写二维线条零件归属目录。'
  return ''
})
const canBuildDrawingInterfaceContext = computed(() =>
  isDrawingAssemblyDiagnostic.value && !diagnosticBusy.value && !drawingInterfaceContextDisabledReason.value,
)
const drawingTypeCountEntries = computed(() =>
  Object.entries(drawingExtractionResult.value?.type_counts ?? {})
    .sort((a, b) => b[1] - a[1])
    .slice(0, 12),
)
const drawingCategoryFiles = computed(() =>
  (drawingExtractionResult.value?.category_files ?? []).filter(file => file.exists || file.item_count > 0),
)
const drawingInterfaceToleranceCandidates = computed(() =>
  drawingInterfaceContextResult.value?.tolerance_assessment_context?.tolerance_candidates ?? [],
)
const drawingInterfaceSummary = computed(() => {
  const context = drawingInterfaceContextResult.value
  const cylindrical = context?.assembly_function_context?.interface_function_profiles?.cylindrical?.length ?? 0
  const planar = context?.assembly_function_context?.interface_function_profiles?.planar?.length ?? 0
  const candidates = context?.tolerance_assessment_context?.tolerance_candidates ?? []
  return {
    components: context?.assembly_function_context?.component_function_profiles?.length ?? 0,
    cylindrical,
    planar,
    tolerances: candidates.length,
    brepBound: candidates.filter(item =>
      (item.drawing_boundary_to_3d_interface_matches ?? []).some(match =>
        match.brep_binding_status === 'brep_face_pair_candidate_by_boundary_pair_and_nominal_diameter',
      ),
    ).length,
    ownershipSamples: context?.drawing_ownership_context?.sample_count ?? 0,
  }
})
const drawingInterfaceRows = computed(() =>
  drawingInterfaceToleranceCandidates.value.map(candidate => {
    const matches = candidate.drawing_boundary_to_3d_interface_matches ?? []
    const strong = matches.filter(match =>
      match.brep_binding_status === 'brep_face_pair_candidate_by_boundary_pair_and_nominal_diameter',
    )
    const interfaces = Array.from(new Set(strong.map(match => match.interface_group_id).filter(Boolean)))
    return {
      id: candidate.candidate_id,
      text: candidate.dwg_annotation?.text || candidate.candidate_id,
      binding: candidate.binding_status,
      geometry: candidate.dwg_annotation?.drawing_geometry_binding?.status || '未绑定',
      brepStrong: strong.length,
      uniqueInterfaces: interfaces.join('、') || '无',
    }
  }),
)

function extractDrawingReportSection(markdown: string, startTitle: string, nextTitle?: string) {
  if (!markdown.trim()) return ''
  const marker = `## ${startTitle}`
  const start = markdown.indexOf(marker)
  if (start < 0) return ''
  const contentStart = start + marker.length
  const end = nextTitle ? markdown.indexOf(`## ${nextTitle}`, contentStart) : -1
  return markdown.slice(contentStart, end >= 0 ? end : undefined).trim()
}

function formatBounds(bounds: DrawingIssueBounds) {
  return `x ${bounds.min_x.toFixed(2)}-${bounds.max_x.toFixed(2)}, y ${bounds.min_y.toFixed(2)}-${bounds.max_y.toFixed(2)}`
}

function parseDiagnosticMarkdown(markdown: string): DiagnosticReportBlock[] {
  const blocks: DiagnosticReportBlock[] = []
  const lines = markdown.replace(/\r\n/g, '\n').split('\n')
  let paragraph: string[] = []
  let list: string[] = []

  const flushParagraph = () => {
    if (!paragraph.length) return
    blocks.push({ type: 'paragraph', text: cleanMarkdownText(paragraph.join(' ')) })
    paragraph = []
  }
  const flushList = () => {
    if (!list.length) return
    blocks.push({ type: 'list', items: list.map(cleanMarkdownText) })
    list = []
  }

  for (const rawLine of lines) {
    const line = rawLine.trim()
    if (!line) {
      flushParagraph()
      flushList()
      continue
    }

    const heading = line.match(/^(#{1,6})\s+(.+)$/)
    if (heading) {
      flushParagraph()
      flushList()
      blocks.push({ type: 'heading', level: heading[1].length, text: cleanMarkdownText(heading[2]) })
      continue
    }

    const listItem = line.match(/^[-*+]\s+(.+)$/)
    if (listItem) {
      flushParagraph()
      list.push(listItem[1])
      continue
    }

    flushList()
    paragraph.push(line)
  }

  flushParagraph()
  flushList()
  return blocks
}

function cleanMarkdownText(value: string) {
  return value
    .replace(/\*\*(.*?)\*\*/g, '$1')
    .replace(/__(.*?)__/g, '$1')
    .replace(/`([^`]+)`/g, '$1')
    .trim()
}

function formatArtifactSize(bytes: number) {
  if (bytes < 1024) return `${bytes} B`
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
  return `${(bytes / 1024 / 1024).toFixed(1)} MB`
}

function formatArtifactDate(value?: string) {
  if (!value) return '未知时间'
  const date = new Date(value)
  return Number.isNaN(date.getTime()) ? value : date.toLocaleString('zh-CN')
}

const diagnosticCurrentStep = computed(() => {
  const steps = diagnosticProgressSteps.value
  return steps[Math.min(diagnosticActiveStep.value, steps.length - 1)]
})
const diagnosticProgressPercent = computed(() => {
  if (diagnosticProgressState.value === 'done') return 100
  if (diagnosticProgressState.value === 'idle') return 0
  if (diagnosticJob.value?.total_units) {
    const stepBase = diagnosticActiveStep.value / diagnosticProgressSteps.value.length
    const stepSpan = 1 / diagnosticProgressSteps.value.length
    const unitProgress = diagnosticJob.value.processed_units / Math.max(1, diagnosticJob.value.total_units)
    return Math.min(98, Math.round((stepBase + stepSpan * unitProgress) * 100))
  }
  const base = diagnosticActiveStep.value / diagnosticProgressSteps.value.length
  return Math.min(96, Math.round(base * 100))
})
const diagnosticProgressText = computed(() => {
  if (!diagnosticJob.value?.total_units) return ''
  return `已处理 ${diagnosticJob.value.processed_units}/${diagnosticJob.value.total_units}，剩余 ${diagnosticJob.value.remaining_units}`
})
const diagnosticCurrentUnit = computed(() => diagnosticJob.value?.current_unit ?? null)
const diagnosticCurrentSubgraphComponent = computed(() => diagnosticJob.value?.current_subgraph_component ?? null)
const diagnosticSubgraphProgressText = computed(() => {
  const component = diagnosticCurrentSubgraphComponent.value
  if (!component) return ''
  const evidence = [
    `面 ${component.face_count}`,
    `特征 ${component.feature_count}`,
    `接触 ${component.contact_count}`,
    `配合 ${component.mate_count}`,
    `图片 ${component.image_count}`,
  ].join(' / ')
  const adjacent = component.adjacent_component_ids?.length ? `，相邻 ${component.adjacent_component_ids.length} 个` : ''
  return `${component.current_index}/${component.total_components} · ${evidence}${adjacent}`
})

function extractIssueJudgement(issue: MechanicalRetrievedIssue, report: string) {
  const text = report.trim()
  if (!text) return '最终判定尚未生成。'

  const normalized = normalizeJudgementBlocks(text)
  const blocks = normalized
    .split(/\n(?=-\s*判定[：:])/)
    .map(block => block.trim())
    .filter(Boolean)
  const matchingBlock = blocks.find(block => block.includes(issue.rule_id) && isSameIssueBlock(block, issue.rule_id))
  if (matchingBlock) return cleanJudgementText(matchingBlock)

  const titleKey = issue.rule_title.slice(0, 10)
  const titleBlock = titleKey ? blocks.find(block => block.includes(titleKey) && isSameIssueBlock(block, issue.rule_id)) : null
  if (titleBlock) return cleanJudgementText(titleBlock)

  return '最终报告未单独给出该条禁忌的判定说明，需要查看完整诊断报告或重新生成更结构化的判定。'
}

function dedupeDiagnosticIssueCards<T extends MechanicalRetrievedIssue & { judgement: string }>(issues: T[]) {
  if (!diagnosticResult.value) return issues
  const seen = new Set<string>()
  return issues.filter(issue => {
    const key = `${diagnosticIssueIdentity(issue)}:${issue.judgement.replace(/\s+/g, '')}`
    if (seen.has(key)) return false
    seen.add(key)
    return true
  })
}

function diagnosticIssueIdentity(issue: MechanicalRetrievedIssue) {
  const unitKey = issue.unit_key || issue.local_id || issue.retrieval_sentence
  return `${unitKey}:${issue.rule_id}`
}

function selectIssueEvidenceImage(issue: MechanicalRetrievedIssue) {
  const first = issue.evidence_images[0]
  if (!first) {
    return {
      name: '',
      url: '',
    }
  }

  const anchorTokens = buildEvidenceSelectionTokens([
    issue.anchor_id,
    issue.anchor_type,
    issue.pattern_type,
    issue.unit_key,
    issue.local_id,
    issue.primary_view,
    ...(issue.image_evidence_names ?? []),
  ])
  const scored = issue.evidence_images.map(image => {
    const imageText = normalizeEvidenceText(`${image.name} ${image.url} ${image.image_kind ?? ''} ${image.evidence_family ?? ''} ${image.evidence_purpose ?? ''}`)
    let score = 0
    for (const token of anchorTokens) {
      if (!token) continue
      if (imageText.includes(`/${token}/`)) score += 240
      else if (imageText.includes(token)) score += 120
    }
    if (issue.anchor_id && imageText.includes(normalizeEvidenceText(issue.anchor_id))) score += 200
    if (issue.image_evidence_names?.some(name => imageText.includes(normalizeEvidenceText(name)))) score += 300
    return { image, score }
  })

  return scored.sort((a, b) => b.score - a.score)[0]?.image ?? first
}

function selectIssueEvidenceImages(issue: DiagnosticIssueCard) {
  if (issue.final_issue) return issue.evidence_images
  const selected = selectIssueEvidenceImage(issue)
  return selected.url ? [selected] : []
}

function formatEvidenceCaption(image: MechanicalEvidenceImage) {
  const source = image.source_anchor_id || image.source_local_id || image.source_unit_key
  return source ? `${source}：${image.name}` : image.name
}

function formatFinalIssueJudgement(issue: MechanicalFinalIssue) {
  return [
    `- 判定：${issue.judgement}`,
    `- 对应禁忌：${issue.rule_id} ${issue.rule_title}`.trim(),
    `- 问题部位：${issue.location}`,
    `- 为什么错：${issue.reason}`,
    `- 建议：${issue.suggestion}`,
  ].join('\n')
}

function buildEvidenceSelectionTokens(values: Array<string | undefined>) {
  const tokens = new Set<string>()
  for (const value of values) {
    const normalized = normalizeEvidenceText(value ?? '')
    if (!normalized) continue
    tokens.add(normalized)
    for (const segment of normalized.split(/[/:\\\s]+/)) {
      if (segment.length >= 3) tokens.add(segment)
    }
    const withoutExt = normalized.replace(/\.[a-z0-9]+$/i, '')
    if (withoutExt.length >= 3) tokens.add(withoutExt)
  }
  return Array.from(tokens)
}

function normalizeEvidenceText(value: string) {
  return value
    .replace(/模型截图：|功能组拆分后零件全脸高亮图：|功能组接触配合证据图：|组件上下文截图：|装配体高亮截图：|零件图截图：|等轴测截图：/g, '')
    .replace(/\\/g, '/')
    .trim()
    .toLowerCase()
}

function normalizeJudgementBlocks(value: string) {
  return value
    .replace(/\r\n/g, '\n')
    .replace(/\s+-\s*判定[：:]/g, '\n- 判定：')
    .replace(/\s+-\s*对应禁忌[：:]/g, '\n- 对应禁忌：')
    .replace(/\s+-\s*问题部位[：:]/g, '\n- 问题部位：')
    .replace(/\s+-\s*为什么错[：:]/g, '\n- 为什么错：')
    .replace(/\s+-\s*建议[：:]/g, '\n- 建议：')
}

function isSameIssueBlock(block: string, ruleId: string) {
  const tabooLine = block.match(/对应禁忌[：:]\s*([^\n]+)/)
  if (tabooLine) return tabooLine[1].includes(ruleId)
  const ruleMatches = Array.from(block.matchAll(/18\.\d+(?:\.\d+)*/g)).map(match => match[0])
  return ruleMatches.length > 0 && ruleMatches[0] === ruleId
}

function isDisplayedFinalIssue(judgement: string) {
  const text = judgement.trim().toLowerCase()
  if (!text) return false
  if (text.includes('最终报告未单独给出')) return false
  if (text.includes('problem') ||
    text.includes('needs_review') ||
    text.includes('问题部位') ||
    text.includes('为什么错') ||
    text.includes('需复核')) return true
  if (text.includes('未发现明确违反')) return false
  if (text.includes('pass') || text.includes('not_applicable') || text.includes('不适用')) return false
  return text.includes('违反')
}

function cleanJudgementText(value: string) {
  return value
    .replace(/```[\s\S]*?```/g, '')
    .replace(/^#{1,4}\s*/gm, '')
    .replace(/本次附加截图证据[：:][\s\S]*$/g, '')
    .replace(/\n{3,}/g, '\n\n')
    .trim()
    .slice(0, 520)
}

function diagnosticStepIndexForPhase(phase: string) {
  if (diagnosticTargetKind.value === 'part') {
    switch (phase) {
      case 'extract_brep':
        return 0
      case 'classify_components':
      case 'export_images':
        return 1
      case 'semantic_bridge':
      case 'validate_semantic_graph':
        return 2
      case 'retrieval':
        return 3
      case 'ai_final_judgement':
        return 5
      case 'completed':
        return diagnosticProgressSteps.value.length - 1
      case 'failed':
        return diagnosticActiveStep.value
      default:
        return 0
    }
  }

  switch (phase) {
    case 'extract_brep':
      return 0
    case 'classify_components':
      return 1
    case 'export_images':
      return 1
    case 'semantic_bridge':
      return 4
    case 'validate_semantic_graph':
      return 4
    case 'retrieval':
      return 5
    case 'ai_final_judgement':
      return 6
    case 'completed':
      return diagnosticProgressSteps.value.length - 1
    case 'failed':
      return diagnosticActiveStep.value
    default:
      return 0
  }
}

async function fetchJson<T>(path: string, init?: RequestInit): Promise<T> {
  const response = await fetch(`${apiBase}${path}`, init)
  if (!response.ok) throw new Error(await response.text())
  return (await response.json()) as T
}

async function loadInitialData() {
  try {
    const health = await fetchJson<{ ok: boolean; backend: string; standards: number }>('/health')
    status.value = health.ok ? `后端已连接 / ${health.backend}` : '连接异常'
    const data = await fetchJson<{ items: Standard[] }>('/api/standards')
    standards.value = data.items
  } catch (error) {
    status.value = error instanceof Error ? error.message : '连接失败'
  }
}

function chooseMode(nextMode: AppMode) {
  resetCourse()
  mode.value = nextMode
  if (nextMode === 'diagnostic') {
    void loadDiagnosticUsage()
  }
}

function chooseDiagnosticTarget(kind: DiagnosticTargetKind) {
  resetDiagnosticRunState()
  diagnosticTargetKind.value = kind
  if (kind === 'part') {
    diagnosticProductDescription.value = ''
    diagnosticPurchasedComponentHints.value = ''
  }
  if (kind === 'drawing-part' || kind === 'drawing-assembly') {
    diagnosticModelPath.value = ''
    diagnosticProductDescription.value = ''
    diagnosticPartFunctionProfile.value = ''
    diagnosticPurchasedComponentHints.value = ''
  }
  if (kind === 'drawing-assembly') {
    void refreshDwgBridgeStatus()
  }
}

function chooseProject() {
  projectSelected.value = true
}

function chooseScope(nextScope: StudyScope) {
  stopIntroductionPlayback()
  stopAutoListening()
  scope.value = nextScope
  selected.value = nextScope === 'overall' ? overallStandard.value : null
  session.value = null
  teachingScript.value = null
  introduction.value = null
  feedback.value = ''
}

async function choosePart(part: Standard) {
  stopIntroductionPlayback()
  stopAutoListening()
  selected.value = part
  session.value = null
  teachingScript.value = null
  introduction.value = null
  feedback.value = ''
  await loadIntroduction(part.standard_id)
  if (part.standard_id === designTargetStandardId) {
    await loadTeachingScript(part.standard_id)
  }
}

function back() {
  stopIntroductionPlayback()
  if (mode.value === 'diagnostic') {
    if (diagnosticTargetKind.value) {
      resetDiagnosticRunState()
      diagnosticTargetKind.value = null
      return
    }
    mode.value = null
    return
  }
  if (session.value) {
    stopAutoListening()
    session.value = null
    feedback.value = ''
    return
  }
  if (selected.value && scope.value === 'part') {
    selected.value = null
    teachingScript.value = null
    introduction.value = null
    return
  }
  if (scope.value) {
    scope.value = null
    selected.value = null
    teachingScript.value = null
    introduction.value = null
    return
  }
  if (projectSelected.value) {
    projectSelected.value = false
    return
  }
  mode.value = null
}

function resetCourse() {
  stopIntroductionPlayback()
  stopAutoListening()
  projectSelected.value = false
  scope.value = null
  selected.value = null
  session.value = null
  teachingScript.value = null
  introduction.value = null
  feedback.value = ''
  diagnosticTargetKind.value = null
  diagnosticProductDescription.value = ''
  diagnosticPartFunctionProfile.value = ''
  diagnosticPurchasedComponentHints.value = ''
  drawingPath.value = ''
  drawingOutputDir.value = ''
  drawingProgId.value = ''
  drawingVisible.value = true
  drawingUseBridge.value = true
  drawingCloseAfterExtract.value = false
  drawingMaxEntities.value = 100000
  drawingStudentViewImage.value = ''
  drawingOwnershipImage.value = ''
  drawingDwgExtractionDir.value = ''
  drawingMechanicalContextDir.value = ''
  drawingOwnershipContextDir.value = ''
  drawingAnalysisOutputDir.value = ''
  resetDiagnosticRunState()
}

function resetDiagnosticRunState() {
  diagnosticResult.value = null
  diagnosticJob.value = null
  diagnosticResultTab.value = 'report'
  diagnosticMessage.value = ''
  diagnosticProgressState.value = 'idle'
  diagnosticActiveStep.value = 0
  diagnosticElapsedSeconds.value = 0
  diagnosticBusy.value = false
  drawingExtractionResult.value = null
  drawingAssemblyAnalysisResult.value = null
  drawingInterfaceContextResult.value = null
  stopDiagnosticStatusTimer()
  stopDiagnosticProgressTimer()
}

async function loadDiagnosticUsage() {
  try {
    diagnosticUsage.value = await fetchJson<MechanicalDiagnosticUsage>('/api/mechanical-diagnostics/usage')
  } catch (error) {
    diagnosticMessage.value = error instanceof Error ? error.message : '加载机械查错说明失败'
  }
}

async function runMechanicalDiagnosis() {
  if (!canRunMechanicalDiagnosis.value) {
    diagnosticMessage.value = diagnosticStartDisabledReason.value
    return
  }

  diagnosticBusy.value = true
  diagnosticMessage.value = `正在启动${diagnosticTitle.value}任务...`
  diagnosticResult.value = null
  diagnosticJob.value = null
  diagnosticResultTab.value = 'report'
  startDiagnosticProgress()
  const isAssembly = diagnosticTargetKind.value === 'assembly'
  try {
    const job = await fetchJson<MechanicalDiagnosisJobStatus>('/api/mechanical-diagnostics/diagnose/start', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        model_path: diagnosticModelPath.value,
        output_dir: diagnosticOutputDir.value || null,
        product_description: isAssembly ? diagnosticProductDescription.value || null : null,
        part_function_profile: !isAssembly ? diagnosticPartFunctionProfile.value || null : null,
        purchased_component_hints: isAssembly
          ? diagnosticPurchasedComponentHints.value
            .split(/\r?\n|[;；]/)
            .map(item => item.trim())
            .filter(Boolean)
          : [],
        deep: false,
        max_iterations: 1,
        max_rules_per_unit: 4,
      }),
    })
    diagnosticJob.value = job
    diagnosticMessage.value = job.message || '任务已启动，正在等待后端进度...'
    diagnosticStatusTimer = window.setInterval(() => {
      void pollDiagnosticStatus(job.job_id)
    }, 1500)
    await pollDiagnosticStatus(job.job_id)
  } catch (error) {
    diagnosticMessage.value = error instanceof Error ? error.message : `${diagnosticTitle.value}失败`
    finishDiagnosticProgress(false)
    diagnosticBusy.value = false
  }
}

async function refreshDwgBridgeStatus() {
  try {
    drawingBridgeStatus.value = await fetchJson<DwgDraftBridgeStatus>('/api/dwg-draft/bridge')
  } catch (error) {
    diagnosticMessage.value = error instanceof Error ? error.message : '读取 AutoCAD 桥接状态失败'
  }
}

async function loadLatestDrawingExtraction() {
  diagnosticBusy.value = true
  diagnosticMessage.value = '正在读取最近一次 DWG 抽取结果...'
  try {
    const latest = await fetchJson<DwgDraftLatestExtractionResult>('/api/dwg-draft/latest-extraction')
    if (!latest.found) {
      drawingExtractionResult.value = null
      diagnosticMessage.value = latest.message || '还没有可用的 DWG 抽取结果。'
      return
    }
    drawingExtractionResult.value = {
      ok: latest.ok,
      source: 'latest extraction',
      prog_id: '',
      started: false,
      drawing_path: '',
      export_dir: latest.export_dir,
      manifest_path: latest.manifest_path,
      entity_count: latest.category_files.find(file => file.category === 'entities-common')?.item_count ?? 0,
      type_counts: {},
      category_files: latest.category_files,
      message: latest.message,
    }
    diagnosticMessage.value = '已读取最近一次 DWG 抽取结果。'
  } catch (error) {
    diagnosticMessage.value = error instanceof Error ? error.message : '读取最近一次 DWG 抽取结果失败'
  } finally {
    diagnosticBusy.value = false
  }
}

async function runDrawingExtraction() {
  if (!canRunDrawingExtraction.value) {
    diagnosticMessage.value = drawingExtractionDisabledReason.value
    return
  }

  diagnosticBusy.value = true
  diagnosticMessage.value = '正在通过 AutoCAD 读取 DWG 图纸...'
  drawingExtractionResult.value = null
  try {
    const result = await fetchJson<DwgDraftExtractionResult>('/api/dwg-draft/extract', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        drawing_path: drawingPath.value,
        output_dir: drawingOutputDir.value || '',
        prog_id: drawingProgId.value || '',
        visible: drawingVisible.value,
        close_after_extract: drawingCloseAfterExtract.value,
        max_entities: drawingMaxEntities.value || 100000,
        use_bridge: drawingUseBridge.value,
        bridge_timeout_seconds: 600,
      }),
    })
    drawingExtractionResult.value = result
    diagnosticMessage.value = result.message || 'DWG 图纸抽取完成。'
    await refreshDwgBridgeStatus()
  } catch (error) {
    diagnosticMessage.value = error instanceof Error ? error.message : 'DWG 图纸抽取失败'
  } finally {
    diagnosticBusy.value = false
  }
}

async function runDrawingPartAnalysis() {
  if (!canRunDrawingPartAnalysis.value) {
    diagnosticMessage.value = drawingPartAnalysisDisabledReason.value
    return
  }

  diagnosticBusy.value = true
  diagnosticMessage.value = '正在执行二维零件图格式查错：抽取 DWG 实体并按程序化规则检查线型/线宽...'
  drawingAssemblyAnalysisResult.value = null
  try {
    const result = await fetchJson<DrawingAssemblyAnalysisResult>('/api/drawing-part-diagnostics/run-full', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        student_dwg_path: drawingPath.value,
        output_dir: drawingAnalysisOutputDir.value || '',
        prog_id: drawingProgId.value || '',
        visible: drawingVisible.value,
        max_entities: drawingMaxEntities.value || 100000,
      }),
    })
    drawingAssemblyAnalysisResult.value = result
    diagnosticMessage.value = result.ok ? '二维零件图格式查错完成。' : result.message
  } catch (error) {
    diagnosticMessage.value = error instanceof Error ? error.message : '二维零件图格式查错失败'
  } finally {
    diagnosticBusy.value = false
  }
}

async function runDrawingAssemblyAnalysis() {
  if (!canRunDrawingAssemblyAnalysis.value) {
    diagnosticMessage.value = drawingAssemblyAnalysisDisabledReason.value
    return
  }

  diagnosticBusy.value = true
  diagnosticMessage.value = '正在执行二维装配图完整查错流程：识别主视图、生成工程图、导出两张 PNG，再进入 AI 三层分析...'
  drawingAssemblyAnalysisResult.value = null
  try {
    const result = await fetchJson<DrawingAssemblyAnalysisResult>('/api/drawing-assembly-diagnostics/run-full', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        student_dwg_path: drawingPath.value,
        model_path: diagnosticModelPath.value,
        mechanical_context_dir: drawingMechanicalContextDir.value || '',
        output_dir: drawingAnalysisOutputDir.value || '',
        visible: drawingVisible.value,
        max_entities: drawingMaxEntities.value || 100000,
      }),
    })
    drawingAssemblyAnalysisResult.value = result
    diagnosticMessage.value = result.ok ? '二维装配图完整查错流程完成。' : result.message
  } catch (error) {
    diagnosticMessage.value = error instanceof Error ? error.message : '二维装配图完整查错流程失败'
  } finally {
    diagnosticBusy.value = false
  }
}

async function buildDrawingInterfaceContext() {
  if (!canBuildDrawingInterfaceContext.value) {
    diagnosticMessage.value = drawingInterfaceContextDisabledReason.value
    return
  }

  diagnosticBusy.value = true
  diagnosticMessage.value = '正在构建二维公差到三维 B-rep 接触功能的证据上下文...'
  drawingInterfaceContextResult.value = null
  try {
    const result = await fetchJson<DrawingInterfaceContextResult>('/api/drawing-assembly-diagnostics/build-interface-drawing-context', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        dwg_extraction_dir: drawingDwgExtractionDir.value || drawingExtractionResult.value?.export_dir || '',
        mechanical_context_dir: drawingMechanicalContextDir.value || '',
        ownership_context_dir: drawingOwnershipContextDir.value || '',
      }),
    })
    drawingInterfaceContextResult.value = result
    diagnosticMessage.value = `证据上下文构建完成：${drawingInterfaceSummary.value.tolerances} 条公差，${drawingInterfaceSummary.value.brepBound} 条已有 B-rep 强证据。`
  } catch (error) {
    diagnosticMessage.value = error instanceof Error ? error.message : '构建二维-三维公差证据上下文失败'
  } finally {
    diagnosticBusy.value = false
  }
}

function drawingCategoryLabel(category: string) {
  const labels: Record<string, string> = {
    'entities-common': '全部实体',
    geometry: '几何线框',
    annotations: '文字标注',
    dimensions: '尺寸标注',
    'dimensional-tolerances': '尺寸公差',
    roughness: '粗糙度',
    'geometric-tolerances': '形位公差',
    datums: '基准',
    'unknown-entities': '未分类实体',
  }
  return labels[category] ?? category
}

async function rerunExistingDiagnosticJudgement() {
  diagnosticBusy.value = true
  diagnosticMessage.value = '正在复用已有 B-rep、图片和局部子图集合重新判定...'
  diagnosticResult.value = null
  diagnosticJob.value = null
  startDiagnosticProgress()
  diagnosticActiveStep.value = Math.max(0, diagnosticProgressSteps.value.length - 2)
  try {
    const result = await fetchJson<MechanicalAiDiagnosisResult>('/api/mechanical-diagnostics/diagnose/finalize-existing', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        output_dir: diagnosticOutputDir.value || null,
        max_rules_per_unit: 20,
      }),
    })
    diagnosticResult.value = result
    diagnosticResultTab.value = 'report'
    diagnosticMessage.value = result.final_status === 'ok' ? '已使用已有证据重新完成最终判定。' : result.final_message
    finishDiagnosticProgress(result.final_status === 'ok')
  } catch (error) {
    diagnosticMessage.value = error instanceof Error ? error.message : '复用已有证据重新判定失败'
    finishDiagnosticProgress(false)
  } finally {
    diagnosticBusy.value = false
  }
}

async function loadExistingDiagnosticResult() {
  diagnosticBusy.value = true
  diagnosticMessage.value = '正在读取已有完整诊断结果...'
  diagnosticResult.value = null
  diagnosticJob.value = null
  diagnosticResultTab.value = 'report'
  try {
    const result = await fetchJson<MechanicalAiDiagnosisResult>('/api/mechanical-diagnostics/diagnose/load-existing', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        output_dir: diagnosticOutputDir.value || null,
        model_path: diagnosticModelPath.value || '',
      }),
    })
    diagnosticResult.value = result
    diagnosticOutputDir.value = result.output_dir
    if (!diagnosticModelPath.value && result.model_path) diagnosticModelPath.value = result.model_path
    diagnosticMessage.value = result.final_message
    diagnosticProgressState.value = result.ok ? 'done' : 'failed'
    diagnosticActiveStep.value = diagnosticProgressSteps.value.length - 1
  } catch (error) {
    diagnosticMessage.value = error instanceof Error ? error.message : '读取已有完整诊断结果失败'
  } finally {
    diagnosticBusy.value = false
  }
}

async function pollDiagnosticStatus(jobId: string) {
  try {
    const job = await fetchJson<MechanicalDiagnosisJobStatus>(`/api/mechanical-diagnostics/diagnose/status/${jobId}`)
    diagnosticJob.value = job
    diagnosticMessage.value = job.message || diagnosticMessage.value
    diagnosticActiveStep.value = Math.max(diagnosticActiveStep.value, diagnosticStepIndexForPhase(job.phase))

    if (job.status === 'done') {
      diagnosticResult.value = job.result ?? null
      diagnosticResultTab.value = 'report'
      diagnosticMessage.value = job.result?.final_status === 'ok' ? `${diagnosticTitle.value}完成。` : job.message
      finishDiagnosticProgress(true)
      diagnosticBusy.value = false
      stopDiagnosticStatusTimer()
    } else if (job.status === 'failed') {
      diagnosticResult.value = job.result ?? null
      diagnosticMessage.value = job.error || job.message || `${diagnosticTitle.value}失败`
      finishDiagnosticProgress(false)
      diagnosticBusy.value = false
      stopDiagnosticStatusTimer()
    }
  } catch (error) {
    diagnosticMessage.value = error instanceof Error ? error.message : '读取查错进度失败'
  }
}

function startDiagnosticProgress() {
  stopDiagnosticProgressTimer()
  diagnosticProgressState.value = 'running'
  diagnosticActiveStep.value = 0
  diagnosticElapsedSeconds.value = 0
  const startedAt = Date.now()
  diagnosticProgressTimer = window.setInterval(() => {
    diagnosticElapsedSeconds.value = Math.floor((Date.now() - startedAt) / 1000)
  }, 1000)
}

function finishDiagnosticProgress(ok: boolean) {
  stopDiagnosticProgressTimer()
  diagnosticProgressState.value = ok ? 'done' : 'failed'
  if (ok) diagnosticActiveStep.value = diagnosticProgressSteps.value.length - 1
}

function stopDiagnosticProgressTimer() {
  if (diagnosticProgressTimer !== undefined) {
    window.clearInterval(diagnosticProgressTimer)
    diagnosticProgressTimer = undefined
  }
}

function stopDiagnosticStatusTimer() {
  if (diagnosticStatusTimer !== undefined) {
    window.clearInterval(diagnosticStatusTimer)
    diagnosticStatusTimer = undefined
  }
}

async function loadIntroduction(standardId: string) {
  loadingIntroduction.value = true
  try {
    introduction.value = await fetchJson<TeachingIntroduction>(`/api/teaching/introduction/${standardId}`)
  } catch (error) {
    feedback.value = error instanceof Error ? error.message : '加载课前导入失败'
  } finally {
    loadingIntroduction.value = false
  }
}

async function loadTeachingScript(standardId: string) {
  loadingTeachingScript.value = true
  try {
    teachingScript.value = await fetchJson<TeachingScript>(`/api/teaching/design/${standardId}`)
  } catch (error) {
    feedback.value = error instanceof Error ? error.message : '教学语言设计失败'
  } finally {
    loadingTeachingScript.value = false
  }
}

async function startSession() {
  if (!selected.value || !mode.value) return
  stopIntroductionPlayback()
  startingSession.value = true
  feedback.value = '正在连接或启动 SolidWorks...'
  try {
    const path = mode.value === 'teaching' ? '/api/teaching/sessions' : '/api/testing/sessions'
    session.value = await fetchJson<Session>(path, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ standard_id: selected.value.standard_id }),
    })
    feedback.value = 'SolidWorks 已连接，课程已开始。'
    if (mode.value === 'teaching') {
      setTimeout(() => speakText(currentStep.value?.voice_text ?? ''), 200)
      startAutoListening()
    }
  } catch (error) {
    feedback.value = error instanceof Error ? error.message : '启动课程失败'
  } finally {
    startingSession.value = false
  }
}

async function openStandardPartForIntroduction() {
  if (!selected.value || !introduction.value) return
  openingStandardPart.value = true
  feedback.value = '正在打开标准零件...'
  try {
    const result = await fetchJson<{ ok: boolean; message: string }>(
      `/api/teaching/introduction/${selected.value.standard_id}/open-standard-part`,
      { method: 'POST' },
    )
    feedback.value = result.message || '标准零件已打开。'
    await playIntroductionSequence()
  } catch (error) {
    feedback.value = error instanceof Error ? error.message : '打开标准零件失败'
  } finally {
    openingStandardPart.value = false
  }
}

async function highlightIntroductionTarget(target: HighlightTarget) {
  stopIntroductionPlayback()
  await highlightTargetForIntroduction(target, true, false)
}

async function highlightTargetForIntroduction(target: HighlightTarget, speakResult: boolean, keepActive: boolean) {
  if (!selected.value || !introduction.value) return
  highlightingTargetId.value = target.id
  feedback.value = `正在高亮：${target.label}...`
  try {
    const result = await fetchJson<HighlightResult>(
      `/api/teaching/introduction/${selected.value.standard_id}/highlight/${target.id}`,
      { method: 'POST' },
    )
    feedback.value = result.message
    if (speakResult) speakText(`${target.description}${result.ok ? result.message : `暂时没有稳定高亮。${result.message}`}`)
  } catch (error) {
    feedback.value = error instanceof Error ? error.message : '高亮功能面失败'
    if (speakResult) speakText(feedback.value)
  } finally {
    if (!keepActive) highlightingTargetId.value = ''
  }
}

async function playIntroductionSequence() {
  if (!introduction.value) return
  stopIntroductionPlayback()
  const runId = ++introPlaybackRun
  introPlaying.value = true

  try {
    const intro = introduction.value
    await speakTextAsync(`先认识标准零件：${intro.title}。下面我会按关键功能面依次讲解，讲到哪里，SolidWorks 里就高亮哪里。`)

    const targets = intro.highlight_targets.slice(0, 12)
    for (const target of targets) {
      if (runId !== introPlaybackRun) return
      const highlightTask = highlightTargetForIntroduction(target, false, true)
      if (runId !== introPlaybackRun) return
      await Promise.all([
        highlightTask,
        speakTextAsync(`现在看${target.label}。${target.description}`),
      ])
      if (runId === introPlaybackRun) highlightingTargetId.value = ''
    }

    if (runId === introPlaybackRun) {
      feedback.value = '零件课前讲解完成，可以开始正式建模教学。'
    }
  } finally {
    if (runId === introPlaybackRun) {
      introPlaying.value = false
      highlightingTargetId.value = ''
    }
  }
}

function stopIntroductionPlayback() {
  introPlaybackRun++
  introPlaying.value = false
  highlightingTargetId.value = ''
  window.speechSynthesis.cancel()
}

async function refreshSession() {
  if (!session.value) return
  session.value = await fetchJson<Session>(`/api/sessions/${session.value.session_id}`)
}

function startAutoListening() {
  stopAutoListening()
  if (!session.value || mode.value !== 'teaching') return
  autoListening.value = true
  lastSpokenFeedbackKey = ''
  pollingMessage.value = '实时监听已开启。'
  pollTimer = window.setInterval(() => {
    void pollActivePart()
  }, 3500)
  void pollActivePart()
}

function stopAutoListening() {
  autoListening.value = false
  pollingBusy.value = false
  pollingMessage.value = ''
  lastSpokenFeedbackKey = ''
  if (pollTimer !== undefined) {
    window.clearInterval(pollTimer)
    pollTimer = undefined
  }
}

async function pollActivePart() {
  if (!session.value || mode.value !== 'teaching' || pollingBusy.value) return
  if (session.value.status === 'completed') {
    stopAutoListening()
    return
  }

  const previousIndex = session.value.current_step_index
  pollingBusy.value = true
  pollingMessage.value = '正在读取 SolidWorks 当前特征树...'
  try {
    const result = await fetchJson<TeachingPollResponse>(
      `/api/teaching/sessions/${session.value.session_id}/poll-active-part`,
      { method: 'POST' },
    )
    feedback.value = result.result.feedback_text
    await refreshSession()

    if (result.result.ok && session.value?.current_step_index !== previousIndex && currentStep.value) {
      lastSpokenFeedbackKey = ''
      speakText(currentStep.value.voice_text)
    } else if (!result.result.ok) {
      speakFeedbackOnce(result.result.feedback_text, previousIndex)
    }

    if (session.value?.status === 'completed') {
      feedback.value = '教学步骤已经完成。'
      stopAutoListening()
    } else {
      pollingMessage.value = autoListening.value ? `实时监听中，已读取 ${result.observed_step_count} 个特征步骤。` : ''
    }
  } catch (error) {
    feedback.value = error instanceof Error ? error.message : '实时读取 SolidWorks 失败'
    speakFeedbackOnce(feedback.value, session.value.current_step_index)
    pollingMessage.value = '监听遇到错误，系统会继续重试。'
  } finally {
    pollingBusy.value = false
  }
}

async function submitObservation() {
  if (!session.value) return
  let args: Record<string, unknown> = {}
  try {
    args = JSON.parse(observedArgs.value || '{}')
  } catch {
    feedback.value = '参数 JSON 格式错误'
    return
  }

  const result = await fetchJson<{ ok: boolean; feedback_text: string }>(
    `/api/teaching/sessions/${session.value.session_id}/observations`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        steps: [{ skill: observedSkill.value, args, name: observedSkill.value }],
      }),
    },
  )
  feedback.value = result.feedback_text
  await refreshSession()
}

function speak() {
  speakText(currentStep.value?.voice_text ?? '')
}

function speakIntroduction() {
  void playIntroductionSequence()
}

function speakText(text: string) {
  void speakTextAsync(text)
}

function speakTextAsync(text: string): Promise<void> {
  if (!text) return Promise.resolve()
  return new Promise(resolve => {
    const utterance = new SpeechSynthesisUtterance(text)
    utterance.lang = 'zh-CN'
    utterance.onend = () => resolve()
    utterance.onerror = () => resolve()
    window.speechSynthesis.cancel()
    window.speechSynthesis.speak(utterance)
  })
}

function speakFeedbackOnce(text: string, stepIndex: number) {
  if (!text) return
  const key = `${session.value?.session_id ?? ''}:${stepIndex}:${text}`
  if (key === lastSpokenFeedbackKey) return
  lastSpokenFeedbackKey = key
  speakText(text)
}

async function initializeApp() {
  await loadInitialData()
  const requestedMode = new URLSearchParams(window.location.search).get('mode')
  if (requestedMode === 'simulation' || requestedMode === 'knowledge') {
    mode.value = requestedMode
  }
}

onMounted(initializeApp)
onBeforeUnmount(() => {
  stopIntroductionPlayback()
  stopAutoListening()
  stopDiagnosticStatusTimer()
  stopDiagnosticProgressTimer()
})
</script>

<template>
  <main class="app">
    <header class="topbar">
      <div>
        <p>Agent4</p>
        <h1>二级齿轮减速器建模训练</h1>
      </div>
      <div class="top-actions">
        <span>{{ status }}</span>
        <button v-if="mode" class="ghost" @click="resetCourse">返回首页</button>
      </div>
    </header>

    <section v-if="!mode" class="stage">
      <div class="stage-head">
        <h2>选择训练模式</h2>
      </div>
      <div class="choice-grid mode-grid">
        <button class="choice" @click="chooseMode('teaching')">
          <strong>学习</strong>
          <span>按步骤引导完成建模操作</span>
        </button>
        <button class="choice" @click="chooseMode('testing')">
          <strong>测试</strong>
          <span>按任务要求建模并提交评分</span>
        </button>
        <button class="choice" @click="chooseMode('diagnostic')">
          <strong>机械设计查错</strong>
          <span>针对第 18 章机械加工件结构设计进行 AI 查错</span>
        </button>
        <button class="choice" @click="chooseMode('simulation')">
          <strong>ANSYS 仿真</strong>
          <span>分析模型、确认工况并跟踪静力结构求解</span>
        </button>
        <button class="choice" @click="chooseMode('knowledge')">
          <strong>知识库管理</strong>
          <span>解析工程文档、构建本地索引并验证知识检索</span>
        </button>
      </div>
    </section>

    <KnowledgeWorkspace v-else-if="mode === 'knowledge'" />

    <SimulationWorkspace v-else-if="mode === 'simulation'" />

    <section v-else-if="mode === 'diagnostic' && !diagnosticTargetKind" class="stage">
      <div class="stage-head">
        <button class="ghost" @click="back">返回</button>
        <h2>选择查错对象</h2>
      </div>
      <div class="choice-grid four">
        <button class="choice diagnostic-choice" @click="chooseDiagnosticTarget('part')">
          <strong>三维零件图查错</strong>
          <span>只分析单个零件的结构设计问题，不需要产品描述和外购件信息。</span>
        </button>
        <button class="choice diagnostic-choice" @click="chooseDiagnosticTarget('assembly')">
          <strong>三维装配体查错</strong>
          <span>先理解整机功能、零件角色和配合关系，再对非标零件逐个查错。</span>
        </button>
        <button class="choice diagnostic-choice" @click="chooseDiagnosticTarget('drawing-part')">
          <strong>二维零件图查错</strong>
          <span>已接入格式查错：按 DWG 图元规则检查线型和线宽，标注/绘制错误保留给 AI。</span>
        </button>
        <button class="choice diagnostic-choice" @click="chooseDiagnosticTarget('drawing-assembly')">
          <strong>二维装配图查错</strong>
          <span>当前已接入：归属信息、工程图导出 DWG、图纸抽取、上色和装配图分析。</span>
        </button>
      </div>
    </section>

    <section v-else-if="mode === 'diagnostic' && diagnosticTargetKind" class="workspace diagnostic-workspace">
      <aside class="course-panel">
        <button class="ghost" @click="back">返回</button>
        <h2>{{ diagnosticTitle }}</h2>
        <p>{{ diagnosticDescriptionText }}</p>

        <template v-if="isDrawingPartDiagnostic">
          <label class="field-label">学生零件 DWG 路径</label>
          <input v-model="drawingPath" placeholder="D:\path\part-drawing.dwg" />

          <label class="field-label">查错输出目录（可选）</label>
          <input v-model="drawingAnalysisOutputDir" placeholder="留空则写入 runtime\drawing-part-diagnostics" />

          <div class="diagnostic-note">
            当前零件图入口先接入格式查错：线型、线宽由后端直接读取 DWG 图元信息判断，不经过 AI。标注错误和绘制错误后续仍走 AI 模块。
          </div>

          <details class="manual-box">
            <summary>调试：AutoCAD 抽取参数</summary>
            <label class="field-label">抽取输出目录（可选）</label>
            <input v-model="drawingOutputDir" placeholder="留空则写入 runtime\dwg-draft\latest-extraction" />

            <label class="field-label">AutoCAD ProgID（可选）</label>
            <input v-model="drawingProgId" placeholder="留空自动选择，例如 AutoCAD.Application.23.1" />

            <label class="field-label">最大读取实体数</label>
            <input v-model.number="drawingMaxEntities" type="number" min="1" step="1000" />

            <label class="inline-check">
              <input v-model="drawingUseBridge" type="checkbox" />
              <span>优先使用常驻 AutoCAD 桥接进程</span>
            </label>
            <label class="inline-check">
              <input v-model="drawingVisible" type="checkbox" />
              <span>抽取时显示 AutoCAD</span>
            </label>
            <label class="inline-check">
              <input v-model="drawingCloseAfterExtract" type="checkbox" />
              <span>抽取完成后关闭图纸</span>
            </label>
          </details>

          <div class="diagnostic-note">
            桥接状态：{{ drawingBridgeStatus ? (drawingBridgeStatus.alive ? '在线' : '未在线') : '未读取' }}
            <br />
            {{ drawingBridgeStatus?.message || '可以直接抽取；如果桥接未在线，后端会尝试启动外部抽取器。' }}
          </div>

          <div class="action-stack">
            <button :disabled="!canRunDrawingPartAnalysis" @click="runDrawingPartAnalysis">开始二维零件图格式查错</button>
            <button class="ghost" :disabled="!canRunDrawingExtraction" @click="runDrawingExtraction">重新抽取DWG结构化信息</button>
            <button class="ghost" :disabled="diagnosticBusy" @click="loadLatestDrawingExtraction">读取最近一次结果</button>
            <button class="ghost" :disabled="diagnosticBusy" @click="refreshDwgBridgeStatus">刷新桥接状态</button>
          </div>
          <p v-if="drawingPartAnalysisDisabledReason && !diagnosticBusy" class="form-hint">{{ drawingPartAnalysisDisabledReason }}</p>
          <p v-if="drawingExtractionDisabledReason && !diagnosticBusy" class="form-hint">{{ drawingExtractionDisabledReason }}</p>
        </template>
        <template v-else-if="isDrawingAssemblyDiagnostic">
          <label class="field-label">学生原始 DWG 路径</label>
          <input v-model="drawingPath" placeholder="D:\path\assembly-drawing.dwg" />

          <label class="field-label">三维装配体模型路径</label>
          <input v-model="diagnosticModelPath" placeholder="D:\path\model.SLDASM" />

          <label class="field-label">三维接触分析目录（可选）</label>
          <input v-model="drawingMechanicalContextDir" placeholder="例如 runtime\2\external_interface_recheck_v3_fast" />
          <p class="form-hint">留空时按同一装配体模型自动选取最新的外部接口分析报告。</p>

          <label class="field-label">二维线条零件归属目录</label>
          <input v-model="drawingOwnershipContextDir" placeholder="例如 runtime\current-standard-flow\41-all-view-ownership" />

          <label class="field-label">查错输出目录（可选）</label>
          <input v-model="drawingAnalysisOutputDir" placeholder="留空则写入 runtime\drawing-assembly-diagnostics" />

          <div class="diagnostic-note">
            该入口会从学生 DWG 和当前打开的 SolidWorks 工程图开始，自动识别主视图、放置/对齐三维工程图、导出学生原始视图 PNG 和归属/上色 PNG，然后进入三层查错。DWG 不会直接传给 AI。
          </div>

          <details class="manual-box">
            <summary>调试：只抽取 DWG 结构化信息</summary>
            <label class="field-label">抽取输出目录（可选）</label>
          <input v-model="drawingOutputDir" placeholder="留空则写入 runtime\dwg-draft\latest-extraction" />

          <label class="field-label">AutoCAD ProgID（可选）</label>
          <input v-model="drawingProgId" placeholder="留空自动选择，例如 AutoCAD.Application.23.1" />

          <label class="field-label">最大读取实体数</label>
          <input v-model.number="drawingMaxEntities" type="number" min="1" step="1000" />

          <label class="inline-check">
            <input v-model="drawingUseBridge" type="checkbox" />
            <span>优先使用常驻 AutoCAD 桥接进程</span>
          </label>
          <label class="inline-check">
            <input v-model="drawingVisible" type="checkbox" />
            <span>抽取时显示 AutoCAD</span>
          </label>
          <label class="inline-check">
            <input v-model="drawingCloseAfterExtract" type="checkbox" />
            <span>抽取完成后关闭图纸</span>
          </label>
          </details>

          <div class="diagnostic-note">
            桥接状态：{{ drawingBridgeStatus ? (drawingBridgeStatus.alive ? '在线' : '未在线') : '未读取' }}
            <br />
            {{ drawingBridgeStatus?.message || '可以直接抽取；如果桥接未在线，后端会尝试启动外部抽取器。' }}
          </div>

          <div class="action-stack">
            <button :disabled="!canRunDrawingAssemblyAnalysis" @click="runDrawingAssemblyAnalysis">开始二维装配图完整查错流程</button>
            <button class="ghost" :disabled="!canBuildDrawingInterfaceContext" @click="buildDrawingInterfaceContext">构建公差-B-rep证据</button>
            <button class="ghost" :disabled="!canRunDrawingExtraction" @click="runDrawingExtraction">重新抽取DWG结构化信息</button>
            <button class="ghost" :disabled="diagnosticBusy" @click="loadLatestDrawingExtraction">读取最近一次结果</button>
            <button class="ghost" :disabled="diagnosticBusy" @click="refreshDwgBridgeStatus">刷新桥接状态</button>
          </div>
          <p v-if="drawingAssemblyAnalysisDisabledReason && !diagnosticBusy" class="form-hint">{{ drawingAssemblyAnalysisDisabledReason }}</p>
          <p v-if="drawingInterfaceContextDisabledReason && !diagnosticBusy" class="form-hint">{{ drawingInterfaceContextDisabledReason }}</p>
          <p v-if="drawingExtractionDisabledReason && !diagnosticBusy" class="form-hint">{{ drawingExtractionDisabledReason }}</p>
        </template>
        <template v-else>
          <label class="field-label">SolidWorks 模型路径</label>
          <input v-model="diagnosticModelPath" :placeholder="diagnosticModelPlaceholder" />

          <label class="field-label">输出目录（可选）</label>
          <input v-model="diagnosticOutputDir" placeholder="留空则写入 runtime" />

          <template v-if="diagnosticTargetKind === 'assembly'">
            <label class="field-label">产品描述</label>
            <textarea
              v-model="diagnosticProductDescription"
              rows="4"
              placeholder="例如：这是机器人小臂关节组件，用于连接大臂和小臂，内部包含电机、减速器、轴承和连接支撑件。"
            ></textarea>

            <label class="field-label">外购件/不分析零件完整名称</label>
            <textarea
              v-model="diagnosticPurchasedComponentHints"
              rows="4"
              placeholder="每行一个完整零件/子装配名或完整组件实例名，不要只写“电机”“减速器”等泛词。例如：&#10;小臂关节装配2&#10;GB／T276-94深沟球轴承61810-2Z"
            ></textarea>
          </template>
          <template v-else>
            <label class="field-label">零件功能画像</label>
            <textarea
              v-model="diagnosticPartFunctionProfile"
              rows="5"
              placeholder="说明该零件的用途、主要功能面、可能承担的定位/支撑/传力/密封/导向/紧固等作用。例如：该零件是减速器箱盖，用于封闭箱体、支撑轴承座并提供安装/定位面。"
            ></textarea>
            <div class="diagnostic-note">
              单零件查错没有装配体自动生成功能画像，必须由用户提供这部分功能上下文。
            </div>
          </template>

          <div class="action-stack">
            <button :disabled="!canRunMechanicalDiagnosis" @click="runMechanicalDiagnosis">开始{{ diagnosticTitle }}</button>
            <button class="ghost" :disabled="diagnosticBusy" @click="loadExistingDiagnosticResult">读取已有完整结果</button>
            <button class="ghost" :disabled="diagnosticBusy" @click="rerunExistingDiagnosticJudgement">使用已有证据重新判定</button>
          </div>
        </template>

        <p v-if="diagnosticStartDisabledReason && !diagnosticBusy" class="form-hint">{{ diagnosticStartDisabledReason }}</p>
        <p class="feedback">{{ diagnosticMessage }}</p>
      </aside>

      <section class="work-panel">
        <div class="section-head">
          <h2>{{ diagnosticTitle }}结果</h2>
          <span v-if="diagnosticBusy" class="status-pill">运行中</span>
        </div>

        <section v-if="isDrawingAssemblyDiagnostic && drawingInterfaceContextResult" class="diagnostic-result">
          <div class="diagnostic-overview">
            <div>
              <strong>公差-B-rep证据上下文已构建</strong>
              <span>{{ drawingInterfaceSummary.tolerances }} 条公差 / {{ drawingInterfaceSummary.brepBound }} 条有 B-rep 强证据</span>
            </div>
            <span class="status-pill">证据收集</span>
          </div>
          <div class="drawing-summary">
            <article>
              <strong>二维归属采样</strong>
              <p>{{ drawingInterfaceSummary.ownershipSamples }} 点</p>
            </article>
            <article>
              <strong>三维接口</strong>
              <p>圆柱 {{ drawingInterfaceSummary.cylindrical }} / 平面 {{ drawingInterfaceSummary.planar }}</p>
            </article>
            <article>
              <strong>零件功能画像</strong>
              <p>{{ drawingInterfaceSummary.components }} 个</p>
            </article>
          </div>
          <div class="drawing-issue-section">
            <h3>公差绑定摘要</h3>
            <div v-if="drawingInterfaceRows.length" class="format-issue-grid">
              <article v-for="row in drawingInterfaceRows" :key="row.id" class="format-issue-card">
                <div>
                  <strong>{{ row.text }}</strong>
                  <span>{{ row.binding }}</span>
                </div>
                <p>二维几何：{{ row.geometry }}</p>
                <p>B-rep强证据：{{ row.brepStrong }} 条</p>
                <p>接口：{{ row.uniqueInterfaces }}</p>
              </article>
            </div>
            <p v-else>未解析到可用于公差判断的候选。</p>
          </div>
        </section>

        <section v-if="isDrawingDiagnostic && drawingAssemblyAnalysisResult" class="diagnostic-result">
          <div class="diagnostic-overview">
            <strong>{{ isDrawingPartDiagnostic ? (drawingAssemblyAnalysisResult.ok ? '格式查错完成' : '格式查错未完成') : (drawingAssemblyAnalysisResult.ok ? 'AI三层分析完成' : 'AI三层分析未完成') }}</strong>
            <span>{{ drawingAssemblyAnalysisResult.stage }} / {{ drawingAssemblyAnalysisResult.artifacts.length }} 个产物</span>
          </div>

          <div class="drawing-summary">
            <article>
              <strong>学生 DWG</strong>
              <p>{{ drawingAssemblyAnalysisResult.student_dwg_path || '由流程输入生成' }}</p>
            </article>
            <article v-if="isDrawingAssemblyDiagnostic">
              <strong>三维模型</strong>
              <p>{{ drawingAssemblyAnalysisResult.model_path || '由流程输入生成' }}</p>
            </article>
            <article v-if="isDrawingAssemblyDiagnostic">
              <strong>学生原始视图</strong>
              <p>{{ drawingAssemblyAnalysisResult.student_view_image }}</p>
            </article>
            <article v-if="isDrawingAssemblyDiagnostic">
              <strong>归属/上色图</strong>
              <p>{{ drawingAssemblyAnalysisResult.ownership_image }}</p>
            </article>
            <article>
              <strong>输出目录</strong>
              <p>{{ drawingAssemblyAnalysisResult.output_dir }}</p>
            </article>
            <article>
              <strong>最终报告</strong>
              <p>{{ drawingAssemblyAnalysisResult.final_report_path }}</p>
            </article>
            <article v-if="drawingAssemblyAnalysisResult.pipeline_manifest_path">
              <strong>完整流程清单</strong>
              <p>{{ drawingAssemblyAnalysisResult.pipeline_manifest_path }}</p>
            </article>
          </div>

          <h3>流程产物</h3>
          <div class="category-grid">
            <article v-if="isDrawingAssemblyDiagnostic" class="category-card">
              <strong>第一轮视图探针</strong>
              <span>图片初筛</span>
              <p>{{ drawingAssemblyAnalysisResult.probe_path }}</p>
            </article>
            <article class="category-card">
              <strong>补充事实</strong>
              <span>三维/DWG摘要</span>
              <p>{{ drawingAssemblyAnalysisResult.supplemental_facts_path }}</p>
            </article>
            <article v-if="isDrawingAssemblyDiagnostic" class="category-card">
              <strong>三层候选问题</strong>
              <span>格式/标注/绘制</span>
              <p>{{ drawingAssemblyAnalysisResult.candidate_issues_path }}</p>
            </article>
            <article v-if="isDrawingAssemblyDiagnostic" class="category-card">
              <strong>知识库检索</strong>
              <span>规则依据</span>
              <p>{{ drawingAssemblyAnalysisResult.retrieved_rules_path }}</p>
            </article>
          </div>

          <div class="drawing-issue-layout">
            <section class="drawing-issue-section">
              <div class="section-head compact">
                <h3>1. 格式错误</h3>
                <span class="status-pill">{{ drawingAssemblyFormatIssues.length }} 条</span>
              </div>
              <p class="form-hint">线型和线宽由后端程序化规则判断，不经过 AI。</p>
              <div v-if="drawingAssemblyFormatIssues.length" class="format-issue-grid">
                <article v-for="issue in drawingAssemblyFormatIssues" :key="issue.id" class="format-issue-card">
                  <img
                    v-if="issue.evidence_image_url"
                    :src="`${apiBase}${issue.evidence_image_url}`"
                    :alt="issue.label || '格式错误区域'"
                  />
                  <div v-else class="issue-image-placeholder">局部图未生成</div>
                  <div class="format-issue-body">
                    <strong>{{ issue.label || '格式错误' }}</strong>
                    <span>{{ issue.expected_style }}</span>
                    <p>{{ issue.reason }}</p>
                    <small>{{ issue.current_style }}</small>
                    <small>{{ formatBounds(issue.bounds) }}</small>
                  </div>
                </article>
              </div>
              <p v-else class="empty-state">未发现确定的线型/线宽格式错误。</p>
            </section>

            <section class="drawing-issue-section">
              <h3>2. 标注错误</h3>
              <pre v-if="drawingAssemblyAnnotationReport" class="report-snippet">{{ drawingAssemblyAnnotationReport }}</pre>
              <p v-else class="empty-state">AI 报告中没有单独列出标注错误。</p>
            </section>

            <section class="drawing-issue-section">
              <h3>3. 绘制错误</h3>
              <pre v-if="drawingAssemblyRepresentationReport" class="report-snippet">{{ drawingAssemblyRepresentationReport }}</pre>
              <p v-else class="empty-state">AI 报告中没有单独列出绘制错误。</p>
            </section>
          </div>

          <div class="diagnostic-note">
            {{ isDrawingPartDiagnostic ? '该流程没有把线型/线宽格式错误交给 AI；当前零件图入口只执行 DWG 程序化格式查错。' : '该流程没有把 DWG 直接传给 AI；AI 输入为两张图片、后端整理后的结构化事实和知识库检索结果。' }}
          </div>
        </section>

        <section v-else-if="isDrawingDiagnostic && drawingExtractionResult" class="diagnostic-result">
          <div class="diagnostic-overview">
            <strong>{{ drawingExtractionResult.ok ? '图纸抽取完成' : '图纸抽取未完成' }}</strong>
            <span>共读取 {{ drawingExtractionResult.entity_count }} 个实体，输出 {{ drawingCategoryFiles.length }} 类结构化文件</span>
          </div>

          <div class="drawing-summary">
            <article>
              <strong>图纸类型</strong>
              <p>{{ diagnosticTitle }}</p>
            </article>
            <article>
              <strong>图纸文件</strong>
              <p>{{ drawingExtractionResult.drawing_path || '来自最近一次结果' }}</p>
            </article>
            <article>
              <strong>输出目录</strong>
              <p>{{ drawingExtractionResult.export_dir }}</p>
            </article>
            <article>
              <strong>抽取方式</strong>
              <p>{{ drawingExtractionResult.source }}{{ drawingExtractionResult.prog_id ? ` / ${drawingExtractionResult.prog_id}` : '' }}</p>
            </article>
          </div>

          <h3>二维证据分类</h3>
          <div class="category-grid">
            <article v-for="file in drawingCategoryFiles" :key="file.category" class="category-card">
              <strong>{{ drawingCategoryLabel(file.category) }}</strong>
              <span>{{ file.item_count }} 项</span>
              <p>{{ file.file }}</p>
            </article>
          </div>

          <template v-if="drawingTypeCountEntries.length">
            <h3>实体类型概览</h3>
            <div class="type-counts">
              <span v-for="[name, count] in drawingTypeCountEntries" :key="name">{{ name }} {{ count }}</span>
            </div>
          </template>

          <div class="diagnostic-note">
            当前这里只显示 DWG 抽取结果；格式查错需要点击左侧开始查错按钮。
          </div>
        </section>

        <section v-else-if="!isDrawingDiagnostic && diagnosticProgressState !== 'idle'" class="diagnostic-progress">
          <div class="progress-summary">
            <div>
              <strong>{{ diagnosticProgressState === 'done' ? '流程完成' : diagnosticProgressState === 'failed' ? '流程中断' : diagnosticCurrentStep.title }}</strong>
              <p>{{ diagnosticJob?.message || (diagnosticProgressState === 'running' ? diagnosticCurrentStep.detail : diagnosticMessage) }}</p>
            </div>
            <span>{{ diagnosticElapsedSeconds }} 秒</span>
          </div>
          <div class="progress-track">
            <div class="progress-bar" :style="{ width: `${diagnosticProgressPercent}%` }"></div>
          </div>
          <div v-if="diagnosticJob" class="progress-stats">
            <span>{{ diagnosticProgressText || diagnosticSubgraphProgressText || (diagnosticTargetKind === 'assembly' ? '正在准备零件功能介绍、功能组和组内查错队列' : '正在准备粗扫描、动态知识检索和局部子图队列') }}</span>
            <span>候选规则 {{ diagnosticJob.retrieved_candidate_count }}</span>
          </div>
          <div v-if="(diagnosticCurrentStep.key === 'group_part_checks' || diagnosticCurrentStep.key === 'local_subgraphs') && diagnosticCurrentSubgraphComponent" class="current-unit current-subgraph-component">
            <strong>正在生成：{{ diagnosticCurrentSubgraphComponent.display_name }}</strong>
            <p>{{ diagnosticCurrentSubgraphComponent.component_id }} / {{ diagnosticCurrentSubgraphComponent.instance_name }}</p>
            <span>{{ diagnosticSubgraphProgressText }} / 第 {{ diagnosticCurrentSubgraphComponent.iteration }} 轮</span>
          </div>
          <div v-if="diagnosticCurrentUnit" class="current-unit">
            <strong>正在分析：{{ diagnosticCurrentUnit.display_name }}</strong>
            <p>{{ diagnosticCurrentUnit.retrieval_sentence || diagnosticCurrentUnit.local_id }}</p>
            <span>{{ diagnosticCurrentUnit.primary_view || '未标注视图' }} / 候选 {{ diagnosticCurrentUnit.candidate_count }}</span>
          </div>
          <ol class="progress-steps">
            <li
              v-for="(step, index) in diagnosticProgressSteps"
              :key="step.key"
              :class="{
                active: diagnosticProgressState === 'running' && index === diagnosticActiveStep,
                done: diagnosticProgressState === 'done' || index < diagnosticActiveStep,
                failed: diagnosticProgressState === 'failed' && index === diagnosticActiveStep,
              }"
            >
              <span>{{ index + 1 }}</span>
              <div>
                <strong>{{ step.title }}</strong>
                <p>{{ step.detail }}</p>
              </div>
            </li>
          </ol>
        </section>

        <section v-if="!isDrawingDiagnostic && (diagnosticResult || diagnosticIssueCards.length)" class="diagnostic-result full-diagnostic-result">
          <div class="diagnostic-overview">
            <div>
              <strong>{{ diagnosticResult ? (diagnosticResult.final_status === 'ok' ? '诊断完成' : '诊断未完成') : '已处理零件的问题' }}</strong>
              <span>{{ diagnosticResult?.final_message || diagnosticMessage }}</span>
            </div>
            <div class="diagnostic-result-stats">
              <span>确认问题 {{ diagnosticIssueCards.length }}</span>
              <span>候选规则 {{ diagnosticResult?.retrieved_candidate_count ?? diagnosticJob?.retrieved_candidate_count ?? 0 }}</span>
              <span>证据图片 {{ diagnosticEvidenceImageCount }}</span>
            </div>
          </div>

          <div v-if="diagnosticResult" class="diagnostic-result-tabs" role="tablist" aria-label="诊断结果视图">
            <button :class="{ active: diagnosticResultTab === 'report' }" role="tab" :aria-selected="diagnosticResultTab === 'report'" @click="diagnosticResultTab = 'report'">完整报告</button>
            <button :class="{ active: diagnosticResultTab === 'issues' }" role="tab" :aria-selected="diagnosticResultTab === 'issues'" @click="diagnosticResultTab = 'issues'">确认问题</button>
            <button :class="{ active: diagnosticResultTab === 'evidence' }" role="tab" :aria-selected="diagnosticResultTab === 'evidence'" @click="diagnosticResultTab = 'evidence'">检索证据</button>
            <button :class="{ active: diagnosticResultTab === 'artifacts' }" role="tab" :aria-selected="diagnosticResultTab === 'artifacts'" @click="diagnosticResultTab = 'artifacts'">产物与路径</button>
          </div>

          <div v-if="diagnosticResult && diagnosticResultTab === 'report'" class="diagnostic-result-panel" role="tabpanel">
            <div v-if="diagnosticReportBlocks.length" class="diagnostic-report">
              <template v-for="(block, blockIndex) in diagnosticReportBlocks" :key="blockIndex">
                <h2 v-if="block.type === 'heading' && block.level === 1">{{ block.text }}</h2>
                <h3 v-else-if="block.type === 'heading' && block.level === 2">{{ block.text }}</h3>
                <h4 v-else-if="block.type === 'heading'">{{ block.text }}</h4>
                <ul v-else-if="block.type === 'list'">
                  <li v-for="(item, itemIndex) in block.items" :key="itemIndex">{{ item }}</li>
                </ul>
                <p v-else>{{ block.text }}</p>
              </template>
            </div>
            <p v-else class="empty-state">最终报告没有返回正文。</p>
          </div>

          <div v-else-if="diagnosticResultTab === 'issues' || !diagnosticResult" class="diagnostic-result-panel" role="tabpanel">
            <h3>{{ diagnosticResult ? '最终确认的问题' : '已处理零件检索到的问题' }}</h3>
            <div v-if="diagnosticIssueGroups.length" class="issue-list">
              <article v-for="group in diagnosticIssueGroups" :key="group.rule_id" class="issue-card grouped">
                <header class="issue-group-head">
                  <strong>{{ group.rule_id }} {{ group.rule_title }}</strong>
                  <span>{{ group.issues.length }} 个位置</span>
                </header>
                <div class="issue-group-items">
                  <div v-for="(issue, issueIndex) in group.issues" :key="`${issue.unit_key || issue.local_id}-${issue.rule_id}-${issue.score}-${issueIndex}`" class="issue-content">
                    <div>
                      <p class="issue-summary">{{ issue.retrieval_sentence }}</p>
                      <div class="issue-judgement">
                        <b>判定说明</b>
                        <p>{{ issue.judgement }}</p>
                      </div>
                    </div>
                    <div v-if="selectIssueEvidenceImages(issue).length" class="issue-evidence-set">
                      <figure v-for="image in selectIssueEvidenceImages(issue)" :key="`${image.source_unit_key || issue.unit_key}-${image.url}`" class="evidence-card issue-evidence-panel">
                        <img :src="`${apiBase}${image.url}`" :alt="image.name" />
                        <figcaption>{{ formatEvidenceCaption(image) }}</figcaption>
                      </figure>
                    </div>
                  </div>
                </div>
              </article>
            </div>
            <p v-else>{{ diagnosticResult ? '最终报告未确认结构设计问题。' : '已处理单元暂未检索到可用于判定的第 18 章禁忌条目。' }}</p>
          </div>

          <div v-else-if="diagnosticResultTab === 'evidence'" class="diagnostic-result-panel" role="tabpanel">
            <div class="result-panel-head">
              <div>
                <h3>全部检索候选与证据</h3>
                <p>这里展示进入最终判定的全部局部单元和候选规则，不代表这些候选都被判定为问题。</p>
              </div>
              <span>{{ diagnosticRetrievedCandidates.length }} 个单元-规则组合</span>
            </div>
            <div v-if="diagnosticRetrievedCandidates.length" class="retrieval-candidate-list">
              <article v-for="(issue, issueIndex) in diagnosticRetrievedCandidates" :key="`${issue.unit_key}-${issue.rule_id}-${issueIndex}`" class="retrieval-candidate">
                <header>
                  <div>
                    <strong>{{ issue.rule_id }} {{ issue.rule_title }}</strong>
                    <span>{{ issue.instance_name || issue.component_id || issue.local_id }}</span>
                  </div>
                  <b>匹配分 {{ issue.score }}</b>
                </header>
                <p>{{ issue.retrieval_sentence }}</p>
                <div class="candidate-meta">
                  <span>视图：{{ issue.primary_view || '未标注' }}</span>
                  <span>局部单元：{{ issue.local_id }}</span>
                  <span>证据：{{ issue.evidence_images.length }} 张</span>
                </div>
                <div v-if="issue.evidence_images.length" class="candidate-evidence-grid">
                  <figure v-for="image in issue.evidence_images" :key="image.url" class="candidate-evidence">
                    <img :src="`${apiBase}${image.url}`" :alt="image.name" />
                    <figcaption>
                      <strong>{{ image.name }}</strong>
                      <span v-if="image.evidence_purpose">{{ image.evidence_purpose }}</span>
                    </figcaption>
                  </figure>
                </div>
                <p v-else class="empty-state">该局部单元没有绑定导出的图片证据，判定依据来自结构化几何与语义事实。</p>
              </article>
            </div>
            <p v-else class="empty-state">没有返回检索候选。</p>
          </div>

          <div v-else class="diagnostic-result-panel" role="tabpanel">
            <h3>运行结果</h3>
            <dl class="diagnostic-paths">
              <div><dt>模型</dt><dd>{{ diagnosticResult.model_path || '未记录' }}</dd></div>
              <div><dt>输出目录</dt><dd>{{ diagnosticResult.output_dir }}</dd></div>
              <div><dt>语义图</dt><dd>{{ diagnosticResult.semantic_graph_path }}</dd></div>
              <div><dt>检索结果</dt><dd>{{ diagnosticResult.retrieval_path }}</dd></div>
              <div><dt>最终报告</dt><dd>{{ diagnosticResult.final_report_path }}</dd></div>
              <div><dt>原始响应</dt><dd>{{ diagnosticResult.final_response_path }}</dd></div>
              <div><dt>桥接阶段</dt><dd>{{ diagnosticResult.bridge.stage }}</dd></div>
            </dl>

            <div class="result-panel-head artifact-head">
              <div>
                <h3>全部产物</h3>
                <p>包括 B-rep、局部子图、知识检索、分阶段判定和最终报告产物。</p>
              </div>
              <span>{{ diagnosticArtifacts.length }} 个文件</span>
            </div>
            <div v-if="diagnosticArtifacts.length" class="artifact-list">
              <div v-for="artifact in diagnosticArtifacts" :key="artifact.file_path" class="artifact-row">
                <div>
                  <strong>{{ artifact.name }}</strong>
                  <span>{{ artifact.file_path }}</span>
                </div>
                <span>{{ formatArtifactSize(artifact.length) }} · {{ formatArtifactDate(artifact.last_modified) }}</span>
              </div>
            </div>
            <p v-else class="empty-state">没有返回产物清单。</p>
          </div>
        </section>

        <section v-else-if="isDrawingDiagnostic && diagnosticBusy" class="diagnostic-empty">
          <h3>正在读取图纸</h3>
          <p>后端正在通过 AutoCAD 抽取 DWG 实体并写出分类证据文件。</p>
        </section>
        <section v-else-if="diagnosticProgressState === 'idle'" class="diagnostic-empty">
          <h3>等待查错</h3>
          <p v-if="isDrawingPartDiagnostic">填写学生零件 DWG 路径后开始格式查错。后端会按 CAD 图元信息直接判断线型和线宽，不经过 AI。</p>
          <p v-else-if="isDrawingAssemblyDiagnostic">填写学生原始 DWG 路径和三维装配体模型路径后开始完整查错流程。后端会自动生成两张 PNG，DWG 不会直接传给 AI。</p>
          <p v-else>{{ diagnosticTargetKind === 'assembly' ? '填写装配体路径、产品描述和外购件完整名称后开始查错。' : '填写零件路径后开始查错。' }}结果会显示截图证据、问题位置、文字判定和对应禁忌条目。</p>
        </section>
        <section v-else class="diagnostic-empty">
          <h3>正在查错</h3>
          <p>后端正在生成或判定局部单元；已完成单元检索到的问题会实时出现在这里。</p>
        </section>
      </section>
    </section>

    <section v-else-if="!projectSelected" class="stage">
      <div class="stage-head">
        <button class="ghost" @click="back">返回</button>
        <h2>选择{{ modeText }}内容</h2>
      </div>
      <div class="choice-grid one">
        <button class="choice project-choice" @click="chooseProject">
          <strong>二级齿轮减速器</strong>
          <span>当前课程</span>
        </button>
      </div>
    </section>

    <section v-else-if="!scope" class="stage">
      <div class="stage-head">
        <button class="ghost" @click="back">返回</button>
        <h2>选择{{ modeText }}方式</h2>
      </div>
      <div class="choice-grid two">
        <button class="choice" :disabled="!overallStandard" @click="chooseScope('overall')">
          <strong>减速器整体{{ modeText }}</strong>
          <span>从第一个零件开始，直到完成装配</span>
        </button>
        <button class="choice" @click="chooseScope('part')">
          <strong>部分零件{{ modeText }}</strong>
          <span>选择一个非标零件单独训练</span>
        </button>
      </div>
    </section>

    <section v-else-if="scope === 'part' && !selected" class="stage">
      <div class="stage-head">
        <button class="ghost" @click="back">返回</button>
        <h2>选择零件</h2>
      </div>
      <div class="part-grid">
        <button v-for="part in partStandards" :key="part.standard_id" class="part-tile" @click="choosePart(part)">
          <strong>{{ part.title }}</strong>
          <span>{{ part.steps.length }} 步</span>
        </button>
      </div>
    </section>

    <section v-else class="workspace">
      <aside class="course-panel">
        <button class="ghost" @click="back">返回</button>
        <h2>{{ scope === 'overall' ? `二级齿轮减速器整体${modeText}` : selected?.title }}</h2>
        <p>{{ selected?.summary }}</p>
        <button v-if="!session && mode === 'teaching'" :disabled="!selected || openingStandardPart || introPlaying || !introduction?.standard_part_path" @click="openStandardPartForIntroduction">
          {{ openingStandardPart ? '正在打开标准零件...' : introPlaying ? '正在讲解并高亮...' : '打开标准零件并讲解' }}
        </button>
        <button v-if="!session" :disabled="!selected || startingSession" @click="startSession">
          {{ startingSession ? '正在连接 SolidWorks...' : `开始${modeText}` }}
        </button>
        <p v-if="!session" class="feedback">{{ feedback }}</p>

        <section v-if="loadingTeachingScript || teachingScript" class="designer-panel">
          <h3>设计智能体</h3>
          <p v-if="loadingTeachingScript">正在分析完整建模步骤...</p>
          <template v-else-if="teachingScript">
            <p>{{ teachingScript.summary }}</p>
            <details>
              <summary>查看前 8 步提示语言</summary>
              <ol>
                <li v-for="step in teachingScript.steps.slice(0, 8)" :key="step.order">
                  {{ step.voice_text }}
                </li>
              </ol>
            </details>
          </template>
        </section>

        <div v-if="session" class="session-meta">
          <span>{{ session.status }}</span>
          <strong>{{ progressText }}</strong>
        </div>
      </aside>

      <section class="work-panel">
        <template v-if="session && currentStep">
          <div class="section-head">
            <h2>{{ currentStep.title }}</h2>
            <button @click="speak">播放语音</button>
          </div>
          <p class="voice">{{ currentStep.voice_text }}</p>
          <div v-if="currentStep.teaching_intent || currentStep.common_mistake" class="step-thinking">
            <p v-if="currentStep.teaching_intent"><strong>操作意图：</strong>{{ currentStep.teaching_intent }}</p>
            <p v-if="currentStep.common_mistake"><strong>易错提醒：</strong>{{ currentStep.common_mistake }}</p>
          </div>

          <div class="listen-state" :class="{ active: autoListening }">
            <strong>{{ autoListening ? '实时检测中' : '实时检测未开启' }}</strong>
            <span>{{ pollingMessage || '系统会自动读取 SolidWorks 当前特征树。' }}</span>
          </div>

          <details v-if="mode === 'teaching'" class="manual-box">
            <summary>手动模拟</summary>
            <input v-model="observedSkill" />
            <textarea v-model="observedArgs" rows="4" />
            <button @click="submitObservation">提交并对比</button>
          </details>

          <p class="feedback">{{ feedback }}</p>
        </template>

        <template v-else-if="session">
          <h2>{{ modeText }}已完成</h2>
          <p class="feedback">{{ feedback }}</p>
        </template>


        <template v-else-if="mode === 'teaching'">
          <div class="section-head">
            <h2>{{ loadingIntroduction ? '正在准备零件导入...' : `认识零件：${introduction?.title || selected?.title || ''}` }}</h2>
            <button :disabled="!introduction || introPlaying" @click="speakIntroduction">
              {{ introPlaying ? '正在讲解并高亮...' : '播放介绍' }}
            </button>
          </div>

          <section v-if="introduction" class="intro-panel">
            <div class="intro-grid">
              <article><h3>零件是什么</h3><p>{{ introduction.overview }}</p></article>
              <article><h3>在减速器中的位置</h3><p>{{ introduction.reducer_position }}</p></article>
              <article><h3>作用</h3><p>{{ introduction.functional_role }}</p></article>
              <article><h3>基本特征</h3><p>{{ introduction.key_features }}</p></article>
            </div>

            <div class="knowledge-box">
              <h3>知识库覆盖情况</h3>
              <p>{{ introduction.knowledge_coverage.highlight_support }}</p>
              <ul>
                <li v-for="item in introduction.knowledge_snippets" :key="item">{{ item }}</li>
                <li v-for="item in introduction.source_notes" :key="item">{{ item }}</li>
                <li v-for="item in introduction.knowledge_coverage.missing_items" :key="item">缺少：{{ item }}</li>
              </ul>
            </div>

            <div class="knowledge-box">
              <h3>可讲解的功能面</h3>
              <p v-if="!introduction.highlight_targets.length">当前知识库还没有足够的功能面语义，暂时只能讲解零件整体和特征树。</p>
              <div v-else class="target-list">
                <button
                  v-for="target in introduction.highlight_targets.slice(0, 12)"
                  :key="target.id"
                  class="target-item"
                  :class="{ active: highlightingTargetId === target.id }"
                  :disabled="highlightingTargetId === target.id"
                  @click="highlightIntroductionTarget(target)"
                >
                  <strong>{{ target.label }}</strong>
                  <span>{{ highlightingTargetId === target.id ? '正在高亮...' : target.description }}</span>
                </button>
              </div>
            </div>

            <p class="path-note">标准零件：{{ introduction.standard_part_path || '未找到' }}</p>
          </section>

          <p class="feedback">{{ feedback }}</p>
        </template>        <template v-else>
          <h2>准备开始</h2>
        </template>
      </section>
    </section>
  </main>
</template>

