mirror of
http://112.124.100.131/ebiz-ai/ebiz-base-ai.git
synced 2025-12-06 17:36:48 +08:00
feat: 新增聊天组件和打字机效果实现
This commit is contained in:
@@ -15,7 +15,7 @@ export default {
|
||||
const content = JSON.parse(markup)
|
||||
return content
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
// console.log(e)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
<script>
|
||||
import SvgIcon from '@/components/svg-icon/index.vue'
|
||||
import { audioToText, gwcsChat } from '@/api/generatedApi'
|
||||
import { md } from './js/markdown-it'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -95,7 +96,6 @@ export default {
|
||||
requestIndex: 1,
|
||||
requestSingle: undefined,
|
||||
// 管控单次请求,当 abort 之后, while 后面的会进行重复请求
|
||||
single: false,
|
||||
isThink: false,
|
||||
newMessage: '',
|
||||
isRecording: false,
|
||||
@@ -208,14 +208,14 @@ export default {
|
||||
axiosGetAiChat() {
|
||||
const abortController = new AbortController()
|
||||
this.requestSingle = abortController
|
||||
this.messageInfo.information = this.single ? this.messageInfo.information : this.newMessage
|
||||
this.messageInfo.information = this.genMessage.information || this.newMessage
|
||||
|
||||
// 重置 answerMap
|
||||
this.answerMap = ''
|
||||
// this.typingQueue = []
|
||||
this.currentMessage = JSON.parse(JSON.stringify(this.messageInfo))
|
||||
let params = {
|
||||
appType: "gwcsHelper",
|
||||
const params = {
|
||||
appType: "gwcsHelper2",
|
||||
conversationId: this.conversationId,
|
||||
message: JSON.stringify(this.messageInfo),
|
||||
user: "gwcs-test",
|
||||
@@ -231,8 +231,7 @@ export default {
|
||||
isLike: false,
|
||||
isDisLike: false,
|
||||
}
|
||||
if (this.single) {
|
||||
// this.$set(this.messages, this.messages.length - 1, { ...this.currentMessage })
|
||||
if (this.genMessage) {
|
||||
this.$set(this.messages, this.messages.length - 1, this.genMessage = { ...this.currentMessage })
|
||||
} else {
|
||||
this.messages.push(this.currentMessage)
|
||||
@@ -253,7 +252,6 @@ export default {
|
||||
})
|
||||
.then(async (res) => {
|
||||
await this.processStreamResponse(res, this.requestIndex)
|
||||
this.single = false
|
||||
})
|
||||
.catch((err) => {
|
||||
// debugger
|
||||
@@ -270,15 +268,15 @@ export default {
|
||||
let buffer = ''
|
||||
while (true) {
|
||||
// try {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
buffer += new TextDecoder().decode(value)
|
||||
const lines = buffer.split('\n')
|
||||
lines.slice(0, -1).forEach((line) => {
|
||||
const parsed = this.parseStreamLine(line)
|
||||
if (parsed) this.updateMessageContent(parsed, requestIndex)
|
||||
})
|
||||
buffer = lines[lines.length - 1] || ''
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
buffer += new TextDecoder().decode(value)
|
||||
const lines = buffer.split('\n')
|
||||
lines.slice(0, -1).forEach((line) => {
|
||||
const parsed = this.parseStreamLine(line)
|
||||
if (parsed) this.updateMessageContent(parsed, requestIndex)
|
||||
})
|
||||
buffer = lines[lines.length - 1] || ''
|
||||
// } catch (error) {
|
||||
// console.error('读取流数据时发生错误:', error)
|
||||
// break
|
||||
@@ -290,25 +288,45 @@ export default {
|
||||
const cleanLine = line.replace(/^data:\s*/, '')
|
||||
if (!cleanLine) return null
|
||||
const data = JSON.parse(cleanLine)
|
||||
// debugger/
|
||||
// this.conversationId = data.conversation_id
|
||||
|
||||
this.$emit('update:conversationId', data.conversation_id)
|
||||
if (data.answer) {
|
||||
this.answerMap += data.answer
|
||||
|
||||
const is_complete = /<is_complete>([^<]*)(?:<\/is_complete>)?/.exec(this.answerMap)
|
||||
const information = /<information>([^<]*)(?:<\/information>)?/.exec(this.answerMap)
|
||||
const text = /<text>([^<]*)(?:<\/text>)?/.exec(this.answerMap)
|
||||
// console.log(this.answerMap);
|
||||
let completeInformation = {}
|
||||
let textContent = ''
|
||||
|
||||
this.messageInfo.information = information ? information[1].trim() : this.newMessage
|
||||
this.messageInfo.is_complete = is_complete ? is_complete[1].trim() : 'false'
|
||||
if (is_complete && is_complete[1] === 'true' && text && text[1].trim() === '') {
|
||||
this.requestSingle.abort()
|
||||
this.single = true
|
||||
try {
|
||||
// 解析 complete_information 容器
|
||||
const json = this.answerMap.match(/:::\s*complete_information\s*([\s\S]*?)\s*:::/)
|
||||
if (json) {
|
||||
completeInformation = JSON.parse(json[1])
|
||||
}
|
||||
|
||||
// 解析 text 容器
|
||||
const text = this.answerMap.match(/:::\s*text\s*([\s\S]*?)\s*:::/)
|
||||
if (text) {
|
||||
textContent = text[1].trim()
|
||||
}
|
||||
|
||||
console.log(`completeInformation: ${JSON.stringify(completeInformation)}, textContent: ${textContent}`);
|
||||
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
if (!completeInformation && !textContent) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { information, is_complete } = completeInformation
|
||||
|
||||
this.messageInfo.information = information ? information : this.newMessage
|
||||
this.messageInfo.is_complete = is_complete && is_complete.toString()
|
||||
|
||||
if (is_complete) {
|
||||
this.axiosGetAiChat()
|
||||
this.requestIndex++
|
||||
// this.typingQueue = []
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -368,13 +386,6 @@ export default {
|
||||
|
||||
// 取出一个完整文本块
|
||||
const chunk = this.typingQueue.shift()
|
||||
// if (chunk.message_id !== this.currentMessageID) {
|
||||
// console.log('message_id !== this.currentMessageID');
|
||||
// typeNextChar()
|
||||
// return
|
||||
// }
|
||||
// console.log(this.messages);
|
||||
|
||||
const chars = Array.from(chunk.answer)
|
||||
|
||||
const isThink = chunk.isThink
|
||||
@@ -386,14 +397,9 @@ export default {
|
||||
return
|
||||
}
|
||||
const char = chars.shift() || ''
|
||||
// this.$set(this.currentMessage, isThink ? 'think' : 'text', this.currentMessage[isThink ? 'think' : 'text'] + char)
|
||||
if (requestIndex === 2) {
|
||||
// console.log('requestIndex === 2', char);
|
||||
this.$set(this.genMessage, 'text', this.genMessage.text + char)
|
||||
// this.messages.splice(this.messages.length - 1, 1, this.genMessage)
|
||||
} if (requestIndex === 1) {
|
||||
// console.log('requestIndex === 1', char);
|
||||
|
||||
this.$set(this.currentMessage, 'text', this.currentMessage.text + char)
|
||||
}
|
||||
const delay = this.getTypingDelay(char)
|
||||
|
||||
@@ -3,12 +3,12 @@ import Vue from "vue";
|
||||
import KnowledgeCard from "../card/index.vue"
|
||||
|
||||
export function markdownContainer(markdownIt) {
|
||||
console.log(markdownIt);
|
||||
console.log(container);
|
||||
// console.log(markdownIt);
|
||||
// console.log(container);
|
||||
|
||||
container(markdownIt, "card", {
|
||||
validate: function (params) {
|
||||
console.log(params.trim(), "title line");
|
||||
// console.log(params.trim(), "title line");
|
||||
return "card"
|
||||
// return params.trim().match(/^card\b/);
|
||||
},
|
||||
@@ -19,28 +19,29 @@ export function markdownContainer(markdownIt) {
|
||||
content: function (tokens, idx, options, env, self) {
|
||||
|
||||
|
||||
console.log(`args`, tokens, idx, options, env, self);
|
||||
// console.log(`args`, tokens, idx, options, env, self);
|
||||
// return [...args]
|
||||
console.log(`tokens[idx]`, tokens[idx]);
|
||||
// console.log(`tokens[idx]`, tokens[idx]);
|
||||
|
||||
const { markup } = tokens[idx]
|
||||
console.log(`markup`, markup);
|
||||
// const { markup } = tokens[idx]
|
||||
// console.log(`markup`, markup);
|
||||
setTimeout(() => {
|
||||
const dom = document.getElementById("cardTest");
|
||||
// if (dom) return
|
||||
const vm = new Vue({
|
||||
el: "#cardTest",
|
||||
render: h => h(KnowledgeCard, {
|
||||
props: {
|
||||
content: tokens[idx]
|
||||
}
|
||||
})
|
||||
})
|
||||
// const vm = new Vue({
|
||||
// el: "#cardTest",
|
||||
// render: h => h(KnowledgeCard, {
|
||||
// props: {
|
||||
// content: tokens[idx]
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
// console.log(`vue instance`, vm);
|
||||
})
|
||||
|
||||
// return tokens[idx]
|
||||
return "<div id='cardTest'></div>"
|
||||
// return "<div id='cardTest'></div>"
|
||||
return tokens[idx].markup
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
125
src/views/AI-new/components/js/test/typing-test.js
Normal file
125
src/views/AI-new/components/js/test/typing-test.js
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* 打字机工具类测试文件
|
||||
* 用于在Node.js环境中验证打字机功能
|
||||
*/
|
||||
|
||||
// 导入打字机模块
|
||||
const { createTyping } = require('../typing.js');
|
||||
|
||||
// 测试函数
|
||||
async function testTyping() {
|
||||
console.log('=== 打字机工具类测试开始 ===\n');
|
||||
|
||||
// 创建打字机实例
|
||||
const typing = createTyping({
|
||||
speed: 100, // 100ms每个字符
|
||||
showCursor: true,
|
||||
cursor: '█'
|
||||
});
|
||||
|
||||
console.log('1. 测试基本打字功能');
|
||||
console.log('-------------------');
|
||||
|
||||
// 测试1: 基本打字
|
||||
await new Promise(resolve => {
|
||||
typing.startTyping('你好,这是一个打字机测试!', (text) => {
|
||||
console.log(`✓ 打字完成,最终文本: "${text}"\n`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// 等待一秒
|
||||
await sleep(1000);
|
||||
|
||||
console.log('2. 测试队列功能');
|
||||
console.log('---------------');
|
||||
|
||||
// 测试2: 队列功能
|
||||
typing.resetTyping();
|
||||
typing.pushText('第一段文本:欢迎使用打字机!');
|
||||
typing.pushText('第二段文本:支持队列播放。');
|
||||
typing.pushText('第三段文本:功能完整强大。');
|
||||
|
||||
console.log(`队列长度: ${typing.getState().queueLength}`);
|
||||
|
||||
await new Promise(resolve => {
|
||||
typing.startTyping(null, (text) => {
|
||||
console.log(`✓ 队列文本完成: "${text}"\n`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// 等待一秒
|
||||
await sleep(1000);
|
||||
|
||||
console.log('3. 测试暂停和恢复功能');
|
||||
console.log('---------------------');
|
||||
|
||||
// 测试3: 暂停和恢复
|
||||
typing.resetTyping();
|
||||
typing.startTyping('这是一个暂停测试,会在中途暂停然后恢复。');
|
||||
|
||||
// 2秒后暂停
|
||||
setTimeout(() => {
|
||||
console.log('>>> 暂停打字');
|
||||
typing.pauseTyping();
|
||||
|
||||
// 2秒后恢复
|
||||
setTimeout(() => {
|
||||
console.log('>>> 恢复打字');
|
||||
typing.resumeTyping();
|
||||
}, 2000);
|
||||
}, 2000);
|
||||
|
||||
// 等待完成
|
||||
await sleep(8000);
|
||||
|
||||
console.log('\n4. 测试状态获取');
|
||||
console.log('---------------');
|
||||
|
||||
// 测试4: 状态获取
|
||||
typing.resetTyping();
|
||||
typing.pushText('状态测试文本');
|
||||
typing.startTyping();
|
||||
|
||||
// 获取状态
|
||||
setTimeout(() => {
|
||||
const state = typing.getState();
|
||||
console.log('当前状态:', {
|
||||
isTyping: state.isTyping,
|
||||
isPaused: state.isPaused,
|
||||
progress: Math.round(state.progress * 100) + '%',
|
||||
currentText: `"${state.currentText}"`,
|
||||
queueLength: state.queueLength
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
await sleep(3000);
|
||||
|
||||
console.log('\n5. 测试速度调整');
|
||||
console.log('---------------');
|
||||
|
||||
// 测试5: 速度调整
|
||||
typing.resetTyping();
|
||||
typing.setSpeed(200); // 设置为200ms
|
||||
typing.startTyping('这是慢速打字测试...');
|
||||
|
||||
await sleep(5000);
|
||||
|
||||
console.log('\n=== 所有测试完成 ===');
|
||||
|
||||
// 最终清理
|
||||
typing.stopTyping();
|
||||
}
|
||||
|
||||
// 辅助函数:延时
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
if (require.main === module) {
|
||||
testTyping().catch(console.error);
|
||||
}
|
||||
|
||||
module.exports = { testTyping };
|
||||
15
src/views/AI-new/components/js/test/typing.js
Normal file
15
src/views/AI-new/components/js/test/typing.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createTyping } from "../typing.js";
|
||||
|
||||
const { startTyping, stopTyping, pauseTyping, pushStr } = createTyping()
|
||||
|
||||
|
||||
pushStr("hello")
|
||||
pushStr("world")
|
||||
|
||||
async function init() {
|
||||
for await (const str of startTyping()) {
|
||||
console.log(str)
|
||||
}
|
||||
}
|
||||
|
||||
init()
|
||||
56
src/views/AI-new/components/js/typing.js
Normal file
56
src/views/AI-new/components/js/typing.js
Normal file
@@ -0,0 +1,56 @@
|
||||
export function createTyping(options) {
|
||||
let text = ''
|
||||
let typingIndex = 0;
|
||||
const typingQueue = []
|
||||
|
||||
const typingOptions = {
|
||||
typingSpeed: 100,
|
||||
showCursor: false,
|
||||
|
||||
cursor: '█',
|
||||
...options
|
||||
}
|
||||
|
||||
function startTyping() {
|
||||
if (!hasNext()) return
|
||||
const _text = getStr()
|
||||
typingIndex += 1
|
||||
}
|
||||
|
||||
function stopTyping() {
|
||||
|
||||
}
|
||||
|
||||
function pauseTyping() {
|
||||
|
||||
}
|
||||
|
||||
function resetIndex() {
|
||||
typingIndex = 0
|
||||
}
|
||||
|
||||
function pushStr(str) {
|
||||
typingQueue.push(str)
|
||||
}
|
||||
|
||||
function getStr() {
|
||||
return typingQueue[typingIndex]
|
||||
}
|
||||
|
||||
function getInputText() {
|
||||
|
||||
}
|
||||
|
||||
function hasNext() {
|
||||
return typingIndex <= typingQueue.length
|
||||
}
|
||||
|
||||
return {
|
||||
startTyping,
|
||||
pauseTyping,
|
||||
stopTyping,
|
||||
pushStr,
|
||||
getStr,
|
||||
resetIndex
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user