ai-manus/chat-client/src/views/chatweb/index.vue

387 lines
11 KiB
Vue

<template>
<div class="main-container">
<el-row :gutter="10" class="content-row">
<!-- 左侧聊天对话框 -->
<el-col :span="showChart ? 14 : 24" class="chat-panel">
<ChatBox
v-if="isRemarkLoaded"
:chat-type="chatType"
:user-id="userId"
:placeholder="placeholder"
:openRemark="openRemark"
:recommend-questions="recommendQuestions"
@stream-complete="handleStreamComplete"
@source-click="handleSourceClick"
@message-received="handleMessageReceived"
@chat-created="handleChatCreated"
@chat-switched="handleChatSwitched"
/>
</el-col>
<!-- 右侧图表区域 -->
<el-col :span="10" v-if="showChart" v-loading="loadChart" class="chart-panel">
<div class="chart-div">
<div class="chart-content">
<ChatChart :chatData="chatData" />
</div>
<!-- <div class="chart-actions">
<el-button type="primary" @click="openPdftable">显示 PDF 列表</el-button>
</div> -->
</div>
</el-col>
</el-row>
<el-dialog title="PDF 列表" v-model="dialogVisible" width="50%" @close="handleClose">
<el-table :data="pdfFiles" style="width: 100%">
<el-table-column prop="key" label="名称" fit></el-table-column>
<el-table-column label="操作" width="180">
<template #default="scope">
<el-button size="small" @click="openPdf(scope.row.value)">打开</el-button>
</template>
</el-table-column>
</el-table>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">关闭</el-button>
</span>
</el-dialog>
<el-dialog v-model="pdfVisible">
<PdfTable ref="pdftableref" :pdfUrl="pdfUrl" />
</el-dialog>
</div>
</template>
<script setup lang="ts">
import ChatChart from './components/ChatChart.vue'
import ChatBox from './components/ChatBox.vue'
import { webSocketService } from '@/utils/websocket'
import { useUserStore } from '@/store/modules/user'
import { useAclStore } from '@/store/modules/acl'
import { ref, reactive, onMounted, onBeforeUnmount, computed } from 'vue'
import { getFilePathList } from '@/api/pdfTbale'
import PdfTable from './components/PdfTable.vue'
import { sendMessages } from '@/api/historicalRecords'
import { getMaintStatData, type TraceFile } from '@/api/chat'
import { ElMessage } from 'element-plus'
import { useI18n } from 'vue-i18n'
import { getRemark,getRecommendations} from '@/api/prologue'
const { t, locale } = useI18n()
const userStore = useUserStore()
const aclStore = useAclStore()
// ChatBox组件所需的配置
const chatType = ref('1') // 聊天类型
const userId = computed(() => aclStore.getUserId) // 用户ID
const placeholder = ref('输入您的问题...') // 输入框占位符
const openRemark = ref('')
const recommendQuestions = ref<string[]>([])
// const openRemark = ref(`Hello😊, I am a BRI assistant, a senior train fault analysis expert. You can ask questions about train faults, and I can analyze the retrieved data. Please refer to the following tips:
// | Question templates for different functions | Description |
// |--------------|----------|
// | Fault diagnosis | [Train car number] + [Fault problem/fault phenomenon] |
// | Maintenance record query | [Train car number] + [Latest N maintenance records] |`) // 开场白
// const recommendQuestions = ref(['T34 train, What to do if the Passenger Door cannot be opened','The latest 5 maintenance records of T34 train'])
const showChart = ref(false)
const pdfUrl = ref('/static-resources/files/pdfFile/开悟大模型智能体平台简介1.pdf')
const pdfVisible = ref(false)
const dialogVisible = ref(false)
const pdfFiles = ref([])
const loadChart = ref(true)
const isRemarkLoaded = ref(false)
// 定义类型
type ConversationIdInfo = Record<string, string>
// 响应式变量(初始值解析为对象)
const conversationIdInfo = ref<ConversationIdInfo | null>(parseLocalStorageValue(localStorage.getItem('conversationIdInfo')))
// 安全解析 localStorage 值
function parseLocalStorageValue(value: string | null): ConversationIdInfo | null {
if (!value) return null
try {
const parsed = JSON.parse(value) as ConversationIdInfo
return Object.keys(parsed).length > 0 ? parsed : null
} catch {
return null
}
}
const tableData = ref([])
const pieChartData = ref([])
const chatData = reactive({
tableData: [] as TableDataItem[],
pieChartData: [] as PieChartDataItem[],
})
// 定义类型接口
interface TableDataItem {
date: string
rootCause: string
incidentId: number
symptom: string
trainNo: string
carNo: string
location: string
findings: string
action: string
system: string
system1: string
equipment: string
time: string
}
interface PieChartDataItem {
percent: string
name: string
}
// 获取数据库中的 openRemark 数据
const fetchOpenRemark = async () => {
try {
const response = await getRemark(chatType.value) // 根据 chatType 获取开场白内容
if (response.data && response.data.content) {
openRemark.value = response.data.content // 更新 openRemark
} else {
openRemark.value = '默认的开场白' // 如果没有返回内容,设置默认值
}
isRemarkLoaded.value = true // 数据加载完成后设置为 true
} catch (error) {
ElMessage.error('加载开场白失败,请稍后重试')
console.error(error)
isRemarkLoaded.value = true
}
}
// 获取推荐问题
const fetchRecommendations = async () => {
try {
const response = await getRecommendations(chatType.value) // 根据 chatType 获取推荐问题列表
if (response.data) {
recommendQuestions.value = response.data.map(item => item.questionContent) // 更新推荐问题
} else {
recommendQuestions.value = []
}
} catch (error) {
ElMessage.error('加载推荐问题失败,请稍后重试')
console.error(error)
}
}
// 在组件加载时获取 openRemark 和推荐问题数据
onMounted(() => {
fetchOpenRemark()
fetchRecommendations()
})
const openPdf = (url: string) => {
pdfUrl.value = url.replace('/var/www/assets/', '/')
console.log('pdfUrl.value', pdfUrl.value)
pdfVisible.value = true
}
const handleClose = () => {
// console.log('Dialog closed');
}
const openPdftable = () => {
console.log('conversationIdInfo@@', conversationIdInfo)
const values = Object.values(conversationIdInfo.value || {})
let conversationId = ''
if (values.length > 0) {
const firstValue = values[0]
conversationId = typeof firstValue === 'string' ? firstValue : ''
}
console.log('conversationId@@', conversationId)
if (conversationId) {
getFilePathList(conversationId).then(({ data }) => {
console.log('data', data)
pdfFiles.value = data
})
}
console.log('pdfFiles.value', pdfFiles.value)
dialogVisible.value = true
}
// 处理 storage 事件
const handleStorageChange = (event: StorageEvent) => {
if (event.key === 'conversationIdInfo') {
conversationIdInfo.value = parseLocalStorageValue(event.newValue)
}
}
// ChatBox组件事件处理器
// 处理流式请求完成事件
const handleStreamComplete = ({ conversationId, messageId, content }) => {
console.log('流式请求完成:', { conversationId, messageId, content })
getMaintStatData(conversationId,messageId).then(({ data }) => {
//console.log('data', data)
console.log('data', data)
if(data.isExistRecord){
chatData.tableData = data.tableData
chatData.pieChartData = data.pieChartData
showChart.value = true
loadChart.value = false
}else{
console.log('常规问答: data',data)
showChart.value = false
loadChart.value = false
}
})
}
// 处理文件来源点击事件
const handleSourceClick = (source: TraceFile) => {
console.log('点击文件来源:', source)
ElMessage.info(`点击了文件: ${source.fileName}`)
// 可以在这里添加文件预览或下载逻辑
}
// 处理消息接收事件
const handleMessageReceived = (message: any) => {
console.log('收到新消息:', message)
// 可以在这里处理消息接收后的逻辑
}
// 处理新会话创建事件
const handleChatCreated = (chatIndex: number) => {
console.log('创建新会话:', chatIndex)
// 可以在这里处理新会话创建后的逻辑
}
// 处理会话切换事件
const handleChatSwitched = (chatIndex: number) => {
console.log('切换会话:', chatIndex)
// 可以在这里处理会话切换后的逻辑
}
onMounted(() => {
window.addEventListener('storage', handleStorageChange)
webSocketService.on('message', (data) => {
try {
const parsedData = JSON.parse(JSON.parse(data))
if (parsedData.pieChartData && parsedData.tableData) {
chatData.tableData = parsedData.tableData
chatData.pieChartData = parsedData.pieChartData
console.log('send chatData', chatData)
showChart.value = true
loadChart.value = false
}
} catch (error) {
console.error('JSON解析错误:', error)
}
})
})
onBeforeUnmount(() => {
window.removeEventListener('storage', handleStorageChange)
})
// 发送历史消息
const sendHistoryMessage = (conversationId: string, query: string) => {
const dateTime = new Date()
const options: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
}
const message = {
conversationId: conversationId,
questionTime: dateTime
.toLocaleString('zh-CN', options)
.replace(/\//g, '-')
.replace(/(\d+)-(\d+)-(\d+), (\d+):(\d+):(\d+)/, '$1-$2-$3 $4:$5:$6'),
content: query,
}
sendMessages(message).then(({ data }) => {
console.log('data', data)
})
}
</script>
<style scoped>
.main-container {
height: 100vh;
padding: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
box-sizing: border-box;
}
.content-row {
flex: 1;
height: 100%;
margin: 0 !important;
}
.chat-panel {
height: 100%;
padding: 0 4px;
}
.chart-panel {
height: 100%;
padding: 0 4px;
}
.chart-div {
height: 100%;
display: flex;
flex-direction: column;
gap: 10px;
}
.chart-content {
flex: 1;
min-height: 0;
}
.chart-actions {
flex-shrink: 0;
padding: 10px 0;
text-align: center;
}
.chart-area {
height: calc(100vh - 40px);
}
.chart-item {
margin-bottom: 15px;
}
.el-card {
margin-bottom: 15px;
transition: box-shadow 0.2s;
}
.el-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
h4 {
margin: 0 0 16px 0;
font-size: 16px;
color: #1f2d3d;
}
/* 确保行和列占满容器高度 */
.el-row {
height: 100%;
}
.el-col {
height: 100%;
}
</style>