feat: chat scroll and load

This commit is contained in:
Quicy
2026-01-28 14:49:14 +08:00
parent e8b690b174
commit d5f5a0a892
8 changed files with 181 additions and 33 deletions
+2 -1
View File
@@ -3,7 +3,8 @@ export interface robot{
time: Date,
id: string | number,
type: string,
action:'robot'
action: 'robot',
state:'thinking'|'generate'|'complete'
}
export interface user{
-2
View File
@@ -8,9 +8,7 @@ import {
} from '@memoh/ui'
import SvgIcon from '@jamescoyle/vue-icon'
import { mdiTranslate } from '@mdi/js'
import i18n from './i18n'
console.log(i18n.global.locale)
</script>
<template>
@@ -10,8 +10,17 @@
<sup class="font-semibold">
{{ robotSay.type }}
</sup>
<p class="leading-7 text-muted-foreground">
{{ robotSay.description }}
<p class="leading-7 text-muted-foreground break-all">
<template v-if="robotSay.state==='thinking'">
<img
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48Y2lyY2xlIGN4PSI0IiBjeT0iMTIiIHI9IjMiIGZpbGw9ImN1cnJlbnRDb2xvciI+PGFuaW1hdGUgaWQ9IlNWRzlJZ2JSYnNsIiBhdHRyaWJ1dGVOYW1lPSJyIiBiZWdpbj0iMDtTVkdGVU5wQ1dkRy5lbmQtMC4yNXMiIGR1cj0iMC43NXMiIHZhbHVlcz0iMzsuMjszIi8+PC9jaXJjbGU+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMyIgZmlsbD0iY3VycmVudENvbG9yIj48YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJyIiBiZWdpbj0iU1ZHOUlnYlJic2wuZW5kLTAuNnMiIGR1cj0iMC43NXMiIHZhbHVlcz0iMzsuMjszIi8+PC9jaXJjbGU+PGNpcmNsZSBjeD0iMjAiIGN5PSIxMiIgcj0iMyIgZmlsbD0iY3VycmVudENvbG9yIj48YW5pbWF0ZSBpZD0iU1ZHRlVOcENXZEciIGF0dHJpYnV0ZU5hbWU9InIiIGJlZ2luPSJTVkc5SWdiUmJzbC5lbmQtMC40NXMiIGR1cj0iMC43NXMiIHZhbHVlcz0iMzsuMjszIi8+PC9jaXJjbGU+PC9zdmc+"
class="inline"
alt="thinking"
>
</template>
<template v-else>
{{ robotSay.description }}
</template>
</p>
</section>
</div>
@@ -22,8 +31,8 @@ import SvgIcon from '@jamescoyle/vue-icon'
import { mdiRobotOutline } from '@mdi/js'
import type {robot} from '@memoh/shared'
const {robotSay}=defineProps<{
robotSay: robot
}>()
</script>
@@ -1,7 +1,7 @@
<template>
<div class="flex">
<p
class="leading-7 not-first:mt-6 max-w-[90%] ml-auto text-muted-foreground bg-[#F9F9F9] p-4 rounded-xl rounded-tr-none
class="leading-7 not-first:mt-6 max-w-[90%] ml-auto text-muted-foreground bg-[#F9F9F9] p-4 rounded-xl rounded-tr-none break-all
"
>
{{ userSay.description }}
+64 -12
View File
@@ -1,5 +1,8 @@
<template>
<div class="flex flex-col gap-4">
<div
ref="displayContainer"
class="flex flex-col gap-4"
>
<template
v-for="chatItem in chatList"
:key="chatItem.id"
@@ -19,18 +22,67 @@
<script setup lang="ts">
import UserChat from './UserChat/index.vue'
import RobotChat from './RobotChat/index.vue'
import { reactive } from 'vue'
import type { user, robot } from '@memoh/shared'
import { inject, ref, watch } from 'vue'
import { useElementBounding } from '@vueuse/core'
import {useChatList} from '@/store/ChatList'
// 模拟一下数据
const chatList = reactive<(((user | robot)))[]>([{
description: 'fjiwofwofjewifwe', time: new Date, id: 2, action: 'user'
const {chatList,add} = useChatList()
const chatSay = inject('chatSay', ref(''))
// 模拟一下对话
watch(chatSay, () => {
if (chatSay.value) {
add({
description: chatSay.value,
time: new Date(),
action: 'user',
id: 1
})
add({
description: '',
time: new Date(),
action: 'robot',
id: 2,
type: 'Openai Gpt5',
state:'thinking'
})
chatSay.value=''
}
}, {
description: 'fjiwofwofjefwfewfwifwe', time: new Date, id: 1000, action: 'robot', type: 'Openai Gpt5'
}, {
description: 'fjiwofwofjewifwe', time: new Date, id: 2, action: 'user'
}, {
description: 'fjiwofwofjefwfewfwifwe', time: new Date, id: 1000, action: 'robot', type: 'Openai Gpt5'
}])
immediate: true
})
const displayContainer = ref()
const { height,top } = useElementBounding(displayContainer)
let prevScroll = 0, curScroll = 0, autoScroll = true
watch(top, () => {
const container = displayContainer.value?.parentElement?.parentElement
if ((container?.scrollHeight - container.clientHeight - container.scrollTop) < 1) {
autoScroll = true
prevScroll=curScroll=container.scrollTop
}
})
watch(height, () => {
const container = displayContainer.value?.parentElement?.parentElement
if (container) {
curScroll = container.scrollTop
if (curScroll < prevScroll) {
autoScroll = false
}
prevScroll = curScroll
}
if (!(container && (container?.scrollHeight - container.clientHeight - container.scrollTop) < 1)&&autoScroll) {
container.scrollTo({
top: container?.scrollHeight - container.clientHeight,
behavior: 'smooth',
})
}
})
</script>
+38 -14
View File
@@ -13,30 +13,40 @@
{{ tag }}
</div>
</template>
</div>
</ScrollArea> -->
</div>
</ScrollArea> -->
<section class="flex-1 h-0">
<ScrollArea class="max-h-full h-full w-full rounded-md border p-4">
<ScrollArea
ref="chat-container"
class="max-h-full h-full w-full rounded-md border p-4 **:focus-visible:ring-0! "
>
<ChatList />
</ScrollArea>
</section>
<section class="flex-none relative">
<Textarea
v-model="curInputSay"
class="pb-16 pt-4"
:placeholder="$t('prompt.enter',{msg:$t('desc.question')})"
:placeholder="$t('prompt.enter', { msg: $t('desc.question') })"
/>
<section
class="absolute bottom-0 h-14 px-2 inset-x-0 flex items-center"
>
<section class="absolute bottom-0 h-14 px-2 inset-x-0 flex items-center">
<Button
variant="default"
class="ml-auto"
@click="send"
>
{{ $t('chat.send') }}
<svg-icon
type="mdi"
:path="mdiSendOutline"
/>
<template v-if="!loading">
{{ $t('chat.send') }}
<svg-icon
type="mdi"
:path="mdiSendOutline"
/>
</template>
<img
v-else
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48Y2lyY2xlIGN4PSI0IiBjeT0iMTIiIHI9IjEuNSIgZmlsbD0iI2ZmZiI+PGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iciIgZHVyPSIwLjc1cyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHZhbHVlcz0iMS41OzM7MS41Ii8+PC9jaXJjbGU+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMyIgZmlsbD0iI2ZmZiI+PGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iciIgZHVyPSIwLjc1cyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHZhbHVlcz0iMzsxLjU7MyIvPjwvY2lyY2xlPjxjaXJjbGUgY3g9IjIwIiBjeT0iMTIiIHI9IjEuNSIgZmlsbD0iI2ZmZiI+PGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iciIgZHVyPSIwLjc1cyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHZhbHVlcz0iMS41OzM7MS41Ii8+PC9jaXJjbGU+PC9zdmc+"
alt="loading"
>
</Button>
</section>
</section>
@@ -45,14 +55,28 @@
<script setup lang="ts">
import {
ScrollArea,
ScrollArea,
Textarea,
Button
} from '@memoh/ui'
import SvgIcon from '@jamescoyle/vue-icon'
import { mdiSendOutline } from '@mdi/js'
import ChatList from '@/components/ChatList/index.vue'
import { provide, ref } from 'vue'
import { useChatList } from '@/store/ChatList'
import {storeToRefs} from 'pinia'
const chatSay = ref('')
const curInputSay = ref('')
const {loading}=storeToRefs(useChatList())
provide('chatSay', chatSay)
const send = () => {
if (loading.value === false) {
chatSay.value = curInputSay.value
curInputSay.value = ''
}
}
</script>
+36
View File
@@ -0,0 +1,36 @@
import { defineStore } from 'pinia'
import { reactive, watch,ref} from 'vue'
import type { user, robot } from '@memoh/shared'
import loadRobotChat from '@/utils/loadRobotChat'
export const useChatList= defineStore('chatList', () => {
const chatList = reactive<(((user | robot)))[]>([])
const loading=ref(false)
const add = (chatItem: user | robot) => {
chatList.push(chatItem)
}
// 监听状态的watch,同一时间只能有一个thinking和complete
watch(chatList, () => {
const robotType=chatList.filter(chatItem => chatItem.action === 'robot')
const isLoading = robotType.some(robotItem => robotItem.state === 'thinking'||robotItem.state==='generate')
if (isLoading) {
loading.value=true
} else {
loading.value=false
}
const generateItem = robotType.find(robotItem => robotItem.state === 'thinking')
// 模拟一下改变状态
setTimeout(() => {
if (generateItem) {
loadRobotChat(generateItem, '对不起,该问题超出我的知识范围')
}
},3000)
}, {
immediate:true
})
return {
chatList,
add,
loading
}
})
+28
View File
@@ -0,0 +1,28 @@
import {type robot } from '@memoh/shared'
export default function (chatItem: robot, desc: string) {
const robotAnswer=new ReadableStream({
async start(controller){
for (const str of [...desc]) {
await new Promise(resolve=>setTimeout(()=>resolve(str),50))
controller.enqueue(str)
}
controller.close()
}
})
async function readRobotAnswer() {
const reader = robotAnswer.getReader()
let answer = await reader.read()
chatItem.state = 'generate'
while (!answer.done) {
chatItem.description = `${chatItem.description}${answer.value}`
answer=await reader.read()
}
chatItem.state = 'complete'
}
if (chatItem.state !== 'complete') {
readRobotAnswer()
}
}