提交 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>
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论