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 return todoCount > 0 || fileCount > 0
}) })
// hasAgentStateContent false true
watch(hasAgentStateContent, (newVal, oldVal) => {
if (newVal && !oldVal) {
//
isAgentPanelOpen.value = true
}
})
const mentionConfig = computed(() => { const mentionConfig = computed(() => {
const rawFiles = currentAgentState.value?.files || {} const rawFiles = currentAgentState.value?.files || {}
const files = [] const files = []
@ -1273,20 +1281,25 @@ const sendMessage = async ({
// //
const isFirstChatEmpty = () => { const isFirstChatEmpty = () => {
if (threads.value.length === 0) return false if (threads.value.length === 0) return false
const firstThread = threads.value[0] const chatToReuse = getFirstNonPinnedChat(threads.value)
const firstThreadMessages = threadMessages.value[firstThread.id] || [] const messages = threadMessages.value[chatToReuse.id]
return firstThreadMessages.length === 0 // true
return messages !== undefined && messages.length === 0
} }
// //
const getFirstNonPinnedChat = (chatList) => { const getFirstNonPinnedChat = (chatList) => {
if (!chatList || chatList.length === 0) return null
return chatList.find((chat) => !chat.is_pinned) || chatList[0] return chatList.find((chat) => !chat.is_pinned) || chatList[0]
} }
// //
const switchToFirstChatIfEmpty = async () => { const switchToFirstChatIfEmpty = async () => {
if (threads.value.length > 0 && isFirstChatEmpty()) { 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 true
} }
return false return false

View File

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

View File

@ -137,7 +137,7 @@
</template> </template>
<script setup> <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 { Download, X, FolderCode, RefreshCw, Folder, FolderOpen } from 'lucide-vue-next'
import { import {
CheckCircleOutlined, CheckCircleOutlined,
@ -225,6 +225,7 @@ const checkOverflow = () => {
onMounted(() => { onMounted(() => {
nextTick(checkOverflow) nextTick(checkOverflow)
updateActiveTab()
}) })
onUpdated(() => { onUpdated(() => {
@ -262,6 +263,22 @@ const normalizedFiles = computed(() => {
return result 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 expandedKeys = ref([])
const buildTreeData = (filesList) => { const buildTreeData = (filesList) => {

View File

@ -153,10 +153,6 @@ const processImageUpload = async (file) => {
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
&:active {
transform: scale(0.98);
}
&.disabled { &.disabled {
cursor: not-allowed; cursor: not-allowed;
opacity: 0.5; opacity: 0.5;

View File

@ -39,7 +39,6 @@
@click.middle="handleMiddleClickDelete(chat.id)" @click.middle="handleMiddleClickDelete(chat.id)"
> >
<div class="conversation-title"> <div class="conversation-title">
<Pin v-if="chat.is_pinned" :size="14" class="pin-icon" />
<span>{{ chat.title || '新的对话' }}</span> <span>{{ chat.title || '新的对话' }}</span>
</div> </div>
<div class="actions-mask"></div> <div class="actions-mask"></div>
@ -70,9 +69,12 @@
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>
</template> </template>
<a-button type="text" class="more-btn" @click.stop> <div class="action-btn-wrapper">
<MoreVertical :size="16" /> <a-button type="text" class="more-btn" @click.stop>
</a-button> <MoreVertical :size="16" />
</a-button>
<Pin v-if="chat.is_pinned" :size="14" class="pinned-indicator" />
</div>
</a-dropdown> </a-dropdown>
</div> </div>
</div> </div>
@ -399,11 +401,6 @@ const togglePin = (chatId) => {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
.pin-icon {
flex-shrink: 0;
color: var(--main-color);
}
} }
.actions-mask { .actions-mask {
@ -428,30 +425,70 @@ const togglePin = (chatId) => {
opacity: 0; opacity: 0;
transition: opacity 0.3s ease; 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 { .more-btn {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
color: var(--gray-600); color: var(--gray-600);
background-color: transparent !important; background-color: transparent !important;
display: flex; display: none;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 0; padding: 0;
z-index: 1;
&:hover { &:hover {
color: var(--main-500); 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 { &:hover {
background-color: var(--gray-25); background-color: var(--gray-25);
.actions-mask { .actions-mask {
background: linear-gradient(to right, transparent, var(--gray-25) 20px); background: linear-gradient(to right, transparent, var(--gray-25) 20px);
opacity: 1;
} }
.actions-mask,
.conversation-actions { .conversation-actions {
opacity: 1; opacity: 1;
.more-btn {
display: flex;
}
.pinned-indicator {
display: none;
}
} }
} }
@ -462,8 +499,16 @@ const togglePin = (chatId) => {
color: var(--main-600); color: var(--main-600);
font-weight: 500; font-weight: 500;
} }
.actions-mask { .actions-mask {
background: linear-gradient(to right, transparent, var(--gray-50) 20px); 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 { &:active {
color: var(--main-color); color: var(--main-color);
transform: scale(0.95); //
} }
.anticon { .anticon {
@ -840,8 +840,8 @@ defineExpose({
} }
&:active { &:active {
transform: translateY(0);
box-shadow: 0 2px 4px var(--shadow-2); box-shadow: 0 2px 4px var(--shadow-2);
//
} }
&:disabled { &:disabled {

View File

@ -111,7 +111,8 @@ import {
Calculator, Calculator,
CheckSquare, CheckSquare,
Wrench, Wrench,
XCircle XCircle,
HelpCircle
} from 'lucide-vue-next' } from 'lucide-vue-next'
import { useAgentStore } from '@/stores/agent' import { useAgentStore } from '@/stores/agent'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
@ -169,6 +170,8 @@ const toolIcon = computed(() => {
if (name.includes('calc') || name.includes('math')) return Calculator if (name.includes('calc') || name.includes('math')) return Calculator
// //
if (name.includes('task') || name.includes('todo')) return CheckSquare if (name.includes('task') || name.includes('todo')) return CheckSquare
//
if (name.includes('ask_user_question') || name.includes('question')) return HelpCircle
// //
return Wrench return Wrench
}) })

View File

@ -62,6 +62,9 @@
<!-- MySQL 列出表 --> <!-- MySQL 列出表 -->
<MysqlListTablesTool v-else-if="toolName === 'mysql_list_tables'" :tool-call="toolCall" /> <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" /> <BaseToolCall v-else :tool-call="toolCall" />
</template> </template>
@ -89,6 +92,7 @@ import EditFileTool from './tools/EditFileTool.vue'
import MysqlQueryTool from './tools/MysqlQueryTool.vue' import MysqlQueryTool from './tools/MysqlQueryTool.vue'
import MysqlDescribeTableTool from './tools/MysqlDescribeTableTool.vue' import MysqlDescribeTableTool from './tools/MysqlDescribeTableTool.vue'
import MysqlListTablesTool from './tools/MysqlListTablesTool.vue' import MysqlListTablesTool from './tools/MysqlListTablesTool.vue'
import AskUserQuestionTool from './tools/AskUserQuestionTool.vue'
const props = defineProps({ const props = defineProps({
toolCall: { 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>