import { useEffect, useMemo, useRef, useState } from 'react'
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
import {
  Avatar,
  Alert,
  Button,
  Dropdown,
  Empty,
  Input,
  List,
  Modal,
  Popover,
  Select,
  Space,
  Spin,
  Tag,
  Typography,
  message,
} from 'antd'
import {
  FolderOutlined,
  EditOutlined,
  DeleteOutlined,
  MoreOutlined,
  PlusOutlined,
  RobotOutlined,
  SearchOutlined,
  ArrowUpOutlined,
  UserOutlined,
  DownOutlined,
  CopyOutlined,
  CheckOutlined,
  StopOutlined,
  ReloadOutlined,
  LinkOutlined,
  LoadingOutlined,
} from '@ant-design/icons'
import ReactMarkdown from 'react-markdown'
import Highlighter from 'react-highlight-words'
import remarkGfm from 'remark-gfm'
import rehypeHighlight from 'rehype-highlight'
import rehypeRaw from 'rehype-raw'
import 'highlight.js/styles/github.css'
import { createChatSession, deleteChatMessage, deleteChatSession, getChatMessages, getChatSessions, markMessageInterrupted, searchChatMessages, sendChatMessageStream, updateChatSessionTitle } from '@/api/chat'
import { getMyProjects } from '@/api/project'
import { getLLMModelConfigs } from '@/api/llmModelConfigs'
import './Chat.css'

const { TextArea } = Input
const { Text } = Typography

const CITATION_RE = /\[(\d+)\]/g

function formatDuration(ms) {
  if (ms == null) return ''
  const totalSeconds = Math.max(0, ms) / 1000
  if (totalSeconds < 60) return `${totalSeconds.toFixed(1)} 秒`
  const minutes = Math.floor(totalSeconds / 60)
  const seconds = Math.round(totalSeconds % 60)
  return `${minutes} 分 ${seconds} 秒`
}

function useElapsedTimer(startedAt, active) {
  const [now, setNow] = useState(Date.now())
  useEffect(() => {
    if (!active) return undefined
    setNow(Date.now())
    const timer = window.setInterval(() => setNow(Date.now()), 500)
    return () => window.clearInterval(timer)
  }, [active, startedAt])
  return active ? Math.max(0, now - (startedAt || now)) : null
}

function ThinkingPanel({ active, log = [], startedAt, durationMs, status, visible, onToggle }) {
  const elapsed = useElapsedTimer(startedAt, active)
  if (active) {
    return (
      <div className="chat-thinking-panel active">
        <div className="chat-thinking-timer">
          <LoadingOutlined spin /> 思考中 · {formatDuration(elapsed)}
        </div>
        {log.length > 0 && (
          <div className="chat-thinking-log">
            {log.map((entry, index) => (
              <div key={index} className="chat-thinking-log-item">
                <span>{entry.message}</span>
                {entry.duration_ms != null && (
                  <span className="chat-thinking-log-time">用时 {formatDuration(entry.duration_ms)}</span>
                )}
              </div>
            ))}
          </div>
        )}
      </div>
    )
  }

  const hasLog = log.length > 0
  const hasDuration = durationMs != null
  if (!hasLog && !hasDuration) return null
  return (
    <div className="chat-thinking-panel collapsed">
      <button
        type="button"
        className="chat-thinking-toggle"
        onClick={onToggle}
        aria-expanded={visible}
      >
        <DownOutlined rotate={visible ? 180 : 0} className="chat-thinking-chevron" />
        <span>思考过程</span>
        {status === 'interrupted' && <span className="chat-thinking-badge">已停止</span>}
        {hasDuration && <span className="chat-thinking-duration">· {formatDuration(durationMs)}</span>}
      </button>
      {visible && (
        <div className="chat-thinking-log">
          {log.map((entry, index) => (
            <div key={index} className="chat-thinking-log-item">
              <span>{entry.message}</span>
              {entry.duration_ms != null && (
                <span className="chat-thinking-log-time">用时 {formatDuration(entry.duration_ms)}</span>
              )}
            </div>
          ))}
          {hasDuration && (
            <div className="chat-thinking-log-item chat-thinking-log-final">
              {status === 'interrupted' ? '已停止' : (status === 'error' ? '生成失败' : '完成')} · 用时 {formatDuration(durationMs)}
            </div>
          )}
        </div>
      )}
    </div>
  )
}

// rehype 插件：将正文中的 [n] 引用编号转换为上角标 <sup>，便于与正文区分。
// 跳过 code/pre 节点，避免破坏代码块中的方括号内容。
function rehypeCitationSup() {
  // 按引用编号统计出现次数，使同一编号的多处引用可以精确区分
  const occurrenceCounters = {}
  const walk = (node, inCode) => {
    if (!node.children) return
    const nextChildren = []
    for (const child of node.children) {
      const childInCode = inCode || child.tagName === 'code' || child.tagName === 'pre'
      if (child.type === 'text' && CITATION_RE.test(child.value)) {
        CITATION_RE.lastIndex = 0
        let lastPush = 0
        let match
        while ((match = CITATION_RE.exec(child.value)) !== null) {
          const citationId = match[1]
          // 即使标记位于代码块内也要计数，保持与后端按文本全文计数的顺序一致
          const occIndex = occurrenceCounters[citationId] = (occurrenceCounters[citationId] ?? -1) + 1
          if (!childInCode) {
            if (match.index > lastPush) {
              nextChildren.push({ type: 'text', value: child.value.slice(lastPush, match.index) })
            }
            nextChildren.push({
              type: 'element',
              tagName: 'sup',
              properties: {
                className: ['chat-citation'],
                'data-citation-id': citationId,
                'data-citation-occ': occIndex,
              },
              children: [{ type: 'text', value: `[${citationId}]` }],
            })
            lastPush = match.index + match[0].length
          }
        }
        if (childInCode) {
          nextChildren.push(child)
        } else if (lastPush < child.value.length) {
          nextChildren.push({ type: 'text', value: child.value.slice(lastPush) })
        }
      } else {
        if (child.type === 'element') walk(child, childInCode)
        nextChildren.push(child)
      }
    }
    node.children = nextChildren
  }
  return (tree) => walk(tree, false)
}

function formatTime(value) {
  if (!value) return ''
  return new Date(value).toLocaleString('zh-CN', {
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
  })
}

function getDateKey(value) {
  if (!value) return 'unknown'
  const date = new Date(value)
  if (Number.isNaN(date.getTime())) return 'unknown'
  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
}

function formatDateGroup(value) {
  if (!value || value === 'unknown') return '未知日期'
  const date = new Date(`${value}T00:00:00`)
  const today = new Date()
  const yesterday = new Date()
  yesterday.setDate(today.getDate() - 1)
  const todayKey = getDateKey(today)
  const yesterdayKey = getDateKey(yesterday)

  if (value === todayKey) return '今天'
  if (value === yesterdayKey) return '昨天'
  if (date.getFullYear() === today.getFullYear()) {
    return `${date.getMonth() + 1}月${date.getDate()}日`
  }
  return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`
}

function stripMarkdown(text) {
  return (text || '')
    .replace(/```[\s\S]*?```/g, ' ')
    .replace(/[#>*_`~\-\[\]()]/g, ' ')
    .replace(/\s+/g, ' ')
    .trim()
}

// 去掉 markdown 语法标记，使关键词能与渲染后的纯文本精确匹配（用于原文定位）
function stripMarkdownForHighlight(text) {
  return String(text || '')
    .replace(/```[\s\S]*?```/g, ' ')
    .replace(/`([^`]*)`/g, '$1')
    .replace(/\*\*([^*]+)\*\*/g, '$1')
    .replace(/\*([^*]+)\*/g, '$1')
    .replace(/__([^_]+)__/g, '$1')
    .replace(/_([^_]+)_/g, '$1')
    .replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
    .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
    .replace(/^#{1,6}\s+/gm, '')
    .replace(/^\s*[-*+]\s+/gm, '')
    .replace(/^\s*\d+\.\s+/gm, '')
    .replace(/^\s*>\s+/gm, '')
    .replace(/~~([^~]+)~~/g, '$1')
    .trim()
}

function buildDocumentPreviewUrl(projectId, filePath, highlight) {
  if (!projectId || !filePath) return ''
  const params = new URLSearchParams()
  params.set('file', filePath)
  if (highlight) params.set('hl', highlight)
  return `/projects/${projectId}/docs?${params.toString()}`
}

// 跳转原文时优先用「支撑句原文」作为定位关键词，文档页据此高亮并滚动到命中位置
function getDocumentJumpKeyword(ref, occData) {
  const quoteText = (occData?.quotes?.[0]?.text || ref?.quotes?.[0]?.text || '').trim()
  if (quoteText) return stripMarkdownForHighlight(quoteText)
  const anchor = (ref?.anchor_text || '').trim()
  return anchor ? stripMarkdownForHighlight(anchor).slice(0, 32) : ''
}

function openDocument(ref, projectId, occData) {
  const previewUrl = buildDocumentPreviewUrl(
    ref?.project_id || projectId,
    ref?.file_path,
    getDocumentJumpKeyword(ref, occData)
  )
  if (previewUrl) window.open(previewUrl, '_blank', 'noopener,noreferrer')
}

function getReferenceExcerpt(ref) {
  return (ref?.excerpt || ref?.anchor_text || '').trim()
}

function getReferenceContext(ref) {
  return (ref?.content || ref?.excerpt || ref?.anchor_text || '').trim()
}

function escapeAngleBrackets(value) {
  return String(value ?? '').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}

// 渲染引用片段：命中的分块区间用「证据条」标出（chat-chunk-band）。
// 提问关键词不再高亮——检索是语义匹配，提问词与原文命中并无字面对应。
function highlightCitation(text, chunkText) {
  const escaped = escapeAngleBrackets(text)
  const chunk = escapeAngleBrackets(chunkText)
  // CommonMark 会在空行处断开段落，跨多段的分块不能整体包 <mark>，否则标签错位
  if (!chunk || /\r?\n\s*\r?\n/.test(chunk) || !escaped.includes(chunk)) {
    return escaped
  }
  const idx = escaped.indexOf(chunk)
  return `${escaped.slice(0, idx)}<mark class="chat-chunk-band">${chunk}</mark>${escaped.slice(idx + chunk.length)}`
}

function loadPendingStops() {
  try {
    return JSON.parse(sessionStorage.getItem('nex-chat-pending-stops') || '{}')
  } catch {
    return {}
  }
}

function savePendingStops(map) {
  try {
    sessionStorage.setItem('nex-chat-pending-stops', JSON.stringify(map))
  } catch {
    // 忽略存储不可用的情况
  }
}

function SearchHighlight({ text, keyword }) {
  const value = text || ''
  if (!keyword) return value
  return (
    <Highlighter
      autoEscape
      highlightClassName="chat-search-highlight"
      searchWords={[keyword]}
      textToHighlight={value}
    />
  )
}

function CitationMarkdown({ children }) {
  return (
    <ReactMarkdown
      remarkPlugins={[remarkGfm]}
      rehypePlugins={[rehypeHighlight, rehypeRaw]}
      components={{
        mark: ({ children, node: _node, ...props }) => <mark {...props}>{children}</mark>,
      }}
    >
      {children}
    </ReactMarkdown>
  )
}

function ReferenceCard({ reference, projectId }) {
  return (
    <span
      className="chat-reference-chip"
      role="button"
      tabIndex={0}
      title="打开原文"
      onClick={() => openDocument(reference, projectId)}
      onKeyDown={(event) => {
        if (event.key === 'Enter' || event.key === ' ') {
          event.preventDefault()
          openDocument(reference, projectId)
        }
      }}
    >
      <span className="chat-reference-number">[{reference.citation_id}]</span>
      <span className="chat-reference-file" title={reference.file_path || reference.file_name}>
        {reference.file_name || reference.file_path}
      </span>
    </span>
  )
}

async function copyText(text) {
  const value = text || ''
  try {
    if (navigator.clipboard?.writeText) {
      await navigator.clipboard.writeText(value)
      return true
    }
  } catch {
    // 回退到 execCommand
  }
  try {
    const textarea = document.createElement('textarea')
    textarea.value = value
    textarea.style.position = 'fixed'
    textarea.style.opacity = '0'
    document.body.appendChild(textarea)
    textarea.select()
    const ok = document.execCommand('copy')
    document.body.removeChild(textarea)
    return ok
  } catch {
    return false
  }
}

function MessageActions({ content, onCopy, onDelete, onRegenerate }) {
  const [copied, setCopied] = useState(false)
  const handleCopy = async () => {
    const ok = await copyText(content)
    if (ok) {
      setCopied(true)
      window.setTimeout(() => setCopied(false), 1500)
    } else {
      message.error('复制失败')
    }
    onCopy?.(ok)
  }
  return (
    <div className="chat-message-actions">
      {onRegenerate && (
        <button
          type="button"
          className="chat-message-action"
          onClick={onRegenerate}
          aria-label="重新生成"
          title="重新生成"
        >
          <ReloadOutlined />
        </button>
      )}
      <button type="button" className="chat-message-action" onClick={handleCopy} aria-label="复制">
        {copied ? <CheckOutlined /> : <CopyOutlined />}
      </button>
      {onDelete && (
        <button type="button" className="chat-message-action danger" onClick={onDelete} aria-label="删除">
          <DeleteOutlined />
        </button>
      )}
    </div>
  )
}

function Chat() {
  const navigate = useNavigate()
  const location = useLocation()
  const [searchParams, setSearchParams] = useSearchParams()
  const [sessions, setSessions] = useState([])
  const [projects, setProjects] = useState([])
  const [models, setModels] = useState([])
  const [currentSession, setCurrentSession] = useState(null)
  const [messages, setMessages] = useState([])
  // 刷新直达已有会话时，首帧即为加载态，避免闪现「新建对话」页
  const [loadingSessions, setLoadingSessions] = useState(
    () => /[?&](session_id|sessionId)=/.test(window.location.search)
  )
  const [loadingMessages, setLoadingMessages] = useState(false)
  const [sending, setSending] = useState(false)
  const [inputValue, setInputValue] = useState('')
  const [searchVisible, setSearchVisible] = useState(false)
  const [searchKeyword, setSearchKeyword] = useState('')
  const [searchResults, setSearchResults] = useState([])
  const [searchLoading, setSearchLoading] = useState(false)
  const [searchedKeyword, setSearchedKeyword] = useState('')
  const [hasSearched, setHasSearched] = useState(false)
  const [newQuestion, setNewQuestion] = useState('')
  const [newProjectId, setNewProjectId] = useState(undefined)
  const [newModelId, setNewModelId] = useState(undefined)
  const [projectPickerOpen, setProjectPickerOpen] = useState(false)
  const [renameVisible, setRenameVisible] = useState(false)
  const [renameSession, setRenameSession] = useState(null)
  const [renameTitle, setRenameTitle] = useState('')
  const messageRefs = useRef(new Map())
  const messageListRef = useRef(null)
  const nearBottomRef = useRef(true)
  const composerRef = useRef(null)
  const abortControllerRef = useRef(null)
  const pendingStopsRef = useRef(loadPendingStops())
  const skipNextOpenSessionRef = useRef(null)
  const searchRequestIdRef = useRef(0)
  const newMode = location.pathname === '/chat/new'

  const sessionId = useMemo(() => {
    const value = searchParams.get('session_id') || searchParams.get('sessionId')
    return value ? Number(value) : null
  }, [searchParams])
  // 刷新/直达已有会话时，会话与消息尚未加载完成前不要闪现「新建对话」页
  const openingExistingSession = Boolean(
    sessionId && !newMode && !currentSession && (loadingSessions || loadingMessages)
  )

  const currentProject = useMemo(
    () => projects.find((item) => item.id === newProjectId),
    [projects, newProjectId]
  )
  const currentModel = useMemo(
    () => models.find((item) => item.config_id === newModelId),
    [models, newModelId]
  )
  const canCreateSession = Boolean(newQuestion.trim() && currentProject && currentModel)
  const canSendMessage = Boolean(inputValue.trim() && currentSession)

  const markPendingStop = (sessionId) => {
    pendingStopsRef.current[String(sessionId)] = true
    savePendingStops(pendingStopsRef.current)
  }

  const clearPendingStop = (sessionId) => {
    if (pendingStopsRef.current[String(sessionId)]) {
      delete pendingStopsRef.current[String(sessionId)]
      savePendingStops(pendingStopsRef.current)
    }
  }

  const groupedSessions = useMemo(() => {
    const groups = []
    const groupMap = new Map()
    sessions.forEach((item) => {
      const key = getDateKey(item.updated_at || item.created_at)
      if (!groupMap.has(key)) {
        const group = {
          key,
          label: formatDateGroup(key),
          items: [],
        }
        groupMap.set(key, group)
        groups.push(group)
      }
      groupMap.get(key).items.push(item)
    })
    return groups
  }, [sessions])

  const loadProjects = async () => {
    try {
      const res = await getMyProjects()
      setProjects(res.data || [])
    } catch (error) {
      console.error(error)
    }
  }

  const loadModels = async () => {
    try {
      const res = await getLLMModelConfigs({ page: 1, page_size: 100, model_type: 'chat', is_active: true })
      const nextModels = res.data || []
      setModels(nextModels)
      const defaultModel = nextModels.find((item) => item.is_default) || nextModels[0]
      setNewModelId((current) => current || defaultModel?.config_id)
    } catch (error) {
      console.error(error)
    }
  }

  const loadSessions = async () => {
    setLoadingSessions(true)
    try {
      const res = await getChatSessions()
      setSessions(res.data || [])
    } catch (error) {
      message.error('加载对话列表失败')
    } finally {
      setLoadingSessions(false)
    }
  }

  // 会话产生新活动后，本地更新 updated_at 并重排，避免整表重载导致的闪烁/跳动
  const touchSession = (sessionId) => {
    const now = new Date().toISOString()
    setSessions((prev) => {
      const next = prev.map((item) => (
        item.session_id === sessionId ? { ...item, updated_at: now } : item
      ))
      return next.sort((a, b) => (
        new Date(b.updated_at || 0).getTime() - new Date(a.updated_at || 0).getTime()
      ))
    })
  }

  const openSession = async (id, extraQuery = {}) => {
    if (!id) return
    const nextQuery = new URLSearchParams()
    nextQuery.set('session_id', String(id))
    if (extraQuery.message_id) {
      nextQuery.set('message_id', String(extraQuery.message_id))
    }
    navigate({ pathname: '/chat', search: `?${nextQuery.toString()}` }, { replace: true })
  }

  const loadSessionMessages = async (id) => {
    setLoadingMessages(true)
    try {
      const res = await getChatMessages(id)
      const rawMessages = res.data || []
      const nextMessages = rawMessages.map((item) => {
        // 后端返回 completed/interrupted/error/pending，前端统一映射为展示状态
        let status = item.status || 'pending'
        if (status === 'completed') status = 'done'
        if (item.role !== 'assistant') status = 'done'
        return {
          ...item,
          status,
          durationMs: item.duration_ms ?? null,
          thinkingLog: Array.isArray(item.thinking_log) ? item.thinking_log : [],
          thinkingVisible: false,
        }
      })

      // 停止后后端写入中断状态需要一点时间：若该会话存在未确认的停止，
      // 且最后一条助手消息仍为空，则把它标记为“已停止”，避免显示“思考中”。
      if (pendingStopsRef.current[String(id)]) {
        const lastAssistantIndex = [...nextMessages].reverse().findIndex((m) => m.role === 'assistant')
        if (lastAssistantIndex !== -1) {
          const idx = nextMessages.length - 1 - lastAssistantIndex
          const lastAssistant = nextMessages[idx]
          if (!(lastAssistant.content || '').trim()) {
            nextMessages[idx] = { ...lastAssistant, status: 'interrupted' }
            // 后端中断状态尚未写入：保留待确认标记，下次加载继续兜底
          } else {
            clearPendingStop(id)
          }
        } else {
          clearPendingStop(id)
        }
      }

      setMessages(nextMessages)
      setCurrentSession(sessions.find((item) => item.session_id === id) || null)
    } catch (error) {
      message.error('加载对话失败')
    } finally {
      setLoadingMessages(false)
    }
  }

  useEffect(() => {
    loadProjects()
    loadModels()
    loadSessions()
  }, [])

  useEffect(() => {
    if (newMode) {
      setCurrentSession(null)
      setMessages([])
    }
  }, [newMode])

  useEffect(() => {
    if (!sessionId || newMode) {
      if (!newMode) {
        setCurrentSession(null)
        setMessages([])
      }
      return
    }
    if (skipNextOpenSessionRef.current === sessionId) {
      skipNextOpenSessionRef.current = null
      return
    }
    loadSessionMessages(sessionId)
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [sessionId, sessions.length, newMode])

  // 新会话打开或消息生成/更新时自动滚动到对话底部；用户向上翻阅时保持不动
  useEffect(() => {
    const el = messageListRef.current
    if (!el || !nearBottomRef.current) return
    el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' })
  }, [messages, currentSession])

  const handleMessageListScroll = () => {
    const el = messageListRef.current
    if (!el) return
    nearBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 120
  }

  // 切换会话后自动聚焦输入框
  useEffect(() => {
    if (currentSession && !newMode) {
      composerRef.current?.focus()
    }
  }, [currentSession?.session_id, newMode])

  useEffect(() => {
    const target = searchParams.get('message_id')
    if (!target) return
    const targetMessage = messageRefs.current.get(String(target))
    if (!targetMessage) return

    targetMessage.scrollIntoView({ behavior: 'smooth', block: 'center' })
    const nextSearchParams = new URLSearchParams(searchParams)
    nextSearchParams.delete('message_id')
    setSearchParams(nextSearchParams, { replace: true })
  }, [messages, searchParams, setSearchParams])

  const handleStartNew = () => {
    setNewQuestion('')
    setNewProjectId(undefined)
    setNewModelId(models.find((item) => item.is_default)?.config_id || models[0]?.config_id)
    navigate('/chat/new')
  }

  const handleCreateSession = async () => {
    if (sending) return
    const question = newQuestion.trim()
    if (!question) {
      message.warning('请输入问题')
      return
    }
    if (!newProjectId) {
      message.warning('请选择项目')
      return
    }
    if (!newModelId) {
      message.warning('请选择模型')
      return
    }
    const title = question.length > 24 ? `${question.slice(0, 24)}...` : question
    setSending(true)
    let session = null
    try {
      const res = await createChatSession(newProjectId, newModelId, title)
      session = res.data
    } catch (error) {
      message.error(error.response?.data?.detail || error.message || '新建对话失败')
      setSending(false)
      return
    }

    const nextSession = {
      session_id: session.session_id,
      project_id: session.project_id ?? newProjectId,
      llm_config_id: session.llm_config_id ?? newModelId,
      title: session.title || title,
      created_at: session.created_at,
      updated_at: session.updated_at || session.created_at,
    }
    const optimisticMessage = {
      id: `tmp-${Date.now()}`,
      role: 'user',
      content: question,
      created_at: new Date().toISOString(),
    }

    setSessions((prev) => [
      nextSession,
      ...prev.filter((item) => item.session_id !== nextSession.session_id),
    ])
    setCurrentSession(nextSession)
    setNewQuestion('')
    skipNextOpenSessionRef.current = session.session_id
    navigate(`/chat?session_id=${session.session_id}`, { replace: true })

    clearPendingStop(nextSession.session_id)
    try {
      await sendMessageWithStream(nextSession, question, {
        initialMessages: [optimisticMessage],
        initialUserMessageId: optimisticMessage.id,
        includeUserMessage: false,
      })
      touchSession(nextSession.session_id)
    } catch (error) {
      message.error(error.response?.data?.detail || error.message || '对话已创建，但首条消息发送失败')
    } finally {
      setSending(false)
    }
  }

  const handleSend = async () => {
    if (sending) return
    const content = inputValue.trim()
    if (!content || !currentSession) return
    setSending(true)
    setInputValue('')
    clearPendingStop(currentSession.session_id)
    try {
      await sendMessageWithStream(currentSession, content)
      touchSession(currentSession.session_id)
    } catch (error) {
      message.error(error.response?.data?.detail || error.message || '发送消息失败')
    } finally {
      setSending(false)
    }
  }

  const handleStopGenerating = () => {
    abortControllerRef.current?.abort()
  }

  const handleRegenerate = async (item) => {
    if (sending) return
    const index = messages.findIndex((m) => m.id === item.id)
    if (index < 0) return
    const userMsg = [...messages.slice(0, index)].reverse().find((m) => m.role === 'user')
    if (!userMsg || !currentSession) return

    const isRealId = typeof item.id === 'number' || /^\d+$/.test(String(item.id))
    if (isRealId) {
      try {
        await deleteChatMessage(item.id)
      } catch (error) {
        message.error(error.response?.data?.detail || '删除旧回复失败')
        return
      }
    }

    const baseMessages = messages.filter((m) => m.id !== item.id)
    setMessages(baseMessages)
    clearPendingStop(currentSession.session_id)
    setSending(true)
    try {
      await sendMessageWithStream(currentSession, userMsg.content, {
        initialMessages: baseMessages,
        includeUserMessage: false,
        insertUserMessage: false,
      })
      touchSession(currentSession.session_id)
    } catch (error) {
      message.error(error.response?.data?.detail || error.message || '重新生成失败')
    } finally {
      setSending(false)
    }
  }

  const sendMessageWithStream = async (session, content, options = {}) => {
    const userMessage = {
      id: `tmp-user-${Date.now()}`,
      role: 'user',
      content,
      created_at: new Date().toISOString(),
    }
    const assistantMessage = {
      id: `tmp-assistant-${Date.now()}`,
      role: 'assistant',
      content: '',
      references: [],
      status: 'thinking',
      referencesVisible: false,
      thinkingLog: [],
      thinkingStartedAt: Date.now(),
      durationMs: null,
      thinkingVisible: false,
      created_at: new Date().toISOString(),
    }
    const baseMessages = options.initialMessages ?? messages
    const nextMessages = options.includeUserMessage === false
      ? [...baseMessages, assistantMessage]
      : [...baseMessages, userMessage, assistantMessage]

    setMessages(nextMessages)

    // 流式过程中后端会回传真实消息 id（ids 事件），这里用可变变量持续追踪当前
    // 助手消息的 id：替换为真实 id 后，后续 onChunk/onDone 仍能命中同一条消息。
    let assistantMsgId = assistantMessage.id
    const tmpUserId = options.initialUserMessageId ?? userMessage.id

    const controller = new AbortController()
    abortControllerRef.current = controller

    try {
      await sendChatMessageStream(session.session_id, content, {
        signal: controller.signal,
        insertUserMessage: options.insertUserMessage,
        onIds: (data) => {
          const realUserId = data?.user_message_id
          const realAssistantId = data?.assistant_message_id
          if (realAssistantId != null) assistantMsgId = realAssistantId
          setMessages((prev) => prev.map((item) => {
            if (realUserId != null && item.id === tmpUserId) {
              return { ...item, id: realUserId }
            }
            if (realAssistantId != null && item.id === assistantMessage.id) {
              return { ...item, id: realAssistantId }
            }
            return item
          }))
        },
        onReferences: (refs) => {
          setMessages((prev) => prev.map((item) => (
            item.id === assistantMsgId ? { ...item, references: refs || [] } : item
          )))
        },
        onThinking: (entry) => {
          setMessages((prev) => prev.map((item) => (
            item.id === assistantMsgId
              ? {
                  ...item,
                  thinkingLog: [...(item.thinkingLog || []), entry],
                  thinkingStartedAt: item.thinkingStartedAt || Date.now(),
                }
              : item
          )))
        },
        onChunk: (chunk) => {
          setMessages((prev) => prev.map((item) => (
            item.id === assistantMsgId ? { ...item, content: `${item.content}${chunk}`, status: 'streaming' } : item
          )))
        },
        onTitle: (data) => {
          const nextTitle = data?.title
          if (!nextTitle) return
          const targetId = data.session_id ?? session.session_id
          setSessions((prev) => prev.map((item) => (
            item.session_id === targetId ? { ...item, title: nextTitle } : item
          )))
          setCurrentSession((prev) => (
            prev && prev.session_id === targetId ? { ...prev, title: nextTitle } : prev
          ))
        },
        onDone: (data) => {
          setMessages((prev) => prev.map((item) => (
            item.id === assistantMsgId
              ? {
                  ...item,
                  content: data?.content ?? item.content,
                  status: 'done',
                  durationMs: data?.duration_ms ?? item.durationMs,
                  thinkingLog: Array.isArray(data?.thinking_log) && data.thinking_log.length > 0
                    ? data.thinking_log
                    : item.thinkingLog,
                  thinkingVisible: false,
                  referencesVisible: true,
                }
              : item
          )))
        },
      })
    } catch (error) {
      const aborted = error?.name === 'AbortError'
      if (aborted) {
        // 会话级待确认标记：兜底覆盖后端中断标记写入前的时间窗口
        markPendingStop(session.session_id)

        // 立即把“已停止”写入数据库，保证切换会话/刷新后依然可识别
        const stoppedId = String(assistantMsgId)
        if (/^\d+$/.test(stoppedId)) {
          markMessageInterrupted(Number(stoppedId)).catch(() => {})
        } else {
          // ids 事件尚未到达（例如在检索阶段就点了停止）：先从服务端解析真实 id
          ;(async () => {
            try {
              const res = await getChatMessages(session.session_id)
              const freshMessages = res.data || []
              const lastAssistant = [...freshMessages].reverse().find((m) => m.role === 'assistant')
              if (lastAssistant) {
                const realId = Number(lastAssistant.id)
                await markMessageInterrupted(realId)
                setMessages((prev) => prev.map((item) => (
                  item.id === assistantMessage.id || item.id === realId
                    ? { ...item, id: realId, status: 'interrupted' }
                    : item
                )))
              }
            } catch (err) {
              // 标记失败不阻塞停止流程，会话级待确认标记继续兜底
            }
          })()
        }
      }
      setMessages((prev) => prev.map((item) => (
        item.id === assistantMsgId
          ? {
              ...item,
              status: aborted ? 'interrupted' : 'error',
              error: aborted ? undefined : (error.message || '回答生成失败，请稍后重试'),
              durationMs: aborted
                ? Date.now() - (item.thinkingStartedAt || Date.now())
                : item.durationMs,
              thinkingVisible: false,
              referencesVisible: true,
            }
          : item
      )))
      if (!aborted) {
        throw error
      }
    } finally {
      if (abortControllerRef.current === controller) {
        abortControllerRef.current = null
      }
    }
  }

  const handleDeleteMessage = (item) => {
    const isTemp = typeof item.id === 'string' && item.id.startsWith('tmp-')
    if (isTemp) {
      setMessages((prev) => prev.filter((m) => m.id !== item.id))
      return
    }
    Modal.confirm({
      title: '删除消息',
      content: '确定删除这条消息吗？删除后无法恢复。',
      okText: '删除',
      okType: 'danger',
      cancelText: '取消',
      onOk: async () => {
        try {
          await deleteChatMessage(item.id)
          setMessages((prev) => prev.filter((m) => m.id !== item.id))
        } catch (error) {
          message.error(error.response?.data?.detail || '删除失败')
        }
      },
    })
  }

  const toggleThinking = (id) => {
    setMessages((prev) => prev.map((item) => (
      item.id === id ? { ...item, thinkingVisible: !item.thinkingVisible } : item
    )))
  }

  const handleSearch = async () => {
    const keyword = searchKeyword.trim()
    if (!keyword) {
      searchRequestIdRef.current += 1
      setSearchResults([])
      setSearchedKeyword('')
      setHasSearched(false)
      setSearchLoading(false)
      return
    }
    const requestId = ++searchRequestIdRef.current
    setSearchLoading(true)
    try {
      const res = await searchChatMessages(keyword)
      if (requestId !== searchRequestIdRef.current) return
      setSearchResults((res.data || []).filter((item) => item.role === 'user' || item.role === 'assistant'))
      setSearchedKeyword(keyword)
      setHasSearched(true)
    } catch (error) {
      if (requestId === searchRequestIdRef.current) {
        message.error(error.response?.data?.detail || error.message || '搜索失败')
      }
    } finally {
      if (requestId === searchRequestIdRef.current) {
        setSearchLoading(false)
      }
    }
  }

  const handleSearchKeywordChange = (event) => {
    const value = event.target.value
    setSearchKeyword(value)
    if (!value.trim()) {
      searchRequestIdRef.current += 1
      setSearchResults([])
      setSearchedKeyword('')
      setHasSearched(false)
      setSearchLoading(false)
    }
  }

  const handleSelectSearchResult = async (item) => {
    setSearchVisible(false)
    await openSession(item.session_id, { message_id: item.message_id })
  }

  const handleRenameSession = async () => {
    const title = renameTitle.trim()
    if (!renameSession || !title) {
      message.warning('请输入对话名称')
      return
    }
    try {
      await updateChatSessionTitle(renameSession.session_id, title)
      setSessions((prev) => prev.map((item) => (
        item.session_id === renameSession.session_id ? { ...item, title } : item
      )))
      if (currentSession?.session_id === renameSession.session_id) {
        setCurrentSession((prev) => ({ ...prev, title }))
      }
      setRenameVisible(false)
      setRenameSession(null)
      setRenameTitle('')
    } catch (error) {
      message.error(error.response?.data?.detail || '重命名失败')
    }
  }

  const handleDeleteSession = (session) => {
    Modal.confirm({
      title: '删除对话',
      content: `确定删除「${session.title}」吗？删除后无法恢复。`,
      okText: '删除',
      okType: 'danger',
      cancelText: '取消',
      onOk: async () => {
        try {
          await deleteChatSession(session.session_id)
          setSessions((prev) => prev.filter((item) => item.session_id !== session.session_id))
          if (currentSession?.session_id === session.session_id) {
            setCurrentSession(null)
            setMessages([])
            navigate('/chat', { replace: true })
          }
        } catch (error) {
          message.error(error.response?.data?.detail || '删除失败')
        }
      },
    })
  }

  const renderSessionList = () => (
    <div className="chat-history-panel">
      {loadingSessions ? (
        <div className="chat-panel-loading">
          <Spin />
        </div>
      ) : sessions.length === 0 ? (
        <Empty description="暂无对话记录" />
      ) : (
        groupedSessions.map((group) => (
          <div className="chat-session-group" key={group.key}>
            <div className="chat-session-group-title">{group.label}</div>
            <List
              dataSource={group.items}
              renderItem={(item) => {
                const active = currentSession?.session_id === item.session_id
                const project = projects.find((project) => project.id === item.project_id)
                const menuItems = [
                  {
                    key: 'rename',
                    icon: <EditOutlined />,
                    label: '重命名',
                  },
                  {
                    key: 'delete',
                    icon: <DeleteOutlined />,
                    label: '删除',
                    danger: true,
                  },
                ]
                return (
                  <div
                    className={`chat-session-item${active ? ' active' : ''}`}
                    onClick={() => openSession(item.session_id)}
                  >
                    <div className="chat-session-item-top">
                      <div className="chat-session-title-area">
                        <div className="chat-session-title-row">
                          <div className="chat-session-title">{item.title}</div>
                          <Dropdown
                            menu={{
                              items: menuItems,
                              onClick: ({ key, domEvent }) => {
                                domEvent.stopPropagation()
                                if (key === 'rename') {
                                  setRenameSession(item)
                                  setRenameTitle(item.title)
                                  setRenameVisible(true)
                                }
                                if (key === 'delete') {
                                  handleDeleteSession(item)
                                }
                              },
                            }}
                            trigger={['click']}
                          >
                            <button
                              type="button"
                              className="chat-session-more"
                              onClick={(event) => event.stopPropagation()}
                              aria-label="更多操作"
                            >
                              <MoreOutlined />
                            </button>
                          </Dropdown>
                        </div>
                        <div className="chat-session-meta">
                          <Tag className="chat-session-meta-text" icon={<FolderOutlined />}>{project?.name || '未命名项目'}</Tag>
                        </div>
                      </div>
                    </div>
                  </div>
                )
              }}
            />
          </div>
        ))
      )}
    </div>
  )

  const renderMessages = () => (
    <>
      <div className="chat-message-list" ref={messageListRef} onScroll={handleMessageListScroll}>
        {loadingMessages ? (
          <div className="chat-panel-loading">
            <Spin />
          </div>
        ) : messages.length === 0 ? (
          <Empty description="开始输入问题" />
        ) : (
          messages.map((item) => {
            const isUser = item.role === 'user'
            const refs = item.references || []
            const rawContent = item.content || ''
            const displayContent = rawContent
            // 生成是否完成/被中断由后端 status 字段决定，不再比对回复内容
            const status = item.status || 'pending'
            const isInterrupted = !isUser && status === 'interrupted'
            const isError = !isUser && status === 'error'
            const isLive = !isUser && (status === 'thinking' || status === 'streaming' || status === 'pending')
            const hasContent = Boolean(displayContent.trim())
            const isThinking = !isUser && !hasContent && isLive
            const isStreaming = !isUser && status === 'streaming'
            const thinkingLog = Array.isArray(item.thinkingLog) ? item.thinkingLog : []
            const showReferences = !isUser && item.referencesVisible !== false && refs.length > 0
            const canDelete = typeof item.id === 'number' || /^\d+$/.test(String(item.id))
            const projectId = currentSession?.project_id
            return (
              <div
                key={item.id}
                ref={(node) => {
                  if (node) {
                    messageRefs.current.set(String(item.id), node)
                  } else {
                    messageRefs.current.delete(String(item.id))
                  }
                }}
                className={`chat-message-row ${isUser ? 'user' : 'assistant'}`}
              >
                <div className="chat-message-column">
                  <div className={`chat-message-bubble ${isUser ? 'user' : 'assistant'}`}>
                    {isUser ? (
                      <div className="chat-plain-text">{displayContent}</div>
                    ) : (
                      <>
                        {isError && !hasContent ? (
                          <div className="chat-message-error">
                            {item.error || '回答生成失败，请稍后重试'}
                          </div>
                        ) : isInterrupted && !hasContent ? (
                          <div className="chat-message-stopped">已停止生成</div>
                        ) : isThinking ? (
                          <ThinkingPanel active log={thinkingLog} startedAt={item.thinkingStartedAt} />
                        ) : (
                          <>
                            {isStreaming && (
                              <ThinkingPanel active log={thinkingLog} startedAt={item.thinkingStartedAt} />
                            )}
                            {!isLive && (thinkingLog.length > 0 || item.durationMs != null) && (
                              <ThinkingPanel
                                log={thinkingLog}
                                durationMs={item.durationMs}
                                status={isInterrupted ? 'interrupted' : (isError ? 'error' : 'done')}
                                visible={item.thinkingVisible}
                                onToggle={() => toggleThinking(item.id)}
                              />
                            )}
                            <div className={`chat-markdown${isStreaming ? ' streaming' : ''}`}>
                            <ReactMarkdown
                              remarkPlugins={[remarkGfm]}
                              rehypePlugins={[rehypeHighlight, rehypeCitationSup]}
                              components={{
                                sup: ({ children, ...props }) => {
                                  const citationId = Number(props['data-citation-id'])
                                  const occIndex = Number(props['data-citation-occ'] ?? 0)
                                  const ref = refs.find((itemRef) => Number(itemRef.citation_id) === citationId)
                                  if (!ref) {
                                    return <sup {...props}>{children}</sup>
                                  }
                                  const occData = Array.isArray(ref?.quote_occurrences)
                                    ? ref.quote_occurrences[occIndex]
                                    : null
                                  const quotes = occData?.quotes?.length
                                    ? occData.quotes
                                    : (Array.isArray(ref?.quotes) ? ref.quotes : [])
                                  const claim = occData?.claim || ''
                                  const excerpt = getReferenceExcerpt(ref)
                                  const context = getReferenceContext(ref)
                                  const hasBand = Boolean(ref?.content && excerpt && context.includes(excerpt))
                                  return (
                                    <Popover
                                      trigger="hover"
                                      placement="top"
                                      overlayClassName="chat-citation-popover"
                                      content={(
                                        <div className="chat-citation-preview">
                                          <div className="chat-citation-preview-head">
                                            <Text type="secondary" className="chat-citation-preview-file" ellipsis={{ tooltip: ref.file_path || ref.file_name }}>
                                              {ref.file_name || ref.file_path}
                                            </Text>
                                          </div>
                                          <div className="chat-citation-preview-content">
                                            {quotes.length > 0 ? (
                                              <div className="chat-citation-quotes">
                                                {claim && (
                                                  <div className="chat-citation-claim">回答：{claim}</div>
                                                )}
                                                {quotes.map((quote, quoteIdx) => (
                                                  <div key={quoteIdx} className="chat-citation-quote-row">
                                                    <span className="chat-citation-quote">原文：{quote.text}</span>
                                                    <LinkOutlined
                                                      className="chat-citation-quote-open"
                                                      title="查看原文"
                                                      onClick={() => openDocument(ref, projectId, occData)}
                                                    />
                                                  </div>
                                                ))}
                                              </div>
                                            ) : (
                                              <div className="chat-citation-fallback">
                                                <CitationMarkdown>
                                                  {highlightCitation(context, hasBand ? excerpt : '') || '暂无引用片段'}
                                                </CitationMarkdown>
                                                <LinkOutlined
                                                  className="chat-citation-quote-open"
                                                  title="查看原文"
                                                  onClick={() => openDocument(ref, projectId, occData)}
                                                />
                                              </div>
                                            )}
                                          </div>
                                        </div>
                                      )}
                                    >
                                      <sup {...props}>{children}</sup>
                                    </Popover>
                                  )
                                },
                              }}
                            >
                              {displayContent}
                            </ReactMarkdown>
                            {isError && (
                              <div className="chat-message-error chat-message-error-inline">
                                {item.error || '回答生成中断，请稍后重试'}
                              </div>
                            )}
                            {isInterrupted && hasContent && (
                              <div className="chat-message-stopped">已停止生成</div>
                            )}
                            </div>
                          </>
                        )}
                        {showReferences && (
                          <div className="chat-references">
                            <div className="chat-references-head">
                              <Text type="secondary" className="chat-references-label">引用来源</Text>
                              <Text type="secondary" className="chat-references-count">
                                {refs.length} 个来源
                              </Text>
                            </div>
                            <div className="chat-references-list">
                              {refs.map((ref) => {
                                return (
                                  <ReferenceCard
                                    key={`${ref.citation_id}-${ref.file_path || ref.file_name}`}
                                    reference={ref}
                                    projectId={projectId}
                                  />
                                )
                              })}
                            </div>
                          </div>
                        )}
                      </>
                    )}
                  </div>
                  {(isUser || !isLive) && (
                    <MessageActions
                      content={displayContent}
                      onDelete={canDelete ? () => handleDeleteMessage(item) : undefined}
                      onRegenerate={!isUser && (isInterrupted || isError) ? () => handleRegenerate(item) : undefined}
                    />
                  )}
                </div>
              </div>
            )
          })
        )}
      </div>
      <div className="chat-composer-shell">
        <TextArea
          ref={composerRef}
          className="chat-composer-input"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          autoSize={{ minRows: 1, maxRows: 8 }}
          placeholder="随心输入"
          onPressEnter={(e) => {
            if (!e.shiftKey) {
              e.preventDefault()
              handleSend()
            }
          }}
        />
        <div className="chat-composer-actions">
          <div className="chat-composer-left">
            <span className="chat-composer-pill chat-composer-kb">
              <FolderOutlined className="chat-composer-pill-icon" />
              <span className="chat-composer-pill-text">
                {projects.find((project) => project.id === currentSession?.project_id)?.name || '-'}
              </span>
            </span>
          </div>
          <div className="chat-composer-right">
            <span className="chat-composer-pill chat-composer-model">
              <RobotOutlined className="chat-composer-pill-icon" />
              <span className="chat-composer-pill-text">
                {models.find((model) => model.config_id === currentSession?.llm_config_id)?.model_name || '-'}
              </span>
            </span>
            {sending ? (
              <Button
                shape="circle"
                icon={<StopOutlined />}
                onClick={handleStopGenerating}
                title="停止生成"
                aria-label="停止生成"
                className="chat-stop-button"
              />
            ) : (
              <Button
                shape="circle"
                icon={<ArrowUpOutlined />}
                onClick={handleSend}
                disabled={!canSendMessage}
                aria-disabled={!canSendMessage}
                className="chat-send-button"
              />
            )}
          </div>
        </div>
      </div>
    </>
  )

  const renderChatShell = () => (
    <div className="chat-shell">
      <div className="chat-header">
        <div className="chat-header-main">
          <Text className="chat-header-title">{currentSession?.title || '对话'}</Text>
        </div>
      </div>
      {renderMessages()}
    </div>
  )

  const renderNewShell = () => (
    <div className="chat-start-shell">
      <div className="chat-new-page-card">
        <div className="chat-start-title">你希望了解什么？</div>
        {projects.length === 0 && (
          <Alert
            type="warning"
            showIcon
            message="暂无可用知识库"
            description="请先在「项目空间」创建项目，再进入这里提问。"
          />
        )}
        {projects.length > 0 && models.length === 0 && (
          <Alert
            type="warning"
            showIcon
            message="暂无可用对话模型"
            description="请先到「系统管理 - 模型配置」添加并启用对话模型。"
          />
        )}
        <div className="chat-composer-shell chat-new-composer">
          <TextArea
            value={newQuestion}
            onChange={(e) => setNewQuestion(e.target.value)}
            className="chat-composer-input"
            autoFocus
            autoSize={{ minRows: 2, maxRows: 10 }}
            placeholder="随心输入"
            onPressEnter={(e) => {
              if (!e.shiftKey) {
                e.preventDefault()
                handleCreateSession()
              }
            }}
          />
          <div className="chat-composer-actions">
            <div className="chat-composer-left">
              <Popover
                open={projectPickerOpen}
                onOpenChange={setProjectPickerOpen}
                trigger="click"
                placement="topLeft"
                content={
                  <div className="chat-project-picker">
                    <Select
                      value={newProjectId}
                      placeholder="选择知识库"
                      options={projects.map((item) => ({ label: item.name, value: item.id }))}
                      onChange={(value) => {
                        setNewProjectId(value)
                        setProjectPickerOpen(false)
                      }}
                      suffixIcon={<DownOutlined />}
                      showSearch
                      optionFilterProp="label"
                      className="chat-project-picker-select"
                    />
                  </div>
                }
              >
                <button type="button" className="chat-composer-plus" aria-label="选择知识库">
                  <PlusOutlined />
                </button>
              </Popover>
              {currentProject && (
                <span className="chat-composer-pill chat-composer-kb">
                  <FolderOutlined className="chat-composer-pill-icon" />
                  <span className="chat-composer-pill-text">{currentProject.name}</span>
                </span>
              )}
            </div>
            <div className="chat-composer-right">
              <Select
                value={newModelId}
                placeholder="选择模型"
                options={models.map((item) => ({ label: item.model_name, value: item.config_id }))}
                onChange={setNewModelId}
                suffixIcon={<DownOutlined />}
                showSearch
                optionFilterProp="label"
                className="chat-composer-model-select"
              />
              {sending ? (
                <Button
                  shape="circle"
                  icon={<StopOutlined />}
                  onClick={handleStopGenerating}
                  title="停止生成"
                  aria-label="停止生成"
                  className="chat-stop-button"
                />
              ) : (
                <Button
                  shape="circle"
                  icon={<ArrowUpOutlined />}
                  onClick={handleCreateSession}
                  disabled={!canCreateSession}
                  aria-disabled={!canCreateSession}
                  className="chat-send-button"
                />
              )}
            </div>
          </div>
        </div>
      </div>
    </div>
  )

  return (
    <div className="chat-page-shell">
      <aside className="chat-left-panel">
        <div className="chat-left-actions">
          <button className="chat-action-entry" onClick={handleStartNew}>
            <EditOutlined />
            <span>新建对话</span>
          </button>
          <button className="chat-action-entry" onClick={() => setSearchVisible(true)}>
            <SearchOutlined />
            <span>搜索对话</span>
          </button>
        </div>
        {renderSessionList()}
      </aside>

      <main className="chat-main-panel">
        {openingExistingSession ? (
          <div className="chat-panel-loading">
            <Spin />
          </div>
        ) : currentSession && !newMode ? renderChatShell() : renderNewShell()}
      </main>

      <Modal
        title="搜索聊天内容"
        open={searchVisible}
        onCancel={() => setSearchVisible(false)}
        footer={null}
        width={840}
        destroyOnClose
        className="chat-search-modal"
      >
        <Space.Compact className="chat-search-bar">
          <Input
            value={searchKeyword}
            onChange={handleSearchKeywordChange}
            placeholder="输入聊天关键词，仅搜索对话内容"
            allowClear
            onPressEnter={handleSearch}
          />
          <Button type="primary" icon={<SearchOutlined />} loading={searchLoading} onClick={handleSearch}>
            搜索
          </Button>
        </Space.Compact>
        <div className="chat-search-result-list">
          {searchLoading ? (
            <div className="chat-panel-loading">
              <Spin />
            </div>
          ) : (
            <List
              dataSource={searchResults}
              locale={{
                emptyText: (
                  <Empty description={hasSearched ? `未找到“${searchedKeyword}”相关内容` : '输入关键词搜索聊天内容'} />
                ),
              }}
              renderItem={(item) => (
                <List.Item className="chat-search-result-item" onClick={() => handleSelectSearchResult(item)}>
                  <List.Item.Meta
                    avatar={<Avatar icon={item.role === 'assistant' ? <RobotOutlined /> : <UserOutlined />} />}
                    title={
                      <Space wrap>
                        <Text strong>{item.session_title}</Text>
                        <Tag>{item.project_name}</Tag>
                        <Tag>{item.model_name}</Tag>
                      </Space>
                    }
                    description={
                      <>
                        <div className="chat-search-result-snippet">
                          <SearchHighlight
                            text={item.snippet || stripMarkdown(item.content)}
                            keyword={searchedKeyword}
                          />
                        </div>
                        <div className="chat-search-result-time">{formatTime(item.created_at)}</div>
                      </>
                    }
                  />
                </List.Item>
              )}
            />
          )}
        </div>
      </Modal>
      <Modal
        title="重命名对话"
        open={renameVisible}
        onCancel={() => setRenameVisible(false)}
        onOk={handleRenameSession}
        okText="保存"
        cancelText="取消"
      >
        <Input
          value={renameTitle}
          onChange={(event) => setRenameTitle(event.target.value)}
          placeholder="输入新的对话名称"
          maxLength={80}
          showCount
        />
      </Modal>
    </div>
  )
}

export default Chat
