feat: 智能体对话界面优化
- 消息列表修改为所有的对话,不区分智能体 - 智能体会在新建对话的时候切换 - 配置选择从右上角移动到输入框 - 其他细节优化
This commit is contained in:
parent
06cc8cbf7a
commit
1764ffedb0
@ -746,7 +746,7 @@ async def create_thread(
|
||||
|
||||
@chat.get("/threads", response_model=list[ThreadResponse])
|
||||
async def list_threads(
|
||||
agent_id: str,
|
||||
agent_id: str | None = Query(None),
|
||||
limit: int = Query(100, ge=1, le=500),
|
||||
offset: int = Query(0, ge=0),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
|
||||
@ -153,15 +153,12 @@ async def create_thread_view(
|
||||
|
||||
async def list_threads_view(
|
||||
*,
|
||||
agent_id: str,
|
||||
agent_id: str | None,
|
||||
db: AsyncSession,
|
||||
current_user_id: str,
|
||||
limit: int | None = None,
|
||||
offset: int = 0,
|
||||
) -> list[dict]:
|
||||
if not agent_id:
|
||||
raise HTTPException(status_code=422, detail="agent_id 不能为空")
|
||||
|
||||
conv_repo = ConversationRepository(db)
|
||||
conversations = await conv_repo.list_conversations(
|
||||
user_id=str(current_user_id),
|
||||
|
||||
@ -285,13 +285,20 @@ export const multimodalApi = {
|
||||
export const threadApi = {
|
||||
/**
|
||||
* 获取对话线程列表
|
||||
* @param {string} agentId - 智能体ID
|
||||
* @param {string | null | undefined} agentId - 智能体ID,可选;不传时返回全部智能体对话
|
||||
* @param {number} limit - 返回数量限制,默认100
|
||||
* @param {number} offset - 偏移量,默认0
|
||||
* @returns {Promise} - 对话线程列表
|
||||
*/
|
||||
getThreads: (agentId, limit = 100, offset = 0) => {
|
||||
const url = `/api/chat/threads?agent_id=${agentId}&limit=${limit}&offset=${offset}`
|
||||
getThreads: (agentId = null, limit = 100, offset = 0) => {
|
||||
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)
|
||||
},
|
||||
|
||||
|
||||
@ -17,7 +17,6 @@
|
||||
@rename-chat="renameChat"
|
||||
@toggle-pin="togglePinChat"
|
||||
@toggle-sidebar="toggleSidebar"
|
||||
@open-agent-modal="openAgentModal"
|
||||
@load-more-chats="loadMoreChats"
|
||||
:class="{
|
||||
'sidebar-open': chatUIStore.isSidebarOpen,
|
||||
@ -51,14 +50,6 @@
|
||||
<MessageCirclePlus v-else class="nav-btn-icon" size="16" />
|
||||
<span class="text">新对话</span>
|
||||
</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 class="header__right">
|
||||
<!-- AgentState 显示按钮已移动到输入框底部 -->
|
||||
@ -131,6 +122,14 @@
|
||||
<h1>👋 您好,我是{{ currentAgentName }}!</h1>
|
||||
</div>
|
||||
|
||||
<div v-if="showStartAgentSegment" class="agent-segment-wrapper">
|
||||
<a-segmented
|
||||
:value="currentAgentId"
|
||||
:options="agentSegmentOptions"
|
||||
@change="handleStartAgentChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AgentInputArea
|
||||
ref="messageInputRef"
|
||||
v-model="userInput"
|
||||
@ -148,7 +147,11 @@
|
||||
@send="handleSendOrStop"
|
||||
@attachment-changed="handleAgentStateRefresh"
|
||||
@toggle-panel="toggleAgentPanel"
|
||||
/>
|
||||
>
|
||||
<template #actions-left-extra>
|
||||
<slot name="input-actions-left"></slot>
|
||||
</template>
|
||||
</AgentInputArea>
|
||||
|
||||
<!-- 示例问题 -->
|
||||
<div
|
||||
@ -167,8 +170,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom-actions" v-else>
|
||||
<p class="note">请注意辨别内容的可靠性</p>
|
||||
<div class="bottom-actions" v-if="conversations.length > 0">
|
||||
<p class="note">当前智能体:{{ currentThreadAgentName }};请注意辨别内容的可靠性</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -210,7 +213,7 @@ import AgentInputArea from '@/components/AgentInputArea.vue'
|
||||
import AgentMessageComponent from '@/components/AgentMessageComponent.vue'
|
||||
import ChatSidebarComponent from '@/components/ChatSidebarComponent.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 { ScrollController } from '@/utils/scrollController'
|
||||
import { AgentValidator } from '@/utils/agentValidator'
|
||||
@ -229,7 +232,6 @@ const props = defineProps({
|
||||
agentId: { type: String, default: '' },
|
||||
singleMode: { type: Boolean, default: true }
|
||||
})
|
||||
const emit = defineEmits(['open-config', 'open-agent-modal'])
|
||||
|
||||
// ==================== STORE MANAGEMENT ====================
|
||||
const agentStore = useAgentStore()
|
||||
@ -336,6 +338,17 @@ const currentThread = computed(() => {
|
||||
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(() => {
|
||||
if (!currentAgent.value) return false
|
||||
@ -510,6 +523,27 @@ const conversations = computed(() => {
|
||||
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 isStreaming = computed(() => {
|
||||
const threadState = currentThreadState.value
|
||||
@ -610,8 +644,8 @@ const resetOnGoingConv = (threadId = null) => {
|
||||
// ==================== 线程管理方法 ====================
|
||||
// 获取当前智能体的线程列表
|
||||
const fetchThreads = async (agentId = null) => {
|
||||
const targetAgentId = agentId || currentAgentId.value
|
||||
if (!targetAgentId) return
|
||||
const targetAgentId = props.singleMode ? agentId || currentAgentId.value : agentId
|
||||
if (props.singleMode && !targetAgentId) return
|
||||
|
||||
chatUIStore.isLoadingThreads = true
|
||||
try {
|
||||
@ -632,8 +666,8 @@ const fetchThreads = async (agentId = null) => {
|
||||
const loadMoreChats = async () => {
|
||||
if (isLoadingMoreChats.value || !hasMoreChats.value) return
|
||||
|
||||
const targetAgentId = currentAgentId.value
|
||||
if (!targetAgentId) return
|
||||
const targetAgentId = props.singleMode ? currentAgentId.value : null
|
||||
if (props.singleMode && !targetAgentId) return
|
||||
|
||||
isLoadingMoreChats.value = true
|
||||
try {
|
||||
@ -1350,17 +1384,19 @@ const createNewChat = async () => {
|
||||
}
|
||||
|
||||
const selectChat = async (chatId) => {
|
||||
if (
|
||||
!AgentValidator.validateAgentIdWithError(
|
||||
currentAgentId.value,
|
||||
'选择对话',
|
||||
handleValidationError
|
||||
)
|
||||
)
|
||||
const targetChat = threads.value.find((chat) => chat.id === chatId) || null
|
||||
const targetAgentId = targetChat?.agent_id || currentAgentId.value
|
||||
const previousThreadId = chatState.currentThreadId
|
||||
|
||||
if (!targetAgentId) {
|
||||
handleValidationError('选择对话失败:缺少智能体信息')
|
||||
return
|
||||
}
|
||||
|
||||
if (!AgentValidator.validateAgentIdWithError(targetAgentId, '选择对话', handleValidationError))
|
||||
return
|
||||
|
||||
// 中断之前线程的流式输出(如果存在)
|
||||
const previousThreadId = chatState.currentThreadId
|
||||
if (previousThreadId && previousThreadId !== chatId) {
|
||||
const previousThreadState = getThreadState(previousThreadId)
|
||||
if (previousThreadState?.isStreaming && previousThreadState.streamAbortController) {
|
||||
@ -1372,10 +1408,22 @@ const selectChat = async (chatId) => {
|
||||
stopRunStreamSubscription(previousThreadId)
|
||||
}
|
||||
|
||||
// 先更新当前线程,确保底部智能体名称与选中项即时同步
|
||||
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
|
||||
try {
|
||||
await fetchThreadMessages({ agentId: currentAgentId.value, threadId: chatId })
|
||||
await fetchThreadMessages({ agentId: targetAgentId, threadId: chatId })
|
||||
} catch (error) {
|
||||
handleChatError(error, 'load')
|
||||
} finally {
|
||||
@ -1384,7 +1432,7 @@ const selectChat = async (chatId) => {
|
||||
|
||||
await nextTick()
|
||||
scrollController.scrollToBottomStaticForce()
|
||||
await fetchAgentState(currentAgentId.value, chatId)
|
||||
await fetchAgentState(targetAgentId, chatId)
|
||||
await resumeActiveRunForThread(chatId)
|
||||
}
|
||||
|
||||
@ -1657,7 +1705,6 @@ defineExpose({
|
||||
const toggleSidebar = () => {
|
||||
chatUIStore.toggleSidebar()
|
||||
}
|
||||
const openAgentModal = () => emit('open-agent-modal')
|
||||
|
||||
const handleAgentStateRefresh = async (threadId = null) => {
|
||||
if (!currentAgentId.value) return
|
||||
@ -1744,8 +1791,8 @@ const getConversationSources = (conv) => {
|
||||
|
||||
// ==================== LIFECYCLE & WATCHERS ====================
|
||||
const loadChatsList = async () => {
|
||||
const agentId = currentAgentId.value
|
||||
if (!agentId) {
|
||||
const agentId = props.singleMode ? currentAgentId.value : null
|
||||
if (props.singleMode && !agentId) {
|
||||
console.warn('No agent selected, cannot load chats list')
|
||||
threads.value = []
|
||||
chatState.currentThreadId = null
|
||||
@ -1754,7 +1801,7 @@ const loadChatsList = async () => {
|
||||
|
||||
try {
|
||||
await fetchThreads(agentId)
|
||||
if (currentAgentId.value !== agentId) return
|
||||
if (props.singleMode && currentAgentId.value !== agentId) return
|
||||
|
||||
// 如果当前线程不在线程列表中,清空当前线程
|
||||
if (
|
||||
@ -1791,6 +1838,13 @@ onMounted(async () => {
|
||||
watch(
|
||||
currentAgentId,
|
||||
async (newAgentId, oldAgentId) => {
|
||||
if (!props.singleMode) {
|
||||
if (oldAgentId === undefined) {
|
||||
await loadChatsList()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (newAgentId !== oldAgentId) {
|
||||
// 清理当前线程状态
|
||||
chatState.currentThreadId = null
|
||||
@ -1895,19 +1949,26 @@ watch(
|
||||
|
||||
.agent-panel-wrapper {
|
||||
flex: 0 0 auto;
|
||||
height: calc(100% - 56px);
|
||||
align-self: flex-end;
|
||||
height: 70vh;
|
||||
overflow: hidden;
|
||||
z-index: 20;
|
||||
margin: 28px 8px;
|
||||
margin-left: 0;
|
||||
background: var(--gray-0);
|
||||
border-radius: 12px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px var(--shadow-1);
|
||||
border: 1px solid var(--gray-150);
|
||||
min-width: 0;
|
||||
will-change: flex-basis;
|
||||
}
|
||||
|
||||
@media (max-height: 700px) {
|
||||
.agent-panel-wrapper {
|
||||
height: calc(100% - 56px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Workbench transition animations */
|
||||
.agent-panel-wrapper {
|
||||
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 {
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
@ -2144,6 +2242,14 @@ watch(
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.agent-segment-wrapper {
|
||||
margin-bottom: 8px;
|
||||
|
||||
:deep(.ant-segmented-item-label) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
.header__left {
|
||||
.text {
|
||||
|
||||
@ -8,7 +8,6 @@
|
||||
:disabled="disabled"
|
||||
:send-button-disabled="sendButtonDisabled"
|
||||
:placeholder="placeholder"
|
||||
:force-multi-line="hasStateContent"
|
||||
:mention="mention"
|
||||
@send="handleSend"
|
||||
@keydown="handleKeyDown"
|
||||
@ -32,6 +31,7 @@
|
||||
</template>
|
||||
<template #actions-left>
|
||||
<div class="input-actions-left">
|
||||
<slot name="actions-left-extra"></slot>
|
||||
<!-- State Toggle Button -->
|
||||
<div
|
||||
v-if="hasStateContent"
|
||||
@ -210,7 +210,6 @@ defineExpose({
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<div class="resize-handle" @mousedown="startResize"></div>
|
||||
<div class="panel-header">
|
||||
<div class="panel-title">
|
||||
<FolderCode :size="18" class="header-icon" />
|
||||
<FolderCode :size="20" class="header-icon" />
|
||||
<span><strong>状态工作台</strong></span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
@ -275,9 +275,13 @@ const updateActiveTab = () => {
|
||||
}
|
||||
|
||||
// 监听 files 和 todos 变化,自动切换 tab
|
||||
watch([() => props.agentState?.files, () => props.agentState?.todos], () => {
|
||||
updateActiveTab()
|
||||
}, { deep: true })
|
||||
watch(
|
||||
[() => props.agentState?.files, () => props.agentState?.todos],
|
||||
() => {
|
||||
updateActiveTab()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const expandedKeys = ref([])
|
||||
|
||||
@ -560,7 +564,7 @@ const stopResize = () => {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 16px;
|
||||
height: 48px;
|
||||
height: 56px;
|
||||
background: var(--gray-25);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@ -568,7 +572,7 @@ const stopResize = () => {
|
||||
.panel-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
gap: 12px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--gray-900);
|
||||
|
||||
@ -1,9 +1,5 @@
|
||||
<template>
|
||||
<div
|
||||
class="input-box"
|
||||
:class="[customClasses, { 'single-line': isSingleLine }]"
|
||||
@click="focusInput"
|
||||
>
|
||||
<div class="input-box" :class="customClasses" @click="focusInput">
|
||||
<div class="top-slot">
|
||||
<slot name="top"></slot>
|
||||
</div>
|
||||
@ -129,16 +125,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
computed,
|
||||
onMounted,
|
||||
nextTick,
|
||||
watch,
|
||||
onBeforeUnmount,
|
||||
useSlots,
|
||||
onUnmounted
|
||||
} from 'vue'
|
||||
import { ref, computed, onMounted, nextTick, watch, onBeforeUnmount, useSlots } from 'vue'
|
||||
import {
|
||||
SendOutlined,
|
||||
ArrowUpOutlined,
|
||||
@ -158,7 +145,6 @@ const closeMentionPopup = (e) => {
|
||||
|
||||
const inputRef = ref(null)
|
||||
const optionsExpanded = ref(false)
|
||||
const singleLineHeight = ref(0) // Add this
|
||||
// 用于防抖的定时器
|
||||
const debounceTimer = ref(null)
|
||||
const props = defineProps({
|
||||
@ -194,10 +180,6 @@ const props = defineProps({
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
forceMultiLine: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
mention: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
@ -281,7 +263,11 @@ const updateMentionItems = (query = '') => {
|
||||
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) => {
|
||||
@ -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 slot = slots['options-left']
|
||||
if (!slot) {
|
||||
@ -572,62 +551,20 @@ const handleSendOrStop = () => {
|
||||
emit('send')
|
||||
}
|
||||
|
||||
// 用于存储固定的单行宽度基准
|
||||
const singleLineWidth = ref(0)
|
||||
|
||||
// @ 提及功能状态
|
||||
const mentionPopupVisible = ref(false)
|
||||
const mentionQuery = ref('')
|
||||
const mentionItems = ref({ files: [], knowledgeBases: [], mcps: [], skills: [] })
|
||||
const mentionSelectedIndex = ref(0)
|
||||
|
||||
// 检查行数
|
||||
const checkLineCount = () => {
|
||||
if (!inputRef.value || singleLineHeight.value === 0) {
|
||||
const adjustTextareaHeight = () => {
|
||||
if (!inputRef.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const textarea = inputRef.value
|
||||
const content = inputValue.value
|
||||
|
||||
// 主要判断依据:内容是否包含换行符
|
||||
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 = ''
|
||||
}
|
||||
textarea.style.height = 'auto'
|
||||
textarea.style.height = `${textarea.scrollHeight}px`
|
||||
}
|
||||
|
||||
// 聚焦输入框
|
||||
@ -639,44 +576,24 @@ const focusInput = () => {
|
||||
|
||||
// 监听输入值变化
|
||||
watch(inputValue, () => {
|
||||
nextTick(() => {
|
||||
checkLineCount()
|
||||
})
|
||||
if (debounceTimer.value) {
|
||||
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(() => {
|
||||
// console.log('Component mounted');
|
||||
document.addEventListener('click', closeMentionPopup)
|
||||
nextTick(() => {
|
||||
if (inputRef.value) {
|
||||
// 记录单行模式下的高度和宽度基准
|
||||
singleLineHeight.value = inputRef.value.clientHeight
|
||||
singleLineWidth.value = inputRef.value.clientWidth
|
||||
checkLineCount()
|
||||
adjustTextareaHeight()
|
||||
inputRef.value.focus()
|
||||
}
|
||||
})
|
||||
// observeTextareaResize();
|
||||
})
|
||||
|
||||
// 组件卸载时清除定时器和事件监听器
|
||||
@ -748,46 +665,6 @@ defineExpose({
|
||||
// background: var(--gray-0);
|
||||
// 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 {
|
||||
|
||||
@ -1,44 +1,6 @@
|
||||
<template>
|
||||
<div class="agent-view">
|
||||
<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
|
||||
v-model:open="createConfigModalOpen"
|
||||
title="新建配置"
|
||||
@ -55,13 +17,19 @@
|
||||
<AgentChatComponent
|
||||
ref="chatComponentRef"
|
||||
:single-mode="false"
|
||||
@open-config="toggleConf"
|
||||
@open-agent-modal="openAgentModal"
|
||||
@close-config-sidebar="() => (chatUIStore.isConfigSidebarOpen = false)"
|
||||
>
|
||||
<template #header-right>
|
||||
<a-dropdown v-if="selectedAgentId" :trigger="['click']">
|
||||
<div type="button" class="agent-nav-btn">
|
||||
<template #input-actions-left>
|
||||
<a-dropdown
|
||||
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" />
|
||||
<span class="text hide-text">
|
||||
{{ selectedConfigSummary?.name || '配置' }}
|
||||
@ -95,7 +63,7 @@
|
||||
@click="openCreateConfigModal"
|
||||
>
|
||||
<div class="menu-item-layout">
|
||||
<Plus :size="16" />
|
||||
<CirclePlus :size="16" />
|
||||
<span>新建配置</span>
|
||||
</div>
|
||||
</a-menu-item>
|
||||
@ -112,6 +80,9 @@
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
|
||||
<template #header-right>
|
||||
<div
|
||||
v-if="selectedAgentId"
|
||||
ref="moreButtonRef"
|
||||
@ -166,16 +137,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import {
|
||||
StarOutlined,
|
||||
StarFilled,
|
||||
MessageOutlined,
|
||||
ShareAltOutlined,
|
||||
EyeOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { ref } from 'vue'
|
||||
import { MessageOutlined, ShareAltOutlined, EyeOutlined } from '@ant-design/icons-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 AgentConfigSidebar from '@/components/AgentConfigSidebar.vue'
|
||||
import FeedbackModalComponent from '@/components/dashboard/FeedbackModalComponent.vue'
|
||||
@ -191,6 +156,7 @@ import { storeToRefs } from 'pinia'
|
||||
// 组件引用
|
||||
const feedbackModal = ref(null)
|
||||
const chatComponentRef = ref(null)
|
||||
const configDropdownOpen = ref(false)
|
||||
|
||||
// Stores
|
||||
const userStore = useUserStore()
|
||||
@ -198,49 +164,8 @@ const agentStore = useAgentStore()
|
||||
const chatUIStore = useChatUIStore()
|
||||
|
||||
// 从 agentStore 中获取响应式状态
|
||||
const {
|
||||
agents,
|
||||
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 { selectedAgentId, agentConfigs, selectedAgentConfigId, selectedConfigSummary } =
|
||||
storeToRefs(agentStore)
|
||||
|
||||
const openConfigSidebar = () => {
|
||||
chatUIStore.isConfigSidebarOpen = true
|
||||
@ -689,6 +614,7 @@ const handlePreview = () => {
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
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 {
|
||||
position: fixed;
|
||||
@ -1127,6 +914,36 @@ const handlePreview = () => {
|
||||
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) {
|
||||
.hide-text {
|
||||
display: none;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user