yuan
2 天以前 eecd51db2f0e1094650206fa534dd02f5de58c9c
src/components/AIChatSidebar/index.vue
@@ -1,7 +1,7 @@
<template>
  <div class="ai-chat-sidebar-wrapper">
    <!-- 悬浮图标 -->
    <div class="ai-chat-trigger" @click="toggleSidebar" v-show="!visible">
    <div v-if="!hideTrigger" class="ai-chat-trigger" @click="toggleSidebar" v-show="!visible">
      <el-tooltip :content="currentAssistant.tooltip" placement="left">
        <div class="trigger-icon">
          <el-icon :size="30" color="#fff"><component :is="currentAssistant.icon" /></el-icon>
@@ -12,14 +12,16 @@
    <!-- 侧边栏对话框 -->
    <el-drawer
        v-model="visible"
        :size="drawerSize"
        direction="rtl"
        :size="computedDrawerSize"
        :direction="drawerDirection"
        :with-header="true"
        class="ai-chat-drawer"
        :modal="false"
        modal-class="ai-chat-overlay"
        :show-close="false"
        :append-to-body="false"
        :close-on-press-escape="!hideTrigger"
        :close-on-click-modal="!hideTrigger"
        @close="handleClose"
    >
      <template #header>
@@ -50,8 +52,16 @@
                <el-icon :size="18"><Plus /></el-icon>
              </el-button>
            </el-tooltip>
            <div class="action-divider"></div>
            <el-tooltip content="关闭" placement="bottom">
            <el-button
                v-if="headerExtraActionText"
                link
                class="header-action-btn header-action-btn--text"
                @click="handleHeaderExtraAction"
            >
              {{ headerExtraActionText }}
            </el-button>
            <div v-if="!hideTrigger" class="action-divider"></div>
            <el-tooltip v-if="!hideTrigger" content="关闭" placement="bottom">
              <el-button link class="header-action-btn close-btn" @click="handleManualClose">
                <el-icon :size="18"><Close /></el-icon>
              </el-button>
@@ -105,23 +115,17 @@
              <div class="assistant-scan-ring"></div>
              <div class="assistant-orbit assistant-orbit-a"></div>
              <div class="assistant-orbit assistant-orbit-b"></div>
              <div class="assistant-bot">
                <div class="assistant-bot-antenna assistant-bot-antenna-left"></div>
                <div class="assistant-bot-antenna assistant-bot-antenna-right"></div>
                <div class="assistant-bot-head">
                  <div class="assistant-bot-head-glow"></div>
                  <div class="assistant-bot-eye assistant-bot-eye-left"></div>
                  <div class="assistant-bot-eye assistant-bot-eye-right"></div>
                  <div class="assistant-bot-mouth"></div>
                </div>
                <div class="assistant-bot-neck"></div>
                <div class="assistant-bot-body">
                  <div class="assistant-bot-core">
                    <div class="assistant-bot-core-ring"></div>
                    <el-icon :size="22"><component :is="currentAssistant.icon" /></el-icon>
              <div class="assistant-model-shell">
                <div class="assistant-model-cut">
                  <img
                      v-if="currentAssistantAvatar"
                      class="assistant-model-img"
                      :src="currentAssistantAvatar"
                      :alt="currentAssistant.label"
                  />
                  <div v-else class="assistant-model-fallback">
                    <el-icon :size="30"><component :is="currentAssistant.icon" /></el-icon>
                  </div>
                  <div class="assistant-bot-arm assistant-bot-arm-left"></div>
                  <div class="assistant-bot-arm assistant-bot-arm-right"></div>
                </div>
              </div>
              <div class="assistant-status">
@@ -169,10 +173,6 @@
            </div>
          </div>
          <div v-show="!hasMessages" class="hero-dot-grid" aria-hidden="true">
            <span v-for="dot in 28" :key="dot"></span>
          </div>
          <div class="message-list" ref="messageListRef">
            <div
                v-for="(message, index) in messages"
@@ -191,7 +191,8 @@
                  <div
                      v-for="(file, fileIndex) in message.localUploadFiles"
                      :key="`${file.previewId || file.name}-${fileIndex}`"
                      class="message-local-file-item"
                      :class="['message-local-file-item', { clickable: !!file.accessUrl && !file.isImage }]"
                      @click="handleMessageFileClick(file)"
                  >
                    <el-image
                        v-if="file.isImage && file.previewUrl"
@@ -205,8 +206,14 @@
                    />
                    <el-icon v-else class="message-local-file-icon"><Document /></el-icon>
                    <div class="message-local-file-meta">
                      <span class="message-local-file-name">{{ file.name }}</span>
                      <small class="message-local-file-size">{{ formatFileSize(file.size) }}</small>
                      <span
                          :class="['message-local-file-name', { clickable: !!file.accessUrl }]"
                          :title="file.name"
                          @click.stop="openMessageAttachment(file)"
                      >
                        {{ file.name }}
                      </span>
                      <small v-if="Number(file.size) > 0" class="message-local-file-size">{{ formatFileSize(file.size) }}</small>
                    </div>
                  </div>
                </div>
@@ -235,7 +242,123 @@
                  </el-table>
                </div>
                <!-- 打字中动画 -->
                <div v-if="message.manufacturingData" class="manufacturing-card">
                  <div class="manufacturing-card__title">{{ getManufacturingTypeLabel(message.type) }}</div>
                  <div
                      v-if="message.manufacturingData.summaryEntries?.length || message.manufacturingData.coreMetrics?.length"
                      class="manufacturing-summary-grid"
                  >
                    <div
                        v-for="(entry, entryIndex) in message.manufacturingData.summaryEntries"
                        :key="`summary-${entry.key}-${entryIndex}`"
                        class="manufacturing-summary-item"
                    >
                      <span class="manufacturing-summary-label">{{ entry.label }}</span>
                      <strong class="manufacturing-summary-value">{{ entry.value }}</strong>
                    </div>
                    <div
                        v-for="(metric, metricIndex) in message.manufacturingData.coreMetrics"
                        :key="`core-${metric.key}-${metricIndex}`"
                        class="manufacturing-summary-item manufacturing-summary-item--core"
                    >
                      <span class="manufacturing-summary-label">{{ metric.label }}</span>
                      <strong class="manufacturing-summary-value">{{ metric.value }}</strong>
                    </div>
                  </div>
                  <div v-if="message.manufacturingData.warningItems?.length" class="manufacturing-warning-list">
                    <div
                        v-for="(warning, warningIndex) in message.manufacturingData.warningItems"
                        :key="`warning-${warning.title || warningIndex}`"
                        class="manufacturing-warning-item"
                    >
                      <div class="manufacturing-warning-item__head">
                        <el-tag size="small" :type="getManufacturingWarningLevelType(warning.level)">
                          {{ getManufacturingWarningLevelLabel(warning.level) }}
                        </el-tag>
                        <strong>{{ warning.title || `预警 ${warningIndex + 1}` }}</strong>
                        <span v-if="warning.count !== '' && warning.count !== null && warning.count !== undefined" class="manufacturing-warning-count">
                          {{ warning.count }}
                        </span>
                      </div>
                      <p v-if="warning.detail" class="manufacturing-warning-detail">{{ warning.detail }}</p>
                    </div>
                  </div>
                  <div
                      v-if="message.manufacturingData.listItems?.length && message.manufacturingData.columns?.length"
                      class="table-wrapper manufacturing-table-wrapper"
                  >
                    <el-table :data="message.manufacturingData.listItems" border stripe size="small" style="width: 100%">
                      <el-table-column
                          v-for="col in message.manufacturingData.columns"
                          :key="col"
                          :label="getStructuredFieldLabel(col)"
                          min-width="140"
                          show-overflow-tooltip
                      >
                        <template #default="{ row }">
                          {{ formatStructuredValue(row[col]) }}
                        </template>
                      </el-table-column>
                    </el-table>
                  </div>
                  <div v-if="message.manufacturingData.actionCards?.length" class="manufacturing-action-list">
                    <div
                        v-for="(card, cardIndex) in message.manufacturingData.actionCards"
                        :key="card.runtimeKey || `${card.code}-${card.targetApi}-${cardIndex}`"
                        class="manufacturing-action-card"
                    >
                      <div class="manufacturing-action-card__head">
                        <strong>{{ card.name || `动作 ${cardIndex + 1}` }}</strong>
                        <el-tag size="small" type="info">{{ getNormalizedRequestMethod(card.method) }}</el-tag>
                      </div>
                      <div class="manufacturing-action-card__meta">
                        <span>{{ card.code || '--' }}</span>
                        <span>{{ card.targetApi || '--' }}</span>
                      </div>
                      <p v-if="card.description" class="manufacturing-action-card__desc">{{ card.description }}</p>
                      <div v-if="card.requiredFields?.length" class="manufacturing-required-fields">
                        <span>必填字段</span>
                        <el-tag
                            v-for="field in card.requiredFields"
                            :key="field"
                            size="small"
                            type="warning"
                        >
                          {{ getStructuredPathLabel(field) }}
                        </el-tag>
                      </div>
                      <el-input
                          v-model="card.payloadText"
                          type="textarea"
                          :rows="6"
                          resize="vertical"
                          :disabled="card.executing"
                          placeholder="请输入 JSON 请求参数"
                      />
                      <div class="manufacturing-action-footer">
                        <span
                            v-if="card.executeResult"
                            :class="['manufacturing-action-result', card.executeError ? 'error' : 'success']"
                        >
                          {{ card.executeResult }}
                        </span>
                        <el-button
                            type="primary"
                            size="small"
                            :loading="card.executing"
                            @click="executeManufacturingAction(message, card, cardIndex)"
                        >
                          确认并执行
                        </el-button>
                      </div>
                    </div>
                  </div>
                </div>
                <div v-if="message.purchaseAnalysisData" class="purchase-confirm-card">
                  <div class="purchase-confirm-header">
                    <span>{{ businessTypeLabelMap[message.purchaseAnalysisData.businessType] || message.purchaseAnalysisData.businessType || '采购业务' }}</span>
@@ -515,8 +638,17 @@
import { ref, onMounted, onUnmounted, nextTick, watch, computed } from 'vue'
import request from '@/utils/request'
import * as echarts from 'echarts'
import { Cpu, User, Plus, Timer, Delete, ChatDotSquare, VideoPause, Upload, Document, Close, ShoppingCart, Promotion, RefreshRight } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { Cpu, User, Plus, Timer, Delete, ChatDotSquare, VideoPause, Upload, Document, Close, Promotion, RefreshRight } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { builtInAssistants, generalAssistant } from './assistants'
import todoAssistantAvatar from '@/assets/AI/待办助手.png'
import salesAssistantAvatar from '@/assets/AI/销售助手.png'
import purchaseAssistantAvatar from '@/assets/AI/采购助手.png'
import productionAssistantAvatar from '@/assets/AI/生产助手.png'
import financeAssistantAvatar from '@/assets/AI/财务助手.png'
import bossAssistantAvatar from '@/assets/AI/待办助手.png'
const emit = defineEmits(['header-extra-action'])
const props = defineProps({
  assistants: {
@@ -526,72 +658,51 @@
  defaultAssistant: {
    type: String,
    default: ''
  },
  hideTrigger: {
    type: Boolean,
    default: false
  },
  autoOpen: {
    type: Boolean,
    default: false
  },
  drawerSize: {
    type: [String, Number],
    default: ''
  },
  drawerDirection: {
    type: String,
    default: 'rtl'
  },
  headerExtraActionText: {
    type: String,
    default: ''
  }
})
const builtInAssistants = [
  {
    key: 'general',
    label: '待办助理',
    title: '待办智能助理',
    tooltip: '待办助手',
    icon: Cpu,
    apiBase: '/xiaozhi',
    storageKey: 'ai_chat_uuid',
    placeholder: '请输入您的问题... (Enter 发送, Shift+Enter 换行)',
    welcomeMessage: '你好',
    description: '我可以回答你的问题,为你提供业务数据解读信息、处理建议和辅助决策支持。',
    allowFileUpload: true,
    emptySessionText: '暂无历史会话'
  },
  {
    key: 'purchase',
    label: '采购助理',
    title: '采购智能助理',
    tooltip: '采购智能助理',
    icon: ShoppingCart,
    apiBase: '/purchase-ai',
    storageKey: 'purchase_ai_chat_uuid',
    placeholder: '请输入采购问题... (Enter 发送, Shift+Enter 换行)',
    welcomeMessage: '你好',
    description: '我可以协助你分析采购订单、到货进度、供应商表现和付款情况,帮助你快速定位采购异常。',
    allowFileUpload: true,
    allowMultipleFileUpload: true,
    fileAnalyzeUrl: '/purchase-ai/analyze-files',
    emptySessionText: '暂无采购会话'
  }
]
const hideTrigger = computed(() => props.hideTrigger)
const headerExtraActionText = computed(() => String(props.headerExtraActionText || '').trim())
const drawerDirection = computed(() => (props.drawerDirection === 'ttb' || props.drawerDirection === 'btt' || props.drawerDirection === 'ltr' || props.drawerDirection === 'rtl')
  ? props.drawerDirection
  : 'rtl')
const assistants = computed(() => props.assistants?.length ? props.assistants : builtInAssistants)
const selectedAssistantKey = ref(props.defaultAssistant || assistants.value[0]?.key || 'general')
const currentAssistant = computed(() => assistants.value.find(item => item.key === selectedAssistantKey.value) || assistants.value[0] || builtInAssistants[0])
const showAssistantSwitch = computed(() => assistants.value.length > 1)
const assistantQuickPromptMap = {
  general: [
    '我当前有哪些审批待办需要处理?',
    '帮我列出今天新增的审批待办。',
    '当前待我审批的单据,按时间倒序列出来。',
    '我发起的审批里,哪些还在处理中?',
    '查询流程编号 XXX 的审批详情。',
    '流程编号 XXX 现在卡在哪个审批节点?当前审批人是谁?',
    '帮我查看流程编号 XXX 的审批流转记录。',
    '近7天我的审批待办统计情况怎么样?',
    '本月我的审批中,通过、驳回、处理中各有多少?',
    '近30天各类型审批数量分布是什么?',
    '帮我审批通过流程编号 XXX,备注“同意”。',
    '帮我驳回流程编号 XXX,备注“请补充说明”。',
    '撤销我刚刚对流程编号 XXX 的审批操作。',
    '帮我修改流程编号 XXX 的备注为“已补充附件”。',
    '删除我发起的流程编号 XXX。'
  ],
  purchase: [
    '本月采购金额排名前十的物料有哪些?',
    '哪些采购订单还未入库?',
    '最近7天供应商到货异常有哪些?',
    '帮我统计待付款采购单',
    '列出本月采购退货情况'
  ]
const assistantAvatarByKey = {
  general: todoAssistantAvatar,
  todo: todoAssistantAvatar,
  purchase: purchaseAssistantAvatar,
  sales: salesAssistantAvatar,
  production: productionAssistantAvatar,
  finance: financeAssistantAvatar,
  boss: bossAssistantAvatar
}
const currentAssistantAvatar = computed(() => {
  const assistant = currentAssistant.value || {}
  return assistant.avatar || assistantAvatarByKey[assistant.key] || ''
})
const showAssistantSwitch = computed(() => assistants.value.length > 1)
const quickPromptLimit = 3
const quickPromptStart = ref(0)
const quickPrompts = computed(() => {
@@ -599,7 +710,7 @@
  if (Array.isArray(assistant.quickPrompts) && assistant.quickPrompts.length) {
    return assistant.quickPrompts
  }
  return assistantQuickPromptMap[assistant.key] || assistantQuickPromptMap.general
  return generalAssistant.quickPrompts || []
})
const displayedQuickPrompts = computed(() => {
  const prompts = quickPrompts.value || []
@@ -615,11 +726,12 @@
const visible = ref(false)
const windowWidth = ref(window.innerWidth)
const drawerSize = computed(() => {
const responsiveDrawerSize = computed(() => {
  if (windowWidth.value < 768) return '100%'
  if (windowWidth.value < 1200) return '50%'
  return '50%'
})
const computedDrawerSize = computed(() => props.drawerSize || responsiveDrawerSize.value)
const messageListRef = ref(null)
const isSending = ref(false)
const currentAbortController = ref(null)
@@ -637,6 +749,75 @@
  payment_registration: '付款登记',
  purchase_return_order: '采购退货单',
  unknown: '未知采购业务'
}
const manufacturingStructuredTypeSet = new Set([
  'manufacturing_site_snapshot',
  'manufacturing_plan_list',
  'manufacturing_workorder_list',
  'manufacturing_device_list',
  'manufacturing_device_repair_list',
  'manufacturing_quality_list',
  'manufacturing_material_list',
  'manufacturing_exception_list',
  'manufacturing_warning',
  'manufacturing_analysis',
  'manufacturing_action_plan'
])
const manufacturingListTypeSet = new Set([
  'manufacturing_plan_list',
  'manufacturing_workorder_list',
  'manufacturing_device_list',
  'manufacturing_device_repair_list',
  'manufacturing_quality_list',
  'manufacturing_material_list',
  'manufacturing_exception_list'
])
const manufacturingTypeLabelMap = {
  manufacturing_site_snapshot: '生产现场概览',
  manufacturing_plan_list: '计划查询',
  manufacturing_workorder_list: '工单查询',
  manufacturing_device_list: '设备查询',
  manufacturing_device_repair_list: '设备维修记录查询',
  manufacturing_quality_list: '质量查询',
  manufacturing_material_list: '物料查询',
  manufacturing_exception_list: '异常查询',
  manufacturing_warning: '预警看板',
  manufacturing_analysis: '经营分析',
  manufacturing_action_plan: '办理建议'
}
const structuredFieldLabelMap = {
  workOrderNo: '工单号',
  planEndTime: '计划结束时间',
  planStartTime: '计划开始时间',
  timeRange: '时间范围',
  startDate: '开始日期',
  endDate: '结束日期',
  warningCount: '预警数量',
  overduePlanCount: '逾期计划数',
  overdueWorkOrderCount: '逾期工单数',
  actionCount: '建议动作数',
  qualityOpenCount: '质量待处理数',
  lowStockCount: '低库存数',
  exceptionCount: '异常数',
  userId: '用户ID',
  tenantId: '租户ID',
  status: '状态',
  deviceName: '设备名称',
  deviceModel: '设备型号',
  pendingRepairCount: '待维修数',
  repairTime: '维修时间',
  repairName: '报修人',
  maintenanceName: '维修人员',
  level: '预警等级',
  title: '标题',
  count: '数量',
  detail: '详情',
  remark: '备注',
  createTime: '创建时间',
  updateTime: '更新时间',
  exceptionType: '异常类型',
  materialName: '物料名称',
  stockQty: '库存量'
}
const purchasePayloadFieldLabelMap = {
  purchaseLedgers: '采购台账',
@@ -790,6 +971,366 @@
  isInspected: 'isInspected',
  isChecked: 'isInspected'
}
const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
const stringifyStructuredPayload = (value, spaces = 2) => {
  if (typeof value === 'string') return value
  try {
    return JSON.stringify(value ?? {}, null, spaces)
  } catch (err) {
    return '{}'
  }
}
const structuredFieldTokenLabelMap = {
  time: '时间',
  range: '范围',
  start: '开始',
  end: '结束',
  date: '日期',
  warning: '预警',
  overdue: '逾期',
  plan: '计划',
  work: '工',
  order: '单',
  workorder: '工单',
  count: '数量',
  quality: '质量',
  low: '低',
  stock: '库存',
  exception: '异常',
  action: '动作',
  user: '用户',
  tenant: '租户',
  id: 'ID',
  no: '编号',
  number: '编号',
  code: '编码',
  name: '名称',
  status: '状态',
  level: '等级',
  title: '标题',
  detail: '详情',
  total: '总数',
  rate: '比率',
  type: '类型',
  pending: '待',
  repair: '维修',
  device: '设备',
  material: '物料'
}
const convertStructuredFieldKeyToChinese = (fieldKey = '') => {
  const key = String(fieldKey || '').trim()
  if (!key) return '-'
  if (structuredFieldLabelMap[key]) return structuredFieldLabelMap[key]
  if (/[\u4e00-\u9fa5]/.test(key)) return key
  const rawTokens = key
    .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
    .replace(/[_-]+/g, ' ')
    .trim()
    .split(/\s+/)
    .filter(Boolean)
    .map(token => token.toLowerCase())
  if (!rawTokens.length) return '字段'
  const mappedTokens = rawTokens
    .map(token => structuredFieldTokenLabelMap[token] || '')
    .filter(Boolean)
  if (mappedTokens.length) {
    return mappedTokens.join('')
  }
  return '字段'
}
const getStructuredFieldLabel = (fieldKey = '') => {
  return convertStructuredFieldKeyToChinese(fieldKey)
}
const getStructuredPathLabel = (fieldPath = '') => {
  const path = String(fieldPath || '').trim()
  if (!path) return '-'
  const segments = path
    .replace(/\[(\d+)]/g, '.$1')
    .split('.')
    .filter(Boolean)
  if (!segments.length) return getStructuredFieldLabel(path)
  return segments.map((segment) => {
    if (/^\d+$/.test(segment)) {
      return `第${Number(segment) + 1}项`
    }
    return getStructuredFieldLabel(segment)
  }).join(' / ')
}
const formatStructuredValue = (value) => {
  if (value === null || value === undefined || value === '') return '-'
  if (Array.isArray(value)) {
    const preview = value.slice(0, 3).map(item => formatStructuredValue(item)).join('、')
    return value.length > 3 ? `${preview} 等${value.length}项` : preview
  }
  if (isPlainObject(value)) return stringifyStructuredPayload(value, 0)
  return String(value)
}
const normalizeManufacturingSummaryEntries = (summary) => {
  if (!isPlainObject(summary)) return []
  return Object.entries(summary)
    .filter(([, value]) => value !== undefined && value !== null && `${value}`.trim() !== '')
    .map(([key, value]) => ({
      key,
      label: getStructuredFieldLabel(key),
      value: formatStructuredValue(value)
    }))
}
const normalizeManufacturingCoreMetrics = (coreMetrics) => {
  if (Array.isArray(coreMetrics)) {
    return coreMetrics.map((item, index) => {
      if (isPlainObject(item)) {
        const label = item.label || item.name || item.key || `指标${index + 1}`
        const metricValue = item.value ?? item.metricValue ?? item.data ?? '-'
        const unit = item.unit ? ` ${item.unit}` : ''
        return {
          key: String(item.key || item.name || index),
          label,
          value: `${formatStructuredValue(metricValue)}${unit}`.trim()
        }
      }
      return {
        key: String(index),
        label: `指标${index + 1}`,
        value: formatStructuredValue(item)
      }
    })
  }
  if (isPlainObject(coreMetrics)) {
    return Object.entries(coreMetrics).map(([key, value]) => ({
      key,
      label: getStructuredFieldLabel(key),
      value: formatStructuredValue(value)
    }))
  }
  return []
}
const normalizeManufacturingWarningItems = (items = []) => {
  if (!Array.isArray(items)) return []
  return items
    .filter(item => isPlainObject(item))
    .map(item => ({
      level: String(item.level || '').toLowerCase(),
      title: item.title || '',
      count: item.count ?? '',
      detail: item.detail ?? ''
    }))
}
const inferManufacturingColumns = (items = []) => {
  if (!Array.isArray(items) || !items.length) return []
  const fieldSet = new Set()
  items.forEach((item) => {
    if (!isPlainObject(item)) return
    Object.keys(item).forEach((key) => fieldSet.add(key))
  })
  return Array.from(fieldSet)
}
const getManufacturingActionCardRuntimeKey = (card = {}, index = 0) => {
  const code = String(card?.code || '').trim()
  const api = String(card?.targetApi || '').trim()
  const name = String(card?.name || '').trim()
  return `${code}::${api}::${name}::${index}`
}
const normalizeManufacturingActionCards = (actionCards = [], previousCards = []) => {
  const previousMap = new Map()
  if (Array.isArray(previousCards)) {
    previousCards.forEach((card, index) => {
      previousMap.set(getManufacturingActionCardRuntimeKey(card, index), card)
    })
  }
  return (Array.isArray(actionCards) ? actionCards : [])
    .filter(card => isPlainObject(card))
    .map((card, index) => {
      const runtimeKey = getManufacturingActionCardRuntimeKey(card, index)
      const previousCard = previousMap.get(runtimeKey)
      const fallbackPayloadText = stringifyStructuredPayload(card.examplePayload, 2)
      return {
        ...card,
        runtimeKey,
        payloadText: previousCard?.payloadText ?? fallbackPayloadText,
        executing: Boolean(previousCard?.executing),
        executed: Boolean(previousCard?.executed),
        executeResult: previousCard?.executeResult || '',
        executeError: Boolean(previousCard?.executeError)
      }
    })
}
const buildManufacturingStructuredData = (parsedData, previousData = null) => {
  const type = String(parsedData?.type || '')
  if (!manufacturingStructuredTypeSet.has(type)) return null
  const rawData = isPlainObject(parsedData?.data) ? parsedData.data : {}
  const items = Array.isArray(rawData.items) ? rawData.items.filter(item => isPlainObject(item)) : []
  const warningItems = type === 'manufacturing_warning' ? normalizeManufacturingWarningItems(items) : []
  const listItems = manufacturingListTypeSet.has(type) ? items : []
  const actionCards = type === 'manufacturing_action_plan'
    ? normalizeManufacturingActionCards(rawData.actionCards, previousData?.actionCards)
    : []
  return {
    type,
    summaryEntries: normalizeManufacturingSummaryEntries(parsedData?.summary),
    coreMetrics: normalizeManufacturingCoreMetrics(rawData.coreMetrics),
    listItems,
    columns: inferManufacturingColumns(listItems),
    warningItems,
    actionCards
  }
}
const getManufacturingTypeLabel = (type = '') => manufacturingTypeLabelMap[String(type || '')] || '制造结果'
const getManufacturingWarningLevelType = (level = '') => {
  const normalizedLevel = String(level || '').toLowerCase()
  if (normalizedLevel === 'high') return 'danger'
  if (normalizedLevel === 'medium') return 'warning'
  return 'info'
}
const getManufacturingWarningLevelLabel = (level = '') => {
  const normalizedLevel = String(level || '').toLowerCase()
  if (normalizedLevel === 'high') return '高'
  if (normalizedLevel === 'medium') return '中'
  return normalizedLevel ? normalizedLevel.toUpperCase() : '一般'
}
const normalizeRequestMethod = (method = 'POST') => {
  const normalized = String(method || 'POST').trim().toUpperCase()
  if (['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(normalized)) return normalized
  return 'POST'
}
const getNormalizedRequestMethod = (method) => normalizeRequestMethod(method)
const getPayloadValueByPath = (payload, fieldPath = '') => {
  const normalizedPath = String(fieldPath || '').trim()
  if (!normalizedPath || !isPlainObject(payload)) return undefined
  const pathSegments = normalizedPath
    .replace(/\[(\d+)]/g, '.$1')
    .split('.')
    .filter(Boolean)
  return pathSegments.reduce((current, segment) => {
    if (current === null || current === undefined) return undefined
    if (!['object', 'function'].includes(typeof current)) return undefined
    return current[segment]
  }, payload)
}
const getMissingRequiredFields = (requiredFields = [], payload = {}) => {
  if (!Array.isArray(requiredFields) || !requiredFields.length) return []
  return requiredFields.filter((fieldPath) => {
    const value = getPayloadValueByPath(payload, fieldPath)
    return !hasMeaningfulPayloadValue(value)
  })
}
const parseManufacturingActionPayload = (payloadText = '') => {
  const text = String(payloadText ?? '').trim()
  if (!text) return {}
  return JSON.parse(text)
}
const executeManufacturingAction = async (message, actionCard, cardIndex = 0) => {
  if (!message?.manufacturingData || !actionCard || actionCard.executing) return
  const actionName = actionCard.name || `动作 ${cardIndex + 1}`
  const targetApi = String(actionCard.targetApi || '').trim()
  if (!targetApi) {
    actionCard.executeError = true
    actionCard.executeResult = '缺少 targetApi,无法执行动作'
    return
  }
  let payload = {}
  try {
    payload = parseManufacturingActionPayload(actionCard.payloadText)
  } catch (err) {
    actionCard.executeError = true
    actionCard.executeResult = '请求参数不是合法 JSON,请检查后重试'
    return
  }
  const requiredFields = Array.isArray(actionCard.requiredFields) ? actionCard.requiredFields : []
  if (requiredFields.length && !isPlainObject(payload)) {
    actionCard.executeError = true
    actionCard.executeResult = '必填字段校验失败:请求参数必须是 JSON 对象'
    return
  }
  const missingFields = getMissingRequiredFields(requiredFields, payload)
  if (missingFields.length) {
    actionCard.executeError = true
    const missingFieldLabels = missingFields.map(field => getStructuredPathLabel(field))
    actionCard.executeResult = `缺少必填字段:${missingFieldLabels.join('、')}`
    return
  }
  try {
    await ElMessageBox.confirm(`确认执行「${actionName}」吗?`, '执行确认', {
      confirmButtonText: '确认执行',
      cancelButtonText: '取消',
      type: 'warning'
    })
  } catch (err) {
    return
  }
  actionCard.executing = true
  actionCard.executeError = false
  actionCard.executeResult = ''
  const method = normalizeRequestMethod(actionCard.method)
  const requestConfig = {
    url: targetApi,
    method: method.toLowerCase()
  }
  if (method === 'GET') {
    requestConfig.params = payload
  } else {
    requestConfig.data = payload
  }
  try {
    const res = await request(requestConfig)
    const successMsg = res?.msg || `${actionName}执行成功`
    actionCard.executed = true
    actionCard.executeError = false
    actionCard.executeResult = successMsg
    ElMessage.success(successMsg)
  } catch (err) {
    actionCard.executed = false
    actionCard.executeError = true
    actionCard.executeResult = err?.message || `${actionName}执行失败,请稍后重试`
  } finally {
    actionCard.executing = false
  }
}
// 历史会话相关
const purchaseValueTypeOptions = [
@@ -831,6 +1372,34 @@
const loadingSessions = ref(false)
const isImageFileType = (fileType = '') => String(fileType || '').toLowerCase().startsWith('image/')
const imageFilePathPattern = /\.(png|jpe?g|gif|webp|bmp|svg)$/i
const getPathnameFromFilePath = (filePath = '') => {
  const rawPath = String(filePath || '').trim()
  if (!rawPath) return ''
  try {
    const baseOrigin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
    return new URL(rawPath, baseOrigin).pathname || ''
  } catch (err) {
    return rawPath.split('?')[0]
  }
}
const isImageFilePath = (filePath = '') => {
  const pathname = getPathnameFromFilePath(filePath).toLowerCase()
  return imageFilePathPattern.test(pathname)
}
const getHistoryFileName = (filePath = '', index = 0) => {
  const pathname = getPathnameFromFilePath(filePath)
  const fileName = pathname.split('/').filter(Boolean).pop()
  if (!fileName) return `file-${index + 1}`
  try {
    return decodeURIComponent(fileName)
  } catch (err) {
    return fileName
  }
}
const getImagePreviewList = (files = []) => {
  if (!Array.isArray(files)) return []
@@ -858,14 +1427,34 @@
  const fileType = rawFile?.type || ''
  const isImage = isImageFileType(fileType)
  const canCreateObjectURL = typeof URL !== 'undefined' && typeof URL.createObjectURL === 'function'
  const previewUrl = isImage && rawFile && canCreateObjectURL ? URL.createObjectURL(rawFile) : ''
  return {
    previewId: `${rawFile?.name || 'file'}-${rawFile?.size || 0}-${rawFile?.lastModified || Date.now()}-${index}`,
    name: rawFile?.name || `file-${index + 1}`,
    size: rawFile?.size || 0,
    type: fileType,
    isImage,
    previewUrl: isImage && rawFile && canCreateObjectURL ? URL.createObjectURL(rawFile) : '',
    rawFile
    previewUrl,
    accessUrl: '',
    rawFile,
    isObjectUrl: !!previewUrl
  }
}
const createHistoryFileSnapshot = (filePath, memoryId = '', messageIndex = 0, fileIndex = 0) => {
  const normalizedPath = String(filePath || '').trim()
  if (!normalizedPath) return null
  const isImage = isImageFilePath(normalizedPath)
  return {
    previewId: `${memoryId || 'history'}-${messageIndex}-${fileIndex}`,
    name: getHistoryFileName(normalizedPath, fileIndex),
    size: 0,
    type: '',
    isImage,
    previewUrl: isImage ? normalizedPath : '',
    accessUrl: normalizedPath,
    rawFile: null,
    isObjectUrl: false
  }
}
@@ -874,10 +1463,29 @@
  if (!canRevokeObjectURL) return
  if (!Array.isArray(snapshots)) return
  snapshots.forEach((snapshot) => {
    if (snapshot?.previewUrl) {
    if (snapshot?.isObjectUrl && snapshot?.previewUrl) {
      URL.revokeObjectURL(snapshot.previewUrl)
    }
  })
}
const mapHistoryFilePathsToSnapshots = (filePaths = [], memoryId = '', messageIndex = 0) => {
  if (!Array.isArray(filePaths)) return []
  return filePaths
    .map((filePath, fileIndex) => createHistoryFileSnapshot(filePath, memoryId, messageIndex, fileIndex))
    .filter(Boolean)
}
const openMessageAttachment = (file) => {
  const accessUrl = String(file?.accessUrl || '').trim()
  if (!accessUrl) return
  if (typeof window === 'undefined' || typeof window.open !== 'function') return
  window.open(accessUrl, '_blank', 'noopener,noreferrer')
}
const handleMessageFileClick = (file) => {
  if (!file?.accessUrl || file?.isImage) return
  openMessageAttachment(file)
}
const revokeMessageLocalFileSnapshots = (messageList = []) => {
@@ -962,7 +1570,7 @@
        const messageObj = {
          isUser,
          content: msg.content,
          content: msg.content || '',
          htmlContent: '',
          isTyping: false,
          chartOptions: null,
@@ -970,7 +1578,10 @@
          type: '',
          tableData: null,
          payloadTreeData: null,
          payloadHiddenData: null
          payloadHiddenData: null,
          purchaseAnalysisData: null,
          manufacturingData: null,
          localUploadFiles: isUser ? mapHistoryFilePathsToSnapshots(msg.filePaths, uuid.value, idx) : []
        }
        messages.value.push(messageObj)
@@ -985,15 +1596,15 @@
          }
          // 解析历史消息中的 JSON
          const extracted = extractEmbeddedSuccessJson(msg.content)
          const extracted = extractEmbeddedSuccessJson(msg.content || '')
          if (extracted) {
            applyStructuredMessageData(messageObj, extracted.data, botMsgIndex)
          }
          updateOutputState(msg.content, botMsgIndex)
          messageObj.htmlContent = convertStreamOutput(msg.content, botMsgIndex)
          updateOutputState(msg.content || '', botMsgIndex)
          messageObj.htmlContent = convertStreamOutput(msg.content || '', botMsgIndex)
        } else {
          messageObj.htmlContent = convertTextToHtml(msg.content)
          messageObj.htmlContent = convertTextToHtml(msg.content || '')
        }
      })
      scrollToBottom()
@@ -1028,11 +1639,10 @@
}
onMounted(() => {
  initUUID()
  // 初始欢迎
  if (messages.value.length === 0) {
    hello()
  if (props.autoOpen) {
    visible.value = true
  }
  initUUID()
  window.addEventListener('resize', handleWindowResize)
})
@@ -1057,11 +1667,26 @@
  inputMessage.value = ''
  quickPromptStart.value = 0
  initUUID()
  hello()
})
watch(() => props.defaultAssistant, (nextKey) => {
  if (!nextKey || nextKey === selectedAssistantKey.value) return
  if (!assistants.value.some(item => item.key === nextKey)) return
  selectedAssistantKey.value = nextKey
})
watch(() => props.autoOpen, (nextValue) => {
  if (nextValue) {
    visible.value = true
  }
})
const handleWindowResize = () => {
  windowWidth.value = window.innerWidth
}
const handleHeaderExtraAction = () => {
  emit('header-extra-action')
}
const toggleSidebar = () => {
@@ -1072,10 +1697,12 @@
}
const handleClose = () => {
  if (hideTrigger.value) return
  visible.value = false
}
const handleManualClose = () => {
  if (hideTrigger.value) return
  if (isSending.value) {
    abortCurrentRequest()
  }
@@ -1091,10 +1718,6 @@
  uuid.value = storedUUID
}
const hello = () => {
  sendRequest(currentAssistant.value.welcomeMessage || '你好')
}
const newChat = () => {
  revokeMessageLocalFileSnapshots(messages.value)
  disposeCharts()
@@ -1106,7 +1729,6 @@
  quickPromptStart.value = 0
  localStorage.removeItem(currentAssistant.value.storageKey)
  initUUID()
  hello()
}
const handleNewChat = () => {
@@ -1198,15 +1820,25 @@
const applyStructuredMessageData = (messageObj, parsedData, msgIndex, shouldRenderCharts = true) => {
  if (!messageObj || !parsedData?.success) return
  const previousManufacturingData = messageObj.manufacturingData
  messageObj.type = parsedData.type || ''
  messageObj.tableData = null
  messageObj.purchaseAnalysisData = null
  messageObj.manufacturingData = null
  if (messageObj.type === 'todo_list' && parsedData.data) {
    messageObj.tableData = parsedData.data
  }
  const manufacturingData = buildManufacturingStructuredData(parsedData, previousManufacturingData)
  if (manufacturingData) {
    messageObj.manufacturingData = manufacturingData
  }
  if (parsedData.action === 'confirm_required' && parsedData.businessType) {
    messageObj.type = 'purchase_analysis_confirm'
    messageObj.purchaseAnalysisData = parsedData
    messageObj.manufacturingData = null
    if (!Array.isArray(messageObj.payloadTreeData) || !messageObj.payloadTreeData.length) {
      initializePurchasePayloadTree(messageObj, parsedData.payload || {})
    }
@@ -1246,6 +1878,19 @@
  }
  return null
}
const getStructuredFallbackText = (parsedData) => {
  if (!parsedData) return '正在为您展示分析结果...'
  if (parsedData.type === 'todo_list') return '已为您整理好相关数据。'
  if (manufacturingStructuredTypeSet.has(parsedData.type)) {
    if (parsedData.type === 'manufacturing_action_plan') return '已为您生成办理建议,请确认动作后执行。'
    if (parsedData.type === 'manufacturing_warning') return '已为您生成制造预警看板。'
    if (parsedData.type === 'manufacturing_analysis') return '已为您生成制造分析结果。'
    return '已返回制造查询结果。'
  }
  if (parsedData.charts && Object.keys(parsedData.charts).length > 0) return '已为您生成分析图表。'
  return '正在为您展示分析结果...'
}
const buildPurchaseMaterialRankCharts = (parsedData) => {
@@ -1454,12 +2099,7 @@
  if (visibleValue === undefined || visibleValue === null) return clonePurchasePayloadValue(hiddenValue)
  if (Array.isArray(visibleValue) && Array.isArray(hiddenValue)) {
    const maxLength = Math.max(visibleValue.length, hiddenValue.length)
    const merged = []
    for (let i = 0; i < maxLength; i++) {
      merged[i] = mergePurchasePayloadWithHidden(visibleValue[i], hiddenValue[i])
    }
    return merged
    return visibleValue.map((item, index) => mergePurchasePayloadWithHidden(item, hiddenValue[index]))
  }
  if (
@@ -1862,6 +2502,23 @@
    ''
}
const prunePurchaseProductRecord = (record) => {
  if (!record || typeof record !== 'object' || Array.isArray(record)) return null
  const normalizedRecord = normalizePurchaseProductRecord(record)
  const hasVisibleFieldValue = Object.entries(normalizedRecord).some(([key, value]) => {
    if (shouldHidePurchaseField(key)) return false
    return hasMeaningfulPayloadValue(value)
  })
  return hasVisibleFieldValue ? normalizedRecord : null
}
const normalizeAndFilterPurchaseProductData = (value) => {
  if (!Array.isArray(value)) return value
  return value
    .map(item => prunePurchaseProductRecord(item))
    .filter(Boolean)
}
const mergeLegacyProductDataIntoLedgers = (payload) => {
  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return payload
  if (!Array.isArray(payload.purchaseLedgers) || !Array.isArray(payload.productData) || !payload.productData.length) {
@@ -1870,13 +2527,11 @@
  const ledgers = payload.purchaseLedgers.map(ledger => ({
    ...ledger,
    productData: Array.isArray(ledger.productData)
      ? ledger.productData.map(normalizePurchaseProductRecord)
      : []
    productData: normalizeAndFilterPurchaseProductData(ledger.productData) || []
  }))
  const unmatchedProducts = []
  payload.productData.map(normalizePurchaseProductRecord).forEach(product => {
  normalizeAndFilterPurchaseProductData(payload.productData).forEach(product => {
    const productMatchKey = getPurchaseProductMatchKey(product)
    const matchedLedger = ledgers.find(ledger => {
      const ledgerKeys = [
@@ -1915,9 +2570,7 @@
  if (!record || typeof record !== 'object' || Array.isArray(record)) return record
  const normalizedRecord = {
    ...record,
    productData: Array.isArray(record.productData)
      ? record.productData.map(normalizePurchaseProductRecord)
      : record.productData
    productData: normalizeAndFilterPurchaseProductData(record.productData)
  }
  return Object.entries(normalizedRecord).reduce((result, [key, value]) => {
    if (purchaseLedgerAllowedFieldKeys.has(key)) {
@@ -2114,6 +2767,12 @@
  sanitized = normalizePurchasePayloadFieldTypes(sanitized)
  if (Array.isArray(sanitized.purchaseLedgers)) {
    sanitized.purchaseLedgers = sanitized.purchaseLedgers.map(filterPurchaseLedgerRecord)
  }
  if (Array.isArray(sanitized.productData)) {
    sanitized.productData = normalizeAndFilterPurchaseProductData(sanitized.productData)
  }
  if (Array.isArray(sanitized.productData) && !sanitized.productData.length) {
    delete sanitized.productData
  }
  purchaseApprovalFieldKeys.forEach(key => {
@@ -2357,7 +3016,9 @@
    type: '',
    tableData: null,
    payloadTreeData: null,
    payloadHiddenData: null
    payloadHiddenData: null,
    purchaseAnalysisData: null,
    manufacturingData: null
  })
  outputState.value[botMsgIndex] = {
@@ -2480,7 +3141,9 @@
    type: '',
    tableData: null,
    payloadTreeData: null,
    payloadHiddenData: null
    payloadHiddenData: null,
    purchaseAnalysisData: null,
    manufacturingData: null
  }
  messages.value.push(botMsg)
@@ -2646,13 +3309,7 @@
    }
    if (!display) {
      if (parsed.type === 'todo_list') {
        display = '已为您整理好相关数据。'
      } else if (parsed.charts && Object.keys(parsed.charts).length > 0) {
        display = '已为您生成分析图表。'
      } else {
        display = '正在为您展示分析结果...'
      }
      display = getStructuredFallbackText(parsed)
    }
  } else if (startIdx !== -1) {
    const lastBraceIdx = output.lastIndexOf('}')
@@ -2674,13 +3331,7 @@
        }
        if (!display) {
          if (parsed.type === 'todo_list') {
            display = '已为您整理好相关数据:'
          } else if (parsed.charts && Object.keys(parsed.charts).length > 0) {
            display = '已为您生成分析图表:'
          } else {
            display = '正在为您展示分析结果...'
          }
          display = getStructuredFallbackText(parsed)
        }
      } catch (e) {
        // 解析失败,说明 JSON 还在传输中或格式不正确
@@ -3026,8 +3677,9 @@
    height: 100%;
  }
  :deep(.el-drawer__header) {
    margin-bottom: 0;
    padding: 0;
    margin-bottom: 0 !important;
    padding: 0 !important;
    border-bottom: 1px solid rgba(255, 255, 255, 0.12);
    background: $gradient-dark;
    color: #fff;
  }
@@ -3038,7 +3690,7 @@
  justify-content: space-between;
  align-items: center;
  width: 100%;
  padding: 18px 20px;
  padding: 12px 18px;
  background: $gradient-dark;
  position: relative;
  overflow: hidden;
@@ -3170,6 +3822,15 @@
        opacity: 1;
      }
    }
    :deep(.header-action-btn--text) {
      width: auto !important;
      min-width: 104px;
      padding: 8px 14px !important;
      font-size: 14px;
      font-weight: 600;
      white-space: nowrap;
    }
  }
  .assistant-switcher {
@@ -3252,7 +3913,7 @@
    top: 0;
    left: 0;
    right: 0;
    height: 240px;
    height: 128px;
    background: linear-gradient(180deg, rgba(0, 85, 212, 0.06) 0%, transparent 100%);
    pointer-events: none;
  }
@@ -3509,6 +4170,16 @@
      border: 1px solid rgba(88, 117, 255, 0.2);
      background: rgba(255, 255, 255, 0.9);
      max-width: 100%;
      &.clickable {
        cursor: pointer;
        transition: all 0.2s ease;
        &:hover {
          border-color: rgba(44, 109, 255, 0.38);
          background: rgba(243, 247, 255, 0.96);
        }
      }
    }
    .message-local-file-thumb {
@@ -3547,6 +4218,15 @@
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      &.clickable {
        color: $primary-blue;
        cursor: pointer;
        &:hover {
          text-decoration: underline;
        }
      }
    }
    .message-local-file-size {
@@ -3671,6 +4351,171 @@
    min-width: 300px;
    --el-table-border-color: rgba(0, 122, 255, 0.08);
    --el-table-header-bg-color: $ice-white;
  }
}
.manufacturing-card {
  margin-top: 12px;
  width: 100%;
  background: #fff;
  border: 1px solid rgba(0, 85, 212, 0.12);
  border-radius: 12px;
  box-shadow: $shadow-card;
  padding: 14px;
}
.manufacturing-card__title {
  font-size: 14px;
  font-weight: 700;
  color: $deep-blue;
  margin-bottom: 10px;
}
.manufacturing-summary-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 8px;
  margin-bottom: 12px;
}
.manufacturing-summary-item {
  border-radius: 10px;
  padding: 10px 12px;
  border: 1px solid rgba(0, 85, 212, 0.08);
  background: linear-gradient(180deg, #f8fbff, #f1f7ff);
  min-height: 66px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 6px;
}
.manufacturing-summary-item--core {
  border-color: rgba(30, 91, 255, 0.24);
}
.manufacturing-summary-label {
  font-size: 12px;
  color: #4b5563;
}
.manufacturing-summary-value {
  font-size: 15px;
  color: #1f2937;
  line-height: 1.4;
  word-break: break-all;
}
.manufacturing-warning-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 12px;
}
.manufacturing-warning-item {
  border-radius: 10px;
  border: 1px solid rgba(245, 158, 11, 0.22);
  background: linear-gradient(135deg, rgba(255, 247, 237, 0.9), rgba(255, 255, 255, 0.98));
  padding: 10px 12px;
}
.manufacturing-warning-item__head {
  display: flex;
  align-items: center;
  gap: 8px;
  color: #92400e;
  font-size: 13px;
}
.manufacturing-warning-count {
  margin-left: auto;
  font-weight: 700;
  color: #c2410c;
}
.manufacturing-warning-detail {
  margin: 8px 0 0;
  font-size: 12px;
  line-height: 1.6;
  color: #7c2d12;
  word-break: break-all;
}
.manufacturing-table-wrapper {
  margin-top: 10px;
}
.manufacturing-action-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 12px;
}
.manufacturing-action-card {
  border: 1px solid rgba(0, 85, 212, 0.1);
  border-radius: 10px;
  padding: 10px 12px;
  background: #f8fbff;
}
.manufacturing-action-card__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  font-size: 13px;
  color: #1f2937;
  margin-bottom: 8px;
}
.manufacturing-action-card__meta {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 8px;
  font-size: 12px;
  color: #64748b;
  word-break: break-all;
}
.manufacturing-action-card__desc {
  margin: 0 0 8px;
  font-size: 12px;
  line-height: 1.6;
  color: #475467;
}
.manufacturing-required-fields {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-bottom: 8px;
  font-size: 12px;
  color: #7c2d12;
}
.manufacturing-action-footer {
  margin-top: 8px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 12px;
}
.manufacturing-action-result {
  flex: 1;
  font-size: 12px;
  line-height: 1.5;
  &.success {
    color: #1f9d55;
  }
  &.error {
    color: #d93025;
  }
}
@@ -4160,75 +5005,59 @@
.chat-hero {
  display: grid;
  grid-template-columns: 164px minmax(0, 1fr);
  gap: 18px;
  align-items: start;
  padding: 14px 18px 6px;
  grid-template-columns: 176px minmax(0, 1fr);
  gap: 14px;
  align-items: stretch;
  padding: 8px 18px 4px;
  &.compact {
    grid-template-columns: 122px minmax(0, 1fr);
    gap: 12px;
    padding: 8px 18px 2px;
    grid-template-columns: 132px minmax(0, 1fr);
    gap: 10px;
    padding: 4px 18px 2px;
  }
}
.assistant-stand {
  position: relative;
  min-height: 252px;
  min-height: 206px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  padding-top: 18px;
  padding-top: 8px;
  overflow: hidden;
  &.compact {
    min-height: 176px;
    padding-top: 8px;
    min-height: 160px;
    padding-top: 4px;
  }
  &.thinking {
    .assistant-halo {
      opacity: 1;
      transform: scale(1.08);
      filter: blur(8px);
      transform: scale(1.12);
      filter: blur(9px);
    }
    .assistant-scan-ring {
      opacity: 1;
      animation-duration: 1.6s;
      opacity: 0.95;
      animation-duration: 1.5s;
    }
    .assistant-orbit {
      opacity: 1;
      opacity: 0.76;
    }
    .assistant-bot {
      transform: translateY(-4px) scale(1.02);
    .assistant-model-shell {
      transform: translateY(-5px) scale(1.02);
    }
    .assistant-bot-head {
      box-shadow: 0 0 30px rgba(80, 157, 255, 0.36);
    .assistant-model-cut {
      animation-duration: 2.2s;
    }
    .assistant-bot-eye {
      animation: robotBlinkFast 1.1s infinite;
      box-shadow: 0 0 16px rgba(72, 186, 255, 0.95);
    }
    .assistant-bot-mouth {
      width: 28px;
      opacity: 1;
      animation: robotTalk 1.2s ease-in-out infinite;
    }
    .assistant-bot-core {
      animation: corePulse 1.4s ease-in-out infinite;
      box-shadow: 0 0 24px rgba(78, 120, 255, 0.26);
    }
    .assistant-bot-core-ring {
      animation: coreRotate 3s linear infinite;
    .assistant-model-img {
      filter: saturate(1.06) drop-shadow(0 18px 20px rgba(22, 48, 80, 0.22));
    }
    .assistant-status {
@@ -4242,219 +5071,174 @@
      animation: thinkingDot 1s ease-in-out infinite;
    }
    .assistant-base-lg {
      animation-duration: 1.8s;
    }
    .assistant-base-md {
      animation-duration: 1.5s;
    }
    .assistant-base-sm {
      box-shadow: 0 0 24px rgba(255, 93, 122, 0.48);
      box-shadow: 0 0 24px rgba(30, 91, 255, 0.36);
      animation-duration: 1.25s;
    }
  }
}
.assistant-halo {
  position: absolute;
  top: 22px;
  width: 130px;
  height: 130px;
  top: 24px;
  width: 146px;
  height: 146px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(46, 140, 224, 0.3) 0%, rgba(0, 85, 212, 0.18) 42%, rgba(113, 54, 244, 0.12) 60%, transparent 78%);
  background: radial-gradient(circle, rgba(31, 122, 114, 0.26) 0%, rgba(30, 91, 255, 0.2) 42%, rgba(109, 65, 237, 0.12) 66%, transparent 80%);
  filter: blur(6px);
  opacity: 0.82;
  opacity: 0.78;
  transition: all 0.35s ease;
}
.assistant-scan-ring {
  position: absolute;
  top: 40px;
  width: 132px;
  height: 132px;
  top: 44px;
  width: 136px;
  height: 136px;
  border-radius: 50%;
  border: 1px solid rgba(90, 159, 224, 0.22);
  border: 1px solid rgba(67, 145, 223, 0.24);
  box-shadow: inset 0 0 16px rgba(255, 255, 255, 0.25);
  opacity: 0.55;
  opacity: 0.52;
  animation: scanRing 4s linear infinite;
}
.assistant-orbit {
  position: absolute;
  top: 52px;
  width: 150px;
  height: 150px;
  width: 156px;
  height: 156px;
  border-radius: 50%;
  border: 1px dashed rgba(92, 135, 255, 0.22);
  opacity: 0.45;
  border: 1px dashed rgba(92, 135, 255, 0.24);
  opacity: 0.42;
}
.assistant-orbit-a {
  animation: orbitRotate 8s linear infinite;
  animation: orbitRotate 8.6s linear infinite;
}
.assistant-orbit-b {
  width: 118px;
  height: 118px;
  width: 124px;
  height: 124px;
  top: 68px;
  border-color: rgba(255, 108, 150, 0.22);
  animation: orbitRotateReverse 5.6s linear infinite;
  border-color: rgba(31, 122, 114, 0.24);
  animation: orbitRotateReverse 6.2s linear infinite;
}
.assistant-bot {
.assistant-model-shell {
  position: relative;
  z-index: 1;
  width: 148px;
  height: 178px;
  display: flex;
  flex-direction: column;
  align-items: center;
  align-items: flex-end;
  justify-content: center;
  margin-top: 12px;
  margin-top: 4px;
  transition: transform 0.35s ease;
}
.assistant-bot-antenna {
  position: absolute;
  top: -4px;
  width: 4px;
  height: 20px;
  border-radius: 999px;
  background: linear-gradient(180deg, #fefefe, #aac9ff);
  &::before {
    content: '';
    position: absolute;
    top: -6px;
    left: 50%;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    bottom: 2px;
    width: 164px;
    height: 42px;
    transform: translateX(-50%);
    background: linear-gradient(135deg, #54bfff, #7a41ff);
    box-shadow: 0 0 14px rgba(84, 191, 255, 0.65);
    border-radius: 50%;
    background: radial-gradient(
      ellipse at center,
      rgba(43, 126, 211, 0.32) 0%,
      rgba(43, 126, 211, 0.14) 46%,
      rgba(43, 126, 211, 0) 74%
    );
    filter: blur(2.6px);
    animation: baseGlow 4.6s ease-in-out infinite;
    z-index: 1;
  }
  &::after {
    content: '';
    position: absolute;
    left: 50%;
    bottom: 10px;
    width: 138px;
    height: 28px;
    transform: translateX(-50%);
    border-radius: 50%;
    border: 1px solid rgba(36, 116, 198, 0.6);
    box-shadow:
      inset 0 0 0 1px rgba(255, 255, 255, 0.58),
      0 0 22px rgba(42, 116, 196, 0.24);
    animation: basePulse 3.2s ease-in-out infinite;
    z-index: 4;
  }
}
.assistant-bot-antenna-left {
  left: 36px;
  transform: rotate(-14deg);
}
.assistant-bot-antenna-right {
  right: 36px;
  transform: rotate(14deg);
}
.assistant-bot-head {
.assistant-model-cut {
  position: relative;
  width: 132px;
  height: 178px;
  z-index: 6;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  transform-origin: center 84%;
  animation: avatarFloat 3.2s ease-in-out infinite;
}
.assistant-model-img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center bottom;
  display: block;
  filter: saturate(1.03) drop-shadow(0 14px 18px rgba(22, 49, 79, 0.2));
  transition: filter 0.35s ease;
}
.assistant-model-fallback {
  width: 92px;
  height: 78px;
  border-radius: 28px;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, #e8f1ff 100%);
  border: 1px solid rgba(0, 85, 212, 0.14);
  box-shadow: 0 16px 32px rgba(0, 85, 212, 0.14);
  height: 92px;
  border-radius: 24px;
  color: #fff;
  background: linear-gradient(145deg, rgba(31, 122, 114, 0.9), rgba(30, 91, 255, 0.9));
  border: 1px solid rgba(255, 255, 255, 0.3);
  box-shadow: 0 12px 24px rgba(31, 85, 173, 0.22);
  display: flex;
  align-items: center;
  justify-content: center;
}
.assistant-bot-head-glow {
  position: absolute;
  inset: 10px 16px auto;
  height: 20px;
  border-radius: 999px;
  background: linear-gradient(180deg, rgba(0, 85, 212, 0.16), transparent);
}
.assistant-bot-eye {
  position: absolute;
  top: 30px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: radial-gradient(circle, #8ef0ff 0%, #56c0ff 42%, #2869ff 100%);
  box-shadow: 0 0 12px rgba(72, 186, 255, 0.72);
  animation: robotBlink 3.2s infinite;
}
.assistant-bot-eye-left {
  left: 22px;
}
.assistant-bot-eye-right {
  right: 22px;
}
.assistant-bot-mouth {
.assistant-base {
  position: absolute;
  left: 50%;
  bottom: 16px;
  width: 22px;
  height: 4px;
  bottom: 8px;
  transform: translateX(-50%);
  border-radius: 999px;
  background: linear-gradient(90deg, rgba(72, 186, 255, 0.2), rgba(72, 186, 255, 0.9), rgba(72, 186, 255, 0.2));
}
.assistant-bot-neck {
  width: 16px;
  height: 10px;
  border-radius: 0 0 10px 10px;
  background: linear-gradient(180deg, #dceaff, #bdd5ff);
  margin-top: -2px;
}
.assistant-bot-body {
  position: relative;
  width: 104px;
  height: 92px;
  margin-top: 2px;
  border-radius: 28px 28px 34px 34px;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, #e3eeff 100%);
  border: 1px solid rgba(0, 85, 212, 0.14);
  box-shadow: 0 18px 36px rgba(0, 85, 212, 0.16);
  display: flex;
  align-items: center;
  justify-content: center;
}
.assistant-bot-arm {
  position: absolute;
  top: 18px;
  width: 16px;
  height: 44px;
  border-radius: 999px;
  background: linear-gradient(180deg, #eff5ff, #c7dbff);
  border: 1px solid rgba(0, 85, 212, 0.12);
}
.assistant-bot-arm-left {
  left: -10px;
  transform: rotate(16deg);
}
.assistant-bot-arm-right {
  right: -10px;
  transform: rotate(-16deg);
}
.assistant-bot-core {
  position: relative;
  width: 46px;
  height: 46px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: $primary-blue;
  background: radial-gradient(circle, rgba(255, 255, 255, 1) 0%, #dae8ff 55%, #adc7ff 100%);
}
.assistant-bot-core-ring {
  position: absolute;
  inset: -6px;
  border-radius: 50%;
  border: 1px solid rgba(88, 135, 255, 0.3);
  border-top-color: rgba(255, 96, 139, 0.85);
  border-right-color: rgba(79, 145, 255, 0.9);
  border: 1px solid rgba(36, 116, 198, 0.28);
  background: radial-gradient(
    ellipse at center,
    rgba(255, 255, 255, 0.94) 0%,
    rgba(81, 164, 233, 0.16) 58%,
    rgba(30, 91, 255, 0.06) 100%
  );
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2);
}
.assistant-status {
  position: relative;
  z-index: 1;
  margin-top: 14px;
  padding: 6px 12px;
  margin-top: 7px;
  padding: 5px 10px;
  border-radius: 999px;
  font-size: 12px;
  font-size: 11px;
  font-weight: 600;
  color: $deep-blue;
  background: rgba(255, 255, 255, 0.95);
@@ -4474,81 +5258,25 @@
}
.assistant-base {
  position: absolute;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  border-radius: 50%;
  border: 2px solid rgba(255, 93, 122, 0.22);
  background: radial-gradient(circle, rgba(255, 255, 255, 0.9) 0%, rgba(255, 111, 145, 0.1) 70%, transparent 100%);
}
.assistant-base-lg {
  width: 118px;
  height: 30px;
  pointer-events: none;
}
.assistant-base-md {
  bottom: 6px;
  width: 88px;
  height: 20px;
  border-color: rgba(255, 93, 122, 0.34);
  bottom: 15px;
  width: 104px;
  height: 22px;
  border-color: rgba(36, 116, 198, 0.48);
  animation: basePulse 2.8s ease-in-out infinite;
}
.assistant-base-sm {
  bottom: 11px;
  width: 54px;
  height: 10px;
  background: linear-gradient(90deg, rgba(255, 93, 122, 0.95), rgba(255, 173, 188, 0.9));
  bottom: 20px;
  width: 68px;
  height: 14px;
  background: linear-gradient(90deg, rgba(31, 122, 114, 0.82), rgba(45, 124, 255, 0.9));
  border: none;
  box-shadow: 0 0 18px rgba(255, 93, 122, 0.38);
}
@keyframes robotBlink {
  0%, 44%, 48%, 100% {
    transform: scaleY(1);
  }
  46% {
    transform: scaleY(0.14);
  }
}
@keyframes robotBlinkFast {
  0%, 100% {
    transform: scaleY(1);
  }
  50% {
    transform: scaleY(0.3);
  }
}
@keyframes robotTalk {
  0%, 100% {
    transform: translateX(-50%) scaleX(1);
  }
  50% {
    transform: translateX(-50%) scaleX(1.35);
  }
}
@keyframes corePulse {
  0%, 100% {
    transform: scale(1);
    filter: brightness(1);
  }
  50% {
    transform: scale(1.08);
    filter: brightness(1.08);
  }
}
@keyframes coreRotate {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
  box-shadow: 0 0 18px rgba(45, 124, 255, 0.34);
  animation: basePulse 2.2s ease-in-out infinite;
}
@keyframes orbitRotate {
@@ -4589,9 +5317,85 @@
  }
}
.assistant-base-lg {
  width: 142px;
  height: 32px;
  animation: basePulse 3.4s ease-in-out infinite;
  &::before {
    content: '';
    position: absolute;
    left: 50%;
    top: 50%;
    width: 130px;
    height: 130px;
    transform: translate(-50%, -50%);
    border-radius: 50%;
    background: conic-gradient(
      from 180deg,
      transparent 0deg,
      rgba(36, 116, 198, 0.65) 48deg,
      transparent 114deg,
      rgba(36, 116, 198, 0.55) 212deg,
      transparent 286deg,
      rgba(31, 122, 114, 0.45) 334deg,
      transparent 360deg
    );
    -webkit-mask: radial-gradient(circle, transparent 61%, #000 62%, #000 68%, transparent 70%);
    mask: radial-gradient(circle, transparent 61%, #000 62%, #000 68%, transparent 70%);
    opacity: 0.62;
    animation: baseSpin 9s linear infinite;
  }
}
@keyframes avatarFloat {
  0%,
  100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-7px);
  }
}
@keyframes basePulse {
  0%,
  100% {
    transform: translateX(-50%) scale(1);
    opacity: 0.88;
  }
  50% {
    transform: translateX(-50%) scale(1.05);
    opacity: 0.98;
  }
}
@keyframes baseSpin {
  from {
    transform: translate(-50%, -50%) rotate(0deg);
  }
  to {
    transform: translate(-50%, -50%) rotate(360deg);
  }
}
@keyframes baseGlow {
  0%,
  100% {
    transform: translateX(-50%) scaleX(1);
    opacity: 0.82;
  }
  50% {
    transform: translateX(-50%) scaleX(1.06);
    opacity: 0.96;
  }
}
.welcome-card {
  position: relative;
  padding: 14px 14px 12px;
  align-self: stretch;
  min-height: 206px;
  padding: 9px 10px 8px;
  border-radius: 16px;
  background:
    linear-gradient(#fff, #fff) padding-box,
@@ -4600,7 +5404,8 @@
  box-shadow: 0 16px 36px rgba(0, 85, 212, 0.12);
  &.compact {
    padding: 10px 12px;
    min-height: 160px;
    padding: 8px 9px 7px;
    border-radius: 12px;
    box-shadow: 0 8px 16px rgba(0, 85, 212, 0.07);
@@ -4609,8 +5414,8 @@
    }
    .welcome-title {
      font-size: 17px;
      line-height: 1.3;
      font-size: 16px;
      line-height: 1.25;
      br {
        display: none;
@@ -4618,65 +5423,69 @@
    }
    .welcome-desc {
      margin-top: 6px;
      font-size: 12px;
      line-height: 1.55;
      margin-top: 4px;
      font-size: 11px;
      line-height: 1.5;
    }
    .quick-prompt-list {
      margin-top: 10px;
      gap: 6px;
      margin-top: 8px;
      gap: 5px;
    }
    .quick-prompt-btn {
      padding: 8px 10px;
      font-size: 12px;
      padding: 7px 9px;
      font-size: 11px;
      border-radius: 7px;
    }
    .more-prompts-btn {
      margin-top: 8px;
      font-size: 12px;
      margin-top: 6px;
      font-size: 11px;
    }
  }
}
.welcome-eyebrow {
  font-size: 11px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 2px;
  color: rgba(0, 85, 212, 0.58);
  margin-bottom: 8px;
  margin-bottom: 5px;
}
.welcome-title {
  margin: 0;
  font-size: 26px;
  line-height: 1.2;
  font-size: 20px;
  line-height: 1.15;
  font-weight: 800;
  color: #172033;
  br {
    display: none;
  }
}
.welcome-desc {
  margin: 10px 0 0;
  font-size: 13px;
  line-height: 1.7;
  margin: 5px 0 0;
  font-size: 12px;
  line-height: 1.5;
  color: #5f6980;
}
.quick-prompt-list {
  display: grid;
  gap: 8px;
  margin-top: 14px;
  gap: 6px;
  margin-top: 8px;
}
.quick-prompt-btn {
  width: 100%;
  border: none;
  border-radius: 10px;
  padding: 11px 14px;
  border-radius: 9px;
  padding: 7px 10px;
  text-align: left;
  font-size: 13px;
  font-size: 12px;
  font-weight: 600;
  color: #fff;
  cursor: pointer;
@@ -4724,14 +5533,14 @@
}
.more-prompts-btn {
  margin-top: 10px;
  padding: 0 12px;
  height: 32px;
  margin-top: 6px;
  padding: 0 10px;
  height: 26px;
  border: 1px solid rgba(208, 65, 81, 0.12);
  border-radius: 999px;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(255, 241, 245, 0.96));
  color: #d04151;
  font-size: 13px;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  display: inline-flex;
@@ -4746,21 +5555,6 @@
    border-color: transparent;
    color: #fff;
    box-shadow: 0 14px 24px rgba(138, 61, 246, 0.18);
  }
}
.hero-dot-grid {
  display: grid;
  grid-template-columns: repeat(14, 1fr);
  gap: 7px;
  padding: 0 18px 14px;
  span {
    display: block;
    width: 100%;
    aspect-ratio: 1;
    border-radius: 2px;
    background: linear-gradient(135deg, rgba(255, 110, 138, 0.95), rgba(255, 190, 201, 0.55));
  }
}
@@ -4856,12 +5650,6 @@
  .welcome-title {
    font-size: 21px;
  }
  .hero-dot-grid {
    grid-template-columns: repeat(12, 1fr);
    gap: 6px;
    padding: 0 14px 12px;
  }
  .message-list {