import { useState, useEffect, useRef, useCallback } from 'react'
import { useLocation, useParams } from 'react-router-dom'
import { Modal } from '@arco-design/web-react'
import { getDifyAppParameters, getAppFullInfo, runWorkflow, runChatMessage, consumeSSE, uploadFile, getChatMessages, getConversations } from '@/service/dify'
import { API_BASE_URL } from '@/config'
import agentEmptyStates from '@/config/agentEmptyStates.json'
import emojiMartData from '@emoji-mart/data'
import { marked } from 'marked'
import wordIcon from '@/assets/icons/word.svg'
import pdfIcon from '@/assets/icons/pdf.svg'
import RichTextEditor from '@/components/RichTextEditor'
import { htmlToDocxBlob } from '@/utils/exportDocx'

// 通过 emoji-mart data 把 emoji 名称转换为 native Unicode 字符
const emojiNameToNative = (name) => {
  if (!name) return '🤖'
  const emoji = emojiMartData.emojis?.[name]
  return emoji?.skins?.[0]?.native || '🤖'
}
const isNativeEmoji = (str) => /[\uD800-\uDBFF][\uDC00-\uDFFF]/.test(str || '')
const resolveIcon = (icon) => {
  if (!icon) return '🤖'
  if (isNativeEmoji(icon)) return icon
  return emojiNameToNative(icon)
}

const buildIconUrl = (icon) => {
  if (!icon) return ''
  if (/^https?:\/\//i.test(icon)) return icon
  if (icon.startsWith('/')) return `${API_BASE_URL}${icon}`
  return icon
}

// ===== AI 消息操作图标（内联 SVG，lucide 风格） =====
const IconCopy = () => (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
    <rect x="9" y="9" width="12" height="12" rx="2" />
    <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
  </svg>
)
const IconVolume = () => (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
    <path d="M11 5 6 9H2v6h4l5 4V5z" />
    <path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
    <path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
  </svg>
)
const IconStop = () => (
  <svg viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
    <rect x="6" y="6" width="12" height="12" rx="2" />
  </svg>
)
const IconThumbUp = () => (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
    <path d="M7 10v12" />
    <path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z" />
  </svg>
)
const IconThumbDown = () => (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
    <path d="M17 14V2" />
    <path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z" />
  </svg>
)
const IconRefresh = () => (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
    <path d="M21 12a9 9 0 1 1-2.64-6.36L21 8" />
    <path d="M21 3v5h-5" />
  </svg>
)
const IconEdit = () => (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
    <path d="M12 20h9" />
    <path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z" />
  </svg>
)

marked.setOptions({
  gfm: true,
  breaks: true,
})

const sanitizeHtml = (html) => {
  if (typeof window === 'undefined') return html
  const template = document.createElement('template')
  template.innerHTML = html
  template.content.querySelectorAll('script, iframe, object, embed').forEach(node => node.remove())
  template.content.querySelectorAll('*').forEach(node => {
    Array.from(node.attributes).forEach(attr => {
      const name = attr.name.toLowerCase()
      const value = attr.value || ''
      if (name.startsWith('on')) node.removeAttribute(attr.name)
      if ((name === 'href' || name === 'src') && /^\s*javascript:/i.test(value)) node.removeAttribute(attr.name)
    })
    if (node.tagName.toLowerCase() === 'a' && node.getAttribute('href')) {
      node.setAttribute('target', '_blank')
      node.setAttribute('rel', 'noopener noreferrer')
    }
  })
  return template.innerHTML
}

const renderMarkdown = (text) => {
  if (!text) return ''
  try {
    return sanitizeHtml(marked.parse(text))
  } catch (_) {
    return String(text).replace(/[<>&]/g, c => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;' }[c]))
  }
}

const splitThinkContent = (text) => {
  const source = String(text || '')
  const lower = source.toLowerCase()
  const thoughts = []
  const answers = []
  let cursor = 0
  let thinking = false
  while (cursor < source.length) {
    if (thinking) {
      const end = lower.indexOf('</think>', cursor)
      if (end === -1) {
        thoughts.push(source.slice(cursor))
        cursor = source.length
      } else {
        thoughts.push(source.slice(cursor, end))
        cursor = end + 8
        thinking = false
      }
    } else {
      const start = lower.indexOf('<think>', cursor)
      if (start === -1) {
        answers.push(source.slice(cursor))
        cursor = source.length
      } else {
        answers.push(source.slice(cursor, start))
        cursor = start + 7
        thinking = true
      }
    }
  }
  return {
    thought: thoughts.join('').trim(),
    answer: answers.join('').trim(),
    thinking,
  }
}

// 提取 AI 消息的正文（剔除思考过程与 <step> 过程提示）
const getAnswerText = (message) => {
  const parsed = splitThinkContent(message?.content || '')
  const answer = parsed.answer || (!parsed.thought ? message?.content || '' : '')
  return splitStepContent(answer).text.trim()
}

// 解析流式返回中的 <step>xxx</step> 过程提示（兼容 <step/>xxx</step> 写法与未闭合的进行中状态）
const splitStepContent = (text) => {
  const source = String(text || '')
  const steps = []
  let rest = source.replace(/<step\/?>([\s\S]*?)<\/step>/gi, (_, s) => {
    const v = s.trim()
    if (v) steps.push(v)
    return ''
  })
  // 流式输出中尚未闭合的 step 段，视为进行中的步骤
  rest = rest.replace(/<step\/?>([\s\S]*)$/i, (_, s) => {
    const v = s.trim()
    if (v) steps.push(v)
    return ''
  })
  return { steps, text: rest }
}

// 流式期间当前展示的过程提示：pending 阶段返回占位文案，之后返回最新的 step 提示
const getActiveStepHint = (message) => {
  if (!message?.streaming) return ''
  if (message.pending) return message.content || STREAM_WAITING_TEXT
  const parsed = splitThinkContent(message?.content || '')
  const answer = parsed.answer || (!parsed.thought ? message?.content || '' : '')
  const { steps } = splitStepContent(answer)
  return steps.length ? steps[steps.length - 1] : ''
}

// 复制文本到剪贴板（安全上下文走 Clipboard API，否则降级 execCommand）
const copyTextToClipboard = async (text) => {
  if (navigator.clipboard && window.isSecureContext) {
    try {
      await navigator.clipboard.writeText(text)
      return true
    } catch (_) { /* 失败则继续降级 */ }
  }
  const ta = document.createElement('textarea')
  ta.value = text
  ta.setAttribute('readonly', '')
  ta.style.position = 'fixed'
  ta.style.top = '0'
  ta.style.left = '-9999px'
  ta.style.opacity = '0'
  document.body.appendChild(ta)
  ta.focus()
  ta.select()
  ta.setSelectionRange(0, ta.value.length)
  let ok = false
  try { ok = document.execCommand('copy') } catch (_) { ok = false }
  document.body.removeChild(ta)
  return ok
}

const renderAssistantMessage = (message) => {
  return <AssistantMessageContent message={message} />
}

function AssistantMessageContent({ message }) {
  const parsed = splitThinkContent(message?.content || '')
  const { text } = splitStepContent(parsed.answer || (!parsed.thought ? message?.content : ''))
  const answer = message?.pending ? '' : text
  // 过程提示始终置顶：pending 占位文案 / 最新 step 提示，随流式输出动态替换
  const activeStep = getActiveStepHint(message)
  const thinking = message?.thinking || parsed.thinking
  const prevThinkingRef = useRef(thinking)
  const [thinkOpen, setThinkOpen] = useState(thinking)

  useEffect(() => {
    if (thinking) {
      setThinkOpen(true)
    } else if (prevThinkingRef.current) {
      setThinkOpen(false)
    }
    prevThinkingRef.current = thinking
  }, [thinking])

  return (
    <>
      {activeStep && (
        <div className={`execute-msg-step-hint ${thinking ? 'is-pending' : ''}`} title={activeStep}>
          <span className="execute-msg-step-spinner" />
          <span className="execute-msg-step-label">{activeStep}</span>
        </div>
      )}
      {parsed.thought && (
        <details
          className={`execute-msg-think ${thinking ? 'is-thinking' : ''}`}
          open={thinkOpen}
          onToggle={e => setThinkOpen(e.currentTarget.open)}
        >
          <summary className="execute-msg-think-title">
            <span>{thinking ? '思考中' : '思考过程'}</span>
            {/* <span className="execute-msg-think-toggle">展开/收起</span> */}
          </summary>
          <div
            className="markdown-body execute-msg-think-body"
            dangerouslySetInnerHTML={{ __html: renderMarkdown(parsed.thought) }}
          />
        </details>
      )}
      {answer && (
        <div
          className="markdown-body execute-msg-answer"
          dangerouslySetInnerHTML={{ __html: renderMarkdown(answer) }}
        />
      )}
    </>
  )
}

const STREAM_WAITING_TEXT = '内容生成中'
const AUTO_SCROLL_THRESHOLD = 24

const isScrollAtBottom = (node) => {
  if (!node) return true
  return node.scrollHeight - node.scrollTop - node.clientHeight <= AUTO_SCROLL_THRESHOLD
}

const scrollToBottom = (node) => {
  if (!node) return
  node.scrollTop = node.scrollHeight
}

const formatDownloadTimestamp = () => {
  const d = new Date()
  const pad = (n) => String(n).padStart(2, '0')
  return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`
}

const sanitizeDownloadName = (name) => {
  return String(name || '智能体').replace(/[\\/:*?"<>|]/g, '_').trim() || '智能体'
}

// 导出文档的正文样式（与编辑器基准一致：正文 14px、行高 1.8、标题按级别缩放）
const EXPORT_DOC_STYLE = [
  'body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;font-size:14px;line-height:1.8;color:#1d2129;margin:0;}',
  'h1,h2,h3,h4,h5{margin:0.6em 0 0.3em;line-height:1.4;}',
  'h1{font-size:28px}h2{font-size:24px}h3{font-size:20px}h4{font-size:17px}h5{font-size:15px}',
  'p{margin:0.4em 0;}',
  'ul,ol{padding-left:1.6em;margin:0.4em 0;}',
  'blockquote{margin:0.5em 0;padding:0.2em 1em;border-left:3px solid #e5e6eb;color:#4e5969;background:#f7f8fa;}',
  'pre{background:#f2f3f5;padding:10px 12px;border-radius:6px;white-space:pre-wrap;margin:0.5em 0;}',
  'code{font-family:Consolas,Menlo,monospace;background:#f2f3f5;padding:1px 4px;border-radius:4px;}',
  'table{border-collapse:collapse;margin:0.5em 0;}td,th{border:1px solid #e5e6eb;padding:4px 8px;}',
  'img{max-width:100%;}',
].join('')

// 组装导出用完整 HTML（DOCX 走 altChunk 需要 head 内样式；PDF 截图渲染同样复用）
const buildExportHtml = (bodyHtml, extraStyle = '') =>
  `<!DOCTYPE html><html><head><meta charset="utf-8"><style>${EXPORT_DOC_STYLE}${extraStyle}</style></head><body>${bodyHtml}</body></html>`

// 触发浏览器下载（Blob）
const downloadBlob = (blob, filename) => {
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = filename
  document.body.appendChild(a)
  a.click()
  document.body.removeChild(a)
  setTimeout(() => URL.revokeObjectURL(url), 1000)
}

// HTML 直接导出为 PDF 文件下载（html2canvas 截图 + jsPDF 按 A4 切页），不再调起浏览器打印
const exportHtmlToPdf = async (htmlSource, filename) => {
  const [{ default: html2canvas }, { jsPDF }] = await Promise.all([
    import('html2canvas'),
    import('jspdf'),
  ])

  // 离屏 iframe 渲染导出 HTML：宽度取 A4 @96dpi，保证与打印方案一致的排版基准
  const A4_W = 794
  const frame = document.createElement('iframe')
  frame.setAttribute('aria-hidden', 'true')
  Object.assign(frame.style, {
    position: 'fixed', left: '-9999px', top: '0',
    width: `${A4_W}px`, height: '200px', border: '0',
  })
  document.body.appendChild(frame)

  try {
    await new Promise((resolve, reject) => {
      frame.onload = resolve
      frame.onerror = () => reject(new Error('导出内容渲染失败'))
      frame.srcdoc = htmlSource
    })
    const doc = frame.contentDocument
    // 等待图片等子资源就绪，避免截图内容缺失
    await Promise.all(Array.from(doc.images || []).map(img =>
      img.complete ? Promise.resolve() : new Promise(resolve => { img.onload = img.onerror = resolve })
    ))
    const canvas = await html2canvas(doc.body, {
      scale: 2,
      backgroundColor: '#ffffff',
      useCORS: true,
    })
    if (!canvas.width || !canvas.height) throw new Error('导出内容为空')

    // 收集“安全分页点”：文本行底部 / 块级元素底部，分页优先落在此处，避免一行文字被拦腰切成两半
    // 原子元素（表格行、图片等）内部不允许切割，否则会破坏其视觉结构
    const bodyRect = doc.body.getBoundingClientRect()
    const scaleY = canvas.height / bodyRect.height
    const breakPoints = (() => {
      const pts = new Set()
      const atomics = []
      doc.querySelectorAll('tr,img,svg,video').forEach((el) => {
        const r = el.getBoundingClientRect()
        if (r.height > 0) atomics.push(r)
      })
      const isSafe = (cssBottom) =>
        cssBottom > 0 && cssBottom <= bodyRect.height &&
        !atomics.some((a) => cssBottom > a.top + 1 && cssBottom < a.bottom - 1)
      doc.body.querySelectorAll('*').forEach((el) => {
        const r = el.getBoundingClientRect()
        if (r.height > 0 && isSafe(r.bottom)) pts.add(Math.round((r.bottom - bodyRect.top) * scaleY))
      })
      // 长段落/代码块内部按“文本行”断页
      const walker = doc.createTreeWalker(doc.body, frame.contentWindow.NodeFilter.SHOW_TEXT)
      const range = doc.createRange()
      let node
      while ((node = walker.nextNode())) {
        range.selectNodeContents(node)
        Array.from(range.getClientRects()).forEach((r) => {
          if (r.height > 0 && isSafe(r.bottom)) pts.add(Math.round((r.bottom - bodyRect.top) * scaleY))
        })
      }
      return Array.from(pts).sort((a, b) => a - b)
    })()

    const pdf = new jsPDF({ unit: 'mm', format: 'a4', compress: true })
    const MARGIN = 12
    const contentW = 210 - MARGIN * 2
    const contentH = 297 - MARGIN * 2
    const pxPerMm = canvas.width / contentW
    const pageHpx = Math.max(1, Math.floor(contentH * pxPerMm))
    const MIN_SLICE = 8 // 每页最小保留高度，防止产生退化切片
    let offset = 0
    let pageIndex = 0
    while (offset < canvas.height) {
      let end
      if (canvas.height - offset <= pageHpx + 4) {
        // 剩余内容不足一页（含舍入残余）时整页放下，避免切出近空白的尾页
        end = canvas.height
      } else {
        // 在 (offset + MIN_SLICE, offset + pageHpx] 内取最大的安全分页点；找不到（如整页大图）退回硬切
        end = 0
        for (let i = breakPoints.length - 1; i >= 0; i--) {
          if (breakPoints[i] <= offset + pageHpx && breakPoints[i] > offset + MIN_SLICE) { end = breakPoints[i]; break }
        }
        if (end <= offset) end = offset + pageHpx
      }
      const sliceH = end - offset
      const slice = document.createElement('canvas')
      slice.width = canvas.width
      slice.height = sliceH
      slice.getContext('2d').drawImage(canvas, 0, offset, canvas.width, sliceH, 0, 0, canvas.width, sliceH)
      if (pageIndex > 0) pdf.addPage()
      pdf.addImage(slice.toDataURL('image/jpeg', 0.92), 'JPEG', MARGIN, MARGIN, contentW, sliceH / pxPerMm)
      offset = end
      pageIndex++
    }
    pdf.save(filename)
  } finally {
    frame.remove()
  }
}

// 数字格式化（保留以备后续接口扩展时使用）
// const formatNumber = (num) => {
//   if (num === null || num === undefined) return '0'
//   return Number(num).toLocaleString('en-US')
// }

// 解析时间戳（秒）→ HH:mm:ss
const formatTime = (ts) => {
  if (!ts) return ''
  const d = new Date(typeof ts === 'number' && ts < 1e12 ? ts * 1000 : ts)
  if (Number.isNaN(d.getTime())) return ''
  const pad = (n) => String(n).padStart(2, '0')
  return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
}

// 会话列表时间（时间戳（秒）→ YYYY-MM-DD HH:mm）
const formatConvTime = (ts) => {
  if (!ts) return ''
  const d = new Date(typeof ts === 'number' && ts < 1e12 ? ts * 1000 : ts)
  if (Number.isNaN(d.getTime())) return ''
  const pad = (n) => String(n).padStart(2, '0')
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
}

// 友好模式名
const MODE_LABEL = {
  workflow: '工作流',
  chat: '基础对话',
  'advanced-chat': '对话流',
  'agent-chat': 'Agent 对话',
  agentic: '智能体编排',
  completion: '文本生成'
}

const normalizeAgentName = (name) => String(name || '').toLowerCase().replace(/[\s_-]/g, '')

const getAgentEmptyState = (agent) => {
  const currentName = normalizeAgentName(agent?.name || agent?.title)
  const matched = agentEmptyStates.agents.find(item => {
    const names = [item.name, ...(item.aliases || [])]
    return names.some(name => {
      const normalized = normalizeAgentName(name)
      return normalized && (currentName.includes(normalized) || normalized.includes(currentName))
    })
  })
  return matched || agentEmptyStates.default
}

// 是否为「报告生成」智能体（按名称/别名匹配，与空状态配置同源）
const isReportAgent = (agent) => {
  const currentName = normalizeAgentName(agent?.name || agent?.title)
  if (!currentName) return false
  const reportCfg = agentEmptyStates.agents.find(item => item.name === '报告生成')
  const names = [reportCfg?.name, ...(reportCfg?.aliases || [])]
  return names.some(name => {
    const normalized = normalizeAgentName(name)
    return normalized && (currentName.includes(normalized) || normalized.includes(currentName))
  })
}

function AgentExecute() {
  const { appId } = useParams()
  const location = useLocation()
  const passedAgent = location.state?.agent || { id: appId, name: '智能体' }
  const apiKey = location.state?.apiKey || ''

  // ===== 通用状态 =====
  const [agent, setAgent] = useState(passedAgent)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState('')
  const [mode, setMode] = useState(passedAgent.mode || '')

  // ===== workflow 模式专属 =====
  const [userInputForm, setUserInputForm] = useState([])  // user_input_form 数组
  const [formValues, setFormValues] = useState({})         // {variable: value}
  const [paramsLoading, setParamsLoading] = useState(false)
  const [paramsError, setParamsError] = useState('')
  const [running, setRunning] = useState(false)
  const [runError, setRunError] = useState('')
  const [steps, setSteps] = useState([])                   // 时间线条目
  const [runSummary, setRunSummary] = useState(null)       // workflow_finished 数据
  const timelineRef = useRef(null)                          // 自动滚动到底部
  const timelineAutoScrollRef = useRef(true)
  const runAbortRef = useRef(null)

  // ===== 聊天模式专属（必须放在 useCallback 之前,避免 TDZ 报错） =====
  const [chatRunning, setChatRunning] = useState(false)
  const [chatConversationId, setChatConversationId] = useState('')
  const [chatStreamId, setChatStreamId] = useState(null)   // 当前正在流式输出的 assistant 消息 id
  const [chatRunError, setChatRunError] = useState('')
  const [showChatParamsModal, setShowChatParamsModal] = useState(false)
  const chatAbortRef = useRef(null)
  // 用 ref 镜像 conversation_id,避免 useCallback 因依赖该 state 而频繁重建
  const chatConversationIdRef = useRef('')
  const chatStreamIdRef = useRef(null)
  const activeChatSnapshotRef = useRef(null)
  const viewingHistoryRef = useRef(false)
  const setChatStreamIdSafe = (val) => {
    chatStreamIdRef.current = val
    setChatStreamId(val)
  }
  const [hasActiveChatSnapshot, setHasActiveChatSnapshot] = useState(false)
  const [historyOpen, setHistoryOpen] = useState(false)
  const [historyList, setHistoryList] = useState([])
  const [historyLoading, setHistoryLoading] = useState(false)
  const [historyError, setHistoryError] = useState('')
  const [historyHasMore, setHistoryHasMore] = useState(false)      // 是否还有下一页
  const [historyLoadingMore, setHistoryLoadingMore] = useState(false)
  const historyListRef = useRef([])                                 // 列表镜像（取 last_id 游标）
  const historyLoadingRef = useRef(false)                           // 加载中互斥（防滚动重复触发）
  const [resumingConv, setResumingConv] = useState(null)
  const [openDownloadId, setOpenDownloadId] = useState(null)
  const messagesRef = useRef(null)
  const messagesAutoScrollRef = useRef(true)
  const emptyState = getAgentEmptyState(agent)

  useEffect(() => {
    runAbortRef.current?.abort()
    chatAbortRef.current?.abort()
    setAgent(passedAgent)
    setMode(passedAgent.mode || '')
    setUserInputForm([])
    setFormValues({})
    setParamsError('')
    setRunError('')
    setSteps([])
    setRunSummary(null)
    setMessages([])
    setInput('')
    setChatRunError('')
    setChatConversationId('')
    chatConversationIdRef.current = ''
    setChatStreamIdSafe(null)
    activeChatSnapshotRef.current = null
    viewingHistoryRef.current = false
    setHasActiveChatSnapshot(false)
    setHistoryOpen(false)
    setHistoryList([])
    setHistoryError('')
    setHistoryHasMore(false)
    setHistoryLoadingMore(false)
    historyListRef.current = []
    historyLoadingRef.current = false
    setOpenDownloadId(null)
  }, [appId])

  // ===== 加载详情 + workflow 参数（走 Dify Service API）=====
  useEffect(() => {
    if (!appId) return
    let cancelled = false
    const controller = new AbortController()
    const fetchAll = async () => {
      setLoading(true)
      setError('')

      // 没有 apiKey 就无法走 Service API，直接报错
      if (!apiKey) {
        setError('缺少 API Key，请从广场重新进入该应用（确保有访问权限）')
        setLoading(false)
        return
      }

      try {
        // 用 Bearer apiKey 调 /v1/info + /v1/site，**不再调 /console/api/apps/<id>**
        const raw = await getAppFullInfo(apiKey, { signal: controller.signal })
        if (cancelled) return
        const m = raw.mode || passedAgent.mode || ''

        // 处理 icon：
        //   icon_type === 'image' 用 icon_url；否则用 emoji（resolveIcon 兼容 native + mart name）
        let iconValue
        if (raw.icon_type === 'image' && raw.icon_url) {
          iconValue = { type: 'image', url: buildIconUrl(raw.icon_url) }
        } else {
          iconValue = resolveIcon(raw.icon)
        }

        setAgent({
          id: appId,
          name: raw.name || passedAgent.name || '智能体',
          desc: raw.description || '',
          icon: iconValue,
          iconType: raw.icon_type || 'emoji',
          iconBackground: raw.icon_background || '#FFEAD5',
          mode: m,
          tags: raw.tags || [],
          author: raw.author_name || '',
          title: raw.title || raw.name || '',
          // 站点级配置（供后续使用）
          showWorkflowSteps: raw.show_workflow_steps,
          chatColorTheme: raw.chat_color_theme,
          copyright: raw.copyright,
          customDisclaimer: raw.custom_disclaimer,
          defaultLanguage: raw.default_language,
        })
        setMode(m)

        // workflow / chat 模式：拉表单参数
        //   workflow 走 /workflows/run，参数在 inputs
        //   advanced-chat / chat / agent-chat / agentic 走 /chat-messages，参数在 inputs
        const isWorkflowMode = m === 'workflow'
        const isChatMode = m === 'advanced-chat' || m === 'chat' || m === 'agent-chat' || m === 'agentic'
        if ((isWorkflowMode || isChatMode) && apiKey) {
          setParamsLoading(true)
          setParamsError('')
          try {
            const p = await getDifyAppParameters(apiKey, { signal: controller.signal })
            if (cancelled) return
            const form = p?.user_input_form || p?.data?.user_input_form || []
            setUserInputForm(form)
            // 初始化默认值
            const init = {}
            for (const item of form) {
              const type = Object.keys(item)[0]
              const cfg = item[type]
              if (cfg?.variable) init[cfg.variable] = cfg.default ?? ''
            }
            setFormValues(init)
          } catch (e) {
            if (!cancelled && e?.name !== 'AbortError') setParamsError(e?.message || '加载表单参数失败')
          } finally {
            if (!cancelled) setParamsLoading(false)
          }
        }
      } catch (e) {
        if (!cancelled && e?.name !== 'AbortError') setError(e?.message || '获取智能体详情失败')
      } finally {
        if (!cancelled) setLoading(false)
      }
    }
    fetchAll()
    return () => {
      cancelled = true
      controller.abort()
    }
  }, [appId, apiKey])

  useEffect(() => {
    return () => {
      runAbortRef.current?.abort()
      chatAbortRef.current?.abort()
    }
  }, [])

  useEffect(() => {
    if (timelineAutoScrollRef.current) scrollToBottom(timelineRef.current)
  }, [steps, running])

  const handleTimelineScroll = useCallback((e) => {
    timelineAutoScrollRef.current = isScrollAtBottom(e.currentTarget)
  }, [])

  const handleMessagesScroll = useCallback((e) => {
    messagesAutoScrollRef.current = isScrollAtBottom(e.currentTarget)
  }, [])

  // ===== 渲染动态表单字段 =====
  // 文件大小格式化
  const formatFileSize = (bytes) => {
    if (!bytes && bytes !== 0) return ''
    if (bytes < 1024) return bytes + ' B'
    if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
    if (bytes < 1024 * 1024 * 1024) return (bytes / 1024 / 1024).toFixed(1) + ' MB'
    return (bytes / 1024 / 1024 / 1024).toFixed(2) + ' GB'
  }

  // 文件 accept 属性构造
  //   - 优先用 allowed_file_extensions（真正的文件后缀）
  //   - 没有 extensions 但有 allowed_file_types（文件类型分类 image/document/...），按类型映射到 MIME 通配
  //   - 都没有则不限制
  const FILE_TYPE_MIME_MAP = {
    image: 'image/*',
    audio: 'audio/*',
    video: 'video/*',
    document: 'application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/plain,text/csv',
    custom: '*/*',
  }
  const buildFileAccept = (cfg) => {
    if (cfg.allowed_file_extensions?.length > 0) {
      return cfg.allowed_file_extensions
        .map(ext => ext.startsWith('.') ? ext : '.' + ext)
        .join(',')
    }
    if (cfg.allowed_file_types?.length > 0) {
      return cfg.allowed_file_types
        .map(t => FILE_TYPE_MIME_MAP[t] || '*/*')
        .join(',')
    }
    return undefined
  }

  const isBrowserFile = (value) => {
    return !!value
      && typeof value === 'object'
      && typeof value.name === 'string'
      && typeof value.size === 'number'
      && typeof value.arrayBuffer === 'function'
  }

  const extractFilesFromValue = (value) => {
    if (isBrowserFile(value)) return [value]
    if (Array.isArray(value)) return value.flatMap(extractFilesFromValue)
    if (value && typeof value === 'object') {
      if (typeof FileList !== 'undefined' && value instanceof FileList) return Array.from(value)
      if (isBrowserFile(value.file)) return [value.file]
      if (isBrowserFile(value.originFileObj)) return [value.originFileObj]
      if (isBrowserFile(value.raw)) return [value.raw]
    }
    return []
  }

  const isRequiredValueEmpty = (value, type) => {
    if (type === 'checkbox') return value !== true
    if (type === 'file' || type === 'file-list') return extractFilesFromValue(value).length === 0
    if (Array.isArray(value)) return value.length === 0
    if (value === 0) return false
    return value === '' || value === undefined || value === null || String(value).trim() === ''
  }

  const getMissingRequiredFields = () => {
    return userInputForm
      .map((item, index) => {
        const type = Object.keys(item)[0]
        const cfg = item[type] || {}
        const varName = cfg.variable || `field_${index}`
        if (!cfg.required) return null
        const value = formValues[varName] ?? cfg.default ?? ''
        if (!isRequiredValueEmpty(value, type)) return null
        return cfg.label || varName
      })
      .filter(Boolean)
  }

  const formatRequiredFieldsError = (fields) => {
    return `请先完善必填参数：${fields.join('、')}`
  }

  const isPdfFile = (file) => {
    const name = file?.name || ''
    return file?.type === 'application/pdf' || name.toLowerCase().endsWith('.pdf')
  }

  const getInputFileType = (file) => {
    if (file?.type?.startsWith('image/')) return 'image'
    if (file?.type?.startsWith('audio/')) return 'audio'
    if (file?.type?.startsWith('video/')) return 'video'
    return 'document'
  }

  const formatUploadError = (file, error) => {
    const status = error?.status || error?.code
    const message = error?.message || 'Upload failed'
    return `${file?.name || 'file'}: ${status ? `[${status}] ` : ''}${message}`
  }

  const buildInputsWithUploadedPdfs = async (inputs, signal) => {
    const nextInputs = { ...inputs }
    const errors = []

    for (const [key, value] of Object.entries(inputs || {})) {
      const files = extractFilesFromValue(value)
      if (files.length === 0) continue

      const uploadedFiles = []
      for (const file of files) {
        if (!isPdfFile(file)) {
          uploadedFiles.push(file)
          continue
        }
        try {
          const uploaded = await uploadFile(apiKey, file, undefined, { signal })
          uploadedFiles.push({
            type: getInputFileType(file),
            transfer_method: 'local_file',
            upload_file_id: uploaded.id,
          })
        } catch (error) {
          if (error?.code === 'REQUEST_ABORTED' || error?.name === 'AbortError') throw error
          errors.push(formatUploadError(file, error))
        }
      }

      nextInputs[key] = Array.isArray(value) ? uploadedFiles : (uploadedFiles[0] || null)
    }

    if (errors.length > 0) {
      const err = new Error(errors.join('\n'))
      err.uploadErrors = errors
      throw err
    }

    return nextInputs
  }

  // 统一解析 select 的 options（兼容 ["a","b"] 和 [{value,label}] 两种格式）
  const normalizeSelectOptions = (options) => {
    if (!Array.isArray(options)) return []
    return options.map((o, i) => {
      if (o == null) return { value: '', label: '' }
      if (typeof o === 'string') return { value: o, label: o }
      if (typeof o === 'object') {
        return { value: o.value ?? o.label ?? '', label: o.label ?? o.value ?? '' }
      }
      return { value: String(o), label: String(o) }
    })
  }

  // 表单字段类型 → 中文徽章文案
  const TYPE_LABEL = {
    'text-input': '文本',
    paragraph: '段落',
    select: '选择',
    number: '数字',
    file: '文件',
    'file-list': '文件',
    checkbox: '布尔',
  }

  // 字段右侧的徽章组：类型 + 是否必填
  const FieldMeta = ({ type, required }) => (
    <span className="wf-form-meta">
      <span className="wf-form-type-badge">{TYPE_LABEL[type] || type}</span>
      {required && <span className="wf-form-required-badge">必填</span>}
    </span>
  )

  // 文件上传子组件
  const FileField = ({ cfg, multiple, value, onChange, varName }) => {
    const fileList = Array.isArray(value) ? value : (value ? [value] : [])
    const fileInputRef = useRef(null)

    const handleFileChange = (e) => {
      const files = Array.from(e.target.files || [])
      e.target.value = '' // 允许选择同名文件
      if (multiple) {
        onChange([...fileList, ...files])
      } else {
        onChange(files[0] || null)
      }
    }

    const removeFile = (idx) => {
      if (multiple) {
        onChange(fileList.filter((_, i) => i !== idx))
      } else {
        onChange(null)
      }
    }

    const accept = buildFileAccept(cfg)

    return (
      <div className="wf-form-field">
        <span className="wf-form-label">
          {cfg.label || varName}
          {cfg.required && <em className="wf-form-required">*</em>}
          <span className="wf-form-tip">
            {multiple ? '（可上传多个文件）' : '（单文件）'}
            {cfg.allowed_file_extensions?.length > 0 && ` 支持: ${cfg.allowed_file_extensions.join(', ')}`}
            {!cfg.allowed_file_extensions?.length && cfg.allowed_file_types?.length > 0 && ` 支持: ${cfg.allowed_file_types.join(', ')}`}
          </span>
          <FieldMeta type={multiple ? 'file-list' : 'file'} required={cfg.required} />
        </span>
        <input
          ref={fileInputRef}
          type="file"
          className="wf-form-file-hidden"
          multiple={multiple}
          accept={accept}
          onChange={handleFileChange}
        />
        <button
          type="button"
          className="wf-form-file-trigger"
          onClick={() => fileInputRef.current?.click()}
        >
          <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
            <polyline points="17 8 12 3 7 8" />
            <line x1="12" y1="3" x2="12" y2="15" />
          </svg>
          <span>点击选择文件</span>
        </button>
        {fileList.length > 0 && (
          <ul className="wf-form-file-list">
            {fileList.map((f, i) => (
              <li key={i} className="wf-form-file-item">
                <span className="wf-form-file-icon">
                  {f.type?.startsWith('image/') ? '🖼️' : f.type?.includes('pdf') ? '📄' : '📎'}
                </span>
                <span className="wf-form-file-name">{f.name}</span>
                <span className="wf-form-file-size">{formatFileSize(f.size)}</span>
                <button
                  type="button"
                  className="wf-form-file-remove"
                  onClick={() => removeFile(i)}
                  title="移除"
                >×</button>
              </li>
            ))}
          </ul>
        )}
      </div>
    )
  }

  const renderFormField = (item, key) => {
    const type = Object.keys(item)[0]
    const cfg = item[type] || {}
    const varName = cfg.variable || `field_${key}`
    const value = formValues[varName] ?? cfg.default ?? ''
    const setVal = (v) => setFormValues(prev => ({ ...prev, [varName]: v }))

    if (type === 'text-input') {
      return (
        <label key={key} className="wf-form-field">
          <span className="wf-form-label">
            {cfg.label || varName}
            {cfg.required && <em className="wf-form-required">*</em>}
            {cfg.max_length && <span className="wf-form-tip">（最多 {cfg.max_length} 字）</span>}
            <FieldMeta type={type} required={cfg.required} />
          </span>
          <input
            type="text"
            className="wf-form-input"
            value={value}
            maxLength={cfg.max_length}
            placeholder={cfg.placeholder || `请输入${cfg.label || varName}`}
            onChange={e => setVal(e.target.value)}
          />
        </label>
      )
    }
    if (type === 'paragraph') {
      return (
        <label key={key} className="wf-form-field">
          <span className="wf-form-label">
            {cfg.label || varName}
            {cfg.required && <em className="wf-form-required">*</em>}
            {cfg.max_length && <span className="wf-form-tip">（最多 {cfg.max_length} 字）</span>}
            <FieldMeta type={type} required={cfg.required} />
          </span>
          <textarea
            className="wf-form-textarea"
            rows={5}
            value={value}
            maxLength={cfg.max_length}
            placeholder={cfg.placeholder || `请输入${cfg.label || varName}`}
            onChange={e => setVal(e.target.value)}
          />
          {cfg.max_length && (
            <div className="wf-form-textarea-counter">
              {String(value || '').length} / {cfg.max_length}
            </div>
          )}
        </label>
      )
    }
    if (type === 'select') {
      return (
        <label key={key} className="wf-form-field">
          <span className="wf-form-label">
            {cfg.label || varName}
            {cfg.required && <em className="wf-form-required">*</em>}
            <FieldMeta type={type} required={cfg.required} />
          </span>
          <div className="wf-form-select-wrapper">
            <select
              className="wf-form-select"
              value={value}
              onChange={e => setVal(e.target.value)}
            >
              <option value="" disabled>请选择</option>
              {normalizeSelectOptions(cfg.options).map((o, i) => (
                <option key={i} value={o.value}>{o.label}</option>
              ))}
            </select>
            <svg className="wf-form-select-arrow" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
              <polyline points="6 9 12 15 18 9" />
            </svg>
          </div>
        </label>
      )
    }
    if (type === 'number') {
      return (
        <label key={key} className="wf-form-field">
          <span className="wf-form-label">
            {cfg.label || varName}
            {cfg.required && <em className="wf-form-required">*</em>}
            <FieldMeta type={type} required={cfg.required} />
          </span>
          <input
            type="number"
            className="wf-form-input"
            value={value}
            min={cfg.min}
            max={cfg.max}
            step={cfg.step}
            placeholder={cfg.placeholder || ''}
            onChange={e => setVal(e.target.value)}
          />
        </label>
      )
    }
    if (type === 'file' || type === 'file-list') {
      return (
        <FileField
          key={key}
          cfg={cfg}
          varName={varName}
          multiple={type === 'file-list'}
          value={value}
          onChange={setVal}
        />
      )
    }
    if (type === 'checkbox') {
      return (
        <label key={key} className="wf-form-checkbox-row">
          <input
            type="checkbox"
            className="wf-form-checkbox"
            checked={!!value}
            onChange={e => setVal(e.target.checked)}
          />
          <span className="wf-form-checkbox-label">
            {cfg.label || varName}
            {cfg.required && <em className="wf-form-required">*</em>}
          </span>
          <FieldMeta type={type} required={cfg.required} />
        </label>
      )
    }
    // 未知类型，文本输入兜底
    return (
      <label key={key} className="wf-form-field">
        <span className="wf-form-label">{cfg.label || varName} <small style={{ color: '#94a3b8' }}>({type})</small><FieldMeta type={type} required={cfg.required} /></span>
        <input
          type="text"
          className="wf-form-input"
          value={value}
          onChange={e => setVal(e.target.value)}
        />
      </label>
    )
  }

  // ===== 处理 SSE 事件 =====
  const handleSSEEvent = useCallback((evt) => {
    const { event, data } = evt
    const ts = Date.now()

    if (event === 'workflow_started') {
      setSteps(prev => [
        ...prev,
        {
          id: 'start-' + ts,
          type: 'workflow_started',
          title: '工作流启动',
          time: ts,
          inputs: data?.inputs,
          workflowId: data?.workflow_id,
        }
      ])
    } else if (event === 'node_started') {
      setSteps(prev => [
        ...prev,
        {
          id: data?.id || `node-${ts}`,
          type: 'node_started',
          nodeId: data?.node_id,
          nodeType: data?.node_type,
          title: data?.title || data?.node_type,
          index: data?.index,
          time: ts,
          status: 'running',
        }
      ])
    } else if (event === 'text_chunk') {
      // 累积到独立的 final-text 条目
      setSteps(prev => {
        const idx = prev.findIndex(s => s.type === 'text_chunk')
        if (idx >= 0) {
          const next = [...prev]
          next[idx] = { ...next[idx], text: (next[idx].text || '') + (data?.text || '') }
          return next
        }
        return [
          ...prev,
          {
            id: 'final-text',
            type: 'text_chunk',
            title: '输出文本',
            time: ts,
            text: data?.text || '',
            status: 'running',
          }
        ]
      })
    } else if (event === 'node_finished') {
      setSteps(prev => prev.map(s =>
        s.id === data?.id
          ? { ...s, status: data?.status || 'succeeded', outputs: data?.outputs, elapsedTime: data?.elapsed_time, error: data?.error, finished: true }
          : s
      ))
    } else if (event === 'workflow_finished') {
      setRunSummary({
        status: data?.status,
        outputs: data?.outputs,
        totalSteps: data?.total_steps,
        elapsedTime: data?.elapsed_time,
        totalTokens: data?.total_tokens,
        createdAt: data?.created_at,
        finishedAt: data?.finished_at,
      })
      setSteps(prev => [
        ...prev,
        {
          id: 'finish-' + ts,
          type: 'workflow_finished',
          title: '工作流完成',
          time: ts,
          status: data?.status,
          totalSteps: data?.total_steps,
          elapsedTime: data?.elapsed_time,
          totalTokens: data?.total_tokens,
        }
      ])
    } else if (event === 'workflow_paused') {
      setSteps(prev => [
        ...prev,
        { id: 'paused-' + ts, type: 'workflow_paused', title: '工作流暂停', time: ts, data }
      ])
    } else if (event === 'human_input_required') {
      setSteps(prev => [
        ...prev,
        {
          id: 'human-input-' + ts,
          type: 'human_input',
          title: '需要人工介入',
          time: ts,
          formId: data?.form_id,
          nodeTitle: data?.node_title,
          formContent: data?.form_content,
        }
      ])
    } else if (event === 'error') {
      setSteps(prev => [
        ...prev,
        {
          id: 'err-' + ts,
          type: 'error',
          title: '执行错误',
          time: ts,
          message: data?.message || evt?.message || '未知错误',
          data
        }
      ])
    }
    // 其它事件忽略
  }, [])

  // ===== 处理 chat-messages SSE 事件 =====
  //   关键事件（来自文档）：
  //   - message            每个回答片段（按顺序拼接）→ 累积到当前 assistant 消息的 answer
  //   - message_end        回答完成 → 标记流式结束
  //   - error              错误 → 写入错误文本
  //   - workflow_started / node_*  advanced-chat 模式还会有节点事件，这里仅记录不影响 UI
  const handleChatSSEEvent = useCallback((evt) => {
    const event = evt?.event
    const data = evt?.data || {}
    const answer = evt?.answer ?? data?.answer ?? ''
    const evtConversationId = evt?.conversation_id ?? data?.conversation_id
    const evtMetadata = evt?.metadata ?? data?.metadata
    const evtMessageId = evt?.id ?? evt?.message_id ?? data?.message_id
    const evtErrorMessage = evt?.message ?? data?.message
    if (event === 'message') {
      const streamId = chatStreamIdRef.current
      if (streamId) {
        const updateMessages = prev => prev.map(m =>
          m.id === streamId
            ? { ...m, content: m.pending ? answer : (m.content || '') + answer, pending: false, thinking: splitThinkContent(m.pending ? answer : (m.content || '') + answer).thinking }
            : m
        )
        if (viewingHistoryRef.current && activeChatSnapshotRef.current) {
          activeChatSnapshotRef.current = {
            ...activeChatSnapshotRef.current,
            messages: updateMessages(activeChatSnapshotRef.current.messages || []),
          }
        } else {
          setMessages(updateMessages)
        }
      }
      if (evtConversationId && !chatConversationIdRef.current) {
        chatConversationIdRef.current = evtConversationId
        setChatConversationId(evtConversationId)
        if (activeChatSnapshotRef.current) {
          activeChatSnapshotRef.current = {
            ...activeChatSnapshotRef.current,
            conversationId: evtConversationId,
          }
        }
      }
    } else if (event === 'message_end') {
      const streamId = chatStreamIdRef.current
      if (streamId) {
        const updateMessages = prev => prev.map(m =>
          m.id === streamId
            ? { ...m, streaming: false, pending: false, thinking: false, metadata: evtMetadata, messageId: evtMessageId }
            : m
        )
        if (viewingHistoryRef.current && activeChatSnapshotRef.current) {
          activeChatSnapshotRef.current = {
            ...activeChatSnapshotRef.current,
            messages: updateMessages(activeChatSnapshotRef.current.messages || []),
            streamId: null,
          }
        } else {
          setMessages(updateMessages)
        }
      }
      setChatStreamIdSafe(null)
      if (evtConversationId) {
        chatConversationIdRef.current = evtConversationId
        setChatConversationId(evtConversationId)
        if (activeChatSnapshotRef.current) {
          activeChatSnapshotRef.current = {
            ...activeChatSnapshotRef.current,
            conversationId: evtConversationId,
          }
        }
      }
    } else if (event === 'error') {
      setChatStreamId(prevId => {
        if (prevId) {
          const updateMessages = prev => prev.map(m =>
            m.id === prevId
              ? { ...m, streaming: false, content: (m.content || '') + '\n\n⚠ ' + (data?.message || '未知错误') }
              : m
          )
          if (viewingHistoryRef.current && activeChatSnapshotRef.current) {
            activeChatSnapshotRef.current = {
              ...activeChatSnapshotRef.current,
              messages: updateMessages(activeChatSnapshotRef.current.messages || []),
              streamId: null,
              error: data?.message || '对话请求失败',
            }
          } else {
            setMessages(updateMessages)
          }
        }
        return null
      })
      setChatRunError(data?.message || '对话请求失败')
    } else if (event === 'message_replace') {
      const streamId = chatStreamIdRef.current
      if (streamId) {
        const updateMessages = prev => prev.map(m =>
          m.id === streamId
            ? { ...m, content: data?.answer || '', thinking: splitThinkContent(data?.answer || '').thinking }
            : m
        )
        if (viewingHistoryRef.current && activeChatSnapshotRef.current) {
          activeChatSnapshotRef.current = {
            ...activeChatSnapshotRef.current,
            messages: updateMessages(activeChatSnapshotRef.current.messages || []),
          }
        } else {
          setMessages(updateMessages)
        }
      }
    }
  }, [])

  // ===== 执行工作流 =====
  const handleRunWorkflow = async () => {
    if (running) return
    const missingFields = getMissingRequiredFields()
    if (missingFields.length > 0) {
      setRunError(formatRequiredFieldsError(missingFields))
      return
    }
    runAbortRef.current?.abort()
    const controller = new AbortController()
    runAbortRef.current = controller
    setRunError('')
    setSteps([])
    setRunSummary(null)
    timelineAutoScrollRef.current = true
    setRunning(true)
    try {
      const workflowInputs = await buildInputsWithUploadedPdfs(formValues, controller.signal)
      const res = await runWorkflow(apiKey, workflowInputs, undefined, { signal: controller.signal })
      await consumeSSE(
        res,
        handleSSEEvent,
        () => { setRunning(false) },
        (err) => { setRunError(err?.message || 'SSE 流读取失败'); setRunning(false) },
        { signal: controller.signal }
      )
    } catch (e) {
      if (e?.name !== 'AbortError') {
        setRunError(e?.message || '执行失败')
      }
    } finally {
      if (runAbortRef.current === controller) {
        runAbortRef.current = null
      }
      setRunning(false)
    }
  }

  // ===== 聊天模式（占位，保留旧 UI） =====
  const [input, setInput] = useState('')
  const [messages, setMessages] = useState([])
  // ===== AI 消息操作：复制 / 听语音 / 点赞点踩 / 编辑 =====
  const [copiedMsgId, setCopiedMsgId] = useState(null)
  const [speakingId, setSpeakingId] = useState(null)
  const [editingMsg, setEditingMsg] = useState(null)   // 正在编辑的消息
  const [editDraft, setEditDraft] = useState('')       // 编辑中的草稿
  const canEditMessage = isReportAgent(agent)
  const isChat = mode === 'chat' || mode === 'advanced-chat' || mode === 'agent-chat' || mode === 'agentic' || mode === 'completion' || !mode

  useEffect(() => {
    if (messagesAutoScrollRef.current) scrollToBottom(messagesRef.current)
  }, [messages, chatRunning])

  // 组件卸载时停止语音朗读
  useEffect(() => () => {
    window.speechSynthesis?.cancel()
  }, [])

  // 正在朗读的消息不再展示（切换历史会话 / 开启新会话清空）时，停止朗读
  useEffect(() => {
    if (speakingId && !messages.some(m => m.id === speakingId)) {
      window.speechSynthesis?.cancel()
      setSpeakingId(null)
    }
  }, [messages, speakingId])

  // ===== API Key 弹框 =====
  const [showApiKeyModal, setShowApiKeyModal] = useState(false)
  const [copied, setCopied] = useState(false)

  // ===== workflow 右侧运行面板折叠 =====
  const [runPanelCollapsed, setRunPanelCollapsed] = useState(false)

  const handleCopyApiKey = async () => {
    if (!apiKey) return
    try {
      await navigator.clipboard.writeText(apiKey)
      setCopied(true)
      setTimeout(() => setCopied(false), 1500)
    } catch (e) {
      // 降级：execCommand
      const ta = document.createElement('textarea')
      ta.value = apiKey
      document.body.appendChild(ta)
      ta.select()
      try { document.execCommand('copy'); setCopied(true); setTimeout(() => setCopied(false), 1500) } catch (_) {}
      document.body.removeChild(ta)
    }
  }

  // ===== 历史会话：滚动分页加载（Dify /v1/conversations，sort_by=-updated_at 倒序，首屏返回最新 limit 条） =====
  const HISTORY_PAGE_SIZE = 20

  // reset=true 拉第一页；否则用上一页最后一条 id 作为 last_id 游标追加下一页
  const fetchConversations = useCallback(async (reset = false) => {
    if (historyLoadingRef.current) return
    historyLoadingRef.current = true
    if (reset) {
      setHistoryLoading(true)
      setHistoryError('')
    } else {
      setHistoryLoadingMore(true)
    }
    try {
      const lastId = reset ? '' : (historyListRef.current[historyListRef.current.length - 1]?.id || '')
      const res = await getConversations(apiKey, { lastId, limit: HISTORY_PAGE_SIZE })
      const list = Array.isArray(res?.data) ? res.data : []
      historyListRef.current = reset ? list : [...historyListRef.current, ...list]
      setHistoryList(historyListRef.current)
      setHistoryHasMore(!!res?.has_more && list.length > 0)
    } catch (e) {
      if (reset) {
        setHistoryError(e?.message || '加载历史记录失败')
        setHistoryList([])
        historyListRef.current = []
        setHistoryHasMore(false)
      } else {
        setHistoryError(e?.message || '加载更多失败')
      }
    } finally {
      historyLoadingRef.current = false
      setHistoryLoading(false)
      setHistoryLoadingMore(false)
    }
  }, [apiKey])

  const handleToggleHistory = useCallback(() => {
    setHistoryOpen(prev => {
      const next = !prev
      // 每次打开都重新拉第一页（最新 limit 条）
      if (next) fetchConversations(true)
      return next
    })
  }, [fetchConversations])

  // 抽屉滚动到底部附近时加载下一页
  const handleHistoryScroll = useCallback((e) => {
    if (historyLoading || historyLoadingMore || !historyHasMore) return
    const el = e.currentTarget
    if (el.scrollHeight - el.scrollTop - el.clientHeight <= 40) {
      fetchConversations(false)
    }
  }, [historyLoading, historyLoadingMore, historyHasMore, fetchConversations])

  const handleSelectHistory = useCallback(async (conv) => {
    if (!conv?.id || resumingConv) return
    if (chatRunning && !viewingHistoryRef.current) {
      activeChatSnapshotRef.current = {
        messages,
        input,
        conversationId: chatConversationIdRef.current || chatConversationId || '',
        streamId: chatStreamIdRef.current,
        error: chatRunError,
      }
      setHasActiveChatSnapshot(true)
    }
    setResumingConv(conv.id)
    try {
      // 恢复会话：直接调 Dify /v1/messages 拉取该会话的历史消息（鉴权用当前智能体 API Key）
      const res = await getChatMessages(apiKey, conv.id)
      const detail = res?.data ?? res ?? {}
      const rows = Array.isArray(detail.data) ? detail.data : (Array.isArray(detail) ? detail : [])
      const restored = rows
        .filter(m => m && (m.query || m.answer))
        .sort((a, b) => (a.created_at || 0) - (b.created_at || 0))
        .flatMap(m => [
          ...(m.query ? [{ id: `history-u-${m.id}`, role: 'user', content: m.query, streaming: false }] : []),
          ...(m.answer ? [{ id: `history-a-${m.id}`, role: 'assistant', content: m.answer, streaming: false, messageId: m.id }] : []),
        ])
      chatConversationIdRef.current = conv.id
      setChatConversationId(conv.id)
      messagesAutoScrollRef.current = true
      viewingHistoryRef.current = true
      setMessages(restored)
      setChatStreamIdSafe(null)
      setHistoryOpen(false)
    } catch (e) {
      setChatRunError(e?.message || '加载历史详情失败')
    } finally {
      setResumingConv(null)
    }
  }, [apiKey, chatConversationId, chatRunError, chatRunning, input, messages, resumingConv])

  const handleBackToActiveChat = useCallback(() => {
    const snapshot = activeChatSnapshotRef.current
    if (!snapshot) return
    viewingHistoryRef.current = false
    chatConversationIdRef.current = snapshot.conversationId || ''
    setChatConversationId(snapshot.conversationId || '')
    setChatStreamIdSafe(snapshot.streamId || chatStreamIdRef.current || null)
    messagesAutoScrollRef.current = true
    setMessages(snapshot.messages || [])
    setInput(snapshot.input || '')
    setChatRunError(snapshot.error || '')
    setHistoryOpen(false)
    setHasActiveChatSnapshot(false)
  }, [])

  // 报告导出（Word / PDF）：只导出正文，剔除思考过程与 <step> 过程提示（与页面展示同源 getAnswerText）
  const handleDownloadMessage = useCallback(async (m, format) => {
    if (m?.downloading) return
    const answer = getAnswerText(m)
    if (!answer) {
      setChatRunError('暂无可导出的正文内容')
      return
    }
    setMessages(prev => prev.map(x => x.id === m.id ? { ...x, downloading: format } : x))
    try {
      const filename = `${sanitizeDownloadName(agent.name)}-${formatDownloadTimestamp()}`
      if (format === 'word') {
        // HTML 打包为 altChunk .docx（Word/WPS 可直接打开）
        downloadBlob(await htmlToDocxBlob(buildExportHtml(renderMarkdown(answer))), `${filename}.docx`)
      } else {
        // html2canvas + jsPDF 直接生成 PDF 文件下载
        await exportHtmlToPdf(buildExportHtml(renderMarkdown(answer)), `${filename}.pdf`)
      }
    } catch (e) {
      setChatRunError(`导出失败：${e?.message || '未知错误'}`)
    } finally {
      setMessages(prev => prev.map(x => x.id === m.id ? { ...x, downloading: null } : x))
    }
  }, [agent.name])

  const handleNewChat = useCallback(() => {
    chatAbortRef.current?.abort()
    chatConversationIdRef.current = ''
    setChatConversationId('')
    setChatStreamIdSafe(null)
    messagesAutoScrollRef.current = true
    setMessages([])
    setInput('')
    setChatRunError('')
    setOpenDownloadId(null)
    setHistoryOpen(false)
  }, [])

  const handleNewChatClick = useCallback(() => {
    if (!chatRunning) {
      handleNewChat()
      return
    }

    Modal.confirm({
      title: '开启新会话？',
      content: '当前正在生成回复，开启新会话会中断本次问答并清空当前内容。',
      okText: '中断并开启',
      cancelText: '继续等待',
      okButtonProps: { status: 'danger' },
      onOk: handleNewChat,
    })
  }, [chatRunning, handleNewChat])

  // 发起一次对话流式请求（发送 & 重新生成共用）
  const runChatStream = async (query, assistantMsgId) => {
    chatAbortRef.current?.abort()
    const controller = new AbortController()
    chatAbortRef.current = controller
    setChatRunError('')
    setChatStreamIdSafe(assistantMsgId)
    setChatRunning(true)

    try {
      // 调 /chat-messages 接口（带 inputs + conversation_id 续传）
      const res = await runChatMessage(
        apiKey,
        {
          query,
          inputs: await buildInputsWithUploadedPdfs(formValues, controller.signal),
          conversation_id: chatConversationIdRef.current || chatConversationId || ''
        },
        undefined,
        { signal: controller.signal }
      )
      // 解析 SSE 流
      await consumeSSE(
        res,
        handleChatSSEEvent,
        () => { /* 流结束 */ },
        (err) => {
          setChatRunError(err?.message || 'SSE 流读取失败')
          // 失败时给当前 assistant 消息加错误提示
          setMessages(prev => prev.map(m =>
            m.id === assistantMsgId && m.streaming
              ? { ...m, streaming: false, content: (m.content || '') + '\n\n⚠ ' + (err?.message || 'SSE 流读取失败') }
              : m
          ))
        },
        { signal: controller.signal }
      )
    } catch (e) {
      if (e?.name !== 'AbortError') {
        setChatRunError(e?.message || '请求失败')
        setMessages(prev => prev.map(m =>
          m.id === assistantMsgId && m.streaming
            ? { ...m, streaming: false, content: '⚠ ' + (e?.message || '请求失败') }
            : m
        ))
      }
    } finally {
      if (chatAbortRef.current === controller) {
        chatAbortRef.current = null
      }
      setChatRunning(false)
      setChatStreamId(prev => {
        // 如果还停留在 assistantMsgId（异常未触发 message_end），把它收尾
        if (prev === assistantMsgId) {
          setMessages(p => p.map(m => m.id === prev ? { ...m, streaming: false, pending: false } : m))
          chatStreamIdRef.current = null
          return null
        }
        return prev
      })
    }
  }

  const handleSend = async () => {
    const text = input.trim()
    if (!text || chatRunning) return
    const missingFields = getMissingRequiredFields()
    if (missingFields.length > 0) {
      setChatRunError(formatRequiredFieldsError(missingFields))
      setShowChatParamsModal(true)
      return
    }
    if (!apiKey) {
      setChatRunError('缺少 API Key')
      return
    }

    // 1) 推入 user 消息
    const userMsgId = 'u-' + Date.now()
    const assistantMsgId = 'a-' + Date.now()
    messagesAutoScrollRef.current = true
    setMessages(prev => [
      ...prev,
      { id: userMsgId, role: 'user', content: text },
      { id: assistantMsgId, role: 'assistant', content: STREAM_WAITING_TEXT, streaming: true, pending: true },
    ])
    setInput('')

    // 2) 发起流式请求
    await runChatStream(text, assistantMsgId)
  }

  // 停止当前对话流
  const handleStopChat = () => {
    chatAbortRef.current?.abort()
  }

  // ===== AI 消息操作 =====
  // 复制正文
  const handleCopyMessage = async (m) => {
    const text = getAnswerText(m)
    if (!text) return
    const ok = await copyTextToClipboard(text)
    if (!ok) {
      Modal.warning({ title: '复制失败', content: '当前浏览器不支持自动复制，请手动选择文本复制。' })
      return
    }
    setCopiedMsgId(m.id)
    setTimeout(() => setCopiedMsgId(prev => (prev === m.id ? null : prev)), 1500)
  }

  // 听语音（Web Speech API，可再次点击停止）
  const handleToggleSpeak = (m) => {
    const synth = window.speechSynthesis
    if (!synth) return
    if (speakingId === m.id) {
      synth.cancel()
      setSpeakingId(null)
      return
    }
    synth.cancel()
    const raw = getAnswerText(m)
    if (!raw) return
    // 去掉代码块与常见 markdown 符号，提升朗读体验
    const text = raw
      .replace(/```[\s\S]*?```/g, ' ')
      .replace(/[#>*_~[\]()!|-]/g, ' ')
      .replace(/\s+/g, ' ')
      .trim()
    if (!text) return
    const utterance = new SpeechSynthesisUtterance(text)
    utterance.lang = 'zh-CN'
    utterance.onend = () => setSpeakingId(prev => (prev === m.id ? null : prev))
    utterance.onerror = () => setSpeakingId(prev => (prev === m.id ? null : prev))
    setSpeakingId(m.id)
    synth.speak(utterance)
  }

  // 点赞 / 点踩（互斥，再次点击取消）
  const handleFeedback = (m, type) => {
    setMessages(prev => prev.map(x =>
      x.id === m.id ? { ...x, feedback: x.feedback === type ? null : type } : x
    ))
  }

  // ===== 编辑 AI 消息（右侧抽屉） =====
  const handleOpenEditor = (m) => {
    // 编辑前停止本条语音朗读
    if (speakingId === m.id) {
      window.speechSynthesis?.cancel()
      setSpeakingId(null)
    }
    setEditingMsg(m)
    // 与「复制」按钮同源（getAnswerText）：剔除思考过程与 <step> 过程提示后再转 HTML 载入富文本
    const parsed = splitThinkContent(m.content || '')
    const answer = parsed.answer || (!parsed.thought ? m.content || '' : '')
    setEditDraft(renderMarkdown(splitStepContent(answer).text))
  }

  const handleCloseEditor = () => {
    setEditingMsg(null)
    setEditDraft('')
  }

  const handleSaveEditor = () => {
    if (!editingMsg) return
    const draft = editDraft
    setMessages(prev => prev.map(x =>
      x.id === editingMsg.id
        ? { ...x, content: draft, thinking: splitThinkContent(draft).thinking, edited: true }
        : x
    ))
    handleCloseEditor()
  }

  // ===== 编辑内容导出（导出当前编辑草稿，所见即所得） =====
  const [editorExporting, setEditorExporting] = useState(false)

  const handleExportDocx = async () => {
    if (!editDraft || editorExporting) return
    setEditorExporting(true)
    try {
      // HTML 打包为 altChunk .docx（Word/WPS 可直接打开）
      const filename = `${sanitizeDownloadName(agent.name)}-${formatDownloadTimestamp()}.docx`
      downloadBlob(await htmlToDocxBlob(buildExportHtml(editDraft)), filename)
    } catch (e) {
      setChatRunError(`导出失败：${e?.message || '未知错误'}`)
    } finally {
      setEditorExporting(false)
    }
  }

  // 重新生成：用上一条用户提问重新请求
  const handleRegenerate = async (m) => {
    if (chatRunning) return
    const idx = messages.findIndex(x => x.id === m.id)
    if (idx < 0) return
    let query = ''
    for (let j = idx - 1; j >= 0; j--) {
      if (messages[j].role === 'user') { query = messages[j].content || ''; break }
    }
    if (!query) return
    // 停掉本条正在播放的语音
    if (speakingId === m.id) {
      window.speechSynthesis?.cancel()
      setSpeakingId(null)
    }
    const assistantMsgId = 'a-' + Date.now()
    messagesAutoScrollRef.current = true
    // 原位替换为新的流式占位消息
    setMessages(prev => prev.map(x =>
      x.id === m.id
        ? { id: assistantMsgId, role: 'assistant', content: STREAM_WAITING_TEXT, streaming: true, pending: true }
        : x
    ))
    await runChatStream(query, assistantMsgId)
  }

  return (
    <div className="agent-execute">
      {/* 智能体详情卡片 */}
      {!isChat && <div className="execute-detail-card">
        <div className="execute-detail-head">
          <div className="execute-detail-icon" style={{ background: agent.iconBackground || '#FFEAD5' }}>
            {agent.icon && typeof agent.icon === 'object' && agent.icon.type === 'image' ? (
              <img src={buildIconUrl(agent.icon.url)} alt={agent.name} className="execute-detail-icon-img" />
            ) : (
              agent.icon || '🤖'
            )}
          </div>
          <div className="execute-detail-meta">
            <h1 className="execute-detail-name">
              {loading ? '加载中…' : (agent.name || '智能体')}
            </h1>
            <div className="execute-detail-sub">
              {mode && <span className="execute-detail-mode-tag">{MODE_LABEL[mode] || mode}</span>}
            </div>
            {error && <div className="execute-detail-error">⚠ {error}</div>}
          </div>
        </div>

        {agent.desc && <div className="execute-detail-desc">{agent.desc}</div>}

        {(agent.tags?.length > 0 || agent.author) && (
          <div className="execute-detail-info">
            {agent.tags?.length > 0 && (
              <div className="execute-detail-tags">
                {agent.tags.map((t, i) => (<span key={i} className="execute-detail-chip">{t}</span>))}
              </div>
            )}
            <div className="execute-detail-stats">
              {agent.author && <span>👤 {agent.author}</span>}
              {agent.copyright && <span>© {agent.copyright}</span>}
            </div>
          </div>
        )}

        {agent.customDisclaimer && (
          <div className="execute-detail-disclaimer">📌 {agent.customDisclaimer}</div>
        )}
      </div>}

      {/* ============== workflow 模式 ============== */}
      {mode === 'workflow' && (
        <div className="workflow-layout">
          {/* 左侧：执行输出（timeline） */}
          <div className="workflow-main">
            {(steps.length > 0 || running) && (
              <div className="execute-card">
                <div className="execute-card-head">
                  <h3 className="execute-card-title">📜 执行过程</h3>
                  {runSummary && (
                    <span className={`wf-summary wf-summary-${runSummary.status}`}>
                      {runSummary.status === 'succeeded' ? '✅' : '❌'} {runSummary.status}
                      {runSummary.elapsedTime != null && ` · ${runSummary.elapsedTime.toFixed(2)}s`}
                      {runSummary.totalSteps != null && ` · 步骤 ${runSummary.totalSteps}`}
                      {runSummary.totalTokens != null && ` · Tokens ${runSummary.totalTokens}`}
                    </span>
                  )}
                </div>

              <div className="wf-timeline" ref={timelineRef} onScroll={handleTimelineScroll}>
                {steps.map((s, i) => (
                  <div key={s.id || i} className={`wf-step wf-step-${s.status || 'pending'} wf-step-type-${s.type}`}>
                    {s.type === 'workflow_started' && (
                      <>
                        <div className="wf-step-marker wf-step-marker-start">▶</div>
                        <div className="wf-step-body">
                          <div className="wf-step-title">工作流启动</div>
                          <div className="wf-step-meta">{formatTime(s.time)}</div>
                          {s.inputs && Object.keys(s.inputs).length > 0 && (
                            <details className="wf-step-details">
                              <summary>输入参数</summary>
                              <pre>{JSON.stringify(s.inputs, null, 2)}</pre>
                            </details>
                          )}
                        </div>
                      </>
                    )}

                    {s.type === 'node_started' && (
                      <>
                        <div className={`wf-step-marker wf-step-marker-${s.status}`}>
                          {s.status === 'running' ? '◐' : '◉'}
                        </div>
                        <div className="wf-step-body">
                          <div className="wf-step-title">
                            {s.title || s.nodeType} {s.index != null && <span className="wf-step-tag">#{s.index}</span>}
                            {s.status === 'running' && <span className="wf-step-pending">运行中</span>}
                          </div>
                          <div className="wf-step-meta">
                            {s.nodeType && <span className="wf-step-type">类型: {s.nodeType}</span>}
                            {formatTime(s.time) && <span> · {formatTime(s.time)}</span>}
                          </div>
                        </div>
                      </>
                    )}

                    {s.type === 'node_finished' && (
                      <>
                        <div className={`wf-step-marker wf-step-marker-${s.status}`}>●</div>
                        <div className="wf-step-body">
                          <div className="wf-step-title">
                            {s.title} · <span className={`wf-step-status wf-step-status-${s.status}`}>{s.status}</span>
                            {s.elapsedTime != null && <span className="wf-step-tag">⏱ {s.elapsedTime.toFixed(2)}s</span>}
                          </div>
                          {s.error && <div className="wf-step-error">{typeof s.error === 'string' ? s.error : JSON.stringify(s.error)}</div>}
                          {s.outputs && Object.keys(s.outputs).length > 0 && (
                            <details className="wf-step-details">
                              <summary>输出</summary>
                              <pre>{JSON.stringify(s.outputs, null, 2)}</pre>
                            </details>
                          )}
                        </div>
                      </>
                    )}

                    {s.type === 'text_chunk' && (
                      <>
                        <div className="wf-step-marker wf-step-marker-output">✎</div>
                        <div className="wf-step-body">
                          <div className="wf-step-title">输出文本 {s.status === 'running' && <span className="wf-step-pending">生成中</span>}</div>
                          <div className="wf-step-text">{s.text}<span className="wf-step-cursor">▍</span></div>
                        </div>
                      </>
                    )}

                    {s.type === 'workflow_finished' && (
                      <>
                        <div className={`wf-step-marker wf-step-marker-${s.status}`}>
                          {s.status === 'succeeded' ? '✓' : '✕'}
                        </div>
                        <div className="wf-step-body">
                          <div className="wf-step-title">
                            工作流完成
                            {s.totalSteps != null && <span className="wf-step-tag">步骤 {s.totalSteps}</span>}
                            {s.elapsedTime != null && <span className="wf-step-tag">⏱ {s.elapsedTime.toFixed(2)}s</span>}
                            {s.totalTokens != null && <span className="wf-step-tag">Tokens {s.totalTokens}</span>}
                          </div>
                        </div>
                      </>
                    )}

                    {s.type === 'workflow_paused' && (
                      <>
                        <div className="wf-step-marker wf-step-marker-paused">⏸</div>
                        <div className="wf-step-body">
                          <div className="wf-step-title">工作流暂停（等待人工介入）</div>
                        </div>
                      </>
                    )}

                    {s.type === 'human_input' && (
                      <>
                        <div className="wf-step-marker wf-step-marker-human">👤</div>
                        <div className="wf-step-body">
                          <div className="wf-step-title">需要人工介入：{s.nodeTitle}</div>
                          {s.formContent && <div className="wf-step-text">{s.formContent}</div>}
                        </div>
                      </>
                    )}

                    {s.type === 'error' && (
                      <>
                        <div className="wf-step-marker wf-step-marker-error">!</div>
                        <div className="wf-step-body">
                          <div className="wf-step-title">执行错误</div>
                          <div className="wf-step-error">{s.message}</div>
                        </div>
                      </>
                    )}
                  </div>
                ))}

                {running && (
                  <div className="wf-step wf-step-running">
                    <div className="wf-step-marker wf-step-marker-running">⋯</div>
                    <div className="wf-step-body">
                      <div className="wf-step-title">等待事件…</div>
                    </div>
                  </div>
                )}
              </div>
            </div>
          )}
          </div>

          {/* 右侧：运行参数面板（可折叠） */}
          <aside className={`workflow-side ${runPanelCollapsed ? 'is-collapsed' : ''}`}>
            {/* 折叠态：仅显示展开按钮（不放运行按钮，必须点开展开才能运行） */}
            {runPanelCollapsed && (
              <div className="workflow-side-collapsed">
                <button
                  type="button"
                  className="workflow-side-expand"
                  onClick={() => setRunPanelCollapsed(false)}
                  title="展开运行面板"
                >
                  <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
                    <polyline points="15 18 9 12 15 6" />
                  </svg>
                </button>
                <div className="workflow-side-collapsed-title">运行</div>
              </div>
            )}

            {/* 展开态：完整表单卡片 */}
            {!runPanelCollapsed && (
              <div className="execute-card workflow-side-card">
                <div className="execute-card-head workflow-side-card-head">
                  <h3 className="execute-card-title">
                    {running ? '⚡ 运行中…' : '⚙️ 运行参数'}
                  </h3>
                  <button
                    type="button"
                    className="workflow-side-close"
                    onClick={() => setRunPanelCollapsed(true)}
                    title="收起运行面板"
                    aria-label="收起运行面板"
                  >
                    <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
                      <line x1="18" y1="6" x2="6" y2="18" />
                      <line x1="6" y1="6" x2="18" y2="18" />
                    </svg>
                  </button>
                </div>

                {paramsLoading && <div className="wf-form-loading">表单参数加载中…</div>}
                {paramsError && <div className="execute-detail-error">⚠ {paramsError}</div>}

                {!paramsLoading && userInputForm.length > 0 && (
                  <div className="wf-form">
                    {userInputForm.map((item, i) => renderFormField(item, i))}
                  </div>
                )}
                {!paramsLoading && !paramsError && userInputForm.length === 0 && (
                  <div className="wf-form-empty">该工作流无输入参数，点击下方按钮直接执行</div>
                )}

                <div className="wf-form-actions">
                  <button
                    className="execute-send-btn"
                    onClick={handleRunWorkflow}
                    disabled={running}
                  >
                    {running ? '执行中…' : '🚀 开始执行'}
                  </button>
                  {runError && <span className="execute-detail-error">⚠ {runError}</span>}
                </div>
              </div>
            )}
          </aside>
        </div>
      )}

      {/* ============== 聊天模式（chat/advanced-chat/agent-chat/agentic） ============== */}
      {isChat && (
        <div className="execute-chat-shell">
          <div className="execute-chat-orb execute-chat-orb-a" />
          <div className="execute-chat-orb execute-chat-orb-b" />
          <div className="execute-card execute-chat-card">
            <div className="execute-chat-toolbar">
              <div className="execute-chat-agent-head">
                <div className="execute-detail-icon" style={{ background: agent.iconBackground || '#FFEAD5' }}>
                  {agent.icon && typeof agent.icon === 'object' && agent.icon.type === 'image' ? (
                    <img src={buildIconUrl(agent.icon.url)} alt={agent.name} className="execute-detail-icon-img" />
                  ) : (
                    agent.icon || '🤖'
                  )}
                </div>
                <div className="execute-chat-agent-meta">
                  <h1 className="execute-detail-name">
                    {loading ? '加载中…' : (agent.name || '智能体')}
                  </h1>
                  <div className="execute-detail-sub">
                    {mode && <span className="execute-detail-mode-tag">{MODE_LABEL[mode] || mode}</span>}
                  </div>
                  {error && <div className="execute-detail-error">⚠ {error}</div>}
                </div>
              </div>
              <div className="execute-chat-toolbar-actions">
                <button
                  type="button"
                  className="execute-detail-history-trigger"
                  onClick={handleNewChatClick}
                  title="新会话"
                >
                  <svg className="new-chat-icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                    <path d="M12 5v14" />
                    <path d="M5 12h14" />
                  </svg>
                  <span>{'新会话'}</span>
                </button>
                <button
                  type="button"
                  className="execute-detail-history-trigger"
                  onClick={handleToggleHistory}
                  title="历史记录"
                >
                  <svg className="history-icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                    <path d="M3 12a9 9 0 1 0 3-6.7L3 8" />
                    <path d="M3 3v5h5" />
                    <path d="M12 7v5l3 2" />
                  </svg>
                  <span>历史记录</span>
                </button>
                {hasActiveChatSnapshot && (
                  <button
                    type="button"
                    className="execute-back-active-btn"
                    onClick={handleBackToActiveChat}
                  >
                    返回正在回答
                  </button>
                )}
                <div className="execute-chat-status">
                  <span className={`execute-chat-status-dot ${chatRunning ? 'is-running' : ''}`} />
                  {chatRunning ? '正在生成' : '随时可问'}
                </div>
              </div>
            </div>
            <div className="execute-messages" ref={messagesRef} onScroll={handleMessagesScroll}>
            {messages.length === 0 ? (
              <div className="execute-empty">
                <div className="execute-empty-icon">{emptyState.icon}</div>
                <div className="execute-empty-title">{emptyState.title}</div>
                <div className="execute-empty-desc">{emptyState.description}</div>
                <div className="execute-empty-prompts">
                  {emptyState.prompts.map(prompt => <span key={prompt}>{prompt}</span>)}
                </div>
              </div>
            ) : (
              messages.map((m, i) => (
                <div key={m.id || i} className={`execute-msg execute-msg-${m.role}`}>
                  <span className="execute-msg-role">{m.role === 'user' ? '我' : agent.name}</span>
                  <div className="execute-msg-bubble">
                    {m.role === 'assistant' ? (
                      renderAssistantMessage(m)
                    ) : (
                      <div
                        className="markdown-body"
                        dangerouslySetInnerHTML={{ __html: renderMarkdown(m.content) }}
                      />
                    )}
                    {m.streaming && !getActiveStepHint(m) && <span className="wf-step-cursor">▍</span>}
                    {!m.streaming && m.role === 'assistant' && m.messageId && m.content && (
                    <div className="execute-msg-actions">
                      <button
                        type="button"
                        className={`execute-msg-icon-btn${copiedMsgId === m.id ? ' is-active' : ''}`}
                        data-tip={copiedMsgId === m.id ? '已复制' : '复制'}
                        onClick={() => handleCopyMessage(m)}
                      >
                        <IconCopy />
                      </button>
                      {canEditMessage && (
                        <button
                          type="button"
                          className={`execute-msg-icon-btn${m.edited ? ' is-active' : ''}`}
                          data-tip="编辑"
                          onClick={() => handleOpenEditor(m)}
                        >
                          <IconEdit />
                        </button>
                      )}
                      <button
                        type="button"
                        className={`execute-msg-icon-btn${speakingId === m.id ? ' is-active is-speaking' : ''}`}
                        data-tip={speakingId === m.id ? '停止播放' : '听语音'}
                        onClick={() => handleToggleSpeak(m)}
                      >
                        {speakingId === m.id ? <IconStop /> : <IconVolume />}
                      </button>
                      <button
                        type="button"
                        className={`execute-msg-icon-btn execute-msg-like-btn${m.feedback === 'like' ? ' is-liked' : ''}`}
                        data-tip={m.feedback === 'like' ? '取消点赞' : '点赞'}
                        onClick={() => handleFeedback(m, 'like')}
                      >
                        <IconThumbUp />
                      </button>
                      <button
                        type="button"
                        className={`execute-msg-icon-btn execute-msg-dislike-btn${m.feedback === 'dislike' ? ' is-disliked' : ''}`}
                        data-tip={m.feedback === 'dislike' ? '取消点踩' : '点踩'}
                        onClick={() => handleFeedback(m, 'dislike')}
                      >
                        <IconThumbDown />
                      </button>
                      <button
                        type="button"
                        className="execute-msg-icon-btn"
                        data-tip="重新生成"
                        disabled={chatRunning || String(m.id || '').startsWith('history-')}
                        onClick={() => handleRegenerate(m)}
                      >
                        <IconRefresh />
                      </button>
                      {isReportAgent(agent) && (
                      <div className="execute-msg-actions-right">
                        <button
                          type="button"
                          className="execute-msg-action-btn execute-msg-download-btn execute-msg-download-word"
                          disabled={!!m.downloading}
                          onClick={() => handleDownloadMessage(m, 'word')}
                          title="下载 Word"
                        >
                          <img src={wordIcon} alt="" className="execute-msg-download-icon" />
                          <span className="execute-msg-download-text">{m.downloading === 'word' ? '生成中...' : 'Word'}</span>
                        </button>
                        <button
                          type="button"
                          className="execute-msg-action-btn execute-msg-download-btn execute-msg-download-pdf"
                          disabled={!!m.downloading}
                          onClick={() => handleDownloadMessage(m, 'pdf')}
                          title="下载 PDF"
                        >
                          <img src={pdfIcon} alt="" className="execute-msg-download-icon" />
                          <span className="execute-msg-download-text">{m.downloading === 'pdf' ? '生成中...' : 'PDF'}</span>
                        </button>
                      </div>
                      )}
                    </div>
                    )}
                  </div>
                </div>
              ))
            )}
            </div>
            {chatRunError && <div className="execute-detail-error execute-chat-error">⚠ {chatRunError}</div>}
            <div className="execute-input-panel">
              <div className="execute-input-hint">Shift + Enter 换行 · Enter 发送</div>
              <div className="execute-input-bar">
                <textarea
                  className="execute-input"
                  value={input}
                  onChange={e => setInput(e.target.value)}
                  onKeyDown={e => {
                    if (e.key === 'Enter' && !e.shiftKey) {
                      e.preventDefault()
                      handleSend()
                    }
                  }}
                  placeholder="请输入你的问题或任务，可补充背景信息和期望的输出要求，我会结合当前智能体能力为你解答..."
                  disabled={chatRunning}
                  rows={2}
                />
            <button
              type="button"
              className="execute-params-btn"
              onClick={() => setShowChatParamsModal(true)}
              title="配置参数"
              disabled={paramsLoading}
            >
              <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <line x1="4" y1="21" x2="4" y2="14" />
                <line x1="4" y1="10" x2="4" y2="3" />
                <line x1="12" y1="21" x2="12" y2="12" />
                <line x1="12" y1="8" x2="12" y2="3" />
                <line x1="20" y1="21" x2="20" y2="16" />
                <line x1="20" y1="12" x2="20" y2="3" />
                <line x1="1" y1="14" x2="7" y2="14" />
                <line x1="9" y1="8" x2="15" y2="8" />
                <line x1="17" y1="16" x2="23" y2="16" />
              </svg>
              <span>参数</span>
              {(() => {
                const filled = userInputForm.filter(item => {
                  const t = Object.keys(item)[0]
                  const c = item[t]
                  if (!c?.variable) return false
                  const v = formValues[c.variable]
                  return v !== '' && v !== undefined && v !== null
                }).length
                return filled > 0 ? <span className="execute-params-dot">{filled}</span> : null
              })()}
            </button>
            {chatRunning ? (
              <button
                className="execute-send-btn execute-stop-btn"
                onClick={handleStopChat}
                type="button"
              >
                <svg className="execute-btn-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true">
                  <rect x="7" y="7" width="10" height="10" rx="1.5" />
                </svg>
                <span>停止</span>
              </button>
            ) : (
              <button
                className="execute-send-btn"
                onClick={handleSend}
                disabled={!input.trim() || loading || paramsLoading}
                type="button"
              >
                <svg className="execute-btn-icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                  <path d="M22 2L11 13" />
                  <path d="M22 2L15 22L11 13L2 9L22 2Z" />
                </svg>
                <span>发送</span>
              </button>
            )}
              </div>
            </div>
          </div>
        </div>
      )}

      {/* ============== API Key 弹框 ============== */}
      {showApiKeyModal && apiKey && (
        <div className="apikey-modal-mask" onClick={() => setShowApiKeyModal(false)}>
          <div className="apikey-modal" onClick={e => e.stopPropagation()}>
            <div className="apikey-modal-head">
              <h3>API Key</h3>
              <button
                type="button"
                className="apikey-modal-close"
                onClick={() => setShowApiKeyModal(false)}
                aria-label="关闭"
              >×</button>
            </div>
            <div className="apikey-modal-body">
              <code className="apikey-modal-value">{apiKey}</code>
              <p className="apikey-modal-tip">请妥善保管，不要在客户端代码中暴露此 Key。</p>
            </div>
            <div className="apikey-modal-foot">
              <button
                type="button"
                className={`apikey-modal-copy ${copied ? 'is-copied' : ''}`}
                onClick={handleCopyApiKey}
              >
                {copied ? '✓ 已复制' : '复制'}
              </button>
            </div>
          </div>
        </div>
      )}

      {/* ============== 聊天模式：参数配置弹框 ============== */}
      {showChatParamsModal && isChat && (
        <div className="apikey-modal-mask" onClick={() => setShowChatParamsModal(false)}>
          <div className="apikey-modal chat-params-modal" onClick={e => e.stopPropagation()}>
            <div className="apikey-modal-head">
              <h3>⚙️ 输入参数</h3>
              <button
                type="button"
                className="apikey-modal-close"
                onClick={() => setShowChatParamsModal(false)}
                aria-label="关闭"
              >×</button>
            </div>
            <div className="apikey-modal-body chat-params-modal-body">
              {paramsLoading && <div className="wf-form-loading">表单参数加载中…</div>}
              {paramsError && <div className="execute-detail-error">⚠ {paramsError}</div>}
              {!paramsLoading && userInputForm.length > 0 && (
                <div className="wf-form">
                  {userInputForm.map((item, i) => renderFormField(item, i))}
                </div>
              )}
              {!paramsLoading && !paramsError && userInputForm.length === 0 && (
                <div className="wf-form-empty">该对话流无输入参数，可直接发送消息</div>
              )}
            </div>
            <div className="apikey-modal-foot">
              <button
                type="button"
                className="apikey-modal-copy"
                onClick={() => setShowChatParamsModal(false)}
              >
                完成
              </button>
            </div>
          </div>
        </div>
      )}
      {historyOpen && (
        <>
          <div className="execute-drawer-mask is-open" onClick={() => setHistoryOpen(false)} />
          <aside className="execute-drawer is-open" role="dialog" aria-label="历史记录">
            <div className="execute-drawer-head">
              <button type="button" className="execute-drawer-close" onClick={() => setHistoryOpen(false)} aria-label="关闭">x</button>
              <div className="execute-drawer-title">历史记录</div>
            </div>
            <div className="execute-drawer-body" onScroll={handleHistoryScroll}>
              {historyLoading ? (
                <div className="execute-drawer-status">加载中...</div>
              ) : historyError ? (
                <div className="execute-drawer-status is-error">{historyError}</div>
              ) : historyList.length === 0 ? (
                <div className="execute-drawer-status">暂无历史记录</div>
              ) : (
                <>
                  <ul className="execute-drawer-list">
                    {historyList.map(conv => (
                      <li key={conv.id} className={`execute-drawer-item ${resumingConv === conv.id ? 'is-loading' : ''}`} onClick={() => handleSelectHistory(conv)}>
                        <div className="execute-drawer-item-main">
                          <div className="execute-drawer-item-name">{conv.name || conv.summary || '新会话'}</div>
                          <div className="execute-drawer-item-meta">{formatConvTime(conv.updated_at)}</div>
                        </div>
                      </li>
                    ))}
                  </ul>
                  {historyLoadingMore && <div className="execute-drawer-status">加载中...</div>}
                  {!historyLoadingMore && !historyHasMore && <div className="execute-drawer-status">没有更多了</div>}
                </>
              )}
            </div>
          </aside>
        </>
      )}

      {editingMsg && (
        <>
          <div className="execute-drawer-mask is-open" onClick={handleCloseEditor} />
          <aside className="execute-drawer is-open execute-editor-drawer" role="dialog" aria-label="编辑内容">
            <div className="execute-drawer-head">
              <button type="button" className="execute-drawer-close" onClick={handleCloseEditor} aria-label="关闭">x</button>
              <div className="execute-drawer-title">编辑内容</div>
            </div>
            <div className="execute-drawer-body execute-editor-body">
              <RichTextEditor
                value={editDraft}
                onChange={setEditDraft}
                placeholder="请编辑报告内容，支持富文本格式..."
              />
            </div>
            <div className="execute-editor-foot">
              <button
                type="button"
                className="execute-editor-export"
                disabled={!editDraft || editorExporting}
                onClick={handleExportDocx}
              >
                {editorExporting ? '导出中…' : '导出 DOCX'}
              </button>
              <button type="button" className="execute-editor-cancel" onClick={handleCloseEditor}>取消</button>
              <button type="button" className="execute-editor-save" onClick={handleSaveEditor}>保存</button>
            </div>
          </aside>
        </>
      )}

    </div>
  )
}

export default AgentExecute
