import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Alert, Avatar, Breadcrumb, Button, Card, Checkbox, Col, Divider, Drawer, Empty, Form, Input, List, Modal, Popover, Progress, QRCode, Row, Select, Skeleton, Space, Switch, Tag, Typography, App, Dropdown } from 'antd';
import {
  AudioOutlined,
  CaretRightFilled,
  ClockCircleOutlined,
  CopyOutlined,
  DownloadOutlined,
  EditOutlined,
  FastForwardOutlined,
  LeftOutlined,
  LinkOutlined,
  LoadingOutlined,
  PauseOutlined,
  QrcodeOutlined,
  RobotOutlined,
  SyncOutlined,
  UserOutlined,
  PlusOutlined,
  CheckCircleFilled,
  EllipsisOutlined,
  FilePdfOutlined,
  FileWordOutlined,
  ShareAltOutlined,
} from '@ant-design/icons';
import dayjs from 'dayjs';
import ReactMarkdown from 'react-markdown';
import {
  downloadMeetingSummary,
  getMeetingDetail,
  getMeetingProgress,
  getTranscripts,
  MeetingProgress,
  MeetingTranscriptVO,
  MeetingVO,
  reSummary,
  retryMeetingTranscription,
  updateMeetingBasic,
  updateMeetingTranscript,
  updateMeetingSummary,
  updateSpeakerInfo,
} from '../../api/business/meeting';
import { getAiModelDefault, getAiModelPage, AiModelVO } from '../../api/business/aimodel';
import { getHotWordPage, 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';

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[];
};

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 buildMeetingPreviewUrl = (meetingId?: number, accessPassword?: string) => {
  if (!meetingId || Number.isNaN(meetingId) || typeof window === 'undefined') {
    return '';
  }
  const url = new URL(`/meetings/${meetingId}/preview`, window.location.origin);
  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')}`;
}

const MeetingProgressDisplay: React.FC<{
  meetingId: number;
  onComplete: () => void;
  onProgressUpdate?: (meeting: MeetingVO) => void;
  compact?: boolean;
}> = ({ meetingId, onComplete, onProgressUpdate, compact }) => {
  const [progress, setProgress] = useState<MeetingProgress | null>(null);

  useEffect(() => {
    const fetchProgress = async () => {
      try {
        const [progressRes, detailRes] = await Promise.all([
          getMeetingProgress(meetingId),
          getMeetingDetail(meetingId),
        ]);

        if (detailRes.data?.data) {
          onProgressUpdate?.(detailRes.data.data);
        }

        if (progressRes.data?.data) {
          setProgress(progressRes.data.data);
          if (progressRes.data.data.percent === 100) {
            onComplete();
          }
        }
      } catch {
        // ignore
      }
    };

    fetchProgress();
    const timer = setInterval(fetchProgress, 3000);
    return () => clearInterval(timer);
  }, [meetingId, onComplete, onProgressUpdate]);

  const percent = progress?.percent || 0;
  const isError = percent < 0;

  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 (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 }}>
            {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 }}>
            {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 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>
  );
};

type ActiveTranscriptRowProps = {
  item: MeetingTranscriptVO;
  meetingId: number;
  isOwner: boolean;
  isEditing: boolean;
  isSaving: boolean;
  speakerLabelMap: Map<string, string>;
  onSeek: (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;
  isActive: boolean;
  audioPlaying: boolean;
};

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

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

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

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

  return (
    <List.Item className={`transcript-row ${isActive ? 'active' : ''}`} onClick={() => onSeek(item.startTime)}>
      <div className="transcript-entry" ref={rowRef}>
        <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}
            >
              {item.content}
            </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.onSeek === nextProps.onSeek
  && prevProps.onStartEdit === nextProps.onStartEdit
  && prevProps.onDraftBlur === nextProps.onDraftBlur
  && prevProps.onDraftKeyDown === nextProps.onDraftKeyDown
  && prevProps.onSpeakerUpdated === nextProps.onSpeakerUpdated
  && prevProps.isActive === nextProps.isActive
  && prevProps.audioPlaying === nextProps.audioPlaying
));

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 [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' | null>(null);
  const [isEditingSummary, setIsEditingSummary] = useState(false);
  const [summaryDraft, setSummaryDraft] = useState('');
  const [summaryTab, setSummaryTab] = useState<'chapters' | 'speakers' | 'actions' | 'todos'>('chapters');
  const [expandKeywords, setExpandKeywords] = useState(false);
  const [expandSummary, setExpandSummary] = useState(false);
  const [selectedKeywords, setSelectedKeywords] = useState<string[]>([]);
  const [addingHotwords, setAddingHotwords] = useState(false);
  const [editingTranscriptId, setEditingTranscriptId] = useState<number | null>(null);
  const [savingTranscriptId, setSavingTranscriptId] = useState<number | null>(null);
  const [audioCurrentTime, setAudioCurrentTime] = useState(0);
  const [audioDuration, setAudioDuration] = useState(0);
  const [audioPlaying, setAudioPlaying] = useState(false);
  const [audioPlaybackRate, setAudioPlaybackRate] = useState(1);
  const [llmModels, setLlmModels] = useState<AiModelVO[]>([]);
  const [prompts, setPrompts] = useState<PromptTemplateVO[]>([]);
  const [, 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 audioRef = useRef<HTMLAudioElement>(null);
  const summaryPdfRef = useRef<HTMLDivElement>(null);
  const transcriptItemRefs = useRef<Record<number, HTMLDivElement | 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 fetchData = useCallback(async (meetingId: number) => {
    try {
      const [detailRes, transcriptRes] = await Promise.all([getMeetingDetail(meetingId), getTranscripts(meetingId)]);
      setMeeting(detailRes.data.data);
      setTranscripts(transcriptRes.data.data || []);
    } 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 hasAnalysis = !!(
    analysis.overview ||
    analysis.keywords.length ||
    analysis.chapters.length ||
    analysis.speakerSummaries.length ||
    analysis.keyPoints.length ||
    analysis.todos.length
  );
  const visibleKeywords = expandKeywords ? analysis.keywords : analysis.keywords.slice(0, 9);
  const speakerLabelMap = useMemo(
    () => new Map(speakerLabels.map((item) => [item.itemValue, item.itemLabel])),
    [speakerLabels],
  );
  const previewAccessPassword = useMemo(() => (meeting?.accessPassword || '').trim(), [meeting?.accessPassword]);
  const sharePreviewUrl = useMemo(() => {
    const meetingId = meeting?.id ?? (id ? Number(id) : NaN);
    return buildMeetingPreviewUrl(meetingId);
  }, [meeting?.id, id]);
  const meetingPreviewUrl = useMemo(() => {
    const meetingId = meeting?.id ?? (id ? Number(id) : NaN);
    return buildMeetingPreviewUrl(meetingId, previewAccessPassword);
  }, [meeting?.id, id, previewAccessPassword]);

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

  const canRetrySummary = isOwner && transcripts.length > 0 && meeting?.status !== 1 && meeting?.status !== 2;
  const canRetryTranscription = isOwner && meeting?.status === 4 && transcripts.length === 0 && !!meeting?.audioUrl;

  useEffect(() => {
    if (!meeting?.audioUrl) {
      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?.audioUrl, meeting?.status]);

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

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

  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);

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

    handleLoadedMetadata();

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

  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,
      });
      message.success('已重新发起总结任务');
      setSummaryVisible(false);
      fetchData(Number(id));
    } catch (error) {
      console.error(error);
    } finally {
      setActionLoading(false);
    }
  };

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

  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 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) =>
          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 seekTo = useCallback((timeMs: number) => {
    if (!audioRef.current) return;
    audioRef.current.currentTime = timeMs / 1000;
    audioRef.current.play();
  }, []);

  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 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 : '',
      });
      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>
    );
  }

  return (
    <div style={{ padding: 24, height: 'calc(100vh - 64px)', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
      <Breadcrumb 
        style={{ marginBottom: 16 }} 
        items={[
          { title: <a onClick={() => navigate('/meetings')}>会议中心</a> },
          { title: '会议详情' }
        ]} 
      />

      <Card style={{ marginBottom: 16, flexShrink: 0 }} styles={{ body: { padding: '16px 24px' } }}>
        <Row justify="space-between" align="middle">
          <Col>
            <Space direction="vertical" size={4}>
              <Title level={4} style={{ margin: 0 }}>
                {meeting.title}
                {isOwner && (
                  <EditOutlined style={{ fontSize: 16, cursor: 'pointer', color: '#1890ff', marginLeft: 8 }} onClick={handleEditMeeting} />
                )}
              </Title>
              <Space split={<Divider type="vertical" />}>
                <Text type="secondary">
                  <ClockCircleOutlined /> {dayjs(meeting.meetingTime).format('YYYY-MM-DD HH:mm')}
                </Text>
                <Space>
                  {meeting.tags?.split(',').filter(Boolean).map((tag) => (
                    <Tag key={tag} color="blue">{tag}</Tag>
                  ))}
                </Space>
                <Text type="secondary">
                  <UserOutlined /> {meeting.participants || '未指定'}
                </Text>
              </Space>
            </Space>
          </Col>
          <Col>
              <Space size={8}>
              {canRetrySummary && (
                <Button icon={<SyncOutlined />} type="primary" ghost onClick={handleOpenSummaryDrawer} disabled={actionLoading}>
                  重新总结
                </Button>
              )}
              {canRetryTranscription && (
                <Button icon={<SyncOutlined />} type="primary" onClick={handleRetryTranscription} loading={actionLoading}>
                  重新识别
                </Button>
              )}
              {isOwner && meeting.status === 2 && (
                <Button icon={<LoadingOutlined />} type="primary" ghost disabled loading>
                  正在总结
                </Button>
              )}
              {meeting.status === 3 && !!meeting.summaryContent && (
                <Dropdown
                  menu={{
                    items: [
                      {
                        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 icon={<ShareAltOutlined />}>
                    分享
                  </Button>
                </Popover>
              ) : null}
              <Button icon={<LeftOutlined />} onClick={() => navigate('/meetings')}>
                返回
              </Button>
            </Space>
          </Col>
        </Row>
      </Card>

      <div style={{ flex: 1, minHeight: 0 }}>
        {meeting.status === 1 ? (
          <MeetingProgressDisplay
            meetingId={meeting.id}
            onComplete={() => fetchData(meeting.id)}
            onProgressUpdate={(updated) => {
              if (updated.status !== meeting.status) {
                void fetchData(updated.id);
              }
            }}
          />
        ) : (
          <Row gutter={24} style={{ height: '100%' }}>
            <Col xs={24} lg={14} style={{ height: '100%' }}>
              <div ref={leftColumnRef} className="detail-side-column detail-left-column">
                <Card className="left-flow-card summary-panel" variant="borderless">
                  <div className="summary-head">
                    <div className="summary-title">
                      <RobotOutlined />
                      <span>智能速览</span>
                    </div>
                    <div className="summary-actions">
                        <span className={`status-pill ${hasAnalysis ? 'success' : 'warning'}`}>{hasAnalysis ? '已生成' : '待生成'}</span>
                    </div>
                  </div>

                  <div className="summary-block">
                    <div className="summary-section-head">
                      <span>关键词</span>
                      {isOwner && analysis.keywords.length > 0 && (
                        <Button
                          size="small"
                          type="primary"
                          shape="round"
                          icon={<PlusOutlined />}
                          disabled={!selectedKeywords.length}
                          loading={addingHotwords}
                          onClick={handleAddSelectedHotwords}
                        >
                          加入热词 {selectedKeywords.length > 0 ? `(${selectedKeywords.length})` : ''}
                        </Button>
                      )}
                    </div>
                    {analysis.keywords.length ? (
                      <>
                        <div className="record-tags">
                          {visibleKeywords.map((tag) => {
                            const isSelected = selectedKeywords.includes(tag);
                            return (
                              <div
                                key={tag}
                                className={`tag selectable-tag ${isSelected ? 'selected' : ''}`}
                                onClick={() => isOwner && handleKeywordToggle(tag, !isSelected)}
                              >
                                <span>{tag}</span>
                                {isOwner && isSelected && <CheckCircleFilled style={{ fontSize: 12 }} />}
                              </div>
                            );
                          })}
                        </div>
                        {analysis.keywords.length > 9 && (
                          <button type="button" className="summary-link" onClick={() => setExpandKeywords((value) => !value)}>
                            {expandKeywords ? '收起' : '展开全部'}
                          </button>
                        )}
                      </>
                    ) : (
                      <Text type="secondary">暂无关键词</Text>
                    )}
                  </div>

                  <div className="summary-block">
                    <div className="summary-section-head">
                      <span>全文概要</span>
                    </div>
                    {analysis.overview ? (
                      <>
                        <div className={!expandSummary && analysis.overview.length > 220 ? 'summary-copy summary-fade' : 'summary-copy'}>
                          {analysis.overview}
                        </div>
                        {analysis.overview.length > 220 && (
                          <button type="button" className="summary-link" onClick={() => setExpandSummary((value) => !value)}>
                            {expandSummary ? '收起' : '展开全部'}
                          </button>
                        )}
                      </>
                    ) : (
                      <Text type="secondary">暂无概要</Text>
                    )}
                  </div>

                  <div className="summary-block">
                    <div className="segmented-tabs">
                      <button type="button" className={summaryTab === 'chapters' ? 'active' : ''} onClick={() => setSummaryTab('chapters')}>
                        章节速览
                      </button>
                      <button type="button" className={summaryTab === 'speakers' ? 'active' : ''} onClick={() => setSummaryTab('speakers')}>
                        发言总结
                      </button>
                      <button type="button" className={summaryTab === 'actions' ? 'active' : ''} onClick={() => setSummaryTab('actions')}>
                        要点回顾
                      </button>
                      <button type="button" className={summaryTab === 'todos' ? 'active' : ''} onClick={() => setSummaryTab('todos')}>
                        待办事项
                      </button>
                    </div>

                    {summaryTab === 'chapters' && (
                      <div className="chapter-list">
                        {analysis.chapters.length ? (
                          analysis.chapters.map((item, index) => (
                            <div className="chapter-item" key={`${item.title}-${index}`}>
                              <div className="chapter-time">{item.time || '--:--'}</div>
                              <div className="chapter-card">
                                <strong>{item.title || `章节 ${index + 1}`}</strong>
                                <span>{item.summary || '暂无章节描述'}</span>
                              </div>
                            </div>
                          ))
                        ) : (
                          <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无章节速览" />
                        )}
                      </div>
                    )}

                    {summaryTab === 'speakers' && (
                      <div className="speaker-summary-list">
                        {analysis.speakerSummaries.length ? (
                          analysis.speakerSummaries.map((item, index) => (
                            <div className="speaker-summary-card" key={`${item.speaker}-${index}`}>
                              <div className="speaker-summary-side">
                                  <div className="speaker-avatar">{(item.speaker || '发').slice(0, 1)}</div>
                                  <div className="speaker-summary-name">{item.speaker || `发言人${index + 1}`}</div>
                              </div>
                              <div className="speaker-summary-body">
                                <div className="speaker-summary-meta">发言概述</div>
                                <div>{item.summary || '暂无发言总结'}</div>
                              </div>
                            </div>
                          ))
                        ) : (
                          <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无发言总结" />
                        )}
                      </div>
                    )}

                    {summaryTab === 'actions' && (
                      <div className="keypoint-list">
                        {analysis.keyPoints.length ? (
                          analysis.keyPoints.map((item, index) => (
                            <div className="keypoint-card" key={`${item.title}-${index}`}>
                              <div className="keypoint-badge">{String(index + 1).padStart(2, '0')}</div>
                              <div className="keypoint-content">
                                <strong>{item.title || `要点 ${index + 1}`}</strong>
                                <span>{item.summary || '暂无要点说明'}</span>
                                {(item.speaker || item.time) && (
                                  <div className="keypoint-meta">
                                    {item.speaker ? <span className="summary-tag">{item.speaker}</span> : null}
                                    {item.time ? <span className="summary-tag">{item.time}</span> : null}
                                  </div>
                                )}
                              </div>
                            </div>
                          ))
                        ) : (
                          <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无要点回顾" />
                        )}
                      </div>
                    )}

                    {summaryTab === 'todos' && (
                      <div className="todo-list">
                        {analysis.todos.length ? (
                          analysis.todos.map((item, index) => (
                            <div className="todo-item" key={`${item}-${index}`}>
                              <span className="todo-dot" />
                              <span>{item}</span>
                            </div>
                          ))
                        ) : (
                          <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无待办事项" />
                        )}
                      </div>
                    )}
                  </div>
                </Card>

                <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={<span><AudioOutlined /> 原文</span>}>
                    {meeting.audioUrl && <audio ref={audioRef} src={meeting.audioUrl} style={{ display: 'none' }} preload="metadata" />}
                    {meeting.audioSaveStatus === 'FAILED' && (
                      <Alert
                        type="warning"
                        showIcon
                        style={{ marginBottom: 16 }}
                        message={meeting.audioSaveMessage || '实时会议已完成，但音频保存失败，当前无法播放会议录音。转写和总结不受影响。'}
                      />
                    )}
                    <List
                      className="transcript-list"
                      dataSource={transcripts}
                      style={{ paddingBottom: meeting.audioUrl ? 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}
                            onSeek={seekTo}
                            onStartEdit={handleStartEditTranscript}
                            onDraftBlur={handleTranscriptDraftBlur}
                            onDraftKeyDown={handleTranscriptDraftKeyDown}
                            onSpeakerUpdated={handleTranscriptSpeakerUpdated}
                            isActive={isActive}
                            audioPlaying={audioPlaying}
                          />
                        );
                      }}
                      locale={{ emptyText: meeting.status < 3 ? '识别任务进行中...' : '暂无数据' }}
                    />
                  </Card>
                </div>
              </div>
            </Col>

            <Col xs={24} lg={10} style={{ height: '100%' }}>
              <div className="detail-side-column ai-summary-column">
                <Card
                  className="left-flow-card"
                  variant="borderless"
                  title={<span><RobotOutlined /> AI 总结</span>}
                  extra={
                    meeting.summaryContent && isOwner && (
                      <Space>
                        {isEditingSummary ? (
                          <>
                            <Button size="small" onClick={() => setIsEditingSummary(false)}>取消</Button>
                            <Button size="small" type="primary" onClick={handleSaveSummary} loading={actionLoading}>保存</Button>
                          </>
                        ) : (
                          <Button
                            size="small"
                            type="link"
                            icon={<EditOutlined />}
                            onClick={() => {
                              setSummaryDraft(meeting.summaryContent || '');
                              setIsEditingSummary(true);
                            }}
                          >
                            编辑
                          </Button>
                        )}
                      </Space>
                    )
                  }
                  style={{ height: '100%' }}
                  styles={{ body: { padding: 24, height: '100%', overflowY: 'auto', overflowX: 'hidden', minWidth: 0 } }}
                >
                  <div ref={summaryPdfRef} className="markdown-shell">
                    {meeting.status === 2 ? (
                      <div style={{ textAlign: 'center' }}>
                        <MeetingProgressDisplay
                          meetingId={meeting.id}
                          onComplete={() => fetchData(meeting.id)}
                          compact
                        />
                      </div>
                    ) : meeting.summaryContent ? (
                      isEditingSummary ? (
                        <Input.TextArea
                          value={summaryDraft}
                          onChange={(event) => setSummaryDraft(event.target.value)}
                          style={{ height: '100%', resize: 'none' }}
                        />
                      ) : (
                        <div className="markdown-body summary-markdown">
                          <ReactMarkdown>{meeting.summaryContent}</ReactMarkdown>
                        </div>
                      )
                    ) : (
                      <div style={{ textAlign: 'center', marginTop: 100 }}>
                        <Empty description="暂无总结" />
                      </div>
                    )}
                  </div>
                </Card>
              </div>
            </Col>
          </Row>
        )}
      </div>

      {meeting?.audioUrl && 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)}
              onChange={handleAudioProgressChange}
            />
          </div>
          <button type="button" className="player-ghost-btn" onClick={cyclePlaybackRate}>
            <FastForwardOutlined />
            {audioPlaybackRate}x
          </button>
        </div>
      )}

      <style>{`
        .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: 16px;
        }
        .left-flow-card {
          min-width: 0;
          border-radius: 22px;
          border: 1px solid rgba(228, 232, 245, 0.96);
          box-shadow: 0 16px 34px rgba(127, 139, 186, 0.08);
          background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 255, 0.98));
        }
        .ai-summary-column,
        .ai-summary-column .left-flow-card {
          overflow: hidden;
        }
        .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: 22px;
          padding: 20px 22px 22px;
        }
        .summary-head {
          display: flex;
          align-items: center;
          justify-content: space-between;
          gap: 12px;
        }
        .summary-title {
          display: inline-flex;
          align-items: center;
          gap: 10px;
          color: #7b63ff;
          font-size: 24px;
          font-weight: 800;
        }
        .summary-actions {
          display: inline-flex;
          align-items: center;
          gap: 8px;
        }
        .status-pill {
          display: inline-flex;
          align-items: center;
          height: 28px;
          padding: 0 12px;
          border-radius: 999px;
          font-size: 12px;
          font-weight: 700;
        }
        .status-pill.success {
          background: rgba(82, 196, 26, 0.12);
          color: #389e0d;
        }
        .status-pill.warning {
          background: rgba(250, 173, 20, 0.14);
          color: #d48806;
        }
        .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));
        }
        .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;
        }
        .summary-block {
          display: flex;
          flex-direction: column;
          gap: 12px;
        }
        .summary-section-head {
          display: flex;
          align-items: center;
          justify-content: space-between;
          gap: 12px;
          font-size: 20px;
          font-weight: 700;
          color: #2f3760;
        }
        .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;
        }
        .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.active .transcript-bubble,
        .ant-list-item.transcript-row.active .transcript-bubble-editing {
          border-color: rgba(22, 119, 255, 0.5);
          background: #f0f5ff;
          box-shadow: 0 4px 12px rgba(22, 119, 255, 0.08);
        }
        .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;
        }
        .transcript-avatar {
          flex-shrink: 0;
          background: linear-gradient(135deg, #1677ff, #36cfc9) !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: 14px 18px;
          border-radius: 4px 16px 16px 16px;
          background: var(--app-bg-surface);
          border: 1px solid var(--app-border-color);
          color: var(--app-text-main);
          line-height: 1.6;
          font-size: 15px;
          white-space: pre-wrap;
          transition: all 0.3s ease;
        }
        .transcript-bubble:hover {
          border-color: var(--app-primary-color);
          box-shadow: 0 4px 12px rgba(22, 119, 255, 0.05);
        }
        .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;
        }
        .transcript-list {
          min-width: 0;
        }
        .transcript-player {
          display: flex;
          align-items: center;
          gap: 16px;
          padding: 14px 16px;
          border-radius: 18px;
          background: var(--app-bg-surface);
          border: 1px solid var(--app-border-color);
          box-shadow: 0 16px 40px rgba(0, 0, 0, 0.12);
          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: 44px;
          height: 44px;
          border-radius: 50%;
          background: linear-gradient(135deg, var(--app-primary-color), #36cfc9);
          color: #fff;
          font-size: 18px;
          box-shadow: 0 4px 12px rgba(22, 119, 255, 0.2);
        }
        .player-ghost-btn {
          gap: 6px;
          padding: 0 12px;
          height: 38px;
          border-radius: 12px;
          background: var(--app-bg-surface-soft);
          color: var(--app-primary-color);
          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;
        }
        .markdown-shell {
          height: 100%;
          min-height: 0;
          min-width: 0;
          width: 100%;
          overflow-x: hidden;
          padding-bottom: 28px;
          box-sizing: border-box;
        }
        .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) {
          .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>
            <Text type="warning">参会人员 ID 当前不支持在这里直接修改，如需调整请联系管理员。</Text>

          </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="userPrompt"
              label="用户提示词"
              extra="可选，用于补充本次重新总结的关注重点、表达偏好或输出侧重点"
            >
              <Input.TextArea
                placeholder="例如：请重点突出结论、待办事项和风险点"
                autoSize={{ minRows: 4, maxRows: 8 }}
                showCount
                maxLength={1000}
              />
            </Form.Item>
            <Divider />
            <Text type="secondary">重新总结会基于当前语音转录全文重新生成纪要，原有总结内容将被覆盖。</Text>
          </Form>
        </Drawer>
      )}
    </div>
  );
};

export default MeetingDetail;
