提交 7e659cdd authored 作者: 王鹏飞's avatar 王鹏飞

chore: 新增AI数据分析

上级 510bd582
......@@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@microsoft/fetch-event-source": "^2.0.1",
"@tinymce/tinymce-vue": "^5.0.0",
"@vant/area-data": "^1.5.1",
"@vueuse/core": "^9.13.0",
......@@ -1108,6 +1109,11 @@
"@jridgewell/sourcemap-codec": "1.4.14"
}
},
"node_modules/@microsoft/fetch-event-source": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
"integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
......@@ -7655,6 +7661,11 @@
"@jridgewell/sourcemap-codec": "1.4.14"
}
},
"@microsoft/fetch-event-source": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
"integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="
},
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
......
......@@ -15,6 +15,7 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@microsoft/fetch-event-source": "^2.0.1",
"@tinymce/tinymce-vue": "^5.0.0",
"@vant/area-data": "^1.5.1",
"@vueuse/core": "^9.13.0",
......
const appConfigList = [
{
system: 'default',
title: '商业数据分析实验室',
logo: 'https://zws-imgs-pub.ezijing.com/pc/base/ezijing-logo.svg',
hosts: ['saas-lab']
......
import httpRequest from '@/utils/axios'
// 聊天(流式响应)
export function qwenChat(data: any) {
return httpRequest.post('/api/lab/v1/experiment/qwen/chat', data, { headers: { 'Content-Type': 'application/json' } })
}
<script setup>
const emit = defineEmits(['success'])
const file = ref()
const onSuccess = res => {
file.value = res.data.detail
emit('success', file.value)
}
</script>
<template>
<el-upload
class="ai-upload"
drag
action="/api/lab/v1/experiment/qwen/upload-file"
accept=".csv, .xls, .xlsx, text/csv, application/csv,text/comma-separated-values, application/csv, application/excel,application/vnd.msexcel, text/anytext, application/vnd. ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
:data="{ purpose: 'file-extract' }"
:show-file-list="false"
:on-success="onSuccess">
<ul class="ai-upload-list" v-if="file">
<li>{{ file.filename }}</li>
</ul>
<div class="ai-upload-box">
<img src="@/assets/images/ai_plus.png" height="40" />
<div class="el-upload__text"><em>点击</em>上传数据文件</div>
</div>
</el-upload>
</template>
<style lang="scss">
.ai-upload {
.el-upload-dragger {
padding: 20px;
}
.ai-upload-list {
margin-bottom: 20px;
text-align: left;
li {
padding: 0 10px;
display: flex;
align-items: center;
min-height: 40px;
background: #ffffff;
box-shadow: 0px 3px 6px 1px rgba(0, 0, 0, 0.16);
}
}
.el-upload__text {
margin-top: 20px;
em {
text-decoration: underline;
}
}
}
</style>
import { fetchEventSource } from '@microsoft/fetch-event-source'
import { ElMessage } from 'element-plus'
export function useChat() {
const messages = ref([])
const isLoading = ref(false)
async function post() {
isLoading.value = true
await fetchEventSource('/api/lab/v1/experiment/qwen/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'qwen-long', messages: messages.value }),
async onopen(response) {
if (response.ok) {
return response
} else {
throw response
}
},
onmessage(res) {
console.log(res.data)
try {
const message = JSON.parse(res.data)
if (message.error) {
ElMessage.error(message.error.message)
return
}
const id = message.id
const messageIndex = messages.value.findIndex(session => session.id === id)
let content = message?.choices[0]?.delta.content || ''
content = content.replaceAll('\n', '<br/>')
if (messageIndex === -1) {
messages.value.push({ id, role: 'assistant', content })
} else {
messages.value[messageIndex].content = messages.value[messageIndex].content + content
}
isLoading.value = false
} catch (error) {
console.log(error)
isLoading.value = false
}
},
onerror(err) {
isLoading.value = false
throw err
}
})
}
return { messages, post, isLoading }
}
import type { RouteRecordRaw } from 'vue-router'
export const routes: Array<RouteRecordRaw> = [
{
path: '/ai',
component: () => import('./views/Index.vue')
}
]
<script setup>
import Upload from '../components/Upload.vue'
import { useChat } from '../composabels/useChat'
const { messages, post, isLoading } = useChat()
const chatInput = ref('')
const onUploadSuccess = res => {
const message = { role: 'system', content: `fileid://${res.id}` }
messages.value.push(message)
}
async function postMessage() {
if (!chatInput.value) return
const message = { role: 'user', content: chatInput.value }
messages.value.push(message)
post(message)
chatInput.value = ''
}
const chatRef = ref()
function scrollToBottom() {
if (!chatRef.value) return
chatRef.value.scrollTo(0, chatRef.value.scrollHeight)
}
watch(messages.value, () => nextTick(() => scrollToBottom()))
</script>
<template>
<div class="ai-wrapper" :class="{ 'is-center': !messages.length }">
<header class="ai-header">
<div class="ai-header-inner">
<div class="ai-header-left">
<img src="https://zws-imgs-pub.ezijing.com/pc/base/ezijing-logo.svg" width="174" />
<div class="ai-header__title">AI商业数据分析</div>
</div>
<div class="ai-header-right">感知AI数据分析,让数据一触即知</div>
</div>
</header>
<main class="ai-main">
<Upload @success="onUploadSuccess" />
<div class="ai-message" ref="chatRef">
<template v-for="(item, index) in messages" :key="index">
<div class="ai-message-item" :class="item.role" v-if="item.role !== 'system'">
<div class="ai-message__avatar"><img :src="item.role === 'assistant' ? '/images/ai_avatar_bot.png' : '/images/ai_avatar_user.png'" /></div>
<div class="ai-message__content" v-html="item.content"></div>
</div>
</template>
<div class="ai-message-item" v-if="isLoading">
<div class="dot-flashing"></div>
</div>
</div>
<footer class="ai-footer">
<el-input placeholder="输入你想提问的问题" v-model="chatInput" @keyup.enter="postMessage">
<template #suffix>
<img src="@/assets/images/ai_send.png" class="ai-footer__button" @click="postMessage" />
</template>
</el-input>
</footer>
</main>
</div>
</template>
<style lang="scss">
.ai-wrapper {
height: 100vh;
display: flex;
align-items: center;
flex-direction: column;
&.is-center {
justify-content: space-evenly;
.ai-header {
box-shadow: none;
}
.ai-main {
flex: none;
}
.el-upload-dragger {
padding: 80px 0;
}
}
}
.ai-main {
display: flex;
flex-direction: column;
flex: 1;
width: 1000px;
overflow: hidden;
.ai-upload {
margin: 40px 0;
}
}
.ai-header {
width: 100%;
box-shadow: 0px 3px 6px 1px rgba(0, 0, 0, 0.16);
}
.ai-header-inner {
max-width: 1000px;
padding: 18px 0;
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 auto;
}
.ai-header__title {
margin-top: 8px;
font-family: Source Han Sans CN, Source Han Sans CN;
font-weight: 400;
font-size: 16px;
color: #2c2c2c;
letter-spacing: 7px;
text-align: right;
}
.ai-header-right {
font-family: Source Han Sans CN, Source Han Sans CN;
font-weight: 400;
font-size: 24px;
color: #2c2c2c;
letter-spacing: 5px;
}
.ai-footer {
margin: 40px 10px;
.el-input__wrapper {
height: 60px;
font-size: 16px;
border-radius: 33px;
box-shadow: 0px 3px 12px 1px rgba(0, 0, 0, 0.12);
}
}
.ai-footer__button {
cursor: pointer;
}
.ai-message {
min-height: 100px;
flex: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
overflow-x: hidden;
overflow-y: auto;
}
.ai-message-item {
margin-bottom: 30px;
padding: 10px;
border-radius: 12px;
background-color: #f5edef;
display: flex;
color: #000;
gap: 10px;
}
.ai-message-item.user {
align-self: flex-end;
color: #fff;
background-color: #ab2940;
flex-direction: row-reverse;
}
.ai-message__avatar {
width: 48px;
height: 48px;
background-color: #fff;
border-radius: 50%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.ai-message__content {
flex: 1;
max-width: 100%;
font-size: 16px;
line-height: 24px;
word-break: break-word;
align-self: center;
}
.dot-flashing {
animation: dot-flashing 0.8s infinite alternate;
animation-delay: -0.2s;
animation-timing-function: ease;
margin: 7px 18px;
overflow: visible !important;
position: relative;
}
.dot-flashing,
.dot-flashing:after,
.dot-flashing:before {
background-color: rgba(0, 0, 0, 0.1);
border-radius: 4px;
color: rgba(0, 0, 0, 0.1);
height: 8px;
width: 8px;
}
.dot-flashing:after,
.dot-flashing:before {
animation: dot-flashing 0.8s infinite alternate;
animation-timing-function: ease;
content: '';
display: inline-block;
position: absolute;
top: 0;
}
.dot-flashing:before {
left: -15px;
animation-delay: -0.4s;
}
.dot-flashing:after {
left: 15px;
animation-delay: 0s;
}
@keyframes dot-flashing {
0% {
background-color: #000;
}
50% {
background-color: rgba(0, 0, 0, 0.1);
}
to {
background-color: #000;
}
}
</style>
......@@ -109,11 +109,9 @@ const LAB_URL: any = computed(() => {
let iframeKey = $ref(Date.now())
// 返回首页
function handleBackHome() {
ElMessageBox.confirm('此操作将会强制返回到实验室首页,您当前的操作内容有可能丢失,确定返回首页吗?', '提示').then(
() => {
ElMessageBox.confirm('此操作将会强制返回到实验室首页,您当前的操作内容有可能丢失,确定返回首页吗?', '提示').then(() => {
iframeKey = Date.now()
}
)
})
}
const reportDialogVisible = $ref(false)
......@@ -170,14 +168,12 @@ function uploadPicture(url: string) {
}
// 提交实验
function handleSubmit() {
ElMessageBox.confirm('此操作将会提交该实验,状态会变为已提交,您将不能再操作该实验,确定提交实验吗?', '提示').then(
() => {
ElMessageBox.confirm('此操作将会提交该实验,状态会变为已提交,您将不能再操作该实验,确定提交实验吗?', '提示').then(() => {
submitExperimentRecord({ experiment_id: form.experiment_id }).then(() => {
ElMessage({ message: '提交成功', type: 'success' })
fetchExperimentRecord()
})
}
)
})
}
let resizeKey = $ref(0)
function handleResize() {
......@@ -275,54 +271,30 @@ function handleReportPreviewReady() {
<AppCard>
<el-row justify="space-between">
<div>
<el-button type="primary" :icon="HomeFilled" :disabled="submitted" @click="handleBackHome"
>返回首页</el-button
>
<el-button type="primary" :icon="HomeFilled" :disabled="submitted" @click="handleBackHome">返回首页</el-button>
<el-button type="primary" :disabled="disabled" @click="handleSubmit">提交实验</el-button>
</div>
<div>
<el-button type="primary" :disabled="disabled" :loading="screenshotLoading" @click="handleCapture"
>截图</el-button
>
<el-button type="primary" v-if="appConfig.system == 'default'">
<router-link to="/ai" target="_blank">AI数据分析</router-link>
</el-button>
<el-button type="primary" :disabled="disabled" :loading="screenshotLoading" @click="handleCapture">截图</el-button>
<el-button type="primary" :disabled="disabled" @click="prepareDialogVisible = true">实验准备</el-button>
<el-button type="primary" :disabled="disabled" @click="resultDialogVisible = true">实验结果</el-button>
<el-button
type="primary"
:disabled="disabled"
v-if="experimentInfo?.report_upload_way === 2 && !experimentInfo?.is_commit_report"
>
<el-button type="primary" :disabled="disabled" v-if="experimentInfo?.report_upload_way === 2 && !experimentInfo?.is_commit_report">
<router-link :to="`/student/lab/report/${form.experiment_id}`" target="_blank">在线实验报告</router-link>
</el-button>
<el-button
type="primary"
:disabled="disabled"
@click="reportDialogVisible = true"
v-if="experimentInfo?.report_upload_way === 1 && !submitted"
<el-button type="primary" :disabled="disabled" @click="reportDialogVisible = true" v-if="experimentInfo?.report_upload_way === 1 && !submitted"
>上传实验报告</el-button
>
<el-button type="primary" @click="handleReportView" v-if="experimentInfo?.is_commit_report"
>查看实验报告</el-button
>
<el-button
type="primary"
@click="handleReportExport"
v-if="detail?.status === 2 && experimentInfo?.is_commit_report"
>导出实验报告</el-button
>
<el-button type="primary" @click="handleReportView" v-if="experimentInfo?.is_commit_report">查看实验报告</el-button>
<el-button type="primary" @click="handleReportExport" v-if="detail?.status === 2 && experimentInfo?.is_commit_report">导出实验报告</el-button>
</div>
</el-row>
</AppCard>
<div class="lab-box">
<el-empty description="您已经提交该实验,不能再进行操作,切换其他实验再做操作吧。" v-if="submitted" />
<iframe
allowfullscreen
:src="LAB_URL"
:key="iframeKey"
frameborder="0"
class="iframe"
ref="iframeRef"
v-else
></iframe>
<iframe allowfullscreen :src="LAB_URL" :key="iframeKey" frameborder="0" class="iframe" ref="iframeRef" v-else></iframe>
</div>
</template>
</DragPanel>
......@@ -347,16 +319,10 @@ function handleReportPreviewReady() {
:experiment_id="form.experiment_id"
v-model="examURL"
:examStatus="experimentInfo?.exam_status"
v-if="experimentInfo?.exam_status === 0"
></Exam>
v-if="experimentInfo?.exam_status === 0"></Exam>
<Question :experiment_id="form.experiment_id" v-else></Question>
</el-tab-pane>
<el-tab-pane
label="理论考试"
name="exam"
lazy
v-show="experimentInfo?.exam_status === 1 && tabActive === 'exam'"
>
<el-tab-pane label="理论考试" name="exam" lazy v-show="experimentInfo?.exam_status === 1 && tabActive === 'exam'">
<Exam :experiment_id="form.experiment_id" v-model="examURL"></Exam>
</el-tab-pane>
<el-tab-pane label="实验信息" lazy>
......@@ -388,58 +354,28 @@ function handleReportPreviewReady() {
<AppCard v-else>
<el-row justify="space-between">
<div>
<el-button type="primary" :icon="HomeFilled" :disabled="submitted" @click="handleBackHome"
>返回首页</el-button
>
<el-button type="primary" :icon="HomeFilled" :disabled="submitted" @click="handleBackHome">返回首页</el-button>
<el-button type="primary" :disabled="disabled" @click="handleSubmit">提交实验</el-button>
</div>
<div>
<el-button type="primary" :disabled="disabled" :loading="screenshotLoading" @click="handleCapture"
>截图</el-button
>
<el-button type="primary" :disabled="disabled" :loading="screenshotLoading" @click="handleCapture">截图</el-button>
<el-button type="primary" :disabled="disabled" @click="prepareDialogVisible = true">实验准备</el-button>
<el-button type="primary" :disabled="disabled" @click="resultDialogVisible = true">实验结果</el-button>
<el-button
type="primary"
:disabled="disabled"
v-if="experimentInfo?.report_upload_way === 2 && !experimentInfo?.is_commit_report"
>
<el-button type="primary" :disabled="disabled" v-if="experimentInfo?.report_upload_way === 2 && !experimentInfo?.is_commit_report">
<router-link :to="`/student/lab/report/${form.experiment_id}`" target="_blank">在线实验报告</router-link>
</el-button>
<el-button
type="primary"
:disabled="disabled"
@click="reportDialogVisible = true"
v-if="experimentInfo?.report_upload_way === 1 && !submitted"
<el-button type="primary" :disabled="disabled" @click="reportDialogVisible = true" v-if="experimentInfo?.report_upload_way === 1 && !submitted"
>上传实验报告</el-button
>
<el-button type="primary" @click="handleReportView" v-if="experimentInfo?.is_commit_report"
>查看实验报告</el-button
>
<el-button
type="primary"
@click="handleReportExport"
v-if="detail?.status === 2 && experimentInfo?.is_commit_report"
>导出实验报告</el-button
>
<el-button type="primary" @click="handleReportView" v-if="experimentInfo?.is_commit_report">查看实验报告</el-button>
<el-button type="primary" @click="handleReportExport" v-if="detail?.status === 2 && experimentInfo?.is_commit_report">导出实验报告</el-button>
</div>
</el-row>
</AppCard>
<div class="lab-box">
<el-empty description="您已经提交该实验,不能再进行操作,切换其他实验再做操作吧。" v-if="submitted" />
<iframe
allowfullscreen
:src="LAB_URL"
:key="iframeKey"
frameborder="0"
class="iframe"
ref="iframeRef"
v-else
></iframe>
<div
style="padding: 10px; background-color: #fff; max-width: 300px; min-width: 300px"
v-if="experimentInfo?.exam_status === 1 && tabActive === 'qa'"
>
<iframe allowfullscreen :src="LAB_URL" :key="iframeKey" frameborder="0" class="iframe" ref="iframeRef" v-else></iframe>
<div style="padding: 10px; background-color: #fff; max-width: 300px; min-width: 300px" v-if="experimentInfo?.exam_status === 1 && tabActive === 'qa'">
<Question :experiment_id="form.experiment_id" :exam_status="experimentInfo?.exam_status"></Question>
</div>
</div>
......@@ -451,26 +387,13 @@ function handleReportPreviewReady() {
v-model="reportDialogVisible"
:data="experimentInfo"
@update="fetchExperimentRecord"
v-if="reportDialogVisible && experimentInfo"
></ReportDialog>
<ReportFilePreview
v-model="reportFilePreviewVisible"
:data="experimentInfo"
v-if="reportFilePreviewVisible && experimentInfo"
></ReportFilePreview>
v-if="reportDialogVisible && experimentInfo"></ReportDialog>
<ReportFilePreview v-model="reportFilePreviewVisible" :data="experimentInfo" v-if="reportFilePreviewVisible && experimentInfo"></ReportFilePreview>
<!-- 实验准备 -->
<PrepareDialog
v-model="prepareDialogVisible"
:data="experimentInfo"
v-if="prepareDialogVisible && experimentInfo"
></PrepareDialog>
<PrepareDialog v-model="prepareDialogVisible" :data="experimentInfo" v-if="prepareDialogVisible && experimentInfo"></PrepareDialog>
<!-- 实验结果 -->
<ResultDialog
v-model="resultDialogVisible"
:data="experimentInfo"
v-if="resultDialogVisible && experimentInfo"
></ResultDialog>
<ResultDialog v-model="resultDialogVisible" :data="experimentInfo" v-if="resultDialogVisible && experimentInfo"></ResultDialog>
<!-- 导出在线报告 -->
<template v-if="experimentInfo?.id && isExport">
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论