fuxiaochun 2023-08-21 21:39:19 +08:00
commit 4d4deaffd7
32 changed files with 1022 additions and 916 deletions

View File

@ -16,10 +16,12 @@
"echarts": "^5.4.3",
"highlight.js": "^11.8.0",
"lodash-es": "^4.17.21",
"mammoth": "^1.6.0",
"markdown-it": "^13.0.1",
"markdown-it-link-attributes": "^4.0.1",
"md5": "^2.3.0",
"mitt": "^3.0.1",
"pdfjs-dist": "2.6.347",
"pinia": "^2.1.3",
"qrcode.vue": "^3.4.1",
"resize-observer-polyfill": "^1.5.1",

View File

@ -57,9 +57,11 @@
margin: 0;
font-weight: normal;
}
html{
height: 100%;
}
body {
height: 100vh;
height: 100%;
/* min-height: 100vh; */
color: var(--color-text);
background: var(--color-background);

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1692600654355" class="icon" viewBox="0 0 1295 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4085" xmlns:xlink="http://www.w3.org/1999/xlink" width="252.9296875" height="200"><path d="M694.151529 942.622118l-0.602353-1.024c-0.813176-1.415529-2.228706-3.222588-3.463529-4.818824l-0.421647-0.692706c-0.632471-0.903529-1.385412-1.867294-1.867294-2.770823l-3.102118-4.48753-0.361412-0.421647-151.943529-233.893647a125.711059 125.711059 0 0 1-18.070588-92.79247 122.699294 122.699294 0 0 1 50.507294-78.817883 116.615529 116.615529 0 0 1 118.061176-8.583529V123.392C682.706824 55.416471 733.936941 0 796.882824 0c62.855529 0 114.176 55.416471 114.176 123.482353v125.590588a91.557647 91.557647 0 0 1 28.672-4.668235c29.756235 0 57.886118 14.336 77.703529 39.092706a96.527059 96.527059 0 0 1 52.073412-16.143059 94.870588 94.870588 0 0 1 58.729411 20.751059c13.101176 10.24 24.094118 23.491765 32.075295 38.881882 9.276235-2.921412 18.913882-4.517647 28.581647-4.517647 58.578824 0 106.194824 55.416471 106.194823 123.482353v328.523294c0 72.673882-32.135529 152.997647-81.859765 204.739765-27.708235 28.822588-57.524706 44.784941-84.239058 44.784941H804.382118c-60.837647 0-88.154353-45.116235-110.230589-81.377882z m441.434353 20.660706c9.848471 0 34.334118-16.444235 58.337883-52.856471 25.750588-39.303529 40.508235-86.347294 40.508235-128.903529V443.602824c0-27.617882-17.046588-51.591529-36.472471-51.59153a25.901176 25.901176 0 0 0-12.288 3.162353v118.181647h-0.12047c-1.385412 19.817412-17.287529 35.177412-36.472471 35.267765-19.245176 0-35.237647-15.661176-36.532706-35.237647h-0.060235V410.804706l-2.198588-28.792471c-1.897412-25.660235-18.311529-46.592-36.653177-46.592-8.131765 0-16.474353 4.517647-23.070117 12.047059v127.819294c-0.060235 20.901647-16.444235 37.797647-36.683294 37.857883-20.178824-0.090353-36.532706-16.986353-36.592942-37.857883v-112.459294l-2.740705-13.131294c-4.517647-21.865412-19.696941-38.309647-35.147295-38.309647-10.541176 0-21.082353 7.469176-28.220235 19.606588l-1.174588 126.795294v47.676236c0 18.492235-14.667294 33.701647-32.557177 33.701647h-7.920941c-17.980235 0-32.587294-15.209412-32.587294-33.701647V124.897882l-0.210823-13.43247c0-28.461176-19.998118-51.501176-44.724706-51.501177-24.696471 0-44.694588 23.100235-44.694589 51.501177l1.596236 369.844706c0.090353 1.114353 0.120471 2.258824 0.12047 3.403294v146.733176c-0.301176 18.883765-13.944471 34.695529-32.045176 37.104941a36.743529 36.743529 0 0 1-40.056471-27.527529l-13.763764-41.472a49.784471 49.784471 0 0 0-31.774118-21.985882 48.489412 48.489412 0 0 0-37.466353 7.770353c-22.889412 15.902118-28.943059 47.947294-13.613176 71.619764l156.641882 240.730353 0.512 0.662588 1.927529 2.95153 2.409412 3.70447c1.475765 2.409412 3.072 4.698353 4.487529 6.475295l1.024 1.325176 0.752942 1.656471c24.395294 40.297412 33.792 49.001412 52.314353 49.001411h335.209411v-0.240941 0.060236z" fill="#000000" p-id="4086"></path><path d="M308.495059 30.117647h60.235294v843.294118h-60.235294zM450.951529 888.832l-105.953882 105.984a9.035294 9.035294 0 0 1-12.769882 0l-105.984-105.984A9.035294 9.035294 0 0 1 232.628706 873.411765h211.937882a9.035294 9.035294 0 0 1 6.384941 15.420235z" fill="#000000" p-id="4087"></path><path d="M150.588235 1024H90.352941V180.705882h60.235294z" fill="#000000" p-id="4088"></path><path d="M8.131765 165.285647l105.953882-105.984a9.035294 9.035294 0 0 1 12.769882 0l105.984 105.984A9.035294 9.035294 0 0 1 226.424471 180.705882H14.516706a9.035294 9.035294 0 0 1-6.415059-15.420235z" fill="#000000" p-id="4089"></path></svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1692435749942" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3272" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M797.696 442.368q-60.416 0-113.152 22.528t-91.648 61.952-61.952 92.16-23.04 113.152q0 50.176 17.408 97.28l-392.192 0q-21.504 0-44.544-9.216t-42.496-26.112-31.744-40.96-12.288-53.76l0-41.984 0-81.92q0-47.104-0.512-100.864t-0.512-99.84l0-78.848 0-35.84q0-61.44 34.304-97.28t94.72-35.84l503.808 0q23.552 0 47.616 8.704t43.008 24.064 31.232 35.84 12.288 44.032l0 13.312-517.12 0q-51.2 0-67.584 57.344-8.192 29.696-18.432 62.976t-18.432 61.952q-10.24 33.792-20.48 65.536-2.048 8.192-2.048 14.336 0 16.384 11.264 28.16t28.672 11.776 26.624-12.288 14.336-27.648l56.32-196.608 541.696 0q18.432 3.072 35.84 11.776t30.208 23.552 18.432 36.864 0.512 51.712q0 2.048-0.512 5.12t-1.536 8.192q-39.936-13.312-88.064-13.312zM802.816 510.976q46.08 0 86.016 17.408t70.144 47.616 47.616 70.144 17.408 86.016-17.408 86.016-47.616 70.144-70.144 47.616-86.016 17.408-86.016-17.408-70.144-47.616-47.616-70.144-17.408-86.016 17.408-86.016 47.616-70.144 70.144-47.616 86.016-17.408zM845.824 729.088l67.584-68.608q9.216-9.216 9.216-22.528t-9.216-22.528-22.528-9.216-22.528 9.216l-68.608 68.608-67.584-68.608q-9.216-9.216-22.528-9.216t-22.528 9.216-9.216 22.528 9.216 22.528l67.584 68.608-67.584 67.584q-9.216 9.216-9.216 22.528t9.216 22.528q9.216 10.24 22.528 10.24t22.528-10.24l67.584-67.584 68.608 67.584q9.216 10.24 22.528 10.24t22.528-10.24q9.216-9.216 9.216-22.528t-9.216-22.528z" p-id="3273"></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1692433413174" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="1991" xmlns:xlink="http://www.w3.org/1999/xlink"
width="200" height="200">
<path
d="M855.04 385.024q19.456 2.048 38.912 10.24t33.792 23.04 21.504 37.376 2.048 54.272q-2.048 8.192-8.192 40.448t-14.336 74.24-18.432 86.528-19.456 76.288q-5.12 18.432-14.848 37.888t-25.088 35.328-36.864 26.112-51.2 10.24H195.584q-21.504 0-44.544-9.216t-42.496-26.112-31.744-40.96-12.288-53.76V327.68q0-62.464 33.792-97.792t95.232-35.328h503.808q22.528 0 46.592 8.704t43.52 24.064 31.744 35.84 12.288 44.032v11.264H778.24q-40.96 0-95.744-0.512t-116.736-0.512-115.712-0.512-92.672-0.512h-47.104q-26.624 0-41.472 16.896t-23.04 44.544q-8.192 29.696-18.432 62.976t-18.432 61.952q-10.24 33.792-20.48 65.536-2.048 8.192-2.048 13.312 0 17.408 11.776 29.184t29.184 11.776q31.744 0 43.008-39.936L324.608 384q133.12 1.024 243.712 1.024h286.72z"
p-id="1992"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -61,10 +61,10 @@ service.interceptors.response.use(
error => {
const authModal = useAuthModal();
const userInfo = useUserInfo();
if (error.message == 'canceled') return Promise.reject(error)
const res = error.response?.data
const { requestBaseUrl } = error.config
if (requestBaseUrl == 'chat') {
if (res.errcode == 401) {
@ -73,6 +73,9 @@ service.interceptors.response.use(
userInfo.updateUserInfo({});
localCache.remove('auth');
localCache.remove('userInfo');
} else if (res.errcode == 10011) {
localCache.remove('auth');
localCache.remove('userInfo');
} else {
showToast(res.errmsg || 'Error')
}

View File

@ -1,43 +1,58 @@
<template>
<div class="layout">
<Header />
<div class="layoutContent">
<Content />
<Footer v-if="isFooter" />
</div>
<div class="layout">
<Header />
<div class="layoutContent">
<Content />
<Footer v-if="isFooter" />
</div>
</div>
<!-- <div class="h-full flex flex-col">
<div class="h-90px w-full"></div>
<div class="fixed h-90px w-full z-99">
<Header />
</div>
<div class="relative min-h-[calc(100vh-90px)]">
<Content />
</div>
<div class="w-full">
<Footer v-if="isFooter" />
</div>
</div> -->
</template>
<script setup>
import Header from './Header.vue';
import Content from './Content.vue';
import Footer from './Footer.vue';
import { useRouter } from 'vue-router';
import { watch,ref } from 'vue'
const router = useRouter();
const isFooter = ref(true);
watch(router.currentRoute, (to) => {
isFooter.value = to.meta?.footer ?? true;
},{
immediate: true
})
import Header from './Header.vue'
import Content from './Content.vue'
import Footer from './Footer.vue'
import { useRouter } from 'vue-router'
import { watch, ref } from 'vue'
const router = useRouter()
const isFooter = ref(true)
watch(
router.currentRoute,
(to) => {
isFooter.value = to.meta?.footer ?? true
},
{
immediate: true,
}
)
</script>
<style lang="scss" scoped>
.layout{
.layout {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
.layoutContent {
flex: 1;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
.layoutContent{
flex: 1;
width: 100%;
overflow-y: auto;
position: relative;
z-index: 1;
}
overflow-y: auto;
position: relative;
z-index: 1;
}
}
</style>
</style>

View File

@ -19,7 +19,7 @@ function platCheck() {
const userAgent = navigator.userAgent.toLowerCase();
const mobileKeywords = ['iphone', 'ipod', 'android', 'silk', 'blackberry', 'bb10', 'phone', 'mobile', 'kindle', 'opera mini', 'mobile safari', 'windows phone'];
const isMobileDevice = mobileKeywords.some(keyword => userAgent.includes(keyword));
if (!isMobileDevice) {
if (!isMobileDevice && import.meta.env.PROD) {
let _href = window.location.href;
let _origin = window.location.origin;
window.location.href = _href.replace(_origin, import.meta.env.VITE_PC_HOST);

View File

@ -16,6 +16,7 @@ const router = createRouter({
meta: {
title: "首页", // 页面标题
group: 'home', // 导航归属
footer: false,
},
component: () => import("@/views/home/index.vue"),
},
@ -33,7 +34,7 @@ const router = createRouter({
component: () => import("@/views/chat/index.vue"),
meta: {
title: 'AI助理',
footer: false,
}
}]
},

View File

@ -100,12 +100,21 @@ export const useChat = defineStore("chat-store", {
requestBaseUrl: 'chat',
}).then((res) => {
const { messages } = res;
const chat = this.chat.find(item => item.id === uuid)
const localMessages = chat?.messages ?? []
const temp = messages.map((item) => {
var key = Object.keys(item)[0];
const localItem = (localMessages.find(item => Object.keys(item)[0] == key) ?? {})[key] ?? {}
const { role } = item[key]
const obj = Object.assign({}, { ...item[key] }, { loading: false, inversion: role == 'user' })
const obj = Object.assign({}, localItem, { ...item[key] }, {
loading: false, inversion: role == 'user'
})
return {
[key]: obj
[key]: obj,
};
});
this.setChat(uuid, { ...res, messages: temp })

View File

@ -0,0 +1,10 @@
import mammoth from 'mammoth';
import { readFileAsArrayBuffer } from './fileUtils'
export default async function extractDocxText(file) {
const arrayBuffer = await readFileAsArrayBuffer(file)
const options = {}
const result = await mammoth.extractRawText({ arrayBuffer }, options)
return result.value
}

View File

@ -0,0 +1,28 @@
export const readFileAsArrayBuffer = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => {
resolve(reader.result)
}
reader.onerror = () => {
reject(new Error('Error reading the file.'))
}
reader.readAsArrayBuffer(file)
})
}
export function getFileInfo(file) {
const fileName = file.name;
const fileExtension = getFileExtension(fileName);
const fileSize = file.size;
return {
name: fileName,
extension: fileExtension,
size: fileSize
};
}
export function getFileExtension(fileName) {
const parts = fileName.split('.');
return parts[parts.length - 1].toLowerCase();
}

View File

@ -0,0 +1,28 @@
import * as pdfjsLib from 'pdfjs-dist'
import * as pdfWorkerMin from 'pdfjs-dist/build/pdf.worker.min?url'
import { readFileAsArrayBuffer } from './fileUtils'
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorkerMin.default
export default async function extractTextFromPDF(file) {
const arrayBuffer = await readFileAsArrayBuffer(file)
const loadingTask = pdfjsLib.getDocument(arrayBuffer);
const pdfDocument = await loadingTask.promise;
const numPages = pdfDocument.numPages;
let text = '';
for (let pageNum = 1; pageNum <= numPages; pageNum++) {
const page = await pdfDocument.getPage(pageNum);
const pageText = await page.getTextContent();
pageText.items.forEach(item => {
text += item.str + ' ';
});
}
return text;
}

View File

@ -1,16 +1,19 @@
<template>
<Layout path="/chat">
<div class="h-full flex flex-col justify-center text-white">
<TitleComp title="AI助理" :src="TitleSrc" />
<div class="mt-36px text-27px font-bold">
CHATGPT接口AI助手
</div>
<div class="text-22px mt-28px text-[#C2C5CA] leading-29px">
在AI助理板块用户可以上传文件给AI翻阅并帮助您解决问题您可以自己输入关键字或者使用自带的prompt模板
</div>
<div class="text-center">
<img class="w-618px mx-auto" src="@/assets/images/ai_assistant_02.png" />
<div class="min-h-900px">
<TitleComp title="AI助理" :src="TitleSrc" />
<div class="mt-36px text-27px font-bold">CHATGPT接口AI助手</div>
<div class="text-22px mt-28px text-[#C2C5CA] leading-29px">
在AI助理板块用户可以上传文件给AI翻阅并帮助您解决问题您可以自己输入关键字或者使用自带的prompt模板
</div>
<div class="text-center">
<img
class="w-618px mx-auto"
src="@/assets/images/ai_assistant_02.png"
/>
</div>
</div>
</div>
</Layout>

View File

@ -1,21 +1,26 @@
<template>
<Layout path="/business">
<div class="h-full flex flex-col justify-center text-white">
<TitleComp title="AI商情" :src="TitleSrc">
<template #en>
<img class="h-104px w-auto" :src="TitleSrc" alt="" srcset="" />
</template>
</TitleComp>
<div class="mt-36px text-27px font-bold">AI商情预测</div>
<div class="text-22px mt-28px text-[#C2C5CA] leading-29px">
在AI助理板块用户可以上传文件给AI翻阅并帮助您解决问题您可以自己输入关键字或者使用自带的prompt模板在AI助理板块用户可以上传文件给AI翻阅并帮助您解决问题您可以自己输入关键字或者使用自带的prompt模
</div>
<div class="mt-61px">
<img class="w-658px mx-auto inline-block" src="@/assets/images/ai_busionessinfomation_01.png" />
</div>
<Layout path="/business">
<div class="h-full flex flex-col justify-center text-white">
<div class="min-h-900px">
<TitleComp title="AI商情" :src="TitleSrc">
<template #en>
<img class="h-104px w-auto" :src="TitleSrc" alt="" srcset="" />
</template>
</TitleComp>
<div class="mt-36px text-27px font-bold">AI商情预测</div>
<div class="text-22px mt-28px text-[#C2C5CA] leading-29px">
在AI助理板块用户可以上传文件给AI翻阅并帮助您解决问题您可以自己输入关键字或者使用自带的prompt模板在AI助理板块用户可以上传文件给AI翻阅并帮助您解决问题您可以自己输入关键字或者使用自带的prompt模
</div>
</Layout>
<div class="mt-61px">
<img
class="w-658px mx-auto inline-block"
src="@/assets/images/ai_busionessinfomation_01.png"
/>
</div>
</div>
</div>
</Layout>
</template>
<script setup>
import TitleComp from '@/views/home/components/title.vue'

View File

@ -35,12 +35,13 @@
<div class="mt-10px h-600px">
<ScrollContainer class="!h-full" ref="scrollRefAi">
<div class="px-0">
<div class="text-27px text-white opacity-40 leading-34px">
<div class="text-27px text-white leading-34px">
<div class="text-center" v-if="contentLoading">
<van-loading type="spinner" size="24"/>
</div>
<div v-else>
{{ contenText }}
<div class="text-white" v-else>
<TextComponent class="cu-text" :text="contenText" :inversion="false"></TextComponent>
<!-- {{ contenText }} -->
</div>
</div>
<Message
@ -89,6 +90,7 @@ import { ref, computed, nextTick } from 'vue'
import http from '@/io/request'
import { v4 as uuidv4 } from 'uuid'
import { useAiChat } from '@/stores/aichat'
import TextComponent from './text.vue'
const props = defineProps({
top: {
type: String,
@ -138,7 +140,9 @@ const options = [
name: 'Q&A',
value: `
Instructions: List the highlights of the content in the form of Q&As, no less than 3 Q&As. Here is an example of the template output you should use:
##### Who are you?
Who are you?
I am AI.
Be sure not to write out the template examples.
@ -360,6 +364,12 @@ function scrollToBottomIfAtBottom() {
</script>
<style lang="scss">
.cu-text{
&.message-reply{
background: transparent !important;
@apply text-sm;
}
}
.cu-field-ai {
height: 100% !important;
background-color: #1e1f21;

View File

@ -1,5 +1,5 @@
<template>
<div class="text-50px">
<div class="text-46px">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 32"

View File

@ -1,344 +0,0 @@
<template>
<LayoutContent>
<div class="flex flex-col w-full h-full">
<ScrollContainer id="scrollRef" ref="scrollRef">
<div class="p-4">
<MessageGroup
v-for="(item, i) in dataSources"
:key="i"
:arr="item"
></MessageGroup>
</div>
</ScrollContainer>
<div class="p-2">
<div class="border border-[#414548]">
<div class="border-b border-[#414548] h-9 flex items-center px-5">
<SvgIcon
@click="handleRefresh"
name="refresh"
class="text-white text-base cursor-pointer mr-6"
></SvgIcon>
<SvgIcon
@click="handleStop"
name="stop"
class="text-white text-base cursor-pointer mr-6"
></SvgIcon>
</div>
<a-textarea
ref="inputRef"
class="bg-transparent border-none rounded-none text-white minp focus:shadow-none placeholder-[#FFFFFF40]"
placeholder="请输入内容"
:rows="4"
v-model:value="inputValue"
@pressEnter="handleEnter"
/>
<div class="flex pb-5 px-5 pt-4 justify-between">
<div class="flex items-end">
<a-button
@click="handleClear"
type="primary"
class="rounded-4px h-9.5 px-6 !bg-[#414548] mr-2.5 border-[#414548] text-white"
>
<template #icon>
<SvgIcon name="delete" class="text-lg mr-2" />
</template>
清除</a-button
>
<a-button type="primary" class="rounded-4px h-9.5 px-6">
<template #icon>
<SvgIcon name="cloud-upload" class="text-lg mr-2" />
</template>
上传</a-button
>
<div class="ml-2">
<span class="text-[#EC4B4B] text-base">*</span>
<span class="opacity-40 text-xs">
可支持的格式.doc.docx.pdf小于20M
</span>
</div>
</div>
<div>
<a-button
@click="handleSubmit"
type="primary"
class="rounded-4px h-9.5 px-6"
>
<template #icon>
<SvgIcon name="generated" class="text-sm mr-2" />
</template>
生成</a-button
>
</div>
</div>
</div>
</div>
</div>
</LayoutContent>
</template>
<script setup>
import { Layout } from 'ant-design-vue'
import MessageGroup from './message-group.vue'
import { ref, computed, onMounted, nextTick, onBeforeMount,getCurrentInstance } from 'vue'
import ScrollContainer from '@/components/ScrollContainer/index.vue'
import http from '@/io/request'
import { v4 as uuidv4 } from 'uuid'
import { useRoute } from 'vue-router'
import { useChat } from '@/stores'
const { proxy } = getCurrentInstance();
let controller = new AbortController()
const inputRef = ref(null)
const LayoutContent = Layout.Content
const route = useRoute()
const { uuid } = route.params
const chatStore = useChat()
const scrollRef = ref(null)
const loading = ref(false)
const inputValue = ref('')
const currentChart = computed(() => chatStore.getCurrentChat)
const conversationList = computed(() => {
return chatStore.getCurrentChat.filter((item) => {
const key = Object.keys(item)[0]
const value = item[key]
return !value.inversion
})
})
const conversationUserList = computed(() => {
return chatStore.getCurrentChat.filter((item) => {
const key = Object.keys(item)[0]
const value = item[key]
return value.inversion
})
})
const dataSources = computed(() => {
const arr = chatStore.getCurrentChat
return groupDataByParentId(arr)
})
function groupDataByParentId(data) {
return data.reduce((groupedData, item) => {
const itemId = Object.keys(item)[0]
const itemData = item[itemId]
if (itemData.parent_id === '') {
groupedData[itemId] = []
} else {
const parentItemId = itemData.parent_id
groupedData[parentItemId] = groupedData[parentItemId] || []
groupedData[parentItemId].push(itemData)
}
return groupedData
}, {})
}
function handleRefresh() {
if (currentChart.value.length && !loading.value) onConversation('variant')
}
function handleStop() {
if (loading.value) {
controller.abort()
loading.value = false
}
}
function handleClear() {
inputValue.value = ''
}
function handleSubmit() {
onConversation()
}
async function onConversation(action = 'next') {
let message = inputValue.value
if (loading.value) return
if ((!message || message.trim() === '') && action == 'next') return
controller = new AbortController()
const params = {
action: action,
conversation_id: chatStore.active,
message: {
text: message,
id: uuidv4(),
},
}
if (action == 'next') {
const lastContext =
conversationList.value[conversationList.value.length - 1]
params.parent_message_id = uuidv4()
if (lastContext) {
params.parent_message_id = lastContext[Object.keys(lastContext)[0]]?.id
}
chatStore.addChatByUuid(chatStore.active, {
text: message,
inversion: true,
parent_id: params.parent_message_id || '',
id: params.message.id,
})
scrollToBottom()
chatStore.addChatByUuid(chatStore.active, {
text: '⋯',
id: uuidv4(),
parent_id: params.message.id,
inversion: false,
})
}
if (action == 'variant') {
const lastUserContext =
conversationUserList.value[conversationUserList.value.length - 1]
if (lastUserContext) {
const obj = lastUserContext[Object.keys(lastUserContext)[0]]
params.message.text = obj.text
params.message.id = obj.id
params.parent_message_id = obj.id
}
chatStore.addChatByUuid(chatStore.active, {
text: '⋯',
id: uuidv4(),
parent_id: params.parent_message_id,
inversion: false,
})
}
loading.value = true
inputValue.value = ''
scrollToBottom()
let tempMessage_id = null
try {
const fetchChatAPIOnce = async () => {
await http.post('/api/v1/conversation', params, {
signal: controller.signal,
requestBaseUrl: 'chat',
onDownloadProgress: async ({ event }) => {
const xhr = event.target
const { responseText } = xhr
if (xhr.status == 200) {
const arr = parseEventMessages(responseText)
const { conversation_id, message_id } = arr[0]
tempMessage_id = message_id
const msg = arr.reduce((acc, item) => {
return acc + item.text
}, '')
chatStore.updateChatByUuid(
conversation_id,
chatStore.getCurrentChat.length - 1,
{
[message_id]: {
conversation_id: conversation_id,
id: message_id,
text: msg,
inversion: false,
loading: true,
parent_id: params.message.id || '',
},
}
)
scrollToBottomIfAtBottom()
}
},
})
chatStore.updateChatSome(chatStore.getCurrentChat.length - 1, {
loading: false,
})
await chatStore.getChat(chatStore.active)
}
await fetchChatAPIOnce()
} catch (error) {
if (error.message === 'canceled') {
chatStore.updateChatSome(chatStore.getCurrentChat.length - 1, {
loading: false,
})
return
}
const errorMessage = error?.errmsg ?? '好像出错了,请稍后再试。'
chatStore.updateChatSome(chatStore.getCurrentChat.length - 1, {
loading: false,
text: errorMessage,
})
} finally {
loading.value = false
}
}
function parseEventMessages(dataString) {
const lines = dataString.trim().split('\n')
const eventMessages = []
let currentEvent = {}
for (const line of lines) {
if (line.startsWith('event:message')) {
if (Object.keys(currentEvent).length > 0) {
eventMessages.push(currentEvent)
currentEvent = {}
}
} else if (line.startsWith('data:')) {
const jsonData = line.substring('data:'.length)
currentEvent = JSON.parse(jsonData)
}
}
if (Object.keys(currentEvent).length > 0) {
eventMessages.push(currentEvent)
}
return eventMessages
}
function handleEnter(event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
handleSubmit()
}
}
function handleDataChange(data) {
inputValue.value = data
}
onMounted(async () => {
proxy.$mitt.on('temp-copy', handleDataChange)
if (inputRef.value) {
inputRef.value.focus()
}
await chatStore.getChat(uuid)
scrollToBottom()
})
function scrollToBottom() {
nextTick()
scrollRef.value.scrollBottom()
}
function scrollToBottomIfAtBottom() {
nextTick()
scrollRef.value.scrollToBottomIfAtBottom()
}
</script>
<style lang="scss" scoped>
.scroll-smooth {
scroll-behavior: smooth;
}
</style>

View File

@ -0,0 +1,47 @@
<template>
<div :class="wrapClass">
<template v-if="file">
<div class="p-12px flex items-center text-white">
<SvgIcon name="文件" class="text-[#FEE77B] w-5 h-5" />
<div class="ml-12px text-22px">{{ file.name }}</div>
</div>
<div
class="bg-[#27292B] flex items-center px-12px py-12px justify-between text-22px"
>
<div>.{{ file.extension }}</div>
<div>{{ kbToMbWithDecimal(file.size) }} M</div>
</div>
</template>
<template v-else>
<div class="p-12px flex items-center text-white">
<SvgIcon name="删除文件夹" class=" text-gray-400 w-5 h-5" />
<div class="ml-12px text-22px">文件已失效</div>
</div>
</template>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
file: {
type: Object,
},
})
function kbToMbWithDecimal(bytes) {
const mb = bytes / (1024 * 1024)
return mb.toFixed(1)
}
const wrapClass = computed(() => {
return [
'text-wrap',
'min-w-[80px]',
'rounded-6px',
'overflow-hidden',
'bg-[#d2f9d1]',
'dark:bg-[#414548]',
'message-request',
]
})
</script>

View File

@ -1,6 +1,8 @@
<template>
<div class="mb-37px">
<Message
:type="currentMsg.type"
:file="currentMsg.fileInfo"
:text="currentMsg.text"
:loading="currentMsg.loading"
:inversion="currentMsg.inversion"

View File

@ -5,7 +5,7 @@
:class="[{ 'flex-row-reverse': inversion }]"
>
<div
class="flex items-center justify-center flex-shrink-0 h-50px overflow-hidden rounded-full basis-50px mt-4px"
class="flex items-center justify-center flex-shrink-0 h-50px overflow-hidden rounded-full basis-50px mt-0px"
:class="[inversion ? 'ml-19px' : 'mr-19px']"
>
<AvatarComponent />
@ -20,11 +20,13 @@
>
<TextComponent
ref="textRef"
v-if="type==='text' || !inversion"
:inversion="inversion"
:text="text"
:loading="loading"
:as-raw-text="asRawText"
/>
<FileComponent :file="file" v-else/>
</div>
</div>
</div>
@ -32,8 +34,17 @@
<script setup>
import TextComponent from './text.vue'
import AvatarComponent from './avatar.vue'
import FileComponent from './file.vue'
import { ref } from 'vue'
const props = defineProps({
type: {
type: String,
default: 'text',
},
file: {
type: Object,
default: () => {},
},
text: {
type: String,
default: '',

View File

@ -11,15 +11,15 @@
></SvgIcon>
<SvgIcon class="text-white text-40px mb-46px" name="dh"></SvgIcon>
<div class="flex-1"></div>
<SvgIcon class="text-white text-40px mb-46px" name="人工客服"></SvgIcon>
<SvgIcon class="text-white text-40px mb-46px" name="疑问"></SvgIcon>
<!-- <SvgIcon class="text-white text-40px mb-46px" name="人工客服"></SvgIcon>
<SvgIcon class="text-white text-40px mb-46px" name="疑问"></SvgIcon> -->
</div>
<van-popup v-model:show="show" position="left" class="slider-class">
<div class="w-479px h-full flex flex-col">
<div class="w-479px h-full flex flex-col overflow-hidden">
<div class="h-100px"></div>
<div
class="bg-gradient-to-b to-[#101011] from-[#414548] flex-1 border-t-7px border-[#3662FE] flex flex-col items-center"
class="bg-gradient-to-b to-[#101011] from-[#414548] flex-1 border-t-7px border-[#3662FE] flex flex-col items-center overflow-y-auto"
>
<SilderBtn @click="openTemplateModal">
<template #icon>
@ -76,7 +76,7 @@
:data="item"
/>
</div>
<SilderBtn>
<!-- <SilderBtn>
<template #icon>
<SvgIcon
class="text-white text-35px transform rotate-45"
@ -93,7 +93,7 @@
></SvgIcon>
</template>
<div>帮助</div>
</SilderBtn>
</SilderBtn> -->
</div>
</div>
</van-popup>

View File

@ -0,0 +1,31 @@
<template>
<div class="inline-block leading-1">
<input
type="file"
ref="fileInput"
class="hidden"
@change="handleFileChange"
/>
<div class="inline-block !leading-0" @click="openFileInput">
<slot>Upload File</slot>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const fileInput = ref(null)
const emits = defineEmits(['change'])
function openFileInput() {
fileInput.value.value = null;
fileInput.value.click()
}
function handleFileChange(event) {
const file = event.target.files[0]
if (file) {
emits('change', file)
}
}
</script>

View File

@ -53,20 +53,22 @@
</template>
清除</van-button
>
<van-button
type="primary"
class="!rounded-2px !h-53px !mr-14px border-[#414548] text-white !border-none text-22px"
>
<template #icon>
<SvgIcon name="cloud-upload" class="text-25px mr-13px" />
</template>
上传</van-button
>
<UploadComp @change="handleFileChange">
<van-button
type="primary"
class="!rounded-2px !h-53px !mr-14px border-[#414548] text-white !border-none !text-22px !leading-53px"
>
<template #icon>
<SvgIcon name="cloud-upload" class="text-25px mr-13px" />
</template>
上传</van-button
>
</UploadComp>
<div class="flex-1"></div>
<van-button
@click="handleSubmit"
type="primary"
class="!rounded-2px !h-53px border-[#414548] text-white !border-none text-22px"
class="!rounded-2px !h-53px border-[#414548] text-white !border-none !text-22px"
>
<template #icon>
<SvgIcon name="generated" class="text-25px mr-13px" />
@ -94,6 +96,20 @@ import http from '@/io/request'
import { v4 as uuidv4 } from 'uuid'
import { useRoute } from 'vue-router'
import { useChat } from '@/stores'
import UploadComp from './components/upload-comp.vue'
import extractTextFromPDF from '@/utils/pdfUtils.js'
import extractDocxText from '@/utils/docxUtils.js'
import { getFileInfo } from '@/utils/fileUtils.js'
import { showToast } from 'vant'
import { useAuthModal } from '@/stores/authModal'
import { useUserInfo } from '@/stores/userInfo'
const userInfo = useUserInfo()
const isLogin = computed(() => {
return !!userInfo.userData?.id ?? false
})
const authModal = useAuthModal()
const { proxy } = getCurrentInstance()
@ -113,8 +129,67 @@ const loading = ref(false)
const inputValue = ref('')
const fileLoading = ref(false)
const currentChart = computed(() => chatStore.getCurrentChat)
async function handleFileChange(file) {
if (!isLogin.value) {
authModal.setAuthModalType('login')
authModal.showAuthModal()
return
}
if (!userInfo.userData?.equity?.pdf) return showToast('上传文件权限未开通')
if (fileLoading.value) return
fileLoading.value = true
try {
const valid = validateFile(file)
if (!valid) return
const info = getFileInfo(file)
let result = ''
if (info.extension === 'docx' || info.extension === 'doc') {
result = await extractDocxText(file)
} else if (info.extension === 'pdf') {
result = await extractTextFromPDF(file)
}
if (!result || result.trim() == '') return showToast('文件内容为空')
onConversation('next', info, result)
} catch (error) {
showToast(error.message)
} finally {
fileLoading.value = false
}
}
function validateFile(file) {
const allowedExtensions = ['doc', 'docx', 'pdf']
const maxFileSize = 20 * 1024 * 1024 // 20MB
const fileExtension = getFileExtension(file.name)
const fileSize = file.size
if (fileSize === 0) {
throw new Error('文件内容为空')
}
if (!allowedExtensions.includes(fileExtension)) {
throw new Error('文件格式无效')
}
if (fileSize > maxFileSize) {
throw new Error('文件大小超出限制')
}
return true
}
function getFileExtension(fileName) {
const parts = fileName.split('.')
return parts[parts.length - 1].toLowerCase()
}
const conversationList = computed(() => {
return chatStore.getCurrentChat.filter((item) => {
const key = Object.keys(item)[0]
@ -172,8 +247,8 @@ function handleSubmit() {
onConversation()
}
async function onConversation(action = 'next') {
let message = inputValue.value
async function onConversation(action = 'next', file, fileText) {
let message = inputValue.value || fileText
if (loading.value) return
if ((!message || message.trim() === '') && action == 'next') return
@ -181,8 +256,10 @@ async function onConversation(action = 'next') {
action: action,
conversation_id: chatStore.active,
message: {
text: message,
text: message?.substring(0, 4000),
id: uuidv4(),
type: (file?.extension === 'doc' ? 'docx' : file?.extension) || 'text',
fileInfo: file,
},
}
if (action == 'next') {
@ -198,6 +275,8 @@ async function onConversation(action = 'next') {
inversion: true,
parent_id: params.parent_message_id || '',
id: params.message.id,
type: params.message.type,
fileInfo: file,
})
scrollToBottom()
chatStore.addChatByUuid(chatStore.active, {
@ -212,7 +291,7 @@ async function onConversation(action = 'next') {
conversationUserList.value[conversationUserList.value.length - 1]
if (lastUserContext) {
const obj = lastUserContext[Object.keys(lastUserContext)[0]]
params.message.text = obj.text
params.message.text = obj.text?.substring(0, 4000)
params.message.id = obj.id
params.parent_message_id = obj.id
}

View File

@ -0,0 +1,277 @@
<template>
<van-popup
v-model:show="isShow"
position="center"
closeable
:style="{
background: '#161718',
borderRadius: '6px',
width: '90%',
}"
>
<div class="text-white">
<!-- <div class="bg-[#111F63] w-75 p-6 flex-1 flex-none flex flex-col">
<div class="text-2xl font-bold">需求留言问卷</div>
<div class="mt-9 text-base font-bold flex-1">
<p>感谢您对海兔AI的关注</p>
<p>请留下您的信息</p>
<p>我们会尽快联系您</p>
</div>
<div class="w-43 h-43 border"></div>
<div class="mt-6 text-base">了解更多一键扫码咨询</div>
</div> -->
<div class="flex justify-between pt-40px">
<div class="text-22px text-white text-opacity-40">
<p class="text-30px font-bold text-white">感谢您对海兔AI的关注</p>
<p class="mt-20px">请留下您的信息</p>
<p class="mt-10px">我们会尽快联系您</p>
</div>
<!-- <div>
<div class="w-200px h-200px border"></div>
</div> -->
</div>
<div class="p-17px flex-1 text-white mt-20px">
<div class="text-27px mb-22px">
<div class="required">需求类型</div>
<div class="flex items-center mt-14px">
<van-radio-group
class="cu-radio"
shape="dot"
direction="horizontal"
v-model="form.type_id"
>
<van-radio
v-for="(item, i) in typeList"
:key="i"
:name="item.id"
>{{ item.name }}</van-radio
>
</van-radio-group>
</div>
</div>
<div class="text-27px mb-22px">
<div class="required">性别</div>
<div class="flex items-center mt-14px">
<van-radio-group
class="cu-radio"
shape="dot"
direction="horizontal"
v-model="form.sex"
>
<van-radio name="1"></van-radio>
<van-radio name="0"></van-radio>
</van-radio-group>
</div>
</div>
<div class="text-27px mb-22px">
<div class="required">姓名</div>
<div class="flex items-center mt-14px">
<van-field
v-model="form.name"
placeholder="请输入姓名"
class="cu-field-input !text-white"
></van-field>
</div>
</div>
<div class="text-27px mb-22px">
<div class="required">联系方式</div>
<div class="flex items-center mt-14px">
<van-field
v-model="form.phone"
placeholder="请输入手机号"
class="cu-field-input !text-white"
></van-field>
</div>
</div>
<div class="text-27px mb-22px">
<div class="">公司名称</div>
<div class="flex items-center mt-14px">
<van-field
v-model="form.company"
placeholder="请输入公司名称"
class="cu-field-input !text-white"
></van-field>
</div>
</div>
<div class="text-27px mb-22px">
<div class="">企业邮箱</div>
<div class="flex items-center mt-14px">
<van-field
v-model="form.email"
placeholder="请输入企业邮箱"
class="cu-field-input !text-white"
></van-field>
</div>
</div>
<div class="text-27px mb-22px">
<div class="">职位</div>
<div class="flex items-center mt-14px">
<van-field
v-model="form.job"
placeholder="请输入职位"
class="cu-field-input !text-white"
></van-field>
</div>
</div>
<div class="text-27px mb-22px">
<div class="">需求描述</div>
<div class="flex items-center mt-14px">
<van-field
v-model="form.content"
type="textarea"
placeholder="请输入需求描述"
class="cu-field-input !text-white"
></van-field>
</div>
</div>
<div class="text-right flex mt-30px">
<van-button
@click="handleSumbit"
type="primary"
class="!rounded-2px !h-66px w-full border-[#414548] text-white !border-none !text-27px"
>
确认
</van-button>
</div>
</div>
</div>
</van-popup>
</template>
<script setup>
import { ref, reactive, toRaw, onBeforeMount, computed, watch } from 'vue'
import { showToast } from 'vant'
import http from '@/io/request'
const emit = defineEmits(['update:value'])
const telReg = /^1[3-9]\d{9}$/
const emailReg = /^[\w\.-]+@[a-zA-Z\d\.-]+\.[a-zA-Z]{2,}$/
const props = defineProps({
value: {
type: Boolean,
default: false,
},
})
const isShow = computed({
get() {
return props.value
},
set(val) {
emit('update:value', val)
},
})
const defaultForm = {
type_id: '',
sex: 1,
name: '',
phone: '',
company: '',
email: '',
job: '',
content: '',
}
const typeList = ref([])
const loading = ref(false)
const form = reactive(Object.assign({}, defaultForm))
const getTypes = async () => {
const res = await http.get('/api/keywords?type_key=feedback')
typeList.value = res
if (res.length && !form.type_id) {
form.type_id = res[0].id
}
}
function handleSumbit() {
const _form = toRaw(form)
if (!_form.type_id) return showToast('请选择需求类型')
if (!_form.name) return showToast('请输入姓名')
if (!_form.phone) return showToast('请输入手机号')
if (!telReg.test(_form.phone)) return showToast('请输入正确的手机号')
if (_form.email && !emailReg.test(_form.email))
return showToast('请输入正确的邮箱')
if (loading.value) return
loading.value = true
http
.post('/api/feedback', _form)
.then(() => {
showToast('提交成功')
form.value = Object.assign({}, defaultForm)
isShow.value = false
loading.value = false
})
.catch(() => {
loading.value = false
})
}
watch(
() => isShow.value,
(val) => {
if (!val) {
form.value = Object.assign({}, defaultForm)
}
}
)
onBeforeMount(() => {
getTypes()
})
</script>
<style lang="scss">
.required {
position: relative;
&::before {
position: absolute;
display: inline-block;
color: #ff4d4f;
font-size: 28px;
line-height: 1;
left: -20px;
top: 50%;
transform: translateY(-50%);
content: '*';
}
}
.cu-field-input {
background: #161718;
border: 1px solid #414548;
color: #fff !important;
font-size: 22px;
padding: 0 19px;
display: flex;
align-items: center;
&::placeholder {
color: #fff;
opacity: 0.4;
}
input {
color: #fff;
height: 72px;
}
textarea {
color: #fff;
}
}
.cu-radio {
.van-radio__icon--dot {
height: 24px;
width: 24px;
border-radius: 4px;
&__icon {
width: 80%;
height: 80%;
border-radius: 4px;
}
}
.van-radio__label {
font-size: 22px;
color: white;
margin-left: 8px !important;
}
}
</style>

View File

@ -27,7 +27,10 @@
一些相关的描述文字一些相关的描述文字一些相关的描述文字一些相关的描述文字一些相关的描述文字一些相关的描述文字一些相关的描述文字一些相关的描述文字一些相关的描述文字
</div>
<div class="mt-43px">
<van-button type="primary" class="!rounded-full !h-56px !text-27px"
<van-button
@click="isShow = true"
type="primary"
class="!rounded-full !h-56px !text-27px"
>立即体验</van-button
>
</div>
@ -63,7 +66,10 @@
一些相关的描述文字一些相关的描述文字一些相关的描述文字一些相关的描述文字一些相关的描述文字一些相关的描述文字一些相关的描述文字一些相关的描述文字一些相关的描述文字
</div>
<div class="mt-43px">
<van-button type="primary" class="!rounded-full !h-56px !text-27px"
<van-button
@click="isShow = true"
type="primary"
class="!rounded-full !h-56px !text-27px"
>立即体验</van-button
>
</div>
@ -210,6 +216,7 @@
</div>
<div class="mt-54px text-center pb-192px">
<van-button
@click="isShow = true"
type="primary"
class="!rounded-full w-300px !h-68px !text-23px"
>联系我们</van-button
@ -233,14 +240,18 @@
</div>
</div>
</van-popup>
<Contacts v-model:value="isShow"></Contacts>
</div>
</template>
<script setup>
import Contacts from './components/contacts.vue'
import TagItem from './components/tag-item.vue'
import videoBg2 from '@/assets/images/stream/l2.png'
import videoBg3 from '@/assets/images/stream/l3.png'
import videoBg4 from '@/assets/images/stream/l4.png'
import { ref } from 'vue'
const isShow = ref(false)
const videoShow = ref(false)
const video = ref(null)
const videos = ref([

View File

@ -1,16 +1,18 @@
<template>
<Layout path="/communication">
<div class="h-full flex flex-col justify-center text-white">
<TitleComp title="AI传播" :src="TitleSrc"></TitleComp>
<div class="mt-34px text-27px font-bold">AI虚拟人直播</div>
<div class="text-22px mt-36px text-[#C2C5CA] leading-29px">
在AI助理板块用户可以上传文件给AI翻阅并帮助您解决问题您可以自己输入关键字或者使用自带的prompt模板在AI助理板块用户可以上传文件给AI翻阅并帮助您解决问题您可以自己输入关键字或者使用自带的prompt模
</div>
<div class="mt-139px">
<img
class="w-670px mx-auto inline-block"
src="@/assets/images/ai_stream_01.png"
/>
<div class="min-h-900px">
<TitleComp title="AI传播" :src="TitleSrc"></TitleComp>
<div class="mt-34px text-27px font-bold">AI虚拟人直播</div>
<div class="text-22px mt-36px text-[#C2C5CA] leading-29px">
在AI助理板块用户可以上传文件给AI翻阅并帮助您解决问题您可以自己输入关键字或者使用自带的prompt模板在AI助理板块用户可以上传文件给AI翻阅并帮助您解决问题您可以自己输入关键字或者使用自带的prompt模
</div>
<div class="mt-139px">
<img
class="w-670px mx-auto inline-block"
src="@/assets/images/ai_stream_01.png"
/>
</div>
</div>
</div>
</Layout>

View File

@ -10,6 +10,7 @@
立即体验
</div>
</div>
<!-- <div class="h-200px w-full"></div> -->
</div>
</template>
<script setup>

View File

@ -31,6 +31,9 @@
>
<component :is="item.component" />
</swiper-slide>
<swiper-slide>
<Footer></Footer>
</swiper-slide>
</swiper>
</div>
</div>
@ -59,6 +62,7 @@ import Assistant from '@/views/assistant/index.vue'
import Business from '@/views/business/index.vue'
import Stream from '@/views/communication/index.vue'
import Training from '@/views/training/index.vue'
import Footer from '@/layouts/Footer.vue'
import { Swiper, SwiperSlide } from 'swiper/vue'
import { Mousewheel, Pagination } from 'swiper/modules'

View File

@ -1,17 +1,19 @@
<template>
<Layout path="/training">
<div class="h-full flex flex-col justify-center text-white">
<TitleComp title="AI培训" :src="TitleSrc"></TitleComp>
<div class="mt-38px text-27px font-bold">AI培训课程</div>
<div class="text-22px mt-36px text-[#C2C5CA] leading-29px">
在AI助理板块用户可以上传文件给AI翻阅并帮助您解决问题您可以自己输入关键字或者使用自带的prompt模板在AI助理板块用户可以上传文件给AI翻阅并帮助您解决问题您可以自己输入关键字或者使用自带的prompt模板在AI
</div>
<div class="min-h-900px">
<TitleComp title="AI培训" :src="TitleSrc"></TitleComp>
<div class="mt-38px text-27px font-bold">AI培训课程</div>
<div class="text-22px mt-36px text-[#C2C5CA] leading-29px">
在AI助理板块用户可以上传文件给AI翻阅并帮助您解决问题您可以自己输入关键字或者使用自带的prompt模板在AI助理板块用户可以上传文件给AI翻阅并帮助您解决问题您可以自己输入关键字或者使用自带的prompt模板在AI
</div>
<div class="mt-69px">
<img
class="w-670px mx-auto inline-block"
src="@/assets/images/ai_stream_01.png"
/>
<div class="mt-69px">
<img
class="w-670px mx-auto inline-block"
src="@/assets/images/cultivate_01.png"
/>
</div>
</div>
</div>
</Layout>

772
yarn.lock

File diff suppressed because it is too large Load Diff