|
@@ -7,9 +7,11 @@ import userBubbleDark from './image/userBubbleDark.png'
|
|
|
import robotBubbleLight from './image/robotBubbleLight.png'
|
|
import robotBubbleLight from './image/robotBubbleLight.png'
|
|
|
import robotBubbleDark from './image/robotBubbleDark.png'
|
|
import robotBubbleDark from './image/robotBubbleDark.png'
|
|
|
import { useThemeStore } from '@/stores/modules/theme'
|
|
import { useThemeStore } from '@/stores/modules/theme'
|
|
|
|
|
+import { useUserStore } from '@/stores/modules/user'
|
|
|
import MarkdownIt from 'markdown-it'
|
|
import MarkdownIt from 'markdown-it'
|
|
|
import 'github-markdown-css/github-markdown.css'
|
|
import 'github-markdown-css/github-markdown.css'
|
|
|
|
|
|
|
|
|
|
+const userStore = useUserStore()
|
|
|
const md = new MarkdownIt({
|
|
const md = new MarkdownIt({
|
|
|
html: true,
|
|
html: true,
|
|
|
linkify: true,
|
|
linkify: true,
|
|
@@ -18,6 +20,10 @@ const md = new MarkdownIt({
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
const renderedMessage = (content) => {
|
|
const renderedMessage = (content) => {
|
|
|
|
|
+ // console.log('content', content)
|
|
|
|
|
+ if (!content) {
|
|
|
|
|
+ return ''
|
|
|
|
|
+ }
|
|
|
const normalized = content
|
|
const normalized = content
|
|
|
.replace(/\\n/g, '\n') // 粘贴带来的 \\n 处理
|
|
.replace(/\\n/g, '\n') // 粘贴带来的 \\n 处理
|
|
|
.replace(/\r\n/g, '\n') // Windows 换行标准化
|
|
.replace(/\r\n/g, '\n') // Windows 换行标准化
|
|
@@ -45,36 +51,21 @@ const isShowHeaderShadow = ref(false)
|
|
|
|
|
|
|
|
const loadingAnswer = ref(false) // 是否正在加载答案
|
|
const loadingAnswer = ref(false) // 是否正在加载答案
|
|
|
interface MessageItem {
|
|
interface MessageItem {
|
|
|
|
|
+ id?: string // 唯一标识
|
|
|
type: 'robot' | 'user'
|
|
type: 'robot' | 'user'
|
|
|
content: string
|
|
content: string
|
|
|
- feedback?: 'good' | 'noGood' | '' // 反馈结果
|
|
|
|
|
|
|
+ feedback?: 'Cood' | 'Not Good' | '' // 反馈结果
|
|
|
isShowFeedback?: boolean // 是否展示反馈样式
|
|
isShowFeedback?: boolean // 是否展示反馈样式
|
|
|
isAnswer?: boolean // 是否为用户问题的答案,是则才能展示反馈组件
|
|
isAnswer?: boolean // 是否为用户问题的答案,是则才能展示反馈组件
|
|
|
isError?: boolean // 是否为错误消息
|
|
isError?: boolean // 是否为错误消息
|
|
|
isCancel?: boolean // 是否为取消消息
|
|
isCancel?: boolean // 是否为取消消息
|
|
|
|
|
+ html?: string // 渲染后的HTML内容
|
|
|
}
|
|
}
|
|
|
const messages = ref<MessageItem[]>([
|
|
const messages = ref<MessageItem[]>([
|
|
|
{
|
|
{
|
|
|
type: 'robot',
|
|
type: 'robot',
|
|
|
isShowFeedback: false,
|
|
isShowFeedback: false,
|
|
|
content: 'You can click on Frequently Asked Questions above or type your own question'
|
|
content: 'You can click on Frequently Asked Questions above or type your own question'
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- type: 'user',
|
|
|
|
|
- content:
|
|
|
|
|
- 'Hi! I am your Freight Assistant. How can I help you?Of course! Please provide me with the details of your shipment.Of course! Please provide me with the details of your shipment.'
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- type: 'robot',
|
|
|
|
|
- feedback: '',
|
|
|
|
|
- isShowFeedback: false,
|
|
|
|
|
- isAnswer: true,
|
|
|
|
|
- content: 'Can you help me with my shipment?'
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- type: 'user',
|
|
|
|
|
- content:
|
|
|
|
|
- 'Of course! Please provide me with the details of your shipment.Of course! Please provide me with the details of your shipment.'
|
|
|
|
|
}
|
|
}
|
|
|
])
|
|
])
|
|
|
messages.value = JSON.parse(sessionStorage.getItem('AIChat')) || messages.value
|
|
messages.value = JSON.parse(sessionStorage.getItem('AIChat')) || messages.value
|
|
@@ -106,10 +97,75 @@ const progressStatus = {
|
|
|
|
|
|
|
|
const isShowTips = ref(false) // 是否展示提示信息
|
|
const isShowTips = ref(false) // 是否展示提示信息
|
|
|
|
|
|
|
|
|
|
+const parseHtmlString = (data) => {
|
|
|
|
|
+ const lines = data.split('\n')
|
|
|
|
|
+
|
|
|
|
|
+ function streamMarkdown() {
|
|
|
|
|
+ const lastMsg: any = messages.value[messages.value.length - 1]
|
|
|
|
|
+ let index = 0
|
|
|
|
|
+ lastMsg.content = '' // 清空上一个消息的内容
|
|
|
|
|
+ const timer = setInterval(() => {
|
|
|
|
|
+ if (index < lines.length) {
|
|
|
|
|
+ lastMsg.content += lines[index] + '\n'
|
|
|
|
|
+ lastMsg.html = renderedMessage(lastMsg.content) // ✅ 每次整段渲染
|
|
|
|
|
+ index++
|
|
|
|
|
+ scrollToBottom() // 滚动到底部
|
|
|
|
|
+ } else {
|
|
|
|
|
+ clearInterval(timer)
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 150)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ streamMarkdown()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
const progressInterval = ref()
|
|
const progressInterval = ref()
|
|
|
-const handleSend = (question, isPresetQuestion = true) => {
|
|
|
|
|
- if (!question) return
|
|
|
|
|
|
|
+const serial_no = ref()
|
|
|
|
|
+const aiChat = (question, isPresetQuestion) => {
|
|
|
|
|
+ serial_no.value = userStore.userInfo?.uname + Date.now().toString()
|
|
|
|
|
+ $api
|
|
|
|
|
+ .aiChat({
|
|
|
|
|
+ serial_no: serial_no.value,
|
|
|
|
|
+ prompt: sessionStorage.getItem('prompt'),
|
|
|
|
|
+ // question_type: isPresetQuestion ? 'Predefined Question' : 'Free Question',
|
|
|
|
|
+ question_type: 'Free Question',
|
|
|
|
|
+ question_content: question
|
|
|
|
|
+ })
|
|
|
|
|
+ .then((res) => {
|
|
|
|
|
+ if (isPause.value) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ clearInterval(progressInterval.value)
|
|
|
|
|
+ const { data } = res.data
|
|
|
|
|
+ messages.value[messages.value.length - 1] = {
|
|
|
|
|
+ id: serial_no.value,
|
|
|
|
|
+ type: 'robot',
|
|
|
|
|
+ content: '',
|
|
|
|
|
+ feedback: '',
|
|
|
|
|
+ isShowFeedback: false,
|
|
|
|
|
+ isAnswer: true
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ parseHtmlString(data)
|
|
|
|
|
+ scrollToBottom()
|
|
|
|
|
+ loadingAnswer.value = false
|
|
|
|
|
+ queryTime.value = -1
|
|
|
|
|
+ } else {
|
|
|
|
|
+ loadingAnswer.value = false
|
|
|
|
|
+ messages.value[messages.value.length - 1].isError = true
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+const handleSend = (question, isPresetQuestion = true, isExternal = false) => {
|
|
|
|
|
+ if (!question) return
|
|
|
|
|
+ if (
|
|
|
|
|
+ isExternal &&
|
|
|
|
|
+ messages.value.length === 1 &&
|
|
|
|
|
+ messages.value[0].content === progressStatus.init
|
|
|
|
|
+ ) {
|
|
|
|
|
+ messages.value.pop()
|
|
|
|
|
+ }
|
|
|
if (loadingAnswer.value) {
|
|
if (loadingAnswer.value) {
|
|
|
isShowTips.value = true
|
|
isShowTips.value = true
|
|
|
setTimeout(() => {
|
|
setTimeout(() => {
|
|
@@ -117,8 +173,11 @@ const handleSend = (question, isPresetQuestion = true) => {
|
|
|
}, 2000)
|
|
}, 2000)
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
+ isPause.value = false
|
|
|
loadingAnswer.value = true
|
|
loadingAnswer.value = true
|
|
|
|
|
|
|
|
|
|
+ aiChat(question, isPresetQuestion)
|
|
|
|
|
+ // 将用户内容添加到消息列表
|
|
|
messages.value.push({
|
|
messages.value.push({
|
|
|
type: 'user',
|
|
type: 'user',
|
|
|
content: question
|
|
content: question
|
|
@@ -181,45 +240,40 @@ function scrollToBottom() {
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const simulateStreamingMarkdown = () => {
|
|
|
|
|
- loadingAnswer.value = true
|
|
|
|
|
- const chunks: any = [userQuestion.value]
|
|
|
|
|
-
|
|
|
|
|
- messages.value.push({
|
|
|
|
|
- type: 'robot',
|
|
|
|
|
- content: '',
|
|
|
|
|
- isAnswer: true,
|
|
|
|
|
- feedback: '',
|
|
|
|
|
- isShowFeedback: false
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- let index = 0
|
|
|
|
|
-
|
|
|
|
|
- autoScroll.value = true
|
|
|
|
|
- scrollToBottom()
|
|
|
|
|
-
|
|
|
|
|
- const interval = setInterval(() => {
|
|
|
|
|
- index === chunks.length - 1 ? (loadingAnswer.value = false) : ''
|
|
|
|
|
- if (index < chunks.length) {
|
|
|
|
|
- messages.value[messages.value.length - 1].content += chunks[index]
|
|
|
|
|
- index++
|
|
|
|
|
-
|
|
|
|
|
- if (autoScroll.value) {
|
|
|
|
|
|
|
+const isPause = ref(false) // 是否暂停回答
|
|
|
|
|
+// 暂停回答
|
|
|
|
|
+const handlePause = () => {
|
|
|
|
|
+ $api
|
|
|
|
|
+ .pauseAiChat({
|
|
|
|
|
+ serial_no: serial_no.value
|
|
|
|
|
+ })
|
|
|
|
|
+ .then((res) => {
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ clearInterval(progressInterval.value)
|
|
|
|
|
+ queryTime.value = -1
|
|
|
|
|
+ messages.value[messages.value.length - 1].isCancel = true
|
|
|
|
|
+ messages.value[messages.value.length - 1].content = progressStatus.cancel
|
|
|
|
|
+ isPause.value = true
|
|
|
|
|
+ loadingAnswer.value = false
|
|
|
scrollToBottom()
|
|
scrollToBottom()
|
|
|
}
|
|
}
|
|
|
- } else {
|
|
|
|
|
- clearInterval(interval)
|
|
|
|
|
- userQuestion.value = ''
|
|
|
|
|
- }
|
|
|
|
|
- }, 200) // 每500ms追加一段
|
|
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 暂停回答
|
|
|
|
|
-const handlePause = () => {
|
|
|
|
|
- clearInterval(progressInterval.value)
|
|
|
|
|
- queryTime.value = -1
|
|
|
|
|
- messages.value[messages.value.length - 1].isCancel = true
|
|
|
|
|
- messages.value[messages.value.length - 1].content = progressStatus.cancel
|
|
|
|
|
|
|
+// 评价
|
|
|
|
|
+const handleFeedback = (index, feedback) => {
|
|
|
|
|
+ const message = messages.value[index]
|
|
|
|
|
+ message.feedback = feedback
|
|
|
|
|
+ $api
|
|
|
|
|
+ .feedbackAiChat({
|
|
|
|
|
+ serial_no: message.id,
|
|
|
|
|
+ feedback: feedback
|
|
|
|
|
+ })
|
|
|
|
|
+ .then((res) => {
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ messages.value[index].isShowFeedback = false
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const emit = defineEmits(['close'])
|
|
const emit = defineEmits(['close'])
|
|
@@ -289,33 +343,35 @@ defineExpose({
|
|
|
src="./image/icon_loading.png"
|
|
src="./image/icon_loading.png"
|
|
|
alt=""
|
|
alt=""
|
|
|
/>
|
|
/>
|
|
|
- <div v-html="renderedMessage(msg.content)" class="markdown-body"></div>
|
|
|
|
|
|
|
+ <div style="display: inline-block; max-width: 100%">
|
|
|
|
|
+ <div v-html="msg.html || renderedMessage(msg.content)" class="markdown-body"></div>
|
|
|
|
|
+ </div>
|
|
|
<LoadingDots
|
|
<LoadingDots
|
|
|
v-if="index === messages.length - 1 && msg.isAnswer && loadingAnswer"
|
|
v-if="index === messages.length - 1 && msg.isAnswer && loadingAnswer"
|
|
|
></LoadingDots>
|
|
></LoadingDots>
|
|
|
<!-- 评价 -->
|
|
<!-- 评价 -->
|
|
|
<div class="review" v-if="msg.isShowFeedback && msg.isAnswer">
|
|
<div class="review" v-if="msg.isShowFeedback && msg.isAnswer">
|
|
|
<el-button
|
|
<el-button
|
|
|
- v-if="msg.feedback !== 'good'"
|
|
|
|
|
|
|
+ v-if="msg.feedback !== 'Cood'"
|
|
|
class="el-button--text"
|
|
class="el-button--text"
|
|
|
- @click="msg.feedback = 'good'"
|
|
|
|
|
|
|
+ @click="handleFeedback(index, 'Cood')"
|
|
|
>
|
|
>
|
|
|
<span class="font_family icon-icon_good_b"></span>
|
|
<span class="font_family icon-icon_good_b"></span>
|
|
|
</el-button>
|
|
</el-button>
|
|
|
- <div v-if="msg.feedback === 'good'" style="width: 16px; text-align: center">
|
|
|
|
|
|
|
+ <div v-if="msg.feedback === 'Cood'" style="width: 16px; text-align: center">
|
|
|
<span
|
|
<span
|
|
|
style="color: var(--color-theme); font-size: 14px"
|
|
style="color: var(--color-theme); font-size: 14px"
|
|
|
class="font_family icon-icon_good__filled_b"
|
|
class="font_family icon-icon_good__filled_b"
|
|
|
></span>
|
|
></span>
|
|
|
</div>
|
|
</div>
|
|
|
<el-button
|
|
<el-button
|
|
|
- v-if="msg.feedback !== 'noGood'"
|
|
|
|
|
|
|
+ v-if="msg.feedback !== 'Not Good'"
|
|
|
class="el-button--text"
|
|
class="el-button--text"
|
|
|
- @click="msg.feedback = 'noGood'"
|
|
|
|
|
|
|
+ @click="handleFeedback(index, 'Not Good')"
|
|
|
>
|
|
>
|
|
|
<span class="font_family icon-icon_notgood_b"></span>
|
|
<span class="font_family icon-icon_notgood_b"></span>
|
|
|
</el-button>
|
|
</el-button>
|
|
|
- <div v-if="msg.feedback === 'noGood'" style="width: 16px; text-align: center">
|
|
|
|
|
|
|
+ <div v-if="msg.feedback === 'Not Good'" style="width: 16px; text-align: center">
|
|
|
<span
|
|
<span
|
|
|
style="color: var(--color-theme); font-size: 14px"
|
|
style="color: var(--color-theme); font-size: 14px"
|
|
|
class="font_family icon-icon_notgood__filled_b"
|
|
class="font_family icon-icon_notgood__filled_b"
|
|
@@ -352,7 +408,6 @@ defineExpose({
|
|
|
@focus="isFooterInputFocus = true"
|
|
@focus="isFooterInputFocus = true"
|
|
|
@blur="isFooterInputFocus = false"
|
|
@blur="isFooterInputFocus = false"
|
|
|
/>
|
|
/>
|
|
|
- <el-button @click="simulateStreamingMarkdown">测试</el-button>
|
|
|
|
|
<div
|
|
<div
|
|
|
class="input-icon"
|
|
class="input-icon"
|
|
|
:class="[!userQuestion || queryTime !== -1 ? 'disable' : '']"
|
|
:class="[!userQuestion || queryTime !== -1 ? 'disable' : '']"
|