Files
AgentCoord/frontend/src/layout/components/Main/TaskTemplate/TaskSyllabus/index.vue
2026-01-09 13:54:32 +08:00

628 lines
17 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 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, ref, nextTick, watch, onMounted } from 'vue'
import { AnchorLocations } from '@jsplumb/browser-ui'
import MultiLineTooltip from '@/components/MultiLineTooltip/index.vue'
import Bg from './Bg.vue'
import BranchButton from './components/BranchButton.vue'
// 判断计划是否就绪
const planReady = computed(() => {
return agentsStore.agentRawPlan.data !== undefined
})
const openPlanModification = () => {
agentsStore.openPlanModification()
}
const emit = defineEmits<{
(el: 'resetAgentRepoLine'): void
(el: 'setCurrentTask', task: IRawStepTask): void
(el: 'add-output', outputName: string): void
(el: 'click-branch'): void
}>()
const jsplumb = new Jsplumb('task-syllabus')
const handleScroll = () => {
emit('resetAgentRepoLine')
}
const agentsStore = useAgentsStore()
const collaborationProcess = computed(() => {
return agentsStore.agentRawPlan.data?.['Collaboration Process'] ?? []
})
// 编辑状态管理
const editingTaskId = ref<string | null>(null)
const editingContent = ref('')
// 添加新产物状态管理
const isAddingOutput = ref(false)
const newOutputInputRef = ref<HTMLElement>()
const newOutputName = ref('')
// 处理加号点击
const handleAddOutputClick = () => {
isAddingOutput.value = true
newOutputName.value = ''
nextTick(() => {
setTimeout(() => {
if (newOutputInputRef.value) {
newOutputInputRef.value?.focus()
}
jsplumb.instance.repaintEverything()
}, 50)
})
}
// 保存新产物
const saveNewOutput = () => {
if (newOutputName.value.trim()) {
const outputName = newOutputName.value.trim()
const success = agentsStore.addNewOutput(outputName)
if (success) {
emit('add-output', outputName)
isAddingOutput.value = false
newOutputName.value = ''
nextTick(() => {
setTimeout(() => {
jsplumb.instance.repaintEverything()
}, 50)
})
console.log('添加新产物成功', outputName)
} else {
// 退出编辑状态
isAddingOutput.value = false
newOutputName.value = ''
}
}
}
// 取消添加产物
const cancelAddOutput = () => {
isAddingOutput.value = false
newOutputName.value = ''
nextTick(() => {
setTimeout(() => {
jsplumb.instance.repaintEverything()
}, 50)
})
}
// 处理新产物的键盘事件
const handleNewOutputKeydown = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault()
saveNewOutput()
} else if (event.key === 'Escape') {
cancelAddOutput()
}
}
// 新产物输入框失去焦点处理
const handleNewOutputBlur = () => {
setTimeout(() => {
if (newOutputName.value.trim() === '') {
cancelAddOutput()
}
}, 100)
}
// 开始编辑
const startEditing = (task: IRawStepTask) => {
if (!task.Id) {
console.warn('Task ID is missing, cannot start editing')
return
}
editingTaskId.value = task.Id
editingContent.value = task.TaskContent || ''
}
// 保存编辑
const saveEditing = () => {
if (editingTaskId.value && editingContent.value.trim()) {
const taskToUpdate = collaborationProcess.value.find(item => item.Id === editingTaskId.value)
if (taskToUpdate) {
taskToUpdate.TaskContent = editingContent.value.trim()
}
}
editingTaskId.value = null
editingContent.value = ''
}
// 取消编辑
const cancelEditing = () => {
editingTaskId.value = null
editingContent.value = ''
}
// 处理键盘事件
const handleKeydown = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault()
saveEditing()
} else if (event.key === 'Escape') {
cancelEditing()
}
}
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 = () => {
console.log('🔄 重新绘制 jsplumb 连线')
// 等待 DOM 更新完成
nextTick(() => {
// 清除旧连线
jsplumb.reset()
// 等待 DOM 稳定后重新绘制
setTimeout(() => {
const arr: ConnectArg[] = []
const currentTaskId = agentsStore.currentTask?.Id
// 重新绘制所有连线
collaborationProcess.value.forEach(item => {
arr.push(...handleCurrentTask(item, item.Id !== currentTaskId))
})
jsplumb.connects(arr)
console.log('✅ jsplumb 连线重绘完成,任务数:', collaborationProcess.value.length)
}, 100)
})
}
// 🆕 监听 collaborationProcess 变化,自动重绘连线
watch(
() => collaborationProcess,
() => {
console.log('🔍 collaborationProcess 发生变化,触发重绘')
redrawConnections()
},
{ deep: true }
)
// 🆕 组件挂载后初始化连线
onMounted(() => {
// 初始化时绘制连线
nextTick(() => {
setTimeout(() => {
const arr: ConnectArg[] = []
collaborationProcess.value.forEach(item => {
arr.push(...handleCurrentTask(item, true))
})
jsplumb.connects(arr)
console.log('✅ 初始化 jsplumb 连线完成')
}, 100)
})
})
defineExpose({
changeTask,
clear
})
</script>
<template>
<div class="h-full flex flex-col">
<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 :is-adding="isAddingOutput" @start-add-output="handleAddOutputClick" />
<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-if="isAddingOutput"
class="card-it w-full flex items-center gap-[14%] bg-[var(--color-card-bg)] add-output-form mb-[100px]"
>
<!-- 左侧空白的流程卡片占位 -->
<div class="w-[43%] relative z-99" style="height: 20px"></div>
<!-- 右侧可编辑的产物卡片 -->
<el-card
class="w-[43%] relative task-syllabus-output-object-card border-dashed border-2 border-[var(--color-primary)]"
>
<div class="h-full flex items-center justify-center">
<!-- 输入框 -->
<el-input
ref="newOutputInputRef"
v-model="newOutputName"
placeholder="Enter保存ESC取消"
@keydown="handleNewOutputKeydown"
@blur="handleNewOutputBlur"
size="large"
class="w-full"
/>
</div>
</el-card>
</div>
<!-- 显示临时产物卡片 -->
<div
v-for="output in agentsStore.additionalOutputs"
:key="output"
class="card-it w-full flex items-center gap-[14%] bg-[var(--color-card-bg)] mb-[100px]"
>
<!-- 左侧空白的流程卡片占位 -->
<div class="w-[43%] relative z-99" style="height: 100px"></div>
<!-- 右侧产物卡片 -->
<el-card class="w-[43%] relative task-syllabus-output-object-card" :shadow="true">
<div class="text-[18px] font-bold text-center">{{ output }}</div>
</el-card>
</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>
<!-- 任务内容区域 - 支持双击编辑 -->
<div v-if="editingTaskId === item.Id" class="w-full">
<div class="flex flex-col gap-3">
<el-input
v-model="editingContent"
type="textarea"
:autosize="{ minRows: 2, maxRows: 4 }"
placeholder="请输入任务内容"
@keydown="handleKeydown"
class="task-content-editor"
size="small"
/>
<div class="flex justify-end">
<svg-icon
icon-class="Check"
size="20px"
color="#328621"
class="cursor-pointer mr-4"
@click="saveEditing"
title="保存"
/>
<svg-icon
icon-class="Cancel"
size="20px"
color="#8e0707"
class="cursor-pointer mr-1"
@click="cancelEditing"
title="取消"
/>
</div>
</div>
</div>
<div v-else @dblclick="startEditing(item)" class="w-full cursor-pointer">
<MultiLineTooltip placement="right" :text="item.TaskContent" :lines="3">
<div class="text-[14px] text-[var(--color-text-secondary)] task-content-display">
{{ item.TaskContent }}
</div>
</MultiLineTooltip>
</div>
<div class="h-[1px] w-full bg-[var(--color-border-separate)] my-[8px]"></div>
<div
class="flex items-center gap-2 overflow-y-auto flex-wrap relative w-full max-h-[72px]"
>
<!-- 连接到智能体库的连接点 -->
<div
class="absolute left-[-10px] top-1/2 transform -translate-y-1/2"
:id="`task-syllabus-flow-agents-${item.Id}`"
></div>
<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>
</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-dev-only 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;
}
.add-output-btn {
opacity: 0.8;
transition: opacity 0.2s ease;
&:hover {
opacity: 1;
}
button {
background: transparent;
cursor: pointer;
&:hover {
background-color: rgba(59, 130, 246, 0.05);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
&:active {
transform: translateY(0);
}
}
}
.add-output-form {
animation: slideDown 0.3s ease-out;
:deep(.el-card__body) {
padding: 16px;
display: flex;
align-items: center;
justify-content: center;
}
:deep(.el-input__wrapper) {
border: 1px solid var(--color-text);
background: transparent;
box-shadow: none;
&.is-focus {
border-color: var(--color-text);
box-shadow: 0 0 0 1px var(--color-primary-light);
}
:deep(.el-input__inner) {
font-size: 14px;
text-align: center;
font-weight: bold;
}
}
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
// 输入框样式
: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);
}
}
}
}
}
</style>