yyb
15 小时以前 5470429a79313630a7ddef601de1d89e7dada754
src/components/AIChatSidebar/index.vue
@@ -4,7 +4,7 @@
    <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>
          <el-icon :size="22" color="#fff"><component :is="currentAssistant.icon" /></el-icon>
        </div>
      </el-tooltip>
    </div>
@@ -356,6 +356,244 @@
                        </el-button>
                      </div>
                    </div>
                  </div>
                </div>
                <div v-if="message.salesData" class="sales-structured-card">
                  <div class="sales-structured-card__title">{{ getSalesTypeLabel(message.type) }}</div>
                  <div v-if="message.salesData.summaryEntries?.length" class="sales-summary-grid">
                    <div
                        v-for="(entry, entryIndex) in message.salesData.summaryEntries"
                        :key="`sales-summary-${entry.key}-${entryIndex}`"
                        class="sales-summary-item"
                    >
                      <span class="sales-summary-label">{{ entry.label }}</span>
                      <strong class="sales-summary-value">{{ entry.value }}</strong>
                    </div>
                  </div>
                  <div v-if="message.type === 'sales_customer_churn_risk' && message.salesData.listItems?.length" class="sales-focus-list">
                    <div
                        v-for="(item, itemIndex) in message.salesData.listItems"
                        :key="`risk-${item.customerName || itemIndex}`"
                        class="sales-focus-item"
                    >
                      <div class="sales-focus-item__head">
                        <strong>{{ formatStructuredValue(item.customerName) }}</strong>
                        <div class="sales-focus-tags">
                          <el-tag size="small" :type="getSalesLevelTagType(item.riskLevel)">
                            {{ getSalesLevelLabel(item.riskLevel, 'risk') }}
                          </el-tag>
                          <el-tag size="small" type="warning">风险分 {{ formatStructuredValue(item.riskScore) }}</el-tag>
                        </div>
                      </div>
                      <div class="sales-focus-metrics">
                        <span>待回款:{{ formatStructuredValue(item.pendingAmount) }}</span>
                        <span>待回款占比:{{ formatStructuredValue(item.pendingRate) }}</span>
                        <span>距上次下单:{{ formatStructuredValue(item.daysSinceLastOrder) }}</span>
                      </div>
                      <div v-if="toStructuredStringArray(item.riskReasons).length" class="sales-focus-reasons">
                        <el-tag
                            v-for="(reason, reasonIndex) in toStructuredStringArray(item.riskReasons)"
                            :key="`${item.customerName || itemIndex}-reason-${reasonIndex}`"
                            size="small"
                            type="danger"
                            effect="plain"
                        >
                          {{ reason }}
                        </el-tag>
                      </div>
                    </div>
                  </div>
                  <div v-if="message.type === 'sales_collection_quote_strategy' && message.salesData.listItems?.length" class="sales-focus-list">
                    <div
                        v-for="(item, itemIndex) in message.salesData.listItems"
                        :key="`strategy-${item.customerName || itemIndex}`"
                        class="sales-focus-item sales-focus-item--strategy"
                    >
                      <div class="sales-focus-item__head">
                        <strong>{{ formatStructuredValue(item.customerName) }}</strong>
                        <div class="sales-focus-tags">
                          <el-tag size="small" :type="getSalesLevelTagType(item.priority)">
                            {{ getSalesLevelLabel(item.priority, 'priority') }}
                          </el-tag>
                          <el-tag size="small" type="success">转化率 {{ formatStructuredValue(item.quoteConversionRate) }}</el-tag>
                        </div>
                      </div>
                      <div class="sales-focus-metrics">
                        <span>待回款:{{ formatStructuredValue(item.pendingAmount) }}</span>
                        <span v-if="item.nextAction">下一步:{{ formatStructuredValue(item.nextAction) }}</span>
                      </div>
                      <p v-if="item.collectionStrategy" class="sales-strategy-line">
                        <strong>回款策略:</strong>{{ formatStructuredValue(item.collectionStrategy) }}
                      </p>
                      <p v-if="item.quotationStrategy" class="sales-strategy-line">
                        <strong>报价策略:</strong>{{ formatStructuredValue(item.quotationStrategy) }}
                      </p>
                    </div>
                  </div>
                  <div
                      v-if="message.salesData.listItems?.length && message.salesData.columns?.length && !isSalesFocusType(message.type)"
                      class="table-wrapper manufacturing-table-wrapper"
                  >
                    <el-table :data="message.salesData.listItems" border stripe size="small" style="width: 100%">
                      <el-table-column
                          v-for="col in message.salesData.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.salesData.topCustomers?.length && message.salesData.topCustomerColumns?.length" class="table-wrapper manufacturing-table-wrapper">
                    <div class="sales-section-title">重点客户</div>
                    <el-table :data="message.salesData.topCustomers" border stripe size="small" style="width: 100%">
                      <el-table-column
                          v-for="col in message.salesData.topCustomerColumns"
                          :key="`top-customer-${col}`"
                          :label="getStructuredFieldLabel(col)"
                          min-width="120"
                          show-overflow-tooltip
                      >
                        <template #default="{ row }">
                          {{ formatStructuredValue(row[col]) }}
                        </template>
                      </el-table-column>
                    </el-table>
                  </div>
                  <div v-if="message.salesData.contractTrend?.length && message.salesData.contractTrendColumns?.length" class="table-wrapper manufacturing-table-wrapper">
                    <div class="sales-section-title">合同趋势</div>
                    <el-table :data="message.salesData.contractTrend" border stripe size="small" style="width: 100%">
                      <el-table-column
                          v-for="col in message.salesData.contractTrendColumns"
                          :key="`contract-trend-${col}`"
                          :label="getStructuredFieldLabel(col)"
                          min-width="120"
                          show-overflow-tooltip
                      >
                        <template #default="{ row }">
                          {{ formatStructuredValue(row[col]) }}
                        </template>
                      </el-table-column>
                    </el-table>
                  </div>
                </div>
                <div v-if="message.purchaseData" class="sales-structured-card">
                  <div class="sales-structured-card__title">{{ getPurchaseTypeLabel(message.type) }}</div>
                  <div v-if="message.purchaseData.summaryEntries?.length" class="sales-summary-grid">
                    <div
                        v-for="(entry, entryIndex) in message.purchaseData.summaryEntries"
                        :key="`purchase-summary-${entry.key}-${entryIndex}`"
                        class="sales-summary-item"
                    >
                      <span class="sales-summary-label">{{ entry.label }}</span>
                      <strong class="sales-summary-value">{{ entry.value }}</strong>
                    </div>
                  </div>
                  <div
                      v-if="message.purchaseData.listItems?.length && message.purchaseData.columns?.length"
                      class="table-wrapper manufacturing-table-wrapper"
                  >
                    <el-table :data="message.purchaseData.listItems" border stripe size="small" style="width: 100%">
                      <el-table-column
                          v-for="col in message.purchaseData.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>
                <div v-if="message.financeData" class="sales-structured-card">
                  <div class="sales-structured-card__title">{{ getFinancialTypeLabel(message.type) }}</div>
                  <div v-if="message.financeData.summaryEntries?.length" class="sales-summary-grid">
                    <div
                        v-for="(entry, entryIndex) in message.financeData.summaryEntries"
                        :key="`finance-summary-${entry.key}-${entryIndex}`"
                        class="sales-summary-item"
                    >
                      <span class="sales-summary-label">{{ entry.label }}</span>
                      <strong class="sales-summary-value">{{ entry.value }}</strong>
                    </div>
                  </div>
                  <div v-if="message.financeData.headline" class="finance-headline">
                    {{ message.financeData.headline }}
                  </div>
                  <div v-if="message.financeData.conclusions?.length" class="finance-text-section">
                    <div class="sales-section-title">核心结论</div>
                    <ul>
                      <li v-for="(item, idx) in message.financeData.conclusions" :key="`finance-conclusion-${idx}`">
                        {{ item }}
                      </li>
                    </ul>
                  </div>
                  <div v-if="message.financeData.riskSuggestions?.length" class="finance-text-section">
                    <div class="sales-section-title">风险建议</div>
                    <ul>
                      <li v-for="(item, idx) in message.financeData.riskSuggestions" :key="`finance-risk-${idx}`">
                        {{ item }}
                      </li>
                    </ul>
                  </div>
                  <div
                      v-for="section in message.financeData.sections"
                      :key="`finance-section-${section.key}`"
                      class="table-wrapper manufacturing-table-wrapper"
                  >
                    <div class="sales-section-title">{{ section.title }}</div>
                    <el-table :data="section.items" border stripe size="small" style="width: 100%">
                      <el-table-column
                          v-for="col in section.columns"
                          :key="`finance-${section.key}-${col}`"
                          :label="getStructuredFieldLabel(col)"
                          min-width="120"
                          show-overflow-tooltip
                      >
                        <template #default="{ row }">
                          {{ formatStructuredValue(row[col]) }}
                        </template>
                      </el-table-column>
                    </el-table>
                  </div>
                </div>
                <div v-if="message.purchaseIntentData?.quickPrompts?.length" class="purchase-intent-quick-prompt-wrap">
                  <div class="purchase-intent-quick-prompt-title">试试以下提问</div>
                  <div class="quick-prompt-list purchase-intent-quick-prompt-list">
                    <button
                        v-for="prompt in message.purchaseIntentData.quickPrompts"
                        :key="`purchase-intent-${prompt}`"
                        type="button"
                        class="quick-prompt-btn"
                        :disabled="isSending"
                        @click="sendQuickPrompt(prompt)"
                    >
                      {{ prompt }}
                    </button>
                  </div>
                </div>
@@ -750,6 +988,56 @@
  purchase_return_order: '采购退货单',
  unknown: '未知采购业务'
}
const salesStructuredTypeSet = new Set([
  'sales_customer_profile_list',
  'sales_quotation_list',
  'sales_ledger_list',
  'sales_return_list',
  'sales_customer_interaction_list',
  'sales_shipping_list',
  'sales_dashboard',
  'sales_customer_churn_risk',
  'sales_collection_quote_strategy'
])
const salesFocusTypeSet = new Set([
  'sales_customer_churn_risk',
  'sales_collection_quote_strategy'
])
const salesTypeLabelMap = {
  sales_customer_profile_list: '客户档案',
  sales_quotation_list: '销售报价',
  sales_ledger_list: '销售台账',
  sales_return_list: '销售退货',
  sales_customer_interaction_list: '客户往来',
  sales_shipping_list: '发货台账',
  sales_dashboard: '销售指标统计',
  sales_customer_churn_risk: '客户流失风险分析',
  sales_collection_quote_strategy: '回款与报价策略建议'
}
const purchaseTypeLabelMap = {
  purchase_material_rank: '采购物料金额排行',
  purchase_pending_payment_list: '待付款采购单'
}
const financialStructuredTypeSet = new Set([
  'financial_cost_accounting',
  'financial_order_profit_analysis',
  'financial_inventory_capital_analysis',
  'financial_cashflow_forecast',
  'financial_business_anomaly_warning',
  'financial_business_cockpit',
  'financial_operation_report',
  'financial_rag_knowledge'
])
const financialTypeLabelMap = {
  financial_cost_accounting: '成本核算',
  financial_order_profit_analysis: '订单利润分析',
  financial_inventory_capital_analysis: '库存资金分析',
  financial_cashflow_forecast: '现金流预测',
  financial_business_anomaly_warning: '经营异常预警',
  financial_business_cockpit: '经营驾驶舱',
  financial_operation_report: '经营报告',
  financial_rag_knowledge: '财务知识检索'
}
const manufacturingStructuredTypeSet = new Set([
  'manufacturing_site_snapshot',
  'manufacturing_plan_list',
@@ -819,6 +1107,54 @@
  materialName: '物料名称',
  stockQty: '库存量'
}
Object.assign(structuredFieldLabelMap, {
  customerName: '客户名称',
  riskLevel: '风险等级',
  riskScore: '风险评分',
  riskReasons: '风险原因',
  pendingAmount: '待回款金额',
  pendingRate: '待回款占比',
  daysSinceLastOrder: '距上次下单天数',
  priority: '优先级',
  quoteConversionRate: '报价转化率',
  collectionStrategy: '回款策略',
  quotationStrategy: '报价策略',
  nextAction: '下一步动作',
  salesContractNo: '销售合同号',
  revenue: '收入',
  materialCost: '材料成本',
  laborCost: '人工成本',
  depreciationCost: '折旧成本',
  scrapCost: '报废成本',
  totalCost: '总成本',
  profit: '利润',
  profitRate: '利润率',
  reasons: '原因',
  suggestion: '建议',
  productName: '产品名称',
  model: '型号',
  quantity: '数量',
  inventoryValue: '库存资金',
  stagnantDays: '呆滞天数',
  overstock: '是否超储',
  month: '月份',
  income: '收入',
  expense: '支出',
  netFlow: '净现金流',
  message: '预警信息',
  headline: '报告标题',
  conclusions: '核心结论',
  riskSuggestions: '风险建议',
  orderProfitTop: '利润Top',
  contractAmountTotal: '合同总额',
  receivedAmountTotal: '已回款金额',
  pendingAmountTotal: '待回款总额',
  shipRate: '发货率',
  pendingOrderCount: '待付款订单数',
  totalContractAmount: '待付款合同总额',
  totalPaidAmount: '已付款总额',
  totalPendingAmount: '待付款总额'
})
const purchasePayloadFieldLabelMap = {
  purchaseLedgers: '采购台账',
  productData: '产品明细',
@@ -1203,6 +1539,147 @@
const getManufacturingTypeLabel = (type = '') => manufacturingTypeLabelMap[String(type || '')] || '制造结果'
const inferSalesColumns = (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 normalizeSalesListItems = (items) => {
  if (!Array.isArray(items)) return []
  return items.filter(item => isPlainObject(item))
}
const buildSalesStructuredData = (parsedData) => {
  const type = String(parsedData?.type || '')
  if (!salesStructuredTypeSet.has(type)) return null
  const rawData = isPlainObject(parsedData?.data) ? parsedData.data : {}
  const listItems = normalizeSalesListItems(rawData.items)
  const topCustomers = normalizeSalesListItems(rawData.topCustomers)
  const contractTrend = normalizeSalesListItems(rawData.contractTrend)
  return {
    type,
    summaryEntries: normalizeManufacturingSummaryEntries(parsedData?.summary),
    listItems,
    columns: inferSalesColumns(listItems),
    topCustomers,
    topCustomerColumns: inferSalesColumns(topCustomers),
    contractTrend,
    contractTrendColumns: inferSalesColumns(contractTrend)
  }
}
const getSalesTypeLabel = (type = '') => salesTypeLabelMap[String(type || '')] || '销售查询结果'
const buildPurchaseStructuredData = (parsedData) => {
  if (parsedData?.success !== true) return null
  const type = String(parsedData?.type || '')
  if (!type.startsWith('purchase_')) return null
  const rawData = isPlainObject(parsedData?.data) ? parsedData.data : {}
  const listItems = normalizeSalesListItems(rawData.items)
  return {
    type,
    summaryEntries: normalizeManufacturingSummaryEntries(parsedData?.summary),
    listItems,
    columns: inferSalesColumns(listItems)
  }
}
const getPurchaseTypeLabel = (type = '') => purchaseTypeLabelMap[String(type || '')] || '采购查询结果'
const financialDataSectionConfig = [
  { key: 'orders', title: '订单明细' },
  { key: 'items', title: '明细列表' },
  { key: 'actualMonthly', title: '历史现金流' },
  { key: 'forecastMonthly', title: '预测现金流' },
  { key: 'receivableRiskTop', title: '应收风险Top' },
  { key: 'payablePressureTop', title: '应付压力Top' },
  { key: 'orderProfitTop', title: '利润Top' }
]
const buildFinancialStructuredSections = (rawData = {}) => {
  const sections = []
  financialDataSectionConfig.forEach((section) => {
    const items = normalizeSalesListItems(rawData?.[section.key])
    if (!items.length) return
    sections.push({
      key: section.key,
      title: section.title,
      items,
      columns: inferSalesColumns(items)
    })
  })
  return sections
}
const buildFinancialStructuredData = (parsedData) => {
  const type = String(parsedData?.type || '')
  if (!financialStructuredTypeSet.has(type) || parsedData?.success !== true) return null
  const rawData = isPlainObject(parsedData?.data) ? parsedData.data : {}
  const headline = String(rawData?.headline || '').trim()
  return {
    type,
    summaryEntries: normalizeManufacturingSummaryEntries(parsedData?.summary),
    headline,
    conclusions: toStructuredStringArray(rawData?.conclusions),
    riskSuggestions: toStructuredStringArray(rawData?.riskSuggestions),
    sections: buildFinancialStructuredSections(rawData)
  }
}
const getFinancialTypeLabel = (type = '') => financialTypeLabelMap[String(type || '')] || '财务查询结果'
const isSalesFocusType = (type = '') => salesFocusTypeSet.has(String(type || ''))
const getSalesLevelTagType = (level = '') => {
  const normalizedLevel = String(level || '').toLowerCase()
  if (normalizedLevel === 'high') return 'danger'
  if (normalizedLevel === 'medium') return 'warning'
  if (normalizedLevel === 'low') return 'success'
  return 'info'
}
const getSalesLevelLabel = (level = '', mode = 'risk') => {
  const normalizedLevel = String(level || '').toLowerCase()
  const suffix = mode === 'priority' ? '优先级' : '风险'
  if (normalizedLevel === 'high') return `高${suffix}`
  if (normalizedLevel === 'medium') return `中${suffix}`
  if (normalizedLevel === 'low') return `低${suffix}`
  if (!normalizedLevel) return mode === 'priority' ? '未分级' : '未评估'
  return normalizedLevel.toUpperCase()
}
const toStructuredStringArray = (value) => {
  if (Array.isArray(value)) {
    return value.map(item => String(item || '').trim()).filter(Boolean)
  }
  if (typeof value === 'string') {
    return value
      .split(/[,\uFF0C\u3001;\uFF1B\n]/)
      .map(item => item.trim())
      .filter(Boolean)
  }
  return []
}
const normalizePurchaseIntentNotRecognizedData = (parsedData) => {
  if (String(parsedData?.type || '') !== 'purchase_intent_not_recognized') return null
  const quickPrompts = toStructuredStringArray(parsedData?.data?.quickPrompts)
  if (!quickPrompts.length) return null
  return { quickPrompts }
}
const getManufacturingWarningLevelType = (level = '') => {
  const normalizedLevel = String(level || '').toLowerCase()
  if (normalizedLevel === 'high') return 'danger'
@@ -1581,6 +2058,10 @@
          payloadHiddenData: null,
          purchaseAnalysisData: null,
          manufacturingData: null,
          salesData: null,
          purchaseData: null,
          purchaseIntentData: null,
          financeData: null,
          localUploadFiles: isUser ? mapHistoryFilePathsToSnapshots(msg.filePaths, uuid.value, idx) : []
        }
@@ -1799,7 +2280,7 @@
          const candidate = text.slice(i, j + 1)
          try {
            const parsed = JSON.parse(candidate)
            if (parsed?.success === true) {
            if (typeof parsed?.success === 'boolean') {
              return {
                data: parsed,
                startIdx: i,
@@ -1818,16 +2299,33 @@
}
const applyStructuredMessageData = (messageObj, parsedData, msgIndex, shouldRenderCharts = true) => {
  if (!messageObj || !parsedData?.success) return
  const isPurchaseIntentNotRecognized = String(parsedData?.type || '') === 'purchase_intent_not_recognized'
  if (!messageObj || (parsedData?.success !== true && !isPurchaseIntentNotRecognized)) return
  const previousManufacturingData = messageObj.manufacturingData
  messageObj.type = parsedData.type || ''
  messageObj.tableData = null
  messageObj.purchaseAnalysisData = null
  messageObj.manufacturingData = null
  messageObj.salesData = null
  messageObj.purchaseData = null
  messageObj.purchaseIntentData = null
  messageObj.financeData = null
  if (isPurchaseIntentNotRecognized) {
    messageObj.purchaseIntentData = normalizePurchaseIntentNotRecognizedData(parsedData)
    messageObj.chartOptions = null
    messageObj.chartRenderReady = false
    return
  }
  if (messageObj.type === 'todo_list' && parsedData.data) {
    messageObj.tableData = parsedData.data
  }
  const salesData = buildSalesStructuredData(parsedData)
  if (salesData) {
    messageObj.salesData = salesData
  }
  const manufacturingData = buildManufacturingStructuredData(parsedData, previousManufacturingData)
@@ -1835,10 +2333,23 @@
    messageObj.manufacturingData = manufacturingData
  }
  const purchaseData = buildPurchaseStructuredData(parsedData)
  if (purchaseData) {
    messageObj.purchaseData = purchaseData
  }
  const financeData = buildFinancialStructuredData(parsedData)
  if (financeData) {
    messageObj.financeData = financeData
  }
  if (parsedData.action === 'confirm_required' && parsedData.businessType) {
    messageObj.type = 'purchase_analysis_confirm'
    messageObj.purchaseAnalysisData = parsedData
    messageObj.manufacturingData = null
    messageObj.salesData = null
    messageObj.purchaseData = null
    messageObj.financeData = null
    if (!Array.isArray(messageObj.payloadTreeData) || !messageObj.payloadTreeData.length) {
      initializePurchasePayloadTree(messageObj, parsedData.payload || {})
    }
@@ -1883,12 +2394,21 @@
const getStructuredFallbackText = (parsedData) => {
  if (!parsedData) return '正在为您展示分析结果...'
  if (parsedData.type === 'todo_list') return '已为您整理好相关数据。'
  if (parsedData.type === 'purchase_intent_not_recognized') return '未识别到可执行的采购查询条件,请补充查询条件后再试。'
  if (salesStructuredTypeSet.has(parsedData.type)) {
    if (parsedData.type === 'sales_customer_churn_risk') return '已为您生成客户流失风险分析。'
    if (parsedData.type === 'sales_collection_quote_strategy') return '已为您生成回款与报价策略建议。'
    if (parsedData.type === 'sales_dashboard') return '已为您生成销售指标统计。'
    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 (financialStructuredTypeSet.has(parsedData.type)) return '已返回财务分析结果。'
  if (String(parsedData.type || '').startsWith('purchase_')) return '已返回采购查询结果。'
  if (parsedData.charts && Object.keys(parsedData.charts).length > 0) return '已为您生成分析图表。'
  return '正在为您展示分析结果...'
}
@@ -3018,7 +3538,11 @@
    payloadTreeData: null,
    payloadHiddenData: null,
    purchaseAnalysisData: null,
    manufacturingData: null
    manufacturingData: null,
    salesData: null,
    purchaseData: null,
    purchaseIntentData: null,
    financeData: null
  })
  outputState.value[botMsgIndex] = {
@@ -3143,7 +3667,11 @@
    payloadTreeData: null,
    payloadHiddenData: null,
    purchaseAnalysisData: null,
    manufacturingData: null
    manufacturingData: null,
    salesData: null,
    purchaseData: null,
    purchaseIntentData: null,
    financeData: null
  }
  messages.value.push(botMsg)
@@ -3175,28 +3703,6 @@
          const extracted = extractEmbeddedSuccessJson(fullText)
          if (extracted) {
            applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex)
          } else {
            const extractJson = (text) => {
              const startIdx = text.indexOf('{"success": true')
              if (startIdx === -1) return null
              // 从后往前找最后一个 '}'
              const lastBraceIdx = text.lastIndexOf('}')
              if (lastBraceIdx === -1 || lastBraceIdx < startIdx) return null
              const potentialJson = text.substring(startIdx, lastBraceIdx + 1)
              try {
                return JSON.parse(potentialJson)
              } catch (err) {
                return null
              }
            }
            const parsedData = extractJson(fullText)
            if (parsedData) {
              applyStructuredMessageData(currentMsg, parsedData, botMsgIndex, true)
            }
          }
          updateOutputState(fullText, botMsgIndex)
@@ -3214,24 +3720,6 @@
    const extracted = extractEmbeddedSuccessJson(currentMsg.content)
    if (extracted) {
      applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex)
    } else {
      const extractJson = (text) => {
        const startIdx = text.indexOf('{"success": true')
        if (startIdx === -1) return null
        const lastBraceIdx = text.lastIndexOf('}')
        if (lastBraceIdx === -1 || lastBraceIdx < startIdx) return null
        const potentialJson = text.substring(startIdx, lastBraceIdx + 1)
        try {
          return JSON.parse(potentialJson)
        } catch (err) {
          return null
        }
      }
      const finalParsed = extractJson(currentMsg.content)
      if (finalParsed) {
        applyStructuredMessageData(currentMsg, finalParsed, botMsgIndex)
      }
    }
  }).catch(err => {
    if (err.name === 'CanceledError' || err.name === 'AbortError') {
@@ -3589,30 +4077,30 @@
.ai-chat-trigger {
  pointer-events: auto;
  position: fixed;
  right: 24px;
  bottom: 100px;
  width: 56px;
  height: 56px;
  right: 10px;
  bottom: 12px;
  width: 40px;
  height: 40px;
  background: $gradient-dark;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: $shadow-deep, 0 0 0 2px rgba(0, 85, 212, 0.3) inset, 0 0 30px rgba(0, 119, 232, 0.2);
  transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
  z-index: 2001;
  animation: triggerPulse 3s ease-in-out infinite;
  box-shadow: 0 8px 18px rgba(0, 68, 170, 0.32), 0 0 0 1px rgba(0, 136, 232, 0.32) inset;
  transition: transform 0.22s ease, box-shadow 0.22s ease, opacity 0.22s ease;
  z-index: 1020;
  opacity: 0.9;
  &::before {
    content: '';
    position: absolute;
    inset: -6px;
    inset: -4px;
    background: linear-gradient(135deg, rgba(0, 85, 212, 0.4), rgba(0, 136, 232, 0.3), rgba(90, 159, 224, 0.2));
    border-radius: 50%;
    z-index: -1;
    filter: blur(16px);
    animation: glowPulse 2s ease-in-out infinite alternate;
    filter: blur(10px);
    opacity: 0.35;
  }
  &::after {
@@ -3625,16 +4113,12 @@
  }
  &:hover {
    transform: scale(1.12) translateY(-4px);
    box-shadow: $shadow-deep, 0 0 0 3px rgba(0, 136, 232, 0.4) inset, 0 0 50px rgba(0, 136, 232, 0.3);
    &::before {
      animation: glowPulse 1s ease-in-out infinite alternate;
    }
    transform: scale(1.05) translateY(-1px);
    box-shadow: 0 10px 22px rgba(0, 68, 170, 0.38), 0 0 0 1px rgba(0, 136, 232, 0.42) inset;
    .trigger-icon {
      transform: rotate(-8deg) scale(1.05);
      filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.5));
      transform: scale(1.03);
      filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.42));
    }
  }
@@ -4519,6 +5003,151 @@
  }
}
.sales-structured-card {
  margin-top: 12px;
  width: 100%;
  background: #fff;
  border: 1px solid rgba(31, 122, 114, 0.2);
  border-radius: 12px;
  box-shadow: $shadow-card;
  padding: 14px;
}
.sales-structured-card__title {
  font-size: 14px;
  font-weight: 700;
  color: #1f5ddf;
  margin-bottom: 10px;
}
.sales-summary-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 8px;
  margin-bottom: 12px;
}
.sales-summary-item {
  border-radius: 10px;
  padding: 10px 12px;
  border: 1px solid rgba(30, 91, 255, 0.12);
  background: linear-gradient(180deg, #f7fbff, #edf6ff);
  min-height: 66px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 6px;
}
.sales-summary-label {
  font-size: 12px;
  color: #4b5563;
}
.sales-summary-value {
  font-size: 15px;
  color: #1f2937;
  line-height: 1.4;
  word-break: break-all;
}
.sales-focus-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.sales-focus-item {
  border-radius: 10px;
  border: 1px solid rgba(30, 91, 255, 0.14);
  background: #f8fbff;
  padding: 10px 12px;
}
.sales-focus-item--strategy {
  border-color: rgba(31, 122, 114, 0.22);
  background: linear-gradient(180deg, #f7fcfb, #edf9f6);
}
.sales-focus-item__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
  font-size: 13px;
  color: #1f2937;
}
.sales-focus-tags {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  justify-content: flex-end;
}
.sales-focus-metrics {
  margin-top: 8px;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  font-size: 12px;
  color: #475467;
}
.sales-focus-reasons {
  margin-top: 8px;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.sales-strategy-line {
  margin: 8px 0 0;
  font-size: 12px;
  line-height: 1.6;
  color: #334155;
}
.sales-section-title {
  margin: 4px 0 8px;
  font-size: 13px;
  font-weight: 700;
  color: $deep-blue;
}
.finance-headline {
  margin-top: 4px;
  font-size: 13px;
  line-height: 1.7;
  color: #344054;
}
.finance-text-section {
  margin-top: 10px;
  ul {
    margin: 6px 0 0;
    padding-left: 18px;
    font-size: 13px;
    line-height: 1.7;
    color: #344054;
  }
}
.purchase-intent-quick-prompt-wrap {
  margin-top: 12px;
}
.purchase-intent-quick-prompt-title {
  font-size: 12px;
  color: #4b5563;
}
.purchase-intent-quick-prompt-list {
  margin-top: 8px;
}
.purchase-confirm-card {
  margin-top: 12px;
  width: 100%;