feat(agent): 支持自定义API配置并优化UI交互

- 为agent.json添加apiUrl、apiKey、apiModel字段支持
- 更新API接口类型定义,支持传递自定义API配置
- 优化AgentRepoList组件UI样式和交互效果
- 增强JSON文件上传校验逻辑,支持API配置验证
- 改进任务结果页面布局和视觉呈现
- 添加任务过程查看抽屉功能
- 实现执行按钮动态样式和悬停效果
- 优化节点连接线渲染逻辑和性能
This commit is contained in:
zhaoweijie
2025-12-15 20:46:54 +08:00
parent 6392301833
commit 77530c49f8
25 changed files with 2040 additions and 306 deletions

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { computed } from 'vue'
import { getActionTypeDisplay, getAgentMapIcon } from '@/layout/components/config.ts'
import SvgIcon from '@/components/SvgIcon/index.vue'
import { type Agent, useAgentsStore } from '@/stores'
@@ -10,9 +11,9 @@ const porps = defineProps<{
const taskProcess = computed(() => {
const list = agentsStore.currentTask?.TaskProcess ?? []
return list.map((item) => ({
return list.map(item => ({
...item,
key: uuidv4(),
key: uuidv4()
}))
})
@@ -26,35 +27,38 @@ const agentsStore = useAgentsStore()
class="user-item"
:class="agentsStore.currentTask?.AgentSelection?.includes(item.Name) ? 'active-card' : ''"
>
<div class="flex items-center justify-between relative h-[41px]">
<div class="flex items-center justify-between relative h-[43px]">
<div
class="w-[44px] h-[44px] rounded-full flex items-center justify-center flex-shrink-0 relative right-[2px] icon-container"
:style="{ background: getAgentMapIcon(item.Name).color }"
>
<svg-icon
:icon-class="getAgentMapIcon(item.Name).icon"
color="var(--color-text)"
size="24px"
/>
<svg-icon :icon-class="getAgentMapIcon(item.Name).icon" color="#fff" size="24px" />
</div>
<div class="flex-1 text-[14px] flex flex-col items-end justify-end truncate ml-1">
<span
class="w-full truncate text-right"
:style="
agentsStore.currentTask?.AgentSelection?.includes(item.Name) ? 'color:#00F3FF' : ''
"
>{{ item.Name }}</span
>
<div class="flex-1 text-[14px] textClass flex flex-col items-end justify-end truncate ml-1">
<div class="flex items-center justify-end gap-2 w-full">
<span
class="truncate"
:style="
agentsStore.currentTask?.AgentSelection?.includes(item.Name)
? 'color:var(--color-accent)'
: ''
"
>{{ item.Name }}</span
>
</div>
<div
v-if="agentsStore.currentTask?.AgentSelection?.includes(item.Name)"
class="flex items-center gap-[7px] h-[8px] mr-1"
>
<!-- 小圆点 -->
<div
v-for="item1 in taskProcess.filter((i) => i.AgentName === item.Name)"
v-for="item1 in taskProcess.filter(i => i.AgentName === item.Name)"
:key="item1.key"
class="w-[6px] h-[6px] rounded-full"
:style="{ background: getActionTypeDisplay(item1.ActionType)?.color }"
:style="{
background: getActionTypeDisplay(item1.ActionType)?.color,
border: `1px solid ${getActionTypeDisplay(item1.ActionType)?.border}`
}"
></div>
</div>
</div>
@@ -71,7 +75,7 @@ const agentsStore = useAgentsStore()
</div>
<div class="p-[8px] pt-0">
<div
v-for="(item1, index1) in taskProcess.filter((i) => i.AgentName === item.Name)"
v-for="(item1, index1) in taskProcess.filter(i => i.AgentName === item.Name)"
:key="item1.key"
class="text-[12px]"
>
@@ -89,8 +93,8 @@ const agentsStore = useAgentsStore()
</div>
<!-- 分割线 -->
<div
v-if="index1 !== taskProcess.filter((i) => i.AgentName === item.Name).length - 1"
class="h-[1px] w-full bg-[#494B51] my-[8px]"
v-if="index1 !== taskProcess.filter(i => i.AgentName === item.Name).length - 1"
class="h-[1px] w-full bg-[var(--color-border-default)] my-[8px]"
></div>
</div>
</div>
@@ -100,13 +104,14 @@ const agentsStore = useAgentsStore()
<style scoped lang="scss">
.user-item {
background: #1d222b;
background: var(--color-agent-list-bg);
border-radius: 40px;
padding-right: 12px;
cursor: pointer;
transition: all 0.25s ease;
color: #969696;
border: 2px solid transparent;
color: var(--color-text-detail);
border: 1px solid var(--color-agent-list-border);
box-sizing: border-box;
.duty-info {
transition: height 0.25s ease;
height: 0;
@@ -118,16 +123,30 @@ const agentsStore = useAgentsStore()
}
&:hover {
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
color: #b8b8b8;
box-shadow: var(--color-agent-list-hover-shadow);
color: var(--color-text);
background: var(--color-agent-list-hover-bg);
border: 1px solid var(--color-agent-list-hover-border);
}
}
.textClass {
color: var(--color-text-agent-list);
&:hover {
color: var(--color-text-agent-list-hover);
}
}
.active-card {
background:
linear-gradient(#171B22, #171B22) padding-box,
linear-gradient(to right, #00c8d2, #315ab4) border-box;
background: linear-gradient(var(--color-bg-quaternary), var(--color-bg-quaternary)) padding-box,
linear-gradient(to right, var(--color-accent), var(--color-accent-secondary)) border-box;
border: 1px solid var(--color-agent-list-border);
&:hover {
box-shadow: var(--color-agent-list-hover-shadow);
color: var(--color-text);
background: var(--color-agent-list-hover-bg);
border: 1px solid var(--color-agent-list-hover-border);
border-radius: 20px;
.duty-info {
height: auto;
@@ -138,4 +157,10 @@ const agentsStore = useAgentsStore()
}
}
}
// 添加头像容器样式修复
.icon-container {
right: 0 !important;
margin-left: 0px;
}
</style>

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElNotification } from 'element-plus'
import { pick } from 'lodash'
@@ -7,7 +8,6 @@ import api from '@/api/index.ts'
import SvgIcon from '@/components/SvgIcon/index.vue'
import { agentMapDuty } from '@/layout/components/config.ts'
import { type Agent, useAgentsStore } from '@/stores'
import { onMounted } from 'vue'
import { readConfig } from '@/utils/readJson.ts'
import AgentRepoList from './AgentRepoList.vue'
@@ -19,7 +19,7 @@ onMounted(async () => {
const res = await readConfig<Agent[]>('agent.json')
agentsStore.setAgents(res)
}
await api.setAgents(agentsStore.agents.map((item) => pick(item, ['Name', 'Profile'])))
await api.setAgents(agentsStore.agents.map(item => pick(item, ['Name', 'Profile'])))
})
// 上传agent文件
@@ -37,50 +37,150 @@ const handleFileSelect = (event: Event) => {
}
}
const readFileContent = async (file: File) => {
const reader = new FileReader()
reader.onload = async (e) => {
if (!e.target?.result) {
return
}
try {
const json = JSON.parse(e.target.result?.toString?.() ?? '{}')
// 处理 JSON 数据
if (Array.isArray(json)) {
const isValid = json.every(
(item) =>
typeof item.Name === 'string' &&
typeof item.Icon === 'string' &&
typeof item.Profile === 'string',
)
if (isValid) {
// 处理有效的 JSON 数据
agentsStore.setAgents(
json.map((item) => ({
Name: item.Name,
Icon: item.Icon.replace(/\.png$/, ''),
Profile: item.Profile,
Classification: item.Classification,
})),
)
await api.setAgents(json.map((item) => pick(item, ['Name', 'Profile'])))
} else {
ElNotification.error({
title: '错误',
message: 'JSON 格式错误',
})
}
} else {
console.error('JSON is not an array')
ElNotification.error({
title: '错误',
message: 'JSON 格式错误',
})
// 在validateApiConfig函数中添加更详细的日志
const validateApiConfig = (agent: any) => {
const hasApiUrl = 'apiUrl' in agent
const hasApiKey = 'apiKey' in agent
const hasApiModel = 'apiModel' in agent
// 三个字段必须同时存在或同时不存在
if (hasApiUrl !== hasApiKey || hasApiKey !== hasApiModel) {
console.error('❌ API配置不完整:', {
agentName: agent.Name,
missingFields: {
apiUrl: !hasApiUrl,
apiKey: !hasApiKey,
apiModel: !hasApiModel
}
} catch (e) {
console.error(e)
})
return false
}
if (hasApiUrl && hasApiKey && hasApiModel) {
console.log('✅ API配置完整将使用自定义API配置:', {
apiUrl: agent.apiUrl,
apiKey: agent.apiKey ? '***' + agent.apiKey.slice(-4) : '未设置',
apiModel: agent.apiModel
})
} else {
console.log(' 未设置API配置将使用默认URL配置')
}
return true
}
const readFileContent = (file: File) => {
console.log('📁 开始读取文件:', file.name, '大小:', file.size, '类型:', file.type)
const reader = new FileReader()
reader.onload = e => {
try {
console.log('📄 文件读取完成开始解析JSON')
const content = e.target?.result as string
const jsonData = JSON.parse(content)
console.log('🔍 解析的JSON数据:', jsonData)
console.log(
'📊 数据类型:',
Array.isArray(jsonData) ? '数组' : '对象',
'长度:',
Array.isArray(jsonData) ? jsonData.length : 'N/A'
)
if (!Array.isArray(jsonData)) {
console.error('❌ JSON格式错误: 必须为数组格式')
ElMessage.error('JSON格式错误: 必须为数组格式')
return
}
console.log('🔍 开始验证智能体数据...')
const validAgents = jsonData.filter((agent, index) => {
console.log(`🔍 验证第${index + 1}个智能体:`, agent.Name || '未命名')
// 验证必需字段
if (!agent.Name || typeof agent.Name !== 'string') {
console.error(`❌ 智能体${index + 1}缺少Name字段或格式错误`)
return false
}
if (!agent.Icon || typeof agent.Icon !== 'string') {
console.error(`❌ 智能体${index + 1}缺少Icon字段或格式错误`)
return false
}
if (!agent.Profile || typeof agent.Profile !== 'string') {
console.error(`❌ 智能体${index + 1}缺少Profile字段或格式错误`)
return false
}
// 验证API配置
if (!validateApiConfig(agent)) {
console.error(`❌ 智能体${index + 1}API配置验证失败`)
return false
}
return true
})
console.log('🔄 开始处理智能体数据...')
// 修改发送到后端的数据
const processedAgents = validAgents.map(agent => ({
Name: agent.Name,
Profile: agent.Profile,
Icon: agent.Icon,
Classification: agent.Classification || '',
apiUrl: agent.apiUrl,
apiKey: agent.apiKey,
apiModel: agent.apiModel
}))
console.log(
'📤 发送到后端的智能体数据:',
processedAgents.map(a => ({
Name: a.Name,
apiUrl: a.apiUrl,
apiKey: a.apiKey ? '***' + a.apiKey.slice(-4) : '未设置',
apiModel: a.apiModel
}))
)
// 调用API
api
.setAgents(processedAgents)
.then(response => {
console.log('✅ 后端API调用成功响应:', response)
ElMessage.success('智能体上传成功')
})
.catch(error => {
console.error('❌ 后端API调用失败:', error)
ElMessage.error('智能体上传失败')
})
// 更新store
console.log('💾 更新智能体store...')
agentsStore.setAgents(processedAgents)
console.log('✅ 智能体store更新完成')
// 调用API
console.log('🌐 开始调用后端API...')
api
.setAgents(processedAgents)
.then(() => {
console.log('✅ 后端API调用成功')
ElMessage.success('智能体上传成功')
})
.catch(error => {
console.error('❌ 后端API调用失败:', error)
ElMessage.error('智能体上传失败')
})
} catch (error) {
console.error('❌ JSON解析错误:', error)
ElMessage.error('JSON解析错误')
}
}
reader.onerror = error => {
console.error('❌ 文件读取错误:', error)
ElMessage.error('文件读取错误')
}
reader.readAsText(file)
}
@@ -95,7 +195,7 @@ const agentList = computed(() => {
if (!agentsStore.agents.length) {
return {
selected,
unselected,
unselected
}
}
for (const agent of agentsStore.agents) {
@@ -110,14 +210,14 @@ const agentList = computed(() => {
obj[agent.Classification] = arr
unselected.push({
title: agent.Classification,
data: arr,
data: arr
})
}
}
return {
selected,
unselected: unselected,
unselected: unselected
}
})
</script>
@@ -126,7 +226,7 @@ const agentList = computed(() => {
<div class="agent-repo h-full flex flex-col" id="agent-repo">
<!-- 头部 -->
<div class="flex items-center justify-between">
<span class="text-[18px] font-bold">智能体库</span>
<span class="text-[18px] font-bold text-[var(--color-text-title-header)]">智能体库</span>
<!-- 上传文件 -->
<input type="file" accept=".json" @change="handleFileSelect" class="hidden" ref="fileInput" />
<div class="plus-button" @click="triggerFileSelect">
@@ -144,14 +244,22 @@ const agentList = computed(() => {
</div>
</div>
<!-- 底部提示栏 -->
<div class="w-full grid grid-cols-3 gap-x-[10px] bg-[#1d222b] rounded-[20px] p-[8px] mt-[10px]">
<div
class="w-full grid grid-cols-3 gap-x-[10px] bg-[var(--color-bg-indicator)] rounded-[20px] p-[8px] mt-[10px]"
>
<div
v-for="item in Object.values(agentMapDuty)"
:key="item.key"
class="flex items-center justify-center gap-x-1"
>
<div
class="w-[8px] h-[8px] rounded-full"
:style="{
background: item.color,
border: `1px solid ${item.border}`
}"
></div>
<span class="text-[12px]">{{ item.name }}</span>
<div class="w-[8px] h-[8px] rounded-full" :style="{ background: item.color }"></div>
</div>
</div>
</div>
@@ -162,7 +270,7 @@ const agentList = computed(() => {
padding: 0 8px;
.plus-button {
background: #1d2128;
background: var(--color-bg-tertiary);
width: 24px;
height: 24px;
padding: 0;
@@ -174,7 +282,7 @@ const agentList = computed(() => {
transition: all 0.3s ease;
&:hover {
background: #374151;
background: var(--color-bg-quaternary);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
}
}

View File

@@ -15,16 +15,14 @@ const md = new MarkdownIt({
html: true,
linkify: true,
typographer: true,
breaks: true,
breaks: true
})
function sanitize(str?: string) {
if (!str) {
return ''
}
const cleanStr = str
.replace(/\\n/g, '\n')
.replace(/\n\s*\d+\./g, '\n$&')
const cleanStr = str.replace(/\\n/g, '\n').replace(/\n\s*\d+\./g, '\n$&')
const html = md.render(cleanStr)
return html
// return DOMPurify.sanitize(html)
@@ -43,7 +41,7 @@ const data = computed<Data | null>(() => {
return {
Description: props.nodeId,
Content: sanitize(result.content),
LogNodeType: result.LogNodeType,
LogNodeType: result.LogNodeType
}
}
@@ -56,7 +54,7 @@ const data = computed<Data | null>(() => {
return {
Description: action.Description,
Content: sanitize(action.Action_Result),
LogNodeType: result.LogNodeType,
LogNodeType: result.LogNodeType
}
}
}
@@ -75,9 +73,11 @@ const data = computed<Data | null>(() => {
class="text-[16px] flex items-center gap-1 text-[var(--color-text-secondary)] mb-1"
>
{{ data.Description }}
<Iod v-if="data.LogNodeType !== 'object'"/>
<Iod v-if="data.LogNodeType !== 'object'" />
</div>
<div class="rounded-[8px] p-[15px] text-[14px] bg-[var(--color-bg-quaternary)]">
<div
class="rounded-[8px] p-[15px] text-[14px] bg-[var(--color-bg-result-detail)] text-[var(--color-text-detail)]"
>
<div
class="markdown-content max-h-[240px] overflow-y-auto max-w-full"
v-html="data.Content"

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { readConfig } from '@/utils/readJson.ts'
import { onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
interface Iod {
name: string
@@ -28,10 +28,22 @@ function handleNext() {
displayIndex.value++
}
}
function handlePrev() {
if (displayIndex.value === 0) {
displayIndex.value = data.value.length - 1
} else {
displayIndex.value--
}
}
</script>
<template>
<el-popover trigger="hover" width="440">
<el-popover
trigger="hover"
width="440"
popper-style="background-color: var(--color-bg-result); border: none;"
>
<template #reference>
<div
class="rounded-full w-[20px] h-[20px] bg-[var(--color-bg-quaternary)] flex justify-center items-center cursor-pointer"
@@ -40,34 +52,67 @@ function handleNext() {
</div>
</template>
<template #default v-if="data.length">
<div>
<div class="bg-[var(--color-bg-result)]">
<div class="flex justify-between items-center p-2 pb-0 rounded-[8px] text-[16px] font-bold">
<span>数联网搜索结果</span>
<div class="flex items-center gap-3">
<div>{{ `${displayIndex + 1}/${data.length}` }}</div>
<el-button type="primary" size="small" @click="handleNext">下一个</el-button>
<!-- <div>{{ `${displayIndex + 1}/${data.length}` }}</div>
<el-button type="primary" size="small" @click="handleNext">下一个</el-button> -->
<!-- 关闭 -->
<SvgIcon icon-class="close" size="15px" />
</div>
</div>
<!-- 分割线 -->
<div class="h-[1px] w-full bg-[#494B51] my-[8px]"></div>
<div class="p-2 pt-0">
<div class="flex items-center w-full gap-3">
<div class="font-bold w-[75px] text-right flex-shrink-0">名称:</div>
<div class="text-[var(--color-text-secondary)] flex-1 break-words">{{ displayIod.name }}</div>
</div>
<div class="flex items-center w-full gap-3">
<div class="font-bold w-[75px] text-right flex-shrink-0">数据空间:</div>
<div class="text-[var(--color-text-secondary)] lex-1 break-words">{{ displayIod.data_space }}</div>
</div>
<div class="flex items-center w-full gap-3">
<div class="font-bold w-[75px] text-right flex-shrink-0">DOID:</div>
<div class="text-[var(--color-text-secondary)] lex-1 break-words">{{ displayIod.doId }}</div>
</div>
<div class="flex items-center w-full gap-3">
<div class="font-bold w-[75px] text-right flex-shrink-0">来源仓库:</div>
<div class="text-[var(--color-text-secondary)] flex-1 break-words break-al">{{ displayIod.fromRepo }}</div>
</div>
</div>
<div class="p-2 pt-0">
<div class="flex items-center w-full gap-3">
<div class="font-bold w-[75px] text-right flex-shrink-0">名称:</div>
<div class="text-[var(--color-text-detail)] flex-1 break-words">
{{ displayIod.name }}
</div>
</div>
<div class="flex items-center w-full gap-3">
<div class="font-bold w-[75px] text-right flex-shrink-0">数据空间:</div>
<div class="text-[var(--color-text-detail)] lex-1 break-words">
{{ displayIod.data_space }}
</div>
</div>
<div class="flex items-center w-full gap-3">
<div class="font-bold w-[75px] text-right flex-shrink-0">DOID:</div>
<div class="text-[var(--color-text-detail)] lex-1 break-words">
{{ displayIod.doId }}
</div>
</div>
<div class="flex items-center w-full gap-3">
<div class="font-bold w-[75px] text-right flex-shrink-0">来源仓库:</div>
<div class="text-[var(--color-text-detail)] flex-1 break-words break-al">
{{ displayIod.fromRepo }}
</div>
</div>
<div>
<div
class="card-item w-[80px] h-[25px] flex justify-between items-center rounded-[25px] bg-[#b1b1b1] ml-auto px-2"
>
<div class="text-[14px] text-[#ffffff] font-medium">
{{ `${displayIndex + 1}/${data.length}` }}
</div>
<div class="flex items-center gap-1">
<svg-icon
icon-class="left"
size="15px"
@click="handlePrev"
class="cursor-pointer hover:opacity-70"
></svg-icon>
<svg-icon
icon-class="right"
size="15px"
@click="handleNext"
class="cursor-pointer hover:opacity-70"
></svg-icon>
</div>
</div>
</div>
</div>
</div>
</template>
</el-popover>

View File

@@ -9,7 +9,7 @@ import { type ConnectArg, Jsplumb } from '@/layout/components/Main/TaskTemplate/
import variables from '@/styles/variables.module.scss'
import { type IRawStepTask, useAgentsStore } from '@/stores'
import api from '@/api'
import ProcessCard from '../TaskProcess/ProcessCard.vue'
import ExecutePlan from './ExecutePlan.vue'
const emit = defineEmits<{
@@ -18,21 +18,68 @@ const emit = defineEmits<{
}>()
const agentsStore = useAgentsStore()
const drawerVisible = ref(false)
const collaborationProcess = computed(() => {
return agentsStore.agentRawPlan.data?.['Collaboration Process'] ?? []
})
// 编辑逻辑
const editMode = ref(false) //全局编辑开关
const editMap = reactive<Record<string, boolean>>({}) //行级编辑状态
const editBuffer = reactive<Record<string, string | undefined>>({}) //临时输入
function getProcessDescription(stepId: string, processId: string) {
const step = collaborationProcess.value.find(s => s.Id === stepId)
if (step) {
const process = step.TaskProcess.find(p => p.ID === processId)
return process?.Description || ''
}
return ''
}
function save() {
Object.keys(editMap).forEach(key => {
if (editMap[key]) {
const [stepId, processId] = key.split('-')
const value = editBuffer[key]
// 确保 value 是字符串类型
if (value !== undefined && value !== null) {
// @ts-ignore - TypeScript 无法正确推断类型,但运行时是安全的
handleSaveEdit(stepId, processId, value)
}
}
})
editMode.value = false
}
function handleOpenEdit(stepId: string, processId: string) {
if (!editMode.value) return
const key = `${stepId}-${processId}`
editMap[key] = true
editBuffer[key] = getProcessDescription(stepId, processId)
}
function handleSaveEdit(stepId: string, processId: string, value: string) {
const key = `${stepId}-${processId}`
const step = collaborationProcess.value.find(s => s.Id === stepId)
if (step) {
const process = step.TaskProcess.find(p => p.ID === processId)
if (process) {
process.Description = value
}
}
editMap[key] = false
ElMessage.success('已保存(前端内存)')
}
const jsplumb = new Jsplumb('task-results-main', {
connector: {
type: BezierConnector.type,
options: { curviness: 30, stub: 10 },
},
options: { curviness: 30, stub: 10 }
}
})
// 操作折叠面板时要实时的刷新连线
let timer: number
let timer: ReturnType<typeof setInterval> | null = null
function handleCollapse() {
if (timer) {
clearInterval(timer)
@@ -40,11 +87,14 @@ function handleCollapse() {
timer = setInterval(() => {
jsplumb.repaintEverything()
emit('refreshLine')
}, 1)
}, 1) as ReturnType<typeof setInterval>
// 默认三秒后已经完全打开
const timer1 = setTimeout(() => {
clearInterval(timer)
if (timer) {
clearInterval(timer)
timer = null
}
}, 3000)
onUnmounted(() => {
@@ -61,14 +111,14 @@ function handleCollapse() {
function createInternalLine(id?: string) {
const arr: ConnectArg[] = []
jsplumb.reset()
collaborationProcess.value.forEach((item) => {
collaborationProcess.value.forEach(item => {
// 创建左侧流程与产出的连线
arr.push({
sourceId: `task-results-${item.Id}-0`,
targetId: `task-results-${item.Id}-1`,
anchor: [AnchorLocations.Left, AnchorLocations.Left],
anchor: [AnchorLocations.Left, AnchorLocations.Left]
})
collaborationProcess.value.forEach((jitem) => {
collaborationProcess.value.forEach(jitem => {
// 创建左侧产出与上一步流程的连线
if (item.InputObject_List!.includes(jitem.OutputObject ?? '')) {
arr.push({
@@ -76,12 +126,12 @@ function createInternalLine(id?: string) {
targetId: `task-results-${item.Id}-0`,
anchor: [AnchorLocations.Left, AnchorLocations.Left],
config: {
type: 'output',
},
type: 'output'
}
})
}
// 创建右侧任务程序与InputObject字段的连线
jitem.TaskProcess.forEach((i) => {
jitem.TaskProcess.forEach(i => {
if (i.ImportantInput?.includes(`InputObject:${item.OutputObject}`)) {
const color = getActionTypeDisplay(i.ActionType)?.color ?? ''
const sourceId = `task-results-${item.Id}-1`
@@ -93,21 +143,21 @@ function createInternalLine(id?: string) {
config: {
stops: [
[0, color],
[1, color],
[1, color]
],
transparent: targetId !== id,
},
transparent: targetId !== id
}
})
}
})
})
// 创建右侧TaskProcess内部连线
item.TaskProcess?.forEach((i) => {
item.TaskProcess?.forEach(i => {
if (!i.ImportantInput?.length) {
return
}
item.TaskProcess?.forEach((i2) => {
item.TaskProcess?.forEach(i2 => {
if (i.ImportantInput.includes(`ActionResult:${i2.ID}`)) {
const color = getActionTypeDisplay(i.ActionType)?.color ?? ''
const sourceId = `task-results-${item.Id}-0-${i2.ID}`
@@ -119,10 +169,10 @@ function createInternalLine(id?: string) {
config: {
stops: [
[0, color],
[1, color],
[1, color]
],
transparent: targetId !== id,
},
transparent: targetId !== id
}
})
}
})
@@ -134,6 +184,14 @@ function createInternalLine(id?: string) {
}
const loading = ref(false)
// 额外产物编辑状态
const editingOutputId = ref<string | null>(null)
const editingOutputContent = ref('')
// 额外产物内容存储
const additionalOutputContents = ref<Record<string, string>>({})
async function handleRun() {
try {
loading.value = true
@@ -144,9 +202,51 @@ async function handleRun() {
}
}
// 查看任务过程
async function handleTaskProcess() {
drawerVisible.value = true
}
// 开始编辑额外产物内容
function startOutputEditing(output: string) {
editingOutputId.value = output
editingOutputContent.value = getAdditionalOutputContent(output) || ''
}
// 保存额外产物内容
function saveOutputEditing() {
if (editingOutputId.value && editingOutputContent.value.trim()) {
additionalOutputContents.value[editingOutputId.value] = editingOutputContent.value.trim()
}
editingOutputId.value = null
editingOutputContent.value = ''
}
// 取消编辑额外产物内容
function cancelOutputEditing() {
editingOutputId.value = null
editingOutputContent.value = ''
}
// 获取额外产物内容
function getAdditionalOutputContent(output: string) {
return additionalOutputContents.value[output] || ''
}
// 处理额外产物的键盘事件
function handleOutputKeydown(event: KeyboardEvent) {
if (event.key === 'Enter') {
event.preventDefault()
saveOutputEditing()
} else if (event.key === 'Escape') {
editingOutputId.value = null
editingOutputContent.value = ''
}
}
// 添加滚动状态标识
const isScrolling = ref(false)
let scrollTimer: number
let scrollTimer: ReturnType<typeof setTimeout> | null = null
// 修改滚动处理函数
function handleScroll() {
@@ -161,11 +261,11 @@ function handleScroll() {
// 设置滚动结束检测
scrollTimer = setTimeout(() => {
isScrolling.value = false
}, 300)
}, 300) as ReturnType<typeof setTimeout>
}
// 修改鼠标事件处理函数
const handleMouseEnter = throttle((id) => {
const handleMouseEnter = throttle(id => {
if (!isScrolling.value) {
createInternalLine(id)
}
@@ -181,34 +281,160 @@ function clear() {
jsplumb.reset()
}
// ========== 按钮交互状态管理 ==========
const buttonHoverState = ref<'process' | 'execute' | null>(null)
let buttonHoverTimer: ReturnType<typeof setTimeout> | null = null
const handleProcessMouseEnter = () => {
if (buttonHoverTimer) {
clearTimeout(buttonHoverTimer)
buttonHoverTimer = null
}
buttonHoverState.value = 'process'
}
const handleExecuteMouseEnter = () => {
if (buttonHoverTimer) {
clearTimeout(buttonHoverTimer)
buttonHoverTimer = null
}
if (agentsStore.agentRawPlan.data) {
buttonHoverState.value = 'execute'
}
}
const handleButtonMouseLeave = () => {
// 添加防抖,防止快速切换时的抖动
if (buttonHoverTimer) {
clearTimeout(buttonHoverTimer)
}
buttonHoverTimer = setTimeout(() => {
buttonHoverState.value = null
}, 50) // 适当减少延迟时间
}
// 添加离开组件时的清理
onUnmounted(() => {
if (buttonHoverTimer) {
clearTimeout(buttonHoverTimer)
}
})
// 计算按钮类名
const processBtnClass = computed(() => {
return buttonHoverState.value === 'process' ? 'ellipse' : 'circle'
})
const executeBtnClass = computed(() => {
// 鼠标悬停在过程按钮上时,执行按钮变圆形
if (buttonHoverState.value === 'process') {
return 'circle'
}
// 其他情况:如果有任务数据就显示椭圆形,否则显示圆形
return agentsStore.agentRawPlan.data ? 'ellipse' : 'circle'
})
// 计算按钮是否显示文字
const showProcessText = computed(() => {
return buttonHoverState.value === 'process'
})
const showExecuteText = computed(() => {
// 鼠标悬停在过程按钮上时,执行按钮不显示文字
if (buttonHoverState.value === 'process') return false
// 其他情况:如果有任务数据就显示文字,否则不显示
return agentsStore.agentRawPlan.data
})
// 计算按钮标题
const processBtnTitle = computed(() => {
return buttonHoverState.value === 'process' ? '任务过程' : '点击查看任务过程'
})
const executeBtnTitle = computed(() => {
return showExecuteText.value ? '任务执行' : '点击运行'
})
defineExpose({
createInternalLine,
clear,
clear
})
</script>
<template>
<div class="h-full flex flex-col relative" id="task-results">
<div
class="h-full flex flex-col relative"
id="task-results"
:class="{ 'is-running': agentsStore.executePlan.length > 0 }"
>
<!-- 标题与执行按钮 -->
<div class="text-[18px] font-bold mb-[18px] flex justify-between items-center px-[20px]">
<span>执行结果</span>
<div class="flex items-center gap-[14px]">
<el-button circle :color="variables.tertiary" disabled title="点击刷新">
<svg-icon icon-class="refresh" />
<span class="text-[var(--color-text-title-header)]">执行结果</span>
<div
class="flex items-center gap-[14px] task-button-group"
@mouseleave="handleButtonMouseLeave"
>
<!-- 任务过程按钮 -->
<el-button
:class="processBtnClass"
:color="variables.tertiary"
:title="processBtnTitle"
@mouseenter="handleProcessMouseEnter"
@click="handleTaskProcess"
>
<svg-icon icon-class="process" />
<span v-if="showProcessText" class="btn-text">任务过程</span>
</el-button>
<el-popover :disabled="Boolean(agentsStore.agentRawPlan.data)" title="请先输入要执行的任务">
<!-- 任务执行按钮 -->
<el-popover
:disabled="Boolean(agentsStore.agentRawPlan.data)"
title="请先输入要执行的任务"
:visible="showPopover"
@hide="showPopover = false"
>
<template #reference>
<el-button
circle
:class="executeBtnClass"
:color="variables.tertiary"
title="点击运行"
:title="executeBtnTitle"
:disabled="!agentsStore.agentRawPlan.data"
@mouseenter="handleExecuteMouseEnter"
@click="handleRun"
>
<svg-icon icon-class="action" />
<span v-if="showExecuteText" class="btn-text">任务执行</span>
</el-button>
</template>
</el-popover>
<el-drawer
v-model="drawerVisible"
title="任务过程"
direction="rtl"
size="30%"
:destroy-on-close="false"
>
<!-- 头部工具栏 -->
<template #header>
<div class="drawer-header">
<span class="title">任务过程</span>
<!-- <el-button v-if="!editMode" text icon="Edit" @click="editMode = true" />
<el-button v-else text icon="Check" @click="save" /> -->
</div>
</template>
<el-scrollbar height="calc(100vh - 120px)">
<el-empty v-if="!collaborationProcess.length" description="暂无任务过程" />
<div v-else class="process-list">
<!-- 使用ProcessCard组件显示每个AgentSelection -->
<ProcessCard
v-for="step in collaborationProcess"
:key="step.Id"
:step="step"
@open-edit="handleOpenEdit"
@save-edit="handleSaveEdit"
/>
</div>
</el-scrollbar>
</el-drawer>
</div>
</div>
<!-- 内容 -->
@@ -218,11 +444,12 @@ defineExpose({
@scroll="handleScroll"
>
<div id="task-results-main" class="px-[40px] relative">
<!-- 原有的流程和产物 -->
<div v-for="item in collaborationProcess" :key="item.Id" class="card-item">
<el-card
class="card-item w-full relative"
:class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''"
shadow="hover"
:shadow="true"
:id="`task-results-${item.Id}-0`"
@click="emit('setCurrentTask', item)"
>
@@ -244,7 +471,14 @@ defineExpose({
<span></span>
</template>
<template #title>
<div class="flex items-center gap-[15px]">
<!-- 运行之前背景颜色是var(--color-bg-detail-list),运行之后背景颜色是var(--color-bg-detail-list-run) -->
<div
class="flex items-center gap-[15px] rounded-[20px]"
:class="{
'bg-[var(--color-bg-detail-list)]': !agentsStore.executePlan.length,
'bg-[var(--color-bg-detail-list-run)]': agentsStore.executePlan.length
}"
>
<!-- 右侧链接点 -->
<div
class="absolute right-0 top-1/2 transform -translate-y-1/2"
@@ -256,12 +490,19 @@ defineExpose({
>
<svg-icon
:icon-class="getAgentMapIcon(item1.AgentName).icon"
color="var(--color-text)"
color="#fff"
size="24px"
/>
</div>
<div class="text-[16px]">
<span>{{ item1.AgentName }}:&nbsp; &nbsp;</span>
<span
:class="{
'text-[var(--color-text-result-detail)]': !agentsStore.executePlan.length,
'text-[var(--color-text-result-detail-run)]':
agentsStore.executePlan.length
}"
>{{ item1.AgentName }}:&nbsp; &nbsp;</span
>
<span :style="{ color: getActionTypeDisplay(item1.ActionType)?.color }">
{{ getActionTypeDisplay(item1.ActionType)?.name }}
</span>
@@ -279,7 +520,7 @@ defineExpose({
<el-card
class="card-item w-full relative output-object-card"
shadow="hover"
:shadow="true"
:class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''"
:id="`task-results-${item.Id}-1`"
@click="emit('setCurrentTask', item)"
@@ -297,31 +538,114 @@ defineExpose({
<span></span>
</template>
<template #title>
<div class="text-[18px]">{{ item.OutputObject }}</div>
<div
class="text-[18px]"
:class="{
'text-[var(--color-text-result-detail)]': !agentsStore.executePlan.length,
'text-[var(--color-text-result-detail-run)]': agentsStore.executePlan.length
}"
>
{{ item.OutputObject }}
</div>
</template>
<ExecutePlan :node-id="item.OutputObject" :execute-plans="agentsStore.executePlan" />
<ExecutePlan
:node-id="item.OutputObject"
:execute-plans="agentsStore.executePlan"
/>
</el-collapse-item>
</el-collapse>
</el-card>
</div>
<!-- 额外产物的编辑卡片 -->
<div
v-for="(output, index) in agentsStore.additionalOutputs"
:key="`additional-output-${index}`"
class="card-item"
>
<!-- 空的流程卡片位置 -->
<div class="w-full"></div>
<!-- 额外产物的编辑卡片 -->
<el-card
class="card-item w-full relative output-object-card additional-output-card"
:shadow="false"
:id="`additional-output-results-${index}`"
>
<!-- 产物名称行 -->
<div class="text-[18px] mb-3">
{{ output }}
</div>
<!-- 编辑区域行 -->
<div class="additional-output-editor">
<div v-if="editingOutputId === output" class="w-full">
<!-- 编辑状态输入框 + 按钮 -->
<div class="flex flex-col gap-3">
<el-input
v-model="editingOutputContent"
type="textarea"
:autosize="{ minRows: 3, maxRows: 6 }"
placeholder="请输入产物内容"
@keydown="handleOutputKeydown"
class="output-editor"
size="small"
/>
<div class="flex justify-end gap-2">
<el-button @click="saveOutputEditing" type="primary" size="small" class="px-3">
</el-button>
<el-button @click="cancelOutputEditing" size="small" class="px-3">
×
</el-button>
</div>
</div>
</div>
<div v-else class="w-full">
<!-- 非编辑状态折叠区域 + 编辑按钮 -->
<div
class="flex items-center justify-between p-3 bg-[var(--color-bg-quinary)] rounded-[8px]"
>
<div
class="text-[14px] text-[var(--color-text-secondary)] output-content-display"
>
{{ getAdditionalOutputContent(output) || '暂无内容,点击编辑' }}
</div>
<el-button
@click="startOutputEditing(output)"
size="small"
type="primary"
plain
class="flex items-center gap-1"
>
<svg-icon icon-class="action" size="12px" />
<span>编辑</span>
</el-button>
</div>
</div>
</div>
</el-card>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
#task-results.is-running {
--color-bg-detail-list: var(--color-bg-detail-list-run); // 直接指向 100 % 版本
}
#task-results {
:deep(.el-collapse) {
border: none;
border-radius: 20px;
.el-collapse-item + .el-collapse-item {
margin-top: 10px;
}
.el-collapse-item__header {
border: none;
background: var(--color-bg-secondary);
background: var(--color-bg-detail-list-run);
min-height: 41px;
line-height: 41px;
border-radius: 20px;
@@ -329,13 +653,16 @@ defineExpose({
position: relative;
.el-collapse-item__title {
background: var(--color-bg-secondary);
background: var(--color-bg-detail-list);
border-radius: 20px;
}
.el-icon {
font-size: 20px;
font-weight: bold;
font-weight: 900;
background: var(--color-bg-icon-rotate);
border-radius: 50px;
color: #d8d8d8;
}
&.is-active {
@@ -357,7 +684,7 @@ defineExpose({
background: none;
.card-item {
background: var(--color-bg-secondary);
background: var(--color-bg-detail);
padding: 25px;
padding-top: 10px;
border-radius: 7px;
@@ -367,7 +694,7 @@ defineExpose({
.el-collapse-item__wrap {
border: none;
background: var(--color-bg-secondary);
background: var(--color-bg-detail-list);
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
}
@@ -376,6 +703,10 @@ defineExpose({
:deep(.el-card) {
.el-card__body {
padding-right: 40px;
background-color: var(--color-bg-detail);
&:hover {
background-color: var(--color-card-bg-result-hover);
}
}
}
@@ -388,13 +719,176 @@ defineExpose({
}
.active-card {
background:
linear-gradient(var(--color-bg-tertiary), var(--color-bg-tertiary)) padding-box,
background: linear-gradient(var(--color-bg-tertiary), var(--color-bg-tertiary)) padding-box,
linear-gradient(to right, #00c8d2, #315ab4) border-box;
}
.card-item + .card-item {
margin-top: 10px;
}
.additional-output-card {
border: 1px dashed #dcdfe6;
opacity: 0.9;
box-shadow: var(--color-agent-list-hover-shadow);
&:hover {
border-color: #409eff;
opacity: 1;
}
:deep(.el-card__body) {
padding: 20px;
}
// 编辑区域样式调整
.el-collapse {
border: none;
.el-collapse-item {
.el-collapse-item__header {
background: var(--color-bg-detail);
min-height: 36px;
line-height: 36px;
border-radius: 8px;
.el-collapse-item__title {
background: transparent;
font-size: 14px;
padding-left: 0;
}
.el-icon {
font-size: 16px;
}
}
.el-collapse-item__wrap {
background: var(--color-bg-detail);
border-radius: 0 0 8px 8px;
}
}
}
}
// 额外产物编辑区域样式
.additional-output-editor {
.output-editor {
:deep(.el-textarea__inner) {
font-size: 14px;
color: var(--color-text-primary);
background: var(--color-bg-detail);
border: 1px solid #dcdfe6;
resize: none;
padding: 12px;
}
}
.output-content-display {
word-break: break-word;
white-space: pre-wrap;
transition: all 0.2s ease;
line-height: 1.5;
min-height: 20px;
&:hover {
background-color: rgba(0, 0, 0, 0.05);
border-color: #409eff;
opacity: 1;
}
}
// 编辑按钮样式
.el-button {
font-weight: bold;
font-size: 16px;
border-radius: 4px;
&.el-button--small {
padding: 4px 12px;
}
}
}
// ========== 新增:按钮交互样式 ==========
.task-button-group {
.el-button {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important;
overflow: hidden !important;
white-space: nowrap !important;
border: none !important;
color: var(--color-text-primary) !important;
position: relative;
background-color: var(--color-bg-tertiary);
&:hover {
transform: translateY(-2px) !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
filter: brightness(1.1) !important;
}
&.is-disabled {
opacity: 0.5;
cursor: not-allowed !important;
&:hover {
transform: none !important;
box-shadow: none !important;
filter: none !important;
}
}
}
// 圆形状态
.circle {
width: 40px !important;
height: 40px !important;
min-width: 40px !important;
padding: 0 !important;
border-radius: 50% !important;
.btn-text {
display: none !important;
}
}
// 椭圆形状态
.ellipse {
height: 40px !important;
border-radius: 20px !important;
padding: 0 16px !important;
gap: 8px;
.btn-text {
display: inline-block !important;
font-size: 14px;
font-weight: 500;
margin-left: 4px;
opacity: 1;
animation: fadeIn 0.3s ease forwards;
}
}
.btn-text {
font-size: 14px;
font-weight: 500;
margin-left: 4px;
opacity: 0;
animation: fadeIn 0.3s ease forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateX(-5px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
}
}
</style>

View File

@@ -1,31 +1,26 @@
<template>
<div class="absolute inset-0 flex items-start gap-[14%]">
<!-- 左侧元素 -->
<!-- 左侧元素 -->
<div class="flex-1 relative h-full flex justify-center">
<!-- 背景那一根线 -->
<div
class="h-full bg-[var(--color-bg-tertiary)] w-[5px]"
>
<div class="h-full bg-[var(--color-bg-flow)] w-[5px]">
<!-- 线底部的小圆球 -->
<div
class="absolute bottom-0 left-1/2 transform -translate-x-1/2 bg-[var(--color-bg-tertiary)] w-[15px] h-[15px] rounded-full"
class="absolute bottom-0 left-1/2 transform -translate-x-1/2 bg-[var(--color-bg-flow)] w-[15px] h-[15px] rounded-full"
></div>
</div>
</div>
<!-- 右侧元素 -->
<!-- 右侧元素 -->
<div class="flex-1 relative h-full flex justify-center">
<!-- 背景那一根线 -->
<div
class="h-full bg-[var(--color-bg-tertiary)] w-[5px]"
>
<div class="h-full bg-[var(--color-bg-flow)] w-[5px]">
<!-- 线底部的小圆球 -->
<div
class="absolute bottom-0 left-1/2 transform -translate-x-1/2 bg-[var(--color-bg-tertiary)] w-[15px] h-[15px] rounded-full"
class="absolute bottom-0 left-1/2 transform -translate-x-1/2 bg-[var(--color-bg-flow)] w-[15px] h-[15px] rounded-full"
></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
</script>
<script setup lang="ts"></script>

View File

@@ -3,10 +3,9 @@ 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 } from 'vue'
import { computed, ref } from 'vue'
import { AnchorLocations } from '@jsplumb/browser-ui'
import MultiLineTooltip from '@/components/MultiLineTooltip/index.vue'
import Bg from './Bg.vue'
const emit = defineEmits<{
@@ -26,6 +25,90 @@ const collaborationProcess = computed(() => {
return agentsStore.agentRawPlan.data?.['Collaboration Process'] ?? []
})
// 编辑状态管理
const editingTaskId = ref<string | null>(null)
const editingContent = ref('')
// 添加新产物状态管理
const showAddOutputForm = ref(false)
const newOutputName = ref('')
// 开始编辑
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()
}
}
// 显示添加产物表单
const showAddOutputDialog = () => {
showAddOutputForm.value = true
newOutputName.value = ''
}
// 添加新产物
const addNewOutput = () => {
if (newOutputName.value.trim()) {
const success = agentsStore.addNewOutput(newOutputName.value.trim())
if (success) {
showAddOutputForm.value = false
newOutputName.value = ''
}
}
}
// 取消添加产物
const cancelAddOutput = () => {
showAddOutputForm.value = false
newOutputName.value = ''
}
// 处理添加产物的键盘事件
const handleAddOutputKeydown = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault()
addNewOutput()
} else if (event.key === 'Escape') {
cancelAddOutput()
}
}
// 删除额外产物
const removeAdditionalOutput = (output: string) => {
agentsStore.removeAdditionalOutput(output)
}
function handleCurrentTask(task: IRawStepTask, transparent: boolean): ConnectArg[] {
// 创建当前流程与产出的连线
const arr: ConnectArg[] = [
@@ -34,14 +117,14 @@ function handleCurrentTask(task: IRawStepTask, transparent: boolean): ConnectArg
targetId: `task-syllabus-output-object-${task.Id}`,
anchor: [AnchorLocations.Right, AnchorLocations.Left],
config: {
transparent,
},
},
transparent
}
}
]
// 创建当前产出与流程的连线
task.InputObject_List?.forEach((item) => {
const id = collaborationProcess.value.find((i) => i.OutputObject === item)?.Id
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}`,
@@ -49,8 +132,8 @@ function handleCurrentTask(task: IRawStepTask, transparent: boolean): ConnectArg
anchor: [AnchorLocations.Left, AnchorLocations.Right],
config: {
type: 'output',
transparent,
},
transparent
}
})
}
})
@@ -61,7 +144,7 @@ function handleCurrentTask(task: IRawStepTask, transparent: boolean): ConnectArg
function changeTask(task?: IRawStepTask, isEmit?: boolean) {
jsplumb.reset()
const arr: ConnectArg[] = []
agentsStore.agentRawPlan.data?.['Collaboration Process']?.forEach((item) => {
agentsStore.agentRawPlan.data?.['Collaboration Process']?.forEach(item => {
arr.push(...handleCurrentTask(item, item.Id !== task?.Id))
})
jsplumb.connects(arr)
@@ -76,26 +159,32 @@ function clear() {
defineExpose({
changeTask,
clear,
clear
})
</script>
<template>
<div class="h-full flex flex-col">
<div class="text-[18px] font-bold mb-[18px]">任务大纲</div>
<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">
<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-[45%] h-[41px] flex justify-center relative z-99 items-center rounded-[20px] bg-[var(--color-bg-tertiary)]"
class="card-item w-[168px] h-[41px] flex justify-center relative z-99 items-center rounded-[20px] bg-[var(--color-bg-flow)]"
>
流程
</div>
@@ -103,7 +192,7 @@ defineExpose({
<div class="flex-1 flex justify-center">
<div
class="card-item w-[45%] h-[41px] flex justify-center relative z-99 items-center rounded-[20px] bg-[var(--color-bg-tertiary)]"
class="card-item w-[168px] h-[41px] flex justify-center relative z-99 items-center rounded-[20px] bg-[var(--color-bg-flow)]"
>
产物
</div>
@@ -113,31 +202,50 @@ defineExpose({
<div
v-for="item in collaborationProcess"
:key="item.Id"
class="card-item w-full flex items-center gap-[14%]"
class="card-it w-full flex items-center gap-[14%] bg-[var(--color-card-bg)]"
>
<!-- 流程卡片 -->
<el-card
class="w-[43%] overflow-y-auto relative z-99 task-syllabus-flow-card"
:class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''"
shadow="hover"
: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>
<div class="text-[18px] font-bold text-center">{{ item.StepName }}</div>
</MultiLineTooltip>
<div class="h-[1px] w-full bg-[#494B51] my-[8px]"></div>
<MultiLineTooltip placement="right" :text="item.StepName" :lines="3">
<div
class="text-[14px] text-[var(--color-text-secondary)]"
:title="item.TaskContent"
>
{{ item.TaskContent }}
<div class="h-[1px] w-full bg-[#d6d6d6] 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 gap-2">
<el-button @click="saveEditing" type="primary" size="small" class="px-3">
</el-button>
<el-button @click="cancelEditing" size="small" class="px-3"> × </el-button>
</div>
</div>
</MultiLineTooltip>
<div class="h-[1px] w-full bg-[#494B51] my-[8px]"></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-[#d6d6d6] my-[8px]"></div>
<div
class="flex items-center gap-2 overflow-y-auto flex-wrap relative w-full max-h-[72px]"
>
@@ -157,9 +265,7 @@ defineExpose({
<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
}}
{{ item.TaskProcess.find(i => i.AgentName === agentSelection)?.Description }}
</div>
</div>
</template>
@@ -169,29 +275,106 @@ defineExpose({
>
<svg-icon
:icon-class="getAgentMapIcon(agentSelection).icon"
color="var(--color-text)"
color="#fff"
size="24px"
/>
</div>
</el-tooltip>
</div>
</el-card>
<!-- 产物卡片 -->
<!-- 产物卡片 -->
<el-card
class="w-[43%] relative"
shadow="hover"
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
v-for="(output, index) in agentsStore.additionalOutputs"
:key="`additional-${index}`"
class="card-item w-full flex items-center gap-[14%]"
> -->
<!-- 空的流程卡片位置 -->
<!-- <div class="w-[43%]"></div> -->
<!-- 额外产物卡片 -->
<!-- <el-card class="w-[43%] relative additional-output-card group" :shadow="false">
<div class="flex items-center justify-between">
<div class="text-[18px] font-bold text-center flex-1">{{ output }}</div>
<button
@click="removeAdditionalOutput(output)"
class="opacity-0 group-hover:opacity-100 text-red-500 hover:text-red-700 transition-all duration-200 ml-2 text-lg font-bold w-5 h-5 flex items-center justify-center rounded hover:bg-red-50"
>
×
</button>
</div>
</el-card>
</div> -->
<!-- 添加新产物按钮 -->
<!-- <div class="card-item w-full flex items-center gap-[14%] mt-[20px]"> -->
<!-- 空的流程卡片位置 -->
<!-- <div class="w-[43%]"></div> -->
<!-- 添加新产物按钮 -->
<!-- <div class="w-[43%] relative"> -->
<!-- <div v-if="!showAddOutputForm" class="add-output-btn">
<button
@click="showAddOutputDialog"
class="w-full h-[50px] border-2 border-dashed border-gray-300 rounded-lg flex items-center justify-center text-gray-500 hover:border-blue-500 hover:text-blue-500 transition-all duration-200 group"
>
<svg-icon icon-class="plus" size="20px" class="mr-2" />
<span class="text-sm font-medium"></span>
</button>
</div> -->
<!-- 添加产物表单 -->
<!-- <div v-else class="add-output-form">
<el-card class="w-full" shadow="hover">
<div class="p-3">
<el-input
v-model="newOutputName"
placeholder="输入产物名称"
size="small"
@keydown="handleAddOutputKeydown"
@blur="addNewOutput"
class="w-full mb-3"
/>
<div class="flex gap-2">
<el-button @click="addNewOutput" type="primary" size="small" class="flex-1">
确定
</el-button>
<el-button @click="cancelAddOutput" size="small" class="flex-1">
取消
</el-button>
</div>
</div>
</el-card>
</div> -->
<!-- </div> -->
<!-- </div> -->
<!-- <div>
<el-button type="info" size="medium" class="flex-1"> branch </el-button>
</div> -->
</div>
</div>
</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;
// box-shadow: var(--color-card-shadow);
margin-bottom: 100px;
&: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;
@@ -201,4 +384,150 @@ defineExpose({
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;
// box-shadow: var(--color-card-shadow-hover);
&: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;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
// 添加产物表单样式
:deep(.el-card__body) {
padding: 16px;
}
// 输入框样式
: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>

View File

@@ -5,7 +5,7 @@ import TaskResult from './TaskResult/index.vue'
import { Jsplumb } from './utils.ts'
import { type IRawStepTask, useAgentsStore } from '@/stores'
import { BezierConnector } from '@jsplumb/browser-ui'
import { ref } from 'vue'
const agentsStore = useAgentsStore()
// 智能体库
@@ -15,9 +15,9 @@ const agentRepoJsplumb = new Jsplumb('task-template', {
options: {
curviness: 30, // 曲线弯曲程度
stub: 20, // 添加连接点与端点的距离
alwaysRespectStubs: true,
},
},
alwaysRespectStubs: true
}
}
})
// 任务流程
@@ -32,18 +32,16 @@ const taskResultRef = ref<{
}>()
const taskResultJsplumb = new Jsplumb('task-template')
function scrollToElementTop(elementId: string) {
const element = document.getElementById(elementId);
const element = document.getElementById(elementId)
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
})
}
}
function handleTaskSyllabusCurrentTask(task: IRawStepTask) {
scrollToElementTop(`task-results-${task.Id}-0`)
agentsStore.setCurrentTask(task)
@@ -76,13 +74,14 @@ function clear() {
defineExpose({
changeTask,
resetAgentRepoLine,
clear,
clear
})
</script>
<template>
<!-- 删除overflow-hidden -->
<div
class="task-template flex gap-6 items-center h-[calc(100%-84px)] relative overflow-hidden"
class="task-template flex gap-6 items-center h-[calc(100%-84px)] relative"
id="task-template"
>
<!-- 智能体库 -->
@@ -111,10 +110,10 @@ defineExpose({
<style scoped lang="scss">
.task-template {
& > div {
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.8);
box-shadow: var(--color-card-shadow-three);
border-radius: 24px;
border: 1px solid #414752;
background: var(--color-bg-quinary);
border: 1px solid var(--color-card-border-three);
background: var(--color-bg-three);
padding-top: 20px;
padding-bottom: 20px;
}

View File

@@ -87,8 +87,8 @@ export class Jsplumb {
const stops = _config.stops ?? this.getStops(config.type)
// 如果config.transparent为true则将stops都加一些透明度
if (config.transparent) {
stops[0][1] = stops[0][1] + '30'
stops[1][1] = stops[1][1] + '30'
stops[0][1] = stops[0][1] + '80'
stops[1][1] = stops[1][1] + '80'
}
if (targetElement && sourceElement) {
this.instance.connect({