Просмотр исходного кода

优化在线回答,与在线模型切换

zkn 3 дней назад
Родитель
Сommit
faf0ec5130

+ 23 - 6
shudao-chat-py/routers/total.py

@@ -67,9 +67,16 @@ async def get_policy_file(
 
 
 
 
 @router.get("/get_function_card")
 @router.get("/get_function_card")
-async def get_function_card(db: Session = Depends(get_db)):
+async def get_function_card(
+    function_type: Optional[int] = None,
+    db: Session = Depends(get_db)
+):
     """获取功能卡片"""
     """获取功能卡片"""
-    cards = db.query(FunctionCard).limit(4).all()
+    query = db.query(FunctionCard).filter(FunctionCard.is_deleted == 0)
+    if function_type is not None:
+        query = query.filter(FunctionCard.function_type == function_type)
+
+    cards = query.order_by(FunctionCard.id.asc()).limit(4).all()
     return {
     return {
         "statusCode": 200,
         "statusCode": 200,
         "msg": "success",
         "msg": "success",
@@ -87,10 +94,19 @@ async def get_function_card(db: Session = Depends(get_db)):
 
 
 
 
 @router.get("/get_hot_question")
 @router.get("/get_hot_question")
-async def get_hot_question(db: Session = Depends(get_db)):
+async def get_hot_question(
+    question_type: Optional[int] = None,
+    db: Session = Depends(get_db)
+):
     """获取热点问题(按点击量排序)"""
     """获取热点问题(按点击量排序)"""
-    questions = db.query(HotQuestion).order_by(
-        HotQuestion.click_count.desc()).limit(3).all()
+    query = db.query(HotQuestion).filter(HotQuestion.is_deleted == 0)
+    if question_type is not None:
+        query = query.filter(HotQuestion.question_type == question_type)
+
+    questions = query.order_by(
+        HotQuestion.click_count.desc(),
+        HotQuestion.id.asc()
+    ).limit(3).all()
     return {
     return {
         "statusCode": 200,
         "statusCode": 200,
         "msg": "success",
         "msg": "success",
@@ -98,7 +114,8 @@ async def get_hot_question(db: Session = Depends(get_db)):
             {
             {
                 "id": q.id,
                 "id": q.id,
                 "question": q.question,
                 "question": q.question,
-                "click_count": q.click_count or 0
+                "click_count": q.click_count or 0,
+                "question_type": q.question_type
             }
             }
             for q in questions
             for q in questions
         ]
         ]

+ 2 - 2
shudao-vue-frontend/src/components/CategoryTitle.vue

@@ -32,7 +32,7 @@ const props = defineProps({
 })
 })
 
 
 const emit = defineEmits(['toggle'])
 const emit = defineEmits(['toggle'])
-const isExpanded = ref(true)
+const isExpanded = ref(false)
 
 
 const toggleExpand = () => {
 const toggleExpand = () => {
   isExpanded.value = !isExpanded.value
   isExpanded.value = !isExpanded.value
@@ -40,7 +40,7 @@ const toggleExpand = () => {
 }
 }
 
 
 onMounted(() => {
 onMounted(() => {
-  emit('toggle', { category: props.category, expanded: true })
+  emit('toggle', { category: props.category, expanded: false })
 })
 })
 </script>
 </script>
 
 

+ 332 - 111
shudao-vue-frontend/src/views/Chat.vue

@@ -104,50 +104,24 @@
             </div>
             </div>
             
             
             <!-- 如果没有数据,显示默认卡片 -->
             <!-- 如果没有数据,显示默认卡片 -->
-            <div v-if="functionCards.length === 0" class="function-card" @click="handleFunctionCard('桥梁结构设计问题')">
-              <div class="card-header">
-                <div class="card-icon">
-                  <img :src="bridgeIcon" alt="桥梁结构设计问题" class="card-icon-img">
-                </div>
-                <h4>桥梁结构设计问题</h4>
-              </div>
-              <div class="card-description">
-                <p>各类桥梁结构设计,计算与分析</p>
-              </div>
-            </div>
-            <div v-if="functionCards.length === 0" class="function-card" @click="handleFunctionCard('桥梁施工技术咨询')">
-              <div class="card-header">
-                <div class="card-icon">
-                  <img :src="constructionIcon" alt="施工技术咨询" class="card-icon-img">
-                </div>
-                <h4>施工技术咨询</h4>
-              </div>
-              <div class="card-description">
-                <p>桥梁施工方法,工艺与技术要点</p>
-              </div>
-            </div>
-            <div v-if="functionCards.length === 0" class="function-card" @click="handleFunctionCard('材料与力学问题')">
-              <div class="card-header">
-                <div class="card-icon">
-                  <img :src="materialIcon" alt="材料与力学问题" class="card-icon-img">
+            <template v-if="functionCards.length === 0">
+              <div
+                v-for="(card, index) in defaultFunctionCards"
+                :key="`default-card-${index}`"
+                class="function-card"
+                @click="handleFunctionCard(card.title)"
+              >
+                <div class="card-header">
+                  <div class="card-icon">
+                    <img :src="card.icon" :alt="card.title" class="card-icon-img">
+                  </div>
+                  <h4>{{ card.title }}</h4>
                 </div>
                 </div>
-                <h4>材料与力学问题</h4>
-              </div>
-              <div class="card-description">
-                <p>建筑材料性能与结构力学分析</p>
-              </div>
-            </div>
-            <div v-if="functionCards.length === 0" class="function-card" @click="handleFunctionCard('规范标准查询')">
-              <div class="card-header">
-                <div class="card-icon">
-                  <img :src="standardIcon" alt="规范标准查询" class="card-icon-img">
+                <div class="card-description">
+                  <p>{{ card.description }}</p>
                 </div>
                 </div>
-                <h4>规范标准查询</h4>
               </div>
               </div>
-              <div class="card-description">
-                <p>行业规范,标准解读与应用</p>
-              </div>
-            </div>
+            </template>
           </div>
           </div>
         </div>
         </div>
 
 
@@ -433,18 +407,17 @@
           </div>
           </div>
           
           
           <!-- 如果没有数据,显示默认问题 -->
           <!-- 如果没有数据,显示默认问题 -->
-          <div v-if="hotQuestions.length === 0" class="question-tag" @click="handleRecommendedQuestion('施工安全生产责任的规定')">
-            <img :src="questionIcon1" alt="问题" class="question-icon">
-            施工安全生产责任的规定
-          </div>
-          <div v-if="hotQuestions.length === 0" class="question-tag" @click="handleRecommendedQuestion('工程建设质量的要求')">
-            <img :src="questionIcon2" alt="问题" class="question-icon">
-            工程建设质量的要求
-          </div>
-          <div v-if="hotQuestions.length === 0" class="question-tag" @click="handleRecommendedQuestion('公路桥梁加固设计规范')">
-            <img :src="questionIcon3" alt="文档" class="question-icon">
-            公路桥梁加固设计规范
-          </div>
+          <template v-if="hotQuestions.length === 0">
+            <div 
+              v-for="(question, index) in defaultHotQuestions" 
+              :key="`default-${index}`"
+              class="question-tag" 
+              @click="handleRecommendedQuestion(question)"
+            >
+              <img :src="getQuestionIcon(question)" alt="问题" class="question-icon">
+              {{ question }}
+            </div>
+          </template>
         </div>
         </div>
 
 
         <!-- 用户推荐问题区域 -->
         <!-- 用户推荐问题区域 -->
@@ -707,15 +680,142 @@ const isOnlineModel = ref(true)  // 是否使用在线大模型
 // 切换模型类型
 // 切换模型类型
 const toggleModelType = () => {
 const toggleModelType = () => {
   isOnlineModel.value = !isOnlineModel.value
   isOnlineModel.value = !isOnlineModel.value
-  showToast(isOnlineModel.value ? '已切换至在线大模型 (glm-4-plus)' : '已切换至本地模型')
+  showToast(
+    isOnlineModel.value ? '启动在线大模型 (glm-4-plus)' : '已禁用在线大模型'
+  )
 }
 }
 
 
-// 当前激活的模块模式:'ai-qa', 'ai-writing', 'safety-training', 'exam-workshop'
 const currentMode = ref('ai-qa')
 const currentMode = ref('ai-qa')
+const hasUserSelectedMode = ref(false)
+
+const defaultHotQuestionsByMode = {
+  'ai-qa': [
+    '施工安全生产责任的规定',
+    '工程建设质量的要求',
+    '公路桥梁加固设计规范'
+  ],
+  'ai-writing': [
+    '生成施工安全培训通知',
+    '编写安全生产责任制范本',
+    '生成隐患排查整改方案'
+  ],
+  'safety-training': [
+    '新员工安全培训内容有哪些',
+    '特种作业人员安全培训要求',
+    '安全培训档案应包含哪些内容'
+  ],
+  'exam-workshop': [
+    '生成安全培训考试题库',
+    '安全生产法规知识点整理',
+    '常见隐患排查考试题'
+  ]
+}
+
+const defaultHotQuestions = computed(
+  () => defaultHotQuestionsByMode[currentMode.value] || defaultHotQuestionsByMode['ai-qa']
+)
+
+const defaultFunctionCardsByMode = {
+  'ai-qa': [
+    {
+      title: '桥梁结构设计问题',
+      description: '各类桥梁结构设计,计算与分析',
+      icon: bridgeIcon
+    },
+    {
+      title: '施工技术咨询',
+      description: '桥梁施工方法,工艺与技术要点',
+      icon: constructionIcon
+    },
+    {
+      title: '材料与力学问题',
+      description: '建筑材料性能与结构力学分析',
+      icon: materialIcon
+    },
+    {
+      title: '规范标准查询',
+      description: '行业规范,标准解读与应用',
+      icon: standardIcon
+    }
+  ],
+  'ai-writing': [
+    {
+      title: '安全培训通知',
+      description: '生成培训通知、计划与执行安排',
+      icon: bridgeIcon
+    },
+    {
+      title: '制度与方案编写',
+      description: '生成制度、方案、流程与模板',
+      icon: constructionIcon
+    },
+    {
+      title: '隐患整改方案',
+      description: '生成整改措施与闭环流程',
+      icon: materialIcon
+    },
+    {
+      title: '会议纪要模板',
+      description: '生成会议纪要与行动项清单',
+      icon: standardIcon
+    }
+  ],
+  'safety-training': [
+    {
+      title: '新员工安全培训',
+      description: '入职培训要点与必修内容',
+      icon: bridgeIcon
+    },
+    {
+      title: '特种作业培训',
+      description: '特种作业上岗培训与复训要求',
+      icon: constructionIcon
+    },
+    {
+      title: '安全教育制度',
+      description: '安全教育制度与实施流程',
+      icon: materialIcon
+    },
+    {
+      title: '培训档案管理',
+      description: '培训记录与档案规范管理',
+      icon: standardIcon
+    }
+  ],
+  'exam-workshop': [
+    {
+      title: '考试题库生成',
+      description: '按主题生成题库与解析',
+      icon: bridgeIcon
+    },
+    {
+      title: '法规知识点',
+      description: '法规标准要点梳理与归纳',
+      icon: constructionIcon
+    },
+    {
+      title: '隐患排查题',
+      description: '隐患识别与处置题型',
+      icon: materialIcon
+    },
+    {
+      title: '考试大纲',
+      description: '生成考试大纲与评分标准',
+      icon: standardIcon
+    }
+  ]
+}
 
 
-// 切换到对应模块
-const setMode = (mode) => {
-  if (currentMode.value === mode) {
+const defaultFunctionCards = computed(
+  () => defaultFunctionCardsByMode[currentMode.value] || defaultFunctionCardsByMode['ai-qa']
+)
+
+const setMode = (mode, options = {}) => {
+  const { allowToggle = true, source = 'user' } = options
+  if (source === 'user') {
+    hasUserSelectedMode.value = true
+  }
+  if (allowToggle && currentMode.value === mode) {
     currentMode.value = 'ai-qa' // 再次点击取消选中,回到默认问答
     currentMode.value = 'ai-qa' // 再次点击取消选中,回到默认问答
   } else {
   } else {
     currentMode.value = mode
     currentMode.value = mode
@@ -1450,7 +1550,7 @@ const getConversationMessages = async (conversationId) => {
               .map(r => r.category)
               .map(r => r.category)
             
             
             categories.forEach(category => {
             categories.forEach(category => {
-              categoryExpandStates.value[index][category] = true
+              categoryExpandStates.value[index][category] = false
             })
             })
           }
           }
           
           
@@ -1643,6 +1743,27 @@ const clearNewConversationState = () => {
   }
   }
 }
 }
 
 
+// 根据当前模式分发发送请求
+const submitQuestionByCurrentMode = async (question, options = {}) => {
+  const { windowSize = 3, nResults = 10 } = options
+  const normalizedQuestion = typeof question === 'string' ? question.trim() : ''
+  if (!normalizedQuestion) return
+
+  if (currentMode.value === 'ai-writing' || currentMode.value === 'safety-training') {
+    await handleNonStreamingSubmit({
+      question: normalizedQuestion,
+      businessType: currentMode.value === 'ai-writing' ? 2 : 1
+    })
+    return
+  }
+
+  await handleReportGeneratorSubmit({
+    question: normalizedQuestion,
+    windowSize,
+    nResults
+  })
+}
+
 // 发送消息
 // 发送消息
 const handleSendMessage = async () => {
 const handleSendMessage = async () => {
   if (!messageText.value.trim() || isSending.value) return
   if (!messageText.value.trim() || isSending.value) return
@@ -1654,19 +1775,11 @@ const handleSendMessage = async () => {
   showChat.value = true
   showChat.value = true
     
     
   clearNewConversationState()
   clearNewConversationState()
-  
-  if (currentMode.value === 'ai-writing' || currentMode.value === 'safety-training') {
-    await handleNonStreamingSubmit({
-      question: messageText.value,
-      businessType: currentMode.value === 'ai-writing' ? 2 : 1
-    })
-  } else {
-    await handleReportGeneratorSubmit({
-      question: messageText.value,
-      windowSize: 3,
-      nResults: 10
-    })
-  }
+
+  await submitQuestionByCurrentMode(messageText.value, {
+    windowSize: 3,
+    nResults: 10
+  })
     
     
   messageText.value = ''
   messageText.value = ''
   clearRecommendQuestions()
   clearRecommendQuestions()
@@ -1680,6 +1793,10 @@ const handleSendMessage = async () => {
 
 
 // 处理非流式请求 (AI写作 和 安全培训)
 // 处理非流式请求 (AI写作 和 安全培训)
 const handleNonStreamingSubmit = async (data) => {
 const handleNonStreamingSubmit = async (data) => {
+  if (!isSending.value) {
+    isSending.value = true
+  }
+
   currentQuestion.value = data.question
   currentQuestion.value = data.question
   
   
   // 添加用户消息
   // 添加用户消息
@@ -1705,26 +1822,43 @@ const handleNonStreamingSubmit = async (data) => {
   
   
   try {
   try {
     const response = await apis.sendDeepseekMessage({
     const response = await apis.sendDeepseekMessage({
-      user_question: data.question,
+      message: data.question,
       business_type: data.businessType,
       business_type: data.businessType,
       enable_online_model: isOnlineModel.value,
       enable_online_model: isOnlineModel.value,
-      ai_conversation_id: ai_conversation_id.value
+      conversation_id: ai_conversation_id.value
     })
     })
 
 
     if (response.statusCode === 200) {
     if (response.statusCode === 200) {
       const aiMessage = chatMessages.value[aiMessageIndex]
       const aiMessage = chatMessages.value[aiMessageIndex]
+      const responseData = response?.data || {}
+      const aiReply =
+        responseData.reply ||
+        responseData.response ||
+        responseData.content ||
+        response.reply ||
+        response.content ||
+        ''
+      const conversationId =
+        responseData.ai_conversation_id ||
+        responseData.conversation_id ||
+        response.ai_conversation_id ||
+        response.conversation_id
       
       
       // 如果用户已经点击了停止,则不再继续输出
       // 如果用户已经点击了停止,则不再继续输出
       if (aiMessage._stopped) {
       if (aiMessage._stopped) {
         return
         return
       }
       }
       
       
+      if (!aiReply || typeof aiReply !== 'string') {
+        throw new Error('AI返回内容为空')
+      }
+
       aiMessage.isTyping = false
       aiMessage.isTyping = false
-      aiMessage.content = response.data
+      aiMessage.content = aiReply
       
       
       // 更新 conversation ID
       // 更新 conversation ID
-      if (response.ai_conversation_id) {
-        ai_conversation_id.value = response.ai_conversation_id
+      if (conversationId) {
+        ai_conversation_id.value = conversationId
       }
       }
       
       
       // 添加打字机效果显示
       // 添加打字机效果显示
@@ -1741,10 +1875,10 @@ const handleNonStreamingSubmit = async (data) => {
           </div>
           </div>
         </div>`
         </div>`
         // 实际内容保存起来,点击查看详情时可以使用
         // 实际内容保存起来,点击查看详情时可以使用
-        aiMessage.fullContent = response.data
+        aiMessage.fullContent = aiReply
       } else {
       } else {
         // AI写作等: 正常打字机输出
         // AI写作等: 正常打字机输出
-        startTypewriterEffect(aiMessage, response.data, 30)
+        startTypewriterEffect(aiMessage, aiReply, 30)
       }
       }
       
       
       // 刷新历史记录
       // 刷新历史记录
@@ -1790,22 +1924,76 @@ const getQuestionIcon = (question) => {
   return icon
   return icon
 }
 }
 
 
+const getModeRecommendationType = (mode) => {
+  if (mode === 'ai-writing') return 2
+  if (mode === 'safety-training') return 1
+  if (mode === 'exam-workshop') return 3
+  return 0
+}
+
+const pickScopedRecommendationItems = (items, expectedType, typeField) => {
+  if (!Array.isArray(items)) return []
+  if (expectedType === 0) return items
+
+  const hasTypeField = items.some(
+    item => item && Object.prototype.hasOwnProperty.call(item, typeField)
+  )
+  if (!hasTypeField) {
+    return null
+  }
+
+  return items.filter(item => Number(item?.[typeField]) === expectedType)
+}
+
 // 点击功能卡片
 // 点击功能卡片
 // ReportGenerator相关方法
 // ReportGenerator相关方法
 const handleCategoryToggle = (messageIndex, data) => {
 const handleCategoryToggle = (messageIndex, data) => {
   if (!categoryExpandStates.value[messageIndex]) {
   if (!categoryExpandStates.value[messageIndex]) {
     categoryExpandStates.value[messageIndex] = {}
     categoryExpandStates.value[messageIndex] = {}
   }
   }
-  categoryExpandStates.value[messageIndex][data.category] = data.expanded
+  const matchedCategory = findCategoryStateKey(messageIndex, data.category)
+  categoryExpandStates.value[messageIndex][matchedCategory || data.category] = data.expanded
+}
+
+const normalizeCategoryName = (category) => {
+  return typeof category === 'string' ? category.trim() : ''
+}
+
+const findCategoryStateKey = (messageIndex, category) => {
+  const stateMap = categoryExpandStates.value[messageIndex]
+  if (!stateMap) return ''
+
+  const normalizedCategory = normalizeCategoryName(category)
+  if (!normalizedCategory) return ''
+
+  if (Object.prototype.hasOwnProperty.call(stateMap, category)) {
+    return category
+  }
+
+  const keys = Object.keys(stateMap)
+  const exactKey = keys.find(key => normalizeCategoryName(key) === normalizedCategory)
+  if (exactKey) return exactKey
+
+  return keys.find(key => {
+    const normalizedKey = normalizeCategoryName(key)
+    return normalizedKey && (
+      normalizedKey.includes(normalizedCategory) ||
+      normalizedCategory.includes(normalizedKey)
+    )
+  }) || ''
 }
 }
 
 
 const isCategoryExpanded = (messageIndex, category) => {
 const isCategoryExpanded = (messageIndex, category) => {
   if (!category) return true
   if (!category) return true
   if (!categoryExpandStates.value[messageIndex]) {
   if (!categoryExpandStates.value[messageIndex]) {
     categoryExpandStates.value[messageIndex] = {}
     categoryExpandStates.value[messageIndex] = {}
-    return true
+    return false
   }
   }
-  return categoryExpandStates.value[messageIndex][category] !== false
+
+  const matchedCategory = findCategoryStateKey(messageIndex, category)
+  if (!matchedCategory) return false
+
+  return categoryExpandStates.value[messageIndex][matchedCategory] === true
 }
 }
 
 
 // 处理安全培训文档点击
 // 处理安全培训文档点击
@@ -2076,7 +2264,7 @@ const handleSSEMessage = (data, aiMessageIndex) => {
       if (!categoryExpandStates.value[aiMessageIndex]) {
       if (!categoryExpandStates.value[aiMessageIndex]) {
         categoryExpandStates.value[aiMessageIndex] = {}
         categoryExpandStates.value[aiMessageIndex] = {}
       }
       }
-      categoryExpandStates.value[aiMessageIndex][data.category] = true
+      categoryExpandStates.value[aiMessageIndex][data.category] = false
       
       
       // 保存当前分类名称,用于后续报告匹配
       // 保存当前分类名称,用于后续报告匹配
       aiMessage.currentCategory = data.category
       aiMessage.currentCategory = data.category
@@ -2100,7 +2288,7 @@ const handleSSEMessage = (data, aiMessageIndex) => {
         similarity: data.similarity,
         similarity: data.similarity,
         metadata: {
         metadata: {
           ...data.metadata,
           ...data.metadata,
-          _displayCategory: aiMessage.currentCategory // 存储当前显示的分类名
+          _displayCategory: data.metadata?.primary_category || aiMessage.currentCategory // 存储当前显示的分类名
         },
         },
         report: {
         report: {
           display_name: '',
           display_name: '',
@@ -2140,7 +2328,9 @@ const handleSSEMessage = (data, aiMessageIndex) => {
       
       
       let targetReport
       let targetReport
       if (idx !== undefined) {
       if (idx !== undefined) {
-        const displayCategory = aiMessage.reports[idx].metadata?._displayCategory
+        const displayCategory = reportData.metadata?.primary_category ||
+          aiMessage.reports[idx].metadata?._displayCategory ||
+          aiMessage.currentCategory
         const fullSummary = reportData.report?.summary || ''
         const fullSummary = reportData.report?.summary || ''
         const fullAnalysis = reportData.report?.analysis || ''
         const fullAnalysis = reportData.report?.analysis || ''
         const fullClauses = reportData.report?.clauses || ''
         const fullClauses = reportData.report?.clauses || ''
@@ -2158,7 +2348,7 @@ const handleSSEMessage = (data, aiMessageIndex) => {
           status: 'completed',
           status: 'completed',
           metadata: {
           metadata: {
             ...reportData.metadata, // 保留所有metadata字段
             ...reportData.metadata, // 保留所有metadata字段
-            _displayCategory: displayCategory || aiMessage.currentCategory
+            _displayCategory: displayCategory
           },
           },
           _fullContent: {
           _fullContent: {
             display_name: fullDisplayName,
             display_name: fullDisplayName,
@@ -2186,7 +2376,7 @@ const handleSSEMessage = (data, aiMessageIndex) => {
           status: 'completed',
           status: 'completed',
           metadata: {
           metadata: {
             ...reportData.metadata, // 保留所有metadata字段
             ...reportData.metadata, // 保留所有metadata字段
-            _displayCategory: aiMessage.currentCategory
+            _displayCategory: reportData.metadata?.primary_category || aiMessage.currentCategory
           },
           },
           _fullContent: {
           _fullContent: {
             display_name: fullDisplayName,
             display_name: fullDisplayName,
@@ -2759,39 +2949,41 @@ const handleReportGeneratorSubmit = async (data) => {
   }
   }
 }
 }
 
 
-const handleFunctionCard = (cardType) => {
+const handleFunctionCard = async (cardType) => {
   clearAllTypeIntervals()
   clearAllTypeIntervals()
   chatMessages.value = []
   chatMessages.value = []
   ai_conversation_id.value = 0
   ai_conversation_id.value = 0
   showChat.value = true
   showChat.value = true
   
   
-  handleReportGeneratorSubmit({
-    question: `请详细介绍${cardType}的相关内容`,
+  await submitQuestionByCurrentMode(`请详细介绍${cardType}的相关内容`, {
     windowSize: 3,
     windowSize: 3,
     nResults: 10
     nResults: 10
   })
   })
 }
 }
 
 
 // 点击推荐问题
 // 点击推荐问题
-const handleRecommendedQuestion = (question) => {
+const handleRecommendedQuestion = async (question) => {
   clearAllTypeIntervals()
   clearAllTypeIntervals()
   chatMessages.value = []
   chatMessages.value = []
   ai_conversation_id.value = 0
   ai_conversation_id.value = 0
   showChat.value = true
   showChat.value = true
   
   
-  handleReportGeneratorSubmit({
-    question: question,
+  await submitQuestionByCurrentMode(question, {
     windowSize: 3,
     windowSize: 3,
     nResults: 10
     nResults: 10
   })
   })
 }
 }
 
 
 // 点击用户推荐问题
 // 点击用户推荐问题
-const handleUserRecommendQuestion = (question) => {
+const handleUserRecommendQuestion = async (question) => {
   clearRecommendQuestions()
   clearRecommendQuestions()
+  showChat.value = true
+
+  if (chatMessages.value.length === 0) {
+    clearNewConversationState()
+  }
   
   
-  handleReportGeneratorSubmit({
-    question: question,
+  await submitQuestionByCurrentMode(question, {
     windowSize: 3,
     windowSize: 3,
     nResults: 10
     nResults: 10
   })
   })
@@ -3507,16 +3699,29 @@ const removeSelectedFile = () => {
 const getFunctionCards = async () => {
 const getFunctionCards = async () => {
   try {
   try {
     console.log('开始获取功能卡片...')
     console.log('开始获取功能卡片...')
-    let functionType = 0; // AI问答
-    if (currentMode.value === 'ai-writing') functionType = 2;
-    if (currentMode.value === 'safety-training') functionType = 1;
-    if (currentMode.value === 'exam-workshop') functionType = 3;
+    const requestMode = currentMode.value
+    const functionType = getModeRecommendationType(requestMode)
 
 
     const response = await apis.getFunctionCard({ function_type: functionType })
     const response = await apis.getFunctionCard({ function_type: functionType })
     console.log('功能卡片响应:', response)
     console.log('功能卡片响应:', response)
     
     
     if (response.statusCode === 200) {
     if (response.statusCode === 200) {
-      functionCards.value = response.data
+      if (currentMode.value !== requestMode) {
+        console.log('模式已切换,忽略旧的功能卡片结果:', requestMode, '->', currentMode.value)
+        return
+      }
+      const scopedCards = pickScopedRecommendationItems(
+        response.data,
+        functionType,
+        'function_type'
+      )
+
+      if (scopedCards === null) {
+        console.warn('功能卡片接口未返回类型字段,当前模式继续使用默认卡片:', requestMode)
+        return
+      }
+
+      functionCards.value = scopedCards
       console.log('功能卡片数据已设置:', functionCards.value)
       console.log('功能卡片数据已设置:', functionCards.value)
     } else {
     } else {
       console.error('获取功能卡片失败:', response.statusCode)
       console.error('获取功能卡片失败:', response.statusCode)
@@ -3530,16 +3735,29 @@ const getFunctionCards = async () => {
 const getHotQuestions = async () => {
 const getHotQuestions = async () => {
   try {
   try {
     console.log('开始获取热点问题...')
     console.log('开始获取热点问题...')
-    let questionType = 0; // AI问答
-    if (currentMode.value === 'ai-writing') questionType = 2;
-    if (currentMode.value === 'safety-training') questionType = 1;
-    if (currentMode.value === 'exam-workshop') questionType = 3;
+    const requestMode = currentMode.value
+    const questionType = getModeRecommendationType(requestMode)
 
 
     const response = await apis.getHotQuestion({ question_type: questionType })
     const response = await apis.getHotQuestion({ question_type: questionType })
     console.log('热点问题响应:', response)
     console.log('热点问题响应:', response)
     
     
     if (response.statusCode === 200) {
     if (response.statusCode === 200) {
-      hotQuestions.value = response.data
+      if (currentMode.value !== requestMode) {
+        console.log('模式已切换,忽略旧的热点问题结果:', requestMode, '->', currentMode.value)
+        return
+      }
+      const scopedQuestions = pickScopedRecommendationItems(
+        response.data,
+        questionType,
+        'question_type'
+      )
+
+      if (scopedQuestions === null) {
+        console.warn('热点问题接口未返回类型字段,当前模式继续使用默认问题:', requestMode)
+        return
+      }
+
+      hotQuestions.value = scopedQuestions
       console.log('热点问题数据已设置:', hotQuestions.value)
       console.log('热点问题数据已设置:', hotQuestions.value)
     } else {
     } else {
       console.error('获取热点问题失败:', response.statusCode)
       console.error('获取热点问题失败:', response.statusCode)
@@ -3551,6 +3769,8 @@ const getHotQuestions = async () => {
 
 
 // 监听模式变化,重新获取卡片和问题
 // 监听模式变化,重新获取卡片和问题
 watch(currentMode, () => {
 watch(currentMode, () => {
+  functionCards.value = []
+  hotQuestions.value = []
   getFunctionCards()
   getFunctionCards()
   getHotQuestions()
   getHotQuestions()
 })
 })
@@ -4115,9 +4335,8 @@ const autoSendMessage = async (message) => {
   aiRelatedQuestions.value = []
   aiRelatedQuestions.value = []
   relatedQuestionsMessageId.value = null
   relatedQuestionsMessageId.value = null
   
   
-  // 使用ReportGenerator的提交逻辑(与直接发送消息一致)
-  await handleReportGeneratorSubmit({
-    question: message,
+  // 与直接发送一致:按当前模式分发到对应接口
+  await submitQuestionByCurrentMode(message, {
     windowSize: 3,
     windowSize: 3,
     nResults: 10
     nResults: 10
   })
   })
@@ -4710,10 +4929,12 @@ onMounted(async () => {
     console.log('🎉 AI问答页面初始化完成')
     console.log('🎉 AI问答页面初始化完成')
     
     
     // 检查是否带有指定的模式
     // 检查是否带有指定的模式
-    const targetMode = route.query.mode
+    const targetMode = Array.isArray(route.query.mode) ? route.query.mode[0] : route.query.mode
     if (targetMode && ['ai-qa', 'ai-writing', 'safety-training', 'exam-workshop'].includes(targetMode)) {
     if (targetMode && ['ai-qa', 'ai-writing', 'safety-training', 'exam-workshop'].includes(targetMode)) {
       console.log('检测到目标模式:', targetMode)
       console.log('检测到目标模式:', targetMode)
-      setMode(targetMode)
+      if (!hasUserSelectedMode.value && currentMode.value !== targetMode) {
+        setMode(targetMode, { allowToggle: false, source: 'system' })
+      }
       
       
       // 立即清除URL中的mode参数,防止刷新时问题
       // 立即清除URL中的mode参数,防止刷新时问题
       router.replace({
       router.replace({

+ 44 - 9
shudao-vue-frontend/src/views/mobile/m-Chat.vue

@@ -1389,7 +1389,7 @@ const getConversationMessages = async (conversationId) => {
               .map(r => r.category)
               .map(r => r.category)
             
             
             categories.forEach(category => {
             categories.forEach(category => {
-              categoryExpandStates.value[index][category] = true
+              categoryExpandStates.value[index][category] = false
             })
             })
           }
           }
         
         
@@ -2282,16 +2282,49 @@ const handleCategoryToggle = (messageIndex, data) => {
   if (!categoryExpandStates.value[messageIndex]) {
   if (!categoryExpandStates.value[messageIndex]) {
     categoryExpandStates.value[messageIndex] = {}
     categoryExpandStates.value[messageIndex] = {}
   }
   }
-  categoryExpandStates.value[messageIndex][data.category] = data.expanded
+  const matchedCategory = findCategoryStateKey(messageIndex, data.category)
+  categoryExpandStates.value[messageIndex][matchedCategory || data.category] = data.expanded
+}
+
+const normalizeCategoryName = (category) => {
+  return typeof category === 'string' ? category.trim() : ''
+}
+
+const findCategoryStateKey = (messageIndex, category) => {
+  const stateMap = categoryExpandStates.value[messageIndex]
+  if (!stateMap) return ''
+
+  const normalizedCategory = normalizeCategoryName(category)
+  if (!normalizedCategory) return ''
+
+  if (Object.prototype.hasOwnProperty.call(stateMap, category)) {
+    return category
+  }
+
+  const keys = Object.keys(stateMap)
+  const exactKey = keys.find(key => normalizeCategoryName(key) === normalizedCategory)
+  if (exactKey) return exactKey
+
+  return keys.find(key => {
+    const normalizedKey = normalizeCategoryName(key)
+    return normalizedKey && (
+      normalizedKey.includes(normalizedCategory) ||
+      normalizedCategory.includes(normalizedKey)
+    )
+  }) || ''
 }
 }
 
 
 const isCategoryExpanded = (messageIndex, category) => {
 const isCategoryExpanded = (messageIndex, category) => {
   if (!category) return true
   if (!category) return true
   if (!categoryExpandStates.value[messageIndex]) {
   if (!categoryExpandStates.value[messageIndex]) {
     categoryExpandStates.value[messageIndex] = {}
     categoryExpandStates.value[messageIndex] = {}
-    return true
+    return false
   }
   }
-  return categoryExpandStates.value[messageIndex][category] !== false
+
+  const matchedCategory = findCategoryStateKey(messageIndex, category)
+  if (!matchedCategory) return false
+
+  return categoryExpandStates.value[messageIndex][matchedCategory] === true
 }
 }
 
 
 // 检查reports数组是否只包含分类标题,没有实际报告
 // 检查reports数组是否只包含分类标题,没有实际报告
@@ -2581,7 +2614,7 @@ const handleSSEMessage = (data, aiMessageIndex) => {
       if (!categoryExpandStates.value[aiMessageIndex]) {
       if (!categoryExpandStates.value[aiMessageIndex]) {
         categoryExpandStates.value[aiMessageIndex] = {}
         categoryExpandStates.value[aiMessageIndex] = {}
       }
       }
-      categoryExpandStates.value[aiMessageIndex][data.category] = true
+      categoryExpandStates.value[aiMessageIndex][data.category] = false
       
       
       // 保存当前分类名称,用于后续报告匹配
       // 保存当前分类名称,用于后续报告匹配
       aiMessage.currentCategory = data.category
       aiMessage.currentCategory = data.category
@@ -2605,7 +2638,7 @@ const handleSSEMessage = (data, aiMessageIndex) => {
         similarity: data.similarity,
         similarity: data.similarity,
         metadata: {
         metadata: {
           ...data.metadata,
           ...data.metadata,
-          _displayCategory: aiMessage.currentCategory // 存储当前显示的分类名
+          _displayCategory: data.metadata?.primary_category || aiMessage.currentCategory // 存储当前显示的分类名
         },
         },
         report: {
         report: {
           display_name: '',
           display_name: '',
@@ -2645,7 +2678,9 @@ const handleSSEMessage = (data, aiMessageIndex) => {
       
       
       let targetReport
       let targetReport
       if (idx !== undefined) {
       if (idx !== undefined) {
-        const displayCategory = aiMessage.reports[idx].metadata?._displayCategory
+        const displayCategory = reportData.metadata?.primary_category ||
+          aiMessage.reports[idx].metadata?._displayCategory ||
+          aiMessage.currentCategory
         const fullSummary = reportData.report?.summary || ''
         const fullSummary = reportData.report?.summary || ''
         const fullAnalysis = reportData.report?.analysis || ''
         const fullAnalysis = reportData.report?.analysis || ''
         const fullClauses = reportData.report?.clauses || ''
         const fullClauses = reportData.report?.clauses || ''
@@ -2663,7 +2698,7 @@ const handleSSEMessage = (data, aiMessageIndex) => {
           status: 'completed',
           status: 'completed',
           metadata: {
           metadata: {
             ...reportData.metadata, // 保留所有metadata字段
             ...reportData.metadata, // 保留所有metadata字段
-            _displayCategory: displayCategory || aiMessage.currentCategory
+            _displayCategory: displayCategory
           },
           },
           _fullContent: {
           _fullContent: {
             display_name: fullDisplayName,
             display_name: fullDisplayName,
@@ -2691,7 +2726,7 @@ const handleSSEMessage = (data, aiMessageIndex) => {
           status: 'completed',
           status: 'completed',
           metadata: {
           metadata: {
             ...reportData.metadata, // 保留所有metadata字段
             ...reportData.metadata, // 保留所有metadata字段
-            _displayCategory: aiMessage.currentCategory
+            _displayCategory: reportData.metadata?.primary_category || aiMessage.currentCategory
           },
           },
           _fullContent: {
           _fullContent: {
             display_name: fullDisplayName,
             display_name: fullDisplayName,