<template>
  <div class="tablet-full-page" v-loading="pageLoading">
    <!-- ── 顶部工具栏 ── -->
    <header class="page-toolbar">
      <div class="title-wrap">
        <h2>专家评审资料</h2>
        <span>当日项目编目 · {{ projectCount }} 项</span>
      </div>
      <div class="tool-wrap">
        <el-input
          v-model="keyword"
          clearable
          placeholder="搜索项目/资料"
          class="search-input"
        />
        <el-button :icon="Refresh" @click="loadCatalog">刷新</el-button>
      </div>
    </header>

    <!-- ── 主体三栏 ── -->
    <div class="page-body" :class="{ 'ai-open': aiPanelOpen && !!activeProjectId }">

      <!-- 左：项目目录 -->
      <aside class="directory-panel">
        <div class="panel-title">项目目录</div>
        <el-empty v-if="treeData.length === 0" description="当前时段暂无可查看资料" />
        <el-tree
          v-else
          ref="treeRef"
          class="catalog-tree"
          :data="treeData"
          node-key="id"
          highlight-current
          :expand-on-click-node="false"
          :default-expanded-keys="expandedKeys"
          :props="treeProps"
          :filter-node-method="filterNode"
          @node-expand="handleNodeExpand"
          @node-collapse="handleNodeCollapse"
          @node-click="handleNodeClick"
        >
          <template #default="{ data }">
            <div v-if="data.type === 'project'" class="tree-project">
              <div class="node-left">
                <el-icon class="node-icon folder-icon"><Folder /></el-icon>
                <span class="node-name">{{ data.label }}</span>
              </div>
              <el-tag size="small" type="info">{{ data.fileCount }} 份</el-tag>
            </div>
            <div v-else class="tree-file">
              <div class="node-left">
                <el-icon class="node-icon file-icon"><Document /></el-icon>
                <span class="node-name">{{ data.label }}</span>
              </div>
              <span class="file-type">{{ data.fileType?.toUpperCase() }}</span>
            </div>
          </template>
        </el-tree>
      </aside>

      <!-- 中：资料预览 -->
      <section class="preview-panel">
        <div class="preview-header">
          <div class="preview-meta">
            <div class="preview-title">{{ activeFileName || '请选择左侧资料进行预览' }}</div>
            <div class="preview-sub">{{ activeFileDesc }}</div>
          </div>
          <div class="preview-actions">
            <el-button :disabled="!activeFileId" @click="refreshPreview">刷新预览</el-button>
            <el-button
              type="primary"
              :icon="Link"
              :disabled="!previewUrl"
              @click="openInNewWindow"
            >
              新窗口打开
            </el-button>
            <!-- AI 助手开关 -->
            <el-button
              v-if="activeProjectId"
              :type="aiPanelOpen ? 'primary' : 'default'"
              @click="toggleAiPanel"
            >
              🤖 AI 助手
            </el-button>
          </div>
        </div>

        <div class="preview-body" v-loading="previewLoading">
          <div v-if="!activeFileId && !previewLoading" class="welcome-panel">
            <div class="welcome-title">欢迎各位专家莅临指导工作</div>
            <div class="welcome-sub">请从左侧目录中选择评审资料开始查阅</div>
          </div>
          <el-result v-else-if="previewError" icon="error" title="预览加载失败" :sub-title="previewError">
            <template #extra>
              <el-button type="primary" :disabled="!activeFileId" @click="refreshPreview">重试</el-button>
              <el-button :disabled="!previewUrl" @click="openInNewWindow">新窗口打开</el-button>
            </template>
          </el-result>
          <template v-else-if="previewUrl">
            <iframe
              :key="previewFrameKey"
              :src="previewUrl"
              class="preview-iframe"
              frameborder="0"
              allowfullscreen
              @load="handlePreviewFrameLoad"
              @error="handlePreviewFrameError"
            ></iframe>
            <div v-if="previewFrameLoading && !previewError" class="preview-loading-mask">
              <div class="welcome-title">{{ previewLoadingTitle }}</div>
              <div class="welcome-sub">{{ previewLoadingSubtitle }}</div>
            </div>
          </template>
        </div>
      </section>

      <!-- 右：AI 助手面板 -->
      <aside v-if="aiPanelOpen && activeProjectId" class="ai-panel">
        <!-- 面板头部 -->
        <div class="ai-header">
          <div class="ai-header-left">
            <span class="ai-title">🤖 AI 助手</span>
            <span
              class="ai-status-badge"
              :class="`status-${aiSummary?.status ?? 0}`"
            >{{ aiSummary?.statusName ?? '未生成' }}</span>
          </div>
          <div class="ai-header-right">
            <el-tooltip content="清空当前项目问答记录" placement="top">
              <el-button
                size="small"
                text
                :disabled="!aiConversationId || aiChatLoading"
                @click="handleClearMessages"
              >清空对话</el-button>
            </el-tooltip>
            <el-button size="small" text @click="aiPanelOpen = false">✕</el-button>
          </div>
        </div>

        <!-- 摘要区 -->
        <div class="ai-summary-wrap" v-loading="aiSummaryLoading">
          <!-- 未生成 -->
          <div v-if="!aiSummary || aiSummary.status === 0" class="ai-summary-empty">
            <el-icon><InfoFilled /></el-icon>
            <span>当前项目尚未生成 AI 摘要，请上传资料后自动触发生成</span>
          </div>
          <!-- 生成中 -->
          <div v-else-if="aiSummary.status === 1" class="ai-summary-building">
            <el-icon class="is-loading"><Loading /></el-icon>
            <span>AI 摘要生成中，请稍候…</span>
          </div>
          <!-- 失败 -->
          <div v-else-if="aiSummary.status === 3" class="ai-summary-failed">
            <el-icon><WarningFilled /></el-icon>
            <span>摘要生成失败：{{ aiSummary.errorMessage || '未知错误' }}</span>
          </div>
          <!-- 已完成 -->
          <div v-else-if="aiSummary.status === 2 && aiSummary.summary" class="ai-summary-card">
            <div class="summary-updated">
              最后更新：{{ aiSummary.updatedTime ? formatDate(new Date(aiSummary.updatedTime), 'YYYY-MM-DD HH:mm') : '-' }}
            </div>
            <div class="summary-field">
              <span class="field-label">项目概述</span>
              <span class="field-value">{{ aiSummary.summary.projectOverview }}</span>
            </div>
            <div class="summary-field">
              <span class="field-label">建设目标</span>
              <span class="field-value">{{ aiSummary.summary.businessGoal }}</span>
            </div>
            <div class="summary-field">
              <span class="field-label">建设范围</span>
              <span class="field-value">{{ aiSummary.summary.constructionScope }}</span>
            </div>
            <div class="summary-field">
              <span class="field-label">实施计划</span>
              <span class="field-value">{{ aiSummary.summary.implementationPlan }}</span>
            </div>
            <div class="summary-field">
              <span class="field-label">预算信息</span>
              <span class="field-value">{{ aiSummary.summary.budgetInfo }}</span>
            </div>
            <div v-if="aiSummary.summary.sourceFiles?.length" class="summary-field">
              <span class="field-label">来源文件</span>
              <span class="field-value">{{ aiSummary.summary.sourceFiles.join('；') }}</span>
            </div>
          </div>
        </div>

        <!-- 分隔线 -->
        <div class="ai-divider" v-if="aiConversationId || aiSummary?.status === 2"></div>

        <!-- 对话区（仅摘要 SUCCESS 后才能对话） -->
        <template v-if="aiSummary?.status === 2">
          <!-- 消息列表 -->
          <div class="ai-messages" ref="aiMessagesRef">
            <div v-if="aiChatLoading && aiMessages.length === 0" class="ai-messages-loading">
              <el-icon class="is-loading"><Loading /></el-icon> 加载中…
            </div>
            <div
              v-for="msg in aiMessages"
              :key="msg.id"
              class="chat-msg"
              :class="msg.type === 'user' ? 'user-msg' : 'ai-msg'"
            >
              <div class="msg-bubble">
                <span class="msg-role">{{ msg.type === 'user' ? '您' : 'AI' }}</span>
                <div class="msg-content">
                  <div
                    class="md-content"
                    v-html="msg.type === 'user' ? renderUserMsgContent(msg.content) : renderAiMsgContent(msg.content)"
                  ></div>
                </div>
              </div>
            </div>
            <!-- 流式生成中的气泡 -->
            <div v-if="streamingText || streamingLoading" class="chat-msg ai-msg">
              <div class="msg-bubble">
                <span class="msg-role">AI</span>
                <div class="msg-content">
                  <span v-if="streamingLoading && !streamingText" class="streaming-dot">思考中…</span>
                  <div v-else class="md-content" v-html="renderAiMsgContent(streamingText)"></div>
                </div>
              </div>
            </div>
          </div>

          <!-- 快捷问题 -->
          <div class="quick-questions">
            <button
              v-for="q in QUICK_QUESTIONS"
              :key="q"
              class="quick-btn"
              :disabled="streamingLoading"
              @click="sendQuestion(q)"
            >{{ q }}</button>
          </div>

          <!-- 输入框 -->
          <div class="ai-input-wrap">
            <el-input
              v-model="chatInput"
              type="textarea"
              :rows="2"
              resize="none"
              placeholder="输入问题，基于当前项目资料回答…"
              :disabled="streamingLoading"
              @keydown.enter.exact.prevent="handleSend"
            />
            <div class="ai-input-actions">
              <el-button
                v-if="streamingLoading"
                type="danger"
                size="small"
                @click="stopStream"
              >停止</el-button>
              <el-button
                v-else
                type="primary"
                size="small"
                :disabled="!chatInput.trim()"
                @click="handleSend"
              >发送</el-button>
            </div>
          </div>
        </template>

        <!-- 摘要未就绪时提示 -->
        <div v-else-if="aiSummary && aiSummary.status !== 2" class="ai-chat-unavail">
          AI 摘要就绪后才能开始对话
        </div>
      </aside>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue'
import { Document, Folder, InfoFilled, Link, Loading, Refresh, WarningFilled } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import MarkdownIt from 'markdown-it'
import { formatDate } from '@/utils/formatTime'
import {
  getFileOpenUrl,
  getProjectFiles,
  getTodayCatalog,
  type ReviewTabletCatalogVO,
  type ReviewTabletOpenUrlVO
} from '@/api/review/tablet'
import type { ReviewMeetingFileRespVO } from '@/api/review/project'
import {
  AI_SUMMARY_STATUS,
  clearProjectAiMessages,
  getProjectAiSummary,
  openProjectAiConversation,
  sendProjectAiChatStream,
  type ReviewAiSummaryVO
} from '@/api/review/ai'
import { ChatMessageApi, type ChatMessageVO } from '@/api/ai/chat/message/index'

defineOptions({ name: 'ReviewTablet' })

// ── 快捷问题 ──────────────────────────────────────────────────
const QUICK_QUESTIONS = [
  '这个项目要解决什么问题？',
  '建设范围包括哪些内容？',
  '实施计划是怎样的？',
  '预算或投入信息有哪些？'
]

// ── 类型 ──────────────────────────────────────────────────────
type TreeNode = ProjectTreeNode | FileTreeNode

interface ProjectTreeNode {
  id: string
  type: 'project'
  label: string
  projectId: number
  meetingName: string
  fileCount: number
  seqNo: number
  loaded: boolean
  loading: boolean
  source: ReviewTabletCatalogVO
  children: FileTreeNode[]
}

interface FileTreeNode {
  id: string
  type: 'file'
  label: string
  projectId: number
  fileId: number
  fileType: string
  fileSize: number
  createTime: string
  source: ReviewMeetingFileRespVO
}

// ── 目录 & 预览 state ─────────────────────────────────────────
const pageLoading = ref(false)
const previewLoading = ref(false)
const previewError = ref('')
const keyword = ref('')

const treeRef = ref<any>()
const treeData = ref<ProjectTreeNode[]>([])
const expandedKeys = ref<string[]>([])
const fileCache = ref<Record<number, ReviewMeetingFileRespVO[]>>({})

const activeProjectId = ref<number>()
const activeFileId = ref<number>()
const activeFileNode = ref<FileTreeNode>()
const previewUrl = ref('')
const previewPayload = ref<ReviewTabletOpenUrlVO>()
const previewFrameLoading = ref(false)
const previewFrameKey = ref(0)
const previewRequestToken = ref(0)

const treeProps = { label: 'label', children: 'children' }

const projectCount = computed(() => treeData.value.length)
const activeFileName = computed(() => activeFileNode.value?.label || previewPayload.value?.fileName || '')
const activeFileDesc = computed(() => {
  if (!activeFileNode.value) return ''
  const segs: string[] = []
  if (activeFileNode.value.fileType) segs.push(activeFileNode.value.fileType.toUpperCase())
  if (activeFileNode.value.fileSize) segs.push(formatFileSize(activeFileNode.value.fileSize))
  if (activeFileNode.value.createTime)
    segs.push(`上传于 ${formatDate(new Date(activeFileNode.value.createTime), 'YYYY-MM-DD HH:mm')}`)
  return segs.join(' · ')
})
const previewLoadingTitle = computed(() =>
  isOfficeFile(activeFileNode.value?.fileType) ? '正在转换文档预览' : '正在加载预览'
)
const previewLoadingSubtitle = computed(() =>
  isOfficeFile(activeFileNode.value?.fileType)
    ? 'Office 文件首次打开可能需要一些时间，请稍候'
    : '正在为您打开当前资料'
)

// ── AI 面板 state ─────────────────────────────────────────────
const aiPanelOpen = ref(false)
const aiSummaryLoading = ref(false)
const aiSummary = ref<ReviewAiSummaryVO | null>(null)
const aiConversationId = ref<number | null>(null)
const aiMessages = ref<ChatMessageVO[]>([])
const aiChatLoading = ref(false)
const chatInput = ref('')
const streamingText = ref('')
const streamingLoading = ref(false)
const aiMessagesRef = ref<HTMLElement>()
let streamAbortCtrl: AbortController | null = null
let summaryPollingTimer: ReturnType<typeof setTimeout> | null = null
let officeWarmupTimers: ReturnType<typeof setTimeout>[] = []
let previewFrameTimeoutTimer: ReturnType<typeof setTimeout> | null = null

// ── 搜索过滤 ──────────────────────────────────────────────────
watch(keyword, (val) => treeRef.value?.filter(val.trim()))

const filterNode = (value: string, data: TreeNode): boolean => {
  if (!value) return true
  const kw = value.toLowerCase()
  if (data.type === 'project') {
    return [data.label, data.meetingName].join('|').toLowerCase().includes(kw)
      || data.children.some((f) => f.label.toLowerCase().includes(kw))
  }
  return [data.label, data.fileType].join('|').toLowerCase().includes(kw)
}

// ── 树节点构建 ────────────────────────────────────────────────
const buildProjectNode = (item: ReviewTabletCatalogVO): ProjectTreeNode => ({
  id: `project-${item.reviewMeetingProjectId}`,
  type: 'project',
  label: item.projectTitle,
  projectId: item.reviewMeetingProjectId,
  meetingName: item.meetingName,
  fileCount: item.fileCount,
  seqNo: item.seqNo,
  loaded: false,
  loading: false,
  source: item,
  children: []
})

const toFileNode = (file: ReviewMeetingFileRespVO, projectId: number): FileTreeNode => ({
  id: `file-${projectId}-${file.id}`,
  type: 'file',
  label: file.fileName,
  projectId,
  fileId: file.id,
  fileType: file.fileType,
  fileSize: file.fileSize,
  createTime: file.createTime,
  source: file
})

// ── 文件加载 ──────────────────────────────────────────────────
const loadProjectFiles = async (projectNode: ProjectTreeNode) => {
  if (projectNode.loaded || projectNode.loading) return
  const cached = fileCache.value[projectNode.projectId]
  if (cached) {
    projectNode.children = cached.map((f) => toFileNode(f, projectNode.projectId))
    projectNode.loaded = true
    return
  }
  projectNode.loading = true
  try {
    const files = await getProjectFiles(projectNode.projectId)
    fileCache.value[projectNode.projectId] = files
    projectNode.children = files.map((f) => toFileNode(f, projectNode.projectId))
    projectNode.loaded = true
    scheduleOfficeWarmup(files)
  } catch {
    ElMessage.error('加载项目资料失败，请稍后重试')
    throw new Error('load-project-files-failed')
  } finally {
    projectNode.loading = false
  }
}

// ── 预览 ──────────────────────────────────────────────────────
const loadPreview = async (fileId: number) => {
  const requestToken = ++previewRequestToken.value
  clearPreviewFrameTimeout()
  previewLoading.value = true
  previewFrameLoading.value = true
  previewError.value = ''
  previewUrl.value = ''
  previewPayload.value = undefined
  previewFrameKey.value += 1
  try {
    const payload = await getFileOpenUrl(fileId)
    if (requestToken !== previewRequestToken.value) return
    previewPayload.value = payload
    if (!payload?.openUrl) { previewUrl.value = ''; previewFrameLoading.value = false; previewError.value = '未获取到预览地址'; return }
    previewUrl.value = payload.openUrl
    startPreviewFrameTimeout(requestToken)
  } catch {
    if (requestToken !== previewRequestToken.value) return
    previewUrl.value = ''
    previewFrameLoading.value = false
    previewError.value = '预览服务暂不可用，请稍后重试'
  } finally {
    if (requestToken === previewRequestToken.value) {
      previewLoading.value = false
    }
  }
}

const selectFileNode = async (fileNode: FileTreeNode) => {
  activeProjectId.value = fileNode.projectId
  activeFileId.value = fileNode.fileId
  activeFileNode.value = fileNode
  await loadPreview(fileNode.fileId)
}

const openInNewWindow = () => {
  const url = previewPayload.value?.openUrl || previewUrl.value
  if (!url) { ElMessage.warning('暂无可打开的预览地址'); return }
  window.open(url, '_blank', 'noopener,noreferrer')
}

const refreshPreview = async () => { if (activeFileId.value) await loadPreview(activeFileId.value) }

const handlePreviewFrameLoad = () => {
  clearPreviewFrameTimeout()
  previewFrameLoading.value = false
}

const handlePreviewFrameError = () => {
  clearPreviewFrameTimeout()
  previewFrameLoading.value = false
  previewError.value = '预览内容加载失败，请稍后重试'
}

// ── 树事件 ────────────────────────────────────────────────────
const handleNodeExpand = async (data: TreeNode) => {
  if (data.type !== 'project') return
  if (!expandedKeys.value.includes(data.id)) expandedKeys.value.push(data.id)
  await loadProjectFiles(data)
}

const handleNodeCollapse = (data: TreeNode) => {
  if (data.type !== 'project') return
  expandedKeys.value = expandedKeys.value.filter((id) => id !== data.id)
}

const handleNodeClick = async (data: TreeNode) => {
  if (data.type === 'project') {
    const switched = activeProjectId.value !== data.projectId
    activeProjectId.value = data.projectId
    await loadProjectFiles(data)
    if (switched) resetProjectAiState()
    if (aiPanelOpen.value) await loadProjectAi(data.projectId)
    return
  }
  await selectFileNode(data)
}

// ── 目录加载 ──────────────────────────────────────────────────
const loadCatalog = async () => {
  pageLoading.value = true
  clearPreviewFrameTimeout()
  clearOfficeWarmupTimers()
  previewRequestToken.value += 1
  previewError.value = ''
  previewUrl.value = ''
  previewPayload.value = undefined
  activeFileId.value = undefined
  activeFileNode.value = undefined
  activeProjectId.value = undefined
  resetProjectAiState()
  aiPanelOpen.value = false
  try {
    const catalog = await getTodayCatalog()
    treeData.value = catalog
      .sort((a, b) => (a.seqNo || 0) - (b.seqNo || 0))
      .map((item) => buildProjectNode(item))
    fileCache.value = {}
    expandedKeys.value = []
    await nextTick()
    treeRef.value?.setCurrentKey(undefined)
  } finally {
    pageLoading.value = false
  }
}

// ── AI 面板 ───────────────────────────────────────────────────
const toggleAiPanel = async () => {
  aiPanelOpen.value = !aiPanelOpen.value
  if (aiPanelOpen.value && activeProjectId.value) {
    await loadProjectAi(activeProjectId.value)
  }
}

const resetProjectAiState = () => {
  stopStream()
  clearSummaryPolling()
  aiSummary.value = null
  aiConversationId.value = null
  aiMessages.value = []
  chatInput.value = ''
  streamingText.value = ''
}

const loadProjectAi = async (projectId: number) => {
  resetProjectAiState()
  aiSummaryLoading.value = true
  try {
    aiSummary.value = await getProjectAiSummary(projectId)
    // 如果生成中，开启轮询
    if (aiSummary.value?.status === AI_SUMMARY_STATUS.BUILDING) {
      startSummaryPolling(projectId)
    }
    // 已完成则加载会话
    if (aiSummary.value?.status === AI_SUMMARY_STATUS.SUCCESS) {
      await openAndLoadConversation(projectId)
    }
  } catch (e: any) {
    // 如果是视图窗口未到，静默处理
    if (e?.code !== 1900009006) ElMessage.error('加载 AI 摘要失败')
  } finally {
    aiSummaryLoading.value = false
  }
}

const openAndLoadConversation = async (projectId: number) => {
  aiChatLoading.value = true
  try {
    const conv = await openProjectAiConversation(projectId)
    aiConversationId.value = conv.conversationId
    const msgs = await ChatMessageApi.getChatMessageListByConversationId(conv.conversationId)
    aiMessages.value = msgs || []
    await scrollAiToBottom()
  } catch {
    // 会话打开失败不影响主流程
  } finally {
    aiChatLoading.value = false
  }
}

// 摘要生成中轮询
const startSummaryPolling = (projectId: number) => {
  clearSummaryPolling()
  summaryPollingTimer = setTimeout(async () => {
    if (activeProjectId.value !== projectId || !aiPanelOpen.value) return
    try {
      aiSummary.value = await getProjectAiSummary(projectId)
    } catch { /* silent */ }
    if (aiSummary.value?.status === AI_SUMMARY_STATUS.BUILDING) {
      startSummaryPolling(projectId)
    } else if (aiSummary.value?.status === AI_SUMMARY_STATUS.SUCCESS) {
      await openAndLoadConversation(projectId)
    }
  }, 4000)
}

const clearSummaryPolling = () => {
  if (summaryPollingTimer) { clearTimeout(summaryPollingTimer); summaryPollingTimer = null }
}

// ── AI 对话 ───────────────────────────────────────────────────
const sendQuestion = (q: string) => { chatInput.value = q; handleSend() }

const handleSend = async () => {
  const content = chatInput.value.trim()
  if (!content || streamingLoading.value || !aiConversationId.value || !activeProjectId.value) return
  chatInput.value = ''

  // 先插入用户消息
  aiMessages.value.push({ id: Date.now(), type: 'user', content, createTime: new Date() } as any)
  streamingLoading.value = true
  streamingText.value = ''
  await scrollAiToBottom()

  streamAbortCtrl = new AbortController()
  let isFirst = true
  const projectId = activeProjectId.value
  const convId = aiConversationId.value

  try {
    await sendProjectAiChatStream(
      projectId,
      convId,
      content,
      streamAbortCtrl,
      async (res: any) => {
        const { code, data, msg } = JSON.parse(res.data)
        if (code !== 0) {
          ElMessage.error(`AI 对话异常：${msg}`)
          streamingLoading.value = false
          return
        }
        if (data?.receive?.content === '' && !data?.receive?.reasoningContent) return
        if (isFirst) {
          isFirst = false
          // 用真实 send 消息替换假消息
          aiMessages.value.pop()
          aiMessages.value.push(data.send)
        }
        if (data?.receive?.content) {
          streamingText.value += data.receive.content
        }
        await scrollAiToBottom()
      },
      (_err: any) => {
        streamingLoading.value = false
        throw _err
      },
      async () => {
        // 流式结束，把完整回复追加为消息记录
        if (streamingText.value) {
          aiMessages.value.push({
            id: Date.now() + 1,
            type: 'assistant',
            content: streamingText.value,
            createTime: new Date()
          } as any)
          streamingText.value = ''
        }
        streamingLoading.value = false
        await scrollAiToBottom()
      }
    )
  } catch {
    streamingLoading.value = false
  }
}

const stopStream = () => {
  if (streamAbortCtrl) { streamAbortCtrl.abort(); streamAbortCtrl = null }
  if (streamingText.value) {
    aiMessages.value.push({
      id: Date.now() + 1,
      type: 'assistant',
      content: streamingText.value,
      createTime: new Date()
    } as any)
    streamingText.value = ''
  }
  streamingLoading.value = false
}

const handleClearMessages = async () => {
  await ElMessageBox.confirm('确认清空当前项目的全部问答记录？摘要和知识库不受影响。', '提示', { type: 'warning' })
  if (!activeProjectId.value) return
  stopStream()
  await clearProjectAiMessages(activeProjectId.value)
  aiMessages.value = []
  ElMessage.success('问答记录已清空')
}

const scrollAiToBottom = async () => {
  await nextTick()
  if (aiMessagesRef.value) aiMessagesRef.value.scrollTop = aiMessagesRef.value.scrollHeight
}

// ── 工具 ──────────────────────────────────────────────────────
const formatFileSize = (bytes: number): string => {
  if (!bytes) return '-'
  if (bytes < 1024) return `${bytes} B`
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
}

const isOfficeFile = (fileType?: string) => ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes((fileType || '').toLowerCase())

const clearOfficeWarmupTimers = () => {
  officeWarmupTimers.forEach((timer) => clearTimeout(timer))
  officeWarmupTimers = []
}

const clearPreviewFrameTimeout = () => {
  if (previewFrameTimeoutTimer) {
    clearTimeout(previewFrameTimeoutTimer)
    previewFrameTimeoutTimer = null
  }
}

const startPreviewFrameTimeout = (requestToken: number) => {
  clearPreviewFrameTimeout()
  previewFrameTimeoutTimer = setTimeout(() => {
    if (requestToken !== previewRequestToken.value) return
    if (!previewFrameLoading.value) return
    previewFrameLoading.value = false
    previewError.value = '预览服务响应超时，请稍后重试；若持续失败请检查 kkFileView 服务状态'
  }, 30000)
}

const scheduleOfficeWarmup = (files: ReviewMeetingFileRespVO[]) => {
  clearOfficeWarmupTimers()
  files
    .filter((file) => isOfficeFile(file.fileType))
    .slice(0, 3)
    .forEach((file, index) => {
      const timer = setTimeout(() => {
        getFileOpenUrl(file.id).catch(() => undefined)
      }, index * 3000)
      officeWarmupTimers.push(timer)
    })
}

const markdown = new MarkdownIt({
  html: false,
  linkify: true,
  breaks: true
})

const escapeHtml = (text: string) =>
  text
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')

const renderUserMsgContent = (text: string) =>
  escapeHtml(text || '').replace(/\n/g, '<br/>')

const renderAiMsgContent = (text: string) =>
  markdown.render(text || '')

loadCatalog()
</script>

<style scoped>
/* ── 整体布局 ── */
.tablet-full-page {
  height: 100vh;
  min-height: 100vh;
  padding: 12px;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: 12px;
  background: #eef2f7;
}

.page-toolbar {
  background: #fff;
  border-radius: 12px;
  padding: 12px 16px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  border: 1px solid #e5e7eb;
}

.title-wrap h2 { margin: 0; font-size: 22px; font-weight: 700; }
.title-wrap span { display: block; margin-top: 4px; font-size: 13px; color: #6b7280; }
.tool-wrap { display: flex; align-items: center; gap: 8px; }
.search-input { width: 320px; }

.page-body {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: 360px minmax(0, 1fr);
  gap: 12px;
  transition: grid-template-columns 0.25s ease;
}

.page-body.ai-open {
  grid-template-columns: 280px minmax(0, 1fr) 360px;
}

/* ── 目录面板 ── */
.directory-panel,
.preview-panel,
.ai-panel {
  border: 1px solid #e5e7eb;
  border-radius: 12px;
  background: #fff;
}

.directory-panel {
  display: flex;
  flex-direction: column;
  min-height: 0;
  padding: 10px;
}

.panel-title { padding: 6px 8px 10px; font-size: 15px; font-weight: 600; }

.catalog-tree { flex: 1; min-height: 0; overflow: auto; }

.tree-project, .tree-file {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.node-left { display: inline-flex; align-items: center; min-width: 0; gap: 6px; }
.node-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.node-icon { font-size: 16px; }
.folder-icon { color: #2563eb; }
.file-icon { color: #475569; }
.file-type { font-size: 11px; color: #64748b; }

/* ── 预览面板 ── */
.preview-panel { display: flex; flex-direction: column; min-width: 0; min-height: 0; }

.preview-header {
  padding: 12px 14px;
  border-bottom: 1px solid #e5e7eb;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.preview-meta { min-width: 0; }
.preview-title { font-size: 16px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.preview-sub { margin-top: 4px; color: #64748b; font-size: 12px; }
.preview-actions { display: flex; gap: 8px; flex-shrink: 0; }

.preview-body { flex: 1; min-height: 0; background: #f8fafc; position: relative; }
.welcome-panel {
  height: 100%;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  color: #1f2937; text-align: center; padding: 24px;
}
.welcome-title { font-size: 30px; font-weight: 700; letter-spacing: 1px; }
.welcome-sub { margin-top: 12px; font-size: 15px; color: #64748b; }
.preview-iframe { width: 100%; height: 100%; border: 0; display: block; background: #fff; }
.preview-loading-mask {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  background: rgba(248, 250, 252, 0.92);
  z-index: 2;
  pointer-events: none;
}

/* ── AI 面板 ── */
.ai-panel {
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
}

/* AI 头部 */
.ai-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 12px;
  border-bottom: 1px solid #e5e7eb;
  flex-shrink: 0;
}
.ai-header-left { display: flex; align-items: center; gap: 8px; }
.ai-header-right { display: flex; align-items: center; gap: 4px; }
.ai-title { font-size: 15px; font-weight: 600; }
.ai-status-badge {
  font-size: 11px;
  padding: 2px 8px;
  border-radius: 10px;
  font-weight: 500;
}
.status-0 { background: #f3f4f6; color: #6b7280; }
.status-1 { background: #fef3c7; color: #d97706; }
.status-2 { background: #d1fae5; color: #059669; }
.status-3 { background: #fee2e2; color: #dc2626; }

/* 摘要区 */
.ai-summary-wrap {
  flex-shrink: 0;
  max-height: 280px;
  overflow-y: auto;
  padding: 10px 12px;
  background: #f8fafc;
  border-bottom: 1px solid #e5e7eb;
}

.ai-summary-empty,
.ai-summary-building,
.ai-summary-failed {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: #6b7280;
  padding: 8px 0;
}
.ai-summary-failed { color: #dc2626; }

.ai-summary-card { display: flex; flex-direction: column; gap: 8px; }
.summary-updated { font-size: 11px; color: #9ca3af; }
.summary-field { display: flex; flex-direction: column; gap: 2px; }
.field-label {
  font-size: 11px;
  font-weight: 600;
  color: #2563eb;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}
.field-value { font-size: 13px; color: #374151; line-height: 1.5; }

/* 分隔线 */
.ai-divider { height: 1px; background: #e5e7eb; flex-shrink: 0; }

/* 消息列表 */
.ai-messages {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 10px 12px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.ai-messages-loading {
  display: flex; align-items: center; gap: 6px;
  font-size: 13px; color: #9ca3af; padding: 8px 0;
}

.chat-msg { display: flex; }
.user-msg { justify-content: flex-end; }
.ai-msg { justify-content: flex-start; }

.msg-bubble {
  max-width: 85%;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.user-msg .msg-bubble { align-items: flex-end; }
.ai-msg .msg-bubble { align-items: flex-start; }

.msg-role { font-size: 11px; color: #9ca3af; }

.msg-content {
  padding: 8px 12px;
  border-radius: 10px;
  font-size: 13px;
  line-height: 1.55;
  word-break: break-word;
}
.user-msg .msg-content { background: #2563eb; color: #fff; border-bottom-right-radius: 3px; }
.ai-msg .msg-content { background: #f3f4f6; color: #1f2937; border-bottom-left-radius: 3px; overflow-x: auto; }
.streaming-dot { color: #9ca3af; }

.msg-content :deep(.md-content > *:first-child) { margin-top: 0; }
.msg-content :deep(.md-content > *:last-child) { margin-bottom: 0; }
.msg-content :deep(p) { margin: 0 0 6px; }
.msg-content :deep(ul),
.msg-content :deep(ol) { margin: 0 0 6px 18px; padding: 0; }
.msg-content :deep(li) { margin: 0 0 4px; }
.msg-content :deep(code) {
  padding: 1px 4px;
  border-radius: 4px;
  font-size: 12px;
  background: rgba(15, 23, 42, 0.08);
}
.msg-content :deep(pre) {
  margin: 6px 0;
  padding: 8px;
  border-radius: 8px;
  overflow-x: auto;
  background: rgba(15, 23, 42, 0.06);
}
.msg-content :deep(pre code) {
  padding: 0;
  border-radius: 0;
  background: transparent;
}
.msg-content :deep(table) {
  width: 100%;
  min-width: 560px;
  border-collapse: collapse;
  margin: 6px 0;
  background: #fff;
}
.msg-content :deep(th),
.msg-content :deep(td) {
  padding: 6px 8px;
  text-align: left;
  border: 1px solid #d1d5db;
  vertical-align: top;
}
.msg-content :deep(th) { background: #f8fafc; font-weight: 600; }
.user-msg .msg-content :deep(code) { background: rgba(255, 255, 255, 0.18); }
.user-msg .msg-content :deep(a) { color: #dbeafe; }
.ai-msg .msg-content :deep(a) { color: #1d4ed8; }

/* 快捷问题 */
.quick-questions {
  flex-shrink: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 8px 12px;
  border-top: 1px solid #e5e7eb;
}
.quick-btn {
  padding: 4px 10px;
  font-size: 12px;
  background: #eff6ff;
  border: 1px solid #bfdbfe;
  border-radius: 12px;
  color: #1d4ed8;
  cursor: pointer;
  transition: all 0.15s;
  white-space: nowrap;
}
.quick-btn:hover:not(:disabled) { background: #dbeafe; }
.quick-btn:disabled { opacity: 0.5; cursor: not-allowed; }

/* 输入区 */
.ai-input-wrap {
  flex-shrink: 0;
  padding: 8px 12px;
  border-top: 1px solid #e5e7eb;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.ai-input-actions { display: flex; justify-content: flex-end; }

/* 不可用提示 */
.ai-chat-unavail {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  color: #9ca3af;
}

/* ── 树样式 ── */
:deep(.el-tree-node__content) { height: 42px; padding-right: 8px; }
:deep(.el-tree-node__content:hover) { background: #eff6ff; }

/* ── 响应式 ── */
@media (max-width: 1100px) {
  .tablet-full-page { height: auto; min-height: 100vh; }
  .page-toolbar { flex-direction: column; align-items: stretch; }
  .search-input { width: 100%; }
  .page-body { grid-template-columns: 1fr !important; }
  .directory-panel { max-height: 42vh; }
  .preview-panel { min-height: 56vh; }
  .ai-panel { min-height: 50vh; }
}
</style>
