|
|
@@ -65,8 +65,13 @@
|
|
|
<h2 v-else class="default-title">AI问答</h2>
|
|
|
</div>
|
|
|
|
|
|
+ <!-- 考试工坊内容区域 -->
|
|
|
+ <div v-if="currentMode === 'exam-workshop'" class="exam-workshop-wrapper">
|
|
|
+ <ExamWorkshop :hideSidebar="true" />
|
|
|
+ </div>
|
|
|
+
|
|
|
<!-- 聊天内容区域 -->
|
|
|
- <div class="chat-content">
|
|
|
+ <div v-else class="chat-content">
|
|
|
<!-- 初始状态:AI助手介绍和功能卡片 -->
|
|
|
<div v-if="!showChat" class="initial-content">
|
|
|
<!-- AI助手介绍 -->
|
|
|
@@ -294,9 +299,23 @@
|
|
|
<WebSearchSummary :summary="message.webSearchSummary" />
|
|
|
</div>
|
|
|
|
|
|
+ <!-- 安全培训文档卡片 -->
|
|
|
+ <div v-if="message.isDocument" class="safety-doc-card" @click="handleSafetyDocClick(message)">
|
|
|
+ <div class="doc-icon-wrapper">
|
|
|
+ <img src="../assets/Chat/26.png" alt="文档" class="doc-icon">
|
|
|
+ </div>
|
|
|
+ <div class="doc-info">
|
|
|
+ <div class="doc-title">{{ currentQuestion || '安全培训文档' }}</div>
|
|
|
+ <div class="doc-time">刚刚生成</div>
|
|
|
+ </div>
|
|
|
+ <div class="doc-action">
|
|
|
+ 查看详情 >
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<!-- 原有的AI文本内容(如果没有报告数据时显示) -->
|
|
|
<div v-if="!message.reports || message.reports.length === 0" class="ai-text">
|
|
|
- <div v-if="message.displayContent && message.displayContent.length > 0" class="ai-markdown-content">
|
|
|
+ <div v-if="message.displayContent && message.displayContent.length > 0 && !message.isDocument" class="ai-markdown-content">
|
|
|
<div v-html="message.displayContent"></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -489,7 +508,9 @@
|
|
|
<!-- 模式标签组 -->
|
|
|
<div class="mode-chips">
|
|
|
<div
|
|
|
+ v-if="currentMode === 'ai-qa' || currentMode === 'ai-writing'"
|
|
|
class="mode-chip"
|
|
|
+ :class="{ active: currentMode === 'ai-writing' }"
|
|
|
@click="setMode('ai-writing')"
|
|
|
>
|
|
|
<img src="../assets/chat/13.png" alt="AI写作" class="chip-icon">
|
|
|
@@ -497,7 +518,9 @@
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
+ v-if="currentMode === 'ai-qa' || currentMode === 'safety-training'"
|
|
|
class="mode-chip"
|
|
|
+ :class="{ active: currentMode === 'safety-training' }"
|
|
|
@click="setMode('safety-training')"
|
|
|
>
|
|
|
<img src="../assets/chat/14.png" alt="安全培训" class="chip-icon">
|
|
|
@@ -505,7 +528,9 @@
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
+ v-if="currentMode === 'ai-qa' || currentMode === 'exam-workshop'"
|
|
|
class="mode-chip"
|
|
|
+ :class="{ active: currentMode === 'exam-workshop' }"
|
|
|
@click="setMode('exam-workshop')"
|
|
|
>
|
|
|
<img src="../assets/chat/19.png" alt="考试工坊" class="chip-icon">
|
|
|
@@ -606,6 +631,7 @@
|
|
|
import { ref, nextTick, reactive, triggerRef, markRaw, onMounted, onBeforeUnmount, computed, watch, onActivated } from 'vue'
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
import Sidebar from '@/components/Sidebar.vue'
|
|
|
+import ExamWorkshop from '@/views/ExamWorkshop.vue'
|
|
|
import * as mammoth from 'mammoth'
|
|
|
|
|
|
// 导入Element Plus组件
|
|
|
@@ -685,14 +711,15 @@ const toggleModelType = () => {
|
|
|
showToast(isOnlineModel.value ? '已切换至在线大模型 (glm-4-plus)' : '已切换至本地模型')
|
|
|
}
|
|
|
|
|
|
-// 跳转到对应模块
|
|
|
+// 当前激活的模块模式:'ai-qa', 'ai-writing', 'safety-training', 'exam-workshop'
|
|
|
+const currentMode = ref('ai-qa')
|
|
|
+
|
|
|
+// 切换到对应模块
|
|
|
const setMode = (mode) => {
|
|
|
- if (mode === 'ai-writing') {
|
|
|
- router.push('/ai-writing')
|
|
|
- } else if (mode === 'safety-training') {
|
|
|
- router.push('/safety-hazard')
|
|
|
- } else if (mode === 'exam-workshop') {
|
|
|
- router.push('/exam-workshop')
|
|
|
+ if (currentMode.value === mode) {
|
|
|
+ currentMode.value = 'ai-qa' // 再次点击取消选中,回到默认问答
|
|
|
+ } else {
|
|
|
+ currentMode.value = mode
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1182,30 +1209,45 @@ const historyData = ref([])
|
|
|
const historyTotal = ref(0) // 历史记录总数
|
|
|
const isLoadingHistory = ref(false) // 是否正在加载历史记录
|
|
|
|
|
|
-// 获取历史记录列表
|
|
|
+// 获取历史记录列表 (合并所有模块)
|
|
|
const getHistoryRecordList = async () => {
|
|
|
try {
|
|
|
isLoadingHistory.value = true
|
|
|
|
|
|
- const response = await apis.getHistoryRecord({
|
|
|
- // ===== 已删除:user_id - 后端从token解析 =====
|
|
|
- ai_conversation_id: 0,
|
|
|
- business_type: 0
|
|
|
+ // 分别请求四种业务类型的历史记录
|
|
|
+ const businessTypes = [0, 1, 2, 3] // 0:AI问答, 1:安全培训, 2:AI写作, 3:考试工坊
|
|
|
+
|
|
|
+ const requests = businessTypes.map(type =>
|
|
|
+ apis.getHistoryRecord({
|
|
|
+ ai_conversation_id: 0,
|
|
|
+ business_type: type
|
|
|
+ })
|
|
|
+ )
|
|
|
+
|
|
|
+ const responses = await Promise.all(requests)
|
|
|
+
|
|
|
+ let allRecords = []
|
|
|
+ let totalCount = 0
|
|
|
+
|
|
|
+ responses.forEach(response => {
|
|
|
+ if (response.statusCode === 200 && response.data) {
|
|
|
+ totalCount += response.total || response.data.length || 0
|
|
|
+ allRecords = allRecords.concat(response.data)
|
|
|
+ }
|
|
|
})
|
|
|
|
|
|
- if (response.statusCode === 200) {
|
|
|
- historyTotal.value = response.total || 0
|
|
|
- historyData.value = response.data.map(conversation => ({
|
|
|
- id: conversation.id,
|
|
|
- title: conversation.title || generateConversationTitle(conversation.content),
|
|
|
- time: formatTime(conversation.updated_at),
|
|
|
- businessType: conversation.business_type,
|
|
|
- isActive: false,
|
|
|
- rawData: conversation
|
|
|
- }))
|
|
|
- } else {
|
|
|
- console.error('获取历史记录失败:', response.statusCode)
|
|
|
- }
|
|
|
+ // 按时间降序排序
|
|
|
+ allRecords.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at))
|
|
|
+
|
|
|
+ historyTotal.value = totalCount
|
|
|
+ historyData.value = allRecords.map(conversation => ({
|
|
|
+ id: conversation.id,
|
|
|
+ title: conversation.title || generateConversationTitle(conversation.content),
|
|
|
+ time: formatTime(conversation.updated_at),
|
|
|
+ businessType: conversation.business_type,
|
|
|
+ isActive: false,
|
|
|
+ rawData: conversation
|
|
|
+ }))
|
|
|
} catch (error) {
|
|
|
console.error('获取历史记录失败:', error)
|
|
|
} finally {
|
|
|
@@ -1609,28 +1651,114 @@ const clearNewConversationState = () => {
|
|
|
const handleSendMessage = async () => {
|
|
|
if (!messageText.value.trim() || isSending.value) return
|
|
|
|
|
|
- clearRecommendQuestions()
|
|
|
- aiRelatedQuestions.value = []
|
|
|
- relatedQuestionsMessageId.value = null
|
|
|
- isSending.value = true
|
|
|
- showChat.value = true
|
|
|
+ clearRecommendQuestions()
|
|
|
+ aiRelatedQuestions.value = []
|
|
|
+ relatedQuestionsMessageId.value = null
|
|
|
+ isSending.value = true
|
|
|
+ showChat.value = true
|
|
|
|
|
|
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
|
|
|
})
|
|
|
+ }
|
|
|
|
|
|
- messageText.value = ''
|
|
|
- clearRecommendQuestions()
|
|
|
-
|
|
|
- if (selectedFile.value) {
|
|
|
- removeSelectedFile()
|
|
|
+ messageText.value = ''
|
|
|
+ clearRecommendQuestions()
|
|
|
+
|
|
|
+ if (selectedFile.value) {
|
|
|
+ removeSelectedFile()
|
|
|
+ }
|
|
|
+
|
|
|
+ scrollToBottom()
|
|
|
+}
|
|
|
+
|
|
|
+// 处理非流式请求 (AI写作 和 安全培训)
|
|
|
+const handleNonStreamingSubmit = async (data) => {
|
|
|
+ currentQuestion.value = data.question
|
|
|
+
|
|
|
+ // 添加用户消息
|
|
|
+ chatMessages.value.push({
|
|
|
+ id: Date.now(),
|
|
|
+ type: 'user',
|
|
|
+ content: data.question,
|
|
|
+ timestamp: new Date().toISOString()
|
|
|
+ })
|
|
|
+
|
|
|
+ // 添加AI消息占位符
|
|
|
+ const aiMessageIndex = chatMessages.value.length
|
|
|
+ chatMessages.value.push({
|
|
|
+ id: Date.now() + 1,
|
|
|
+ type: 'ai',
|
|
|
+ userQuestion: data.question,
|
|
|
+ isTyping: true, // 显示 loading 状态
|
|
|
+ content: '',
|
|
|
+ displayContent: '',
|
|
|
+ timestamp: new Date().toISOString(),
|
|
|
+ reports: []
|
|
|
+ })
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await apis.sendDeepseekMessage({
|
|
|
+ user_question: data.question,
|
|
|
+ business_type: data.businessType,
|
|
|
+ enable_online_model: isOnlineModel.value,
|
|
|
+ ai_conversation_id: ai_conversation_id.value
|
|
|
+ })
|
|
|
+
|
|
|
+ if (response.statusCode === 200) {
|
|
|
+ const aiMessage = chatMessages.value[aiMessageIndex]
|
|
|
+ aiMessage.isTyping = false
|
|
|
+ aiMessage.content = response.data
|
|
|
+
|
|
|
+ // 更新 conversation ID
|
|
|
+ if (response.ai_conversation_id) {
|
|
|
+ ai_conversation_id.value = response.ai_conversation_id
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加打字机效果显示
|
|
|
+ if (data.businessType === 1) {
|
|
|
+ // 安全培训: 只展示一个输出文档
|
|
|
+ aiMessage.isDocument = true
|
|
|
+ aiMessage.displayContent = `<div class="safety-training-doc-card">
|
|
|
+ <div class="doc-header">
|
|
|
+ <span class="doc-icon">📄</span>
|
|
|
+ <span class="doc-title">安全培训生成文档</span>
|
|
|
+ </div>
|
|
|
+ <div class="doc-actions">
|
|
|
+ <button class="view-doc-btn">查看详情</button>
|
|
|
+ </div>
|
|
|
+ </div>`
|
|
|
+ // 实际内容保存起来,点击查看详情时可以使用
|
|
|
+ aiMessage.fullContent = response.data
|
|
|
+ } else {
|
|
|
+ // AI写作等: 正常打字机输出
|
|
|
+ startTypewriterEffect(aiMessage, response.data, 30)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 刷新历史记录
|
|
|
+ getHistoryRecordList()
|
|
|
+ } else {
|
|
|
+ throw new Error(response.msg || '请求失败')
|
|
|
}
|
|
|
-
|
|
|
- scrollToBottom()
|
|
|
+ } catch (error) {
|
|
|
+ console.error('发送请求失败:', error)
|
|
|
+ const aiMessage = chatMessages.value[aiMessageIndex]
|
|
|
+ aiMessage.isTyping = false
|
|
|
+ aiMessage.displayContent = '抱歉,生成内容时发生错误,请重试。'
|
|
|
+ ElMessage.error(`请求失败: ${error.message}`)
|
|
|
+ } finally {
|
|
|
+ isSending.value = false
|
|
|
+ }
|
|
|
}
|
|
|
// 功能卡片图标计数器
|
|
|
let functionCardIconIndex = 0
|
|
|
@@ -1673,6 +1801,15 @@ const isCategoryExpanded = (messageIndex, category) => {
|
|
|
return categoryExpandStates.value[messageIndex][category] !== false
|
|
|
}
|
|
|
|
|
|
+// 处理安全培训文档点击
|
|
|
+const handleSafetyDocClick = (message) => {
|
|
|
+ if (message.rawData && message.rawData.ai_conversation_id) {
|
|
|
+ router.push({ path: '/safety-hazard', query: { id: message.rawData.ai_conversation_id } })
|
|
|
+ } else if (ai_conversation_id.value) {
|
|
|
+ router.push({ path: '/safety-hazard', query: { id: ai_conversation_id.value } })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// 检查reports数组是否只包含分类标题,没有实际报告
|
|
|
const hasOnlyCategoryTitles = (reports) => {
|
|
|
if (!reports || reports.length === 0) return false
|
|
|
@@ -2691,6 +2828,18 @@ const handleHistoryItem = async (historyItem) => {
|
|
|
|
|
|
if (success) {
|
|
|
chatMessages.value = chatMessages.value.filter(msg => msg.id !== loadingMessage.id)
|
|
|
+
|
|
|
+ // 根据历史记录的业务类型切换当前模式
|
|
|
+ if (historyItem.businessType === 2) {
|
|
|
+ currentMode.value = 'ai-writing'
|
|
|
+ } else if (historyItem.businessType === 1) {
|
|
|
+ currentMode.value = 'safety-training'
|
|
|
+ } else if (historyItem.businessType === 3) {
|
|
|
+ currentMode.value = 'exam-workshop'
|
|
|
+ } else {
|
|
|
+ currentMode.value = 'ai-qa'
|
|
|
+ }
|
|
|
+
|
|
|
await nextTick()
|
|
|
scrollToBottom()
|
|
|
|
|
|
@@ -3347,7 +3496,12 @@ const removeSelectedFile = () => {
|
|
|
const getFunctionCards = async () => {
|
|
|
try {
|
|
|
console.log('开始获取功能卡片...')
|
|
|
- const response = await apis.getFunctionCard({ function_type: 0 }) // 0为AI问答类型
|
|
|
+ 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 response = await apis.getFunctionCard({ function_type: functionType })
|
|
|
console.log('功能卡片响应:', response)
|
|
|
|
|
|
if (response.statusCode === 200) {
|
|
|
@@ -3365,7 +3519,12 @@ const getFunctionCards = async () => {
|
|
|
const getHotQuestions = async () => {
|
|
|
try {
|
|
|
console.log('开始获取热点问题...')
|
|
|
- const response = await apis.getHotQuestion({ question_type: 0 }) // 0为AI问答类型
|
|
|
+ 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 response = await apis.getHotQuestion({ question_type: questionType })
|
|
|
console.log('热点问题响应:', response)
|
|
|
|
|
|
if (response.statusCode === 200) {
|
|
|
@@ -3379,6 +3538,12 @@ const getHotQuestions = async () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// 监听模式变化,重新获取卡片和问题
|
|
|
+watch(currentMode, () => {
|
|
|
+ getFunctionCards()
|
|
|
+ getHotQuestions()
|
|
|
+})
|
|
|
+
|
|
|
// 清除推荐问题
|
|
|
const clearRecommendQuestions = () => {
|
|
|
userRecommendQuestions.value = []
|
|
|
@@ -4823,6 +4988,21 @@ onActivated(async () => {
|
|
|
|
|
|
|
|
|
/* 主聊天区域 */
|
|
|
+.embedded-exam-workshop {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ overflow: hidden;
|
|
|
+ height: 100vh;
|
|
|
+}
|
|
|
+
|
|
|
+.exam-workshop-wrapper {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ position: relative;
|
|
|
+ background: #f5f7fb;
|
|
|
+}
|
|
|
+
|
|
|
.main-chat {
|
|
|
flex: 1;
|
|
|
background: #EBF3FF;
|
|
|
@@ -5632,6 +5812,65 @@ onActivated(async () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ .safety-doc-card {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ background: white;
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 16px 20px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
|
|
+ max-width: 400px;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
|
|
+ transform: translateY(-2px);
|
|
|
+ border-color: #3e7bfa;
|
|
|
+ }
|
|
|
+
|
|
|
+ .doc-icon-wrapper {
|
|
|
+ width: 48px;
|
|
|
+ height: 48px;
|
|
|
+ background: #f0f5ff;
|
|
|
+ border-radius: 10px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ margin-right: 16px;
|
|
|
+
|
|
|
+ .doc-icon {
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+ object-fit: contain;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .doc-info {
|
|
|
+ flex: 1;
|
|
|
+
|
|
|
+ .doc-title {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1f2937;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .doc-time {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #9ca3af;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .doc-action {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #3e7bfa;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
.ai-markdown-content {
|
|
|
/* Markdown样式 - 与StreamMarkdown保持一致 */
|
|
|
:deep(h2) {
|