Files
AgentCoord/frontend/src/layout/components/Main/TaskTemplate/TaskSyllabus/index.vue
2026-01-30 15:27:00 +08:00

510 lines
15 KiB
Vue

<script setup lang="ts">
import SvgIcon from '@/components/SvgIcon/index.vue'
import { getAgentMapIcon } from '@/layout/components/config.ts'
import { type ConnectArg, Jsplumb } from '@/layout/components/Main/TaskTemplate/utils.ts'
import { type IRawStepTask, useAgentsStore } from '@/stores'
import { computed, nextTick, watch, onMounted } from 'vue'
import { AnchorLocations } from '@jsplumb/browser-ui'
import { Loading } from '@element-plus/icons-vue'
import MultiLineTooltip from '@/components/MultiLineTooltip/index.vue'
import Bg from './Bg.vue'
import BranchButton from './components/BranchButton.vue'
import Notification from '@/components/Notification/Notification.vue'
import { useNotification } from '@/composables/useNotification'
import TaskContentEditor from '@/components/TaskContentEditor/index.vue'
// 判断计划是否就绪
const planReady = computed(() => {
return agentsStore.agentRawPlan.data !== undefined
})
const openPlanModification = () => {
agentsStore.openPlanModification()
}
const emit = defineEmits<{
(el: 'resetAgentRepoLine'): void
(el: 'setCurrentTask', task: IRawStepTask): void
}>()
const jsplumb = new Jsplumb('task-syllabus')
const handleScroll = () => {
emit('resetAgentRepoLine')
}
const agentsStore = useAgentsStore()
const collaborationProcess = computed(() => {
return agentsStore.agentRawPlan.data?.['Collaboration Process'] ?? []
})
// 检测是否正在填充详情
const isFillingDetails = computed(() => {
const process = agentsStore.agentRawPlan.data?.['Collaboration Process'] || []
return (
process.length > 0 &&
process.some(step => !step.AgentSelection || step.AgentSelection.length === 0)
)
})
// 计算填充进度
const completedSteps = computed(() => {
const process = agentsStore.agentRawPlan.data?.['Collaboration Process'] || []
return process.filter(step => step.AgentSelection && step.AgentSelection.length > 0).length
})
const totalSteps = computed(() => {
return agentsStore.agentRawPlan.data?.['Collaboration Process']?.length || 0
})
// Notification 通知
const {
notifications,
progress: showProgress,
updateProgressDetail,
removeNotification
} = useNotification()
const fillingProgressNotificationId = ref<string | null>(null)
// 监听填充进度,显示通知
watch(
[isFillingDetails, completedSteps, totalSteps, () => agentsStore.hasStoppedFilling],
([filling, completed, total, hasStopped]) => {
// 如果用户已停止,关闭进度通知
if (hasStopped && fillingProgressNotificationId.value) {
removeNotification(fillingProgressNotificationId.value)
fillingProgressNotificationId.value = null
return
}
if (filling && total > 0) {
if (!fillingProgressNotificationId.value) {
// 创建进度通知
fillingProgressNotificationId.value = showProgress('生成协作流程', completed, total)
updateProgressDetail(
fillingProgressNotificationId.value,
`${completed}/${total}`,
'正在分配智能体...',
completed,
total
)
} else {
// 更新进度通知
updateProgressDetail(
fillingProgressNotificationId.value,
`${completed}/${total}`,
'正在分配智能体...',
completed,
total
)
}
} else if (fillingProgressNotificationId.value && !filling) {
// 填充完成,关闭进度通知
removeNotification(fillingProgressNotificationId.value)
fillingProgressNotificationId.value = null
}
},
{ immediate: true }
)
// 保存编辑内容
const handleContentSave = (taskId: string, content: string) => {
const taskToUpdate = collaborationProcess.value.find(item => item.Id === taskId)
if (taskToUpdate && content !== taskToUpdate.TaskContent) {
taskToUpdate.TaskContent = content
// 记录修改过的步骤索引到store用于重新执行
const stepIndex = collaborationProcess.value.findIndex(item => item.Id === taskId)
if (stepIndex >= 0) {
agentsStore.addModifiedStep(stepIndex)
}
}
}
function handleCurrentTask(task: IRawStepTask, transparent: boolean): ConnectArg[] {
// 创建当前流程与产出的连线
const arr: ConnectArg[] = [
{
sourceId: `task-syllabus-flow-${task.Id}`,
targetId: `task-syllabus-output-object-${task.Id}`,
anchor: [AnchorLocations.Right, AnchorLocations.Left],
config: {
transparent
}
}
]
// 创建当前产出与流程的连线
task.InputObject_List?.forEach(item => {
const id = collaborationProcess.value.find(i => i.OutputObject === item)?.Id
if (id) {
arr.push({
sourceId: `task-syllabus-output-object-${id}`,
targetId: `task-syllabus-flow-${task.Id}`,
anchor: [AnchorLocations.Left, AnchorLocations.Right],
config: {
type: 'output',
transparent
}
})
}
})
return arr
}
function changeTask(task?: IRawStepTask, isEmit?: boolean) {
jsplumb.reset()
const arr: ConnectArg[] = []
agentsStore.agentRawPlan.data?.['Collaboration Process']?.forEach(item => {
arr.push(...handleCurrentTask(item, item.Id !== task?.Id))
})
jsplumb.connects(arr)
if (isEmit && task) {
emit('setCurrentTask', task)
}
}
function clear() {
jsplumb.reset()
}
// 封装连线重绘方法
const redrawConnections = () => {
nextTick(() => {
jsplumb.reset()
setTimeout(() => {
const arr: ConnectArg[] = []
const currentTaskId = agentsStore.currentTask?.Id
collaborationProcess.value.forEach(item => {
arr.push(...handleCurrentTask(item, item.Id !== currentTaskId))
})
jsplumb.connects(arr)
}, 100)
})
}
// 监听 collaborationProcess 变化,自动重绘连线
watch(
() => collaborationProcess,
() => {
redrawConnections()
},
{ deep: true }
)
// 组件挂载后初始化连线
onMounted(() => {
nextTick(() => {
setTimeout(() => {
const arr: ConnectArg[] = []
collaborationProcess.value.forEach(item => {
arr.push(...handleCurrentTask(item, true))
})
jsplumb.connects(arr)
}, 100)
})
})
defineExpose({
changeTask,
clear
})
</script>
<template>
<div class="h-full flex flex-col">
<!-- Notification 通知 -->
<Notification :notifications="notifications" @close="id => removeNotification(id)" />
<div class="text-[18px] font-bold mb-[18px] text-[var(--color-text-title-header)]">
任务大纲
</div>
<div
v-loading="agentsStore.agentRawPlan.loading"
class="flex-1 w-full overflow-y-auto relative"
@scroll="handleScroll"
>
<div
v-show="collaborationProcess.length > 0"
class="w-full relative min-h-full"
id="task-syllabus"
>
<Bg />
<div class="w-full flex items-center gap-[14%] mb-[35px]">
<div class="flex-1 flex justify-center">
<div
class="card-item w-[168px] h-[41px] flex justify-center relative z-99 items-center rounded-[20px] bg-[var(--color-bg-flow)]"
>
流程
</div>
</div>
<div class="flex-1 flex justify-center">
<div
class="card-item w-[168px] h-[41px] flex justify-center relative z-99 items-center rounded-[20px] bg-[var(--color-bg-flow)]"
>
产物
</div>
</div>
</div>
<div
v-for="item in collaborationProcess"
:key="item.Id"
class="card-it w-full flex items-center gap-[14%] bg-[var(--color-card-bg)] mb-[100px]"
>
<!-- 流程卡片 -->
<el-card
class="w-[43%] overflow-y-auto relative z-99 task-syllabus-flow-card"
:class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''"
:shadow="true"
:id="`task-syllabus-flow-${item.Id}`"
@click="changeTask(item, true)"
>
<MultiLineTooltip placement="right" :text="item.StepName" :lines="2">
<div class="text-[18px] font-bold text-center">{{ item.StepName }}</div>
</MultiLineTooltip>
<div class="h-[1px] w-full bg-[var(--color-border-separate)] my-[8px]"></div>
<!-- 任务内容区域 - 支持双击编辑 -->
<TaskContentEditor :task="item" @save="handleContentSave">
<template #display>
<MultiLineTooltip placement="right" :text="item.TaskContent" :lines="3">
<div class="text-[14px] text-[var(--color-text-secondary)] task-content-display">
{{ item.TaskContent }}
</div>
</MultiLineTooltip>
</template>
</TaskContentEditor>
<div class="h-[1px] w-full bg-[var(--color-border-separate)] my-[8px]"></div>
<div
class="flex items-center gap-2 flex-wrap relative w-full"
:class="
!item.AgentSelection || item.AgentSelection.length === 0
? 'min-h-[40px]'
: 'overflow-y-auto max-h-[72px]'
"
>
<!-- 连接到智能体库的连接点 -->
<div
class="absolute left-[-10px] top-1/2 transform -translate-y-1/2"
:id="`task-syllabus-flow-agents-${item.Id}`"
></div>
<!-- 未填充智能体时显示Loading -->
<div
v-if="
(!item.AgentSelection || item.AgentSelection.length === 0) &&
!agentsStore.hasStoppedFilling
"
class="flex items-center gap-2 text-[var(--color-text-secondary)] text-[14px]"
>
<el-icon class="is-loading" :size="20">
<Loading />
</el-icon>
<span>正在分配智能体<span class="loading-dots"></span></span>
</div>
<!-- 已填充智能体时显示智能体列表 -->
<template v-else>
<el-tooltip
v-for="agentSelection in item.AgentSelection"
:key="agentSelection"
effect="light"
placement="right"
>
<template #content>
<div class="w-[150px]">
<div class="text-[18px] font-bold">{{ agentSelection }}</div>
<div class="h-[1px] w-full bg-[#494B51] my-[8px]"></div>
<div>
{{
item.TaskProcess.find(i => i.AgentName === agentSelection)?.Description
}}
</div>
</div>
</template>
<div
class="w-[31px] h-[31px] rounded-full flex items-center justify-center"
:style="{ background: getAgentMapIcon(agentSelection).color }"
>
<svg-icon
:icon-class="getAgentMapIcon(agentSelection).icon"
color="#fff"
size="24px"
/>
</div>
</el-tooltip>
</template>
</div>
</el-card>
<!-- 产物卡片 -->
<el-card
class="w-[43%] relative task-syllabus-output-object-card"
:shadow="true"
:class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''"
:id="`task-syllabus-output-object-${item.Id}`"
>
<div class="text-[18px] font-bold text-center">{{ item.OutputObject }}</div>
</el-card>
</div>
</div>
</div>
<BranchButton v-if="planReady" @click="openPlanModification" />
</div>
</template>
<style lang="scss" scoped>
.task-syllabus-flow-card {
background-color: var(--color-card-bg-task);
border: 1px solid var(--color-card-border-task);
box-sizing: border-box;
transition: border-color 0.2s ease;
&:hover {
background-color: var(--color-card-bg-task-hover);
border-color: var(--color-card-border-hover);
box-shadow: var(--color-card-shadow-hover);
}
:deep(.el-card__body) {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: auto;
}
}
.task-syllabus-output-object-card {
background-color: var(--color-card-bg-task);
border: 1px solid var(--color-card-border-task);
box-sizing: border-box;
transition: border-color 0.2s ease;
&:hover {
background-color: var(--color-card-bg-task-hover);
border-color: var(--color-card-border-hover);
box-shadow: var(--color-card-shadow-hover);
}
:deep(.el-card__body) {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: auto;
}
}
.task-content-editor {
:deep(.el-textarea__inner) {
font-size: 14px;
color: var(--color-text-secondary);
background: transparent;
border: 1px solid #dcdfe6;
border-radius: 4px;
resize: none;
}
}
.task-content-display {
min-height: 40px;
word-break: break-word;
white-space: pre-wrap;
}
// 输入框样式
:deep(.el-input__wrapper) {
background: transparent;
border: 1px solid #dcdfe6;
border-radius: 4px;
box-shadow: none;
transition: all 0.2s ease;
&:hover {
border-color: #c0c4cc;
}
&.is-focus {
border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
}
:deep(.el-input__inner) {
color: var(--color-text-primary);
font-size: 14px;
background: transparent;
}
// 任务内容编辑按钮样式 - 匹配执行结果编辑按钮样式
.task-content-editor {
.el-button {
font-weight: bold;
font-size: 16px;
border-radius: 4px;
&.el-button--small {
padding: 4px 12px;
}
}
// 在深色模式下,按钮背景会自动适配为深色
html.dark & {
.el-button {
&.el-button--primary {
background-color: var(--color-bg-detail);
border-color: var(--color-border);
color: var(--color-text);
&:hover {
background-color: var(--color-bg-hover);
border-color: var(--color-text-hover);
}
}
&:not(.el-button--primary) {
background-color: var(--color-bg-detail);
border-color: var(--color-border);
color: var(--color-text);
&:hover {
background-color: var(--color-bg-hover);
border-color: var(--color-text-hover);
}
}
}
}
}
// 加载动画省略号
.loading-dots {
display: inline-block;
width: 1.2em;
text-align: left;
&::after {
content: '';
animation: dots 1.5s steps(4, end) infinite;
}
}
@keyframes dots {
0%,
20% {
content: '';
}
40% {
content: '.';
}
60% {
content: '..';
}
80%,
100% {
content: '...';
}
}
</style>