feat: 智能体对话界面优化

- 消息列表修改为所有的对话,不区分智能体
- 智能体会在新建对话的时候切换
- 配置选择从右上角移动到输入框
- 其他细节优化
This commit is contained in:
Wenjie Zhang 2026-03-16 21:39:49 +08:00
parent 06cc8cbf7a
commit 1764ffedb0
8 changed files with 236 additions and 429 deletions

View File

@ -746,7 +746,7 @@ async def create_thread(
@chat.get("/threads", response_model=list[ThreadResponse]) @chat.get("/threads", response_model=list[ThreadResponse])
async def list_threads( async def list_threads(
agent_id: str, agent_id: str | None = Query(None),
limit: int = Query(100, ge=1, le=500), limit: int = Query(100, ge=1, le=500),
offset: int = Query(0, ge=0), offset: int = Query(0, ge=0),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),

View File

@ -153,15 +153,12 @@ async def create_thread_view(
async def list_threads_view( async def list_threads_view(
*, *,
agent_id: str, agent_id: str | None,
db: AsyncSession, db: AsyncSession,
current_user_id: str, current_user_id: str,
limit: int | None = None, limit: int | None = None,
offset: int = 0, offset: int = 0,
) -> list[dict]: ) -> list[dict]:
if not agent_id:
raise HTTPException(status_code=422, detail="agent_id 不能为空")
conv_repo = ConversationRepository(db) conv_repo = ConversationRepository(db)
conversations = await conv_repo.list_conversations( conversations = await conv_repo.list_conversations(
user_id=str(current_user_id), user_id=str(current_user_id),

View File

@ -285,13 +285,20 @@ export const multimodalApi = {
export const threadApi = { export const threadApi = {
/** /**
* 获取对话线程列表 * 获取对话线程列表
* @param {string} agentId - 智能体ID * @param {string | null | undefined} agentId - 智能体ID可选不传时返回全部智能体对话
* @param {number} limit - 返回数量限制默认100 * @param {number} limit - 返回数量限制默认100
* @param {number} offset - 偏移量默认0 * @param {number} offset - 偏移量默认0
* @returns {Promise} - 对话线程列表 * @returns {Promise} - 对话线程列表
*/ */
getThreads: (agentId, limit = 100, offset = 0) => { getThreads: (agentId = null, limit = 100, offset = 0) => {
const url = `/api/chat/threads?agent_id=${agentId}&limit=${limit}&offset=${offset}` const params = new URLSearchParams({
limit: String(limit),
offset: String(offset)
})
if (agentId) {
params.set('agent_id', agentId)
}
const url = `/api/chat/threads?${params.toString()}`
return apiGet(url) return apiGet(url)
}, },

View File

@ -17,7 +17,6 @@
@rename-chat="renameChat" @rename-chat="renameChat"
@toggle-pin="togglePinChat" @toggle-pin="togglePinChat"
@toggle-sidebar="toggleSidebar" @toggle-sidebar="toggleSidebar"
@open-agent-modal="openAgentModal"
@load-more-chats="loadMoreChats" @load-more-chats="loadMoreChats"
:class="{ :class="{
'sidebar-open': chatUIStore.isSidebarOpen, 'sidebar-open': chatUIStore.isSidebarOpen,
@ -51,14 +50,6 @@
<MessageCirclePlus v-else class="nav-btn-icon" size="16" /> <MessageCirclePlus v-else class="nav-btn-icon" size="16" />
<span class="text">新对话</span> <span class="text">新对话</span>
</div> </div>
<div v-if="!props.singleMode" class="agent-nav-btn" @click="openAgentModal">
<LoaderCircle v-if="!currentAgent" class="nav-btn-icon loading-icon" size="18" />
<Bot v-else :size="18" class="nav-btn-icon" />
<span class="text hide-text">
{{ currentAgentName || '选择智能体' }}
</span>
<ChevronDown size="16" class="switch-icon" />
</div>
</div> </div>
<div class="header__right"> <div class="header__right">
<!-- AgentState 显示按钮已移动到输入框底部 --> <!-- AgentState 显示按钮已移动到输入框底部 -->
@ -131,6 +122,14 @@
<h1>👋 您好我是{{ currentAgentName }}</h1> <h1>👋 您好我是{{ currentAgentName }}</h1>
</div> </div>
<div v-if="showStartAgentSegment" class="agent-segment-wrapper">
<a-segmented
:value="currentAgentId"
:options="agentSegmentOptions"
@change="handleStartAgentChange"
/>
</div>
<AgentInputArea <AgentInputArea
ref="messageInputRef" ref="messageInputRef"
v-model="userInput" v-model="userInput"
@ -148,7 +147,11 @@
@send="handleSendOrStop" @send="handleSendOrStop"
@attachment-changed="handleAgentStateRefresh" @attachment-changed="handleAgentStateRefresh"
@toggle-panel="toggleAgentPanel" @toggle-panel="toggleAgentPanel"
/> >
<template #actions-left-extra>
<slot name="input-actions-left"></slot>
</template>
</AgentInputArea>
<!-- 示例问题 --> <!-- 示例问题 -->
<div <div
@ -167,8 +170,8 @@
</div> </div>
</div> </div>
<div class="bottom-actions" v-else> <div class="bottom-actions" v-if="conversations.length > 0">
<p class="note">请注意辨别内容的可靠性</p> <p class="note">当前智能体{{ currentThreadAgentName }}请注意辨别内容的可靠性</p>
</div> </div>
</div> </div>
</div> </div>
@ -210,7 +213,7 @@ import AgentInputArea from '@/components/AgentInputArea.vue'
import AgentMessageComponent from '@/components/AgentMessageComponent.vue' import AgentMessageComponent from '@/components/AgentMessageComponent.vue'
import ChatSidebarComponent from '@/components/ChatSidebarComponent.vue' import ChatSidebarComponent from '@/components/ChatSidebarComponent.vue'
import RefsComponent from '@/components/RefsComponent.vue' import RefsComponent from '@/components/RefsComponent.vue'
import { PanelLeftOpen, MessageCirclePlus, LoaderCircle, ChevronDown, Bot } from 'lucide-vue-next' import { PanelLeftOpen, MessageCirclePlus, LoaderCircle } from 'lucide-vue-next'
import { handleChatError, handleValidationError } from '@/utils/errorHandler' import { handleChatError, handleValidationError } from '@/utils/errorHandler'
import { ScrollController } from '@/utils/scrollController' import { ScrollController } from '@/utils/scrollController'
import { AgentValidator } from '@/utils/agentValidator' import { AgentValidator } from '@/utils/agentValidator'
@ -229,7 +232,6 @@ const props = defineProps({
agentId: { type: String, default: '' }, agentId: { type: String, default: '' },
singleMode: { type: Boolean, default: true } singleMode: { type: Boolean, default: true }
}) })
const emit = defineEmits(['open-config', 'open-agent-modal'])
// ==================== STORE MANAGEMENT ==================== // ==================== STORE MANAGEMENT ====================
const agentStore = useAgentStore() const agentStore = useAgentStore()
@ -336,6 +338,17 @@ const currentThread = computed(() => {
return threads.value.find((thread) => thread.id === currentChatId.value) || null return threads.value.find((thread) => thread.id === currentChatId.value) || null
}) })
const currentThreadAgentName = computed(() => {
const threadAgentId = currentThread.value?.agent_id
if (threadAgentId && agents.value?.length) {
const threadAgent = agents.value.find((agent) => agent.id === threadAgentId)
if (threadAgent?.name) {
return threadAgent.name
}
}
return currentAgentName.value
})
// //
const supportsFileUpload = computed(() => { const supportsFileUpload = computed(() => {
if (!currentAgent.value) return false if (!currentAgent.value) return false
@ -510,6 +523,27 @@ const conversations = computed(() => {
return historyConvs return historyConvs
}) })
const agentSegmentOptions = computed(() => {
return (agents.value || []).map((agent) => ({
label: agent.name || 'Unknown',
value: agent.id
}))
})
const showStartAgentSegment = computed(() => {
return !props.singleMode && !conversations.value.length && agentSegmentOptions.value.length > 1
})
const handleStartAgentChange = async (agentId) => {
if (!agentId || agentId === currentAgentId.value) return
if (conversations.value.length > 0) return
try {
await agentStore.selectAgent(agentId)
} catch (error) {
handleChatError(error, 'load')
}
}
const isLoadingMessages = computed(() => chatUIStore.isLoadingMessages) const isLoadingMessages = computed(() => chatUIStore.isLoadingMessages)
const isStreaming = computed(() => { const isStreaming = computed(() => {
const threadState = currentThreadState.value const threadState = currentThreadState.value
@ -610,8 +644,8 @@ const resetOnGoingConv = (threadId = null) => {
// ==================== 线 ==================== // ==================== 线 ====================
// 线 // 线
const fetchThreads = async (agentId = null) => { const fetchThreads = async (agentId = null) => {
const targetAgentId = agentId || currentAgentId.value const targetAgentId = props.singleMode ? agentId || currentAgentId.value : agentId
if (!targetAgentId) return if (props.singleMode && !targetAgentId) return
chatUIStore.isLoadingThreads = true chatUIStore.isLoadingThreads = true
try { try {
@ -632,8 +666,8 @@ const fetchThreads = async (agentId = null) => {
const loadMoreChats = async () => { const loadMoreChats = async () => {
if (isLoadingMoreChats.value || !hasMoreChats.value) return if (isLoadingMoreChats.value || !hasMoreChats.value) return
const targetAgentId = currentAgentId.value const targetAgentId = props.singleMode ? currentAgentId.value : null
if (!targetAgentId) return if (props.singleMode && !targetAgentId) return
isLoadingMoreChats.value = true isLoadingMoreChats.value = true
try { try {
@ -1350,17 +1384,19 @@ const createNewChat = async () => {
} }
const selectChat = async (chatId) => { const selectChat = async (chatId) => {
if ( const targetChat = threads.value.find((chat) => chat.id === chatId) || null
!AgentValidator.validateAgentIdWithError( const targetAgentId = targetChat?.agent_id || currentAgentId.value
currentAgentId.value, const previousThreadId = chatState.currentThreadId
'选择对话',
handleValidationError if (!targetAgentId) {
) handleValidationError('选择对话失败:缺少智能体信息')
) return
}
if (!AgentValidator.validateAgentIdWithError(targetAgentId, '选择对话', handleValidationError))
return return
// 线 // 线
const previousThreadId = chatState.currentThreadId
if (previousThreadId && previousThreadId !== chatId) { if (previousThreadId && previousThreadId !== chatId) {
const previousThreadState = getThreadState(previousThreadId) const previousThreadState = getThreadState(previousThreadId)
if (previousThreadState?.isStreaming && previousThreadState.streamAbortController) { if (previousThreadState?.isStreaming && previousThreadState.streamAbortController) {
@ -1372,10 +1408,22 @@ const selectChat = async (chatId) => {
stopRunStreamSubscription(previousThreadId) stopRunStreamSubscription(previousThreadId)
} }
// 线
chatState.currentThreadId = chatId 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
}
}
chatUIStore.isLoadingMessages = true chatUIStore.isLoadingMessages = true
try { try {
await fetchThreadMessages({ agentId: currentAgentId.value, threadId: chatId }) await fetchThreadMessages({ agentId: targetAgentId, threadId: chatId })
} catch (error) { } catch (error) {
handleChatError(error, 'load') handleChatError(error, 'load')
} finally { } finally {
@ -1384,7 +1432,7 @@ const selectChat = async (chatId) => {
await nextTick() await nextTick()
scrollController.scrollToBottomStaticForce() scrollController.scrollToBottomStaticForce()
await fetchAgentState(currentAgentId.value, chatId) await fetchAgentState(targetAgentId, chatId)
await resumeActiveRunForThread(chatId) await resumeActiveRunForThread(chatId)
} }
@ -1657,7 +1705,6 @@ defineExpose({
const toggleSidebar = () => { const toggleSidebar = () => {
chatUIStore.toggleSidebar() chatUIStore.toggleSidebar()
} }
const openAgentModal = () => emit('open-agent-modal')
const handleAgentStateRefresh = async (threadId = null) => { const handleAgentStateRefresh = async (threadId = null) => {
if (!currentAgentId.value) return if (!currentAgentId.value) return
@ -1744,8 +1791,8 @@ const getConversationSources = (conv) => {
// ==================== LIFECYCLE & WATCHERS ==================== // ==================== LIFECYCLE & WATCHERS ====================
const loadChatsList = async () => { const loadChatsList = async () => {
const agentId = currentAgentId.value const agentId = props.singleMode ? currentAgentId.value : null
if (!agentId) { if (props.singleMode && !agentId) {
console.warn('No agent selected, cannot load chats list') console.warn('No agent selected, cannot load chats list')
threads.value = [] threads.value = []
chatState.currentThreadId = null chatState.currentThreadId = null
@ -1754,7 +1801,7 @@ const loadChatsList = async () => {
try { try {
await fetchThreads(agentId) await fetchThreads(agentId)
if (currentAgentId.value !== agentId) return if (props.singleMode && currentAgentId.value !== agentId) return
// 线线线 // 线线线
if ( if (
@ -1791,6 +1838,13 @@ onMounted(async () => {
watch( watch(
currentAgentId, currentAgentId,
async (newAgentId, oldAgentId) => { async (newAgentId, oldAgentId) => {
if (!props.singleMode) {
if (oldAgentId === undefined) {
await loadChatsList()
}
return
}
if (newAgentId !== oldAgentId) { if (newAgentId !== oldAgentId) {
// 线 // 线
chatState.currentThreadId = null chatState.currentThreadId = null
@ -1895,19 +1949,26 @@ watch(
.agent-panel-wrapper { .agent-panel-wrapper {
flex: 0 0 auto; flex: 0 0 auto;
height: calc(100% - 56px); align-self: flex-end;
height: 70vh;
overflow: hidden; overflow: hidden;
z-index: 20; z-index: 20;
margin: 28px 8px; margin: 28px 8px;
margin-left: 0; margin-left: 0;
background: var(--gray-0); background: var(--gray-0);
border-radius: 12px; border-radius: 16px;
box-shadow: 0 4px 20px var(--shadow-1); box-shadow: 0 4px 20px var(--shadow-1);
border: 1px solid var(--gray-150); border: 1px solid var(--gray-150);
min-width: 0; min-width: 0;
will-change: flex-basis; will-change: flex-basis;
} }
@media (max-height: 700px) {
.agent-panel-wrapper {
height: calc(100% - 56px);
}
}
/* Workbench transition animations */ /* Workbench transition animations */
.agent-panel-wrapper { .agent-panel-wrapper {
transition: flex-basis 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: flex-basis 0.3s cubic-bezier(0.4, 0, 0.2, 1);
@ -1937,6 +1998,43 @@ watch(
} }
} }
.agent-segment-wrapper {
width: fit-content;
max-width: 100%;
margin: 0 auto 10px;
overflow-x: auto;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
:deep(.ant-segmented) {
width: auto;
max-width: 100%;
white-space: nowrap;
background: var(--gray-50);
border: 1px solid var(--gray-150);
border-radius: 10px;
}
:deep(.ant-segmented-group) {
width: auto;
display: inline-flex;
}
:deep(.ant-segmented-item) {
flex: 0 0 auto;
min-width: 0;
}
:deep(.ant-segmented-item-label) {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.example-questions { .example-questions {
margin-top: 16px; margin-top: 16px;
text-align: center; text-align: center;
@ -2144,6 +2242,14 @@ watch(
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.agent-segment-wrapper {
margin-bottom: 8px;
:deep(.ant-segmented-item-label) {
font-size: 12px;
}
}
.chat-header { .chat-header {
.header__left { .header__left {
.text { .text {

View File

@ -8,7 +8,6 @@
:disabled="disabled" :disabled="disabled"
:send-button-disabled="sendButtonDisabled" :send-button-disabled="sendButtonDisabled"
:placeholder="placeholder" :placeholder="placeholder"
:force-multi-line="hasStateContent"
:mention="mention" :mention="mention"
@send="handleSend" @send="handleSend"
@keydown="handleKeyDown" @keydown="handleKeyDown"
@ -32,6 +31,7 @@
</template> </template>
<template #actions-left> <template #actions-left>
<div class="input-actions-left"> <div class="input-actions-left">
<slot name="actions-left-extra"></slot>
<!-- State Toggle Button --> <!-- State Toggle Button -->
<div <div
v-if="hasStateContent" v-if="hasStateContent"
@ -210,7 +210,6 @@ defineExpose({
font-weight: 500; font-weight: 500;
} }
&.disabled { &.disabled {
opacity: 0.5; opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;

View File

@ -4,7 +4,7 @@
<div class="resize-handle" @mousedown="startResize"></div> <div class="resize-handle" @mousedown="startResize"></div>
<div class="panel-header"> <div class="panel-header">
<div class="panel-title"> <div class="panel-title">
<FolderCode :size="18" class="header-icon" /> <FolderCode :size="20" class="header-icon" />
<span><strong>状态工作台</strong></span> <span><strong>状态工作台</strong></span>
</div> </div>
<div class="header-actions"> <div class="header-actions">
@ -275,9 +275,13 @@ const updateActiveTab = () => {
} }
// files todos tab // files todos tab
watch([() => props.agentState?.files, () => props.agentState?.todos], () => { watch(
updateActiveTab() [() => props.agentState?.files, () => props.agentState?.todos],
}, { deep: true }) () => {
updateActiveTab()
},
{ deep: true }
)
const expandedKeys = ref([]) const expandedKeys = ref([])
@ -560,7 +564,7 @@ const stopResize = () => {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 4px 16px; padding: 4px 16px;
height: 48px; height: 56px;
background: var(--gray-25); background: var(--gray-25);
flex-shrink: 0; flex-shrink: 0;
} }
@ -568,7 +572,7 @@ const stopResize = () => {
.panel-title { .panel-title {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 12px;
font-weight: 600; font-weight: 600;
font-size: 14px; font-size: 14px;
color: var(--gray-900); color: var(--gray-900);

View File

@ -1,9 +1,5 @@
<template> <template>
<div <div class="input-box" :class="customClasses" @click="focusInput">
class="input-box"
:class="[customClasses, { 'single-line': isSingleLine }]"
@click="focusInput"
>
<div class="top-slot"> <div class="top-slot">
<slot name="top"></slot> <slot name="top"></slot>
</div> </div>
@ -129,16 +125,7 @@
</template> </template>
<script setup> <script setup>
import { import { ref, computed, onMounted, nextTick, watch, onBeforeUnmount, useSlots } from 'vue'
ref,
computed,
onMounted,
nextTick,
watch,
onBeforeUnmount,
useSlots,
onUnmounted
} from 'vue'
import { import {
SendOutlined, SendOutlined,
ArrowUpOutlined, ArrowUpOutlined,
@ -158,7 +145,6 @@ const closeMentionPopup = (e) => {
const inputRef = ref(null) const inputRef = ref(null)
const optionsExpanded = ref(false) const optionsExpanded = ref(false)
const singleLineHeight = ref(0) // Add this
// //
const debounceTimer = ref(null) const debounceTimer = ref(null)
const props = defineProps({ const props = defineProps({
@ -194,10 +180,6 @@ const props = defineProps({
type: Object, type: Object,
default: () => ({}) default: () => ({})
}, },
forceMultiLine: {
type: Boolean,
default: false
},
mention: { mention: {
type: Object, type: Object,
default: () => null default: () => null
@ -281,7 +263,11 @@ const updateMentionItems = (query = '') => {
item.type, item.type,
mentionTypePrefixMap[item.type] mentionTypePrefixMap[item.type]
] ]
return searchTexts.some((text) => String(text || '').toLowerCase().includes(lowerQuery)) return searchTexts.some((text) =>
String(text || '')
.toLowerCase()
.includes(lowerQuery)
)
}) })
const fileItems = files.map((f) => { const fileItems = files.map((f) => {
@ -470,13 +456,6 @@ const handleMentionNavigation = (e) => {
} }
} }
// Update isSingleLine logic to respect forceMultiLine
const isSingleLineMode = ref(true)
const isSingleLine = computed(() => {
if (props.forceMultiLine) return false
return isSingleLineMode.value
})
const hasOptionsLeft = computed(() => { const hasOptionsLeft = computed(() => {
const slot = slots['options-left'] const slot = slots['options-left']
if (!slot) { if (!slot) {
@ -572,62 +551,20 @@ const handleSendOrStop = () => {
emit('send') emit('send')
} }
//
const singleLineWidth = ref(0)
// @ // @
const mentionPopupVisible = ref(false) const mentionPopupVisible = ref(false)
const mentionQuery = ref('') const mentionQuery = ref('')
const mentionItems = ref({ files: [], knowledgeBases: [], mcps: [], skills: [] }) const mentionItems = ref({ files: [], knowledgeBases: [], mcps: [], skills: [] })
const mentionSelectedIndex = ref(0) const mentionSelectedIndex = ref(0)
// const adjustTextareaHeight = () => {
const checkLineCount = () => { if (!inputRef.value) {
if (!inputRef.value || singleLineHeight.value === 0) {
return return
} }
const textarea = inputRef.value const textarea = inputRef.value
const content = inputValue.value textarea.style.height = 'auto'
textarea.style.height = `${textarea.scrollHeight}px`
//
const hasNewlines = content.includes('\n')
// 使
let contentExceedsWidth = false
if (!hasNewlines && content.trim() && singleLineWidth.value > 0) {
// 使
const measureDiv = document.createElement('div')
measureDiv.style.cssText = `
position: absolute;
visibility: hidden;
white-space: nowrap;
font-family: ${getComputedStyle(textarea).fontFamily};
font-size: ${getComputedStyle(textarea).fontSize};
line-height: ${getComputedStyle(textarea).lineHeight};
padding: 0;
border: none;
width: ${singleLineWidth.value}px;
`
measureDiv.textContent = content
document.body.appendChild(measureDiv)
//
contentExceedsWidth = measureDiv.scrollWidth > measureDiv.clientWidth
document.body.removeChild(measureDiv)
}
const shouldBeMultiLine = hasNewlines || contentExceedsWidth
isSingleLineMode.value = !shouldBeMultiLine
//
if (shouldBeMultiLine || props.forceMultiLine) {
// textarea
textarea.style.height = 'auto'
textarea.style.height = `${Math.max(textarea.scrollHeight, singleLineHeight.value)}px`
} else {
// CSS
textarea.style.height = ''
}
} }
// //
@ -639,44 +576,24 @@ const focusInput = () => {
// //
watch(inputValue, () => { watch(inputValue, () => {
nextTick(() => { if (debounceTimer.value) {
checkLineCount() clearTimeout(debounceTimer.value)
}) }
debounceTimer.value = setTimeout(() => {
nextTick(() => {
adjustTextareaHeight()
})
}, 100)
}) })
//
/* const observeTextareaResize = () => {
if (inputRef.value) {
const textarea = inputRef.value;
if (textarea) {
// ResizeObserver
const resizeObserver = new ResizeObserver(() => {
checkLineCount();
});
resizeObserver.observe(textarea);
//
onBeforeUnmount(() => {
resizeObserver.disconnect();
});
}
}
}; */
// Wait for component to mount before setting up onStartTyping
onMounted(() => { onMounted(() => {
// console.log('Component mounted');
document.addEventListener('click', closeMentionPopup) document.addEventListener('click', closeMentionPopup)
nextTick(() => { nextTick(() => {
if (inputRef.value) { if (inputRef.value) {
// adjustTextareaHeight()
singleLineHeight.value = inputRef.value.clientHeight
singleLineWidth.value = inputRef.value.clientWidth
checkLineCount()
inputRef.value.focus() inputRef.value.focus()
} }
}) })
// observeTextareaResize();
}) })
// //
@ -748,46 +665,6 @@ defineExpose({
// background: var(--gray-0); // background: var(--gray-0);
// box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); // box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
// } // }
&.single-line {
padding: 0.75rem 0.75rem;
grid-template-columns: auto 1fr auto;
grid-template-rows: auto 1fr auto;
grid-template-areas:
'top top top'
'options input send'
'bottom bottom bottom';
align-items: center;
gap: 0px;
.user-input {
min-height: 24px;
height: 24px; /* Fix height for single line */
align-self: center;
white-space: nowrap;
overflow: hidden;
margin-bottom: 0rem;
}
.expand-options,
.send-button-container {
align-self: center;
}
.expand-options {
display: flex;
align-items: center;
gap: 8px;
}
.top-slot {
grid-area: top;
}
.bottom-slot {
grid-area: bottom;
}
}
} }
.expand-options { .expand-options {

View File

@ -1,44 +1,6 @@
<template> <template>
<div class="agent-view"> <div class="agent-view">
<div class="agent-view-body"> <div class="agent-view-body">
<!-- 智能体选择弹窗 -->
<a-modal
v-model:open="chatUIStore.agentModalOpen"
title="选择智能体"
:width="800"
:footer="null"
:maskClosable="true"
class="agent-modal"
>
<div class="agent-modal-content">
<div class="agents-grid">
<div
v-for="agent in agents"
:key="agent.id"
class="agent-card"
:class="{ selected: agent.id === selectedAgentId }"
@click="selectAgentFromModal(agent.id)"
>
<div class="agent-card-header">
<div class="agent-card-title">
<span class="agent-card-name">{{ agent.name || 'Unknown' }}</span>
</div>
<StarFilled v-if="agent.id === defaultAgentId" class="default-icon" />
<StarOutlined
v-else
@click.prevent="setAsDefaultAgent(agent.id)"
class="default-icon"
/>
</div>
<div class="agent-card-description">
{{ agent.description || '' }}
</div>
</div>
</div>
</div>
</a-modal>
<a-modal <a-modal
v-model:open="createConfigModalOpen" v-model:open="createConfigModalOpen"
title="新建配置" title="新建配置"
@ -55,13 +17,19 @@
<AgentChatComponent <AgentChatComponent
ref="chatComponentRef" ref="chatComponentRef"
:single-mode="false" :single-mode="false"
@open-config="toggleConf"
@open-agent-modal="openAgentModal"
@close-config-sidebar="() => (chatUIStore.isConfigSidebarOpen = false)" @close-config-sidebar="() => (chatUIStore.isConfigSidebarOpen = false)"
> >
<template #header-right> <template #input-actions-left>
<a-dropdown v-if="selectedAgentId" :trigger="['click']"> <a-dropdown
<div type="button" class="agent-nav-btn"> v-if="selectedAgentId"
v-model:open="configDropdownOpen"
:trigger="['click']"
>
<div
type="button"
class="agent-nav-btn config-toggle-btn"
:class="{ active: configDropdownOpen }"
>
<Settings2 size="18" class="nav-btn-icon" /> <Settings2 size="18" class="nav-btn-icon" />
<span class="text hide-text"> <span class="text hide-text">
{{ selectedConfigSummary?.name || '配置' }} {{ selectedConfigSummary?.name || '配置' }}
@ -95,7 +63,7 @@
@click="openCreateConfigModal" @click="openCreateConfigModal"
> >
<div class="menu-item-layout"> <div class="menu-item-layout">
<Plus :size="16" /> <CirclePlus :size="16" />
<span>新建配置</span> <span>新建配置</span>
</div> </div>
</a-menu-item> </a-menu-item>
@ -112,6 +80,9 @@
</a-menu> </a-menu>
</template> </template>
</a-dropdown> </a-dropdown>
</template>
<template #header-right>
<div <div
v-if="selectedAgentId" v-if="selectedAgentId"
ref="moreButtonRef" ref="moreButtonRef"
@ -166,16 +137,10 @@
</template> </template>
<script setup> <script setup>
import { ref, watch } from 'vue' import { ref } from 'vue'
import { import { MessageOutlined, ShareAltOutlined, EyeOutlined } from '@ant-design/icons-vue'
StarOutlined,
StarFilled,
MessageOutlined,
ShareAltOutlined,
EyeOutlined
} from '@ant-design/icons-vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { Settings2, Ellipsis, ChevronDown, Star, Plus, SquarePen } from 'lucide-vue-next' import { Settings2, Ellipsis, ChevronDown, Star, CirclePlus, SquarePen } from 'lucide-vue-next'
import AgentChatComponent from '@/components/AgentChatComponent.vue' import AgentChatComponent from '@/components/AgentChatComponent.vue'
import AgentConfigSidebar from '@/components/AgentConfigSidebar.vue' import AgentConfigSidebar from '@/components/AgentConfigSidebar.vue'
import FeedbackModalComponent from '@/components/dashboard/FeedbackModalComponent.vue' import FeedbackModalComponent from '@/components/dashboard/FeedbackModalComponent.vue'
@ -191,6 +156,7 @@ import { storeToRefs } from 'pinia'
// //
const feedbackModal = ref(null) const feedbackModal = ref(null)
const chatComponentRef = ref(null) const chatComponentRef = ref(null)
const configDropdownOpen = ref(false)
// Stores // Stores
const userStore = useUserStore() const userStore = useUserStore()
@ -198,49 +164,8 @@ const agentStore = useAgentStore()
const chatUIStore = useChatUIStore() const chatUIStore = useChatUIStore()
// agentStore // agentStore
const { const { selectedAgentId, agentConfigs, selectedAgentConfigId, selectedConfigSummary } =
agents, storeToRefs(agentStore)
selectedAgentId,
defaultAgentId,
agentConfigs,
selectedAgentConfigId,
selectedConfigSummary
} = storeToRefs(agentStore)
//
const setAsDefaultAgent = async (agentId) => {
if (!agentId || !userStore.isAdmin) return
try {
await agentStore.setDefaultAgent(agentId)
message.success('已将当前智能体设为默认')
} catch (error) {
console.error('设置默认智能体错误:', error)
message.error(error.message || '设置默认智能体时发生错误')
}
}
// agentStore
// 使store
const selectAgent = async (agentId) => {
await agentStore.selectAgent(agentId)
}
//
const openAgentModal = () => {
chatUIStore.agentModalOpen = true
}
//
const selectAgentFromModal = async (agentId) => {
await selectAgent(agentId)
chatUIStore.agentModalOpen = false
}
const toggleConf = () => {
chatUIStore.isConfigSidebarOpen = !chatUIStore.isConfigSidebarOpen
}
const openConfigSidebar = () => { const openConfigSidebar = () => {
chatUIStore.isConfigSidebarOpen = true chatUIStore.isConfigSidebarOpen = true
@ -689,6 +614,7 @@ const handlePreview = () => {
line-height: 1.5; line-height: 1.5;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -832,145 +758,6 @@ const handlePreview = () => {
} }
} }
//
.agent-selector {
border: 1px solid var(--gray-300);
border-radius: 8px;
padding: 8px 12px;
background: var(--gray-0);
transition: border-color 0.2s ease;
&:hover {
border-color: var(--main-color);
}
.selected-agent-display {
display: flex;
justify-content: space-between;
align-items: center;
.agent-name {
font-size: 14px;
color: var(--gray-900);
font-weight: 500;
}
}
}
//
.agent-modal {
:deep(.ant-modal-content) {
border-radius: 8px;
overflow: hidden;
}
:deep(.ant-modal-header) {
background: var(--gray-0);
border-bottom: 1px solid var(--gray-200);
padding: 16px 20px;
.ant-modal-title {
font-size: 16px;
font-weight: 600;
color: var(--gray-900);
}
}
:deep(.ant-modal-body) {
padding: 20px;
background: var(--gray-0);
}
.agent-modal-content {
.agents-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 12px;
max-height: 500px;
overflow-y: auto;
}
.agent-card {
border: 1px solid var(--gray-200);
border-radius: 8px;
padding: 16px;
cursor: pointer;
transition: border-color 0.2s ease;
background: var(--gray-0);
&:hover {
border-color: var(--main-color);
}
.agent-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
.agent-card-title {
flex: 1;
.agent-card-name {
font-size: 16px;
font-weight: 600;
color: var(--gray-900);
line-height: 1.4;
}
}
.default-icon {
color: var(--color-warning-500);
font-size: 16px;
flex-shrink: 0;
margin-left: 8px;
cursor: pointer;
&:hover {
color: var(--color-warning-600);
}
}
}
.agent-card-description {
font-size: 14px;
color: var(--gray-700);
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
&.selected {
border-color: var(--main-color);
background: var(--main-20);
// outline: 2px solid var(--main-color);
.agent-card-header .agent-card-title .agent-card-name {
color: var(--main-color);
}
.agent-card-description {
color: var(--gray-900);
}
}
}
}
}
//
@media (max-width: 768px) {
.agent-modal {
.agent-modal-content {
.agents-grid {
grid-template-columns: 1fr;
}
}
}
}
// //
.more-popup-menu { .more-popup-menu {
position: fixed; position: fixed;
@ -1127,6 +914,36 @@ const handlePreview = () => {
width: 100%; width: 100%;
} }
.agent-nav-btn.config-toggle-btn {
gap: 6px;
padding: 0 8px;
height: 28px;
border-radius: 8px;
font-size: 14px;
color: var(--gray-600);
transition: all 0.2s ease;
user-select: none;
.nav-btn-icon {
height: 16px;
}
.text {
line-height: 1;
}
&:hover {
color: var(--main-color);
background: var(--gray-100);
}
&.active {
color: var(--main-color);
background: var(--main-50);
font-weight: 500;
}
}
@media (max-width: 768px) { @media (max-width: 768px) {
.hide-text { .hide-text {
display: none; display: none;