Files
AgentCoord/frontend/src/layout/components/Main/Task.vue
2026-01-26 16:46:58 +08:00

614 lines
17 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 websocket from '@/utils/websocket'
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)
// 添加一个状态来跟踪是否正在填充步骤数据
const isFillingSteps = ref(false)
// 存储当前填充任务的取消函数
const currentStepAbortController = ref<{ cancel: () => void } | null>(null)
// 解析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 = () => {
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)
}
// 🆕 预加载所有任务的智能体评分数据(顺序加载,确保任务详情已填充)
async function preloadAllTaskAgentScores(outlineData: any, goal: string) {
const tasks = outlineData['Collaboration Process'] || []
if (tasks.length === 0) {
console.log(' 没有任务需要预加载评分数据')
return
}
console.log(`🚀 开始预加载 ${tasks.length} 个任务的智能体评分数据...`)
// 🆕 顺序预加载:等待每个任务详情填充完成后再预加载其评分
for (const task of tasks) {
// 确保任务有 Id
if (!task.Id) {
task.Id = `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
}
const taskId = task.Id
// 检查是否已有缓存数据
if (agentsStore.hasTaskScoreData(taskId)) {
console.log(`⏭️ 任务 "${task.StepName}" (${taskId}) 已有缓存数据,跳过`)
continue
}
// 🆕 等待任务详情填充完成(通过检查 AgentSelection 是否存在)
// 最多等待 60 秒,超时则跳过该任务
let waitCount = 0
const maxWait = 60 // 60 * 500ms = 30秒
while (!task.AgentSelection && waitCount < maxWait) {
await new Promise(resolve => setTimeout(resolve, 500))
waitCount++
}
if (!task.AgentSelection) {
console.warn(`⚠️ 任务 "${task.StepName}" (${taskId}) 详情未填充完成,跳过评分预加载`)
continue
}
try {
// 调用初始化接口获取评分数据
const agentScores = await api.agentSelectModifyInit({
goal: goal,
stepTask: {
StepName: task.StepName,
TaskContent: task.TaskContent,
InputObject_List: task.InputObject_List,
OutputObject: task.OutputObject
}
})
// 提取维度列表
const firstAgent = Object.keys(agentScores)[0]
const aspectList = firstAgent ? Object.keys(agentScores[firstAgent] || {}) : []
// 存储到 store按任务ID存储
agentsStore.setTaskScoreData(taskId, {
aspectList,
agentScores
})
console.log(`✅ 任务 "${task.StepName}" (${taskId}) 的评分数据预加载完成,维度数: ${aspectList.length}`)
} catch (error) {
console.error(`❌ 任务 "${task.StepName}" (${taskId}) 的评分数据预加载失败:`, error)
}
}
console.log(`🎉 所有 ${tasks.length} 个任务的智能体评分数据预加载完成(或已跳过)`)
}
// 重置文本区域高度到最小行数
function resetTextareaHeight() {
nextTick(() => {
// 获取textarea元素
const textarea =
document.querySelector('#task-container .el-textarea__inner') ||
document.querySelector('#task-container textarea')
if (textarea instanceof HTMLElement) {
// 强制设置最小高度
textarea.style.height = 'auto'
textarea.style.minHeight = '56px'
textarea.style.overflowY = 'hidden'
}
})
}
// 停止填充数据的处理函数
async function handleStop() {
try {
// 通过 WebSocket 发送停止信号
if (websocket.connected) {
await websocket.send('stop_generation', {
goal: searchValue.value
})
ElMessage.success('已发送停止信号,正在停止生成...')
} else {
ElMessage.warning('WebSocket 未连接,无法停止')
}
} catch (error) {
console.error('停止生成失败:', error)
ElMessage.error('停止生成失败')
} finally {
// 无论后端是否成功停止,都重置状态
isFillingSteps.value = false
currentStepAbortController.value = null
// 标记用户已停止填充
agentsStore.setHasStoppedFilling(true)
}
}
// 处理按钮点击事件
function handleButtonClick() {
if (isFillingSteps.value) {
// 如果正在填充数据,点击停止
handleStop()
} else {
// 否则开始搜索
handleSearch()
}
}
async function handleSearch() {
// 用于标记大纲是否成功加载
let outlineLoaded = false
try {
triggerOnFocus.value = false
if (!searchValue.value) {
ElMessage.warning('请输入搜索内容')
return
}
emit('search-start')
agentsStore.resetAgent()
agentsStore.setAgentRawPlan({ loading: true })
// 重置停止状态
agentsStore.setHasStoppedFilling(false)
// 获取大纲
const outlineData = await api.generateBasePlan({
goal: searchValue.value,
inputs: []
})
// 检查是否已被停止
if (!isFillingSteps.value && currentStepAbortController.value) {
return
}
// 处理简报数据格式
outlineData['Collaboration Process'] = changeBriefs(outlineData['Collaboration Process'])
// 立即显示大纲
agentsStore.setAgentRawPlan({ data: outlineData, loading: false })
outlineLoaded = true
emit('search', searchValue.value)
// 🆕 预加载所有任务的智能体评分数据(在后台静默执行)
preloadAllTaskAgentScores(outlineData, searchValue.value)
// 开始填充步骤详情,设置状态
isFillingSteps.value = true
// 并行填充所有步骤的详情
const steps = outlineData['Collaboration Process'] || []
// 带重试的填充函数
const fillStepWithRetry = async (step: any, retryCount = 0): Promise<void> => {
const maxRetries = 2 // 最多重试2次
// 检查是否已停止
if (!isFillingSteps.value) {
console.log('检测到停止信号,跳过步骤填充')
return
}
try {
if (!step.StepName) {
console.warn('步骤缺少 StepName跳过填充详情')
return
}
// 使用现有的 fillStepTask API 填充每个步骤的详情
const detailedStep = await api.fillStepTask({
goal: searchValue.value,
stepTask: {
StepName: step.StepName,
TaskContent: step.TaskContent,
InputObject_List: step.InputObject_List,
OutputObject: step.OutputObject
}
})
// 再次检查是否已停止(在 API 调用后)
if (!isFillingSteps.value) {
console.log('检测到停止信号,跳过更新步骤详情')
return
}
// 更新该步骤的详情到 store
updateStepDetail(step.StepName, detailedStep)
} catch (error) {
console.error(
`填充步骤 ${step.StepName} 详情失败 (尝试 ${retryCount + 1}/${maxRetries + 1}):`,
error
)
// 如果未达到最大重试次数,延迟后重试
if (retryCount < maxRetries) {
console.log(`正在重试步骤 ${step.StepName}...`)
// 延迟1秒后重试避免立即重试导致同样的问题
await new Promise(resolve => setTimeout(resolve, 1000))
return fillStepWithRetry(step, retryCount + 1)
} else {
console.error(`步骤 ${step.StepName}${maxRetries + 1} 次尝试后仍然失败`)
}
}
}
// // 为每个步骤并行填充详情(选人+过程)
// const fillPromises = steps.map(step => fillStepWithRetry(step))
// // 等待所有步骤填充完成(包括重试)
// await Promise.all(fillPromises)
// 串行填充所有步骤的详情(避免字段混乱)
for (const step of steps) {
await fillStepWithRetry(step)
}
} finally {
triggerOnFocus.value = true
// 完成填充,重置状态
isFillingSteps.value = false
currentStepAbortController.value = null
// 如果大纲加载失败确保关闭loading
if (!outlineLoaded) {
agentsStore.setAgentRawPlan({ loading: false })
}
}
}
// 辅助函数:更新单个步骤的详情
function updateStepDetail(stepId: string, detailedStep: any) {
const planData = agentsStore.agentRawPlan.data
if (!planData) return
const collaborationProcess = planData['Collaboration Process']
if (!collaborationProcess) return
const index = collaborationProcess.findIndex((s: any) => s.StepName === stepId)
if (index !== -1 && collaborationProcess[index]) {
// 保持响应式更新 - 使用 Vue 的响应式系统
Object.assign(collaborationProcess[index], {
AgentSelection: detailedStep.AgentSelection || [],
TaskProcess: detailedStep.TaskProcess || [],
Collaboration_Brief_frontEnd: detailedStep.Collaboration_Brief_frontEnd || {
template: '',
data: {}
}
})
}
}
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="isFillingSteps ? '点击停止生成' : '点击搜索任务'"
circle
:loading="agentsStore.agentRawPlan.loading"
:disabled="!searchValue"
@click.stop="handleButtonClick"
>
<SvgIcon
v-if="!agentsStore.agentRawPlan.loading && !isFillingSteps"
icon-class="paper-plane"
size="18px"
color="#ffffff"
/>
<SvgIcon
v-if="!agentsStore.agentRawPlan.loading && isFillingSteps"
icon-class="stoprunning"
size="30px"
color="#ffffff"
/>
</el-button>
</div>
<AssignmentButton 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;
}
}
/* 非展开状态时,确保文本区域高度固定 */
&: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>