mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
feat: chat scroll and load
This commit is contained in:
@@ -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{
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user