feat(chat): 增加对话配置变更提示,优化用户体验
This commit is contained in:
parent
4035f3c02d
commit
1b053f3cd4
@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
### BREAKING CHANGE(不兼容变更,0.7 版本再实现)
|
### BREAKING CHANGE(不兼容变更,0.7 版本再实现)
|
||||||
- 将自定义provider 的实现逻辑,从文件移动到数据库中,并将相关处理代码,移出 config 文件,放到 provider 模块中
|
- 将自定义provider 的实现逻辑,从文件移动到数据库中,并将相关处理代码,移出 config 文件,放到 provider 模块中
|
||||||
|
- 已补充方案文档:`docs/vibe/2026-04-18-custom-provider-db-refactor-plan.md`,明确采用“provider 一行、models 放 JSON、移除 provider 默认模型”的落地方案
|
||||||
- 优化知识库的 API 接口设计,使用 /{db_id}/xxx 的形式,整合 mindmap / eval 接口
|
- 优化知识库的 API 接口设计,使用 /{db_id}/xxx 的形式,整合 mindmap / eval 接口
|
||||||
|
|
||||||
|
|
||||||
@ -54,6 +55,7 @@
|
|||||||
- 调整智能体对话中的工具调用展示:连续工具调用默认折叠为“调用了 N 个工具”的轻量摘要,展开后改为弱化时间线样式,减少工具结果卡片对正文阅读节奏的干扰。
|
- 调整智能体对话中的工具调用展示:连续工具调用默认折叠为“调用了 N 个工具”的轻量摘要,展开后改为弱化时间线样式,减少工具结果卡片对正文阅读节奏的干扰。
|
||||||
- 调整聊天首页的智能体切换入口:在无历史对话时,智能体数量 `<= 3` 且 `chat-main` 宽度不小于 `380px` 时继续使用横向 segmented;当智能体数量 `>= 4` 或内容区宽度小于 `380px` 时自动收敛为“当前智能体 + 下拉按钮”形式。
|
- 调整聊天首页的智能体切换入口:在无历史对话时,智能体数量 `<= 3` 且 `chat-main` 宽度不小于 `380px` 时继续使用横向 segmented;当智能体数量 `>= 4` 或内容区宽度小于 `380px` 时自动收敛为“当前智能体 + 下拉按钮”形式。
|
||||||
- 调整对话配置入口与侧边栏头尾交互:输入区配置按钮改为轻量 dropdown 触发器,默认保持透明、hover 才出现背景;下拉中直接提供配置切换、新建配置,以及查看/编辑当前配置的入口。配置侧边栏则移除头部配置切换和新建入口,改为仅显示当前配置名称、可点击星标和关闭图标,并将删除操作下沉到底部保存按钮右侧。
|
- 调整对话配置入口与侧边栏头尾交互:输入区配置按钮改为轻量 dropdown 触发器,默认保持透明、hover 才出现背景;下拉中直接提供配置切换、新建配置,以及查看/编辑当前配置的入口。配置侧边栏则移除头部配置切换和新建入口,改为仅显示当前配置名称、可点击星标和关闭图标,并将删除操作下沉到底部保存按钮右侧。
|
||||||
|
- 为历史线程补充前端本地配置变更提示:当已有历史消息的对话中切换 Agent、切换配置或编辑任意配置项时,在聊天消息流中插入一条非持久化的信息提示,提醒继续在当前线程运行可能影响最终效果,建议新建对话;提示仅保留在前端当前线程状态中,不写入后端历史消息。后续补齐了 3 个关键边界:提示插入时主动滚动到底部确保可见、历史消息尚未加载完成时先挂起提示并在加载完成后落地、流式回复过程中切换配置时将提示锚定在当时完整对话流之后,避免把正在生成的回复错误归因到新配置。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -85,32 +85,42 @@
|
|||||||
<!-- Main Chat Area -->
|
<!-- Main Chat Area -->
|
||||||
<div class="chat-main" ref="chatMainRef">
|
<div class="chat-main" ref="chatMainRef">
|
||||||
<div class="chat-box">
|
<div class="chat-box">
|
||||||
<div class="conv-box" v-for="(conv, index) in conversations" :key="index">
|
<template v-for="row in conversationRows" :key="row.key">
|
||||||
<template v-for="(displayItem, itemIndex) in getConversationDisplayItems(conv)" :key="displayItem.key">
|
<div v-if="row.type === 'conversation'" class="conv-box">
|
||||||
<AgentMessageComponent
|
<template
|
||||||
v-if="displayItem.type === 'message'"
|
v-for="(displayItem, itemIndex) in getConversationDisplayItems(row.conv)"
|
||||||
:message="displayItem.message"
|
:key="displayItem.key"
|
||||||
:is-processing="isDisplayMessageProcessing(conv, displayItem)"
|
|
||||||
:show-refs="showMsgRefs(displayItem.message)"
|
|
||||||
:hide-tool-calls="true"
|
|
||||||
@retry="retryMessage(displayItem.message)"
|
|
||||||
>
|
>
|
||||||
</AgentMessageComponent>
|
<AgentMessageComponent
|
||||||
<ToolCallsGroupComponent
|
v-if="displayItem.type === 'message'"
|
||||||
v-else
|
:message="displayItem.message"
|
||||||
:tool-calls="displayItem.toolCalls"
|
:is-processing="isDisplayMessageProcessing(row.conv, displayItem)"
|
||||||
:is-active="isToolGroupActive(conv, itemIndex, getConversationDisplayItems(conv))"
|
:show-refs="showMsgRefs(displayItem.message)"
|
||||||
|
:hide-tool-calls="true"
|
||||||
|
@retry="retryMessage(displayItem.message)"
|
||||||
|
>
|
||||||
|
</AgentMessageComponent>
|
||||||
|
<ToolCallsGroupComponent
|
||||||
|
v-else
|
||||||
|
:tool-calls="displayItem.toolCalls"
|
||||||
|
:is-active="
|
||||||
|
isToolGroupActive(row.conv, itemIndex, getConversationDisplayItems(row.conv))
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<!-- 显示对话最后一个消息使用的模型 -->
|
||||||
|
<RefsComponent
|
||||||
|
v-if="shouldShowRefs(row.conv)"
|
||||||
|
:message="getLastMessage(row.conv)"
|
||||||
|
:show-refs="['model', 'copy', 'sources']"
|
||||||
|
:is-latest-message="false"
|
||||||
|
:sources="getConversationSources(row.conv)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</div>
|
||||||
<!-- 显示对话最后一个消息使用的模型 -->
|
<div v-else class="chat-inline-notice">
|
||||||
<RefsComponent
|
<span>{{ row.notice.message }}</span>
|
||||||
v-if="shouldShowRefs(conv)"
|
</div>
|
||||||
:message="getLastMessage(conv)"
|
</template>
|
||||||
:show-refs="['model', 'copy', 'sources']"
|
|
||||||
:is-latest-message="false"
|
|
||||||
:sources="getConversationSources(conv)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 生成中的加载状态 - 增强条件支持主聊天和resume流程 -->
|
<!-- 生成中的加载状态 - 增强条件支持主聊天和resume流程 -->
|
||||||
<div class="generating-status" v-if="isProcessing && conversations.length > 0">
|
<div class="generating-status" v-if="isProcessing && conversations.length > 0">
|
||||||
@ -394,6 +404,11 @@ const hasMoreChats = ref(true) // 是否还有更多对话可加载
|
|||||||
const isLoadingMoreChats = ref(false) // 加载更多对话中
|
const isLoadingMoreChats = ref(false) // 加载更多对话中
|
||||||
const threadFilesMap = ref({})
|
const threadFilesMap = ref({})
|
||||||
const threadAttachmentsMap = ref({})
|
const threadAttachmentsMap = ref({})
|
||||||
|
const threadConfigNoticeMap = ref({})
|
||||||
|
const threadPendingConfigNoticeMap = ref({})
|
||||||
|
const threadConfigSnapshotMap = ref({})
|
||||||
|
const configNoticeSyncDepth = ref(0)
|
||||||
|
const configNoticeScrollVersion = ref(0)
|
||||||
|
|
||||||
// 本地 UI 状态(仅在本组件使用)
|
// 本地 UI 状态(仅在本组件使用)
|
||||||
const localUIState = reactive({
|
const localUIState = reactive({
|
||||||
@ -508,6 +523,11 @@ const { mentionConfig } = useAgentMentionConfig({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const currentThreadMessages = computed(() => threadMessages.value[currentChatId.value] || [])
|
const currentThreadMessages = computed(() => threadMessages.value[currentChatId.value] || [])
|
||||||
|
const currentThreadHasHistory = computed(() => currentThreadMessages.value.length > 0)
|
||||||
|
const currentThreadConfigNotice = computed(() => {
|
||||||
|
if (!currentChatId.value) return null
|
||||||
|
return threadConfigNoticeMap.value[currentChatId.value] || null
|
||||||
|
})
|
||||||
|
|
||||||
// 计算是否显示Refs组件的条件
|
// 计算是否显示Refs组件的条件
|
||||||
const shouldShowRefs = computed(() => {
|
const shouldShowRefs = computed(() => {
|
||||||
@ -560,6 +580,28 @@ const conversations = computed(() => {
|
|||||||
return historyConvs
|
return historyConvs
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const conversationRows = computed(() => {
|
||||||
|
const rows = conversations.value.map((conv, index) => ({
|
||||||
|
type: 'conversation',
|
||||||
|
key: conv.status === 'streaming' ? 'ongoing-conversation' : `history-${index}`,
|
||||||
|
conv
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (currentThreadConfigNotice.value) {
|
||||||
|
const insertAfterCount = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(Number(currentThreadConfigNotice.value.insertAfterConversationCount) || 0, rows.length)
|
||||||
|
)
|
||||||
|
rows.splice(insertAfterCount, 0, {
|
||||||
|
type: 'notice',
|
||||||
|
key: currentThreadConfigNotice.value.id,
|
||||||
|
notice: currentThreadConfigNotice.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows
|
||||||
|
})
|
||||||
|
|
||||||
// 智能体图标映射
|
// 智能体图标映射
|
||||||
const agentIconMap = {
|
const agentIconMap = {
|
||||||
ChatbotAgent: Bot,
|
ChatbotAgent: Bot,
|
||||||
@ -629,6 +671,143 @@ const startSendCooldown = () => {
|
|||||||
}, 2000)
|
}, 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CONFIG_CHANGE_NOTICE_MESSAGE =
|
||||||
|
'在运行过程中切换或修改配置可能会影响最终效果,建议新建一个对话。'
|
||||||
|
|
||||||
|
const withConfigNoticeSync = async (task) => {
|
||||||
|
configNoticeSyncDepth.value += 1
|
||||||
|
try {
|
||||||
|
return await task()
|
||||||
|
} finally {
|
||||||
|
configNoticeSyncDepth.value = Math.max(0, configNoticeSyncDepth.value - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildThreadConfigSnapshot = () => {
|
||||||
|
return {
|
||||||
|
agentId: currentAgentId.value || '',
|
||||||
|
agentConfigId: selectedAgentConfigId.value ?? null,
|
||||||
|
configJson: JSON.stringify(agentConfig.value || {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearThreadConfigTracking = (threadId) => {
|
||||||
|
if (!threadId) return
|
||||||
|
|
||||||
|
const nextNotices = { ...threadConfigNoticeMap.value }
|
||||||
|
delete nextNotices[threadId]
|
||||||
|
threadConfigNoticeMap.value = nextNotices
|
||||||
|
|
||||||
|
const nextPendingNotices = { ...threadPendingConfigNoticeMap.value }
|
||||||
|
delete nextPendingNotices[threadId]
|
||||||
|
threadPendingConfigNoticeMap.value = nextPendingNotices
|
||||||
|
|
||||||
|
const nextSnapshots = { ...threadConfigSnapshotMap.value }
|
||||||
|
delete nextSnapshots[threadId]
|
||||||
|
threadConfigSnapshotMap.value = nextSnapshots
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncThreadConfigSnapshot = (threadId, options = {}) => {
|
||||||
|
if (!threadId) return
|
||||||
|
|
||||||
|
const { overwrite = true } = options
|
||||||
|
if (!overwrite && threadConfigSnapshotMap.value[threadId]) return
|
||||||
|
if (threadPendingConfigNoticeMap.value[threadId]) return
|
||||||
|
|
||||||
|
// 线程切换时先记录当前 UI 的配置快照,避免同步 thread 绑定配置时误报。
|
||||||
|
threadConfigSnapshotMap.value = {
|
||||||
|
...threadConfigSnapshotMap.value,
|
||||||
|
[threadId]: buildThreadConfigSnapshot()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const upsertThreadConfigNotice = (threadId, insertAfterConversationCount) => {
|
||||||
|
if (!threadId) return
|
||||||
|
|
||||||
|
const existingNotice = threadConfigNoticeMap.value[threadId]
|
||||||
|
const nextNotice = {
|
||||||
|
id: existingNotice?.id || `config-change-notice-${threadId}`,
|
||||||
|
message: existingNotice?.message || CONFIG_CHANGE_NOTICE_MESSAGE,
|
||||||
|
insertAfterConversationCount
|
||||||
|
}
|
||||||
|
const shouldScroll =
|
||||||
|
!existingNotice || existingNotice.insertAfterConversationCount !== insertAfterConversationCount
|
||||||
|
|
||||||
|
threadConfigNoticeMap.value = {
|
||||||
|
...threadConfigNoticeMap.value,
|
||||||
|
[threadId]: nextNotice
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threadPendingConfigNoticeMap.value[threadId]) {
|
||||||
|
const nextPendingNotices = { ...threadPendingConfigNoticeMap.value }
|
||||||
|
delete nextPendingNotices[threadId]
|
||||||
|
threadPendingConfigNoticeMap.value = nextPendingNotices
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldScroll) {
|
||||||
|
configNoticeScrollVersion.value += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const queuePendingThreadConfigNotice = (threadId) => {
|
||||||
|
if (!threadId) return
|
||||||
|
threadPendingConfigNoticeMap.value = {
|
||||||
|
...threadPendingConfigNoticeMap.value,
|
||||||
|
[threadId]: {
|
||||||
|
id: `config-change-notice-${threadId}`,
|
||||||
|
message: CONFIG_CHANGE_NOTICE_MESSAGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const flushPendingThreadConfigNotice = (threadId) => {
|
||||||
|
if (!threadId || !currentThreadHasHistory.value || !threadPendingConfigNoticeMap.value[threadId]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
upsertThreadConfigNotice(threadId, conversations.value.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
const maybeInsertThreadConfigNotice = () => {
|
||||||
|
const threadId = currentChatId.value
|
||||||
|
if (!threadId || configNoticeSyncDepth.value > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousSnapshot = threadConfigSnapshotMap.value[threadId]
|
||||||
|
const currentSnapshot = buildThreadConfigSnapshot()
|
||||||
|
|
||||||
|
if (!previousSnapshot) {
|
||||||
|
threadConfigSnapshotMap.value = {
|
||||||
|
...threadConfigSnapshotMap.value,
|
||||||
|
[threadId]: currentSnapshot
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
previousSnapshot.agentId === currentSnapshot.agentId &&
|
||||||
|
previousSnapshot.agentConfigId === currentSnapshot.agentConfigId &&
|
||||||
|
previousSnapshot.configJson === currentSnapshot.configJson
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentThreadHasHistory.value) {
|
||||||
|
upsertThreadConfigNotice(threadId, conversations.value.length)
|
||||||
|
} else if (chatUIStore.isLoadingMessages) {
|
||||||
|
// 历史线程仍在加载时先挂起提示,避免消息返回后把变更误当成新的基线。
|
||||||
|
queuePendingThreadConfigNotice(threadId)
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
threadConfigSnapshotMap.value = {
|
||||||
|
...threadConfigSnapshotMap.value,
|
||||||
|
[threadId]: currentSnapshot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== SCROLL & RESIZE HANDLING ====================
|
// ==================== SCROLL & RESIZE HANDLING ====================
|
||||||
const scrollController = new ScrollController('.chat-main')
|
const scrollController = new ScrollController('.chat-main')
|
||||||
const chatMainRef = ref(null)
|
const chatMainRef = ref(null)
|
||||||
@ -798,6 +977,7 @@ const deleteThread = async (threadId) => {
|
|||||||
delete threadMessages.value[threadId]
|
delete threadMessages.value[threadId]
|
||||||
delete threadFilesMap.value[threadId]
|
delete threadFilesMap.value[threadId]
|
||||||
delete threadAttachmentsMap.value[threadId]
|
delete threadAttachmentsMap.value[threadId]
|
||||||
|
clearThreadConfigTracking(threadId)
|
||||||
|
|
||||||
if (chatState.currentThreadId === threadId) {
|
if (chatState.currentThreadId === threadId) {
|
||||||
chatState.currentThreadId = null
|
chatState.currentThreadId = null
|
||||||
@ -1087,21 +1267,18 @@ const selectChat = async (chatId) => {
|
|||||||
isAgentPanelOpen.value = false
|
isAgentPanelOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 先更新当前线程,确保底部智能体名称与选中项即时同步
|
|
||||||
chatState.currentThreadId = chatId
|
|
||||||
|
|
||||||
if (!props.singleMode && targetChat?.agent_id && targetChat.agent_id !== currentAgentId.value) {
|
|
||||||
try {
|
|
||||||
await agentStore.selectAgent(targetChat.agent_id)
|
|
||||||
} catch (error) {
|
|
||||||
chatState.currentThreadId = previousThreadId
|
|
||||||
handleChatError(error, 'load')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await syncSelectedConfigForThread(targetChat)
|
await withConfigNoticeSync(async () => {
|
||||||
|
// 先更新当前线程,确保底部智能体名称与选中项即时同步。
|
||||||
|
chatState.currentThreadId = chatId
|
||||||
|
|
||||||
|
if (!props.singleMode && targetChat?.agent_id && targetChat.agent_id !== currentAgentId.value) {
|
||||||
|
await agentStore.selectAgent(targetChat.agent_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
await syncSelectedConfigForThread(targetChat)
|
||||||
|
syncThreadConfigSnapshot(chatId)
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
chatState.currentThreadId = previousThreadId
|
chatState.currentThreadId = previousThreadId
|
||||||
handleChatError(error, 'load')
|
handleChatError(error, 'load')
|
||||||
@ -1121,6 +1298,7 @@ const selectChat = async (chatId) => {
|
|||||||
scrollController.scrollToBottomStaticForce()
|
scrollController.scrollToBottomStaticForce()
|
||||||
// await fetchAgentState(targetAgentId, chatId)
|
// await fetchAgentState(targetAgentId, chatId)
|
||||||
await handleAgentStateRefresh(chatId)
|
await handleAgentStateRefresh(chatId)
|
||||||
|
syncThreadConfigSnapshot(chatId, { overwrite: false })
|
||||||
await resumeActiveRunForThread(chatId)
|
await resumeActiveRunForThread(chatId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1778,6 +1956,35 @@ watch(
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
currentThreadMessages,
|
||||||
|
() => {
|
||||||
|
if (currentThreadHasHistory.value) {
|
||||||
|
flushPendingThreadConfigNotice(currentChatId.value)
|
||||||
|
syncThreadConfigSnapshot(currentChatId.value, { overwrite: false })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: false }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(currentAgentId, (newAgentId, oldAgentId) => {
|
||||||
|
if (oldAgentId === undefined || newAgentId === oldAgentId) return
|
||||||
|
maybeInsertThreadConfigNotice()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(selectedAgentConfigId, (newConfigId, oldConfigId) => {
|
||||||
|
if (oldConfigId === undefined || newConfigId === oldConfigId) return
|
||||||
|
maybeInsertThreadConfigNotice()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => JSON.stringify(agentConfig.value || {}),
|
||||||
|
(newConfigJson, oldConfigJson) => {
|
||||||
|
if (oldConfigJson === undefined || newConfigJson === oldConfigJson) return
|
||||||
|
maybeInsertThreadConfigNotice()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
conversations,
|
conversations,
|
||||||
() => {
|
() => {
|
||||||
@ -1788,6 +1995,15 @@ watch(
|
|||||||
{ deep: true, flush: 'post' }
|
{ deep: true, flush: 'post' }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
configNoticeScrollVersion,
|
||||||
|
() => {
|
||||||
|
if (!currentChatId.value) return
|
||||||
|
scrollController.scrollToBottom(true)
|
||||||
|
},
|
||||||
|
{ flush: 'post' }
|
||||||
|
)
|
||||||
|
|
||||||
watch(currentChatId, (threadId, oldThreadId) => {
|
watch(currentChatId, (threadId, oldThreadId) => {
|
||||||
if (threadId === oldThreadId) return
|
if (threadId === oldThreadId) return
|
||||||
emit('thread-change', threadId || '')
|
emit('thread-change', threadId || '')
|
||||||
@ -2176,6 +2392,16 @@ watch(currentChatId, (threadId, oldThreadId) => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-inline-notice {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 6px 16px 12px;
|
||||||
|
color: var(--gray-500);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.bottom {
|
.bottom {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user