feat: 新增聊天组件和打字机效果实现

This commit is contained in:
huangzhe
2025-08-04 11:32:15 +08:00
parent 218609d50b
commit e390edf77e
6 changed files with 260 additions and 57 deletions

View File

@@ -15,7 +15,7 @@ export default {
const content = JSON.parse(markup)
return content
} catch (e) {
console.log(e)
// console.log(e)
return {}
}
}

View File

@@ -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)

View File

@@ -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
}
})
}

View 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 };

View 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()

View 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
}
}