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

chore: 优化AI相关内容

上级 6653e230
...@@ -17,13 +17,14 @@ ...@@ -17,13 +17,14 @@
"@vue-flow/core": "^1.39.0", "@vue-flow/core": "^1.39.0",
"@vueuse/components": "^13.3.0", "@vueuse/components": "^13.3.0",
"@vueuse/core": "^13.3.0", "@vueuse/core": "^13.3.0",
"axios": "^1.6.8", "axios": "^1.9.0",
"blueimp-md5": "^2.19.0", "blueimp-md5": "^2.19.0",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"echarts": "^5.5.0", "echarts": "^5.5.0",
"echarts-wordcloud": "^2.1.0", "echarts-wordcloud": "^2.1.0",
"element-plus": "^2.8.7", "element-plus": "^2.8.7",
"eventsource-parser": "^3.0.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"jspdf": "^2.5.1", "jspdf": "^2.5.1",
...@@ -3579,9 +3580,9 @@ ...@@ -3579,9 +3580,9 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.7.7", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
...@@ -5453,6 +5454,15 @@ ...@@ -5453,6 +5454,15 @@
"node": ">=0.8.x" "node": ">=0.8.x"
} }
}, },
"node_modules/eventsource-parser": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.2.tgz",
"integrity": "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/evp_bytestokey": { "node_modules/evp_bytestokey": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
......
...@@ -24,13 +24,14 @@ ...@@ -24,13 +24,14 @@
"@vue-flow/core": "^1.39.0", "@vue-flow/core": "^1.39.0",
"@vueuse/components": "^13.3.0", "@vueuse/components": "^13.3.0",
"@vueuse/core": "^13.3.0", "@vueuse/core": "^13.3.0",
"axios": "^1.6.8", "axios": "^1.9.0",
"blueimp-md5": "^2.19.0", "blueimp-md5": "^2.19.0",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"echarts": "^5.5.0", "echarts": "^5.5.0",
"echarts-wordcloud": "^2.1.0", "echarts-wordcloud": "^2.1.0",
"element-plus": "^2.8.7", "element-plus": "^2.8.7",
"eventsource-parser": "^3.0.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"jspdf": "^2.5.1", "jspdf": "^2.5.1",
......
...@@ -96,3 +96,26 @@ textarea:focus { ...@@ -96,3 +96,26 @@ textarea:focus {
margin: -8px -15px; margin: -8px -15px;
padding: 8px 15px; padding: 8px 15px;
} }
.markdown-body {
line-height: 1.6;
}
.markdown-body h1 {
font-size: 24px;
}
.markdown-body h2 {
font-size: 20px;
}
.markdown-body h3 {
font-size: 18px;
}
.markdown-body table {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
}
.markdown-body table th,
.markdown-body table td {
padding: 8px 16px;
border: 1px solid #e6e6e6;
}
...@@ -4,50 +4,58 @@ export function useChat() { ...@@ -4,50 +4,58 @@ export function useChat() {
const messages = ref([]) const messages = ref([])
const isLoading = ref(false) const isLoading = ref(false)
async function post(message, isReplace = true) { function post(message, isReplace = true) {
isLoading.value = true isLoading.value = true
await fetchEventSource('/api/lab/v1/experiment/qwen/chat', { return new Promise((resolve, reject) => {
method: 'POST', fetchEventSource('/api/lab/v1/experiment/qwen/chat', {
headers: { 'Content-Type': 'application/json' }, method: 'POST',
body: JSON.stringify({ model: 'qwen-long', messages: [message] }), headers: { 'Content-Type': 'application/json' },
async onopen(response) { body: JSON.stringify({ model: 'qwen-long', messages: [message] }),
if (response.ok) { async onopen(response) {
return response if (response.ok) {
} else { return response
throw response } else {
} isLoading.value = false
}, reject(response)
onmessage(res) { throw response
console.log(res.data)
if (res.data === '[DONE]') {
isLoading.value = false
}
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) onmessage(res) {
let content = message?.choices[0]?.delta.content || '' console.log(res.data)
if (isReplace) { if (res.data === '[DONE]') {
content = content.replaceAll('\n', '<br/>') isLoading.value = false
resolve(messages.value.at(-1))
return
} }
if (messageIndex === -1) { try {
messages.value.push({ id, role: 'assistant', content }) const message = JSON.parse(res.data)
} else { if (message.error) {
messages.value[messageIndex].content = messages.value[messageIndex].content + content 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 || ''
if (isReplace) {
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
}
} catch (error) {
console.log(error)
isLoading.value = false
reject(error)
} }
} catch (error) { },
console.log(error) onerror(err) {
isLoading.value = false isLoading.value = false
} reject(err)
}, throw err
onerror(err) { },
isLoading.value = false })
throw err
},
}) })
} }
......
...@@ -37,5 +37,5 @@ export function getUserTags(params: { sso_id: string; limit: number }) { ...@@ -37,5 +37,5 @@ export function getUserTags(params: { sso_id: string; limit: number }) {
// AI分析与总结 // AI分析与总结
export function getAISummary() { export function getAISummary() {
return httpRequest.get('/api/lab/v1/experiment/member/ai-all-person') return httpRequest.get('/api/lab/v1/experiment/member/ai-all-person', { adapter: 'fetch', responseType: 'stream' })
} }
<script setup> <script setup>
import { getAISummary } from '../api' import { getAISummary } from '../api'
import { aiStreamParse } from '@/utils/parse'
import VueMarkdown from 'vue-markdown-render'
const content = ref('') const content = ref('')
const isLoading = ref(false) const isLoading = ref(false)
async function fetchAI() { async function fetchAI() {
isLoading.value = true isLoading.value = true
const res = await getAISummary() const stream = await getAISummary()
content.value = res.data.result aiStreamParse(stream, (json, messageContent) => {
content.value += messageContent
})
isLoading.value = false isLoading.value = false
} }
onMounted(() => { onMounted(() => {
...@@ -17,7 +21,7 @@ onMounted(() => { ...@@ -17,7 +21,7 @@ onMounted(() => {
<template> <template>
<el-dialog title="AI用户整体画像分析与建议"> <el-dialog title="AI用户整体画像分析与建议">
<div v-loading="isLoading"> <div v-loading="isLoading">
<el-input type="textarea" :rows="15" :value="content"></el-input> <VueMarkdown :source="content" class="markdown-body" />
</div> </div>
<template #footer> <template #footer>
<el-row justify="center"> <el-row justify="center">
......
import httpRequest from '@/utils/axios' import httpRequest from '@/utils/axios'
import type { LabelTypeListRequest, LabelTypeCreateRequest, LabelTypeUpdateRequest, LabelListRequest, LabelCreateRequest, LabelUpdateRequest } from './types' import type {
LabelTypeListRequest,
LabelTypeCreateRequest,
LabelTypeUpdateRequest,
LabelListRequest,
LabelCreateRequest,
LabelUpdateRequest,
} from './types'
// 获取标签类型列表 // 获取标签类型列表
export function getLabelTypeList(params?: LabelTypeListRequest) { export function getLabelTypeList(params?: LabelTypeListRequest) {
...@@ -73,5 +80,5 @@ export function getLabelMembers(params: { tag_id: string }) { ...@@ -73,5 +80,5 @@ export function getLabelMembers(params: { tag_id: string }) {
// AI分析与总结 // AI分析与总结
export function getAISummary() { export function getAISummary() {
return httpRequest.get('/api/lab/v1/experiment/member/ai-tag') return httpRequest.get('/api/lab/v1/experiment/member/ai-tag', { responseType: 'stream', adapter: 'fetch' })
} }
<script setup> <script setup>
import { getAISummary } from '../api' import { getAISummary } from '../api'
import { aiStreamParse } from '@/utils/parse'
import VueMarkdown from 'vue-markdown-render'
const content = ref('') const content = ref('')
const isLoading = ref(false) const isLoading = ref(false)
async function fetchAI() { async function fetchAI() {
isLoading.value = true isLoading.value = true
const res = await getAISummary() const stream = await getAISummary()
content.value = res.data.result aiStreamParse(stream, (json, messageContent) => {
content.value += messageContent
})
isLoading.value = false isLoading.value = false
} }
onMounted(() => { onMounted(() => {
...@@ -15,9 +19,9 @@ onMounted(() => { ...@@ -15,9 +19,9 @@ onMounted(() => {
</script> </script>
<template> <template>
<el-dialog title="AI用户整体画像分析与建议"> <el-dialog title="AI标签建议">
<div v-loading="isLoading"> <div v-loading="isLoading">
<el-input type="textarea" :rows="15" :value="content"></el-input> <VueMarkdown :source="content" class="markdown-body" />
</div> </div>
<template #footer> <template #footer>
<el-row justify="center"> <el-row justify="center">
......
...@@ -3,19 +3,11 @@ import { useChat } from '@/composables/useChat' ...@@ -3,19 +3,11 @@ import { useChat } from '@/composables/useChat'
import { getAttrList } from '../api' import { getAttrList } from '../api'
const form = inject('form') const form = inject('form')
const { isLoading, post, messages } = useChat() const { post } = useChat()
watch(
messages,
() => {
const lastMessage = messages.value[messages.value.length - 1]
if (lastMessage) {
form.shopping_guide_short_title = lastMessage.content.replace('推荐导购短标题:', '')
}
},
{ deep: true }
)
const descLoading = ref(false)
function handleAIGenerate() { function handleAIGenerate() {
descLoading.value = true
post({ post({
role: 'user', role: 'user',
content: `请根据以下内容,给出1个用于抖音电商使用的推荐的“导购短标题”内容:${form.title} content: `请根据以下内容,给出1个用于抖音电商使用的推荐的“导购短标题”内容:${form.title}
...@@ -25,6 +17,24 @@ function handleAIGenerate() { ...@@ -25,6 +17,24 @@ function handleAIGenerate() {
3. 标题要具有吸引力,能够激发消费者的购买欲望。 3. 标题要具有吸引力,能够激发消费者的购买欲望。
4. 标题要简洁明了,不要过于冗长或复杂。 4. 标题要简洁明了,不要过于冗长或复杂。
5. 输出结果以“推荐导购短标题:”开始`, 5. 输出结果以“推荐导购短标题:”开始`,
}).then((res) => {
form.shopping_guide_short_title = res.content.replace('推荐导购短标题:', '')
descLoading.value = false
})
}
const titleLoading = ref(false)
function handleAIGenerateTitle() {
titleLoading.value = true
post({
role: 'user',
content: `请优化用于抖音电商使用的“商品标题”内容:${form.title}
要求:
1. 标题要简洁明了,一句话描述商品特点,不要超过10个字。
`,
}).then((res) => {
form.title = res.content
titleLoading.value = false
}) })
} }
...@@ -76,7 +86,13 @@ const unimportanceTotal = computed(() => { ...@@ -76,7 +86,13 @@ const unimportanceTotal = computed(() => {
<div> <div>
<el-form-item label="商品标题" prop="title" :rules="[{ required: true, message: '请输入', trigger: 'blur' }]"> <el-form-item label="商品标题" prop="title" :rules="[{ required: true, message: '请输入', trigger: 'blur' }]">
<div class="form-tips">标题不规范会有可能引起商品下架,影响您的正常销售,请点击学习商品发布规范认真填写</div> <div class="form-tips">标题不规范会有可能引起商品下架,影响您的正常销售,请点击学习商品发布规范认真填写</div>
<el-input placeholder="至少输入8个字(16个字符)以上,30个字(60个字符)以下" size="large" v-model="form.title" /> <div style="display: flex; width: 100%; align-items: center; gap: 20px">
<el-input
placeholder="至少输入8个字(16个字符)以上,30个字(60个字符)以下"
size="large"
v-model="form.title" />
<el-button type="primary" plain @click="handleAIGenerateTitle" :loading="titleLoading">一键智能优化</el-button>
</div>
</el-form-item> </el-form-item>
<el-form-item label="导购短标题" prop="shopping_guide_short_title"> <el-form-item label="导购短标题" prop="shopping_guide_short_title">
<div class="form-tips">短标题可用于商品搜索、首页推荐、物流单等场景,请提炼商品关键信息,客观准确填写</div> <div class="form-tips">短标题可用于商品搜索、首页推荐、物流单等场景,请提炼商品关键信息,客观准确填写</div>
...@@ -86,7 +102,7 @@ const unimportanceTotal = computed(() => { ...@@ -86,7 +102,7 @@ const unimportanceTotal = computed(() => {
size="large" size="large"
v-model="form.shopping_guide_short_title" v-model="form.shopping_guide_short_title"
style="flex: 1" /> style="flex: 1" />
<el-button type="primary" plain @click="handleAIGenerate" :loading="isLoading">一键智能推荐</el-button> <el-button type="primary" plain @click="handleAIGenerate" :loading="descLoading">一键智能推荐</el-button>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item <el-form-item
......
...@@ -102,6 +102,7 @@ const onStatsChange = (stats) => { ...@@ -102,6 +102,7 @@ const onStatsChange = (stats) => {
<template v-else> <template v-else>
<Live <Live
ref="live" ref="live"
:orderCount="orderCount"
:isLocalUpload="isLocalUpload" :isLocalUpload="isLocalUpload"
:onStart="() => reset(duration)" :onStart="() => reset(duration)"
:onStop="stop" :onStop="stop"
...@@ -154,13 +155,13 @@ const onStatsChange = (stats) => { ...@@ -154,13 +155,13 @@ const onStatsChange = (stats) => {
<div class="live-col-box" v-else> <div class="live-col-box" v-else>
<h2 class="h2-title">倒计时</h2> <h2 class="h2-title">倒计时</h2>
<h3 class="live-time">{{ formattedTime }}</h3> <h3 class="live-time">{{ formattedTime }}</h3>
<div class="live-data"> <div class="live-data2">
<dl> <dl>
<dt>观众人数</dt> <dt>观众人数</dt>
<dd>{{ currentStats.viewers }}</dd> <dd>{{ currentStats.viewers }}</dd>
</dl> </dl>
<dl> <dl>
<dt>订单量</dt> <dt>订单量</dt>
<dd>{{ orderCount }}</dd> <dd>{{ orderCount }}</dd>
</dl> </dl>
</div> </div>
...@@ -220,6 +221,25 @@ const onStatsChange = (stats) => { ...@@ -220,6 +221,25 @@ const onStatsChange = (stats) => {
font-weight: bold; font-weight: bold;
} }
} }
.live-data2 {
display: flex;
gap: 10px;
text-align: center;
dl {
flex: 1;
text-align: center;
margin-bottom: 40px;
}
dt {
font-size: 20px;
font-weight: bold;
}
dd {
font-size: 42px;
text-align: center;
color: var(--main-color);
}
}
.live-time { .live-time {
height: 140px; height: 140px;
font-size: 72px; font-size: 72px;
......
...@@ -14,6 +14,7 @@ import { useLiveChat } from '../composables/useLiveChat' ...@@ -14,6 +14,7 @@ import { useLiveChat } from '../composables/useLiveChat'
import { useSpeechTranscriber } from '../composables/useSpeechTranscriber' import { useSpeechTranscriber } from '../composables/useSpeechTranscriber'
const props = defineProps({ const props = defineProps({
orderCount: { type: Number, default: 0 },
isLocalUpload: { type: Boolean, default: false }, isLocalUpload: { type: Boolean, default: false },
onStart: { type: Function, default: () => {} }, onStart: { type: Function, default: () => {} },
onStop: { type: Function, default: () => {} }, onStop: { type: Function, default: () => {} },
...@@ -129,6 +130,7 @@ const handleUpdateRecord = async (params) => { ...@@ -129,6 +130,7 @@ const handleUpdateRecord = async (params) => {
live_duration: duration.value.toString(), live_duration: duration.value.toString(),
live_video_addres: fileUrl.value, live_video_addres: fileUrl.value,
live_info: JSON.stringify({ messages: messages.value, stats }), live_info: JSON.stringify({ messages: messages.value, stats }),
order_count: props.orderCount,
} }
const requestParams = { ...defaultParams, ...params } const requestParams = { ...defaultParams, ...params }
if (!requestParams.live_practice_id) return if (!requestParams.live_practice_id) return
......
...@@ -128,5 +128,9 @@ export function clearMember() { ...@@ -128,5 +128,9 @@ export function clearMember() {
// AI分析与总结 // AI分析与总结
export function getAISummary(params: { member_id: string }) { export function getAISummary(params: { member_id: string }) {
return httpRequest.get('/api/lab/v1/experiment/member/ai-one-person', { params }) return httpRequest.get('/api/lab/v1/experiment/member/ai-one-person', {
params,
responseType: 'stream',
adapter: 'fetch',
})
} }
<script setup> <script setup>
import { getAISummary } from '../api' import { getAISummary } from '../api'
import { aiStreamParse } from '@/utils/parse'
import VueMarkdown from 'vue-markdown-render'
const props = defineProps(['id']) const props = defineProps(['id'])
...@@ -7,8 +9,10 @@ const content = ref('') ...@@ -7,8 +9,10 @@ const content = ref('')
const isLoading = ref(false) const isLoading = ref(false)
async function fetchAI() { async function fetchAI() {
isLoading.value = true isLoading.value = true
const res = await getAISummary({ member_id: props.id }) const stream = await getAISummary({ member_id: props.id })
content.value = res.data.result aiStreamParse(stream, (json, messageContent) => {
content.value += messageContent
})
isLoading.value = false isLoading.value = false
} }
onMounted(() => { onMounted(() => {
...@@ -19,7 +23,7 @@ onMounted(() => { ...@@ -19,7 +23,7 @@ onMounted(() => {
<template> <template>
<el-dialog title="AI用户画像分析与建议"> <el-dialog title="AI用户画像分析与建议">
<div v-loading="isLoading"> <div v-loading="isLoading">
<el-input type="textarea" :rows="15" :value="content"></el-input> <VueMarkdown :source="content" class="markdown-body" />
</div> </div>
<template #footer> <template #footer>
<el-row justify="center"> <el-row justify="center">
......
import { createParser, type EventSourceMessage } from 'eventsource-parser'
export async function parse(stream: ReadableStream<Uint8Array>, onEvent: (event: EventSourceMessage) => void) {
const parser = createParser({ onEvent })
const reader = stream.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
parser.feed(decoder.decode(value, { stream: true }))
}
}
export async function aiStreamParse(
stream: ReadableStream<Uint8Array>,
onMessage: (json: any, content: string) => void
) {
parse(stream, (event) => {
if (event.data === '[DONE]') return
const json = JSON.parse(event.data)
onMessage(json, json.choices[0].delta.content)
})
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论