Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
admin
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
cdf0a113
authored
May 25, 2024
by
YunaiV
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
【优化】AI:对话的 user、role 头像从前端获取
parent
46eb8969
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
75 additions
and
86 deletions
+75
-86
src/views/ai/chat/Conversation.vue
+2
-1
src/views/ai/chat/Message.vue
+10
-17
src/views/ai/chat/index.vue
+63
-68
No files found.
src/views/ai/chat/Conversation.vue
View file @
cdf0a113
...
...
@@ -43,7 +43,7 @@
:class=
"conversation.id === activeConversationId ? 'conversation active' : 'conversation'"
>
<div
class=
"title-wrapper"
>
<img
class=
"avatar"
:src=
"conversation.roleAvatar"
/>
<img
class=
"avatar"
:src=
"conversation.roleAvatar
|| roleAvatarDefaultImg
"
/>
<span
class=
"title"
>
{{ conversation.title }}
</span>
</div>
<div
class=
"button-wrapper"
v-show=
"hoverConversationId === conversation.id"
>
...
...
@@ -99,6 +99,7 @@ import {ChatConversationApi, ChatConversationVO} from '@/api/ai/chat/conversatio
import
{
ref
}
from
"vue"
;
import
Role
from
"@/views/ai/chat/role/index.vue"
;
import
{
Bottom
,
Top
}
from
"@element-plus/icons-vue"
;
import
roleAvatarDefaultImg
from
'@/assets/ai/gpt.svg'
const
message
=
useMessage
()
// 消息弹窗
...
...
src/views/ai/chat/Message.vue
View file @
cdf0a113
<
template
>
<div
ref=
"messageContainer"
style=
"height: 100%; overflow-y: auto; position: relative"
>
<div
class=
"chat-list"
v-for=
"(item, index) in
messageL
ist"
:key=
"index"
>
<div
class=
"chat-list"
v-for=
"(item, index) in
l
ist"
:key=
"index"
>
<!-- 靠左 message -->
<div
class=
"left-message message-item"
v-if=
"item.type !== 'user'"
>
<div
class=
"avatar"
>
<el-avatar
:src=
"
item.
roleAvatar"
/>
<el-avatar
:src=
"roleAvatar"
/>
</div>
<div
class=
"message"
>
<div>
...
...
@@ -26,7 +26,7 @@
<!-- 靠右 message -->
<div
class=
"right-message message-item"
v-if=
"item.type === 'user'"
>
<div
class=
"avatar"
>
<el-avatar
:src=
"
item.
userAvatar"
/>
<el-avatar
:src=
"userAvatar"
/>
</div>
<div
class=
"message"
>
<div>
...
...
@@ -70,12 +70,19 @@ import { useClipboard } from '@vueuse/core'
import
{
PropType
}
from
'vue'
import
{
ArrowDownBold
,
Edit
,
RefreshRight
}
from
'@element-plus/icons-vue'
import
{
ChatConversationVO
}
from
'@/api/ai/chat/conversation'
import
{
useUserStore
}
from
'@/store/modules/user'
;
import
userAvatarDefaultImg
from
'@/assets/imgs/avatar.gif'
import
roleAvatarDefaultImg
from
'@/assets/ai/gpt.svg'
const
{
copy
}
=
useClipboard
()
// 初始化 copy 到粘贴板
// 判断 消息列表 滚动的位置(用于判断是否需要滚动到消息最下方)
const
messageContainer
:
any
=
ref
(
null
)
const
isScrolling
=
ref
(
false
)
//用于判断用户是否在滚动
const
userStore
=
useUserStore
()
const
userAvatar
=
computed
(()
=>
userStore
.
user
.
avatar
??
userAvatarDefaultImg
)
const
roleAvatar
=
computed
(()
=>
props
.
conversation
.
roleAvatar
??
roleAvatarDefaultImg
)
// 定义 props
const
props
=
defineProps
({
conversation
:
{
...
...
@@ -88,20 +95,6 @@ const props = defineProps({
}
})
const
messageList
=
computed
(()
=>
{
if
(
props
.
list
&&
props
.
list
.
length
>
0
)
{
return
props
.
list
}
if
(
props
.
conversation
&&
props
.
conversation
.
systemMessage
)
{
return
[{
id
:
0
,
type
:
'system'
,
content
:
props
.
conversation
.
systemMessage
}]
}
return
[]
})
// ============ 处理对话滚动 ==============
const
scrollToBottom
=
async
(
isIgnore
?:
boolean
)
=>
{
...
...
src/views/ai/chat/index.vue
View file @
cdf0a113
<
template
>
<el-container
class=
"ai-layout"
>
<!-- 左侧:对话列表 -->
<Conversation
:active-id=
"activeConversationId"
ref=
"conversationRef"
@
onConversationCreate=
"handleConversationCreate"
@
onConversationClick=
"handleConversationClick"
@
onConversationClear=
"handlerConversationClear"
@
onConversationDelete=
"handlerConversationDelete"
<Conversation
:active-id=
"activeConversationId"
ref=
"conversationRef"
@
onConversationCreate=
"handleConversationCreate"
@
onConversationClick=
"handleConversationClick"
@
onConversationClear=
"handlerConversationClear"
@
onConversationDelete=
"handlerConversationDelete"
/>
<!-- 右侧:对话详情 -->
<el-container
class=
"detail-container"
>
<el-header
class=
"header"
>
<div
class=
"title"
>
{{
activeConversation
?.
title
?
activeConversation
?.
title
:
'对话'
}}
<span
v-if=
"list.length"
>
(
{{
list
.
length
}}
)
</span>
<span
v-if=
"list.length"
>
(
{{
list
.
length
}}
)
</span>
</div>
<div
class=
"btns"
v-if=
"activeConversation"
>
<el-button
type=
"primary"
bg
plain
size=
"small"
@
click=
"openChatConversationUpdateForm"
>
<span
v-html=
"activeConversation?.modelName"
></span>
<Icon
icon=
"ep:setting"
style=
"margin-left: 10px"
/>
<Icon
icon=
"ep:setting"
style=
"margin-left: 10px"
/>
</el-button>
<el-button
size=
"small"
class=
"btn"
@
click=
"handlerMessageClear"
>
<!-- TODO @fan:style 部分,可以考虑用 unocss 替代 -->
<img
src=
"@/assets/ai/clear.svg"
style=
"height: 14px
;
"
/>
<img
src=
"@/assets/ai/clear.svg"
style=
"height: 14px"
/>
</el-button>
<!-- TODO @fan:下面两个 icon,可以使用类似
<Icon
icon=
"ep:question-filled"
/>
替代哈 -->
<el-button
size=
"small"
:icon=
"Download"
class=
"btn"
/>
<el-button
size=
"small"
:icon=
"Top"
class=
"btn"
@
click=
"handlerGoTop"
/>
<el-button
size=
"small"
:icon=
"Download"
class=
"btn"
/>
<el-button
size=
"small"
:icon=
"Top"
class=
"btn"
@
click=
"handlerGoTop"
/>
</div>
</el-header>
<!-- main:消息列表 -->
<el-main
class=
"main-container"
>
<div
>
<div
class=
"message-container"
>
<el-main
class=
"main-container"
>
<div>
<div
class=
"message-container"
>
<MessageLoading
v-if=
"listLoading"
/>
<MessageNewChat
v-if=
"!activeConversation"
@
on-new-chat=
"handlerNewChat"
/>
<ChatEmpty
v-if=
"!listLoading && messageList.length === 0 && activeConversation"
@
on-prompt=
"doSend"
/>
<Message
v-if=
"!listLoading && messageList.length > 0"
ref=
"messageRef"
:conversation=
"activeConversation"
:list=
"messageList"
@
on-delete-success=
"handlerMessageDelete"
@
on-edit=
"handlerMessageEdit"
@
on-refresh=
"handlerMessageRefresh"
/>
<ChatEmpty
v-if=
"!listLoading && messageList.length === 0 && activeConversation"
@
on-prompt=
"doSend"
/>
<Message
v-if=
"!listLoading && messageList.length > 0"
ref=
"messageRef"
:conversation=
"activeConversation"
:list=
"messageList"
@
on-delete-success=
"handlerMessageDelete"
@
on-edit=
"handlerMessageEdit"
@
on-refresh=
"handlerMessageRefresh"
/>
</div>
</div>
</el-main>
...
...
@@ -62,7 +68,8 @@
></textarea>
<div
class=
"prompt-btns"
>
<div>
<el-switch
v-model=
"enableContext"
/>
<span
style=
"font-size: 14px; color: #8f8f8f;"
>
上下文
</span>
<el-switch
v-model=
"enableContext"
/>
<span
style=
"font-size: 14px; color: #8f8f8f"
>
上下文
</span>
</div>
<el-button
type=
"primary"
...
...
@@ -102,11 +109,10 @@ import Message from './Message.vue'
import
ChatEmpty
from
'./ChatEmpty.vue'
import
MessageLoading
from
'./MessageLoading.vue'
import
MessageNewChat
from
'./MessageNewChat.vue'
import
{
ChatMessageApi
,
ChatMessageVO
}
from
'@/api/ai/chat/message'
import
{
ChatConversationApi
,
ChatConversationVO
}
from
'@/api/ai/chat/conversation'
import
{
getUserProfile
,
ProfileVO
}
from
'@/api/system/user/profile'
import
ChatConversationUpdateForm
from
"@/views/ai/chat/components/ChatConversationUpdateForm.vue"
;
import
{
Download
,
Top
}
from
"@element-plus/icons-vue"
;
import
{
ChatMessageApi
,
ChatMessageVO
}
from
'@/api/ai/chat/message'
import
{
ChatConversationApi
,
ChatConversationVO
}
from
'@/api/ai/chat/conversation'
import
ChatConversationUpdateForm
from
'@/views/ai/chat/components/ChatConversationUpdateForm.vue'
import
{
Download
,
Top
}
from
'@element-plus/icons-vue'
const
route
=
useRoute
()
// 路由
const
message
=
useMessage
()
// 消息弹窗
...
...
@@ -118,14 +124,13 @@ const conversationInProgress = ref(false) // 对话进行中
const
conversationInAbortController
=
ref
<
any
>
()
// 对话进行中 abort 控制器(控制 stream 对话)
const
inputTimeout
=
ref
<
any
>
()
// 处理输入中回车的定时器
const
prompt
=
ref
<
string
>
()
// prompt
const
userInfo
=
ref
<
ProfileVO
>
()
// 用户信息
const
enableContext
=
ref
<
boolean
>
(
true
)
// 是否开启上下文
// TODO @fan:这几个变量,可以注释在补下哈;另外,fullText 可以明确是生成中的消息 Text,这样更容易理解哈;
const
fullText
=
ref
(
''
)
;
const
displayedText
=
ref
(
''
)
;
const
textSpeed
=
ref
<
number
>
(
50
)
;
// Typing speed in milliseconds
const
textRoleRunning
=
ref
<
boolean
>
(
false
)
;
// Typing speed in milliseconds
const
fullText
=
ref
(
''
)
const
displayedText
=
ref
(
''
)
const
textSpeed
=
ref
<
number
>
(
50
)
// Typing speed in milliseconds
const
textRoleRunning
=
ref
<
boolean
>
(
false
)
// Typing speed in milliseconds
// chat message 列表
// TODO @fan:list、listLoading、listLoadingTime 不能体现出来是消息列表,是不是可以变量再优化下
...
...
@@ -139,13 +144,14 @@ const conversationRef = ref()
const
isComposing
=
ref
(
false
)
// 判断用户是否在输入
// 默认 role 头像
const
defaultRoleAvatar
=
'http://test.yudao.iocoder.cn/eaef5f41acb911dd718429a0702dcc3c61160d16e57ba1d543132fab58934f9f.png'
const
defaultRoleAvatar
=
'http://test.yudao.iocoder.cn/eaef5f41acb911dd718429a0702dcc3c61160d16e57ba1d543132fab58934f9f.png'
// =========== 自提滚动效果
// TODO @fan:这个方法,要不加个方法注释
const
textRoll
=
async
()
=>
{
let
index
=
0
;
let
index
=
0
try
{
// 只能执行一次
if
(
textRoleRunning
.
value
)
{
...
...
@@ -174,8 +180,8 @@ const textRoll = async () => {
// console.log('index
<
fullText
.
value
.
length
'
,
index
<
fullText
.
value
.
length
,
conversationInProgress
.
value
)
if
(
index
<
fullText
.
value
.
length
)
{
displayedText
.
value
+=
fullText
.
value
[
index
]
;
index
++
;
displayedText
.
value
+=
fullText
.
value
[
index
]
index
++
// 更新 message
const
lastMessage
=
list
.
value
[
list
.
value
.
length
-
1
]
...
...
@@ -185,24 +191,23 @@ const textRoll = async () => {
// 滚动到住下面
await
scrollToBottom
()
// 重新设置任务
timer
=
setTimeout
(
task
,
textSpeed
.
value
)
;
timer
=
setTimeout
(
task
,
textSpeed
.
value
)
}
else
{
// 不是对话中可以结束
if
(
!
conversationInProgress
.
value
)
{
textRoleRunning
.
value
=
false
clearTimeout
(
timer
)
;
console
.
log
(
"字体滚动退出!"
)
clearTimeout
(
timer
)
console
.
log
(
'字体滚动退出!'
)
}
else
{
// 重新设置任务
timer
=
setTimeout
(
task
,
textSpeed
.
value
)
;
timer
=
setTimeout
(
task
,
textSpeed
.
value
)
}
}
}
let
timer
=
setTimeout
(
task
,
textSpeed
.
value
)
;
let
timer
=
setTimeout
(
task
,
textSpeed
.
value
)
}
finally
{
}
};
}
// ============ 处理对话滚动 ==============
...
...
@@ -266,12 +271,12 @@ const onSend = async (event) => {
if
(
event
.
key
===
'Enter'
)
{
if
(
event
.
shiftKey
)
{
// 插入换行
prompt
.
value
+=
'\r\n'
;
event
.
preventDefault
()
;
// 防止默认的换行行为
prompt
.
value
+=
'\r\n'
event
.
preventDefault
()
// 防止默认的换行行为
}
else
{
// 发送消息
await
doSend
(
content
)
event
.
preventDefault
()
;
// 防止默认的提交行为
event
.
preventDefault
()
// 防止默认的提交行为
}
}
}
...
...
@@ -317,13 +322,11 @@ const doSendStream = async (userMessage: ChatMessageVO) => {
fullText
.
value
=
''
try
{
// 先添加两个假数据,等 stream 返回再替换
// TODO @fan:idea 这里会报类型错误,是不是可以解决下哈
list
.
value
.
push
({
id
:
-
1
,
conversationId
:
activeConversationId
.
value
,
type
:
'user'
,
content
:
userMessage
.
content
,
userAvatar
:
userInfo
.
value
?.
avatar
,
createTime
:
new
Date
()
}
as
ChatMessageVO
)
list
.
value
.
push
({
...
...
@@ -331,7 +334,6 @@ const doSendStream = async (userMessage: ChatMessageVO) => {
conversationId
:
activeConversationId
.
value
,
type
:
'system'
,
content
:
'思考中...'
,
roleAvatar
:
(
activeConversation
.
value
as
ChatConversationVO
).
roleAvatar
,
createTime
:
new
Date
()
}
as
ChatMessageVO
)
// 滚动到最下面
...
...
@@ -415,11 +417,13 @@ const messageList = computed(() => {
// 没有消息时,如果有 systemMessage 则展示它
// TODO add by 芋艿:这个消息下面,不能有复制、删除按钮
if
(
activeConversation
.
value
?.
systemMessage
)
{
return
[{
id
:
0
,
type
:
'system'
,
content
:
activeConversation
.
value
.
systemMessage
}]
return
[
{
id
:
0
,
type
:
'system'
,
content
:
activeConversation
.
value
.
systemMessage
}
]
}
return
[]
})
...
...
@@ -438,13 +442,7 @@ const getMessageList = async () => {
return
}
// 获取列表数据
const
messageListRes
=
await
ChatMessageApi
.
messageList
(
activeConversationId
.
value
)
// 设置用户头像
messageListRes
.
map
(
item
=>
{
// 设置 role 默认头像
item
.
roleAvatar
=
item
.
roleAvatar
?
item
.
roleAvatar
:
defaultRoleAvatar
})
list
.
value
=
messageListRes
list
.
value
=
await
ChatMessageApi
.
messageList
(
activeConversationId
.
value
)
// 滚动到最下面
await
nextTick
(()
=>
{
// 滚动到最后
...
...
@@ -466,7 +464,6 @@ const openChatConversationUpdateForm = async () => {
chatConversationUpdateFormRef
.
value
.
open
(
activeConversationId
.
value
)
}
/**
* 对话 - 标题修改成功
*/
...
...
@@ -489,7 +486,7 @@ const handleConversationCreate = async () => {
const
handleConversationClick
=
async
(
conversation
:
ChatConversationVO
)
=>
{
// 对话进行中,不允许切换
if
(
conversationInProgress
.
value
)
{
await
message
.
alert
(
"对话中,不允许切换!"
)
await
message
.
alert
(
'对话中,不允许切换!'
)
return
false
}
...
...
@@ -512,7 +509,7 @@ const handleConversationClick = async (conversation: ChatConversationVO) => {
/**
* 对话 - 清理全部对话
*/
const
handlerConversationClear
=
async
()
=>
{
const
handlerConversationClear
=
async
()
=>
{
// TODO @fan:需要加一个 对话进行中,不允许切换
activeConversationId
.
value
=
null
activeConversation
.
value
=
null
...
...
@@ -596,7 +593,7 @@ const handlerMessageClear = async () => {
}
// TODO @fan:需要 try catch 下,不然点击取消会报异常
// 确认提示
await
message
.
delConfirm
(
"确认清空对话消息?"
)
await
message
.
delConfirm
(
'确认清空对话消息?'
)
// 清空对话
await
ChatMessageApi
.
deleteByConversationId
(
activeConversationId
.
value
as
string
)
// TODO @fan:是不是直接置空就好啦;
...
...
@@ -615,8 +612,6 @@ onMounted(async () => {
// 获取列表数据
listLoading
.
value
=
true
await
getMessageList
()
// 获取用户信息
userInfo
.
value
=
await
getUserProfile
()
})
</
script
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment