vue3笔记(42-1)deepseek-用户输入总结

收到了用户反馈,需要加上键盘快捷键,不能点 enter 就发送。
和大模型通信的后续版本,有语音输入的需求,问了下某代码生成很厉害的模型,这里做一个记录。

键盘快捷键

原来使用@keyup.enter="handleSubmit",这样用户只要输入 enter 就发送请求,很有可能只输入了半句话,或者需要换行,导致用户体验不好。

新增了一个函数handleMultiLineNewline作为换行处理。测试了@keydown.meta.enter,可以在 mac 系统下使用 cmd + enter/windows 系统下使用 win + enter 触发换行。@keydown.ctrl.enter@keydown.shift.enter可以触发换行。
但是以上连用不知道为啥消息又自动发出去了。。。
索性自己写了一个函数handleEnter来处理换行。

另一个需求是中文输入法下,未输入完时,使用 enter 键,默认不发送消息,整体如下:

1
2
3
4
5
6
7
8
9
10
11
12
<el-input
id="userInput"
v-model="userInput"
placeholder="请输入您的问题"
@compositionstart="onCompositionStart"
@compositionend="onCompositionEnd"
@keydown.enter="handleEnter"
type="textarea"
rows="5"
maxlength="2048"
resize="none"
/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const handleEnter = async (event: KeyboardEvent) => {
if (
(event.shiftKey && event.key === 'Enter') ||
(event.ctrlKey && event.key === 'Enter') ||
(event.metaKey && event.key === 'Enter')
) {
const start = (event.target as HTMLTextAreaElement).selectionStart
const end = (event.target as HTMLTextAreaElement).selectionEnd
const value = userInput.value
userInput.value = value.slice(0, start) + '\n' + value.slice(end)
;(event.target as HTMLTextAreaElement).selectionStart = (
event.target as HTMLTextAreaElement
).selectionEnd = start + 1
event.preventDefault() // 阻止默认行为
} else if (!isComposing.value && event.key === 'Enter') {
// 发送消息
handleSubmit(event)
event.preventDefault()
}
}

// 标记输入法是否处于输入候选状态
const isComposing = ref(false)
const onCompositionStart = () => {
isComposing.value = true
}
const onCompositionEnd = () => {
isComposing.value = false
}

当用户输入为空时,不做处理;当大模型在输出时,禁止用户再次提交信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const handleSubmit = async (event: any) => {
console.log('handleSubmit', event)
if (!userInput.value || userInput.value.trim().length === 0) {
return
} else if (loadingSend.value == true) {
// 防止在大模型输出时提交
event.preventDefault()
return
} else {
if (abortController.signal.aborted) {
abortController = new AbortController()
}
loadingSend.value = true
let text = userInput.value
if (text.endsWith('\n')) {
// 去掉末尾的换行符
text = text.trimEnd()
}
chatList.value.push({ role: 'user', message: text })
userInput.value = ''
scrollToBottom()
handleChat(text)
}
}

结果某用户提了个 bug,说按了 Enter 键,消息还是发出去了,最终排查出来是 Safari 浏览器的问题,需要前端做一个兼容。
尝试了推荐的以下事件垫片代码,实测无效。

1
2
3
4
5
6
7
import { useEventListener } from '@vueuse/core'
const inputRef = ref(null)
onMounted(() => {
// 使用useEventListener来添加事件监听器,处理兼容性
useEventListener(inputRef.value, 'compositionstart', onCompositionStart)
useEventListener(inputRef.value, 'compositionend', onCompositionEnd)
})

最后改了好几版,测试下来以下代码可以兼容 Safari 浏览器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const isComposing = ref(false) // 标记输入法是否处于输入候选状态
const ignoreEnter = ref(false) // 标记是否忽略 enter 事件
const onCompositionStart = () => {
isComposing.value = true
}
const onCompositionEnd = () => {
isComposing.value = false
ignoreEnter.value = true // 组合输入结束后,设置忽略 enter 事件
setTimeout(() => {
ignoreEnter.value = false // 一段时间后恢复对 enter 事件的响应
}, 200) // 可以根据实际情况调整这个时间
}

onMounted(() => {
const inputElement = document.getElementById('userInput')
if (inputElement) {
if (!('oncompositionstart' in inputElement)) {
// 不支持 compositionstart 事件,进行模拟
let isComposing = false
const keydownHandler = (event) => {
if (event.keyCode === 229) {
// 229 是输入法开始输入时的键码
isComposing = true
onCompositionStart()
}
}
const keyupHandler = (event) => {
if (isComposing) {
isComposing = false
onCompositionEnd()
}
}
inputElement.addEventListener('keydown', keydownHandler)
inputElement.addEventListener('keyup', keyupHandler)

// 在组件卸载时移除事件监听器
onUnmounted(() => {
inputElement.removeEventListener('keydown', keydownHandler)
inputElement.removeEventListener('keyup', keyupHandler)
})
}
}
})

语音输入

在 vue3 项目中,使用useSpeechRecognition这个库,实现语音输入转文字功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<CustomIcon
v-if="!isRecording"
name="icon-voice"
size="24"
@click="isRecording = true"
class="cursor-pointer"
/>
<CustomIcon
v-else
name="icon-voice-active"
size="24"
@click="isRecording = false"
class="cursor-pointer"
/>

</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { useSpeechRecognition } from '@vueuse/core'

// 语音输入
const isRecording = ref(false)
const { isListening, result, start, stop, isSupported } = useSpeechRecognition({
lang: 'zh-CN',
continuous: true
})
if (isSupported.value) {
watch(result, (value) => {
if (isRecording.value) {
userInput.value = value
}
})
}

watch(isListening, (value) => {
if (!value && isRecording.value) {
start()
}
})

watch(isRecording, (value) => {
if (value) {
start()
} else {
stop()
}
})