fix: 优化交互逻辑
This commit is contained in:
parent
4ea9f8b15e
commit
1dd4bd5988
@ -381,6 +381,14 @@ const hasAgentStateContent = computed(() => {
|
||||
return todoCount > 0 || fileCount > 0
|
||||
})
|
||||
|
||||
// 监听 hasAgentStateContent 从 false → true 时,自动展开面板
|
||||
watch(hasAgentStateContent, (newVal, oldVal) => {
|
||||
if (newVal && !oldVal) {
|
||||
// 从无状态变为有状态时,自动展开面板
|
||||
isAgentPanelOpen.value = true
|
||||
}
|
||||
})
|
||||
|
||||
const mentionConfig = computed(() => {
|
||||
const rawFiles = currentAgentState.value?.files || {}
|
||||
const files = []
|
||||
@ -1273,20 +1281,25 @@ const sendMessage = async ({
|
||||
// 检查第一个对话是否为空
|
||||
const isFirstChatEmpty = () => {
|
||||
if (threads.value.length === 0) return false
|
||||
const firstThread = threads.value[0]
|
||||
const firstThreadMessages = threadMessages.value[firstThread.id] || []
|
||||
return firstThreadMessages.length === 0
|
||||
const chatToReuse = getFirstNonPinnedChat(threads.value)
|
||||
const messages = threadMessages.value[chatToReuse.id]
|
||||
// 只有当消息已加载且为空时才返回 true
|
||||
return messages !== undefined && messages.length === 0
|
||||
}
|
||||
|
||||
// 获取第一个非置顶的对话
|
||||
const getFirstNonPinnedChat = (chatList) => {
|
||||
if (!chatList || chatList.length === 0) return null
|
||||
return chatList.find((chat) => !chat.is_pinned) || chatList[0]
|
||||
}
|
||||
|
||||
// 如果第一个对话为空,直接切换到第一个非置顶对话
|
||||
const switchToFirstChatIfEmpty = async () => {
|
||||
if (threads.value.length > 0 && isFirstChatEmpty()) {
|
||||
await selectChat(getFirstNonPinnedChat(threads.value).id)
|
||||
const chatToReuse = getFirstNonPinnedChat(threads.value)
|
||||
if (chatState.currentThreadId !== chatToReuse.id) {
|
||||
await selectChat(chatToReuse.id)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
@ -210,9 +210,6 @@ defineExpose({
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
|
||||
@ -137,7 +137,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, onMounted, onUpdated, nextTick } from 'vue'
|
||||
import { computed, ref, onMounted, onUpdated, nextTick, watch } from 'vue'
|
||||
import { Download, X, FolderCode, RefreshCw, Folder, FolderOpen } from 'lucide-vue-next'
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
@ -225,6 +225,7 @@ const checkOverflow = () => {
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(checkOverflow)
|
||||
updateActiveTab()
|
||||
})
|
||||
|
||||
onUpdated(() => {
|
||||
@ -262,6 +263,22 @@ const normalizedFiles = computed(() => {
|
||||
return result
|
||||
})
|
||||
|
||||
// 自动切换 tab 逻辑:如果 files 为空且有 todos,自动切换到 todos tab
|
||||
const updateActiveTab = () => {
|
||||
const fileList = normalizedFiles.value
|
||||
const todoList = todos.value
|
||||
|
||||
// 如果当前是 files tab,但文件为空且有 todos,切换到 todos
|
||||
if (activeTab.value === 'files' && fileList.length === 0 && todoList.length > 0) {
|
||||
activeTab.value = 'todos'
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 files 和 todos 变化,自动切换 tab
|
||||
watch([() => props.agentState?.files, () => props.agentState?.todos], () => {
|
||||
updateActiveTab()
|
||||
}, { deep: true })
|
||||
|
||||
const expandedKeys = ref([])
|
||||
|
||||
const buildTreeData = (filesList) => {
|
||||
|
||||
@ -153,10 +153,6 @@ const processImageUpload = async (file) => {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
|
||||
@ -39,7 +39,6 @@
|
||||
@click.middle="handleMiddleClickDelete(chat.id)"
|
||||
>
|
||||
<div class="conversation-title">
|
||||
<Pin v-if="chat.is_pinned" :size="14" class="pin-icon" />
|
||||
<span>{{ chat.title || '新的对话' }}</span>
|
||||
</div>
|
||||
<div class="actions-mask"></div>
|
||||
@ -70,9 +69,12 @@
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<a-button type="text" class="more-btn" @click.stop>
|
||||
<MoreVertical :size="16" />
|
||||
</a-button>
|
||||
<div class="action-btn-wrapper">
|
||||
<a-button type="text" class="more-btn" @click.stop>
|
||||
<MoreVertical :size="16" />
|
||||
</a-button>
|
||||
<Pin v-if="chat.is_pinned" :size="14" class="pinned-indicator" />
|
||||
</div>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
@ -399,11 +401,6 @@ const togglePin = (chatId) => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.pin-icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
|
||||
.actions-mask {
|
||||
@ -428,30 +425,70 @@ const togglePin = (chatId) => {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
.action-btn-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pinned-indicator {
|
||||
color: var(--main-color);
|
||||
opacity: 0.8;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.more-btn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--gray-600);
|
||||
background-color: transparent !important;
|
||||
display: flex;
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
z-index: 1;
|
||||
|
||||
&:hover {
|
||||
color: var(--main-500);
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 默认显示置顶图标
|
||||
&:has(.pinned-indicator) {
|
||||
.conversation-actions,
|
||||
.actions-mask {
|
||||
opacity: 1;
|
||||
}
|
||||
.actions-mask {
|
||||
background: linear-gradient(to right, transparent, var(--bg-sider) 30px);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--gray-25);
|
||||
|
||||
.actions-mask {
|
||||
background: linear-gradient(to right, transparent, var(--gray-25) 20px);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.actions-mask,
|
||||
.conversation-actions {
|
||||
opacity: 1;
|
||||
|
||||
.more-btn {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.pinned-indicator {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -462,8 +499,16 @@ const togglePin = (chatId) => {
|
||||
color: var(--main-600);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.actions-mask {
|
||||
background: linear-gradient(to right, transparent, var(--gray-50) 20px);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:has(.pinned-indicator) {
|
||||
.actions-mask {
|
||||
background: linear-gradient(to right, transparent, var(--gray-50) 30px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -769,7 +769,7 @@ defineExpose({
|
||||
|
||||
&:active {
|
||||
color: var(--main-color);
|
||||
transform: scale(0.95);
|
||||
// 移除点击缩小效果
|
||||
}
|
||||
|
||||
.anticon {
|
||||
@ -840,8 +840,8 @@ defineExpose({
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 4px var(--shadow-2);
|
||||
// 移除点击动画效果
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
|
||||
@ -111,7 +111,8 @@ import {
|
||||
Calculator,
|
||||
CheckSquare,
|
||||
Wrench,
|
||||
XCircle
|
||||
XCircle,
|
||||
HelpCircle
|
||||
} from 'lucide-vue-next'
|
||||
import { useAgentStore } from '@/stores/agent'
|
||||
import { storeToRefs } from 'pinia'
|
||||
@ -169,6 +170,8 @@ const toolIcon = computed(() => {
|
||||
if (name.includes('calc') || name.includes('math')) return Calculator
|
||||
// 任务
|
||||
if (name.includes('task') || name.includes('todo')) return CheckSquare
|
||||
// 向用户提问
|
||||
if (name.includes('ask_user_question') || name.includes('question')) return HelpCircle
|
||||
// 默认
|
||||
return Wrench
|
||||
})
|
||||
|
||||
@ -62,6 +62,9 @@
|
||||
<!-- MySQL 列出表 -->
|
||||
<MysqlListTablesTool v-else-if="toolName === 'mysql_list_tables'" :tool-call="toolCall" />
|
||||
|
||||
<!-- 向用户提问 -->
|
||||
<AskUserQuestionTool v-else-if="toolName === 'ask_user_question'" :tool-call="toolCall" />
|
||||
|
||||
<!-- 默认展示 -->
|
||||
<BaseToolCall v-else :tool-call="toolCall" />
|
||||
</template>
|
||||
@ -89,6 +92,7 @@ import EditFileTool from './tools/EditFileTool.vue'
|
||||
import MysqlQueryTool from './tools/MysqlQueryTool.vue'
|
||||
import MysqlDescribeTableTool from './tools/MysqlDescribeTableTool.vue'
|
||||
import MysqlListTablesTool from './tools/MysqlListTablesTool.vue'
|
||||
import AskUserQuestionTool from './tools/AskUserQuestionTool.vue'
|
||||
|
||||
const props = defineProps({
|
||||
toolCall: {
|
||||
|
||||
@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<BaseToolCall :tool-call="toolCall" hide-params>
|
||||
<template #header>
|
||||
<div class="sep-header">
|
||||
<span class="note">提问</span>
|
||||
<span class="separator">|</span>
|
||||
<span class="description">{{ shortQuestion }}</span>
|
||||
<span v-if="userAnswer" class="tag tag-answered">
|
||||
已回答: {{ displayAnswer }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</BaseToolCall>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import BaseToolCall from '../BaseToolCall.vue'
|
||||
|
||||
const props = defineProps({
|
||||
toolCall: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
// 解析参数
|
||||
const parsedArgs = computed(() => {
|
||||
const args = props.toolCall.args || props.toolCall.function?.arguments
|
||||
if (!args) return {}
|
||||
if (typeof args === 'object') return args
|
||||
try {
|
||||
return JSON.parse(args)
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
})
|
||||
|
||||
// 解析结果
|
||||
const parsedResult = computed(() => {
|
||||
const content = props.toolCall.tool_call_result?.content
|
||||
if (!content) return null
|
||||
if (typeof content === 'object') return content
|
||||
try {
|
||||
return JSON.parse(content)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
const question = computed(() => parsedArgs.value.question || '')
|
||||
const shortQuestion = computed(() => {
|
||||
const q = question.value
|
||||
return q.length > 50 ? q.slice(0, 50) + '...' : q
|
||||
})
|
||||
|
||||
// 用户答案
|
||||
const userAnswer = computed(() => {
|
||||
const result = parsedResult.value
|
||||
if (!result) return null
|
||||
return result.user_answer || result.answer || null
|
||||
})
|
||||
|
||||
// 显示答案
|
||||
const displayAnswer = computed(() => {
|
||||
const answer = userAnswer.value
|
||||
if (!answer) return ''
|
||||
if (Array.isArray(answer)) {
|
||||
return answer.join(', ')
|
||||
}
|
||||
return String(answer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.sep-header {
|
||||
.tag-answered {
|
||||
margin-left: 8px;
|
||||
color: var(--green-600);
|
||||
background: var(--green-50);
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue
Block a user