fix: 优化交互逻辑

This commit is contained in:
Wenjie Zhang 2026-03-09 01:23:15 +08:00
parent 4ea9f8b15e
commit 1dd4bd5988
9 changed files with 188 additions and 27 deletions

View File

@ -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

View File

@ -210,9 +210,6 @@ defineExpose({
font-weight: 500;
}
&:active {
transform: scale(0.95);
}
&.disabled {
opacity: 0.5;

View File

@ -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) => {

View File

@ -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;

View File

@ -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);
}
}
}
}

View File

@ -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 {

View File

@ -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
})

View File

@ -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: {

View File

@ -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>