Files
AgentCoord/frontend/src/layout/components/Main/Task.vue
2025-12-31 19:04:58 +08:00

383 lines
9.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, onMounted, computed, reactive, nextTick } from 'vue'
import SvgIcon from '@/components/SvgIcon/index.vue'
import { useAgentsStore, useConfigStore } from '@/stores'
import api from '@/api'
import { changeBriefs } from '@/utils/collaboration_Brief_FrontEnd.ts'
import { ElMessage } from 'element-plus'
import AssignmentButton from './TaskTemplate/TaskSyllabus/components/AssignmentButton.vue'
const emit = defineEmits<{
(e: 'search-start'): void
(e: 'search', value: string): void
}>()
const agentsStore = useAgentsStore()
const configStore = useConfigStore()
const searchValue = ref('')
const triggerOnFocus = ref(true)
const isFocus = ref(false)
const hasAutoSearched = ref(false) // 防止重复自动搜索
const isExpanded = ref(false) // 控制搜索框是否展开
// 解析URL参数
function getUrlParam(param: string): string | null {
const urlParams = new URLSearchParams(window.location.search)
return urlParams.get(param)
}
const planReady = computed(() => {
return agentsStore.agentRawPlan.data !== undefined
})
const openAgentAllocationDialog = () => {
console.log('打开智能体分配弹窗')
agentsStore.openAgentAllocationDialog()
}
// 自动搜索函数
async function autoSearchFromUrl() {
const query = getUrlParam('q')
if (query && !hasAutoSearched.value) {
// 解码URL参数
const decodedQuery = decodeURIComponent(query)
searchValue.value = decodedQuery
hasAutoSearched.value = true
// 延迟执行搜索,确保组件已完全渲染
setTimeout(() => {
handleSearch()
}, 100)
}
}
// 处理获取焦点事件
function handleFocus() {
isFocus.value = true
isExpanded.value = true // 搜索框展开
}
const taskContainerRef = ref<HTMLDivElement | null>(null)
// 处理失去焦点事件
function handleBlur() {
isFocus.value = false
// 延迟收起搜索框,以便点击按钮等操作
setTimeout(() => {
isExpanded.value = false
// 强制重置文本区域高度到最小行数
resetTextareaHeight()
}, 200)
}
// 重置文本区域高度到最小行数
function resetTextareaHeight() {
nextTick(() => {
// 修复使用更可靠的方式获取textarea元素
const textarea =
document.querySelector('#task-container .el-textarea__inner') ||
document.querySelector('#task-container textarea')
if (textarea) {
// 强制设置最小高度
textarea.style.height = 'auto'
textarea.style.minHeight = '56px'
textarea.style.overflowY = 'hidden'
}
})
}
async function handleSearch() {
try {
triggerOnFocus.value = false
if (!searchValue.value) {
ElMessage.warning('请输入搜索内容')
return
}
emit('search-start')
agentsStore.resetAgent()
agentsStore.setAgentRawPlan({ loading: true })
const data = await api.generateBasePlan({
goal: searchValue.value,
inputs: []
})
data['Collaboration Process'] = changeBriefs(data['Collaboration Process'])
agentsStore.setAgentRawPlan({ data })
emit('search', searchValue.value)
} finally {
triggerOnFocus.value = true
agentsStore.setAgentRawPlan({ loading: false })
}
}
const querySearch = (queryString: string, cb: (v: { value: string }[]) => void) => {
const results = queryString
? configStore.config.taskPromptWords.filter(createFilter(queryString))
: configStore.config.taskPromptWords
// call callback function to return suggestions
cb(results.map(item => ({ value: item })))
}
const createFilter = (queryString: string) => {
return (restaurant: string) => {
return restaurant.toLowerCase().includes(queryString.toLowerCase())
}
}
// 组件挂载时检查URL参数
onMounted(() => {
autoSearchFromUrl()
})
</script>
<template>
<el-tooltip
content="请先点击智能体库右侧的按钮上传智能体信息"
placement="top"
effect="light"
:disabled="agentsStore.agents.length > 0"
>
<div class="task-root-container">
<div
class="task-container"
ref="taskContainerRef"
id="task-container"
:class="{ expanded: isExpanded }"
>
<span class="text-[var(--color-text-task)] font-bold task-title">任务</span>
<el-autocomplete
ref="autocompleteRef"
v-model.trim="searchValue"
class="task-input"
size="large"
:rows="1"
:autosize="{ minRows: 1, maxRows: 10 }"
placeholder="请输入您的任务"
type="textarea"
:append-to="taskContainerRef"
:fetch-suggestions="querySearch"
@change="agentsStore.setSearchValue"
:disabled="!(agentsStore.agents.length > 0)"
:debounce="0"
:clearable="true"
:trigger-on-focus="triggerOnFocus"
@focus="handleFocus"
@blur="handleBlur"
@select="isFocus = false"
>
</el-autocomplete>
<el-button
class="task-button"
color="linear-gradient(to right, #00C7D2, #315AB4)"
size="large"
title="点击搜索任务"
circle
:loading="agentsStore.agentRawPlan.loading"
:disabled="!searchValue"
@click.stop="handleSearch"
>
<SvgIcon
v-if="!agentsStore.agentRawPlan.loading"
icon-class="paper-plane"
size="18px"
color="#ffffff"
/>
</el-button>
</div>
<AssignmentButton v-dev-only v-if="planReady" @click="openAgentAllocationDialog" />
</div>
</el-tooltip>
</template>
<style scoped lang="scss">
.task-root-container {
height: 60px;
margin-bottom: 24px;
position: relative;
}
.task-container {
width: 40%;
margin: 0 auto;
border: 2px solid transparent;
$bg: var(--el-input-bg-color, var(--el-fill-color-blank));
background: linear-gradient(var(--color-bg-taskbar), var(--color-bg-taskbar)) padding-box,
linear-gradient(to right, #00c8d2, #315ab4) border-box;
border-radius: 30px;
position: absolute;
left: 50%;
transform: translateX(-50%);
z-index: 998;
min-height: 100%;
overflow: hidden;
padding: 0 55px 0 47px;
transition: all 0.3s ease;
/* 搜索框展开时的样式 */
&.expanded {
box-shadow: var(--color-task-shadow);
:deep(.el-autocomplete .el-textarea .el-textarea__inner) {
overflow-y: auto !important;
min-height: 56px !important;
// overflow-y: hidden;
// background-color: black;
}
}
/* 非展开状态时,确保文本区域高度固定 */
&:not(.expanded) {
:deep(.el-textarea__inner) {
height: 56px !important;
overflow-y: hidden !important;
min-height: 56px !important;
}
}
:deep(.el-popper) {
position: static !important;
width: calc(100% + 102px); /*增加左右padding的总和 */
min-width: calc(100% + 102px); /* 确保最小宽度也增加 */
margin-left: -47px; /* 向左偏移左padding的值 */
margin-right: -55px; /*向右偏移右padding的值 */
background: var(--color-bg-taskbar);
border: none;
transition: height 0s ease-in-out;
border-top: 1px solid var(--color-border);
border-radius: 0;
box-shadow: none;
li {
height: 45px;
box-sizing: border-box;
line-height: 45px;
font-size: 14px;
padding-left: 27px;
&:hover {
background: var(--color-bg-hover);
color: var(--color-text-hover);
}
}
.el-popper__arrow {
display: none;
}
}
:deep(.el-autocomplete) {
min-height: 56px;
width: 100%;
.task-input {
height: 100%;
}
.el-textarea__inner {
border-radius: 0;
box-shadow: none;
font-size: 14px;
height: 100%;
line-height: 1.5;
padding: 18px 0 0 18px;
resize: none;
color: var(--color-text-taskbar);
/* 聚焦时的样式 */
.expanded & {
overflow-y: auto;
}
&::placeholder {
line-height: 1.2;
font-size: 18px;
vertical-align: middle;
}
.el-icon.is-loading {
& + span {
display: none;
}
}
}
}
.task-title {
position: absolute;
top: 28px;
left: 27px;
z-index: 999;
transform: translateY(-50%);
}
.task-button {
background: linear-gradient(to right, #00c7d2, #315ab4);
border: none; // 如果需要移除边框
position: absolute;
top: 28px;
right: 10px;
transform: translateY(-50%);
z-index: 999;
display: flex;
justify-content: center;
align-items: center;
padding: 0;
}
.task-button.is-loading {
:deep(span) {
display: none !important;
}
}
}
.drawer-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.title {
font-size: 18px;
font-weight: bold;
}
}
.process-list {
padding: 0 8px;
}
.process-item {
margin-bottom: 16px;
padding: 12px;
border-radius: 8px;
background: var(--color-bg-list);
border: 1px solid var(--color-border-default);
.process-content {
display: flex;
align-items: flex-start;
gap: 8px;
.agent-tag {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
flex-shrink: 0;
}
.process-text {
line-height: 1.6;
font-size: 14px;
color: var(--el-text-color-primary);
white-space: pre-wrap;
}
}
.edit-container {
margin-top: 8px;
}
}
.process-item:hover {
border-color: var(--el-border-color);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
</style>