From 6faf65a6a7e72b7ecff52355f798fd00a516ba77 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期四, 30 四月 2026 17:37:13 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_NEW_pro' into dev_NEW_pro

---
 src/components/AIChatSidebar/index.vue | 3465 ++++++++++++++++++++++++++++++++++++++++++++++++++++-------
 1 files changed, 3,034 insertions(+), 431 deletions(-)

diff --git a/src/components/AIChatSidebar/index.vue b/src/components/AIChatSidebar/index.vue
index d14978c..3bc2b8f 100644
--- a/src/components/AIChatSidebar/index.vue
+++ b/src/components/AIChatSidebar/index.vue
@@ -2,46 +2,64 @@
   <div class="ai-chat-sidebar-wrapper">
     <!-- 鎮诞鍥炬爣 -->
     <div class="ai-chat-trigger" @click="toggleSidebar" v-show="!visible">
-      <el-tooltip content="AI 鍔╂墜" placement="left">
+      <el-tooltip :content="currentAssistant.tooltip" placement="left">
         <div class="trigger-icon">
-          <el-icon :size="30" color="#fff"><Cpu /></el-icon>
+          <el-icon :size="30" color="#fff"><component :is="currentAssistant.icon" /></el-icon>
         </div>
       </el-tooltip>
     </div>
 
     <!-- 渚ц竟鏍忓璇濇 -->
     <el-drawer
-      v-model="visible"
-      :size="drawerSize"
-      direction="rtl"
-      :with-header="true"
-      class="ai-chat-drawer"
-      :modal="false"
-      :show-close="true"
-      :append-to-body="false"
-      @close="handleClose"
+        v-model="visible"
+        :size="drawerSize"
+        direction="rtl"
+        :with-header="true"
+        class="ai-chat-drawer"
+        :modal="false"
+        modal-class="ai-chat-overlay"
+        :show-close="false"
+        :append-to-body="false"
+        @close="handleClose"
     >
       <template #header>
         <div class="drawer-header">
           <div class="header-left">
-            <el-icon :size="20" class="header-icon"><Cpu /></el-icon>
-            <span class="title">AI 鏅鸿兘鍔╂墜</span>
+            <el-icon :size="20" class="header-icon"><component :is="currentAssistant.icon" /></el-icon>
+            <span class="title">{{ currentAssistant.title }}</span>
+          </div>
+          <div v-if="showAssistantSwitch" class="assistant-switcher">
+            <el-radio-group v-model="selectedAssistantKey" size="small">
+              <el-radio-button
+                  v-for="assistant in assistants"
+                  :key="assistant.key"
+                  :label="assistant.key"
+              >
+                {{ assistant.label }}
+              </el-radio-button>
+            </el-radio-group>
           </div>
           <div class="header-actions">
             <el-tooltip content="浼氳瘽鍘嗗彶" placement="bottom">
-              <el-button link @click="toggleHistory">
+              <el-button link class="header-action-btn" @click="handleToggleHistory">
                 <el-icon :size="18"><Timer /></el-icon>
               </el-button>
             </el-tooltip>
             <el-tooltip content="寮�鍚柊浼氳瘽" placement="bottom">
-              <el-button link @click="newChat">
+              <el-button link class="header-action-btn" @click="handleNewChat">
                 <el-icon :size="18"><Plus /></el-icon>
+              </el-button>
+            </el-tooltip>
+            <div class="action-divider"></div>
+            <el-tooltip content="鍏抽棴" placement="bottom">
+              <el-button link class="header-action-btn close-btn" @click="handleManualClose">
+                <el-icon :size="18"><Close /></el-icon>
               </el-button>
             </el-tooltip>
           </div>
         </div>
       </template>
-      
+
       <div class="chat-container">
         <!-- 鍘嗗彶浼氳瘽鍒楄〃 -->
         <div v-if="showHistory" class="history-panel">
@@ -56,124 +74,273 @@
               </div>
             </template>
             <div class="session-list">
-              <div 
-                v-for="session in sessions" 
-                :key="session.memoryId"
-                :class="['session-item', { active: uuid === session.memoryId }]"
-                @click="selectSession(session)"
+              <div
+                  v-for="session in sessions"
+                  :key="session.memoryId"
+                  :class="['session-item', { active: uuid === session.memoryId }]"
+                  @click="selectSession(session)"
               >
                 <el-icon><ChatDotSquare /></el-icon>
                 <span class="session-name" :title="session.lastMessage || '鏂颁細璇�'">
                   {{ session.lastMessage || '鏂颁細璇�' }}
                 </span>
-                <el-button 
-                  link 
-                  type="danger" 
-                  class="delete-btn"
-                  @click.stop="handleDeleteSession(session.memoryId)"
+                <el-button
+                    link
+                    type="danger"
+                    class="delete-btn"
+                    @click.stop="handleDeleteSession(session.memoryId)"
                 >
                   <el-icon><Delete /></el-icon>
                 </el-button>
               </div>
-              <el-empty v-if="sessions.length === 0" description="鏆傛棤鍘嗗彶浼氳瘽" />
+              <el-empty v-if="sessions.length === 0" :description="currentAssistant.emptySessionText" />
             </div>
           </el-skeleton>
         </div>
 
         <div v-else class="chat-main">
+          <div :class="['chat-hero', { compact: hasMessages }]">
+            <div :class="['assistant-stand', { thinking: isSending, compact: hasMessages }]">
+              <div class="assistant-halo"></div>
+              <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>
+                  <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">
+                <span class="assistant-status-dot"></span>
+                {{ isSending ? '鎬濊�冧腑...' : currentAssistant.label }}
+              </div>
+              <div class="assistant-base assistant-base-lg"></div>
+              <div class="assistant-base assistant-base-md"></div>
+              <div class="assistant-base assistant-base-sm"></div>
+            </div>
+
+            <div :class="['welcome-card', { compact: hasMessages }]">
+              <div class="welcome-eyebrow">鏅鸿兘鍔╂墜</div>
+              <h3 class="welcome-title">
+                鎮ㄥソ
+                <br />
+                鎴戞槸{{ currentAssistant.label }}鍒嗘瀽瑙h鍔╂墜
+              </h3>
+              <p class="welcome-desc">
+                {{ currentAssistant.description || '鎴戝彲浠ュ洿缁曚笟鍔¢棶棰樻彁渚涜В璇汇�佹煡璇㈠缓璁拰鍒嗘瀽鏀寔锛屽府鍔╀綘鏇村揩瀹屾垚鍒ゆ柇涓庡鐞嗐��' }}
+              </p>
+
+              <div class="quick-prompt-list">
+                <button
+                    v-for="prompt in displayedQuickPrompts"
+                    :key="prompt"
+                    type="button"
+                    class="quick-prompt-btn"
+                    :disabled="isSending"
+                    @click="sendQuickPrompt(prompt)"
+                >
+                  {{ prompt }}
+                </button>
+              </div>
+
+              <button
+                  v-if="quickPrompts.length > quickPromptLimit"
+                  type="button"
+                  class="more-prompts-btn"
+                  @click="refreshQuickPrompts"
+              >
+                <el-icon><RefreshRight /></el-icon>
+                <span>鎹竴鎹�</span>
+              </button>
+            </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"
-            :key="index"
-            :class="['message-item', message.isUser ? 'user-message' : 'bot-message']"
-          >
-            <div class="avatar">
-              <el-icon v-if="message.isUser"><User /></el-icon>
-              <el-icon v-else><Cpu /></el-icon>
-            </div>
-            <div class="message-content">
-              <!-- 鏂囨湰鍐呭 -->
-              <div class="text-box" v-html="message.htmlContent"></div>
-
-              <!-- 鍥捐〃鍐呭 -->
-              <div v-if="message.chartOptions && message.chartRenderReady" class="charts-wrapper">
-                <div
-                  v-for="(option, key) in message.chartOptions"
-                  :key="key"
-                  class="chart-item"
-                  :id="`ai-chart-${index}-${key}`"
-                ></div>
+            <div
+                v-for="(message, index) in messages"
+                :key="index"
+                :class="['message-item', message.isUser ? 'user-message' : 'bot-message']"
+            >
+              <div class="avatar">
+                <el-icon v-if="message.isUser"><User /></el-icon>
+                <el-icon v-else><Cpu /></el-icon>
               </div>
+              <div class="message-content">
+                <!-- 鏂囨湰鍐呭 -->
+                <div class="text-box" v-html="message.htmlContent"></div>
 
-              <!-- 琛ㄦ牸鍐呭 -->
-              <div v-if="message.type === 'todo_list' && message.tableData" class="table-wrapper">
-                <el-table :data="message.tableData.items" border stripe size="small" style="width: 100%">
-                  <el-table-column
-                    v-for="col in message.tableData.columns"
-                    :key="col"
-                    :prop="col"
-                    :label="columnLabelMap[col] || col"
-                    min-width="100"
-                    show-overflow-tooltip
+                <!-- 鍥捐〃鍐呭 -->
+                <div v-if="message.chartOptions && message.chartRenderReady" class="charts-wrapper">
+                  <div
+                      v-for="(option, key) in message.chartOptions"
+                      :key="key"
+                      class="chart-item"
+                      :id="`ai-chart-${index}-${key}`"
+                  ></div>
+                </div>
+
+                <!-- 琛ㄦ牸鍐呭 -->
+                <div v-if="message.type === 'todo_list' && message.tableData" class="table-wrapper">
+                  <el-table :data="message.tableData.items" border stripe size="small" style="width: 100%">
+                    <el-table-column
+                        v-for="col in message.tableData.columns"
+                        :key="col"
+                        :prop="col"
+                        :label="columnLabelMap[col] || col"
+                        min-width="100"
+                        show-overflow-tooltip
+                    />
+                  </el-table>
+                </div>
+
+                <!-- 鎵撳瓧涓姩鐢� -->
+                <div v-if="message.purchaseAnalysisData" class="purchase-confirm-card">
+                  <div class="purchase-confirm-header">
+                    <span>{{ businessTypeLabelMap[message.purchaseAnalysisData.businessType] || message.purchaseAnalysisData.businessType || '閲囪喘涓氬姟' }}</span>
+                    <el-tag size="small" type="success" v-if="message.purchaseAnalysisData.confidence !== undefined">
+                      缃俊搴� {{ formatPercent(message.purchaseAnalysisData.confidence) }}
+                    </el-tag>
+                  </div>
+                  <div class="purchase-confirm-desc">
+                    {{ getPurchaseConfirmDescription(message.purchaseAnalysisData) }}
+                  </div>
+                  <div v-if="isPurchasePayloadEmpty(message.purchaseAnalysisData.payload)" class="purchase-empty-state">
+                    <div class="empty-title">娌℃湁璇嗗埆鍒板彲鐩存帴鎻愪氦鐨勯噰璐彴璐︿俊鎭�</div>
+                    <div class="empty-desc">褰撳墠鏂囦欢閲岀己灏戦噰璐悎鍚屽彿銆佷緵搴斿晢銆侀」鐩�佹棩鏈熴�佺墿鏂欐槑缁嗙瓑鍏抽敭鍐呭銆傝涓婁紶鏇村畬鏁寸殑鍚堝悓銆佽鍗曟垨鏄庣粏琛紝鎴栧湪涓嬫柟琛ュ厖鏁版嵁鍚庡啀纭銆�</div>
+                  </div>
+                  <div v-if="message.purchaseAnalysisData.warnings?.length" class="purchase-alert warning">
+                    <strong>椋庨櫓鎻愮ず</strong>
+                    <ul>
+                      <li v-for="(warning, warningIndex) in message.purchaseAnalysisData.warnings" :key="warningIndex">
+                        {{ formatPreviewItem(warning) }}
+                      </li>
+                    </ul>
+                  </div>
+                  <div v-if="getVisiblePurchaseMissingFields(message.purchaseAnalysisData).length" class="purchase-alert missing">
+                    <strong>闇�瑕佽ˉ鍏� {{ getVisiblePurchaseMissingFields(message.purchaseAnalysisData).length }} 椤�</strong>
+                    <el-tag
+                        v-for="field in getVisiblePurchaseMissingFields(message.purchaseAnalysisData)"
+                        :key="field"
+                        size="small"
+                        type="danger"
+                    >
+                      {{ field }}
+                    </el-tag>
+                  </div>
+                  <div v-if="message.purchaseAnalysisData.preview?.length" class="purchase-preview">
+                    <div class="purchase-section-title">纭鎽樿</div>
+                    <ul>
+                      <li v-for="(item, previewIndex) in message.purchaseAnalysisData.preview" :key="previewIndex">
+                        {{ formatPreviewItem(item) }}
+                      </li>
+                    </ul>
+                  </div>
+                  <div class="purchase-section-title">琛ュ厖鎴栫‘璁ゆ暟鎹�</div>
+                  <el-input
+                      v-model="message.payloadText"
+                      type="textarea"
+                      :rows="8"
+                      resize="vertical"
+                      spellcheck="false"
+                      class="payload-editor"
                   />
-                </el-table>
-              </div>
+                  <div class="payload-editor-tip">
+                    鏃ユ湡璇峰~鍐� yyyy-MM-dd锛屼緥濡� 2026-04-30銆備骇鍝佹槑缁嗗缓璁斁鍦ㄦ瘡鏉¢噰璐彴璐︾殑 productData 涓紝纭鏃朵細鑷姩鍏煎鏃ф牸寮忓苟娓呯悊瀹℃壒瀛楁銆�
+                  </div>
+                  <div class="purchase-confirm-actions">
+                    <span v-if="message.confirmResult" :class="['confirm-result', message.confirmed ? 'success' : 'error']">
+                      {{ message.confirmResult }}
+                    </span>
+                    <el-button
+                        type="primary"
+                        size="small"
+                        :loading="message.confirming"
+                        :disabled="message.confirmed || isSending"
+                        @click="confirmPurchaseAnalysis(message)"
+                    >
+                      纭骞舵墽琛�
+                    </el-button>
+                  </div>
+                </div>
 
-              <!-- 鎵撳瓧涓姩鐢� -->
-              <div v-if="message.isTyping" class="typing-indicator">
-                <span class="dot"></span>
-                <span class="dot"></span>
-                <span class="dot"></span>
+                <div v-if="message.isTyping" class="typing-indicator">
+                  <span class="dot"></span>
+                  <span class="dot"></span>
+                  <span class="dot"></span>
+                </div>
               </div>
             </div>
           </div>
-        </div>
 
-        <div class="input-area">
-          <div class="input-actions">
-            <el-button link type="primary" size="small" @click="newChat">
-              <el-icon><Plus /></el-icon>鏂颁細璇�
-            </el-button>
-            <el-button v-if="isSending" link type="danger" size="small" @click="stopGeneration">
-              <el-icon><VideoPause /></el-icon>鍋滄鐢熸垚
-            </el-button>
-            <el-upload
-              class="file-upload-trigger"
-              action="#"
-              :auto-upload="false"
-              :show-file-list="false"
-              :on-change="handleFileChange"
-              :disabled="isSending"
-            >
-              <el-button link type="primary" size="small" :disabled="isSending">
-                <el-icon><Upload /></el-icon>鍒嗘瀽鏂囦欢
+          <div class="input-area">
+            <div class="input-actions">
+              <el-button link class="utility-action-btn" type="primary" size="small" @click="handleNewChat">
+                <el-icon><Plus /></el-icon>鏂颁細璇�
               </el-button>
-            </el-upload>
-          </div>
-          <div class="input-box">
-            <div v-if="selectedFile" class="selected-file-tag">
-              <el-icon><Document /></el-icon>
-              <span class="file-name">{{ selectedFile.name }}</span>
-              <el-icon class="remove-file" @click="removeSelectedFile"><Close /></el-icon>
+              <el-button v-if="isSending" link class="utility-action-btn stop-action-btn" type="danger" size="small" @click="stopGeneration">
+                <el-icon><VideoPause /></el-icon>鍋滄鐢熸垚
+              </el-button>
+              <el-upload
+                  v-if="currentAssistant.allowFileUpload"
+                  class="file-upload-trigger"
+                  action="#"
+                  :auto-upload="false"
+                  :show-file-list="false"
+                  v-model:file-list="uploadFileList"
+                  :multiple="currentAssistant.allowMultipleFileUpload"
+                  :on-change="handleFileChange"
+                  :disabled="isSending"
+              >
+                <el-button link class="utility-action-btn upload-action-btn" type="primary" size="small" :disabled="isSending">
+                  <el-icon><Upload /></el-icon>鍒嗘瀽鏂囦欢
+                </el-button>
+              </el-upload>
             </div>
-            <el-input
-              v-model="inputMessage"
-              type="textarea"
-              :rows="selectedFile ? 2 : 3"
-              placeholder="璇疯緭鍏ユ偍鐨勯棶棰�... (Enter 鍙戦��, Shift+Enter 鎹㈣)"
-              resize="none"
-              @keydown.enter.exact.prevent="sendMessage"
-            />
-            <el-button 
-              type="primary" 
-              class="send-btn"
-              :disabled="isSending || (!inputMessage.trim() && !selectedFile)"
-              @click="sendMessage"
-            >
-              鍙戦��
-            </el-button>
+            <div class="input-box">
+              <div v-if="selectedFiles.length" class="selected-file-list">
+                <div v-for="(file, fileIndex) in selectedFiles" :key="`${file.name}-${fileIndex}`" class="selected-file-tag">
+                  <el-icon><Document /></el-icon>
+                  <span class="file-name">{{ file.name }}</span>
+                  <el-icon class="remove-file" @click="removeSelectedFile(fileIndex)"><Close /></el-icon>
+                </div>
+              </div>
+              <el-input
+                  v-model="inputMessage"
+                  type="textarea"
+                  :rows="selectedFiles.length ? 2 : 3"
+                  :placeholder="currentAssistant.placeholder"
+                  resize="none"
+                  @keydown.enter.exact.prevent="sendMessage"
+              />
+              <el-button
+                  type="primary"
+                  class="send-btn"
+                  :disabled="isSending || (!inputMessage.trim() && !selectedFiles.length)"
+                  @click="sendMessage"
+                  aria-label="鍙戦��"
+              >
+                <el-icon><Promotion /></el-icon>
+              </el-button>
+            </div>
           </div>
-        </div>
         </div>
       </div>
     </el-drawer>
@@ -184,8 +351,103 @@
 import { ref, onMounted, onUnmounted, nextTick, watch, computed } from 'vue'
 import request from '@/utils/request'
 import * as echarts from 'echarts'
-import { Cpu, User, Plus, Loading, Timer, Delete, ChatDotSquare, VideoPause, Upload, Document, Close } from '@element-plus/icons-vue'
+import { Cpu, User, Plus, Timer, Delete, ChatDotSquare, VideoPause, Upload, Document, Close, ShoppingCart, Promotion, RefreshRight } from '@element-plus/icons-vue'
 import { ElMessage } from 'element-plus'
+
+const props = defineProps({
+  assistants: {
+    type: Array,
+    default: () => []
+  },
+  defaultAssistant: {
+    type: String,
+    default: ''
+  }
+})
+
+const builtInAssistants = [
+  {
+    key: 'general',
+    label: '寰呭姙鍔╃悊',
+    title: '寰呭姙鏅鸿兘鍔╃悊',
+    tooltip: '寰呭姙鍔╂墜',
+    icon: Cpu,
+    apiBase: '/xiaozhi',
+    storageKey: 'ai_chat_uuid',
+    placeholder: '璇疯緭鍏ユ偍鐨勯棶棰�... (Enter 鍙戦��, Shift+Enter 鎹㈣)',
+    welcomeMessage: '浣犲ソ',
+    description: '鎴戝彲浠ュ洖绛斾綘鐨勯棶棰橈紝涓轰綘鎻愪緵涓氬姟鏁版嵁瑙h淇℃伅銆佸鐞嗗缓璁拰杈呭姪鍐崇瓥鏀寔銆�',
+    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 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 quickPromptLimit = 3
+const quickPromptStart = ref(0)
+const quickPrompts = computed(() => {
+  const assistant = currentAssistant.value || {}
+  if (Array.isArray(assistant.quickPrompts) && assistant.quickPrompts.length) {
+    return assistant.quickPrompts
+  }
+  return assistantQuickPromptMap[assistant.key] || assistantQuickPromptMap.general
+})
+const displayedQuickPrompts = computed(() => {
+  const prompts = quickPrompts.value || []
+  if (prompts.length <= quickPromptLimit) return prompts
+
+  const result = []
+  for (let i = 0; i < quickPromptLimit; i++) {
+    result.push(prompts[(quickPromptStart.value + i) % prompts.length])
+  }
+  return result
+})
+const hasMessages = computed(() => messages.value.length > 0)
 
 const visible = ref(false)
 const windowWidth = ref(window.innerWidth)
@@ -198,17 +460,187 @@
 const isSending = ref(false)
 const currentAbortController = ref(null)
 const inputMessage = ref('')
-const selectedFile = ref(null)
+const selectedFiles = ref([])
+const uploadFileList = ref([])
 const messages = ref([])
 const uuid = ref('')
 const chartInstances = ref({})
 const resizeHandlers = ref([])
 const outputState = ref({})
+const businessTypeLabelMap = {
+  purchase_ledger: '閲囪喘鍙拌处',
+  payment_registration: '浠樻鐧昏',
+  purchase_return_order: '閲囪喘閫�璐у崟',
+  unknown: '鏈煡閲囪喘涓氬姟'
+}
+const purchasePayloadFieldLabelMap = {
+  purchaseLedgers: '閲囪喘鍙拌处',
+  productData: '浜у搧鏄庣粏',
+  purchaseContractNumber: '閲囪喘鍚堝悓鍙�',
+  purchaseContractNo: '閲囪喘鍚堝悓鍙�',
+  purchaseOrderNumber: '閲囪喘鍚堝悓鍙�',
+  salesContractNo: '閿�鍞悎鍚屽彿',
+  salesContractNumber: '閿�鍞悎鍚屽彿',
+  salesOrderNumber: '閿�鍞悎鍚屽彿',
+  salesContractNoId: '閿�鍞悎鍚孖D',
+  approveUserIds: '瀹℃壒鐢ㄦ埛ID鍒楄〃',
+  entryDateStart: '褰曞叆寮�濮嬫棩鏈�',
+  entryDateEnd: '褰曞叆缁撴潫鏃ユ湡',
+  id: 'ID',
+  supplierId: '渚涘簲鍟咺D',
+  projectName: '椤圭洰鍚嶇О',
+  supplierName: '渚涘簲鍟嗗悕绉�',
+  isWhite: '鏄惁鐧藉悕鍗�',
+  recorderId: '褰曞叆浜篒D',
+  recorderName: '褰曞叆浜�',
+  contractDate: '鎵ц鏃ユ湡',
+  executionDate: '鎵ц鏃ユ湡',
+  inputPerson: '褰曞叆浜�',
+  inputDate: '褰曞叆鏃ユ湡',
+  entryDate: '褰曞叆鏃ユ湡',
+  paymentMethod: '浠樻鏂瑰紡',
+  auditors: '瀹℃壒浜�',
+  approverId: '瀹℃壒浜篒D',
+  approvalStatus: '瀹℃壒鐘舵��',
+  remark: '澶囨敞',
+  remarks: '澶囨敞',
+  attachmentMaterials: '闄勪欢鏉愭枡',
+  createdAt: '鍒涘缓鏃堕棿',
+  updatedAt: '鏇存柊鏃堕棿',
+  salesLedgerId: '閿�鍞彴璐D',
+  hasChildren: '鏄惁鏈夊瓙椤�',
+  Type: '绫诲瀷',
+  type: '绫诲瀷',
+  tempFileIds: '涓存椂鏂囦欢ID',
+  SalesLedgerFiles: '閿�鍞彴璐﹂檮浠�',
+  phoneNumber: '鑱旂郴鐢佃瘽',
+  businessPersonId: '涓氬姟鍛業D',
+  productId: '浜у搧ID',
+  productModelId: '浜у搧鍨嬪彿ID',
+  invoiceNumber: '鍙戠エ鍙风爜',
+  invoiceAmount: '鍙戠エ閲戦',
+  ticketRegistrationId: '寮�绁ㄧ櫥璁癐D',
+  contractAmount: '鍚堝悓閲戦',
+  receiptPaymentAmount: '宸叉敹浠樻閲戦',
+  unReceiptPaymentAmount: '鏈敹浠樻閲戦',
+  templateName: '妯℃澘鍚嶇О',
+  productCategory: '浜у搧绫诲埆',
+  specificationModel: '瑙勬牸鍨嬪彿',
+  unit: '鍗曚綅',
+  taxRate: '绋庣巼',
+  taxInclusiveUnitPrice: '鍚◣鍗曚环',
+  priceWithTax: '鍚◣鍗曚环',
+  quantity: '鏁伴噺',
+  taxInclusiveTotalPrice: '鍚◣鎬讳环',
+  totalPriceWithTax: '鍚◣鎬讳环',
+  invoiceType: '鍙戠エ绫诲瀷',
+  inventoryWarningQuantity: '搴撳瓨棰勮鏁伴噺',
+  isInspected: '鏄惁璐ㄦ'
+}
+const purchasePayloadFieldKeyMap = {
+  閲囪喘鍙拌处: 'purchaseLedgers',
+  浜у搧鏄庣粏: 'productData',
+  閲囪喘鍚堝悓鍙�: 'purchaseContractNumber',
+  閲囪喘鍗曞彿: 'purchaseContractNumber',
+  閲囪喘璁㈠崟鍙�: 'purchaseContractNumber',
+  閿�鍞悎鍚屽彿: 'salesContractNo',
+  閿�鍞崟鍙�: 'salesContractNo',
+  閿�鍞鍗曞彿: 'salesContractNo',
+  閿�鍞悎鍚孖D: 'salesContractNoId',
+  瀹℃壒鐢ㄦ埛ID鍒楄〃: 'approveUserIds',
+  褰曞叆寮�濮嬫棩鏈�: 'entryDateStart',
+  褰曞叆缁撴潫鏃ユ湡: 'entryDateEnd',
+  ID: 'id',
+  椤圭洰鍚嶇О: 'projectName',
+  渚涘簲鍟咺D: 'supplierId',
+  渚涘簲鍟嗗悕绉�: 'supplierName',
+  鏄惁鐧藉悕鍗�: 'isWhite',
+  褰曞叆浜篒D: 'recorderId',
+  褰曞叆浜�: 'recorderName',
+  绛捐鏃ユ湡: 'executionDate',
+  鎵ц鏃ユ湡: 'executionDate',
+  褰曞叆鏃ユ湡: 'entryDate',
+  浠樻鏂瑰紡: 'paymentMethod',
+  瀹℃牳浜�: 'approverId',
+  瀹℃壒浜�: 'approverId',
+  瀹℃壒浜篒D: 'approverId',
+  瀹℃壒鐘舵��: 'approvalStatus',
+  澶囨敞: 'remarks',
+  闄勪欢鏉愭枡: 'attachmentMaterials',
+  鍒涘缓鏃堕棿: 'createdAt',
+  鏇存柊鏃堕棿: 'updatedAt',
+  閿�鍞彴璐D: 'salesLedgerId',
+  鏄惁鏈夊瓙椤�: 'hasChildren',
+  绫诲瀷: 'type',
+  涓存椂鏂囦欢ID: 'tempFileIds',
+  閿�鍞彴璐﹂檮浠�: 'SalesLedgerFiles',
+  鑱旂郴鐢佃瘽: 'phoneNumber',
+  涓氬姟鍛業D: 'businessPersonId',
+  浜у搧ID: 'productId',
+  浜у搧鍨嬪彿ID: 'productModelId',
+  鍙戠エ鍙风爜: 'invoiceNumber',
+  鍙戠エ閲戦: 'invoiceAmount',
+  寮�绁ㄧ櫥璁癐D: 'ticketRegistrationId',
+  鍚堝悓閲戦: 'contractAmount',
+  宸叉敹浠樻閲戦: 'receiptPaymentAmount',
+  鏈敹浠樻閲戦: 'unReceiptPaymentAmount',
+  妯℃澘鍚嶇О: 'templateName',
+  浜у搧绫诲埆: 'productCategory',
+  浜у搧鍚嶇О: 'productCategory',
+  瑙勬牸鍨嬪彿: 'specificationModel',
+  鍗曚綅: 'unit',
+  绋庣巼: 'taxRate',
+  鍚◣鍗曚环: 'taxInclusiveUnitPrice',
+  鏁伴噺: 'quantity',
+  鍚◣鎬讳环: 'taxInclusiveTotalPrice',
+  鍙戠エ绫诲瀷: 'invoiceType',
+  搴撳瓨棰勮鏁伴噺: 'inventoryWarningQuantity',
+  鏄惁璐ㄦ: 'isInspected',
+  purchaseLedgers: 'purchaseLedgers',
+  productData: 'productData',
+  purchaseContractNumber: 'purchaseContractNumber',
+  purchaseContractNo: 'purchaseContractNumber',
+  purchaseOrderNumber: 'purchaseContractNumber',
+  salesContractNo: 'salesContractNo',
+  salesContractNumber: 'salesContractNo',
+  salesOrderNumber: 'salesContractNo',
+  contractDate: 'executionDate',
+  inputPerson: 'recorderName',
+  inputDate: 'entryDate',
+  auditors: 'approverId',
+  remark: 'remarks',
+  productCategory: 'productCategory',
+  productName: 'productCategory',
+  specificationModel: 'specificationModel',
+  unit: 'unit',
+  taxRate: 'taxRate',
+  priceWithTax: 'taxInclusiveUnitPrice',
+  taxInclusiveUnitPrice: 'taxInclusiveUnitPrice',
+  quantity: 'quantity',
+  totalPriceWithTax: 'taxInclusiveTotalPrice',
+  taxInclusiveTotalPrice: 'taxInclusiveTotalPrice',
+  invoiceType: 'invoiceType',
+  inventoryWarningQuantity: 'inventoryWarningQuantity',
+  isInspected: 'isInspected'
+}
 
 // 鍘嗗彶浼氳瘽鐩稿叧
 const showHistory = ref(false)
 const sessions = ref([])
 const loadingSessions = ref(false)
+
+const abortCurrentRequest = () => {
+  if (!currentAbortController.value) return
+
+  currentAbortController.value.abort()
+  currentAbortController.value = null
+  isSending.value = false
+
+  const lastMsg = messages.value[messages.value.length - 1]
+  if (lastMsg && !lastMsg.isUser) {
+    lastMsg.isTyping = false
+  }
+}
 
 const toggleHistory = () => {
   showHistory.value = !showHistory.value
@@ -217,10 +649,17 @@
   }
 }
 
+const handleToggleHistory = () => {
+  if (isSending.value) {
+    abortCurrentRequest()
+  }
+  toggleHistory()
+}
+
 const loadSessions = async () => {
   loadingSessions.value = true
   try {
-    const res = await request.get('/xiaozhi/history/sessions')
+    const res = await request.get(`${currentAssistant.value.apiBase}/history/sessions`)
     if (res.code === 200) {
       sessions.value = res.data || []
     }
@@ -234,21 +673,21 @@
 const selectSession = async (session) => {
   showHistory.value = false
   uuid.value = session.memoryId
-  localStorage.setItem('ai_chat_uuid', uuid.value)
-  
+  localStorage.setItem(currentAssistant.value.storageKey, uuid.value)
+
   // 鍔犺浇浼氳瘽娑堟伅
   try {
-    const res = await request.get(`/xiaozhi/history/messages/${uuid.value}`)
+    const res = await request.get(`${currentAssistant.value.apiBase}/history/messages/${uuid.value}`)
     if (res.code === 200) {
       disposeCharts()
       messages.value = []
       const historyMsgs = res.data || []
-      
+
       // 閲嶆柊鏋勯�犳秷鎭垪琛ㄥ苟瑙f瀽
       historyMsgs.forEach((msg, idx) => {
         const isUser = msg.role === 'user'
         const botMsgIndex = messages.value.length
-        
+
         const messageObj = {
           isUser,
           content: msg.content,
@@ -259,9 +698,9 @@
           type: '',
           tableData: null
         }
-        
+
         messages.value.push(messageObj)
-        
+
         if (!isUser) {
           outputState.value[botMsgIndex] = {
             isPaused: false,
@@ -270,27 +709,13 @@
             blockEndPos: -1,
             hasRenderedChart: false
           }
-          
+
           // 瑙f瀽鍘嗗彶娑堟伅涓殑 JSON
-          const jsonRegex = /\{"success":\s*true,[\s\S]*\}/
-          const jsonMatch = msg.content.match(jsonRegex)
-          if (jsonMatch) {
-            try {
-              const parsedData = JSON.parse(jsonMatch[0])
-              if (parsedData.success) {
-                messageObj.type = parsedData.type || ''
-                if (messageObj.type === 'todo_list' && parsedData.data) {
-                  messageObj.tableData = parsedData.data
-                }
-                if (parsedData.charts && Object.keys(parsedData.charts).length > 0) {
-                  messageObj.chartOptions = parsedData.charts
-                  messageObj.chartRenderReady = true
-                  renderCharts(botMsgIndex, messageObj.chartOptions)
-                }
-              }
-            } catch (err) {}
+          const extracted = extractEmbeddedSuccessJson(msg.content)
+          if (extracted) {
+            applyStructuredMessageData(messageObj, extracted.data, botMsgIndex)
           }
-          
+
           updateOutputState(msg.content, botMsgIndex)
           messageObj.htmlContent = convertStreamOutput(msg.content, botMsgIndex)
         } else {
@@ -306,7 +731,7 @@
 
 const handleDeleteSession = async (memoryId) => {
   try {
-    const res = await request.delete(`/xiaozhi/history/${memoryId}`)
+    const res = await request.delete(`${currentAssistant.value.apiBase}/history/${memoryId}`)
     if (res.code === 200) {
       loadSessions()
       if (uuid.value === memoryId) {
@@ -342,6 +767,23 @@
   window.removeEventListener('resize', handleWindowResize)
 })
 
+watch(selectedAssistantKey, (nextKey, prevKey) => {
+  if (!prevKey || nextKey === prevKey) return
+
+  abortCurrentRequest()
+  disposeCharts()
+  messages.value = []
+  outputState.value = {}
+  sessions.value = []
+  showHistory.value = false
+  selectedFiles.value = []
+  uploadFileList.value = []
+  inputMessage.value = ''
+  quickPromptStart.value = 0
+  initUUID()
+  hello()
+})
+
 const handleWindowResize = () => {
   windowWidth.value = window.innerWidth
 }
@@ -357,26 +799,57 @@
   visible.value = false
 }
 
+const handleManualClose = () => {
+  if (isSending.value) {
+    abortCurrentRequest()
+  }
+  handleClose()
+}
+
 const initUUID = () => {
-  let storedUUID = localStorage.getItem('ai_chat_uuid')
+  let storedUUID = localStorage.getItem(currentAssistant.value.storageKey)
   if (!storedUUID) {
     storedUUID = Math.random().toString(36).substring(2, 10) + Date.now().toString(36).substring(4)
-    localStorage.setItem('ai_chat_uuid', storedUUID)
+    localStorage.setItem(currentAssistant.value.storageKey, storedUUID)
   }
   uuid.value = storedUUID
 }
 
 const hello = () => {
-  sendRequest('浣犲ソ')
+  sendRequest(currentAssistant.value.welcomeMessage || '浣犲ソ')
 }
 
 const newChat = () => {
   disposeCharts()
   messages.value = []
   outputState.value = {}
-  localStorage.removeItem('ai_chat_uuid')
+  sessions.value = []
+  showHistory.value = false
+  selectedFiles.value = []
+  uploadFileList.value = []
+  quickPromptStart.value = 0
+  localStorage.removeItem(currentAssistant.value.storageKey)
   initUUID()
   hello()
+}
+
+const handleNewChat = () => {
+  if (isSending.value) {
+    abortCurrentRequest()
+  }
+  newChat()
+}
+
+const sendQuickPrompt = (prompt) => {
+  if (!prompt || isSending.value) return
+  inputMessage.value = prompt
+  sendMessage()
+}
+
+const refreshQuickPrompts = () => {
+  const prompts = quickPrompts.value || []
+  if (prompts.length <= quickPromptLimit) return
+  quickPromptStart.value = (quickPromptStart.value + quickPromptLimit) % prompts.length
 }
 
 const disposeCharts = () => {
@@ -384,6 +857,519 @@
   resizeHandlers.value.forEach(handler => window.removeEventListener('resize', handler))
   chartInstances.value = {}
   resizeHandlers.value = []
+}
+
+const extractEmbeddedSuccessJson = (text) => {
+  if (!text || typeof text !== 'string') return null
+
+  const startMatch = text.match(/\{\s*"success"\s*:/)
+  if (!startMatch) return null
+  const startIdx = startMatch.index ?? -1
+  if (startIdx < 0) return null
+
+  for (let i = startIdx; i < text.length; i++) {
+    if (text[i] !== '{') continue
+
+    let depth = 0
+    let inString = false
+    let escaped = false
+
+    for (let j = i; j < text.length; j++) {
+      const char = text[j]
+
+      if (inString) {
+        if (escaped) {
+          escaped = false
+        } else if (char === '\\') {
+          escaped = true
+        } else if (char === '"') {
+          inString = false
+        }
+        continue
+      }
+
+      if (char === '"') {
+        inString = true
+        continue
+      }
+
+      if (char === '{') {
+        depth++
+      } else if (char === '}') {
+        depth--
+        if (depth === 0) {
+          const candidate = text.slice(i, j + 1)
+          try {
+            const parsed = JSON.parse(candidate)
+            if (parsed?.success === true) {
+              return {
+                data: parsed,
+                startIdx: i,
+                endIdx: j + 1
+              }
+            }
+          } catch (err) {
+            continue
+          }
+        }
+      }
+    }
+  }
+
+  return null
+}
+
+const applyStructuredMessageData = (messageObj, parsedData, msgIndex, shouldRenderCharts = true) => {
+  if (!messageObj || !parsedData?.success) return
+
+  messageObj.type = parsedData.type || ''
+
+  if (messageObj.type === 'todo_list' && parsedData.data) {
+    messageObj.tableData = parsedData.data
+  }
+
+  if (parsedData.action === 'confirm_required' && parsedData.businessType) {
+    messageObj.type = 'purchase_analysis_confirm'
+    messageObj.purchaseAnalysisData = parsedData
+    if (!messageObj.payloadText) {
+      messageObj.payloadText = JSON.stringify(localizePurchasePayload(parsedData.payload || {}), null, 2)
+    }
+    messageObj.confirmResult = ''
+    messageObj.confirmed = false
+    messageObj.confirming = false
+  }
+
+  const chartOptions = getStructuredChartOptions(parsedData)
+  if (chartOptions && Object.keys(chartOptions).length > 0) {
+    messageObj.chartOptions = chartOptions
+    messageObj.chartRenderReady = true
+
+    if (shouldRenderCharts) {
+      renderCharts(msgIndex, messageObj.chartOptions)
+      if (outputState.value[msgIndex]) {
+        outputState.value[msgIndex].hasRenderedChart = true
+      }
+    }
+  }
+}
+
+const getStructuredChartOptions = (parsedData) => {
+  if (!parsedData?.success) return null
+
+  if (parsedData.charts && Object.keys(parsedData.charts).length > 0) {
+    return parsedData.charts
+  }
+
+  if (parsedData.type === 'purchase_material_rank') {
+    return buildPurchaseMaterialRankCharts(parsedData)
+  }
+
+  return null
+}
+
+const buildPurchaseMaterialRankCharts = (parsedData) => {
+  const items = Array.isArray(parsedData?.data?.items) ? parsedData.data.items : []
+  if (!items.length) return null
+
+  const names = items.map(item => item.productCategory || '-')
+  const amounts = items.map(item => Number(item.amount) || 0)
+
+  return {
+    purchaseMaterialAmountRank: {
+      title: {
+        text: '\u91c7\u8d2d\u7269\u6599\u91d1\u989d\u6392\u884c',
+        left: 'center',
+        textStyle: {
+          fontSize: 14,
+          fontWeight: 600,
+          color: '#1a1a2e'
+        }
+      },
+      tooltip: {
+        trigger: 'axis',
+        axisPointer: {
+          type: 'shadow'
+        },
+        formatter(params) {
+          const dataIndex = params?.[0]?.dataIndex ?? 0
+          const item = items[dataIndex] || {}
+          const amount = Number(item.amount) || 0
+          const quantity = Number(item.quantity) || 0
+          return [
+            `${item.productCategory || '-'}`,
+            `${params?.[0]?.marker || ''} \u91d1\u989d\uff1a${formatCurrency(amount)}`,
+            `\u89c4\u683c\u578b\u53f7\uff1a${item.specificationModel || '-'}`,
+            `\u6570\u91cf\uff1a${quantity}${item.unit || ''}`
+          ].join('<br/>')
+        }
+      },
+      grid: {
+        left: '3%',
+        right: '4%',
+        bottom: names.some(name => String(name).length > 6) ? 72 : 48,
+        top: 48,
+        containLabel: true
+      },
+      xAxis: {
+        type: 'category',
+        data: names,
+        axisLabel: {
+          interval: 0,
+          rotate: names.some(name => String(name).length > 6) ? 28 : 0,
+          color: '#4b5563'
+        }
+      },
+      yAxis: {
+        type: 'value',
+        name: '\u91d1\u989d(\u5143)',
+        axisLabel: {
+          color: '#4b5563',
+          formatter: value => formatCompactNumber(value)
+        }
+      },
+      series: [{
+        name: '\u91c7\u8d2d\u91d1\u989d',
+        type: 'bar',
+        barMaxWidth: 36,
+        data: amounts,
+        itemStyle: {
+          borderRadius: [6, 6, 0, 0],
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: '#2f7cf6' },
+            { offset: 1, color: '#55d7ff' }
+          ])
+        },
+        label: {
+          show: true,
+          position: 'top',
+          color: '#1f2937',
+          formatter: params => formatCompactNumber(params.value)
+        }
+      }]
+    }
+  }
+}
+
+const formatCurrency = (value) => {
+  const amount = Number(value) || 0
+  return `\u00a5${amount.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}`
+}
+
+const formatCompactNumber = (value) => {
+  const amount = Number(value) || 0
+  if (Math.abs(amount) >= 10000) {
+    return `${(amount / 10000).toFixed(2).replace(/\.?0+$/, '')}\u4e07`
+  }
+  return amount.toLocaleString('zh-CN', { maximumFractionDigits: 2 })
+}
+
+const formatPercent = (value) => {
+  const number = Number(value)
+  if (Number.isNaN(number)) return '-'
+  return `${Math.round(number * 100)}%`
+}
+
+const formatPreviewItem = (item) => {
+  if (item === null || item === undefined) return '-'
+  if (typeof item === 'string') return item
+  try {
+    return JSON.stringify(item)
+  } catch (err) {
+    return String(item)
+  }
+}
+
+const mapPayloadKeys = (value, keyMap) => {
+  if (Array.isArray(value)) {
+    return value.map(item => mapPayloadKeys(item, keyMap))
+  }
+  if (value && typeof value === 'object') {
+    return Object.entries(value).reduce((result, [key, item]) => {
+      result[keyMap[key] || key] = mapPayloadKeys(item, keyMap)
+      return result
+    }, {})
+  }
+  return value
+}
+
+const localizePurchasePayload = (payload) => mapPayloadKeys(payload, purchasePayloadFieldLabelMap)
+
+const normalizePurchasePayload = (payload) => mapPayloadKeys(payload, purchasePayloadFieldKeyMap)
+
+const purchaseDateFieldKeys = new Set([
+  'entryDateStart',
+  'entryDateEnd',
+  'entryDate',
+  'executionDate',
+  'contractDate',
+  'inputDate',
+  'createdAt',
+  'updatedAt'
+])
+
+const purchaseLedgerAllowedFieldKeys = new Set([
+  'entryDateStart',
+  'entryDateEnd',
+  'id',
+  'purchaseContractNumber',
+  'supplierId',
+  'supplierName',
+  'isWhite',
+  'recorderId',
+  'recorderName',
+  'salesContractNo',
+  'salesContractNoId',
+  'projectName',
+  'entryDate',
+  'executionDate',
+  'remarks',
+  'attachmentMaterials',
+  'createdAt',
+  'updatedAt',
+  'salesLedgerId',
+  'hasChildren',
+  'Type',
+  'productData',
+  'tempFileIds',
+  'SalesLedgerFiles',
+  'phoneNumber',
+  'businessPersonId',
+  'productId',
+  'productModelId',
+  'invoiceNumber',
+  'invoiceAmount',
+  'ticketRegistrationId',
+  'contractAmount',
+  'receiptPaymentAmount',
+  'unReceiptPaymentAmount',
+  'type',
+  'paymentMethod',
+  'approvalStatus',
+  'templateName'
+])
+
+const purchaseApprovalFieldKeys = new Set([
+  'approveUserIds',
+  'approverId',
+  'auditors',
+  '瀹℃牳浜�',
+  '瀹℃壒浜�',
+  '瀹℃壒浜篒D',
+  '瀹℃壒鐢ㄦ埛ID鍒楄〃'
+])
+
+const normalizePurchaseProductRecord = (record) => {
+  if (!record || typeof record !== 'object' || Array.isArray(record)) return record
+  return mapPayloadKeys(record, purchasePayloadFieldKeyMap)
+}
+
+const getPurchaseProductMatchKey = (record) => {
+  if (!record || typeof record !== 'object') return ''
+  return record.purchaseContractNumber ||
+    record.purchaseContractNo ||
+    record.salesContractNo ||
+    record.salesContractNumber ||
+    ''
+}
+
+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) {
+    return payload
+  }
+
+  const ledgers = payload.purchaseLedgers.map(ledger => ({
+    ...ledger,
+    productData: Array.isArray(ledger.productData)
+      ? ledger.productData.map(normalizePurchaseProductRecord)
+      : []
+  }))
+  const unmatchedProducts = []
+
+  payload.productData.map(normalizePurchaseProductRecord).forEach(product => {
+    const productMatchKey = getPurchaseProductMatchKey(product)
+    const matchedLedger = ledgers.find(ledger => {
+      const ledgerKeys = [
+        ledger.purchaseContractNumber,
+        ledger.purchaseContractNo,
+        ledger.salesContractNo,
+        ledger.salesContractNumber
+      ].filter(Boolean)
+      return productMatchKey && ledgerKeys.includes(productMatchKey)
+    })
+
+    if (matchedLedger) {
+      matchedLedger.productData.push(product)
+    } else if (ledgers.length === 1) {
+      ledgers[0].productData.push(product)
+    } else {
+      unmatchedProducts.push(product)
+    }
+  })
+
+  const nextPayload = {
+    ...payload,
+    purchaseLedgers: ledgers
+  }
+
+  if (unmatchedProducts.length) {
+    nextPayload.productData = unmatchedProducts
+  } else {
+    delete nextPayload.productData
+  }
+
+  return nextPayload
+}
+
+const filterPurchaseLedgerRecord = (record) => {
+  if (!record || typeof record !== 'object' || Array.isArray(record)) return record
+  const normalizedRecord = {
+    ...record,
+    productData: Array.isArray(record.productData)
+      ? record.productData.map(normalizePurchaseProductRecord)
+      : record.productData
+  }
+  return Object.entries(normalizedRecord).reduce((result, [key, value]) => {
+    if (purchaseLedgerAllowedFieldKeys.has(key)) {
+      result[key] = value
+    }
+    return result
+  }, {})
+}
+
+const sanitizePurchasePayloadForSubmit = (payload, businessType) => {
+  if (businessType !== 'purchase_ledger' || !payload || typeof payload !== 'object') return payload
+
+  const sanitized = mergeLegacyProductDataIntoLedgers(Array.isArray(payload) ? [...payload] : { ...payload })
+  if (Array.isArray(sanitized.purchaseLedgers)) {
+    sanitized.purchaseLedgers = sanitized.purchaseLedgers.map(filterPurchaseLedgerRecord)
+  }
+
+  purchaseApprovalFieldKeys.forEach(key => {
+    if (!Array.isArray(sanitized)) {
+      delete sanitized[key]
+    }
+  })
+
+  return sanitized
+}
+
+const getVisiblePurchaseMissingFields = (analysisData) => {
+  const fields = Array.isArray(analysisData?.missingFields) ? analysisData.missingFields : []
+  const visibleFields = analysisData?.businessType === 'purchase_ledger'
+    ? fields.filter(field => !purchaseApprovalFieldKeys.has(field))
+    : fields
+  return visibleFields.map(field => purchasePayloadFieldLabelMap[field] || field)
+}
+
+const formatDateParts = (year, month, day) => {
+  const normalizedYear = Number(year)
+  const normalizedMonth = Number(month)
+  const normalizedDay = Number(day)
+  if (!normalizedYear || !normalizedMonth || !normalizedDay) return ''
+
+  const date = new Date(normalizedYear, normalizedMonth - 1, normalizedDay)
+  if (
+    date.getFullYear() !== normalizedYear ||
+    date.getMonth() !== normalizedMonth - 1 ||
+    date.getDate() !== normalizedDay
+  ) {
+    return ''
+  }
+
+  return [
+    String(normalizedYear).padStart(4, '0'),
+    String(normalizedMonth).padStart(2, '0'),
+    String(normalizedDay).padStart(2, '0')
+  ].join('-')
+}
+
+const normalizeDateString = (value) => {
+  if (typeof value !== 'string') return value
+  const text = value.trim()
+  if (!text) return value
+
+  let match = text.match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[T\s].*)?$/)
+  if (match) return formatDateParts(match[1], match[2], match[3]) || value
+
+  match = text.match(/^(\d{4})\/(\d{1,2})\/(\d{1,2})(?:\s.*)?$/)
+  if (match) return formatDateParts(match[1], match[2], match[3]) || value
+
+  match = text.match(/^(\d{4})骞�(\d{1,2})鏈�(\d{1,2})鏃�?(?:\s.*)?$/)
+  if (match) return formatDateParts(match[1], match[2], match[3]) || value
+
+  match = text.match(/^(\d{1,2})\/(\d{1,2})\/(\d{2}|\d{4})(?:\s.*)?$/)
+  if (match) {
+    const year = match[3].length === 2 ? Number(`20${match[3]}`) : Number(match[3])
+    return formatDateParts(year, match[1], match[2]) || value
+  }
+
+  return value
+}
+
+const normalizePurchasePayloadDates = (value, key = '') => {
+  if (Array.isArray(value)) {
+    return value.map(item => normalizePurchasePayloadDates(item, key))
+  }
+  if (value && typeof value === 'object') {
+    return Object.entries(value).reduce((result, [itemKey, item]) => {
+      result[itemKey] = normalizePurchasePayloadDates(item, itemKey)
+      return result
+    }, {})
+  }
+  return purchaseDateFieldKeys.has(key) ? normalizeDateString(value) : value
+}
+
+const isEmptyValue = (value) => {
+  if (value === null || value === undefined || value === '') return true
+  if (Array.isArray(value)) return value.every(item => isEmptyValue(item))
+  if (typeof value === 'object') return Object.values(value).every(item => isEmptyValue(item))
+  return false
+}
+
+const isPurchasePayloadEmpty = (payload) => isEmptyValue(payload)
+
+const getPurchaseConfirmDescription = (analysisData) => {
+  if (!analysisData) return ''
+  if (isPurchasePayloadEmpty(analysisData.payload)) {
+    return '鎴戞病鏈変粠鏂囦欢涓彁鍙栧埌瀹屾暣鐨勯噰璐笟鍔℃暟鎹紝鏆傛椂涓嶈兘鐩存帴鐢熸垚閲囪喘鍙拌处銆�'
+  }
+  return analysisData.description || '宸叉暣鐞嗗嚭寰呯‘璁ょ殑閲囪喘涓氬姟鏁版嵁锛岃鏍稿鍚庢彁浜ゃ��'
+}
+
+const confirmPurchaseAnalysis = async (message) => {
+  if (!message?.purchaseAnalysisData || message.confirming || message.confirmed) return
+
+  let payload
+  try {
+    const parsedPayload = message.payloadText?.trim() ? JSON.parse(message.payloadText) : {}
+    payload = sanitizePurchasePayloadForSubmit(
+      normalizePurchasePayloadDates(normalizePurchasePayload(parsedPayload)),
+      message.purchaseAnalysisData.businessType
+    )
+  } catch (err) {
+    message.confirmResult = '寰呮彁浜ゆ暟鎹笉鏄悎娉� JSON锛岃淇敼鍚庡啀纭'
+    message.confirmed = false
+    return
+  }
+
+  message.confirming = true
+  message.confirmResult = ''
+
+  try {
+    const res = await request.post(`${currentAssistant.value.apiBase}/analyze-files/confirm`, {
+      businessType: message.purchaseAnalysisData.businessType,
+      payload
+    })
+    message.confirmed = true
+    message.confirmResult = res?.msg || '纭鎴愬姛锛屼笟鍔″鐞嗗凡鎻愪氦'
+    ElMessage.success(message.confirmResult)
+  } catch (err) {
+    message.confirmed = false
+    message.confirmResult = err?.message || '纭澶辫触锛岃妫�鏌ユ暟鎹悗閲嶈瘯'
+  } finally {
+    message.confirming = false
+  }
 }
 
 const scrollToBottom = () => {
@@ -394,30 +1380,38 @@
   })
 }
 
-const handleFileChange = (file) => {
+const handleFileChange = (file, fileList = []) => {
   if (!file) return
-  const rawFile = file.raw
-  if (rawFile) {
-    // 闄愬埗鏂囦欢澶у皬锛屼緥濡� 10MB
+  const nextFiles = currentAssistant.value.allowMultipleFileUpload
+    ? fileList.map(item => item.raw).filter(Boolean)
+    : [file.raw].filter(Boolean)
+
+  const validFiles = nextFiles.filter(rawFile => {
     const isLt10M = rawFile.size / 1024 / 1024 < 10
     if (!isLt10M) {
-      ElMessage.error('鏂囦欢澶у皬涓嶈兘瓒呰繃 10MB!')
-      return
+      ElMessage.error(`${rawFile.name} 鏂囦欢澶у皬涓嶈兘瓒呰繃 10MB!`)
     }
-    selectedFile.value = rawFile
-  }
+    return isLt10M
+  })
+
+  selectedFiles.value = validFiles
+  uploadFileList.value = fileList.filter(item => item.raw && validFiles.includes(item.raw))
 }
 
-const removeSelectedFile = () => {
-  selectedFile.value = null
+const removeSelectedFile = (index) => {
+  selectedFiles.value.splice(index, 1)
+  uploadFileList.value.splice(index, 1)
 }
 
-const analyzeFile = async (file, message = '') => {
+const analyzeFiles = async (files, message = '') => {
+  const uploadFiles = Array.isArray(files) ? files : [files].filter(Boolean)
+  if (!uploadFiles.length) return
   if (isSending.value) return
   isSending.value = true
   currentAbortController.value = new AbortController()
-  
-  const userMsg = message ? `${message}\n[涓婁紶鏂囦欢鍒嗘瀽] ${file.name}` : `[涓婁紶鏂囦欢鍒嗘瀽] ${file.name}`
+
+  const fileNames = uploadFiles.map(file => file.name).join('銆�')
+  const userMsg = message ? `${message}\n[涓婁紶鏂囦欢鍒嗘瀽] ${fileNames}` : `[涓婁紶鏂囦欢鍒嗘瀽] ${fileNames}`
   messages.value.push({
     isUser: true,
     content: userMsg,
@@ -448,13 +1442,15 @@
   scrollToBottom()
 
   const formData = new FormData()
-  formData.append('file', file)
+  const fileFieldName = currentAssistant.value.allowMultipleFileUpload ? 'files' : 'file'
+  uploadFiles.forEach(file => formData.append(fileFieldName, file))
   formData.append('memoryId', uuid.value)
   if (message.trim()) {
     formData.append('message', message.trim())
   }
 
-  request.post('/xiaozhi/analyze-file', formData, {
+  const analyzeUrl = currentAssistant.value.fileAnalyzeUrl || `${currentAssistant.value.apiBase}/analyze-file`
+  request.post(analyzeUrl, formData, {
     headers: {
       'Content-Type': 'multipart/form-data'
     },
@@ -462,34 +1458,16 @@
     onDownloadProgress: (e) => {
       const fullText = e.target ? e.target.responseText : (e.event ? e.event.target.responseText : '')
       if (!fullText) return
-      
+
       const currentMsg = messages.value[botMsgIndex]
       if (!currentMsg) return
-      
+
       currentMsg.content = fullText
 
       // 瑙f瀽 JSON 鏁版嵁锛堥拡瀵瑰祵鍏ュ紡 JSON锛�
-      const jsonRegex = /\{"success":\s*true,[\s\S]*\}/
-      const jsonMatch = fullText.match(jsonRegex)
-
-      if (jsonMatch) {
-        try {
-          const parsedData = JSON.parse(jsonMatch[0])
-          if (parsedData.success) {
-            currentMsg.type = parsedData.type || ''
-            if (currentMsg.type === 'todo_list' && parsedData.data) {
-              currentMsg.tableData = parsedData.data
-            }
-            if (parsedData.charts && Object.keys(parsedData.charts).length > 0) {
-              currentMsg.chartOptions = parsedData.charts
-              currentMsg.chartRenderReady = true
-              if (!outputState.value[botMsgIndex].hasRenderedChart) {
-                renderCharts(botMsgIndex, currentMsg.chartOptions)
-                outputState.value[botMsgIndex].hasRenderedChart = true
-              }
-            }
-          }
-        } catch (err) {}
+      const extracted = extractEmbeddedSuccessJson(fullText)
+      if (extracted) {
+        applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex, !outputState.value[botMsgIndex].hasRenderedChart)
       }
 
       updateOutputState(fullText, botMsgIndex)
@@ -501,7 +1479,12 @@
     currentMsg.isTyping = false
     isSending.value = false
     currentAbortController.value = null
-    
+
+    const extracted = extractEmbeddedSuccessJson(currentMsg.content)
+    if (extracted) {
+      applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex, !outputState.value[botMsgIndex].hasRenderedChart)
+    }
+
     // 鏈�缁堣В鏋愮‘淇濆浘琛ㄦ覆鏌�
     if (currentMsg.chartOptions && !outputState.value[botMsgIndex].hasRenderedChart) {
       renderCharts(botMsgIndex, currentMsg.chartOptions)
@@ -510,6 +1493,8 @@
   }).catch(err => {
     if (err.name === 'CanceledError' || err.name === 'AbortError') {
       console.log('Analysis aborted by user')
+      isSending.value = false
+      currentAbortController.value = null
       return
     }
     console.error('File analysis error:', err)
@@ -526,10 +1511,11 @@
 
 const sendMessage = () => {
   const msg = inputMessage.value?.trim() || ''
-  if ((msg || selectedFile.value) && !isSending.value) {
-    if (selectedFile.value) {
-      analyzeFile(selectedFile.value, msg)
-      selectedFile.value = null
+  if ((msg || selectedFiles.value.length) && !isSending.value) {
+    if (selectedFiles.value.length) {
+      analyzeFiles([...selectedFiles.value], msg)
+      selectedFiles.value = []
+      uploadFileList.value = []
     } else {
       sendRequest(msg)
     }
@@ -538,17 +1524,7 @@
 }
 
 const stopGeneration = () => {
-  if (currentAbortController.value) {
-    currentAbortController.value.abort()
-    currentAbortController.value = null
-    isSending.value = false
-    
-    // 灏嗘渶鍚庝竴鏉℃秷鎭爣璁颁负闈炴墦瀛楃姸鎬�
-    const lastMsg = messages.value[messages.value.length - 1]
-    if (lastMsg && !lastMsg.isUser) {
-      lastMsg.isTyping = false
-    }
-  }
+  abortCurrentRequest()
 }
 
 const sendRequest = (message) => {
@@ -556,14 +1532,12 @@
   currentAbortController.value = new AbortController()
 
   // 鐢ㄦ埛娑堟伅
-  if (messages.value.length > 0) {
-    messages.value.push({
-      isUser: true,
-      content: message,
-      htmlContent: convertTextToHtml(message),
-      isTyping: false
-    })
-  }
+  messages.value.push({
+    isUser: true,
+    content: message,
+    htmlContent: convertTextToHtml(message),
+    isTyping: false
+  })
 
   // 鏈哄櫒浜哄崰浣�
   const botMsgIndex = messages.value.length
@@ -578,7 +1552,7 @@
     tableData: null
   }
   messages.value.push(botMsg)
-  
+
   outputState.value[botMsgIndex] = {
     isPaused: false,
     jsonBlockStartPos: -1,
@@ -586,86 +1560,84 @@
     blockEndPos: -1,
     hasRenderedChart: false
   }
-  
+
   scrollToBottom()
 
-  request.post('/xiaozhi/chat', 
-    { memoryId: uuid.value, message },
-    {
-      signal: currentAbortController.value.signal,
-      onDownloadProgress: (e) => {
-        // 鍏煎涓嶅悓鐗堟湰鐨� axios 鑾峰彇鍝嶅簲鏂囨湰鐨勬柟寮�
-        const fullText = e.target ? e.target.responseText : (e.event ? e.event.target.responseText : '')
-        if (!fullText) return
-        
-        const currentMsg = messages.value[botMsgIndex]
-        if (!currentMsg) return
-        
-        currentMsg.content = fullText
+  request.post(`${currentAssistant.value.apiBase}/chat`,
+      { memoryId: uuid.value, message },
+      {
+        signal: currentAbortController.value.signal,
+        onDownloadProgress: (e) => {
+          // 鍏煎涓嶅悓鐗堟湰鐨� axios 鑾峰彇鍝嶅簲鏂囨湰鐨勬柟寮�
+          const fullText = e.target ? e.target.responseText : (e.event ? e.event.target.responseText : '')
+          if (!fullText) return
 
-        // 瑙f瀽 JSON 鏁版嵁锛堥拡瀵瑰祵鍏ュ紡 JSON锛�
-        const jsonRegex = /\{"success":\s*true,[\s\S]*\}/
-        const jsonMatch = fullText.match(jsonRegex)
+          const currentMsg = messages.value[botMsgIndex]
+          if (!currentMsg) return
 
-        if (jsonMatch) {
-          try {
-            const parsedData = JSON.parse(jsonMatch[0])
-            if (parsedData.success) {
-              currentMsg.type = parsedData.type || ''
-              if (currentMsg.type === 'todo_list' && parsedData.data) {
-                currentMsg.tableData = parsedData.data
-              }
-              if (parsedData.charts && Object.keys(parsedData.charts).length > 0) {
-                currentMsg.chartOptions = parsedData.charts
-                currentMsg.chartRenderReady = true
-                if (!outputState.value[botMsgIndex].hasRenderedChart) {
-                  renderCharts(botMsgIndex, currentMsg.chartOptions)
-                  outputState.value[botMsgIndex].hasRenderedChart = true
-                }
+          currentMsg.content = fullText
+
+          // 灏濊瘯鎻愬彇骞惰В鏋� JSON
+          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
               }
             }
-          } catch (err) {}
-        }
 
-        updateOutputState(fullText, botMsgIndex)
-        currentMsg.htmlContent = convertStreamOutput(fullText, botMsgIndex)
-        scrollToBottom()
+            const parsedData = extractJson(fullText)
+            if (parsedData) {
+              applyStructuredMessageData(currentMsg, parsedData, botMsgIndex, true)
+            }
+
+          }
+
+          updateOutputState(fullText, botMsgIndex)
+          currentMsg.htmlContent = convertStreamOutput(fullText, botMsgIndex)
+          scrollToBottom()
+        }
       }
-    }
   ).then(() => {
     const currentMsg = messages.value[botMsgIndex]
     currentMsg.isTyping = false
     isSending.value = false
     currentAbortController.value = null
-    
+
     // 鏈�缁堣В鏋�
-    const fullText = currentMsg.content
-    const jsonRegex = /\{"success":\s*true,[\s\S]*\}/
-    const jsonMatch = fullText.match(jsonRegex)
-    if (jsonMatch) {
-      try {
-        const parsedData = JSON.parse(jsonMatch[0])
-        if (parsedData.success) {
-          currentMsg.type = parsedData.type || ''
-          if (currentMsg.type === 'todo_list' && parsedData.data) {
-            currentMsg.tableData = parsedData.data
-          }
-          if (parsedData.charts && Object.keys(parsedData.charts).length > 0) {
-            currentMsg.chartOptions = parsedData.charts
-            currentMsg.chartRenderReady = true
-            if (!outputState.value[botMsgIndex].hasRenderedChart) {
-              renderCharts(botMsgIndex, currentMsg.chartOptions)
-              outputState.value[botMsgIndex].hasRenderedChart = true
-            }
-          }
-          currentMsg.htmlContent = convertStreamOutput(fullText, botMsgIndex)
+    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
         }
-      } catch (err) {}
-    }
-    
-    // 鍏滃簳娓叉煋
-    if (currentMsg.chartOptions && !outputState.value[botMsgIndex].hasRenderedChart) {
-      renderCharts(botMsgIndex, currentMsg.chartOptions)
+      }
+
+      const finalParsed = extractJson(currentMsg.content)
+      if (finalParsed) {
+        applyStructuredMessageData(currentMsg, finalParsed, botMsgIndex)
+      }
     }
   }).catch(err => {
     if (err.name === 'CanceledError' || err.name === 'AbortError') {
@@ -704,10 +1676,10 @@
 const convertTextToHtml = (text) => {
   if (!text) return ''
   return text
-    .replace(/&/g, '&amp;')
-    .replace(/</g, '&lt;')
-    .replace(/>/g, '&gt;')
-    .replace(/\n/g, '<br>')
+      .replace(/&/g, '&amp;')
+      .replace(/</g, '&lt;')
+      .replace(/>/g, '&gt;')
+      .replace(/\n/g, '<br>')
 }
 
 const convertStreamOutput = (output, msgIndex) => {
@@ -715,29 +1687,87 @@
   const state = outputState.value[msgIndex]
   let display = output
 
-  const jsonRegex = /\{"success":\s*true,[\s\S]*\}/
-  const jsonMatch = output.match(jsonRegex)
-  if (jsonMatch) {
-    try {
-      const parsed = JSON.parse(jsonMatch[0])
-      display = output.replace(jsonMatch[0], '').trim()
-      if (!display && parsed.description) display = parsed.description
-    } catch (e) {
-      const start = output.search(/\{"success":\s*true/)
-      display = output.substring(0, start) + '... (姝e湪鐢熸垚鏁版嵁鍥捐〃)'
+  // 灏濊瘯鎻愬彇 JSON 閮ㄥ垎
+  const extracted = extractEmbeddedSuccessJson(output)
+  const startMatch = output.match(/\{\s*"success"\s*:/)
+  const startIdx = extracted ? extracted.startIdx : (startMatch?.index ?? -1)
+
+  // 濡傛灉杩樺湪浠g爜鍧椾腑涓旀湭缁撴潫锛屾樉绀烘彁绀烘枃瀛�
+  if (state && ((state.jsonBlockStartPos !== -1) || (state.jsBlockStartPos !== -1)) && state.blockEndPos === -1) {
+    const startPos = state.jsonBlockStartPos !== -1 ? state.jsonBlockStartPos : state.jsBlockStartPos
+    const textBeforeBlock = display.substring(0, startPos).trim()
+    display = textBeforeBlock || '姝e湪鍒嗘瀽鏁版嵁骞剁敓鎴愬浘琛�...'
+    return convertTextToHtml(display)
+  }
+
+  if (extracted) {
+    const parsed = extracted.data
+    display = (output.substring(0, extracted.startIdx) + output.substring(extracted.endIdx)).trim()
+
+    if (/^[\s}\],锛屻��.:锛�;锛沒+$/.test(display)) {
+      display = ''
+    }
+
+    if (parsed.description) {
+      display = parsed.action === 'confirm_required'
+        ? getPurchaseConfirmDescription(parsed)
+        : parsed.description
+    }
+
+    if (!display) {
+      if (parsed.type === 'todo_list') {
+        display = '宸蹭负鎮ㄦ暣鐞嗗ソ鐩稿叧鏁版嵁銆�'
+      } else if (parsed.charts && Object.keys(parsed.charts).length > 0) {
+        display = '宸蹭负鎮ㄧ敓鎴愬垎鏋愬浘琛ㄣ��'
+      } else {
+        display = '姝e湪涓烘偍灞曠ず鍒嗘瀽缁撴灉...'
+      }
+    }
+  } else if (startIdx !== -1) {
+    const lastBraceIdx = output.lastIndexOf('}')
+    if (lastBraceIdx !== -1 && lastBraceIdx > startIdx) {
+      const potentialJson = output.substring(startIdx, lastBraceIdx + 1)
+      try {
+        const parsed = JSON.parse(potentialJson)
+        // 鎴愬姛瑙f瀽锛岀Щ闄� JSON 閮ㄥ垎鏄剧ず鏂囧瓧
+        display = (output.substring(0, startIdx) + output.substring(lastBraceIdx + 1)).trim()
+
+        if (/^[\s}\],锛屻��.:锛�;锛沒+$/.test(display)) {
+          display = ''
+        }
+
+        if (parsed.description) {
+          display = parsed.action === 'confirm_required'
+            ? getPurchaseConfirmDescription(parsed)
+            : parsed.description
+        }
+
+        if (!display) {
+          if (parsed.type === 'todo_list') {
+            display = '宸蹭负鎮ㄦ暣鐞嗗ソ鐩稿叧鏁版嵁锛�'
+          } else if (parsed.charts && Object.keys(parsed.charts).length > 0) {
+            display = '宸蹭负鎮ㄧ敓鎴愬垎鏋愬浘琛細'
+          } else {
+            display = '姝e湪涓烘偍灞曠ず鍒嗘瀽缁撴灉...'
+          }
+        }
+      } catch (e) {
+        // 瑙f瀽澶辫触锛岃鏄� JSON 杩樺湪浼犺緭涓垨鏍煎紡涓嶆纭�
+        display = output.substring(0, startIdx).trim() || '姝e湪鍒嗘瀽鏁版嵁骞剁敓鎴愬浘琛�...'
+      }
+    } else {
+      // 鎵惧埌浜嗗紑濮嬩絾杩樻病鎵惧埌缁撴潫
+      display = output.substring(0, startIdx).trim() || '姝e湪鍒嗘瀽鏁版嵁骞剁敓鎴愬浘琛�...'
     }
   }
 
-  if (state.jsonBlockStartPos !== -1 && state.blockEndPos === -1) {
-    display = display.substring(0, state.jsonBlockStartPos)
-  } else if (state.jsBlockStartPos !== -1 && state.blockEndPos === -1) {
-    display = display.substring(0, state.jsBlockStartPos)
-  }
+  let html = convertTextToHtml(display)
 
-  display = display.replace(/```(javascript|js)([\s\S]*?)```/g, '<pre class="code-block js-code">$2</pre>')
-  display = display.replace(/```([\s\S]*?)```/g, '<pre class="code-block">$1</pre>')
+  // 杩樺師浠g爜鍧�
+  html = html.replace(/```(javascript|js)([\s\S]*?)```/g, '<pre class="code-block js-code">$2</pre>')
+  html = html.replace(/```([\s\S]*?)```/g, '<pre class="code-block">$1</pre>')
 
-  return convertTextToHtml(display)
+  return html || '...'
 }
 
 const renderCharts = (msgIndex, chartOptions) => {
@@ -747,14 +1777,27 @@
       const tryInit = (count = 0) => {
         const dom = document.getElementById(id)
         if (dom) {
-          if (chartInstances.value[id]) chartInstances.value[id].dispose()
+          if (chartInstances.value[id]) {
+            // 濡傛灉宸茬粡鍒濆鍖栬繃锛岀洿鎺ユ洿鏂版暟鎹�
+            const chart = chartInstances.value[id]
+            const option = normalizeAiChartOption(chartOptions[key])
+            if (option) chart.setOption(option)
+            return
+          }
+
           const chart = echarts.init(dom)
           chartInstances.value[id] = chart
-          chart.setOption(chartOptions[key])
+          const option = normalizeAiChartOption(chartOptions[key])
+          if (option) {
+            chart.setOption(option)
+          } else {
+            console.warn('Invalid chart option for:', id, chartOptions[key])
+          }
+
           const handler = () => chart.resize()
           resizeHandlers.value.push(handler)
           window.addEventListener('resize', handler)
-        } else if (count < 10) {
+        } else if (count < 15) { // 绋嶅井澧炲姞閲嶈瘯娆℃暟
           setTimeout(() => tryInit(count + 1), 200)
         }
       }
@@ -763,19 +1806,198 @@
   })
 }
 
+// 鏍煎紡鍖� AI 杩斿洖鐨勫浘琛ㄩ厤缃紝灏嗗叾杞崲涓烘爣鍑嗙殑 ECharts 閰嶇疆
+const formatChartOption = (rawOption) => {
+  if (!rawOption) return null
+
+  // 濡傛灉宸茬粡鏄爣鍑� ECharts 閰嶇疆锛堝寘鍚� series锛夛紝鍒欑洿鎺ヨ繑鍥�
+  const hasSeries = rawOption.series && Array.isArray(rawOption.series)
+
+  // 灏濊瘯杞崲绠�鏄撴牸寮�
+  try {
+    const isPie = rawOption.type === 'pie' || (rawOption.title && rawOption.title.includes('鍗犳瘮'))
+
+    const option = {
+      title: {
+        text: rawOption.title || '',
+        left: 'center',
+        textStyle: { fontSize: 14 }
+      },
+      tooltip: {
+        trigger: isPie ? 'item' : 'axis'
+      },
+      legend: {
+        bottom: '0'
+      },
+      grid: {
+        left: '3%',
+        right: '4%',
+        bottom: '15%',
+        containLabel: true
+      },
+      xAxis: isPie ? undefined : {
+        type: 'category',
+        data: rawOption.xAxisData || (Array.isArray(rawOption.xAxis) ? rawOption.xAxis : []),
+        name: typeof rawOption.xAxis === 'string' ? rawOption.xAxis : ''
+      },
+      yAxis: isPie ? undefined : {
+        type: 'value',
+        name: typeof rawOption.yAxis === 'string' ? rawOption.yAxis : ''
+      },
+      series: rawOption.series || [{
+        name: rawOption.title || '鏁板��',
+        type: rawOption.type || 'line',
+        data: rawOption.seriesData || (Array.isArray(rawOption.data) ? rawOption.data : []),
+        smooth: true,
+        radius: isPie ? '50%' : undefined
+      }]
+    }
+
+    // 閽堝楗煎浘鐨勭壒娈婂鐞�
+    if (isPie && !option.series[0].data && Array.isArray(rawOption.data)) {
+      option.series[0].data = rawOption.data
+    }
+
+    return option
+  } catch (err) {
+    console.error('Chart option conversion failed:', err)
+    return null
+  }
+}
+
+const normalizeAiChartOption = (rawOption) => {
+  if (!rawOption) return null
+
+  try {
+    const hasSeries = Array.isArray(rawOption.series) && rawOption.series.length > 0
+    const firstSeriesType = hasSeries ? rawOption.series[0]?.type : rawOption.type
+    const titleConfig = rawOption.title && typeof rawOption.title === 'object'
+      ? rawOption.title
+      : null
+    const tooltipConfig = rawOption.tooltip && typeof rawOption.tooltip === 'object'
+      ? rawOption.tooltip
+      : null
+    const legendConfig = rawOption.legend && typeof rawOption.legend === 'object'
+      ? rawOption.legend
+      : null
+    const rawXAxisConfig = rawOption.xAxis && typeof rawOption.xAxis === 'object' && !Array.isArray(rawOption.xAxis)
+      ? rawOption.xAxis
+      : null
+    const rawYAxisConfig = rawOption.yAxis && typeof rawOption.yAxis === 'object' && !Array.isArray(rawOption.yAxis)
+      ? rawOption.yAxis
+      : null
+    const titleText = typeof rawOption.title === 'string' ? rawOption.title : rawOption.title?.text || ''
+    const isPie = firstSeriesType === 'pie' || titleText.includes('鍗犳瘮')
+    const baseXAxisData = Array.isArray(rawOption.xAxisData)
+        ? rawOption.xAxisData
+        : (Array.isArray(rawOption.xAxis) ? rawOption.xAxis : (Array.isArray(rawXAxisConfig?.data) ? rawXAxisConfig.data : []))
+    const fallbackSeries = [{
+      name: titleText || '鏁版嵁',
+      type: rawOption.type || 'line',
+      data: Array.isArray(rawOption.seriesData) ? rawOption.seriesData : (Array.isArray(rawOption.data) ? rawOption.data : [])
+    }]
+    const normalizedSeries = (hasSeries ? rawOption.series : fallbackSeries).map((seriesItem, index) => {
+      const seriesType = seriesItem?.type || rawOption.type || 'line'
+      const nextSeries = {
+        ...seriesItem,
+        name: seriesItem?.name || titleText || `绯诲垪${index + 1}`,
+        type: seriesType
+      }
+
+      if (isPie) {
+        nextSeries.radius = nextSeries.radius || '55%'
+        nextSeries.data = Array.isArray(nextSeries.data) ? nextSeries.data : (Array.isArray(rawOption.data) ? rawOption.data : [])
+      } else {
+        nextSeries.smooth = typeof nextSeries.smooth === 'boolean' ? nextSeries.smooth : seriesType === 'line'
+        nextSeries.data = Array.isArray(nextSeries.data) ? nextSeries.data : []
+      }
+
+      return nextSeries
+    })
+    const categorySource = !isPie
+      ? normalizedSeries.find(seriesItem => Array.isArray(seriesItem.data) && seriesItem.data.every(item => item && typeof item === 'object' && 'name' in item && 'value' in item))
+      : null
+    const xAxisData = categorySource
+      ? categorySource.data.map(item => item.name)
+      : baseXAxisData
+    const finalSeries = !isPie && categorySource
+      ? normalizedSeries.map(seriesItem => ({
+          ...seriesItem,
+          data: Array.isArray(seriesItem.data)
+            ? seriesItem.data.map(item => (item && typeof item === 'object' && 'value' in item ? item.value : item))
+            : []
+        }))
+      : normalizedSeries
+
+    return {
+      title: titleConfig || {
+        text: titleText,
+        left: 'center',
+        textStyle: { fontSize: 14 }
+      },
+      tooltip: tooltipConfig || {
+        trigger: isPie ? 'item' : 'axis'
+      },
+      legend: legendConfig || {
+        bottom: '0'
+      },
+      grid: isPie ? undefined : {
+        left: '3%',
+        right: '4%',
+        bottom: '15%',
+        containLabel: true
+      },
+      xAxis: isPie ? undefined : {
+        ...(rawXAxisConfig || {}),
+        type: 'category',
+        data: xAxisData,
+        name: typeof rawOption.xAxis === 'string' ? rawOption.xAxis : (rawXAxisConfig?.name || '')
+      },
+      yAxis: isPie ? undefined : (rawYAxisConfig || {
+        type: 'value',
+        name: typeof rawOption.yAxis === 'string' ? rawOption.yAxis : ''
+      }),
+      series: finalSeries
+    }
+  } catch (err) {
+    console.error('AI chart normalization failed:', err, rawOption)
+    return formatChartOption(rawOption)
+  }
+}
+
 watch(messages, () => scrollToBottom(), { deep: true })
 </script>
 
-<style scoped lang="scss">
-.ai-chat-sidebar-wrapper {
-  position: fixed;
-  inset: 0;
-  z-index: 2000;
-  pointer-events: none;
+<style lang="scss">
+.ai-chat-overlay {
+  pointer-events: none !important;
+  background: transparent !important;
+}
 
-  :deep(.el-drawer__container) {
-    pointer-events: none;
-  }
+.ai-chat-overlay .el-drawer {
+  pointer-events: auto;
+}
+</style>
+
+<style scoped lang="scss">
+$primary-blue: #0055d4;
+$secondary-blue: #2e8ce0;
+$light-blue: #7ab8ff;
+$pale-blue: #c5dcff;
+$ice-white: #e8f2ff;
+$deep-blue: #003b8e;
+$deepest-blue: #002b66;
+$gradient-blue: linear-gradient(145deg, #004fc7 0%, #0066e0 40%, #2580e8 70%, #5a9fe0 100%);
+$gradient-dark: linear-gradient(145deg, #003b8e 0%, #0055d4 50%, #0077e8 100%);
+$gradient-ice: linear-gradient(180deg, #e0ecff 0%, #d4e5ff 50%, #e8f0ff 100%);
+$shadow-blue: 0 8px 40px rgba(0, 85, 212, 0.35);
+$shadow-deep: 0 12px 48px rgba(0, 40, 120, 0.4);
+$shadow-card: 0 6px 24px rgba(0, 51, 136, 0.12);
+
+.ai-chat-sidebar-wrapper {
+  position: static;
+  z-index: 2000;
+  pointer-events: auto;
 
   :deep(.el-drawer) {
     pointer-events: auto;
@@ -785,29 +2007,82 @@
 .ai-chat-trigger {
   pointer-events: auto;
   position: fixed;
-  right: 20px;
+  right: 24px;
   bottom: 100px;
-  width: 60px;
-  height: 60px;
-  background: linear-gradient(135deg, #409eff 0%, #007aff 100%);
+  width: 56px;
+  height: 56px;
+  background: $gradient-dark;
   border-radius: 50%;
   display: flex;
   align-items: center;
   justify-content: center;
   cursor: pointer;
-  box-shadow: 0 4px 12px rgba(0, 122, 255, 0.4);
-  transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+  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;
+
+  &::before {
+    content: '';
+    position: absolute;
+    inset: -6px;
+    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;
+  }
+
+  &::after {
+    content: '';
+    position: absolute;
+    inset: 0;
+    border-radius: 50%;
+    background: linear-gradient(135deg, rgba(255, 255, 255, 0.3) 0%, transparent 50%);
+    pointer-events: none;
+  }
 
   &:hover {
-    transform: scale(1.1) translateY(-5px);
-    box-shadow: 0 8px 20px rgba(0, 122, 255, 0.5);
+    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;
+    }
+
+    .trigger-icon {
+      transform: rotate(-8deg) scale(1.05);
+      filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.5));
+    }
   }
 
   .trigger-icon {
     display: flex;
     align-items: center;
     justify-content: center;
+    transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+    color: #fff;
+    filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
+  }
+}
+
+@keyframes triggerPulse {
+  0%, 100% {
+    box-shadow: $shadow-blue, 0 0 0 2px rgba(0, 85, 212, 0.25) inset, 0 0 20px rgba(0, 119, 232, 0.15);
+  }
+  50% {
+    box-shadow: $shadow-deep, 0 0 0 3px rgba(0, 136, 232, 0.35) inset, 0 0 40px rgba(0, 136, 232, 0.25);
+  }
+}
+
+@keyframes glowPulse {
+  0% {
+    opacity: 0.6;
+    transform: scale(1);
+  }
+  100% {
+    opacity: 1;
+    transform: scale(1.1);
   }
 }
 
@@ -815,13 +2090,15 @@
   :deep(.el-drawer__body) {
     padding: 0;
     overflow: hidden;
+    display: flex;
+    flex-direction: column;
+    height: 100%;
   }
   :deep(.el-drawer__header) {
     margin-bottom: 0;
-    padding: 12px 16px;
-    background: #fff;
-    border-bottom: 1px solid #ebeef5;
-    color: #303133;
+    padding: 0;
+    background: $gradient-dark;
+    color: #fff;
   }
 }
 
@@ -830,27 +2107,202 @@
   justify-content: space-between;
   align-items: center;
   width: 100%;
-  padding-right: 32px;
+  padding: 18px 20px;
+  background: $gradient-dark;
+  position: relative;
+  overflow: hidden;
+
+  &::before {
+    content: '';
+    position: absolute;
+    top: -60%;
+    right: -25%;
+    width: 250px;
+    height: 250px;
+    background: radial-gradient(circle, rgba(0, 136, 232, 0.4) 0%, transparent 70%);
+    pointer-events: none;
+    animation: headerGlow 4s ease-in-out infinite alternate;
+  }
+
+  &::after {
+    content: '';
+    position: absolute;
+    bottom: -40%;
+    left: -15%;
+    width: 200px;
+    height: 200px;
+    background: radial-gradient(circle, rgba(0, 85, 212, 0.3) 0%, transparent 70%);
+    pointer-events: none;
+    animation: headerGlow 5s ease-in-out infinite alternate-reverse;
+  }
 
   .header-left {
     display: flex;
     align-items: center;
-    gap: 8px;
-    
+    gap: 12px;
+    position: relative;
+    z-index: 1;
+
     .header-icon {
-      color: #409eff;
+      color: rgba(255, 255, 255, 0.95);
+      filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.2));
+      animation: iconFloat 3s ease-in-out infinite;
     }
-    
+
     .title {
-      font-size: 16px;
+      font-size: 17px;
       font-weight: 600;
-      color: #303133;
+      color: #fff;
+      text-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
+      letter-spacing: 0.5px;
     }
   }
 
   .header-actions {
     display: flex;
-    gap: 8px;
+    align-items: center;
+    gap: 10px;
+    position: relative;
+    z-index: 1;
+
+    .action-divider {
+      width: 1px;
+      height: 16px;
+      background: rgba(255, 255, 255, 0.2);
+      margin: 0 2px;
+    }
+
+    :deep(.el-button) {
+      color: rgba(255, 255, 255, 0.85);
+      transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+      background: rgba(255, 255, 255, 0.12);
+      border: 1px solid rgba(255, 255, 255, 0.1);
+      border-radius: 8px;
+      padding: 8px;
+      height: 32px;
+      width: 32px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+
+      &:hover {
+        color: #fff;
+        background: rgba(255, 255, 255, 0.25);
+        border-color: rgba(255, 255, 255, 0.3);
+        transform: translateY(-1px);
+        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+      }
+
+      &:active {
+        transform: translateY(0);
+      }
+
+      &.close-btn {
+        background: rgba(255, 255, 255, 0.1);
+        &:hover {
+          background: rgba(245, 108, 108, 0.8);
+          border-color: rgba(245, 108, 108, 0.5);
+        }
+      }
+    }
+
+    :deep(.header-action-btn) {
+      position: relative;
+      overflow: hidden;
+      background: linear-gradient(180deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.08));
+      border: 1px solid rgba(255, 255, 255, 0.16);
+      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14), 0 10px 18px rgba(0, 0, 0, 0.12);
+
+      &::before {
+        content: '';
+        position: absolute;
+        inset: 0;
+        background: linear-gradient(135deg, rgba(255, 255, 255, 0.22), transparent 55%);
+        pointer-events: none;
+      }
+
+      &::after {
+        content: '';
+        position: absolute;
+        top: -120%;
+        left: -40%;
+        width: 60%;
+        height: 260%;
+        background: linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.28), transparent);
+        transform: rotate(24deg);
+        opacity: 0;
+        transition: all 0.35s ease;
+      }
+
+      &:hover::after {
+        left: 100%;
+        opacity: 1;
+      }
+    }
+  }
+
+  .assistant-switcher {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex: 1;
+    padding: 0 12px;
+    position: relative;
+    z-index: 1;
+
+    :deep(.el-radio-group) {
+      display: flex;
+      gap: 6px;
+      flex-wrap: wrap;
+      justify-content: center;
+      padding: 4px;
+      border-radius: 999px;
+      background: linear-gradient(180deg, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0.08));
+      border: 1px solid rgba(255, 255, 255, 0.12);
+      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14), 0 10px 18px rgba(0, 0, 0, 0.1);
+    }
+
+    :deep(.el-radio-button__inner) {
+      border-radius: 999px;
+      border: 1px solid rgba(255, 255, 255, 0.18);
+      background: rgba(255, 255, 255, 0.12);
+      color: rgba(255, 255, 255, 0.86);
+      box-shadow: none;
+      padding: 7px 14px;
+      font-weight: 500;
+    }
+
+    :deep(.el-radio-button:first-child .el-radio-button__inner),
+    :deep(.el-radio-button:last-child .el-radio-button__inner) {
+      border-radius: 999px;
+    }
+
+    :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
+      background: #fff;
+      color: $primary-blue;
+      border-color: #fff;
+      box-shadow: 0 6px 14px rgba(0, 40, 120, 0.16);
+    }
+  }
+}
+
+@keyframes headerGlow {
+  0% {
+    opacity: 0.6;
+    transform: scale(1);
+  }
+  100% {
+    opacity: 1;
+    transform: scale(1.15);
+  }
+}
+
+@keyframes iconFloat {
+  0%, 100% {
+    transform: translateY(0) rotate(0);
+  }
+  50% {
+    transform: translateY(-2px) rotate(3deg);
   }
 }
 
@@ -858,61 +2310,122 @@
   display: flex;
   flex-direction: column;
   height: 100%;
-  background-color: #f5f7fa;
+  width: 100%;
+  background: $ice-white;
   position: relative;
+  overflow: hidden;
+
+  &::before {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    height: 240px;
+    background: linear-gradient(180deg, rgba(0, 85, 212, 0.06) 0%, transparent 100%);
+    pointer-events: none;
+  }
 }
 
 .history-panel {
   position: absolute;
   inset: 0;
-  background: #fff;
+  background: linear-gradient(180deg, #fff 0%, $ice-white 100%);
   z-index: 10;
   display: flex;
   flex-direction: column;
-  
+  box-shadow: -8px 0 32px rgba(0, 85, 212, 0.15);
+
   .history-header {
-    padding: 16px;
-    border-bottom: 1px solid #ebeef5;
+    padding: 18px 20px;
+    border-bottom: 1px solid rgba(0, 85, 212, 0.12);
     display: flex;
     justify-content: space-between;
     align-items: center;
     font-weight: 600;
     font-size: 14px;
+    color: $deep-blue;
+    background: linear-gradient(135deg, rgba(0, 85, 212, 0.08) 0%, rgba(0, 136, 232, 0.05) 100%);
+    position: relative;
+
+    &::after {
+      content: '';
+      position: absolute;
+      bottom: 0;
+      left: 0;
+      right: 0;
+      height: 1px;
+      background: linear-gradient(90deg, transparent, rgba(0, 85, 212, 0.2), transparent);
+    }
   }
 
   .session-list {
     flex: 1;
     overflow-y: auto;
-    padding: 8px;
+    padding: 12px 16px;
+
+    &::-webkit-scrollbar {
+      width: 8px;
+    }
+    &::-webkit-scrollbar-thumb {
+      background: linear-gradient(180deg, $secondary-blue, $primary-blue);
+      border-radius: 4px;
+      box-shadow: 0 0 6px rgba(0, 85, 212, 0.25);
+    }
 
     .session-item {
       display: flex;
       align-items: center;
-      padding: 12px;
-      margin-bottom: 4px;
-      border-radius: 8px;
+      padding: 14px 16px;
+      margin-bottom: 6px;
+      border-radius: 12px;
       cursor: pointer;
-      transition: all 0.2s;
-      gap: 10px;
+      transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+      gap: 12px;
       position: relative;
       border: 1px solid transparent;
+      background: #fff;
+      animation: sessionSlideIn 0.35s ease;
+
+      @keyframes sessionSlideIn {
+        from {
+          opacity: 0;
+          transform: translateX(-15px);
+        }
+        to {
+          opacity: 1;
+          transform: translateX(0);
+        }
+      }
 
       &:hover {
-        background-color: #f5f7fa;
+        background: linear-gradient(135deg, rgba(0, 85, 212, 0.06) 0%, rgba(0, 136, 232, 0.08) 100%);
+        border-color: rgba(0, 85, 212, 0.12);
+        box-shadow: 0 4px 16px rgba(0, 85, 212, 0.1);
+        transform: translateX(4px);
+
         .delete-btn {
           opacity: 1;
+          transform: scale(1);
         }
       }
 
       &.active {
-        background-color: #ecf5ff;
-        border-color: #d9ecff;
-        color: #409eff;
+        background: linear-gradient(135deg, rgba(0, 85, 212, 0.12) 0%, rgba(0, 136, 232, 0.15) 100%);
+        border-color: rgba(0, 85, 212, 0.25);
+        color: $primary-blue;
+        box-shadow: 0 4px 16px rgba(0, 85, 212, 0.15);
+
+        .el-icon {
+          color: $primary-blue;
+        }
       }
 
       .el-icon {
-        font-size: 16px;
+        font-size: 18px;
         flex-shrink: 0;
+        color: $secondary-blue;
+        transition: color 0.2s;
       }
 
       .session-name {
@@ -921,14 +2434,22 @@
         white-space: nowrap;
         overflow: hidden;
         text-overflow: ellipsis;
+        color: #1a1a2e;
+        font-weight: 500;
       }
 
       .delete-btn {
         opacity: 0;
-        transition: opacity 0.2s;
-        padding: 4px;
+        transform: scale(0.8);
+        transition: all 0.25s ease;
+        padding: 6px;
+        border-radius: 6px;
+        color: #c0c4cc;
+
         &:hover {
-          color: #f56c6c;
+          color: #fff;
+          background: rgba(245, 108, 108, 0.85);
+          transform: scale(1.1) rotate(8deg);
         }
       }
     }
@@ -946,58 +2467,98 @@
 .message-list {
   flex: 1;
   overflow-y: auto;
-  padding: 20px;
+  padding: 24px 20px;
   display: flex;
   flex-direction: column;
   gap: 20px;
+  background: linear-gradient(180deg, transparent 0%, rgba(0, 85, 212, 0.02) 100%);
 
   &::-webkit-scrollbar {
-    width: 6px;
+    width: 8px;
   }
   &::-webkit-scrollbar-thumb {
-    background: #dcdfe6;
-    border-radius: 3px;
+    background: linear-gradient(180deg, $secondary-blue, $primary-blue);
+    border-radius: 4px;
+    box-shadow: 0 0 8px rgba(0, 85, 212, 0.3);
   }
 }
 
 .message-item {
   display: flex;
-  gap: 12px;
+  gap: 14px;
   width: 100%;
+  animation: messageSlideIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+
+  @keyframes messageSlideIn {
+    from {
+      opacity: 0;
+      transform: translateY(20px) scale(0.95);
+    }
+    to {
+      opacity: 1;
+      transform: translateY(0) scale(1);
+    }
+  }
 
   .avatar {
-    width: 36px;
-    height: 36px;
-    border-radius: 8px;
+    width: 42px;
+    height: 42px;
+    border-radius: 14px;
     display: flex;
     align-items: center;
     justify-content: center;
     flex-shrink: 0;
-    font-size: 20px;
+    font-size: 24px;
+    position: relative;
+    overflow: hidden;
+
+    &::before {
+      content: '';
+      position: absolute;
+      inset: 0;
+      background: inherit;
+      filter: blur(10px);
+      opacity: 0.5;
+      z-index: -1;
+    }
+
+    &::after {
+      content: '';
+      position: absolute;
+      top: -50%;
+      left: -50%;
+      width: 200%;
+      height: 200%;
+      background: linear-gradient(45deg, transparent 40%, rgba(255, 255, 255, 0.2) 50%, transparent 60%);
+      animation: shimmer 3s infinite;
+    }
   }
 
   .message-content {
     flex: 1;
-    overflow-x: hidden; // 淇敼涓� hidden锛屽唴閮ㄥ鍣ㄥ鐞嗘粴鍔�
+    overflow-x: hidden;
     display: flex;
     flex-direction: column;
-    max-width: calc(100% - 48px); // 鍑忓幓澶村儚鍜岄棿璺�
-    
+    max-width: calc(100% - 56px);
+
     .text-box {
-      padding: 12px 16px;
-      border-radius: 12px;
+      padding: 14px 20px;
+      border-radius: 18px;
       font-size: 14px;
-      line-height: 1.6;
+      line-height: 1.7;
       word-break: break-word;
       max-width: 100%;
       width: fit-content;
       overflow-x: auto;
+      transition: all 0.3s ease;
+      position: relative;
+
       &::-webkit-scrollbar {
-        height: 6px;
+        height: 4px;
       }
       &::-webkit-scrollbar-thumb {
-        background: #dcdfe6;
-        border-radius: 3px;
+        background: rgba(0, 85, 212, 0.25);
+        border-radius: 2px;
       }
     }
   }
@@ -1007,13 +2568,26 @@
       align-items: flex-start;
     }
     .avatar {
-      background-color: #409eff;
+      background: $gradient-dark;
       color: #fff;
+      box-shadow: 0 6px 20px rgba(0, 85, 212, 0.35);
     }
     .text-box {
-      background-color: #fff;
-      color: #303133;
-      box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+      background: #fff;
+      color: #1a1a2e;
+      box-shadow: $shadow-card;
+      border: 1px solid rgba(0, 85, 212, 0.08);
+      border-top-left-radius: 6px;
+
+      &::before {
+        content: '';
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        height: 1px;
+        background: linear-gradient(90deg, rgba(0, 85, 212, 0.15), transparent);
+      }
     }
   }
 
@@ -1023,29 +2597,52 @@
       align-items: flex-end;
     }
     .avatar {
-      background-color: #95d475;
+      background: linear-gradient(145deg, #5a9fe0, #3d8bd4);
       color: #fff;
+      box-shadow: 0 6px 20px rgba(0, 85, 212, 0.4);
     }
     .text-box {
-      background-color: #409eff;
+      background: $gradient-dark;
       color: #fff;
+      border-top-right-radius: 6px;
+      box-shadow: 0 6px 24px rgba(0, 85, 212, 0.3);
+
+      &::before {
+        content: '';
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        height: 1px;
+        background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3));
+      }
     }
   }
 }
 
+@keyframes shimmer {
+  0% {
+    transform: translateX(-100%) rotate(45deg);
+  }
+  100% {
+    transform: translateX(100%) rotate(45deg);
+  }
+}
+
 .charts-wrapper {
-  margin-top: 10px;
+  margin-top: 12px;
   display: flex;
   flex-direction: column;
-  gap: 10px;
+  gap: 12px;
   overflow-x: auto;
   width: 100%;
   padding-bottom: 8px;
+
   &::-webkit-scrollbar {
     height: 6px;
   }
   &::-webkit-scrollbar-thumb {
-    background: #dcdfe6;
+    background: linear-gradient(90deg, $light-blue, $secondary-blue);
     border-radius: 3px;
   }
 }
@@ -1054,153 +2651,1159 @@
   width: 100%;
   min-width: 300px;
   height: 300px;
-  background: #fff;
-  border-radius: 8px;
-  padding: 10px;
+  border-radius: 12px;
+  padding: 12px;
+  margin-bottom: 12px;
 }
 
 .table-wrapper {
-  margin-top: 10px;
+  margin-top: 12px;
   background: #fff;
-  border-radius: 8px;
+  border-radius: 12px;
   overflow: hidden;
   overflow-x: auto;
   width: 100%;
+  box-shadow: $shadow-card;
+  border: 1px solid rgba(0, 122, 255, 0.06);
+
   &::-webkit-scrollbar {
     height: 6px;
   }
   &::-webkit-scrollbar-thumb {
-    background: #dcdfe6;
+    background: linear-gradient(90deg, $light-blue, $secondary-blue);
     border-radius: 3px;
   }
+
   .el-table {
     min-width: 300px;
+    --el-table-border-color: rgba(0, 122, 255, 0.08);
+    --el-table-header-bg-color: $ice-white;
+  }
+}
+
+.purchase-confirm-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;
+  color: #1a1a2e;
+}
+
+.purchase-confirm-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  font-size: 15px;
+  font-weight: 700;
+  margin-bottom: 12px;
+}
+
+.purchase-confirm-desc {
+  margin-bottom: 12px;
+  color: #374151;
+  font-size: 13px;
+  line-height: 1.6;
+}
+
+.purchase-empty-state {
+  margin-bottom: 12px;
+  padding: 12px;
+  border-radius: 10px;
+  background: linear-gradient(135deg, rgba(255, 247, 237, 0.96), rgba(255, 255, 255, 0.98));
+  border: 1px solid rgba(245, 158, 11, 0.25);
+
+  .empty-title {
+    font-size: 14px;
+    font-weight: 700;
+    color: #92400e;
+    margin-bottom: 6px;
+  }
+
+  .empty-desc {
+    color: #78350f;
+    font-size: 13px;
+    line-height: 1.6;
+  }
+}
+
+.purchase-alert {
+  border-radius: 8px;
+  padding: 10px 12px;
+  margin-bottom: 10px;
+  font-size: 13px;
+
+  ul {
+    margin: 6px 0 0;
+    padding-left: 18px;
+  }
+
+  &.warning {
+    background: rgba(230, 162, 60, 0.12);
+    color: #9a5b00;
+  }
+
+  &.missing {
+    display: flex;
+    flex-wrap: wrap;
+    align-items: center;
+    gap: 8px;
+    background: rgba(245, 108, 108, 0.1);
+    color: #b42318;
+  }
+}
+
+.purchase-preview {
+  margin-bottom: 12px;
+
+  ul {
+    margin: 6px 0 0;
+    padding-left: 18px;
+    font-size: 13px;
+    line-height: 1.7;
+  }
+}
+
+.purchase-section-title {
+  margin: 10px 0 6px;
+  font-size: 13px;
+  font-weight: 700;
+  color: $deep-blue;
+}
+
+.payload-editor {
+  :deep(.el-textarea__inner) {
+    font-family: Consolas, Monaco, monospace;
+    font-size: 12px;
+    line-height: 1.55;
+  }
+}
+
+.payload-editor-tip {
+  margin-top: 6px;
+  font-size: 12px;
+  line-height: 1.5;
+  color: #6b7280;
+}
+
+.purchase-confirm-actions {
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  gap: 12px;
+  margin-top: 12px;
+
+  .confirm-result {
+    flex: 1;
+    font-size: 13px;
+
+    &.success {
+      color: #1f9d55;
+    }
+
+    &.error {
+      color: #d93025;
+    }
   }
 }
 
 .input-area {
-  padding: 16px;
-  background-color: #fff;
-  border-top: 1px solid #dcdfe6;
+  padding: 18px 20px;
+  background: linear-gradient(180deg, rgba(232, 242, 255, 0.95) 0%, #fff 100%);
+  border-top: 1px solid rgba(0, 85, 212, 0.1);
+  position: relative;
+
+  &::before {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 20px;
+    right: 20px;
+    height: 1px;
+    background: linear-gradient(90deg, transparent, rgba(0, 85, 212, 0.15), transparent);
+  }
 
   .input-actions {
-  display: flex;
-  gap: 12px;
-  margin-bottom: 8px;
-  align-items: center;
-
-  .file-upload-trigger {
-    display: inline-flex;
+    display: flex;
+    gap: 14px;
+    margin-bottom: 12px;
     align-items: center;
+
+    .file-upload-trigger {
+      display: inline-flex;
+      align-items: center;
+    }
+
+    :deep(.utility-action-btn) {
+      position: relative;
+      height: 34px;
+      padding: 0 14px;
+      border-radius: 999px;
+      border: 1px solid rgba(92, 119, 255, 0.18);
+      background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(236, 243, 255, 0.98));
+      color: $primary-blue;
+      font-weight: 600;
+      box-shadow: 0 10px 20px rgba(0, 85, 212, 0.08);
+      transition: all 0.25s ease;
+
+      .el-icon {
+        margin-right: 5px;
+      }
+
+      &:hover:not(.is-disabled) {
+        color: #fff;
+        border-color: transparent;
+        background: linear-gradient(135deg, #1f6dff 0%, #6b38ef 100%);
+        box-shadow: 0 14px 24px rgba(64, 90, 255, 0.2);
+        transform: translateY(-1px);
+      }
+    }
+
+    :deep(.stop-action-btn) {
+      border-color: rgba(255, 99, 123, 0.18);
+      color: #d33e5e;
+
+      &:hover:not(.is-disabled) {
+        background: linear-gradient(135deg, #f5536e 0%, #a33cff 100%);
+      }
+    }
   }
-}
 
   .input-box {
-    padding: 12px;
+    padding: 16px;
     position: relative;
     background: #fff;
-    border: 1px solid #dcdfe6;
-    border-radius: 8px;
-    margin: 0 16px 16px;
-    transition: border-color 0.2s;
+    border: 2px solid rgba(0, 85, 212, 0.12);
+    border-radius: 16px;
+    margin: 0 4px;
+    transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
 
     &:focus-within {
-      border-color: #409eff;
+      border-color: $primary-blue;
+      box-shadow: 0 0 0 4px rgba(0, 85, 212, 0.12), $shadow-deep;
+      transform: translateY(-2px);
+      background: #fff;
+    }
+
+    .selected-file-list {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 8px;
+      margin-bottom: 12px;
     }
 
     .selected-file-tag {
       display: flex;
       align-items: center;
-      background: #f0f7ff;
-      border: 1px solid #d9ecff;
-      border-radius: 4px;
-      padding: 4px 8px;
-      margin-bottom: 8px;
-      gap: 6px;
+      background: linear-gradient(135deg, rgba(0, 85, 212, 0.1) 0%, rgba(0, 136, 232, 0.15) 100%);
+      border: 1px solid rgba(0, 85, 212, 0.2);
+      border-radius: 10px;
+      padding: 8px 12px;
+      gap: 10px;
       width: fit-content;
       max-width: 100%;
+      animation: tagSlideIn 0.3s ease;
+
+      @keyframes tagSlideIn {
+        from {
+          opacity: 0;
+          transform: translateX(-10px);
+        }
+        to {
+          opacity: 1;
+          transform: translateX(0);
+        }
+      }
 
       .el-icon {
-        color: #409eff;
-        font-size: 14px;
+        color: $primary-blue;
+        font-size: 18px;
       }
 
       .file-name {
-        font-size: 12px;
-        color: #606266;
+        font-size: 13px;
+        color: $deep-blue;
         white-space: nowrap;
         overflow: hidden;
         text-overflow: ellipsis;
+        font-weight: 600;
       }
 
       .remove-file {
         cursor: pointer;
-        color: #909399;
-        transition: color 0.2s;
+        color: $secondary-blue;
+        transition: all 0.2s;
+        padding: 4px;
+        border-radius: 50%;
+
         &:hover {
-          color: #f56c6c;
+          color: #fff;
+          background: rgba(245, 108, 108, 0.8);
+          transform: scale(1.1) rotate(90deg);
         }
       }
     }
 
     :deep(.el-textarea__inner) {
       padding: 0;
+      padding-bottom: 35px;
       border: none;
       box-shadow: none;
       background: transparent;
       font-family: inherit;
       font-size: 14px;
-      line-height: 1.5;
-      color: #303133;
+      line-height: 1.6;
+      color: #1a1a2e;
+
       &::placeholder {
-        color: #c0c4cc;
+        color: #7ab8ff;
+      }
+
+      &:focus {
+        box-shadow: none;
       }
     }
 
     .send-btn {
       position: absolute;
-      right: 12px;
-      bottom: 12px;
-      padding: 8px 16px;
+      right: 16px;
+      bottom: 16px;
+      padding: 10px 22px;
+      background: $gradient-dark;
+      border: none;
+      border-radius: 10px;
+      font-weight: 600;
+      font-size: 14px;
+      color: #fff;
+      box-shadow: 0 6px 20px rgba(0, 85, 212, 0.4);
+      transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+      overflow: hidden;
+      display: inline-flex;
+      align-items: center;
+      gap: 6px;
+      letter-spacing: 0.3px;
+
+      &::before {
+        content: '';
+        position: absolute;
+        top: 0;
+        left: -100%;
+        width: 100%;
+        height: 100%;
+        background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
+        transition: left 0.5s;
+      }
+
+      &:hover:not(:disabled) {
+        transform: translateY(-3px) scale(1.02);
+        box-shadow: 0 10px 30px rgba(0, 85, 212, 0.5);
+
+        &::before {
+          left: 100%;
+        }
+      }
+
+      &:active:not(:disabled) {
+        transform: translateY(-1px) scale(0.98);
+      }
+
+      &:disabled {
+        background: linear-gradient(145deg, #b0b0b0, #c5c5c5);
+        box-shadow: none;
+        cursor: not-allowed;
+      }
+
+      .el-icon {
+        font-size: 15px;
+        transform: translateY(-1px);
+      }
     }
   }
 }
 
 .typing-indicator {
   display: flex;
-  gap: 4px;
-  padding: 8px 12px;
+  gap: 5px;
+  padding: 10px 14px;
   background: #fff;
-  border-radius: 12px;
+  border-radius: 14px;
   width: fit-content;
-  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
-  margin-top: 4px;
+  box-shadow: $shadow-card;
+  margin-top: 6px;
+  border: 1px solid rgba(0, 122, 255, 0.06);
+  border-top-left-radius: 4px;
+
   .dot {
-    width: 6px;
-    height: 6px;
-    background-color: #909399;
+    width: 7px;
+    height: 7px;
+    background: $secondary-blue;
     border-radius: 50%;
     animation: typing 1.4s infinite ease-in-out;
-    &:nth-child(2) { animation-delay: 0.2s; }
-    &:nth-child(3) { animation-delay: 0.4s; }
+
+    &:nth-child(2) {
+      animation-delay: 0.2s;
+      background: $primary-blue;
+    }
+    &:nth-child(3) {
+      animation-delay: 0.4s;
+      background: $deep-blue;
+    }
   }
 }
 
 @keyframes typing {
-  0%, 80%, 100% { transform: scale(0); }
-  40% { transform: scale(1); }
+  0%, 80%, 100% {
+    transform: scale(0.6);
+    opacity: 0.4;
+  }
+  40% {
+    transform: scale(1);
+    opacity: 1;
+  }
 }
 
 .code-block {
-  background: #2d2d2d;
-  color: #ccc;
-  padding: 12px;
-  border-radius: 6px;
-  font-family: monospace;
-  margin: 8px 0;
+  background: linear-gradient(145deg, #1a1a2e, #16213e);
+  color: #a8d8ff;
+  padding: 14px;
+  border-radius: 10px;
+  font-family: 'Fira Code', 'Consolas', monospace;
+  margin: 10px 0;
   overflow-x: auto;
+  border: 1px solid rgba(90, 200, 250, 0.15);
+  box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.2);
+
   &.js-code {
-    color: #f08d49;
+    color: #5ac8fa;
+  }
+}
+
+.chat-main {
+  background:
+    radial-gradient(circle at top left, rgba(46, 140, 224, 0.12) 0%, transparent 34%),
+    linear-gradient(180deg, #fff 0%, #f7fbff 46%, #fff 100%);
+}
+
+.chat-hero {
+  display: grid;
+  grid-template-columns: 164px minmax(0, 1fr);
+  gap: 18px;
+  align-items: start;
+  padding: 14px 18px 6px;
+
+  &.compact {
+    grid-template-columns: 122px minmax(0, 1fr);
+    gap: 12px;
+    padding: 8px 18px 2px;
+  }
+}
+
+.assistant-stand {
+  position: relative;
+  min-height: 252px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+  padding-top: 18px;
+  overflow: hidden;
+
+  &.compact {
+    min-height: 176px;
+    padding-top: 8px;
+  }
+
+  &.thinking {
+    .assistant-halo {
+      opacity: 1;
+      transform: scale(1.08);
+      filter: blur(8px);
+    }
+
+    .assistant-scan-ring {
+      opacity: 1;
+      animation-duration: 1.6s;
+    }
+
+    .assistant-orbit {
+      opacity: 1;
+    }
+
+    .assistant-bot {
+      transform: translateY(-4px) scale(1.02);
+    }
+
+    .assistant-bot-head {
+      box-shadow: 0 0 30px rgba(80, 157, 255, 0.36);
+    }
+
+    .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-status {
+      color: #6a3bee;
+      box-shadow: 0 10px 22px rgba(106, 59, 238, 0.14);
+    }
+
+    .assistant-status-dot {
+      background: #6a3bee;
+      box-shadow: 0 0 12px rgba(106, 59, 238, 0.9);
+      animation: thinkingDot 1s ease-in-out infinite;
+    }
+
+    .assistant-base-sm {
+      box-shadow: 0 0 24px rgba(255, 93, 122, 0.48);
+    }
+  }
+}
+
+.assistant-halo {
+  position: absolute;
+  top: 22px;
+  width: 130px;
+  height: 130px;
+  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%);
+  filter: blur(6px);
+  opacity: 0.82;
+  transition: all 0.35s ease;
+}
+
+.assistant-scan-ring {
+  position: absolute;
+  top: 40px;
+  width: 132px;
+  height: 132px;
+  border-radius: 50%;
+  border: 1px solid rgba(90, 159, 224, 0.22);
+  box-shadow: inset 0 0 16px rgba(255, 255, 255, 0.25);
+  opacity: 0.55;
+  animation: scanRing 4s linear infinite;
+}
+
+.assistant-orbit {
+  position: absolute;
+  top: 52px;
+  width: 150px;
+  height: 150px;
+  border-radius: 50%;
+  border: 1px dashed rgba(92, 135, 255, 0.22);
+  opacity: 0.45;
+}
+
+.assistant-orbit-a {
+  animation: orbitRotate 8s linear infinite;
+}
+
+.assistant-orbit-b {
+  width: 118px;
+  height: 118px;
+  top: 68px;
+  border-color: rgba(255, 108, 150, 0.22);
+  animation: orbitRotateReverse 5.6s linear infinite;
+}
+
+.assistant-bot {
+  position: relative;
+  z-index: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  margin-top: 12px;
+  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%;
+    transform: translateX(-50%);
+    background: linear-gradient(135deg, #54bfff, #7a41ff);
+    box-shadow: 0 0 14px rgba(84, 191, 255, 0.65);
+  }
+}
+
+.assistant-bot-antenna-left {
+  left: 36px;
+  transform: rotate(-14deg);
+}
+
+.assistant-bot-antenna-right {
+  right: 36px;
+  transform: rotate(14deg);
+}
+
+.assistant-bot-head {
+  position: relative;
+  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);
+}
+
+.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 {
+  position: absolute;
+  left: 50%;
+  bottom: 16px;
+  width: 22px;
+  height: 4px;
+  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);
+}
+
+.assistant-status {
+  position: relative;
+  z-index: 1;
+  margin-top: 14px;
+  padding: 6px 12px;
+  border-radius: 999px;
+  font-size: 12px;
+  font-weight: 600;
+  color: $deep-blue;
+  background: rgba(255, 255, 255, 0.95);
+  border: 1px solid rgba(0, 85, 212, 0.12);
+  box-shadow: 0 8px 20px rgba(0, 85, 212, 0.08);
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.assistant-status-dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background: #2e8ce0;
+  box-shadow: 0 0 10px rgba(46, 140, 224, 0.72);
+}
+
+.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;
+}
+
+.assistant-base-md {
+  bottom: 6px;
+  width: 88px;
+  height: 20px;
+  border-color: rgba(255, 93, 122, 0.34);
+}
+
+.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));
+  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);
+  }
+}
+
+@keyframes orbitRotate {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+@keyframes orbitRotateReverse {
+  from {
+    transform: rotate(360deg);
+  }
+  to {
+    transform: rotate(0deg);
+  }
+}
+
+@keyframes scanRing {
+  0%, 100% {
+    transform: scale(0.96);
+    opacity: 0.42;
+  }
+  50% {
+    transform: scale(1.04);
+    opacity: 0.86;
+  }
+}
+
+@keyframes thinkingDot {
+  0%, 100% {
+    transform: scale(1);
+  }
+  50% {
+    transform: scale(1.35);
+  }
+}
+
+.welcome-card {
+  position: relative;
+  padding: 14px 14px 12px;
+  border-radius: 16px;
+  background:
+    linear-gradient(#fff, #fff) padding-box,
+    linear-gradient(135deg, rgba(255, 64, 96, 0.85), rgba(117, 65, 255, 0.9)) border-box;
+  border: 1px solid transparent;
+  box-shadow: 0 16px 36px rgba(0, 85, 212, 0.12);
+
+  &.compact {
+    padding: 10px 12px;
+    border-radius: 12px;
+    box-shadow: 0 8px 16px rgba(0, 85, 212, 0.07);
+
+    .welcome-eyebrow {
+      margin-bottom: 4px;
+    }
+
+    .welcome-title {
+      font-size: 17px;
+      line-height: 1.3;
+
+      br {
+        display: none;
+      }
+    }
+
+    .welcome-desc {
+      margin-top: 6px;
+      font-size: 12px;
+      line-height: 1.55;
+    }
+
+    .quick-prompt-list {
+      margin-top: 10px;
+      gap: 6px;
+    }
+
+    .quick-prompt-btn {
+      padding: 8px 10px;
+      font-size: 12px;
+      border-radius: 7px;
+    }
+
+    .more-prompts-btn {
+      margin-top: 8px;
+      font-size: 12px;
+    }
+  }
+}
+
+.welcome-eyebrow {
+  font-size: 11px;
+  font-weight: 700;
+  letter-spacing: 2px;
+  color: rgba(0, 85, 212, 0.58);
+  margin-bottom: 8px;
+}
+
+.welcome-title {
+  margin: 0;
+  font-size: 26px;
+  line-height: 1.2;
+  font-weight: 800;
+  color: #172033;
+}
+
+.welcome-desc {
+  margin: 10px 0 0;
+  font-size: 13px;
+  line-height: 1.7;
+  color: #5f6980;
+}
+
+.quick-prompt-list {
+  display: grid;
+  gap: 8px;
+  margin-top: 14px;
+}
+
+.quick-prompt-btn {
+  width: 100%;
+  border: none;
+  border-radius: 10px;
+  padding: 11px 14px;
+  text-align: left;
+  font-size: 13px;
+  font-weight: 600;
+  color: #fff;
+  cursor: pointer;
+  background: linear-gradient(90deg, #ff4c55 0%, #7c38ef 100%);
+  box-shadow: 0 12px 22px rgba(124, 56, 239, 0.18);
+  transition: transform 0.25s ease, box-shadow 0.25s ease, opacity 0.2s ease;
+  position: relative;
+  overflow: hidden;
+
+  &::before {
+    content: '';
+    position: absolute;
+    inset: 0;
+    background: linear-gradient(135deg, rgba(255, 255, 255, 0.22), transparent 56%);
+    pointer-events: none;
+  }
+
+  &::after {
+    content: '';
+    position: absolute;
+    top: -120%;
+    left: -30%;
+    width: 45%;
+    height: 260%;
+    background: linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.3), transparent);
+    transform: rotate(22deg);
+    opacity: 0;
+    transition: all 0.35s ease;
+  }
+
+  &:hover:not(:disabled) {
+    transform: translateY(-2px) scale(1.01);
+    box-shadow: 0 16px 28px rgba(124, 56, 239, 0.24);
+
+    &::after {
+      left: 100%;
+      opacity: 1;
+    }
+  }
+
+  &:disabled {
+    cursor: not-allowed;
+    opacity: 0.65;
+  }
+}
+
+.more-prompts-btn {
+  margin-top: 10px;
+  padding: 0 12px;
+  height: 32px;
+  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-weight: 600;
+  cursor: pointer;
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  box-shadow: 0 10px 18px rgba(208, 65, 81, 0.08);
+  transition: all 0.25s ease;
+
+  &:hover {
+    transform: translateY(-1px);
+    background: linear-gradient(135deg, #ff5570 0%, #8a3df6 100%);
+    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));
+  }
+}
+
+.message-list {
+  padding: 8px 18px 18px;
+  gap: 16px;
+  background: transparent;
+}
+
+.input-area {
+  padding: 12px 18px 16px;
+  background: #fff;
+  border-top: none;
+
+  &::before {
+    display: none;
+  }
+
+  .input-box {
+    padding: 14px 16px 16px;
+    border: 1px solid rgba(123, 56, 239, 0.9);
+    border-radius: 22px;
+    margin: 0;
+    transition: all 0.25s ease;
+    box-shadow: 0 14px 34px rgba(0, 85, 212, 0.08);
+
+    &:focus-within {
+      border-color: #7c38ef;
+      box-shadow: 0 0 0 3px rgba(124, 56, 239, 0.1), 0 18px 40px rgba(0, 85, 212, 0.12);
+      transform: none;
+    }
+
+    :deep(.el-textarea__inner) {
+      padding-right: 58px;
+      padding-bottom: 0;
+      min-height: 104px;
+
+      &::placeholder {
+        color: #a0a9bc;
+      }
+    }
+
+    .send-btn {
+      right: 25px;
+      top: 50%;
+      transform: translateY(-50%);
+      width: 36px;
+      min-width: 36px;
+      height: 36px;
+      padding: 0;
+      background: linear-gradient(135deg, #ff5570 0%, #7a36f2 58%, #2d79ff 100%);
+      border-radius: 50%;
+      box-shadow: 0 12px 24px rgba(109, 50, 236, 0.24);
+      transition: all 0.25s ease;
+      gap: 0;
+
+      &:hover:not(:disabled) {
+        transform: translateY(calc(-50% - 1px)) scale(1.04);
+        box-shadow: 0 16px 28px rgba(109, 50, 236, 0.3);
+      }
+
+      &:active:not(:disabled) {
+        transform: translateY(-50%) scale(0.96);
+      }
+
+      .el-icon {
+        margin: 0;
+        font-size: 16px;
+        transform: translate(0, -1px);
+      }
+    }
+  }
+}
+
+@media (max-width: 767px) {
+  .chat-hero {
+    grid-template-columns: 1fr;
+    gap: 10px;
+    padding: 14px 14px 6px;
+
+    &.compact {
+      padding: 8px 14px 4px;
+    }
+  }
+
+  .assistant-stand {
+    min-height: 184px;
+  }
+
+  .welcome-card {
+    padding: 12px 12px 10px;
+  }
+
+  .welcome-title {
+    font-size: 21px;
+  }
+
+  .hero-dot-grid {
+    grid-template-columns: repeat(12, 1fr);
+    gap: 6px;
+    padding: 0 14px 12px;
+  }
+
+  .message-list {
+    padding: 8px 14px 14px;
+  }
+
+  .input-area {
+    padding: 10px 14px 14px;
+  }
+
+  .input-area .input-actions {
+    gap: 10px;
+    flex-wrap: wrap;
   }
 }
 </style>

--
Gitblit v1.9.3