﻿import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Alert, Avatar, Button, Card, Col, Divider, Drawer, Empty, Form, Input, List, Modal, Popover, Progress, QRCode, Radio, Row, Select, Skeleton, Space, Switch, Tag, Typography, App, Dropdown } from 'antd';
import {
  AudioOutlined,
  CaretRightFilled,
  ClockCircleOutlined,
  CopyOutlined,
  DownloadOutlined,
  EditOutlined,
  FastForwardOutlined,
  FileTextOutlined,
  LeftOutlined,
  LinkOutlined,
  LoadingOutlined,
  PauseOutlined,
  SyncOutlined,
  UserOutlined,
  CheckCircleFilled,
  FilePdfOutlined,
  FileWordOutlined,
  ShareAltOutlined, InfoCircleOutlined,
} from '@ant-design/icons';
import dayjs from 'dayjs';
import ReactMarkdown from 'react-markdown';
import {
  downloadMeetingTranscript,
  downloadMeetingSummary,
  getMeetingDetail,
  getMeetingChapters,
  getMeetingProgress,
  getMeetingShareConfig,
  getTranscripts,
  MeetingChapterVO,
  MeetingProgress,
  SummaryDetailLevel,
  MeetingTranscriptVO,
  MeetingVO,
  reSummary,
  resolveAudioMimeType,
  resolveMeetingPlaybackAudioUrl,
  retryMeetingChapter,
  retryMeetingSummary,
  retryMeetingTranscription,
  retryScheduleMeeting,
  updateMeetingBasic,
  updateMeetingTranscript,
  updateMeetingSummary,
  updateSpeakerInfo,
} from '../../api/business/meeting';
import { getAiModelDefault, getAiModelPage, AiModelVO } from '../../api/business/aimodel';
import { getHotWordPage, getPinyinSuggestion, saveHotWord } from '../../api/business/hotword';
import { getPromptPage, PromptTemplateVO } from '../../api/business/prompt';
import { listUsers } from '../../api';
import { useDict } from '../../hooks/useDict';
import { SysUser } from '../../types';
import PageContainer from "../../components/shared/PageContainer";
import SectionCard from "../../components/shared/SectionCard";
import "./MeetingDetail.css";

const { Title, Text } = Typography;
const { Option } = Select;

type AnalysisChapter = {
  time?: string;
  title: string;
  summary: string;
};

type AnalysisSpeakerSummary = {
  speaker: string;
  summary: string;
};

type AnalysisKeyPoint = {
  title: string;
  summary: string;
  speaker?: string;
  time?: string;
};

type MeetingAnalysis = {
  overview: string;
  keywords: string[];
  chapters: AnalysisChapter[];
  speakerSummaries: AnalysisSpeakerSummary[];
  keyPoints: AnalysisKeyPoint[];
  todos: string[];
};

type WorkspaceTab = 'catalog' | 'transcript';

type ChapterTranscriptLink = {
  key: string;
  title: string;
  timeLabel: string;
  transcriptIds: number[];
  firstTranscriptId: number | null;
  firstTranscriptStartTime: number | null;
};

type MeetingStateNotice = {
  title: string;
  description: string;
  type: 'info' | 'warning';
  hasFallbackContent: boolean;
  scope: 'summary' | 'catalog' | 'global';
};

const QUEUED_RETRY_THRESHOLD_MS = 2 * 60 * 1000;

const ANALYSIS_EMPTY: MeetingAnalysis = {
  overview: '',
  keywords: [],
  chapters: [],
  speakerSummaries: [],
  keyPoints: [],
  todos: [],
};

const ACCESS_PASSWORD_PATTERN = /^[A-Za-z0-9]{4}$/;
const ACCESS_PASSWORD_SOURCE = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';

const copyMeetingLink = async (text: string) => {
  if (navigator.clipboard?.writeText) {
    await navigator.clipboard.writeText(text);
    return;
  }

  const textarea = document.createElement('textarea');
  textarea.value = text;
  textarea.setAttribute('readonly', 'true');
  textarea.style.position = 'fixed';
  textarea.style.opacity = '0';
  document.body.appendChild(textarea);
  textarea.select();
  document.execCommand('copy');
  document.body.removeChild(textarea);
};

const generateAccessPassword = () =>
  Array.from({ length: 4 }, () => ACCESS_PASSWORD_SOURCE[Math.floor(Math.random() * ACCESS_PASSWORD_SOURCE.length)]).join('');

const normalizeAccessPasswordInput = (value?: string | null) => (value || '').replace(/[^A-Za-z0-9]/g, '').slice(0, 4);

const sanitizeDownloadFileName = (value?: string | null, fallback = 'meeting-recording') => {
  const normalized = (value || '').replace(/[\\/:*?"<>|\r\n]/g, '_').trim();
  return normalized || fallback;
};

const resolveAudioExtension = (audioUrl?: string) => {
  const normalizedUrl = audioUrl?.split('#')[0]?.split('?')[0] || '';
  return normalizedUrl.match(/\.([a-z0-9]+)$/i)?.[1]?.toLowerCase() || 'mp3';
};

const normalizeTagDisplay = (tag: string) => tag.replace(/^#+\s*/, '').trim();

const getMeetingAudioDownloadName = (meeting?: Pick<MeetingVO, 'title' | 'audioUrl' | 'playbackAudioUrl'> | null) => {
  const audioUrl = resolveMeetingPlaybackAudioUrl(meeting);
  const extension = resolveAudioExtension(audioUrl);
  return `${sanitizeDownloadFileName(meeting?.title)}-录音.${extension}`;
};

const buildMeetingPreviewUrl = (baseUrl?: string, meetingId?: number, accessPassword?: string) => {
  const normalizedBaseUrl = (baseUrl || '').trim();
  if (!normalizedBaseUrl || !meetingId || Number.isNaN(meetingId)) {
    return '';
  }
  const safeBaseUrl = normalizedBaseUrl.endsWith('/') ? normalizedBaseUrl.slice(0, -1) : normalizedBaseUrl;
  const url = new URL(`${safeBaseUrl}/meetings/${meetingId}/preview`);
  const normalizedPassword = (accessPassword || '').trim();
  if (normalizedPassword) {
    url.searchParams.set('accessPassword', normalizedPassword);
  }
  return url.toString();
};


const splitLines = (value?: string | null) =>
  (value || '')
    .split(/\r?\n/)
    .map((line) => line.trim())
    .filter(Boolean);

const parseLooseJson = (raw?: string | null) => {
  const input = (raw || '').trim();
  if (!input) return null;

  const tryParse = (text: string) => {
    try {
      return JSON.parse(text);
    } catch {
      return null;
    }
  };

  const direct = tryParse(input);
  if (direct && typeof direct === 'object') return direct;

  const fenced = input.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1]?.trim();
  if (fenced) {
    const fencedParsed = tryParse(fenced);
    if (fencedParsed && typeof fencedParsed === 'object') return fencedParsed;
  }

  const start = input.indexOf('{');
  const end = input.lastIndexOf('}');
  if (start >= 0 && end > start) {
    const wrapped = tryParse(input.slice(start, end + 1));
    if (wrapped && typeof wrapped === 'object') return wrapped;
  }

  return null;
};

const extractSection = (markdown: string, aliases: string[]) => {
  const lines = markdown.split(/\r?\n/);
  const lowerAliases = aliases.map((item) => item.toLowerCase());
  const cleanHeading = (line: string) => line.replace(/^#{1,6}\s*/, '').trim().toLowerCase();

  let start = -1;
  for (let index = 0; index < lines.length; index += 1) {
    const line = lines[index].trim();
    if (!line.startsWith('#')) continue;
    const heading = cleanHeading(line);
    if (lowerAliases.some((alias) => heading.includes(alias))) {
      start = index + 1;
      break;
    }
  }

  if (start < 0) return '';

  const buffer: string[] = [];
  for (let index = start; index < lines.length; index += 1) {
    const line = lines[index];
    if (line.trim().startsWith('#')) break;
    buffer.push(line);
  }
  return buffer.join('\n').trim();
};

const parseBulletList = (content?: string | null) =>
  splitLines(content)
    .map((line) => line.replace(/^[-*鈥s]+/, '').replace(/^\d+[.)]\s*/, '').trim())
    .filter(Boolean);

const parseOverviewSection = (markdown: string) =>
  extractSection(markdown, ['全文概要', '概要', '摘要', '概览']) || markdown.replace(/^---[\s\S]*?---/, '').trim();

const parseKeywordsSection = (markdown: string, tags: string) => {
  const section = extractSection(markdown, ['关键词', '关键字', '标签']);
  const fromSection = parseBulletList(section)
    .flatMap((line) => line.split(/[，,、/]/))
    .map((item) => item.trim())
    .filter(Boolean);

  if (fromSection.length) {
    return Array.from(new Set(fromSection)).slice(0, 12);
  }

  return Array.from(new Set((tags || '').split(',').map((item) => item.trim()).filter(Boolean))).slice(0, 12);
};

const buildMeetingAnalysis = (
  sourceAnalysis: MeetingVO['analysis'] | undefined,
  summaryContent: string | undefined,
  tags: string,
): MeetingAnalysis => {
  const parseStructured = (parsed: Record<string, any>): MeetingAnalysis => {
    const chapters = Array.isArray(parsed.chapters) ? parsed.chapters : [];
    const speakerSummaries = Array.isArray(parsed.speakerSummaries) ? parsed.speakerSummaries : [];
    const keyPoints = Array.isArray(parsed.keyPoints) ? parsed.keyPoints : [];
    const todos = Array.isArray(parsed.todos)
      ? parsed.todos
      : Array.isArray(parsed.actionItems)
      ? parsed.actionItems
      : [];

    return {
      overview: String(parsed.overview || '').trim(),
      keywords: Array.from(
        new Set((Array.isArray(parsed.keywords) ? parsed.keywords : []).map((item) => String(item).trim()).filter(Boolean)),
      ).slice(0, 12),
      chapters: chapters
        .map((item: any) => ({
          time: item?.time ? String(item.time).trim() : undefined,
          title: String(item?.title || '').trim(),
          summary: String(item?.summary || '').trim(),
        }))
        .filter((item: AnalysisChapter) => item.title || item.summary),
      speakerSummaries: speakerSummaries
        .map((item: any) => ({
          speaker: String(item?.speaker || '').trim(),
          summary: String(item?.summary || '').trim(),
        }))
        .filter((item: AnalysisSpeakerSummary) => item.speaker || item.summary),
      keyPoints: keyPoints
        .map((item: any) => ({
          title: String(item?.title || '').trim(),
          summary: String(item?.summary || '').trim(),
          speaker: item?.speaker ? String(item.speaker).trim() : undefined,
          time: item?.time ? String(item.time).trim() : undefined,
        }))
        .filter((item: AnalysisKeyPoint) => item.title || item.summary),
      todos: todos.map((item: any) => String(item).trim()).filter(Boolean).slice(0, 10),
    };
  };

  if (sourceAnalysis) {
    return parseStructured(sourceAnalysis as Record<string, any>);
  }

  const raw = (summaryContent || '').trim();
  if (!raw && !tags) return ANALYSIS_EMPTY;

  const loose = parseLooseJson(raw);
  if (loose) {
    return parseStructured(loose);
  }

  return {
    overview: parseOverviewSection(raw),
    keywords: parseKeywordsSection(raw, tags),
    chapters: [],
    speakerSummaries: [],
    keyPoints: [],
    todos: [],
  };
};

function formatTime(ms: number) {
  const seconds = Math.floor(ms / 1000);
  const minutes = Math.floor(seconds / 60);
  const remainSeconds = seconds % 60;
  return `${minutes.toString().padStart(2, '0')}:${remainSeconds.toString().padStart(2, '0')}`;
}

function formatPlayerTime(seconds: number) {
  const safeSeconds = Math.max(0, Math.floor(seconds || 0));
  const hours = Math.floor(safeSeconds / 3600);
  const minutes = Math.floor((safeSeconds % 3600) / 60);
  const remainSeconds = safeSeconds % 60;

  if (hours > 0) {
    return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${remainSeconds.toString().padStart(2, '0')}`;
  }

  return `${minutes.toString().padStart(2, '0')}:${remainSeconds.toString().padStart(2, '0')}`;
}

function parseChapterTimeToMs(value?: string) {
  const raw = String(value || '').trim();
  if (!raw) return null;

  const matched = raw.match(/(\d{1,2}:\d{2}(?::\d{2})?)/)?.[1];
  if (!matched) return null;

  const parts = matched.split(':').map((item) => Number(item));
  if (parts.some((item) => Number.isNaN(item))) {
    return null;
  }

  const totalSeconds = parts.length === 3
    ? (parts[0] * 3600) + (parts[1] * 60) + parts[2]
    : (parts[0] * 60) + parts[1];

  return totalSeconds * 1000;
}

type MeetingProgressPhase = 'queued' | 'asr' | 'chapter' | 'summary' | 'terminal';

const meetingProgressTerminalRefreshCache = new Map<number, string>();
const meetingProgressPhaseRefreshCache = new Map<number, MeetingProgressPhase>();
const resolveProgressPhase = (progress: MeetingProgress | null | undefined): MeetingProgressPhase => {
  const unifiedStatusCode = progress?.unifiedStatus?.statusCode;
  if (unifiedStatusCode?.startsWith('FAILED_') || unifiedStatusCode === 'COMPLETED') {
    return 'terminal';
  }
  if (unifiedStatusCode === 'SUMMARIZING') {
    return 'summary';
  }
  if (unifiedStatusCode === 'TRANSCRIBING') {
    return 'asr';
  }
  if (unifiedStatusCode === 'INITIALIZING' || unifiedStatusCode === 'WAITING_UPLOAD') {
    return 'queued';
  }
  const percent = progress?.percent ?? 0;
  if (percent < 0 || percent >= 100) {
    return 'terminal';
  }
  if (percent >= 90) {
    return 'summary';
  }
  if (percent >= 85) {
    return 'chapter';
  }
  if (percent >= 5) {
    return 'asr';
  }
  return 'queued';
};

const MeetingProgressDisplay: React.FC<{
  meetingId: number;
  onComplete: () => void;
  onRefreshNeeded?: (phase: MeetingProgressPhase) => void;
  onProgressChange?: (progress: MeetingProgress | null) => void;
  compact?: boolean;
  inline?: boolean;
}> = ({ meetingId, onComplete, onRefreshNeeded, onProgressChange, compact, inline }) => {
  const [progress, setProgress] = useState<MeetingProgress | null>(null);

  const onCompleteRef = useRef(onComplete);
  const onRefreshNeededRef = useRef(onRefreshNeeded);
  const onProgressChangeRef = useRef(onProgressChange);

  useEffect(() => {
    onCompleteRef.current = onComplete;
    onRefreshNeededRef.current = onRefreshNeeded;
    onProgressChangeRef.current = onProgressChange;
  }, [onComplete, onRefreshNeeded, onProgressChange]);

  useEffect(() => {
    let completed = false;
    let timer: ReturnType<typeof setTimeout> | null = null;
    let requesting = false;

    const fetchProgress = async () => {
      if (completed || requesting) {
        return;
      }
      requesting = true;
      let shouldContinue = true;
      try {
        const progressRes = await getMeetingProgress(meetingId, { suppressErrorToast: true });
        if (progressRes.data?.data) {
          const nextProgress = progressRes.data.data;
          setProgress(nextProgress);
          onProgressChangeRef.current?.(nextProgress);
          const phase = resolveProgressPhase(nextProgress);
          const previousPhase = meetingProgressPhaseRefreshCache.get(meetingId);
          meetingProgressPhaseRefreshCache.set(meetingId, phase);
          if ((phase === 'chapter' || phase === 'summary') && previousPhase !== phase) {
            onRefreshNeededRef.current?.(phase);
          }
          if (nextProgress.percent === 100 || nextProgress.percent < 0) {
            completed = true;
            shouldContinue = false;
            const terminalKey = `${nextProgress.updateAt}:${nextProgress.percent}`;
            if (meetingProgressTerminalRefreshCache.get(meetingId) !== terminalKey) {
              meetingProgressTerminalRefreshCache.set(meetingId, terminalKey);
              onCompleteRef.current?.();
            }
          } else {
            meetingProgressTerminalRefreshCache.delete(meetingId);
          }
        }
      } catch {
        // ignore
      } finally {
        requesting = false;
        if (!completed && shouldContinue) {
          timer = setTimeout(() => {
            void fetchProgress();
          }, 3000);
        }
      }
    };

    void fetchProgress();
    return () => {
      completed = true;
      if (timer) {
        clearTimeout(timer);
      }
    };
  }, [meetingId]);

  const percent = progress?.percent || 0;
  const isError = percent < 0;
  const unifiedStatusText = progress?.unifiedStatus?.statusText;
  const unifiedStatusMessage = progress?.unifiedStatus?.message;

  const formatEta = (seconds?: number) => {
    if (!seconds || seconds <= 0) return '计算中';
    if (seconds < 60) return `${seconds} 秒`;
    const minutes = Math.floor(seconds / 60);
    const remainSeconds = seconds % 60;
    return remainSeconds > 0 ? `${minutes} 分 ${remainSeconds} 秒` : `${minutes} 分钟`;
  };

  if (inline) {
    return (
      <div
        style={{
          marginTop: 12,
          padding: '12px 14px',
          borderRadius: 12,
          border: `1px solid ${isError ? '#ffccc7' : '#d6dbff'}`,
          background: isError ? '#fff2f0' : '#f7f8ff',
        }}
      >
        <div
          style={{
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'space-between',
            gap: 12,
            marginBottom: 8,
          }}
        >
          <Text strong style={{ color: isError ? '#cf1322' : '#4f46e5' }}>
            {unifiedStatusMessage || unifiedStatusText || progress?.message || '正在生成最新结果...'}
          </Text>
          <Text strong style={{ color: isError ? '#cf1322' : '#4f46e5', whiteSpace: 'nowrap' }}>
            {isError ? '失败' : `${percent}%`}
          </Text>
        </div>
        <Progress
          percent={isError ? 100 : percent}
          status={isError ? 'exception' : percent === 100 ? 'success' : 'active'}
          strokeColor={isError ? '#ff4d4f' : '#6c73ff'}
          showInfo={false}
          size="small"
          style={{ marginBottom: 6 }}
        />
        <Text type="secondary" style={{ fontSize: 12 }}>
          {`预计剩余：${isError ? '--' : formatEta(progress?.eta)}`}
        </Text>
      </div>
    );
  }

  if (compact) {
    return (
      <div
        style={{
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          padding: '40px 20px',
          textAlign: 'center',
        }}
      >
        <Title level={4} style={{ marginBottom: 24 }}>
          AI 智能总结中
        </Title>
        <Progress
          type="circle"
          percent={isError ? 100 : percent}
          status={isError ? 'exception' : percent === 100 ? 'success' : 'active'}
          strokeColor={isError ? '#ff4d4f' : { '0%': '#6c73ff', '100%': '#8d63ff' }}
          size={140}
          strokeWidth={10}
        />
        <div style={{ marginTop: 24 }}>
          <Text strong style={{ fontSize: 16, color: isError ? '#ff4d4f' : '#5d67ff', display: 'block', marginBottom: 4 }}>
            {unifiedStatusMessage || unifiedStatusText || progress?.message || '正在分析内容...'}
          </Text>
          <Text type="secondary" style={{ fontSize: 13 }}>
            {`预计剩余：${isError ? '--' : formatEta(progress?.eta)}`}
          </Text>
        </div>
      </div>
    );
  }

  return (
    <div
      style={{
        height: '100%',
        display: 'flex',
        flexDirection: 'column',
        justifyContent: 'center',
        alignItems: 'center',
        background: '#fff',
        borderRadius: 16,
        padding: 40,
      }}
    >
      <div style={{ width: '100%', maxWidth: 600, textAlign: 'center' }}>
        <Title level={3} style={{ marginBottom: 24 }}>
          AI 智能分析中
        </Title>
        <Progress
          type="circle"
          percent={isError ? 100 : percent}
          status={isError ? 'exception' : percent === 100 ? 'success' : 'active'}
          strokeColor={isError ? '#ff4d4f' : { '0%': '#6c73ff', '100%': '#8d63ff' }}
          size={180}
          strokeWidth={8}
        />
        <div style={{ marginTop: 32 }}>
          <Text strong style={{ fontSize: 18, color: isError ? '#ff4d4f' : '#5d67ff', display: 'block', marginBottom: 8 }}>
            {unifiedStatusMessage || unifiedStatusText || progress?.message || '正在准备计算资源...'}
          </Text>
          <Text type="secondary">分析进行中，请稍候，你可以先处理其他工作。</Text>
        </div>
        <Divider style={{ margin: '32px 0' }} />
        <Row gutter={24}>
          <Col span={8}>
            <Space direction="vertical" size={0}>
              <Text type="secondary">当前进度</Text>
              <Title level={4} style={{ margin: 0 }}>
                {isError ? 'ERROR' : `${percent}%`}
              </Title>
            </Space>
          </Col>
          <Col span={8}>
            <Space direction="vertical" size={0}>
              <Text type="secondary">预计剩余</Text>
              <Title level={4} style={{ margin: 0 }}>
                {isError ? '--' : formatEta(progress?.eta)}
              </Title>
            </Space>
          </Col>
          <Col span={8}>
            <Space direction="vertical" size={0}>
              <Text type="secondary">任务状态</Text>
              <Title level={4} style={{ margin: 0, color: isError ? '#ff4d4f' : '#52c41a' }}>
                {isError ? '失败' : '正常'}
              </Title>
            </Space>
          </Col>
        </Row>
      </div>
    </div>
  );
};

const DETAIL_STAGE_STEP_DISPLAY_ITEMS = [
  { code: 'INITIALIZING', label: '数据初始化', hint: '完成会议数据准备' },
  { code: 'TRANSCRIBING', label: '转译音频', hint: '完成语音转写' },
  { code: 'SUMMARIZING', label: '生成总结', hint: '生成总结与 AI 目录' },
  { code: 'COMPLETED', label: '处理完成', hint: '全部任务处理完成' },
] as const;

const UnifiedMeetingProgressDisplay: React.FC<{
  progress: MeetingProgress | null;
  compact?: boolean;
  inline?: boolean;
}> = ({ progress, compact, inline }) => {
  const percent = progress?.percent || 0;
  const isError = percent < 0;
  const unifiedStatusCode = progress?.unifiedStatus?.statusCode;
  const unifiedStatusText = progress?.unifiedStatus?.statusText;
  const unifiedStatusMessage = progress?.unifiedStatus?.message;
  const failedStageCode = progress?.unifiedStatus?.failedStageCode;
  const primaryStatusText = unifiedStatusText || (isError ? '处理失败' : '处理中');

  const formatEta = (seconds?: number) => {
    if (!seconds || seconds <= 0) return '计算中';
    if (seconds < 60) return `${seconds} 秒`;
    const minutes = Math.floor(seconds / 60);
    const remainSeconds = seconds % 60;
    return remainSeconds > 0 ? `${minutes} 分 ${remainSeconds} 秒` : `${minutes} 分钟`;
  };

  const resolveStageCode = () => {
    if (failedStageCode) {
      return failedStageCode;
    }
    if (unifiedStatusCode === 'WAITING_UPLOAD' || unifiedStatusCode === 'INITIALIZING') {
      return 'INITIALIZING';
    }
    if (unifiedStatusCode === 'TRANSCRIBING') {
      return 'TRANSCRIBING';
    }
    if (unifiedStatusCode === 'SUMMARIZING') {
      return 'SUMMARIZING';
    }
    if (unifiedStatusCode === 'COMPLETED') {
      return 'COMPLETED';
    }
    const phase = resolveProgressPhase(progress);
    if (phase === 'queued') {
      return 'INITIALIZING';
    }
    if (phase === 'asr') {
      return 'TRANSCRIBING';
    }
    if (phase === 'chapter' || phase === 'summary') {
      return 'SUMMARIZING';
    }
    return 'COMPLETED';
  };

  const currentStageCode = resolveStageCode();
  const currentStageIndex = Math.max(0, DETAIL_STAGE_STEP_DISPLAY_ITEMS.findIndex((item) => item.code === currentStageCode));
  const isFailedStage = unifiedStatusCode?.startsWith('FAILED_') || isError;
  const isCompletedStage = unifiedStatusCode === 'COMPLETED' || percent === 100;
  const progressText = isError ? '--' : `${Math.max(percent, 0)}%`;
  const helperText = unifiedStatusMessage || progress?.message || (isError ? '当前阶段执行失败，请稍后重试。' : '阶段状态已与安卓端统一。');

  const renderStageTimeline = (options?: {
    compact?: boolean;
    inline?: boolean;
  }) => {
    const compactMode = options?.compact;
    const inlineMode = options?.inline;
    const containerStyle: React.CSSProperties = inlineMode
      ? {
          marginTop: 10,
          display: 'grid',
          gridTemplateColumns: 'repeat(4, minmax(0, 1fr))',
          gap: 8,
        }
      : compactMode
      ? {
          marginTop: 18,
          display: 'grid',
          gridTemplateColumns: 'repeat(4, minmax(0, 1fr))',
          gap: 8,
          width: '100%',
          maxWidth: 560,
        }
      : {
          display: 'flex',
          alignItems: 'flex-start',
          justifyContent: 'space-between',
          gap: 12,
        };

    return (
      <div style={containerStyle}>
        {DETAIL_STAGE_STEP_DISPLAY_ITEMS.map((item, index) => {
          const isCurrent = index === currentStageIndex;
          const isDone = isCompletedStage || (!isFailedStage && index < currentStageIndex);
          const isFailed = isFailedStage && isCurrent;
          const dotBg = isFailed ? '#ff4d4f' : isDone ? '#52c41a' : isCurrent ? '#4f46e5' : '#d9d9d9';
          const textColor = isFailed ? '#cf1322' : isDone || isCurrent ? '#1f1f1f' : '#8c8c8c';
          const cardPadding = inlineMode ? '8px 6px' : compactMode ? '10px 8px' : '0';
          const cardRadius = inlineMode || compactMode ? 12 : 0;
          const cardBorder = inlineMode || compactMode
            ? `1px solid ${isFailed ? '#ffccc7' : isCurrent ? '#c7d2fe' : '#e5e7eb'}`
            : 'none';
          const cardBackground = inlineMode || compactMode
            ? isFailed ? '#fff2f0' : isCurrent ? '#eef2ff' : isDone ? '#f6ffed' : '#fafafa'
            : 'transparent';

          return (
            <React.Fragment key={item.code}>
              <div
                style={{
                  flex: 1,
                  minWidth: 0,
                  padding: cardPadding,
                  borderRadius: cardRadius,
                  border: cardBorder,
                  background: cardBackground,
                  textAlign: 'center',
                }}
              >
                <div
                  style={{
                    width: compactMode || inlineMode ? 28 : 36,
                    height: compactMode || inlineMode ? 28 : 36,
                    margin: `0 auto ${compactMode || inlineMode ? 8 : 12}px`,
                    borderRadius: '50%',
                    display: 'flex',
                    alignItems: 'center',
                    justifyContent: 'center',
                    background: dotBg,
                    color: '#fff',
                    fontWeight: 700,
                    boxShadow: isCurrent ? `0 0 0 ${compactMode || inlineMode ? 4 : 6}px ${isFailed ? 'rgba(255,77,79,0.12)' : 'rgba(79,70,229,0.12)'}` : 'none',
                  }}
                >
                  {isDone ? '√' : index + 1}
                </div>
                <div
                  style={{
                    fontSize: compactMode || inlineMode ? 12 : 15,
                    fontWeight: 600,
                    color: textColor,
                    marginBottom: compactMode || inlineMode ? 0 : 6,
                  }}
                >
                  {isFailed && item.code === currentStageCode ? `${item.label}失败` : item.label}
                </div>
                {!compactMode && !inlineMode ? (
                  <Text type="secondary" style={{ fontSize: 12, lineHeight: 1.6 }}>
                    {item.hint}
                  </Text>
                ) : null}
              </div>
              {!inlineMode && !compactMode && index < DETAIL_STAGE_STEP_DISPLAY_ITEMS.length - 1 ? (
                <div
                  style={{
                    flex: 1,
                    height: 2,
                    marginTop: 17,
                    background: (!isFailedStage && index < currentStageIndex) || isCompletedStage ? '#52c41a' : '#e5e7eb',
                  }}
                />
              ) : null}
            </React.Fragment>
          );
        })}
      </div>
    );
  };

  if (inline) {
    return (
      <div
        style={{
          marginTop: 12,
          padding: '12px 14px',
          borderRadius: 12,
          border: `1px solid ${isError ? '#ffccc7' : '#d6dbff'}`,
          background: isError ? '#fff2f0' : '#f7f8ff',
        }}
      >
        <div
          style={{
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'space-between',
            gap: 12,
            marginBottom: 8,
          }}
        >
          <Text strong style={{ color: isError ? '#cf1322' : '#4f46e5' }}>
            {helperText}
          </Text>
          <Text strong style={{ color: isError ? '#cf1322' : '#4f46e5', whiteSpace: 'nowrap' }}>
            {isError ? '失败' : `${percent}%`}
          </Text>
        </div>
        <Progress
          percent={isError ? 100 : percent}
          status={isError ? 'exception' : percent === 100 ? 'success' : 'active'}
          strokeColor={isError ? '#ff4d4f' : '#6c73ff'}
          showInfo={false}
          size="small"
          style={{ marginBottom: 6 }}
        />
        <Text type="secondary" style={{ fontSize: 12 }}>
          {`预计剩余：${isError ? '--' : formatEta(progress?.eta)}`}
        </Text>
        {renderStageTimeline({ inline: true })}
      </div>
    );
  }

  if (compact) {
    return (
      <div
        style={{
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          padding: '40px 20px',
          textAlign: 'center',
        }}
      >
        <Title level={4} style={{ marginBottom: 24 }}>
          {primaryStatusText}
        </Title>
        <Progress
          type="circle"
          percent={isError ? 100 : percent}
          status={isError ? 'exception' : percent === 100 ? 'success' : 'active'}
          strokeColor={isError ? '#ff4d4f' : { '0%': '#6c73ff', '100%': '#8d63ff' }}
          size={140}
          strokeWidth={10}
        />
        <div style={{ marginTop: 24 }}>
          <Text strong style={{ fontSize: 16, color: isError ? '#ff4d4f' : '#5d67ff', display: 'block', marginBottom: 4 }}>
            {helperText}
          </Text>
          <Text type="secondary" style={{ fontSize: 13 }}>
            {`预计剩余：${isError ? '--' : formatEta(progress?.eta)}`}
          </Text>
          {renderStageTimeline({ compact: true })}
        </div>
      </div>
    );
  }

  return (
    <div
      style={{
        height: '100%',
        display: 'flex',
        flexDirection: 'column',
        justifyContent: 'center',
        alignItems: 'center',
        background: '#fff',
        borderRadius: 16,
        padding: 40,
      }}
    >
      <div style={{ width: '100%', maxWidth: 760, textAlign: 'center' }}>
        <Title level={3} style={{ marginBottom: 8 }}>
          {primaryStatusText}
        </Title>
        <Text type="secondary" style={{ fontSize: 15 }}>
          {helperText}
        </Text>

        <div
          style={{
            marginTop: 28,
            marginBottom: 32,
            padding: '20px 24px',
            borderRadius: 20,
            background: isError ? 'linear-gradient(135deg, #fff2f0, #fff7f6)' : 'linear-gradient(135deg, #f5f7ff, #faf8ff)',
            border: `1px solid ${isError ? '#ffccc7' : '#d9ddff'}`,
          }}
        >
          <Text type="secondary" style={{ fontSize: 13 }}>
            当前进度
          </Text>
          <div
            style={{
              marginTop: 6,
              fontSize: 40,
              lineHeight: 1,
              fontWeight: 700,
              color: isError ? '#cf1322' : '#4f46e5',
            }}
          >
            {progressText}
          </div>
          <Progress
            percent={isError ? 100 : percent}
            status={isError ? 'exception' : percent === 100 ? 'success' : 'active'}
            strokeColor={isError ? '#ff4d4f' : '#6c73ff'}
            showInfo={false}
            style={{ marginTop: 18, marginBottom: 12 }}
          />
          <Text type="secondary" style={{ fontSize: 13 }}>
            {`预计剩余：${isError ? '--' : formatEta(progress?.eta)}`}
          </Text>
        </div>

        {renderStageTimeline()}

        <Divider style={{ margin: '32px 0 24px' }} />
        <Row gutter={24}>
          <Col span={8}>
            <Space direction="vertical" size={0}>
              <Text type="secondary">当前阶段</Text>
              <Title level={4} style={{ margin: 0 }}>
                {DETAIL_STAGE_STEP_DISPLAY_ITEMS[currentStageIndex]?.label || primaryStatusText}
              </Title>
            </Space>
          </Col>
          <Col span={8}>
            <Space direction="vertical" size={0}>
              <Text type="secondary">任务状态</Text>
              <Title level={4} style={{ margin: 0, color: isError ? '#ff4d4f' : '#52c41a' }}>
                {isError ? '失败' : isCompletedStage ? '已完成' : '处理中'}
              </Title>
            </Space>
          </Col>
          <Col span={8}>
            <Space direction="vertical" size={0}>
              <Text type="secondary">可查看内容</Text>
              <Title level={4} style={{ margin: 0, color: '#4f46e5' }}>
                {[
                  progress?.unifiedStatus?.canViewTranscript ? '转录' : null,
                  progress?.unifiedStatus?.canViewAiChapters ? '目录' : null,
                  progress?.unifiedStatus?.canViewSummary ? '总结' : null,
                ].filter(Boolean).join(' / ') || '暂无'}
              </Title>
            </Space>
          </Col>
        </Row>
        <div style={{ marginTop: 20 }}>
          <Text type="secondary">处理进度仅作辅助展示，阶段状态以统一状态机为准。</Text>
        </div>
      </div>
    </div>
  );
};

const SpeakerEditor: React.FC<{
  meetingId: number;
  speakerId: string;
  initialName: string;
  initialLabel: string;
  onSuccess: () => void;
}> = ({ meetingId, speakerId, initialName, initialLabel, onSuccess }) => {
  const [name, setName] = useState(initialName || speakerId);
  const [label, setLabel] = useState(initialLabel);
  const [loading, setLoading] = useState(false);
  const { message } = App.useApp();
  const { items: speakerLabels } = useDict('biz_speaker_label');


  const handleSave = async (event: React.MouseEvent) => {
    event.stopPropagation();
    setLoading(true);
    try {
      await updateSpeakerInfo({ meetingId, speakerId, newName: name, label });
      message.success('发言人信息已更新');
      onSuccess();
    } catch (error) {
      console.error(error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div style={{ width: 250, padding: '8px 4px' }} onClick={(event) => event.stopPropagation()}>
      <div style={{ marginBottom: 12 }}>
        <Text type="secondary">发言人姓名</Text>
        <Input value={name} onChange={(event) => setName(event.target.value)} placeholder="输入姓名" size="small" style={{ marginTop: 4 }} />
      </div>
      <div style={{ marginBottom: 16 }}>
        <Text type="secondary">角色标签</Text>
        <Select value={label} onChange={setLabel} placeholder="选择角色" style={{ width: '100%', marginTop: 4 }} size="small" allowClear>
          {speakerLabels.map((item) => (
            <Select.Option key={item.itemValue} value={item.itemValue}>
              {item.itemLabel}
            </Select.Option>
          ))}
        </Select>
      </div>
      <Button type="primary" size="small" block onClick={handleSave} loading={loading}>
        同步到全局
      </Button>
    </div>
  );
};

const renderContentWithHighlight = (content: string, keyword: string) => {
  if (!keyword || !content.toLowerCase().includes(keyword.toLowerCase())) {
    return content;
  }

  const parts = content.split(new RegExp(`(${keyword})`, 'gi'));
  return (
    <>
      {parts.map((part, i) => (
        part.toLowerCase() === keyword.toLowerCase() ? (
          <span key={i} className="highlight-text">{part}</span>
        ) : part
      ))}
    </>
  );
};

type ActiveTranscriptRowProps = {
  item: MeetingTranscriptVO;
  meetingId: number;
  isOwner: boolean;
  isEditing: boolean;
  isSaving: boolean;
  speakerLabelMap: Map<string, string>;
  onPlay: (timeMs: number) => void;
  onStartEdit: (item: MeetingTranscriptVO, event: React.MouseEvent) => void;
  onDraftBlur: (item: MeetingTranscriptVO, value: string) => void;
  onDraftKeyDown: (item: MeetingTranscriptVO, value: string, event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
  onSpeakerUpdated: () => void;
  registerRow: (id: number, node: HTMLDivElement | null) => void;
  isActive: boolean;
  isLinkedHighlight: boolean;
  audioPlaying: boolean;
  highlightKeyword?: string;
};

const ActiveTranscriptRow = React.memo<ActiveTranscriptRowProps>(({
  item,
  meetingId,
  isOwner,
  isEditing,
  isSaving,
  speakerLabelMap,
  onPlay,
  onStartEdit,
  onDraftBlur,
  onDraftKeyDown,
  onSpeakerUpdated,
  registerRow,
  isActive,
  isLinkedHighlight,
  audioPlaying,
  highlightKeyword = '',
}) => {
  const [draftValue, setDraftValue] = useState(item.content);
  const rowRef = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    if (isEditing) {
      setDraftValue(item.content);
    }
  }, [isEditing, item.content]);

  useEffect(() => {
    if ((isActive && audioPlaying) || (highlightKeyword && item.content.toLowerCase().includes(highlightKeyword.toLowerCase()))) {
      if (rowRef.current) {
        rowRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
      }
    }
  }, [isActive, audioPlaying, highlightKeyword, item.content]);

  const speakerTagLabel = item.speakerLabel ? (speakerLabelMap.get(item.speakerLabel) || item.speakerLabel) : '';

  return (
    <List.Item
      className={`transcript-row ${isActive ? 'active' : ''} ${isLinkedHighlight ? 'linked' : ''}`}
      onClick={() => onPlay(item.startTime)}
    >
      <div
        className="transcript-entry"
        ref={(node) => {
          rowRef.current = node;
          registerRow(item.id, node);
        }}
      >
        <Avatar icon={<UserOutlined />} className="transcript-avatar" />
        <div className="transcript-content-wrap">
          <div className="transcript-meta">
            {isOwner ? (
              <Popover
                content={(
                  <SpeakerEditor
                    meetingId={meetingId}
                    speakerId={item.speakerId}
                    initialName={item.speakerName}
                    initialLabel={item.speakerLabel}
                    onSuccess={onSpeakerUpdated}
                  />
                )}
                title="编辑发言人"
                trigger="click"
              >
                <span className="transcript-speaker editable" onClick={(event) => event.stopPropagation()}>
                  {item.speakerName || item.speakerId || '发言人'} <EditOutlined style={{ fontSize: 12 }} />
                </span>
              </Popover>
            ) : (
              <span className="transcript-speaker">{item.speakerName || item.speakerId || '发言人'}</span>
            )}
            <Text type="secondary">{formatTime(item.startTime)}</Text>
            {speakerTagLabel && <Tag color="blue">{speakerTagLabel}</Tag>}
          </div>
          {isEditing ? (
            <div
              className={`transcript-bubble transcript-bubble-editing ${isSaving ? 'is-saving' : ''}`}
              onClick={(event) => event.stopPropagation()}
            >
              <Input.TextArea
                autoFocus
                value={draftValue}
                style={{ width: '100%' }}
                onChange={(event) => setDraftValue(event.target.value)}
                onKeyDown={(event) => onDraftKeyDown(item, draftValue, event)}
                onBlur={(event) => {
                  event.stopPropagation();
                  onDraftBlur(item, draftValue);
                }}
                autoSize={{ minRows: 1, maxRows: 8 }}
                className="transcript-bubble-input"
                bordered={false}
              />
            </div>
          ) : (
            <div
              className={`transcript-bubble ${isOwner ? 'editable' : ''}`}
              onDoubleClick={isOwner ? (event) => onStartEdit(item, event) : undefined}
            >
              {renderContentWithHighlight(item.content, highlightKeyword)}
            </div>
          )}
        </div>
      </div>
    </List.Item>
  );
}, (prevProps, nextProps) => (
  prevProps.item === nextProps.item
  && prevProps.meetingId === nextProps.meetingId
  && prevProps.isOwner === nextProps.isOwner
  && prevProps.isEditing === nextProps.isEditing
  && prevProps.isSaving === nextProps.isSaving
  && prevProps.speakerLabelMap === nextProps.speakerLabelMap
  && prevProps.onPlay === nextProps.onPlay
  && prevProps.onStartEdit === nextProps.onStartEdit
  && prevProps.onDraftBlur === nextProps.onDraftBlur
  && prevProps.onDraftKeyDown === nextProps.onDraftKeyDown
  && prevProps.onSpeakerUpdated === nextProps.onSpeakerUpdated
  && prevProps.registerRow === nextProps.registerRow
  && prevProps.isActive === nextProps.isActive
  && prevProps.isLinkedHighlight === nextProps.isLinkedHighlight
  && prevProps.audioPlaying === nextProps.audioPlaying
  && prevProps.highlightKeyword === nextProps.highlightKeyword
));


const MeetingDetail: React.FC = () => {
  const { message } = App.useApp();
  const { id } = useParams<{ id: string }>();
  const navigate = useNavigate();
  const [form] = Form.useForm();
  const [summaryForm] = Form.useForm();

  const [meeting, setMeeting] = useState<MeetingVO | null>(null);
  const [transcripts, setTranscripts] = useState<MeetingTranscriptVO[]>([]);
  const [meetingChapters, setMeetingChapters] = useState<MeetingChapterVO[]>([]);
  const [loading, setLoading] = useState(true);
  const [editVisible, setEditVisible] = useState(false);
  const [summaryVisible, setSummaryVisible] = useState(false);
  const [actionLoading, setActionLoading] = useState(false);
  const [downloadLoading, setDownloadLoading] = useState<'pdf' | 'word' | 'transcript' | null>(null);
  const [isEditingSummary, setIsEditingSummary] = useState(false);
  const [summaryDraft, setSummaryDraft] = useState('');
  const [selectedKeywords, setSelectedKeywords] = useState<string[]>([]);
  const [workspaceTab, setWorkspaceTab] = useState<WorkspaceTab>('transcript');
  const [addingHotwords, setAddingHotwords] = useState(false);
  const [editingTranscriptId, setEditingTranscriptId] = useState<number | null>(null);
  const [savingTranscriptId, setSavingTranscriptId] = useState<number | null>(null);
  const [llmModels, setLlmModels] = useState<AiModelVO[]>([]);
  const [prompts, setPrompts] = useState<PromptTemplateVO[]>([]);
  const [userList, setUserList] = useState<SysUser[]>([]);
  const { items: speakerLabels } = useDict('biz_speaker_label');
  const [sharePopoverOpen, setSharePopoverOpen] = useState(false);
  const [shareSaving, setShareSaving] = useState(false);
  const [sharePasswordEnabled, setSharePasswordEnabled] = useState(false);
  const [sharePasswordDraft, setSharePasswordDraft] = useState('');
  const [highlightKeyword, setHighlightKeyword] = useState('');
  const [linkedTranscriptIds, setLinkedTranscriptIds] = useState<number[]>([]);
  const [linkedChapterKey, setLinkedChapterKey] = useState<string | null>(null);
  const [meetingShareBaseUrl, setMeetingShareBaseUrl] = useState('');

  const audioRef = useRef<HTMLAudioElement>(null);
  const [audioCurrentTime, setAudioCurrentTime] = useState(0);
  const [audioDuration, setAudioDuration] = useState(0);
  const [audioPlaying, setAudioPlaying] = useState(false);
  const [audioPlaybackRate, setAudioPlaybackRate] = useState(1);

  const emptyTranscriptNoticeShownRef = useRef<number | null>(null);
  const audioPlaybackErrorShownRef = useRef<string | null>(null);

  const transcriptItemRefs = useRef<Record<number, HTMLDivElement | null>>({});
  const pendingTranscriptScrollIdRef = useRef<number | null>(null);
  const leftColumnRef = useRef<HTMLDivElement>(null);
  const transcriptSectionRef = useRef<HTMLDivElement>(null);
  const [showFloatingTranscriptPlayer, setShowFloatingTranscriptPlayer] = useState(false);
  const [floatingTranscriptPlayerLayout, setFloatingTranscriptPlayerLayout] = useState<{ left: number; width: number } | null>(null);
  const [generationProgress, setGenerationProgress] = useState<MeetingProgress | null>(null);
  const autoOpenedCatalogAttemptRef = useRef<string | null>(null);

  const fetchData = useCallback(async (meetingId: number) => {
    try {
      const [detailRes, transcriptRes, chapterRes, shareConfigRes, progressRes] = await Promise.all([
        getMeetingDetail(meetingId),
        getTranscripts(meetingId),
        getMeetingChapters(meetingId),
        getMeetingShareConfig().catch(() => null),
        getMeetingProgress(meetingId, { suppressErrorToast: true }).catch(() => null),
      ]);
      setMeeting(detailRes.data.data);
      setTranscripts(transcriptRes.data.data || []);
      setMeetingChapters(chapterRes.data.data || []);
      setMeetingShareBaseUrl(shareConfigRes?.data?.data?.h5BaseUrl || '');
      setGenerationProgress(progressRes?.data?.data || null);
    } catch (error) {
      console.error(error);
    } finally {
      setLoading(false);
    }
  }, []);

  const analysis = useMemo(
    () => buildMeetingAnalysis(meeting?.analysis, meeting?.summaryContent, meeting?.tags || ''),
    [meeting?.analysis, meeting?.summaryContent, meeting?.tags],
  );
  const expandKeywords = false;
  const visibleKeywords = expandKeywords ? analysis.keywords : analysis.keywords.slice(0, 9);
  const meetingTags = useMemo(
    () => (meeting?.tags?.split(',').map((item) => item.trim()).filter(Boolean) || []),
    [meeting?.tags],
  );
  const discussionItems = useMemo(() => {
    if (analysis.keyPoints.length) {
      return analysis.keyPoints;
    }
    return analysis.chapters.map((item) => ({
      title: item.title,
      summary: item.summary,
      time: item.time,
    }));
  }, [analysis.chapters, analysis.keyPoints]);
  const speakerLabelMap = useMemo(
    () => new Map(speakerLabels.map((item) => [item.itemValue, item.itemLabel])),
    [speakerLabels],
  );
  const previewAccessPassword = useMemo(() => (meeting?.accessPassword || '').trim(), [meeting?.accessPassword]);
  const playbackAudioUrl = useMemo(() => resolveMeetingPlaybackAudioUrl(meeting), [meeting]);
  const audioDownloadFileName = useMemo(() => getMeetingAudioDownloadName(meeting), [meeting]);
  const audioDownloadFormatLabel = useMemo(
    () => resolveAudioExtension(playbackAudioUrl).toUpperCase(),
    [playbackAudioUrl],
  );
  const keywordItems = useMemo(
    () => (analysis.keywords.length ? visibleKeywords : meetingTags),
    [analysis.keywords.length, meetingTags, visibleKeywords],
  );
  const catalogChapterLinks = useMemo<ChapterTranscriptLink[]>(() => {
    const transcriptIdToIndex = new Map(transcripts.map((item, index) => [item.id, index]));
    const sourceChapters: MeetingChapterVO[] = meetingChapters.length
      ? meetingChapters
      : analysis.chapters.map((item) => ({
          title: item.title,
          time: item.time,
        } as MeetingChapterVO));

    return sourceChapters.map((chapter, index) => {
      let matchedTranscripts: MeetingTranscriptVO[] = [];
      const sourceTranscriptIds = Array.isArray(chapter.sourceTranscriptIds)
        ? chapter.sourceTranscriptIds
            .map((item: number) => Number(item))
            .filter((item: number) => Number.isFinite(item) && transcriptIdToIndex.has(item))
        : [];

      if (sourceTranscriptIds.length) {
        matchedTranscripts = sourceTranscriptIds
          .map((item: number) => transcripts[transcriptIdToIndex.get(item)!])
          .filter(Boolean);
      } else if (chapter.startTranscriptId && chapter.endTranscriptId) {
        const startIndex = transcriptIdToIndex.get(Number(chapter.startTranscriptId));
        const endIndex = transcriptIdToIndex.get(Number(chapter.endTranscriptId));
        if (startIndex !== undefined && endIndex !== undefined) {
          matchedTranscripts = transcripts.slice(Math.min(startIndex, endIndex), Math.max(startIndex, endIndex) + 1);
        }
      } else {
        const startMs = typeof chapter.startTime === 'number' ? chapter.startTime : parseChapterTimeToMs(chapter.time);
        const nextChapterStartMs = sourceChapters
          .slice(index + 1)
          .map((item: MeetingChapterVO) => (typeof item.startTime === 'number' ? item.startTime : parseChapterTimeToMs(item.time)))
          .find((item): item is number => item !== null && startMs !== null && item > startMs);

        if (startMs !== null) {
          const firstTranscriptIndex = transcripts.findIndex((item) => item.endTime > startMs);
          if (firstTranscriptIndex >= 0) {
            const lastTranscriptIndex = nextChapterStartMs === undefined
              ? transcripts.length
              : transcripts.findIndex((item) => item.startTime >= nextChapterStartMs);
            matchedTranscripts = transcripts.slice(
              firstTranscriptIndex,
              lastTranscriptIndex >= 0 ? lastTranscriptIndex : transcripts.length,
            );
          }
        }
      }

      return {
        key: `${chapter.chapterNo ?? index}-${chapter.title || 'chapter'}`,
        title: chapter.title || `章节 ${index + 1}`,
        timeLabel: chapter.time || '--:--',
        transcriptIds: matchedTranscripts.map((item) => item.id),
        firstTranscriptId: matchedTranscripts[0]?.id ?? null,
        firstTranscriptStartTime: matchedTranscripts[0]?.startTime ?? null,
      };
    });
  }, [analysis.chapters, meetingChapters, transcripts]);
  const sharePreviewUrl = useMemo(() => {
    const meetingId = meeting?.id ?? (id ? Number(id) : NaN);
    return buildMeetingPreviewUrl(meetingShareBaseUrl, meetingId);
  }, [meetingShareBaseUrl, meeting?.id, id]);
  const summaryModelDisplayName = useMemo(() => {
    const matchedModel = llmModels.find((item) => item.id === meeting?.summaryModelId);
    if (matchedModel?.modelName) {
      return matchedModel.modelName;
    }
    if (meeting?.summaryModelId) {
      return `模型 #${meeting.summaryModelId}`;
    }
    return '未配置';
  }, [llmModels, meeting?.summaryModelId]);
  const promptDisplayName = useMemo(() => {
    const matchedPrompt = prompts.find((item) => item.id === meeting?.promptId);
    if (matchedPrompt?.templateName) {
      return matchedPrompt.templateName;
    }
    if (meeting?.promptId) {
      return `模板 #${meeting.promptId}`;
    }
    return '未配置';
  }, [meeting?.promptId, prompts]);
  const hotWordGroupDisplayName = useMemo(() => {
    if (meeting?.hotWordGroupName?.trim()) {
      return meeting.hotWordGroupName.trim();
    }
    if (meeting?.hotWordGroupId) {
      return `热词组 #${meeting.hotWordGroupId}`;
    }
    return '未使用';
  }, [meeting?.hotWordGroupId, meeting?.hotWordGroupName]);

  const isOwner = useMemo(() => {
    if (!meeting) return false;
    const profileStr = sessionStorage.getItem('userProfile');
    if (profileStr) {
      const profile = JSON.parse(profileStr);
      return profile.isPlatformAdmin === true || profile.isTenantAdmin === true || profile.userId === meeting.creatorId;
    }
    return false;
  }, [meeting]);

  const aiCatalogEnabled = meeting?.aiCatalogEnabled !== false;
  const canRetrySummary = isOwner
    && transcripts.length > 0
    && meeting?.status !== 1
    && meeting?.status !== 2
    && meeting?.latestSummaryAttemptStatus !== 3
    && (!aiCatalogEnabled || meeting?.latestChapterAttemptStatus !== 3);
  const canRetryTranscription = isOwner && meeting?.status === 4 && transcripts.length === 0 && !!meeting?.audioUrl;
  const canRetryFailedSummaryTask = isOwner && meeting?.latestSummaryAttemptStatus === 3 && meeting?.status !== 2;
  const canRetryFailedChapterTask = aiCatalogEnabled && isOwner && meeting?.latestChapterAttemptStatus === 3 && meeting?.status !== 2;
  
  const canRetrySchedule = isOwner && meeting?.status === 0 && (!generationProgress || generationProgress.percent <= 0) && !!generationProgress?.queuedAt && dayjs().diff(dayjs(generationProgress.queuedAt)) >= QUEUED_RETRY_THRESHOLD_MS;

  const handleRetrySchedule = async () => {
    if (!meeting) return;
    setActionLoading(true);
    try {
      const response = await retryScheduleMeeting(meeting.id);
      if (response.data?.data) {
        message.success("已触发重新调度");
      } else {
        message.info("当前没有可重新调度的排队任务");
      }
      await fetchData(meeting.id);
    } catch {
      message.error("重新调度失败");
    } finally {
      setActionLoading(false);
    }
  };

  const hasSummaryContent = Boolean(meeting?.summaryContent?.trim());
  const hasCatalogContent = aiCatalogEnabled && catalogChapterLinks.length > 0;
  const generationFailureNotice = useMemo<MeetingStateNotice | null>(() => {
    if (!meeting || meeting.status !== 4) {
      return null;
    }

    const hasFallbackContent = hasSummaryContent || hasCatalogContent;
    if (aiCatalogEnabled && meeting.latestChapterAttemptStatus === 3) {
      const detail = meeting.latestChapterAttemptErrorMsg || '章节生成失败';
      return {
        title: hasFallbackContent ? '历史内容仍可查看' : '本次 AI 目录生成失败',
        description: hasFallbackContent
          ? `最近一次 AI 目录生成失败，当前展示的是最近一次成功生成的内容。失败原因：${detail}`
          : `AI 目录生成失败，当前没有可展示的历史目录或总结内容。失败原因：${detail}`,
        type: hasFallbackContent ? 'info' : 'warning',
        hasFallbackContent,
        scope: 'catalog',
      };
    }

    if (meeting.latestSummaryAttemptStatus === 3) {
      const detail = meeting.latestSummaryAttemptErrorMsg || '总结生成失败';
      return {
        title: hasFallbackContent ? '历史总结可用' : '本次总结生成失败',
        description: hasFallbackContent
          ? `最近一次重新总结失败，当前展示的是最近一次成功生成的总结内容。失败原因：${detail}`
          : `总结生成失败，当前没有可展示的历史总结内容。失败原因：${detail}`,
        type: hasFallbackContent ? 'info' : 'warning',
        hasFallbackContent,
        scope: 'summary',
      };
    }

    return {
      title: '会议处理异常',
      description: '会议在处理过程中遇到了问题。您可以尝试重新发起识别或总结。',
      type: hasFallbackContent ? 'info' : 'warning',
      hasFallbackContent,
      scope: 'global',
    };
  }, [aiCatalogEnabled, hasCatalogContent, hasSummaryContent, meeting]);
  const summaryPanelNotice = useMemo<MeetingStateNotice | null>(() => {
    if (!meeting || !hasSummaryContent) {
      return null;
    }
    if (meeting.status === 2) {
      return {
        title: '正在生成新版总结',
        description: '当前展示的是上一版已成功生成的总结，系统会在新版完成后自动替换。',
        type: 'info',
        hasFallbackContent: true,
        scope: 'summary',
      };
    }
    return null;
  }, [generationFailureNotice, hasSummaryContent, meeting]);
  const catalogPanelNotice = useMemo<MeetingStateNotice | null>(() => {
    if (!aiCatalogEnabled || !generationFailureNotice || generationFailureNotice.scope !== 'catalog') {
      return null;
    }
    if (generationFailureNotice.hasFallbackContent && hasCatalogContent) {
      return {
        title: '当前展示的是历史成功目录',
        description: generationFailureNotice.description,
        type: generationFailureNotice.type,
        hasFallbackContent: true,
        scope: 'catalog',
      };
    }
    return generationFailureNotice;
  }, [aiCatalogEnabled, generationFailureNotice, hasCatalogContent]);
  const emptyTranscriptFailureNotice = useMemo(() => {
    if (!meeting || meeting.status !== 4 || transcripts.length > 0) {
      return null;
    }

    return {
      title: '识别已结束，但没有生成可用转录',
      description: canRetryTranscription
        ? 'ASR 调用已返回，但当前没有写入任何转录文本，所以列表状态仍显示失败。你可以直接重新识别，或先检查录音是否静音、时长过短、音量过低。'
        : 'ASR 调用已返回，但当前没有写入任何转录文本，所以列表状态仍显示失败。建议先检查录音是否静音、时长过短、音量过低或音质异常。',
      hint: canRetryTranscription ? '可以直接点击“重新识别”继续处理。' : '当前没有可重试的音频入口，请先确认原始录音文件是否有效。',
    };
  }, [canRetryTranscription, meeting, transcripts.length]);

  useEffect(() => {
    if (meeting?.status !== 1 && meeting?.status !== 2) {
      setGenerationProgress(null);
      autoOpenedCatalogAttemptRef.current = null;
    }
  }, [meeting?.id, meeting?.status]);

  useEffect(() => {
    if (!aiCatalogEnabled && workspaceTab === 'catalog') {
      setWorkspaceTab('transcript');
    }
  }, [aiCatalogEnabled, workspaceTab]);

  useEffect(() => {
    const attemptKey = String(meeting?.latestChapterAttemptTaskId ?? meeting?.latestSummaryAttemptTaskId ?? '');
    if (meeting?.status !== 2 || !attemptKey) {
      return;
    }
    if (!aiCatalogEnabled || (generationProgress?.percent ?? 0) < 88 || catalogChapterLinks.length === 0) {
      return;
    }
    if (autoOpenedCatalogAttemptRef.current === attemptKey) {
      return;
    }
    autoOpenedCatalogAttemptRef.current = attemptKey;
    setWorkspaceTab('catalog');
  }, [
    aiCatalogEnabled,
    catalogChapterLinks.length,
    generationProgress?.percent,
    meeting?.latestChapterAttemptTaskId,
    meeting?.latestSummaryAttemptTaskId,
    meeting?.status,
  ]);

  useEffect(() => {
    if (!playbackAudioUrl) {
      setShowFloatingTranscriptPlayer(false);
      setFloatingTranscriptPlayerLayout(null);
      return undefined;
    }

    const updateFloatingPlayerState = () => {
      const target = transcriptSectionRef.current;
      if (!target) {
        setShowFloatingTranscriptPlayer(false);
        setFloatingTranscriptPlayerLayout(null);
        return;
      }

      const rect = target.getBoundingClientRect();
      const rootRect = leftColumnRef.current?.getBoundingClientRect();

      setFloatingTranscriptPlayerLayout({
        left: rect.left,
        width: rect.width,
      });

      if (rootRect) {
        const isVisible = rect.bottom > rootRect.top + 80 && rect.top < rootRect.bottom - 40;
        setShowFloatingTranscriptPlayer(isVisible);
        return;
      }

      const isVisible = rect.bottom > 120 && rect.top < window.innerHeight - 80;
      setShowFloatingTranscriptPlayer(isVisible);
    };

    updateFloatingPlayerState();

    const target = transcriptSectionRef.current;
    const root = leftColumnRef.current;
    const resizeObserver = target ? new ResizeObserver(() => updateFloatingPlayerState()) : null;
    if (target && resizeObserver) {
      resizeObserver.observe(target);
    }

    window.addEventListener('resize', updateFloatingPlayerState);
    window.addEventListener('scroll', updateFloatingPlayerState, { passive: true });
    root?.addEventListener('scroll', updateFloatingPlayerState, { passive: true });

    return () => {
      window.removeEventListener('resize', updateFloatingPlayerState);
      window.removeEventListener('scroll', updateFloatingPlayerState);
      root?.removeEventListener('scroll', updateFloatingPlayerState);
      resizeObserver?.disconnect();
    };
  }, [meeting?.status, playbackAudioUrl]);

  useEffect(() => {
    if (!id) return;
    fetchData(Number(id));
    loadAiConfigs();
    loadUsers();
  }, [id, fetchData]);

  useEffect(() => {
    setSelectedKeywords((current) => current.filter((item) => analysis.keywords.includes(item)));
  }, [analysis.keywords]);

  useEffect(() => {
    if (workspaceTab !== 'transcript') {
      return;
    }

    const pendingId = pendingTranscriptScrollIdRef.current;
    if (!pendingId) {
      return;
    }

    const frameId = window.requestAnimationFrame(() => {
      const target = transcriptItemRefs.current[pendingId];
      if (target) {
        target.scrollIntoView({ behavior: 'smooth', block: 'center' });
        pendingTranscriptScrollIdRef.current = null;
      }
    });

    return () => window.cancelAnimationFrame(frameId);
  }, [workspaceTab, transcripts, linkedTranscriptIds]);

  useEffect(() => {
    if (meeting?.audioSaveStatus === 'FAILED') {
      message.warning(meeting.audioSaveMessage || '实时会议已完成，但音频保存失败，当前无法播放会议录音。转写和总结不受影响。');
    }
  }, [meeting?.id, meeting?.audioSaveStatus, meeting?.audioSaveMessage]);

  useEffect(() => {
    if (!meeting?.id || !emptyTranscriptFailureNotice) {
      return;
    }
    if (emptyTranscriptNoticeShownRef.current === meeting.id) {
      return;
    }

    message.warning({
      content: emptyTranscriptFailureNotice.title,
      duration: 4,
    });
    emptyTranscriptNoticeShownRef.current = meeting.id;
  }, [emptyTranscriptFailureNotice, meeting?.id, message]);

  useEffect(() => {
    if (!sharePopoverOpen) {
      return;
    }
    const normalizedPassword = normalizeAccessPasswordInput(meeting?.accessPassword);
    setSharePasswordEnabled(!!normalizedPassword);
    setSharePasswordDraft(normalizedPassword);
  }, [sharePopoverOpen, meeting?.accessPassword]);

  const loadAiConfigs = async () => {
    try {
      const [modelRes, promptRes, defaultRes] = await Promise.all([
        getAiModelPage({ current: 1, size: 100, type: 'LLM' }),
        getPromptPage({ current: 1, size: 100 }),
        getAiModelDefault('LLM'),
      ]);
      setLlmModels((modelRes.data?.data?.records || []).filter((item) => item.status === 1));
      setPrompts((promptRes.data?.data?.records || []).filter((item) => item.status === 1));
      summaryForm.setFieldsValue({ summaryModelId: defaultRes.data.data?.id });
    } catch {
      // ignore
    }
  };

  const loadUsers = async () => {
    try {
      const users = await listUsers();
      setUserList(users || []);
    } catch {
      // ignore
    }
  };

  const handleEditMeeting = () => {
    if (!meeting || !isOwner) return;
    form.setFieldsValue({
      ...meeting,
      tags: meeting.tags?.split(',').filter(Boolean),
    });
    setEditVisible(true);
  };

  const handleUpdateBasic = async () => {
    const values = await form.validateFields();
    setActionLoading(true);
    try {
      await updateMeetingBasic({
        ...values,
        meetingId: meeting?.id,
        tags: values.tags?.join(','),
      });
      message.success('会议信息已更新');
      setEditVisible(false);
      fetchData(Number(id));
    } catch (error) {
      console.error(error);
    } finally {
      setActionLoading(false);
    }
  };

  const handleSaveSummary = async () => {
    setActionLoading(true);
    try {
      await updateMeetingSummary({
        meetingId: Number(id),
        summaryContent: summaryDraft,
      });
      message.success('总结内容已更新');
      setIsEditingSummary(false);
      fetchData(Number(id));
    } catch (error) {
      console.error(error);
    } finally {
      setActionLoading(false);
    }
  };

  const handleReSummary = async () => {
    const values = await summaryForm.validateFields();
    setActionLoading(true);
    try {
      await reSummary({
        meetingId: Number(id),
        summaryModelId: values.summaryModelId,
        promptId: values.promptId,
        userPrompt: values.userPrompt,
        summaryDetailLevel: values.summaryDetailLevel as SummaryDetailLevel,
      });
      setMeeting((current) => current ? {
        ...current,
        status: 2,
        latestSummaryAttemptStatus: 0,
        latestSummaryAttemptErrorMsg: undefined,
      } : current);
      setGenerationProgress({
        percent: 90,
        message: '已重新发起总结任务',
        updateAt: Date.now(),
        eta: 0,
      });
      message.success('操作成功');
      await fetchData(Number(id));
    } catch (error) {
      console.error(error);
    } finally {
      setActionLoading(false);
    }
  };

  const handleOpenSummaryDrawer = () => {
    summaryForm.setFieldsValue({
      summaryModelId:
        summaryForm.getFieldValue('summaryModelId') ??
        meeting?.summaryModelId ??
        llmModels.find((model) => model.isDefault === 1)?.id ??
        llmModels[0]?.id,
      promptId:
        summaryForm.getFieldValue('promptId') ??
        meeting?.promptId ??
        prompts[0]?.id,
      userPrompt: meeting?.lastUserPrompt ?? '',
      summaryDetailLevel:
        summaryForm.getFieldValue('summaryDetailLevel') ??
        meeting?.summaryDetailLevel ??
        'STANDARD',
    });
    setSummaryVisible(true);
  };

  const seekTo = useCallback((timeMs: number) => {
    if (!audioRef.current) return;
    audioRef.current.currentTime = timeMs / 1000;
    audioRef.current.play();
  }, []);

  const handleTranscriptRowPlay = useCallback((timeMs: number) => {
    setLinkedTranscriptIds([]);
    setLinkedChapterKey(null);
    seekTo(timeMs);
  }, [seekTo]);

  const handleLocateChapterTranscript = useCallback((chapterIndex: number) => {
    const targetLink = catalogChapterLinks[chapterIndex];
    if (!targetLink || !targetLink.transcriptIds.length || targetLink.firstTranscriptId === null || targetLink.firstTranscriptStartTime === null) {
      message.warning('当前章节暂未匹配到可关联的转录原文');
      return;
    }

    setWorkspaceTab('transcript');
    setHighlightKeyword('');
    setLinkedTranscriptIds(targetLink.transcriptIds);
    setLinkedChapterKey(targetLink.key);
    pendingTranscriptScrollIdRef.current = targetLink.firstTranscriptId;
    seekTo(targetLink.firstTranscriptStartTime);
  }, [catalogChapterLinks, message, seekTo]);

  const handleRetryTranscription = async () => {
    setActionLoading(true);
    try {
      await retryMeetingTranscription(Number(id));
      message.success('已重新提交识别任务');
      await fetchData(Number(id));
    } catch (error) {
      console.error(error);
    } finally {
      setActionLoading(false);
    }
  };

  const handleRetrySummaryTask = async () => {
    setActionLoading(true);
    try {
      await retryMeetingSummary(Number(id));
      message.success('已重新提交总结任务');
      setMeeting((current) => current ? {
        ...current,
        status: 2,
        latestSummaryAttemptStatus: 0,
        latestSummaryAttemptErrorMsg: undefined,
      } : current);
      await fetchData(Number(id));
    } catch (error) {
      console.error(error);
    } finally {
      setActionLoading(false);
    }
  };

  const handleRetryChapterTask = async () => {
    setActionLoading(true);
    try {
      await retryMeetingChapter(Number(id));
      message.success('已重新提交 AI 目录任务');
      setMeeting((current) => current ? {
        ...current,
        status: 2,
        latestChapterAttemptStatus: 0,
        latestChapterAttemptErrorMsg: undefined,
      } : current);
      await fetchData(Number(id));
    } catch (error) {
      console.error(error);
    } finally {
      setActionLoading(false);
    }
  };

  const handleKeywordToggle = (keyword: string, checked: boolean) => {
    setSelectedKeywords((current) => {
      if (checked) {
        return current.includes(keyword) ? current : [...current, keyword];
      }
      return current.filter((item) => item !== keyword);
    });
  };

  const handleAddSelectedHotwords = async () => {
    const keywords = selectedKeywords.map((item) => item.trim()).filter(Boolean);
    if (!keywords.length) {
      message.warning('请先选择关键词');
      return;
    }

    setAddingHotwords(true);
    try {
      const existingRes = await getHotWordPage({ current: 1, size: 500, word: '' });
      const existingWords = new Set(
        (existingRes.data?.data?.records || [])
          .map((item) => item.word?.trim())
          .filter(Boolean),
      );
      const toCreate = keywords.filter((item) => !existingWords.has(item));

      if (!toCreate.length) {
        message.info('所选关键词已存在于热词库');
        return;
      }

      await Promise.all(
        toCreate.map((word) =>
          (async () => {
            let pinyinList: string[] = [];
            try {
              const pinyinRes = await getPinyinSuggestion(word);
              pinyinList = (pinyinRes.data?.data || []).map((item) => item.trim()).filter(Boolean);
            } catch {
              pinyinList = [];
            }
            return saveHotWord({
              word,
              pinyinList,
              matchStrategy: 1,
              category: '',
              weight: 2,
              status: 1,
            remark: meeting ? `来源于会议：${meeting.title}` : '来源于会议关键词',
            });
          })(),
        ),
      );

      const skippedCount = keywords.length - toCreate.length;
      message.success(
        skippedCount > 0
          ? `已新增 ${toCreate.length} 个热词，跳过 ${skippedCount} 个重复项`
          : `已新增 ${toCreate.length} 个热词`,
      );
      setSelectedKeywords([]);
    } catch (error) {
      console.error(error);
    } finally {
      setAddingHotwords(false);
    }
  };

  const handleStartEditTranscript = useCallback((item: MeetingTranscriptVO, event: React.MouseEvent) => {
    event.stopPropagation();
    setEditingTranscriptId(item.id);
  }, []);

  const handleCancelEditTranscript = useCallback((event?: React.SyntheticEvent) => {
    event?.stopPropagation();
    setEditingTranscriptId(null);
  }, []);

  const handleSaveTranscript = useCallback(async (item: MeetingTranscriptVO, nextContent?: string) => {
    const content = (nextContent ?? item.content ?? '').trim();
    if (!content) {
      message.warning('转录内容不能为空');
      return;
    }

    if (!meeting) return;
    if (content === (item.content || '').trim()) {
      handleCancelEditTranscript();
      return;
    }

    setSavingTranscriptId(item.id);
    try {
      await updateMeetingTranscript({
        meetingId: meeting.id,
        transcriptId: item.id,
        content,
      });
      message.success('原文已更新，如需同步摘要请重新总结');
      handleCancelEditTranscript();
      await fetchData(meeting.id);
    } catch (error) {
      console.error(error);
    } finally {
      setSavingTranscriptId(null);
    }
  }, [fetchData, handleCancelEditTranscript, meeting]);

  const handleTranscriptDraftKeyDown = useCallback((item: MeetingTranscriptVO, value: string, event: React.KeyboardEvent<HTMLTextAreaElement>) => {
    if (event.key === 'Escape') {
      handleCancelEditTranscript();
      return;
    }
    if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
      event.preventDefault();
      void handleSaveTranscript(item, value);
    }
  }, [handleCancelEditTranscript, handleSaveTranscript]);

  const handleTranscriptDraftBlur = useCallback((item: MeetingTranscriptVO, value: string) => {
    void handleSaveTranscript(item, value);
  }, [handleSaveTranscript]);

  const handleTranscriptSpeakerUpdated = useCallback(() => {
    if (!meeting) return;
    void fetchData(meeting.id);
  }, [fetchData, meeting]);

  const registerTranscriptRow = useCallback((transcriptId: number, node: HTMLDivElement | null) => {
    transcriptItemRefs.current[transcriptId] = node;
  }, []);

  const handleAudioPlaybackError = useCallback(() => {
    const currentAudioUrl = playbackAudioUrl || '';
    if (!currentAudioUrl || audioPlaybackErrorShownRef.current === currentAudioUrl) {
      return;
    }
    const normalizedUrl = currentAudioUrl.split('#')[0]?.split('?')[0]?.toLowerCase() || '';
    const isM4a = normalizedUrl.endsWith('.m4a');
    message.warning(
      isM4a
        ? '当前 m4a 文件在本机浏览器中无法直接播放。已确认文件与服务端响应基本正常，更可能是浏览器对该录音参数或容器实现的兼容性问题。建议优先使用 mp3、wav，或下载到本地播放。'
        : '当前音频文件无法播放，请检查文件是否损坏或格式是否兼容。',
    );
    audioPlaybackErrorShownRef.current = currentAudioUrl;
    setAudioPlaying(false);
  }, [message, playbackAudioUrl]);

  useEffect(() => {
    const audio = audioRef.current;
    if (!audio) return undefined;

    const handleLoadedMetadata = () => {
      setAudioDuration(Number.isFinite(audio.duration) ? audio.duration : 0);
      setAudioCurrentTime(audio.currentTime || 0);
      audio.playbackRate = audioPlaybackRate;
    };
    const handleTimeUpdate = () => {
      setAudioCurrentTime(audio.currentTime || 0);
    };
    const handlePlay = () => setAudioPlaying(true);
    const handlePause = () => setAudioPlaying(false);
    const handleEnded = () => setAudioPlaying(false);
    const handleError = () => handleAudioPlaybackError();

    audio.addEventListener('loadedmetadata', handleLoadedMetadata);
    audio.addEventListener('durationchange', handleLoadedMetadata);
    audio.addEventListener('canplay', handleLoadedMetadata);
    audio.addEventListener('timeupdate', handleTimeUpdate);
    audio.addEventListener('play', handlePlay);
    audio.addEventListener('pause', handlePause);
    audio.addEventListener('ended', handleEnded);
    audio.addEventListener('error', handleError);

    handleLoadedMetadata();

    return () => {
      audio.removeEventListener('loadedmetadata', handleLoadedMetadata);
      audio.removeEventListener('durationchange', handleLoadedMetadata);
      audio.removeEventListener('canplay', handleLoadedMetadata);
      audio.removeEventListener('timeupdate', handleTimeUpdate);
      audio.removeEventListener('play', handlePlay);
      audio.removeEventListener('pause', handlePause);
      audio.removeEventListener('ended', handleEnded);
      audio.removeEventListener('error', handleError);
    };
  }, [audioPlaybackRate, meeting?.status, playbackAudioUrl, handleAudioPlaybackError]);

  const toggleAudioPlayback = () => {
    if (!audioRef.current) return;
    if (audioRef.current.paused) {
      audioRef.current.play();
    } else {
      audioRef.current.pause();
    }
  };

  const handleAudioProgressChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const nextTime = Number(event.target.value || 0);
    setAudioCurrentTime(nextTime);
    if (audioRef.current) {
      audioRef.current.currentTime = nextTime;
    }
  };

  const cyclePlaybackRate = () => {
    if (!audioRef.current) return;
    const rates = [1, 1.25, 1.5, 2];
    const currentIndex = rates.findIndex((item) => item === audioPlaybackRate);
    const nextRate = rates[(currentIndex + 1) % rates.length];
    audioRef.current.playbackRate = nextRate;
    setAudioPlaybackRate(nextRate);
  };

  const getFileNameFromDisposition = (disposition?: string, fallback?: string) => {
    if (!disposition) return fallback || 'summary';
    const utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i);
    if (utf8Match?.[1]) return decodeURIComponent(utf8Match[1]);
    const normalMatch = disposition.match(/filename="?([^";]+)"?/i);
    return normalMatch?.[1] || fallback || 'summary';
  };

  const handleDownloadSummary = async (format: 'pdf' | 'word') => {
    if (!meeting) return;
    if (!meeting.summaryContent) {
      message.warning('当前暂无可下载的 AI 总结');
      return;
    }

    try {
      setDownloadLoading(format);
      const res = await downloadMeetingSummary(meeting.id, format);
      const contentType =
        res.headers['content-type'] ||
        (format === 'pdf'
          ? 'application/pdf'
          : 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');

      if (contentType.includes('application/json')) {
        const text = await (res.data as Blob).text();
        try {
          const json = JSON.parse(text);
          message.error(json?.msg || '下载失败');
        } catch {
          message.error('下载失败');
        }
        return;
      }

      const blob = new Blob([res.data], { type: contentType });
      const url = window.URL.createObjectURL(blob);
      const anchor = document.createElement('a');
      anchor.href = url;
      anchor.download = getFileNameFromDisposition(
        res.headers['content-disposition'],
        `${(meeting.title || 'meeting').replace(/[\\/:*?"<>|\r\n]/g, '_')}-AI纪要.${format === 'pdf' ? 'pdf' : 'docx'}`,
      );
      document.body.appendChild(anchor);
      anchor.click();
      anchor.remove();
      window.URL.revokeObjectURL(url);
    } catch (error) {
      console.error(error);
      message.error(`${format.toUpperCase()} 下载失败`);
    } finally {
      setDownloadLoading(null);
    }
  };

  const handleDownloadTranscript = async () => {
    if (!meeting) return;
    if (transcripts.length === 0) {
      message.warning('当前暂无可下载的会议转录');
      return;
    }

    try {
      setDownloadLoading('transcript');
      const res = await downloadMeetingTranscript(meeting.id);
      const contentType = res.headers['content-type'] || 'text/markdown; charset=UTF-8';

      if (contentType.includes('application/json')) {
        const text = await (res.data as Blob).text();
        try {
          const json = JSON.parse(text);
          message.error(json?.msg || '下载失败');
        } catch {
          message.error('下载失败');
        }
        return;
      }

      const blob = new Blob([res.data], { type: contentType });
      const url = window.URL.createObjectURL(blob);
      const anchor = document.createElement('a');
      anchor.href = url;
      anchor.download = getFileNameFromDisposition(
        res.headers['content-disposition'],
        `${sanitizeDownloadFileName(meeting.title, 'meeting-transcript')}-Transcript.md`,
      );
      document.body.appendChild(anchor);
      anchor.click();
      anchor.remove();
      window.URL.revokeObjectURL(url);
    } catch (error) {
      console.error(error);
      message.error('转录下载失败');
    } finally {
      setDownloadLoading(null);
    }
  };

  const handleDownloadAudio = () => {
    if (!playbackAudioUrl) {
      message.warning('当前暂无可下载的会议录音');
      return;
    }

    const anchor = document.createElement('a');
    anchor.href = playbackAudioUrl;
    anchor.download = audioDownloadFileName;
    anchor.target = '_blank';
    anchor.rel = 'noopener noreferrer';
    document.body.appendChild(anchor);
    anchor.click();
    anchor.remove();
  };

  const handleCopyPreviewLink = async () => {
    if (!sharePreviewUrl) {
      message.error('预览链接暂不可用');
      return;
    }
    try {
      await copyMeetingLink(sharePreviewUrl);
      message.success('预览链接已复制');
    } catch (error) {
      console.error(error);
      message.error('复制预览链接失败');
    }
  };

  const handleOpenPreview = () => {
    if (!sharePreviewUrl) {
      message.error('预览链接暂不可用');
      return;
    }
    window.open(sharePreviewUrl, '_blank', 'noopener,noreferrer');
  };

  const handleSharePopoverOpenChange = (open: boolean) => {
    setSharePopoverOpen(open);
  };

  const handleSharePasswordToggle = (checked: boolean) => {
    setSharePasswordEnabled(checked);
    setSharePasswordDraft((current) => {
      const normalizedCurrent = normalizeAccessPasswordInput(current);
      if (checked) {
        return normalizedCurrent || generateAccessPassword();
      }
      return '';
    });
  };

  const handleRegenerateSharePassword = () => {
    setSharePasswordEnabled(true);
    setSharePasswordDraft(generateAccessPassword());
  };

  const handleSaveShareAccess = async () => {
    if (!meeting) {
      return;
    }
      const normalizedPassword = normalizeAccessPasswordInput(sharePasswordDraft);
      if (sharePasswordEnabled && !ACCESS_PASSWORD_PATTERN.test(normalizedPassword)) {
        message.error('\u8bbf\u95ee\u5bc6\u7801\u9700\u4e3a 4 \u4f4d\u82f1\u6587\u6216\u6570\u5b57');
        return;
      }

    setShareSaving(true);
    try {
      await updateMeetingBasic({
        meetingId: meeting.id,
        accessPassword: sharePasswordEnabled ? normalizedPassword : '',
        summaryModelId: summaryForm.getFieldValue('summaryModelId') ?? meeting.summaryModelId,
      });
      const nextPassword = sharePasswordEnabled ? normalizedPassword : '';
      setMeeting((current) => (current ? { ...current, accessPassword: nextPassword } : current));
      message.success(
        sharePasswordEnabled
          ? '\u9884\u89c8\u5bc6\u7801\u5df2\u66f4\u65b0'
          : '\u9884\u89c8\u5bc6\u7801\u5df2\u5173\u95ed',
      );
      setSharePopoverOpen(false);
    } catch (error) {
      console.error(error);
      message.error('\u4fdd\u5b58\u9884\u89c8\u5bc6\u7801\u5931\u8d25');
    } finally {
      setShareSaving(false);
    }
  };

  const shareQrContent = sharePreviewUrl ? (
    <div className="meeting-share-card">

      {isOwner ? (
        <div className="meeting-share-settings">
          <div className="meeting-share-settings-row">
            <div className="meeting-share-settings-copy">
              <strong>{'\u9884\u89c8\u5bc6\u7801'}</strong>
              <span>
                {sharePasswordEnabled && previewAccessPassword
                  ? `访问密码${previewAccessPassword}`
                  : '关闭后将不需要访问密码'}
              </span>
            </div>
            <Switch
              checked={sharePasswordEnabled}
              checkedChildren={'\u5f00\u542f'}
              unCheckedChildren={'\u5173\u95ed'}
              onChange={handleSharePasswordToggle}
            />
          </div>
          {sharePasswordEnabled ? (
            <div className="meeting-share-settings-actions">
              <Input
                value={sharePasswordDraft}
                maxLength={4}
                placeholder={'\u4f8b\u5982 A7K2'}
                onChange={(event) => setSharePasswordDraft(normalizeAccessPasswordInput(event.target.value))}
              />
              <Button onClick={handleRegenerateSharePassword}>{'\u91cd\u7f6e\u9ed8\u8ba4'}</Button>
            </div>
          ) : null}
          <Button type="primary" loading={shareSaving} onClick={handleSaveShareAccess}>
            {'\u4fdd\u5b58\u5bc6\u7801\u8bbe\u7f6e'}
          </Button>
        </div>
      ) : null}
      <div className="meeting-share-qr-wrap">
        <QRCode
          value={sharePreviewUrl}
          type="svg"
          size={172}
          color="#1677ff"
          bgColor="transparent"
          errorLevel="H"
          bordered={false}
        />
      </div>
      <div className="meeting-share-caption">
        {'\u4f7f\u7528\u624b\u673a\u626b\u7801\u540e\u5c06\u8df3\u8f6c\u5230\u4f1a\u8bae\u9884\u89c8\u9875\uff0c\u82e5\u5df2\u5f00\u542f\u5bc6\u7801\u9700\u624b\u52a8\u8f93\u5165\u3002'}
      </div>
      <div className="meeting-share-link-box">
        <LinkOutlined />
        <span title={sharePreviewUrl}>{sharePreviewUrl}</span>
      </div>
      <div className="meeting-share-actions">
        <Button size="small" icon={<CopyOutlined />} onClick={handleCopyPreviewLink}>
          {'\u590d\u5236\u94fe\u63a5'}
        </Button>
        <Button size="small" type="primary" ghost onClick={handleOpenPreview}>
          {'\u6253\u5f00\u9884\u89c8'}
        </Button>
      </div>
    </div>
  ) : null;

  if (loading) {
    return (
      <div style={{ padding: 24 }}>
        <Skeleton active />
      </div>
    );
  }

  if (!meeting) {
    return (
      <div style={{ padding: 24 }}>
        <Empty description="会议不存在" />
      </div>
    );
  }

  const playerProgressPercent = audioDuration > 0
    ? Math.min(100, Math.max(0, (audioCurrentTime / audioDuration) * 100))
    : 0;

  return (
    <PageContainer title={null} className="meeting-detail-page-v2">
      <SectionCard
        className="meeting-detail-section-card"
        contentClassName="meeting-detail-section-content"
        title={(
          <div className="meeting-detail-title-wrap">
            <div className="meeting-detail-title-icon">
              <FileTextOutlined />
            </div>
            <div className="meeting-detail-title-copy">
              <div className="meeting-detail-title-row">
                <span className="meeting-detail-title-text">{meeting.title}</span>
                {isOwner && (
                  <button type="button" className="meeting-detail-title-edit" onClick={handleEditMeeting} aria-label="编辑会议信息">
                    <EditOutlined />
                  </button>
                )}
              </div>
              <div className="meeting-detail-meta-row">
                <span>
                  <ClockCircleOutlined /> {dayjs(meeting.meetingTime).format('YYYY-MM-DD HH:mm')}
                </span>
                <span>
                  <UserOutlined /> {meeting.participants || '未指定'}
                </span>
              </div>
              <div className="meeting-detail-config-row">
                <span className="meeting-detail-config-item">
                  <Text type="secondary">总结模型</Text>
                  <strong>{summaryModelDisplayName}</strong>
                </span>
                <span className="meeting-detail-config-item">
                  <Text type="secondary">会议模板</Text>
                  <strong>{promptDisplayName}</strong>
                </span>
                <span className="meeting-detail-config-item">
                  <Text type="secondary">热词组</Text>
                  <strong>{hotWordGroupDisplayName}</strong>
                </span>
              </div>
            </div>
          </div>
        )}
        extra={(
          <Space size={10} wrap>
            <Button icon={<LeftOutlined />} onClick={() => navigate('/meetings')}>
              返回
            </Button>
            {canRetrySchedule && (
              <Button icon={<SyncOutlined />} onClick={handleRetrySchedule} loading={actionLoading}>
                重新调度
              </Button>
            )}
            {canRetrySummary && (
              <Button icon={<SyncOutlined />} onClick={handleOpenSummaryDrawer} disabled={actionLoading}>
                重新总结
              </Button>
            )}
            {canRetryFailedChapterTask && (
              <Button icon={<SyncOutlined />} onClick={handleRetryChapterTask} loading={actionLoading}>
                重试 AI 目录
              </Button>
            )}
            {canRetryFailedSummaryTask && (
              <Button icon={<SyncOutlined />} onClick={handleRetrySummaryTask} loading={actionLoading}>
                重试总结
              </Button>
            )}
            {canRetryTranscription && (
              <Button icon={<SyncOutlined />} onClick={handleRetryTranscription} loading={actionLoading}>
                重新识别
              </Button>
            )}
            {isOwner && meeting.status === 2 && (
              <Button icon={<LoadingOutlined />} disabled loading>
                正在总结
              </Button>
            )}
            {(playbackAudioUrl || transcripts.length > 0 || hasSummaryContent) && (
              <Dropdown
                menu={{
                  items: [
                    ...(playbackAudioUrl ? [{
                      key: 'audio',
                      label: `下载录音 (${audioDownloadFormatLabel})`,
                      icon: <AudioOutlined />,
                      onClick: handleDownloadAudio,
                    }] : []),
                    ...(transcripts.length > 0 ? [{
                      key: 'transcript',
                      label: '下载转录 MD',
                      icon: <DownloadOutlined />,
                      onClick: handleDownloadTranscript,
                      disabled: downloadLoading === 'transcript',
                    }] : []),
                    ...(hasSummaryContent ? [
                      {
                        key: 'pdf',
                        label: '下载 PDF',
                        icon: <FilePdfOutlined />,
                        onClick: () => handleDownloadSummary('pdf'),
                        disabled: downloadLoading === 'pdf',
                      },
                      {
                        key: 'word',
                        label: '下载 Word',
                        icon: <FileWordOutlined />,
                        onClick: () => handleDownloadSummary('word'),
                        disabled: downloadLoading === 'word',
                      },
                    ] : []),
                  ],
                }}
                placement="bottomRight"
              >
                <Button icon={<DownloadOutlined />} loading={!!downloadLoading}>
                  下载
                </Button>
              </Dropdown>
            )}
            {shareQrContent ? (
              <Popover
                content={shareQrContent}
                trigger="click"
                open={sharePopoverOpen}
                onOpenChange={handleSharePopoverOpenChange}
                placement="bottomRight"
                overlayClassName="meeting-share-popover"
              >
                <Button type="primary" icon={<ShareAltOutlined />}>
                  分享
                </Button>
              </Popover>
            ) : null}
          </Space>
        )}
      >
        <div className="meeting-detail-workspace">
        {meeting.status === 0 || meeting.status === 1 ? (
          <>
            <div style={{ display: 'none' }}>
              <MeetingProgressDisplay
                meetingId={meeting.id}
                onComplete={() => fetchData(meeting.id)}
                onRefreshNeeded={() => { void fetchData(meeting.id); }}
                onProgressChange={setGenerationProgress}
              />
            </div>
            <UnifiedMeetingProgressDisplay
              progress={generationProgress}
            />
          </>
        ) : (
          <>
            <Row gutter={24} style={{ height: '100%' }}>
            <Col xs={24} xl={13} style={{ height: '100%' }}>
              <div className="detail-side-column detail-left-column">
                {(generationFailureNotice || emptyTranscriptFailureNotice || meeting.audioSaveStatus === 'FAILED') && (
                  <Alert
                    type={generationFailureNotice?.type || 'warning'}
                    showIcon
                    style={{ marginBottom: 16, borderRadius: 12 }}
                    message={
                      <div style={{ fontWeight: 600 }}>
                        {generationFailureNotice?.title || emptyTranscriptFailureNotice?.title || '会议处理提醒'}
                      </div>
                    }
                    description={
                      <div style={{ fontSize: 13, marginTop: 4 }}>
                        <div>{generationFailureNotice?.description || emptyTranscriptFailureNotice?.description}</div>
                        {meeting.audioSaveStatus === 'FAILED' && (
                          <div style={{ marginTop: 4, color: '#d48806' }}>
                            <InfoCircleOutlined style={{ marginRight: 4 }} />
                            {meeting.audioSaveMessage || '录音保存失败，当前无法播放'}
                          </div>
                        )}
                        {canRetryTranscription && (
                          <div style={{ marginTop: 8 }}>
                            {emptyTranscriptFailureNotice?.hint}
                          </div>
                        )}
                      </div>
                    }
                  />
                )}
                <Card className="left-flow-card summary-panel" variant="borderless">

                  {(meeting.status === 1 || meeting.status === 2) && (
                    <div style={{ display: 'none' }}>
                      <MeetingProgressDisplay
                        meetingId={meeting.id}
                        onComplete={() => fetchData(meeting.id)}
                        onRefreshNeeded={() => { void fetchData(meeting.id); }}
                        onProgressChange={setGenerationProgress}
                      />
                    </div>
                  )}

                  {meeting.status === 2 && !hasSummaryContent ? (
                    <div className="summary-progress-shell">
                      <UnifiedMeetingProgressDisplay
                        progress={generationProgress}
                        compact
                      />
                    </div>
                  ) : hasSummaryContent ? (
                    <div className="summary-content-box">
                      <div className="summary-head">
                        <div className="summary-title">会议总结</div>
                      </div>
                      {summaryPanelNotice && (
                        <Alert
                          type={summaryPanelNotice.type}
                          showIcon
                          style={{ marginBottom: 20, borderRadius: 14 }}
                          message={summaryPanelNotice.title}
                          description={
                            <div>
                              <div>{summaryPanelNotice.description}</div>
                              {meeting.status === 2 ? (
                                <UnifiedMeetingProgressDisplay
                                  progress={generationProgress}
                                  inline
                                />
                              ) : null}
                            </div>
                          }
                        />
                      )}
                      {/* Keywords placed between title and overview */}
                      <div className="summary-section" style={{ marginBottom: 24 }}>
                        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
                          <div className="summary-section-title" style={{ marginBottom: 0 }}>关键词</div>
                          {isOwner && analysis.keywords.length > 0 && (
                            <Button
                              size="small"
                              type="primary"
                              ghost
                              disabled={!selectedKeywords.length}
                              loading={addingHotwords}
                              onClick={handleAddSelectedHotwords}
                            >
                              加入热词 {selectedKeywords.length > 0 ? `(${selectedKeywords.length})` : ''}
                            </Button>
                          )}
                        </div>
                        <div className="record-tags">
                          {keywordItems.length ? (
                            keywordItems.map((tag) => {
                              const isSelected = selectedKeywords.includes(tag);
                              return (
                                <div
                                  key={tag}
                                  className={`tag selectable-tag ${isSelected ? 'selected' : ''}`}
                                  onClick={() => {
                                    if (isOwner && analysis.keywords.length) {
                                      handleKeywordToggle(tag, !isSelected);
                                    }
                                  }}
                                >
                                  <span>{normalizeTagDisplay(tag)}</span>
                                  {isOwner && isSelected && <CheckCircleFilled style={{ fontSize: 12 }} />}
                                </div>
                              );
                            })
                          ) : (
                            <Text type="secondary">暂无关键词</Text>
                          )}
                        </div>
                      </div>

                      <div className="summary-section">
                        <div style={{
                          display: 'flex',
                          justifyContent: 'space-between',
                          alignItems: 'center',
                          marginBottom: 12
                        }}>
                          <div className="summary-section-title" style={{marginBottom: 0}}>会议概述</div>
                          {isOwner ? (
                            isEditingSummary ? (
                              <Space size={8}>
                                <Button onClick={() => setIsEditingSummary(false)}>取消</Button>
                                <Button type="primary" onClick={handleSaveSummary} loading={actionLoading}>保存</Button>
                              </Space>
                            ) : (
                              <Button
                                size="small"
                                type="primary"
                                ghost
                                icon={<EditOutlined/>}
                                onClick={() => {
                                  setSummaryDraft(meeting.summaryContent || '');
                                  setIsEditingSummary(true);
                                }}
                              >
                                编辑
                              </Button>
                            )
                          ) : null}
                        </div>
                        <div className="markdown-body summary-markdown">
                          {isEditingSummary ? (
                            <Input.TextArea
                              value={summaryDraft}
                              onChange={(e) => setSummaryDraft(e.target.value)}
                              autoSize={{ minRows: 10 }}
                              className="summary-inline-edit"
                            />
                          ) : (
                            <ReactMarkdown>
                              {analysis.overview || meeting.summaryContent}
                            </ReactMarkdown>
                          )}
                        </div>
                      </div>

                      {discussionItems.length > 0 && !isEditingSummary && (
                        <div className="summary-section" style={{ marginTop: 24 }}>
                          <div className="summary-section-title">主要讨论点</div>
                          <div className="discussion-list">
                            {discussionItems.map((item, index) => (
                              <div className="discussion-item" key={`${item.title}-${index}`}>
                                <div className="discussion-marker" />
                                <div className="discussion-body">
                                  <div className="discussion-title-row">
                                    <strong>{item.title || `讨论点 ${index + 1}`}</strong>
                                  </div>
                                  <div className="discussion-copy">{item.summary}</div>
                                </div>
                              </div>
                            ))}
                          </div>
                        </div>
                      )}
                    </div>
                  ) : (
                    <div className="summary-empty-state">
                      <Empty description="暂无智能总结内容" />
                    </div>
                  )}
                </Card>

                {!emptyTranscriptFailureNotice && (
                <div className="section-divider-note">
                  <div className="section-divider-line" />
                  <div className="section-divider-text">
                    智能内容由 AI 模型生成，我们不对内容准确性和完整性作任何保证，也不代表我们的观点或态度
                  </div>
                  <div className="section-divider-line" />
                </div>
                )}

                <div ref={transcriptSectionRef} className="transcript-player-anchor">
                  <Card className="left-flow-card" variant="borderless" title={<Space size={8}><AudioOutlined />原文</Space>}>
                    {playbackAudioUrl && (
                      <audio ref={audioRef} style={{ display: 'none' }} preload="auto">
                        <source src={playbackAudioUrl} type={resolveAudioMimeType(playbackAudioUrl)} />
                      </audio>
                    )}
                    {emptyTranscriptFailureNotice && (
                      <div className="empty-transcript-inline-note">
                        <div className="empty-transcript-inline-note__title">当前没有可展示的转录内容</div>
                        <div className="empty-transcript-inline-note__text">{emptyTranscriptFailureNotice.description}</div>
                      </div>
                    )}
                    <List
                      className="transcript-list"
                      dataSource={transcripts}
                      style={{ paddingBottom: playbackAudioUrl ? 156 : 0 }}
                      renderItem={(item, index) => {
                        const nextStartTime = transcripts[index + 1]?.startTime || Infinity;
                        const isActive = (audioCurrentTime * 1000) >= item.startTime && (audioCurrentTime * 1000) < nextStartTime;

                        return (
                          <ActiveTranscriptRow
                            item={item}
                            meetingId={meeting.id}
                            isOwner={isOwner}
                            isEditing={editingTranscriptId === item.id}
                            isSaving={savingTranscriptId === item.id}
                            speakerLabelMap={speakerLabelMap}
                            onPlay={handleTranscriptRowPlay}
                            onStartEdit={handleStartEditTranscript}
                            onDraftBlur={handleTranscriptDraftBlur}
                            onDraftKeyDown={handleTranscriptDraftKeyDown}
                            onSpeakerUpdated={handleTranscriptSpeakerUpdated}
                            registerRow={registerTranscriptRow}
                            isActive={isActive}
                            isLinkedHighlight={linkedTranscriptIds.includes(item.id)}
                            audioPlaying={audioPlaying}
                          />
                        );
                      }}
                      locale={{ emptyText: meeting.status < 3 ? '识别任务进行中...' : '暂无数据' }}
                    />
                  </Card>
                </div>
              </div>
            </Col>

            <Col xs={24} xl={11} style={{ height: '100%' }}>
              <div ref={leftColumnRef} className="detail-side-column detail-right-column">
                {( 
                <div ref={transcriptSectionRef} className="transcript-player-anchor">
                  <Card
                    className="left-flow-card transcript-workspace-card"
                    variant="borderless"
                    style={{ height: '100%' }}
                    styles={{ body: { padding: 0, height: '100%', display: 'flex', flexDirection: 'column', minHeight: 0 } }}
                  >
                    {playbackAudioUrl && (
                      <audio ref={audioRef} style={{ display: 'none' }} preload="auto">
                        <source src={playbackAudioUrl} type={resolveAudioMimeType(playbackAudioUrl)} />
                      </audio>
                    )}

                    <div className="transcript-panel-header">
                      <div className="transcript-stage-tabs" role="tablist" aria-label="内容切换">
                        {aiCatalogEnabled && (
                          <button
                            type="button"
                            role="tab"
                            aria-selected={workspaceTab === 'catalog'}
                            className={workspaceTab === 'catalog' ? 'active' : ''}
                            onClick={() => setWorkspaceTab('catalog')}
                          >
                            AI目录
                          </button>
                        )}
                        <button
                          type="button"
                          role="tab"
                          aria-selected={workspaceTab === 'transcript'}
                          className={workspaceTab === 'transcript' ? 'active' : ''}
                          onClick={() => setWorkspaceTab('transcript')}
                        >
                          转录原文
                        </button>
                      </div>
                    </div>

                    <div className="transcript-scroll-shell">
                      {aiCatalogEnabled && workspaceTab === 'catalog' ? (
                        <div className="catalog-list">
                          {catalogPanelNotice && (
                            <Alert
                              type={catalogPanelNotice.type}
                              showIcon
                              style={{ marginBottom: 16 }}
                              message="AI 目录生成失败"
                              description={catalogPanelNotice.description}
                            />
                          )}
                          {catalogChapterLinks.length ? (
                            catalogChapterLinks.map((chapter, index) => (
                              <div
                                key={chapter.key}
                                className={`catalog-item-container ${linkedChapterKey === chapter.key ? 'active' : ''}`}
                              >
                                <div className="catalog-timeline-axis">
                                  <div className="catalog-timeline-dot" />
                                  <div className="catalog-timeline-line" />
                                </div>
                                <div 
                                  className="catalog-item-card"
                                  onClick={() => handleLocateChapterTranscript(index)}
                                  style={{ cursor: 'pointer' }}
                                >
                                  <div className="catalog-item-time">{chapter.timeLabel}</div>
                                  <div className="catalog-item-title-row" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginTop: 4 }}>
                                    <div className="catalog-item-title">{chapter.title}</div>
                                    <button
                                      type="button"
                                      className="catalog-item-link"
                                      onClick={(e) => {
                                        e.stopPropagation();
                                        handleLocateChapterTranscript(index);
                                      }}
                                    >
                                      <LinkOutlined /> 关联原文
                                    </button>
                                  </div>
                                </div>
                              </div>                            ))
                          ) : meeting.status === 2 ? (
                            <Alert
                              type="info"
                              showIcon
                              style={{ marginBottom: 16 }}
                              message="AI 目录生成中"
                              description={generationProgress?.message || '正在生成 AI 目录，章节完成后会自动展示。'}
                            />
                          ) : (
                            <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无 AI 目录" />
                          )}
                        </div>
                      ) : (
                        <>
                          {emptyTranscriptFailureNotice && (
                            <div className="empty-transcript-inline-note">
                              <div className="empty-transcript-inline-note__title">当前没有可展示的转录内容</div>
                              <div className="empty-transcript-inline-note__text">{emptyTranscriptFailureNotice.description}</div>
                            </div>
                          )}
                          <List
                            className="transcript-list"
                            dataSource={transcripts}
                            style={{ paddingBottom: playbackAudioUrl ? 156 : 0 }}
                            renderItem={(item, index) => {
                              const nextStartTime = transcripts[index + 1]?.startTime || Infinity;
                              const isActive = (audioCurrentTime * 1000) >= item.startTime && (audioCurrentTime * 1000) < nextStartTime;

                              return (
                                <ActiveTranscriptRow
                                  item={item}
                                  meetingId={meeting.id}
                                  isOwner={isOwner}
                                  isEditing={editingTranscriptId === item.id}
                                  isSaving={savingTranscriptId === item.id}
                                  speakerLabelMap={speakerLabelMap}
                                  onPlay={handleTranscriptRowPlay}
                                  onStartEdit={handleStartEditTranscript}
                                  onDraftBlur={handleTranscriptDraftBlur}
                                  onDraftKeyDown={handleTranscriptDraftKeyDown}
                                  onSpeakerUpdated={handleTranscriptSpeakerUpdated}
                                  registerRow={registerTranscriptRow}
                                  isActive={isActive}
                                  isLinkedHighlight={linkedTranscriptIds.includes(item.id)}
                                  audioPlaying={audioPlaying}
                                  highlightKeyword={highlightKeyword}
                                />
                              );
                            }}
                            locale={{ emptyText: meeting.status < 3 ? '识别任务进行中...' : '暂无数据' }}
                          />
                        </>
                      )}
                    </div>
                  </Card>
                </div>
                )}
              </div>
            </Col>
            </Row>
          </>
        )}
        </div>
      </SectionCard>

      {playbackAudioUrl && showFloatingTranscriptPlayer && floatingTranscriptPlayerLayout && (
        <div
          className="transcript-player transcript-player--floating"
          style={{ left: floatingTranscriptPlayerLayout.left, width: floatingTranscriptPlayerLayout.width }}
        >
          <button type="button" className="player-main-btn" onClick={toggleAudioPlayback} aria-label="toggle-audio">
            {audioPlaying ? <PauseOutlined /> : <CaretRightFilled />}
          </button>
          <div className="player-progress-shell">
            <div className="player-time-row">
              <span>{formatPlayerTime(audioCurrentTime)}</span>
              <span>{formatPlayerTime(audioDuration)}</span>
            </div>
            <input
              className="player-range"
              type="range"
              min={0}
              max={audioDuration || 0}
              step={0.1}
              value={Math.min(audioCurrentTime, audioDuration || 0)}
              style={{ '--player-progress': `${playerProgressPercent}%` } as React.CSSProperties}
              onChange={handleAudioProgressChange}
            />
          </div>
          <button type="button" className="player-ghost-btn" onClick={cyclePlaybackRate}>
            <FastForwardOutlined />
            {audioPlaybackRate}x
          </button>
        </div>
      )}

      <style>{`
        @keyframes highlight-pulse {
          0% { box-shadow: 0 0 0 rgba(95, 81, 255, 0); transform: scale(1); }
          50% { box-shadow: 0 0 15px rgba(95, 81, 255, 0.3); transform: scale(1.02); }
          100% { box-shadow: 0 0 0 rgba(95, 81, 255, 0); transform: scale(1); }
        }
        .highlight-text {
          background: linear-gradient(120deg, rgba(95, 81, 255, 0.15) 0%, rgba(108, 140, 255, 0.1) 100%);
          border-bottom: 2px solid #5f51ff;
          border-radius: 3px;
          padding: 1px 4px;
          color: #4335eb;
          font-weight: 600;
          transition: all 0.3s ease;
          display: inline-block;
          line-height: 1.2;
          margin: 0 1px;
          cursor: pointer;
          animation: highlight-pulse 2s infinite ease-in-out;
        }
        .highlight-text:hover {
          background: rgba(95, 81, 255, 0.25);
          transform: translateY(-1px);
        }
        /* 当转录行处于活动状态时，调整高亮样式以保持可读性 */
        .ant-list-item.transcript-row.active .highlight-text {
          background: linear-gradient(120deg, rgba(95, 81, 255, 0.15) 0%, rgba(108, 140, 255, 0.1) 100%);
          border-bottom-color: #5f51ff;
          color: #4335eb;
          text-shadow: none;
        }
        .summary-keyword-link {
          color: #5f51ff;
          cursor: pointer;
          font-weight: 500;
          padding: 0 2px;
          border-radius: 2px;
          transition: all 0.2s;
        }
        .summary-keyword-link:hover {
          background: rgba(95, 81, 255, 0.1);
          text-decoration: underline;
        }
        .meeting-detail-page {
          padding: 24px;
          height: calc(100vh - 64px);
          overflow: hidden;
          display: flex;
          flex-direction: column;
          background: 
            radial-gradient(circle at 20% 20%, rgba(95, 81, 255, 0.05) 0%, transparent 40%),
            radial-gradient(circle at 80% 80%, rgba(108, 140, 255, 0.05) 0%, transparent 40%),
            #fbfcfd;
          position: relative;
        }
        .meeting-detail-page::before {
          content: "";
          position: absolute;
          inset: 0;
          background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3%3Ffilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
          opacity: 0.015;
          pointer-events: none;
        }
        .meeting-detail-page-header {
          margin-bottom: 18px;
          padding: 24px 32px;
          border-radius: 28px;
          border: 1px solid rgba(220, 226, 242, 0.5);
          background: rgba(255, 255, 255, 0.8);
          box-shadow: 0 20px 50px rgba(0, 0, 0, 0.03);
          backdrop-filter: blur(20px);
        }
        .meeting-detail-title-wrap {
          display: flex;
          align-items: flex-start;
          gap: 16px;
        }
        .meeting-detail-title-icon {
          width: 42px;
          height: 42px;
          border-radius: 14px;
          display: inline-flex;
          align-items: center;
          justify-content: center;
          color: #fff;
          font-size: 20px;
          background: linear-gradient(135deg, #5f51ff, #6c8cff);
          box-shadow: 0 10px 24px rgba(95, 81, 255, 0.22);
          flex-shrink: 0;
        }
        .meeting-detail-title-copy {
          display: grid;
          gap: 6px;
        }
        .meeting-detail-title-row {
          display: flex;
          align-items: center;
          gap: 10px;
          flex-wrap: wrap;
        }
        .meeting-detail-title-text {
          color: #1a1f36;
          font-size: 24px;
          line-height: 1.2;
          font-weight: 800;
          letter-spacing: -0.03em;
        }
        .meeting-detail-title-edit {
          width: 28px;
          height: 28px;
          border: 0;
          border-radius: 50%;
          background: rgba(95, 81, 255, 0.08);
          color: #5f51ff;
          cursor: pointer;
          display: inline-flex;
          align-items: center;
          justify-content: center;
        }
        .meeting-detail-meta-row {
          display: flex;
          flex-wrap: wrap;
          gap: 10px 16px;
          color: #6e7695;
          font-size: 13px;
        }
        .meeting-detail-config-row {
          display: flex;
          flex-wrap: wrap;
          gap: 10px 12px;
          margin-top: 2px;
        }
        .meeting-detail-config-item {
          display: inline-flex;
          align-items: center;
          gap: 8px;
          min-height: 34px;
          padding: 6px 12px;
          border-radius: 999px;
          background: rgba(244, 246, 255, 0.92);
          border: 1px solid rgba(227, 232, 247, 0.95);
          color: #58627f;
          font-size: 13px;
          line-height: 1;
        }
        .meeting-detail-config-item strong {
          color: #313a5a;
          font-weight: 700;
        }
        .meeting-detail-workspace {
          flex: 1;
          min-height: 0;
        }
        .detail-side-column {
          height: 100%;
          min-height: 0;
          min-width: 0;
        }
        .detail-left-column {
          overflow-y: auto;
          overflow-x: hidden;
          padding-right: 6px;
          display: flex;
          flex-direction: column;
          gap: 18px;
        }
        .detail-left-column > .section-divider-note,
        .detail-left-column > .transcript-player-anchor {
          display: none;
        }
        .detail-right-column {
          overflow: hidden;
          display: flex;
          flex-direction: column;
        }
        .left-flow-card {
          min-width: 0;
          border-radius: 24px;
          border: 1px solid rgba(228, 232, 245, 0.96);
          box-shadow: 0 18px 38px rgba(127, 139, 186, 0.08);
          background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 255, 0.98));
        }
        .left-flow-card .ant-card-head,
        .left-flow-card .ant-card-body {
          min-width: 0;
        }
        .summary-panel .ant-card-body {
          display: flex;
          flex-direction: column;
          gap: 20px;
          padding: 24px;
        }
        .summary-head {
          display: flex;
          align-items: center;
          justify-content: space-between;
          margin-bottom: 4px;
        }
        .summary-title {
          display: inline-flex;
          align-items: center;
          gap: 8px;
          color: #1a1f36;
          font-size: 18px;
          font-weight: 800;
        }
        .summary-content-box {
          background: #f8faff;
          border: 1px solid #eef1f9;
          border-radius: 16px;
          padding: 24px;
        }
        .summary-section-title {
          color: #9aa0bd;
          font-size: 14px;
          font-weight: 700;
          margin-bottom: 12px;
          letter-spacing: 0.02em;
        }
        .summary-markdown {
          font-size: 15px;
          line-height: 1.8;
          color: #2d3553;
        }
        .summary-markdown p {
          margin-bottom: 16px;
        }
        .summary-markdown ul {
          padding-left: 20px;
          margin-bottom: 16px;
        }
        .summary-markdown li {
          margin-bottom: 8px;
          position: relative;
        }
        .summary-markdown li::marker {
          color: #5f51ff;
        }
        .keyword-panel .ant-card-body {
          display: grid;
          gap: 16px;
          padding: 20px 24px 24px;
        }
        .keyword-panel-head {
          display: flex;
          align-items: flex-start;
          justify-content: space-between;
          gap: 12px;
          flex-wrap: wrap;
        }
        .keyword-panel-title {
          color: #2d3553;
          font-size: 16px;
          font-weight: 800;
          letter-spacing: 0.04em;
        }
        .brief-section {
          display: grid;
          gap: 14px;
          padding: 18px 18px 0;
          border-top: 1px solid rgba(230, 235, 247, 0.95);
        }
        .brief-section:first-of-type {
          padding-top: 0;
          border-top: 0;
        }
        .brief-section-title {
          color: #5d6685;
          font-size: 14px;
          font-weight: 800;
          letter-spacing: 0.06em;
        }
        .meeting-share-popover .ant-popover-inner {
          padding: 0;
          overflow: hidden;
          border-radius: 24px;
          box-shadow: 0 24px 56px rgba(76, 61, 39, 0.18);
          background:
            radial-gradient(circle at top right, rgba(82, 164, 255, 0.12), transparent 32%),
            radial-gradient(circle at top left, rgba(252, 208, 157, 0.18), transparent 26%),
            linear-gradient(180deg, rgba(255, 252, 247, 0.98), rgba(248, 241, 231, 0.98));
        }
        .empty-transcript-hero {
          margin-bottom: 16px;
          padding: 18px 20px;
          border-radius: 24px;
          border: 1px solid rgba(220, 170, 77, 0.24);
          background:
            radial-gradient(circle at top right, rgba(255, 233, 188, 0.72), transparent 34%),
            linear-gradient(135deg, rgba(255, 252, 245, 0.98), rgba(255, 245, 223, 0.98));
          box-shadow: 0 22px 48px rgba(188, 148, 68, 0.14);
        }
        .empty-transcript-hero__badge {
          display: inline-flex;
          align-items: center;
          padding: 6px 10px;
          border-radius: 999px;
          background: rgba(122, 80, 14, 0.08);
          color: #9a6700;
          font-size: 11px;
          font-weight: 800;
          letter-spacing: 0.12em;
          text-transform: uppercase;
        }
        .empty-transcript-hero__content {
          margin-top: 14px;
          display: flex;
          align-items: flex-start;
          justify-content: space-between;
          gap: 18px;
        }
        .empty-transcript-hero__title {
          color: #7a4f0f;
          font-size: 20px;
          font-weight: 800;
          line-height: 1.3;
        }
        .empty-transcript-hero__description {
          margin-top: 8px;
          max-width: 760px;
          color: #7a5b27;
          font-size: 14px;
          line-height: 1.8;
        }
        .empty-transcript-hero__hint {
          margin-top: 8px;
          color: #b26a00;
          font-size: 13px;
          font-weight: 600;
        }
        .empty-transcript-hero__actions {
          display: flex;
          flex-shrink: 0;
          align-items: center;
          gap: 10px;
        }
        .empty-transcript-player {
          margin-top: 16px;
          max-width: 480px;
          background: rgba(255, 255, 255, 0.8);
        }
        .empty-transcript-inline-note {
          margin-bottom: 16px;
          padding: 14px 16px;
          border-radius: 18px;
          border: 1px solid rgba(220, 170, 77, 0.22);
          background: linear-gradient(135deg, rgba(255, 250, 240, 0.96), rgba(255, 245, 224, 0.92));
        }
        .empty-transcript-inline-note__title {
          color: #8a5a00;
          font-size: 14px;
          font-weight: 700;
        }
        .empty-transcript-inline-note__text {
          margin-top: 6px;
          color: #876537;
          font-size: 13px;
          line-height: 1.7;
        }
        .meeting-share-popover .ant-popover-inner-content {
          padding: 0;
        }
        .meeting-share-card {
          width: min(82vw, 300px);
          padding: 18px;
          display: flex;
          flex-direction: column;
          gap: 14px;
          color: #33261c;
        }
        .meeting-share-card-head {
          display: flex;
          align-items: flex-start;
          justify-content: space-between;
          gap: 12px;
        }
        .meeting-share-kicker {
          color: rgba(72, 53, 39, 0.64);
          font-size: 11px;
          letter-spacing: 0.18em;
          text-transform: uppercase;
        }
        .meeting-share-title {
          margin-top: 6px;
          font-family: Georgia, 'Times New Roman', 'Songti SC', serif;
          font-size: 22px;
          line-height: 1.08;
          color: #33261c;
        }
        .meeting-share-pill {
          display: inline-flex;
          align-items: center;
          padding: 6px 10px;
          border-radius: 999px;
          background: rgba(49, 95, 139, 0.08);
          color: #315f8b;
          font-size: 12px;
          font-weight: 700;
          white-space: nowrap;
        }
        .meeting-share-qr-wrap {
          display: flex;
          align-items: center;
          justify-content: center;
          padding: 16px;
          border-radius: 22px;
          background: rgba(255, 255, 255, 0.72);
          border: 1px solid rgba(84, 57, 31, 0.08);
          box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
        }
        .meeting-share-caption {
          color: rgba(72, 53, 39, 0.76);
          font-size: 13px;
          line-height: 1.7;
        }
        .meeting-share-settings {
          display: grid;
          gap: 12px;
          padding: 14px;
          border-radius: 18px;
          background: rgba(255, 255, 255, 0.62);
          border: 1px solid rgba(84, 57, 31, 0.08);
        }
        .meeting-share-settings-row {
          display: flex;
          align-items: center;
          justify-content: space-between;
          gap: 12px;
        }
        .meeting-share-settings-copy {
          display: grid;
          gap: 4px;
        }
        .meeting-share-settings-copy strong {
          color: #33261c;
          font-size: 14px;
        }
        .meeting-share-settings-copy span {
          color: rgba(72, 53, 39, 0.72);
          font-size: 12px;
          line-height: 1.6;
        }
        .meeting-share-settings-actions {
          display: grid;
          grid-template-columns: minmax(0, 1fr) auto;
          gap: 10px;
        }
        .meeting-share-link-box {
          display: flex;
          align-items: center;
          gap: 8px;
          padding: 10px 12px;
          border-radius: 14px;
          background: rgba(255, 255, 255, 0.72);
          border: 1px solid rgba(84, 57, 31, 0.08);
          color: #315f8b;
          font-size: 12px;
        }
        .meeting-share-link-box span {
          flex: 1;
          min-width: 0;
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
        }
        .meeting-share-actions {
          display: grid;
          grid-template-columns: repeat(2, minmax(0, 1fr));
          gap: 10px;
        }
        .record-tags {
          display: flex;
          flex-wrap: wrap;
          gap: 10px;
        }
        .tag,
        .summary-tag {
          display: inline-flex;
          align-items: center;
          gap: 6px;
          padding: 6px 14px;
          border-radius: 999px;
          background: rgba(22, 119, 255, 0.06);
          color: var(--app-primary-color);
          border: 1px solid rgba(22, 119, 255, 0.15);
          font-size: 13px;
          font-weight: 500;
        }
        .selectable-tag {
          cursor: pointer;
          transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
        }
        .selectable-tag:hover {
          background: rgba(22, 119, 255, 0.12);
          border-color: rgba(22, 119, 255, 0.3);
          transform: translateY(-1px);
        }
        .selectable-tag.selected {
          background: linear-gradient(135deg, var(--app-primary-color), #36cfc9);
          color: #ffffff;
          border-color: transparent;
          font-weight: 600;
          box-shadow: 0 4px 12px rgba(22, 119, 255, 0.24);
        }
        .selectable-tag.selected:hover {
          box-shadow: 0 6px 16px rgba(22, 119, 255, 0.35);
          transform: translateY(-2px);
        }
        .summary-copy {
          color: #465072;
          font-size: 14px;
          line-height: 1.92;
          position: relative;
          white-space: pre-wrap;
        }
        .summary-fade {
          max-height: 162px;
          overflow: hidden;
        }
        .summary-fade::after {
          content: "";
          position: absolute;
          left: 0;
          right: 0;
          bottom: 0;
          height: 52px;
          background: linear-gradient(180deg, rgba(248, 250, 255, 0), rgba(248, 250, 255, 0.98));
        }
        .summary-link {
          width: fit-content;
          padding: 0;
          border: 0;
          background: transparent;
          color: #6470ff;
          font-size: 13px;
          font-weight: 700;
          cursor: pointer;
        }
        .discussion-list {
          display: grid;
          gap: 16px;
        }
        .discussion-item {
          display: grid;
          grid-template-columns: 12px minmax(0, 1fr);
          gap: 14px;
          align-items: flex-start;
        }
        .discussion-marker {
          width: 8px;
          height: 8px;
          margin-top: 10px;
          border-radius: 50%;
          background: linear-gradient(135deg, #5f51ff, #7f8dff);
          box-shadow: 0 0 0 5px rgba(95, 81, 255, 0.08);
        }
        .discussion-body {
          display: grid;
          gap: 8px;
          color: #4f5876;
          line-height: 1.8;
        }
        .discussion-title-row {
          display: flex;
          flex-wrap: wrap;
          align-items: center;
          gap: 10px;
        }
        .discussion-title-row strong {
          color: #233053;
          font-size: 15px;
        }
        .discussion-copy {
          color: #4f5876;
          line-height: 1.86;
        }
        .discussion-meta {
          display: flex;
          flex-wrap: wrap;
          gap: 8px;
        }
        .transcript-workspace-card {
          height: 100%;
          overflow: hidden;
        }
        .transcript-keyword-bar {
          padding: 18px 18px 12px;
          border-bottom: 1px solid rgba(228, 232, 245, 0.92);
          display: grid;
          gap: 14px;
          background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 255, 0.92));
        }
        .transcript-keyword-label {
          color: #2d3553;
          font-size: 14px;
          font-weight: 800;
          letter-spacing: 0.04em;
        }
        .transcript-keyword-body {
          display: grid;
          gap: 12px;
        }
        .transcript-keyword-actions {
          display: flex;
          align-items: center;
          justify-content: space-between;
          gap: 12px;
          flex-wrap: wrap;
        }
        .transcript-stage-tabs {
          display: flex;
          align-items: center;
          gap: 32px;
          padding: 0 24px;
          min-height: 56px;
          border-bottom: 1px solid #eef1f9;
          background: #ffffff;
        }
        .transcript-stage-tabs button {
          padding: 0;
          border: 0;
          background: transparent;
          color: #6e7695;
          font-size: 16px;
          font-weight: 700;
          height: 100%;
          cursor: pointer;
          position: relative;
          transition: color 0.2s;
        }
        .transcript-stage-tabs button:hover {
          color: #5f51ff;
        }
        .transcript-stage-tabs button.active {
          color: #5f51ff;
        }
        .transcript-stage-tabs button.active::after {
          content: "";
          position: absolute;
          left: 0;
          right: 0;
          bottom: 0;
          height: 3px;
          background: #5f51ff;
          border-radius: 99px 99px 0 0;
        }
        .transcript-scroll-shell {
          flex: 1;
          min-height: 0;
          overflow-y: auto;
          overflow-x: hidden;
          padding: 18px 18px 0;
        }
        .catalog-list {
          display: flex;
          flex-direction: column;
          gap: 0;
          padding: 10px 0 20px;
        }
        .catalog-item-container {
          display: flex;
          gap: 20px;
          padding: 0 10px;
        }
        .catalog-timeline-axis {
          display: flex;
          flex-direction: column;
          align-items: center;
          width: 20px;
          flex-shrink: 0;
          position: relative;
        }
        .catalog-timeline-dot {
          width: 10px;
          height: 10px;
          background: #5f51ff;
          border-radius: 50%;
          margin-top: 24px;
          z-index: 2;
          transition: all 0.3s ease;
        }
        .catalog-timeline-line {
          position: absolute;
          top: 0;
          bottom: 0;
          width: 2px;
          background: #eef1f9;
          z-index: 1;
        }
        .catalog-item-container:first-child .catalog-timeline-line {
          top: 24px;
        }
        .catalog-item-container:last-child .catalog-timeline-line {
          bottom: calc(100% - 34px);
        }
        .catalog-item-card {
          flex: 1;
          background: #ffffff;
          border: 1px solid #eef1f9;
          border-radius: 12px;
          padding: 16px 20px;
          margin-bottom: 20px;
          transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
        }
        .catalog-item-card:hover {
          background: #f9faff;
          border-color: rgba(95, 81, 255, 0.2);
        }
        .catalog-item-link {
          opacity: 0;
          visibility: hidden;
          padding: 4px 10px;
          border-radius: 6px;
          border: 1px solid transparent;
          background: rgba(95, 81, 255, 0.06);
          color: #5f51ff;
          font-size: 12px;
          font-weight: 600;
          cursor: pointer;
          display: flex;
          align-items: center;
          gap: 4px;
          transition: all 0.2s ease;
        }
        .catalog-item-container:hover .catalog-item-link {
          opacity: 1;
          visibility: visible;
        }
        
        .summary-inline-edit {
          width: 100% !important;
          border-radius: 12px !important;
          border: 1px solid #d9d9d9 !important;
          padding: 12px 16px !important;
          font-size: 15px !important;
          line-height: 1.8 !important;
          color: #2d3553 !important;
          background: #fff !important;
        }
        .summary-inline-edit:focus {
          border-color: #5f51ff !important;
          box-shadow: 0 0 0 2px rgba(95, 81, 255, 0.1) !important;
        }
        .catalog-item-link:hover {
          background: rgba(95, 81, 255, 0.12);
          border-color: rgba(95, 81, 255, 0.2);
        }
        
        .tag,
        .summary-tag {
          display: inline-flex;
          align-items: center;
          gap: 6px;
          padding: 5px 14px;
          border-radius: 99px;
          background: #ffffff;
          color: #6e7695;
          border: 1px solid #eef1f9;
          font-size: 13px;
          font-weight: 500;
          transition: all 0.2s ease;
        }
        .selectable-tag:hover {
          border-color: #5f51ff;
          color: #5f51ff;
          background: rgba(95, 81, 255, 0.02);
        }
        .selectable-tag.selected {
          border-color: #5f51ff;
          background: rgba(95, 81, 255, 0.06);
          color: #5f51ff;
        }
        .selectable-tag.highlighted-tag {
          border-color: #5f51ff;
          background: rgba(95, 81, 255, 0.05);
          color: #5f51ff;
        }
        .segmented-tabs {
          display: flex;
          gap: 28px;
          align-items: center;
          flex-wrap: wrap;
          padding-bottom: 8px;
          border-bottom: 1px solid rgba(226, 231, 245, 0.95);
        }
        .segmented-tabs button {
          padding: 0 0 10px;
          border: 0;
          background: transparent;
          font-size: 14px;
          font-weight: 700;
          color: #9aa0bd;
          cursor: pointer;
          position: relative;
          white-space: nowrap;
          transition: color 0.2s ease;
        }
        .segmented-tabs button:hover {
          color: #5a638f;
        }
        .segmented-tabs button.active {
          color: #3a4476;
        }
        .segmented-tabs button.active::after {
          content: "";
          position: absolute;
          left: 0;
          right: 0;
          bottom: -9px;
          height: 2px;
          border-radius: 999px;
          background: #6b73ff;
        }
        .chapter-list,
        .speaker-summary-list,
        .keypoint-list,
        .todo-list {
          display: flex;
          flex-direction: column;
          gap: 14px;
          padding-top: 12px;
        }
        .chapter-item {
          display: flex;
          gap: 14px;
          align-items: flex-start;
        }
        .chapter-time {
          position: relative;
          padding-top: 10px;
          color: #58627f;
          font-size: 14px;
          font-weight: 700;
          flex-shrink: 0;
          width: 64px;
        }
        .chapter-time::after {
          content: "";
          display: inline-block;
          width: 8px;
          height: 8px;
          margin-left: 8px;
          border-radius: 50%;
          background: #6e76ff;
          vertical-align: middle;
        }
        .chapter-card,
        .speaker-summary-card,
        .keypoint-card {
          border-radius: 16px;
          padding: 14px 16px;
          background: rgba(244, 246, 255, 0.96);
          border: 1px solid rgba(231, 235, 248, 0.98);
        }
        .chapter-card {
          display: flex;
          flex-direction: column;
          gap: 8px;
        }
        .chapter-card strong,
        .keypoint-content strong {
          color: #414b78;
          font-size: 15px;
        }
        .chapter-card span,
        .speaker-summary-body,
        .keypoint-content span {
          color: #67718f;
          line-height: 1.75;
        }
        .speaker-summary-card {
          display: grid;
          grid-template-columns: 96px minmax(0, 1fr);
          gap: 16px;
          align-items: start;
        }
        .speaker-summary-side {
          display: flex;
          flex-direction: column;
          align-items: center;
          gap: 8px;
        }
        .speaker-avatar {
          width: 48px;
          height: 48px;
          border-radius: 16px;
          background: linear-gradient(135deg, #7480ff, #8c63ff);
          color: #fff;
          display: flex;
          align-items: center;
          justify-content: center;
          font-weight: 800;
        }
        .speaker-summary-name {
          color: #3d4774;
          font-weight: 700;
        }
        .speaker-summary-meta,
        .keypoint-meta {
          display: flex;
          flex-wrap: wrap;
          gap: 8px;
          margin-top: 10px;
          color: #96a0c1;
          font-size: 12px;
        }
        .keypoint-card {
          display: flex;
          gap: 14px;
          align-items: start;
        }
        .keypoint-badge {
          width: 36px;
          height: 36px;
          flex-shrink: 0;
          border-radius: 12px;
          background: rgba(107, 115, 255, 0.12);
          color: #5c64ff;
          font-weight: 800;
          display: flex;
          align-items: center;
          justify-content: center;
        }
        .keypoint-content {
          display: flex;
          flex-direction: column;
          gap: 8px;
        }
        .todo-item {
          display: flex;
          align-items: flex-start;
          gap: 10px;
          color: #495271;
          line-height: 1.7;
          padding: 12px 14px;
          border-radius: 14px;
          background: rgba(247, 249, 255, 0.98);
          border: 1px solid rgba(232, 236, 248, 0.98);
        }
        .todo-dot {
          width: 8px;
          height: 8px;
          margin-top: 9px;
          border-radius: 50%;
          background: #6e76ff;
          flex: 0 0 auto;
        }
        .section-divider-note {
          display: flex;
          align-items: center;
          gap: 14px;
          color: #c0c6dc;
          font-size: 12px;
          padding: 2px 4px;
        }
        .section-divider-line {
          flex: 1;
          height: 1px;
          background: linear-gradient(90deg, rgba(220, 225, 240, 0), rgba(220, 225, 240, 0.92), rgba(220, 225, 240, 0));
        }
        .section-divider-text {
          white-space: nowrap;
        }
        .ant-list-item.transcript-row {
          display: flex !important;
          justify-content: flex-start !important;
          align-items: flex-start !important;
          gap: 12px;
          padding: 12px 0 !important;
          border-bottom: 0 !important;
          cursor: pointer;
        }
        .ant-list-item.transcript-row.linked .transcript-bubble,
        .ant-list-item.transcript-row.linked .transcript-bubble-editing {
          background: #ffffff;
          border-color: rgba(60, 112, 245, 0.28);
          color: #2d3553;
          box-shadow: 0 10px 24px rgba(60, 112, 245, 0.08);
        }
        .ant-list-item.transcript-row.active .transcript-bubble,
        .ant-list-item.transcript-row.active .transcript-bubble-editing {
          border-color: rgba(60, 112, 245, 0.34);
          background: #ffffff;
          color: #2d3553;
          box-shadow: 0 12px 28px rgba(60, 112, 245, 0.12);
        }
        .transcript-entry {
          flex: 1;
          justify-self: start;
          text-align: left;
          display: flex;
          align-items: flex-start;
          gap: 16px;
          width: 100%;
          min-width: 0;
        }
        .transcript-content-wrap {
          flex: 1;
          display: flex;
          flex-direction: column;
          gap: 8px;
          min-width: 0;
        }
        .transcript-meta {
          display: flex;
          align-items: center;
          gap: 8px;
          flex-wrap: wrap;
          color: #8e98b8;
          font-size: 12px;
        }
        .transcript-avatar {
          flex-shrink: 0;
          background: linear-gradient(135deg, #d9ebff, #eaf2ff) !important;
          color: #4362ff !important;
          margin-top: 4px;
        }
        .transcript-speaker {
          color: var(--app-text-main);
          font-weight: 700;
          font-size: 14px;
        }
        .transcript-speaker.editable {
          color: var(--app-primary-color);
          cursor: pointer;
        }
        .transcript-bubble {
          display: block;
          align-self: stretch;
          width: 100%;
          max-width: 100%;
          box-sizing: border-box;
          padding: 16px 22px;
          border-radius: 20px;
          background: #ffffff;
          border: 1px solid rgba(228, 232, 245, 0.6);
          color: #2d3553;
          line-height: 1.8;
          font-size: 15px;
          white-space: pre-wrap;
          transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
          box-shadow: 0 4px 12px rgba(0, 0, 0, 0.02);
        }
        .transcript-bubble:hover {
          border-color: rgba(95, 81, 255, 0.2);
          box-shadow: 0 12px 24px rgba(95, 81, 255, 0.05);
          transform: translateY(-1px);
        }
        .transcript-bubble.editable {
          cursor: text;
        }
        .transcript-bubble.editable:hover {
          border-color: rgba(100, 112, 255, 0.24);
        }
        .transcript-bubble-editing {
          display: block;
          align-self: stretch;
          width: 100%;
          max-width: 100%;
          box-sizing: border-box;
          outline: none;
        }
        .transcript-bubble-input {
          display: block;
          width: 100% !important;
          max-width: 100%;
        }
        .transcript-bubble-input .ant-input-affix-wrapper,
        .transcript-bubble-input .ant-input-textarea,
        .transcript-bubble-editing .ant-input-textarea,
        .transcript-bubble-editing .ant-input-textarea-show-count {
          display: block;
          width: 100% !important;
          max-width: 100%;
        }
        .transcript-bubble-input .ant-input {
          display: block;
          width: 100% !important;
          min-height: 1.86em;
          padding: 0;
          border: 0;
          background: transparent;
          color: #3f496a;
          line-height: 1.86;
          white-space: pre-wrap;
          resize: none;
          box-shadow: none;
          direction: ltr;
          text-align: left;
        }
        .transcript-bubble-input .ant-input:focus,
        .transcript-bubble-input .ant-input:focus-within {
          box-shadow: none;
        }
        .transcript-bubble-actions {
          display: none;
        }
        .transcript-bubble-hint {
          display: none;
        }
        .transcript-player-anchor {
          position: relative;
          flex: 1;
          min-height: 0;
          display: flex;
        }
        .transcript-player-anchor > .transcript-workspace-card {
          flex: 1;
        }
        .transcript-list {
          min-width: 0;
          padding-top: 4px;
        }
        .transcript-player {
          display: flex;
          align-items: center;
          gap: 16px;
          padding: 14px 16px;
          border-radius: 20px;
          background: rgba(255, 255, 255, 0.92);
          border: 1px solid rgba(225, 231, 246, 0.95);
          box-shadow: 0 18px 44px rgba(72, 86, 128, 0.16);
          backdrop-filter: blur(24px);
        }
        .transcript-player--floating {
          position: fixed;
          bottom: 84px;
          z-index: 100;
          max-width: calc(100vw - 32px);
        }
        .player-main-btn,
        .player-ghost-btn {
          border: 0;
          cursor: pointer;
          display: inline-flex;
          align-items: center;
          justify-content: center;
        }
        .player-main-btn {
          width: 48px;
          height: 48px;
          border-radius: 50%;
          background: linear-gradient(135deg, #5f51ff, #6b7dff);
          color: #fff;
          font-size: 18px;
          box-shadow: 0 10px 24px rgba(95, 81, 255, 0.28);
        }
        .player-ghost-btn {
          gap: 6px;
          padding: 0 12px;
          height: 38px;
          border-radius: 12px;
          background: rgba(95, 81, 255, 0.08);
          color: #5f51ff;
          font-weight: 700;
        }
        .player-progress-shell {
          flex: 1;
          display: flex;
          flex-direction: column;
          gap: 8px;
          min-width: 0;
        }
        .player-time-row {
          display: flex;
          align-items: center;
          justify-content: space-between;
          color: #8b94b5;
          font-size: 12px;
          font-weight: 700;
        }
        .player-range {
          width: 100%;
          appearance: none;
          height: 6px;
          border-radius: 999px;
          background: linear-gradient(90deg, rgba(108, 117, 255, 0.92), rgba(108, 117, 255, 0.92)) 0/0% 100% no-repeat,
            linear-gradient(90deg, rgba(219, 224, 247, 0.9), rgba(231, 235, 250, 0.95));
          outline: none;
        }
        .player-range::-webkit-slider-thumb {
          appearance: none;
          width: 16px;
          height: 16px;
          border-radius: 50%;
          background: #fff;
          border: 3px solid #6e75ff;
          box-shadow: 0 4px 12px rgba(110, 117, 255, 0.24);
          cursor: pointer;
        }
        .player-range::-moz-range-thumb {
          width: 16px;
          height: 16px;
          border-radius: 50%;
          background: #fff;
          border: 3px solid #6e75ff;
          box-shadow: 0 4px 12px rgba(110, 117, 255, 0.24);
          cursor: pointer;
        }
        .summary-markdown {
          font-size: 14px;
          line-height: 1.9;
          color: #3e4768;
          min-width: 0;
          width: 100%;
          max-width: 100%;
          overflow-x: hidden;
          padding-bottom: 12px;
          box-sizing: border-box;
        }
        .markdown-body {
          font-size: 14px;
          line-height: 1.8;
          color: #333;
          min-width: 0;
          width: 100%;
          box-sizing: border-box;
          max-width: 100%;
          overflow-wrap: anywhere;
          word-break: break-word;
        }
        .markdown-body p {
          margin-bottom: 16px;
        }
        .markdown-body h1,
        .markdown-body h2,
        .markdown-body h3 {
          margin-top: 24px;
          margin-bottom: 16px;
          font-weight: 600;
          overflow-wrap: anywhere;
          word-break: break-word;
        }
        .markdown-body ul,
        .markdown-body ol,
        .markdown-body li,
        .markdown-body blockquote,
        .markdown-body table,
        .markdown-body tr,
        .markdown-body td,
        .markdown-body th,
        .markdown-body a,
        .markdown-body code,
        .markdown-body pre {
          max-width: 100%;
          overflow-wrap: anywhere;
          word-break: break-word;
        }
        .markdown-body pre {
          overflow-x: auto;
          padding: 12px 14px;
          border-radius: 12px;
          background: rgba(245, 247, 255, 0.96);
        }
        .markdown-body table {
          display: block;
          overflow-x: auto;
        }
        .markdown-body img {
          max-width: 100%;
          height: auto;
        }
        @media (max-width: 1200px) {
          .detail-left-column {
            padding-right: 0;
          }
          .speaker-summary-card {
            grid-template-columns: 1fr;
          }
        }
        @media (max-width: 992px) {
          .empty-transcript-hero__content {
            flex-direction: column;
          }
          .empty-transcript-hero__actions {
            width: 100%;
            flex-wrap: wrap;
          }
          .meeting-share-card {
            width: min(86vw, 292px);
          }
          .detail-side-column {
            height: auto;
          }
          .detail-left-column {
            overflow: visible;
          }
          .transcript-player--floating {
            bottom: 72px;
            max-width: calc(100vw - 24px);
          }
          .section-divider-text {
            white-space: normal;
            text-align: center;
          }
        }
      `}</style>

      {isOwner && (
        <Modal title="编辑会议信息" open={editVisible} onOk={handleUpdateBasic} onCancel={() => setEditVisible(false)} confirmLoading={actionLoading} width={600} forceRender>
          <Form form={form} layout="vertical" style={{ marginTop: 16 }}>
            <Form.Item name="title" label="会议标题" rules={[{ required: true }]}>
              <Input />
            </Form.Item>
            <Form.Item name="tags" label="业务标签">
              <Select mode="tags" placeholder="输入标签后回车" />
            </Form.Item>
            <Form.Item name="participantIds" label="参会人员">
              <Select mode="multiple" placeholder="请选择参会人" showSearch optionFilterProp="children">
                {userList.map((u) => (
                  <Option key={u.userId} value={u.userId}>
                    <Space>
                      <Avatar size="small" icon={<UserOutlined />} />
                      {u.displayName || u.username}
                    </Space>
                  </Option>
                ))}
              </Select>
            </Form.Item>

          </Form>
        </Modal>
      )}

      {isOwner && (
        <Drawer
          title="重新生成 AI 总结"
          width={400}
          onClose={() => setSummaryVisible(false)}
          open={summaryVisible}
          extra={<Button type="primary" onClick={handleReSummary} loading={actionLoading}>开始总结</Button>}
          forceRender
        >
          <Form form={summaryForm} layout="vertical">
            <Form.Item name="summaryModelId" label="总结模型 (LLM)" rules={[{ required: true }]}>
              <Select placeholder="选择 LLM 模型">
                {llmModels.map((model) => (
                  <Option key={model.id} value={model.id}>
                    {model.modelName} {model.isDefault === 1 && <Tag color="gold" style={{ marginLeft: 4 }}>默认</Tag>}
                  </Option>
                ))}
              </Select>
            </Form.Item>
            <Form.Item name="promptId" label="提示词模板" rules={[{ required: true }]}>
              <Select placeholder="选择模板">
                {prompts.map((prompt) => (
                  <Option key={prompt.id} value={prompt.id}>{prompt.templateName}</Option>
                ))}
              </Select>
            </Form.Item>
            <Form.Item name="summaryDetailLevel" label="总结详细程度" rules={[{ required: true }]}>
              <Radio.Group>
                <Radio value="DETAILED">详细</Radio>
                <Radio value="STANDARD">标准</Radio>
                <Radio value="BRIEF">简洁</Radio>
              </Radio.Group>
            </Form.Item>
            <Form.Item
              name="userPrompt"
              label="用户提示词"
              extra="可选，用于补充本次重新总结的关注重点、表达偏好或输出侧重点"
            >
              <Input.TextArea
                placeholder="例如：请重点突出结论、待办事项和风险点"
                autoSize={{ minRows: 4, maxRows: 8 }}
                showCount
                maxLength={1000}
              />
            </Form.Item>
            <Divider />
            <Text type="secondary">重新总结会基于当前语音转录全文重新生成纪要，原有总结内容将被覆盖。</Text>
          </Form>
        </Drawer>
      )}
    </PageContainer>
  );
};

export default MeetingDetail;
