feat:rename subtree from frontend-vue to frontend

This commit is contained in:
zhaoweijie
2025-11-20 09:56:51 +08:00
parent 1aa9e280b0
commit ab8c9e294d
80 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,141 @@
<script setup lang="ts">
import { getActionTypeDisplay, getAgentMapIcon } from '@/layout/components/config.ts'
import SvgIcon from '@/components/SvgIcon/index.vue'
import { type Agent, useAgentsStore } from '@/stores'
import { v4 as uuidv4 } from 'uuid'
const porps = defineProps<{
agentList: Agent[]
}>()
const taskProcess = computed(() => {
const list = agentsStore.currentTask?.TaskProcess ?? []
return list.map((item) => ({
...item,
key: uuidv4(),
}))
})
const agentsStore = useAgentsStore()
</script>
<template>
<div
v-for="item in porps.agentList"
:key="item.Name"
class="user-item"
:class="agentsStore.currentTask?.AgentSelection?.includes(item.Name) ? 'active-card' : ''"
>
<div class="flex items-center justify-between relative h-[41px]">
<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"
/>
</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
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)"
:key="item1.key"
class="w-[6px] h-[6px] rounded-full"
:style="{ background: getActionTypeDisplay(item1.ActionType)?.color }"
></div>
</div>
</div>
</div>
<!-- 职责信息只有当执行流程中有当前智能体并且鼠标移入时才显示 -->
<div class="duty-info">
<div class="w-full flex justify-center">
<div
class="rounded-[9px] bg-[var(--color-bg-quaternary)] text-[12px] py-0.5 px-5 text-center my-2"
>
当前职责
</div>
</div>
<div class="p-[8px] pt-0">
<div
v-for="(item1, index1) in taskProcess.filter((i) => i.AgentName === item.Name)"
:key="item1.key"
class="text-[12px]"
>
<div>
<div class="mx-1 inline-block h-[14px]">
<div
:style="{ background: getActionTypeDisplay(item1.ActionType)?.color }"
class="w-[6px] h-[6px] rounded-full mt-[7px]"
></div>
</div>
<span :style="{ color: getActionTypeDisplay(item1.ActionType)?.color }"
>{{ getActionTypeDisplay(item1.ActionType)?.name }}</span
>
<span>{{ item1.Description }}</span>
</div>
<!-- 分割线 -->
<div
v-if="index1 !== taskProcess.filter((i) => i.AgentName === item.Name).length - 1"
class="h-[1px] w-full bg-[#494B51] my-[8px]"
></div>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.user-item {
background: #1d222b;
border-radius: 40px;
padding-right: 12px;
cursor: pointer;
transition: all 0.25s ease;
color: #969696;
border: 2px solid transparent;
.duty-info {
transition: height 0.25s ease;
height: 0;
overflow: hidden;
}
& + .user-item {
margin-top: 8px;
}
&:hover {
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
color: #b8b8b8;
}
}
.active-card {
background:
linear-gradient(#171B22, #171B22) padding-box,
linear-gradient(to right, #00c8d2, #315ab4) border-box;
&:hover {
border-radius: 20px;
.duty-info {
height: auto;
}
.icon-container {
bottom: 2px;
}
}
}
</style>

View File

@@ -0,0 +1,190 @@
<script setup lang="ts">
import { ElNotification } from 'element-plus'
import { pick } from 'lodash'
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'
const agentsStore = useAgentsStore()
// 如果agentsStore.agents不存在就读取默认配置的json文件
onMounted(async () => {
if (!agentsStore.agents.length) {
const res = await readConfig<Agent[]>('agent.json')
agentsStore.setAgents(res)
}
await api.setAgents(agentsStore.agents.map((item) => pick(item, ['Name', 'Profile'])))
})
// 上传agent文件
const fileInput = ref<HTMLInputElement>()
const triggerFileSelect = () => {
fileInput.value?.click()
}
const handleFileSelect = (event: Event) => {
const input = event.target as HTMLInputElement
if (input.files && input.files[0]) {
const file = input.files[0]
readFileContent(file)
}
}
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 格式错误',
})
}
} catch (e) {
console.error(e)
}
}
reader.readAsText(file)
}
// 根据currentTask排序agent列表
const agentList = computed(() => {
const selected: Agent[] = []
const unselected: {
title: string
data: Agent[]
}[] = []
const obj: Record<string, Agent[]> = {}
if (!agentsStore.agents.length) {
return {
selected,
unselected,
}
}
for (const agent of agentsStore.agents) {
// if (agentsStore.currentTask?.AgentSelection?.includes(agent.Name)) {
// selected.push(agent)
// continue
// }
if (obj[agent.Classification]) {
obj[agent.Classification]!.push(agent)
} else {
const arr = [agent]
obj[agent.Classification] = arr
unselected.push({
title: agent.Classification,
data: arr,
})
}
}
return {
selected,
unselected: unselected,
}
})
</script>
<template>
<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>
<!-- 上传文件 -->
<input type="file" accept=".json" @change="handleFileSelect" class="hidden" ref="fileInput" />
<div class="plus-button" @click="triggerFileSelect">
<svg-icon icon-class="plus" color="var(--color-text)" size="18px" />
</div>
</div>
<!-- 智能体列表 -->
<div class="pt-[18px] flex-1 overflow-y-auto relative">
<!-- 已选中的智能体 -->
<AgentRepoList :agent-list="agentList.selected" />
<!-- 为选择的智能体 -->
<div v-for="agent in agentList.unselected" :key="agent.title">
<p class="text-[12px] font-bold py-[8px]">{{ agent.title }}</p>
<AgentRepoList :agent-list="agent.data" />
</div>
</div>
<!-- 底部提示栏 -->
<div class="w-full grid grid-cols-3 gap-x-[10px] bg-[#1d222b] 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"
>
<span class="text-[12px]">{{ item.name }}</span>
<div class="w-[8px] h-[8px] rounded-full" :style="{ background: item.color }"></div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.agent-repo {
padding: 0 8px;
.plus-button {
background: #1d2128;
width: 24px;
height: 24px;
padding: 0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: #374151;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
}
}
}
#agent-repo {
:deep(.agent-repo-item-popover) {
padding: 0;
border-radius: 20px;
background: var(--color-bg-secondary);
}
}
</style>

View File

@@ -0,0 +1,105 @@
<script setup lang="ts">
import type { IExecuteRawResponse } from '@/api'
import { computed } from 'vue'
import MarkdownIt from 'markdown-it'
import DOMPurify from 'dompurify'
import Iod from './Iod.vue'
const props = defineProps<{
executePlans: IExecuteRawResponse[]
nodeId?: string
actionId?: string
}>()
const md = new MarkdownIt({
html: true,
linkify: true,
typographer: true,
breaks: true,
})
function sanitize(str?: string) {
if (!str) {
return ''
}
const cleanStr = str
.replace(/\\n/g, '\n')
.replace(/\n\s*\d+\./g, '\n$&')
const html = md.render(cleanStr)
return html
// return DOMPurify.sanitize(html)
}
interface Data {
Description: string
Content: string
LogNodeType: string
}
const data = computed<Data | null>(() => {
for (const result of props.executePlans) {
if (result.NodeId === props.nodeId) {
// LogNodeType 为 object直接渲染Content
if (result.LogNodeType === 'object') {
return {
Description: props.nodeId,
Content: sanitize(result.content),
LogNodeType: result.LogNodeType,
}
}
if (!result.ActionHistory) {
return null
}
for (const action of result.ActionHistory) {
if (action.ID === props.actionId) {
return {
Description: action.Description,
Content: sanitize(action.Action_Result),
LogNodeType: result.LogNodeType,
}
}
}
}
}
return null
})
</script>
<template>
<div v-if="data" class="card-item w-full pl-[56px] pr-[41px]">
<!-- 分割线 -->
<div v-if="data.LogNodeType !== 'object'" class="h-[1px] w-full bg-[#494B51] my-[8px]"></div>
<div
v-if="data.Description"
class="text-[16px] flex items-center gap-1 text-[var(--color-text-secondary)] mb-1"
>
{{ data.Description }}
<Iod v-if="data.LogNodeType !== 'object'"/>
</div>
<div class="rounded-[8px] p-[15px] text-[14px] bg-[var(--color-bg-quaternary)]">
<div
class="markdown-content max-h-[240px] overflow-y-auto max-w-full"
v-html="data.Content"
></div>
</div>
</div>
</template>
<style scoped lang="scss">
.card-item + .card-item {
margin-top: 10px;
}
.markdown-content {
:deep(code) {
display: block;
width: 100px;
max-width: 100%;
}
:deep(pre) {
overflow-x: auto;
max-width: 100%;
}
}
</style>

View File

@@ -0,0 +1,76 @@
<script setup lang="ts">
import { readConfig } from '@/utils/readJson.ts'
import { onMounted } from 'vue'
interface Iod {
name: string
data_space: string
doId: string
fromRepo: string
}
const data = ref<Iod[]>([])
const displayIndex = ref(0)
const displayIod = computed(() => {
return data.value[displayIndex.value]!
})
onMounted(async () => {
const res = await readConfig<{ data: Iod[] }>('iodConfig.json')
data.value = res.data
})
function handleNext() {
if (displayIndex.value === data.value.length - 1) {
displayIndex.value = 0
} else {
displayIndex.value++
}
}
</script>
<template>
<el-popover trigger="hover" width="440">
<template #reference>
<div
class="rounded-full w-[20px] h-[20px] bg-[var(--color-bg-quaternary)] flex justify-center items-center cursor-pointer"
>
{{ data.length }}
</div>
</template>
<template #default v-if="data.length">
<div>
<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>
</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>
</template>
</el-popover>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,400 @@
<script setup lang="ts">
import { computed, onUnmounted, ref } from 'vue'
import { throttle } from 'lodash'
import { AnchorLocations, BezierConnector } from '@jsplumb/browser-ui'
import SvgIcon from '@/components/SvgIcon/index.vue'
import { getActionTypeDisplay, getAgentMapIcon } from '@/layout/components/config.ts'
import { type ConnectArg, Jsplumb } from '@/layout/components/Main/TaskTemplate/utils.ts'
import variables from '@/styles/variables.module.scss'
import { type IRawStepTask, useAgentsStore } from '@/stores'
import api from '@/api'
import ExecutePlan from './ExecutePlan.vue'
const emit = defineEmits<{
(e: 'refreshLine'): void
(el: 'setCurrentTask', task: IRawStepTask): void
}>()
const agentsStore = useAgentsStore()
const collaborationProcess = computed(() => {
return agentsStore.agentRawPlan.data?.['Collaboration Process'] ?? []
})
const jsplumb = new Jsplumb('task-results-main', {
connector: {
type: BezierConnector.type,
options: { curviness: 30, stub: 10 },
},
})
// 操作折叠面板时要实时的刷新连线
let timer: number
function handleCollapse() {
if (timer) {
clearInterval(timer)
}
timer = setInterval(() => {
jsplumb.repaintEverything()
emit('refreshLine')
}, 1)
// 默认三秒后已经完全打开
const timer1 = setTimeout(() => {
clearInterval(timer)
}, 3000)
onUnmounted(() => {
if (timer) {
clearInterval(timer)
}
if (timer1) {
clearInterval(timer1)
}
})
}
// 创建内部连线
function createInternalLine(id?: string) {
const arr: ConnectArg[] = []
jsplumb.reset()
collaborationProcess.value.forEach((item) => {
// 创建左侧流程与产出的连线
arr.push({
sourceId: `task-results-${item.Id}-0`,
targetId: `task-results-${item.Id}-1`,
anchor: [AnchorLocations.Left, AnchorLocations.Left],
})
collaborationProcess.value.forEach((jitem) => {
// 创建左侧产出与上一步流程的连线
if (item.InputObject_List!.includes(jitem.OutputObject ?? '')) {
arr.push({
sourceId: `task-results-${jitem.Id}-1`,
targetId: `task-results-${item.Id}-0`,
anchor: [AnchorLocations.Left, AnchorLocations.Left],
config: {
type: 'output',
},
})
}
// 创建右侧任务程序与InputObject字段的连线
jitem.TaskProcess.forEach((i) => {
if (i.ImportantInput?.includes(`InputObject:${item.OutputObject}`)) {
const color = getActionTypeDisplay(i.ActionType)?.color ?? ''
const sourceId = `task-results-${item.Id}-1`
const targetId = `task-results-${jitem.Id}-0-${i.ID}`
arr.push({
sourceId,
targetId,
anchor: [AnchorLocations.Right, AnchorLocations.Right],
config: {
stops: [
[0, color],
[1, color],
],
transparent: targetId !== id,
},
})
}
})
})
// 创建右侧TaskProcess内部连线
item.TaskProcess?.forEach((i) => {
if (!i.ImportantInput?.length) {
return
}
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}`
const targetId = `task-results-${item.Id}-0-${i.ID}`
arr.push({
sourceId,
targetId,
anchor: [AnchorLocations.Right, AnchorLocations.Right],
config: {
stops: [
[0, color],
[1, color],
],
transparent: targetId !== id,
},
})
}
})
})
})
jsplumb.connects(arr)
jsplumb.repaintEverything()
}
const loading = ref(false)
async function handleRun() {
try {
loading.value = true
const d = await api.executePlan(agentsStore.agentRawPlan.data!)
agentsStore.setExecutePlan(d)
} finally {
loading.value = false
}
}
// 添加滚动状态标识
const isScrolling = ref(false)
let scrollTimer: number
// 修改滚动处理函数
function handleScroll() {
isScrolling.value = true
emit('refreshLine')
// 清除之前的定时器
if (scrollTimer) {
clearTimeout(scrollTimer)
}
jsplumb.repaintEverything()
// 设置滚动结束检测
scrollTimer = setTimeout(() => {
isScrolling.value = false
}, 300)
}
// 修改鼠标事件处理函数
const handleMouseEnter = throttle((id) => {
if (!isScrolling.value) {
createInternalLine(id)
}
}, 100)
const handleMouseLeave = throttle(() => {
if (!isScrolling.value) {
createInternalLine()
}
}, 100)
function clear() {
jsplumb.reset()
}
defineExpose({
createInternalLine,
clear,
})
</script>
<template>
<div class="h-full flex flex-col relative" id="task-results">
<!-- 标题与执行按钮 -->
<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" />
</el-button>
<el-popover :disabled="Boolean(agentsStore.agentRawPlan.data)" title="请先输入要执行的任务">
<template #reference>
<el-button
circle
:color="variables.tertiary"
title="点击运行"
:disabled="!agentsStore.agentRawPlan.data"
@click="handleRun"
>
<svg-icon icon-class="action" />
</el-button>
</template>
</el-popover>
</div>
</div>
<!-- 内容 -->
<div
v-loading="agentsStore.agentRawPlan.loading"
class="flex-1 overflow-auto relative"
@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"
:id="`task-results-${item.Id}-0`"
@click="emit('setCurrentTask', item)"
>
<div class="text-[18px] mb-[15px]">{{ item.StepName }}</div>
<!-- 折叠面板 -->
<el-collapse @change="handleCollapse">
<el-collapse-item
v-for="item1 in item.TaskProcess"
:key="`task-results-${item.Id}-${item1.ID}`"
:name="`task-results-${item.Id}-${item1.ID}`"
:disabled="Boolean(!agentsStore.executePlan.length || loading)"
@mouseenter="() => handleMouseEnter(`task-results-${item.Id}-0-${item1.ID}`)"
@mouseleave="handleMouseLeave"
>
<template v-if="loading" #icon>
<SvgIcon icon-class="loading" size="20px" class="animate-spin" />
</template>
<template v-else-if="!agentsStore.executePlan.length" #icon>
<span></span>
</template>
<template #title>
<div class="flex items-center gap-[15px]">
<!-- 右侧链接点 -->
<div
class="absolute right-0 top-1/2 transform -translate-y-1/2"
:id="`task-results-${item.Id}-0-${item1.ID}`"
></div>
<div
class="w-[41px] h-[41px] rounded-full flex items-center justify-center"
:style="{ background: getAgentMapIcon(item1.AgentName).color }"
>
<svg-icon
:icon-class="getAgentMapIcon(item1.AgentName).icon"
color="var(--color-text)"
size="24px"
/>
</div>
<div class="text-[16px]">
<span>{{ item1.AgentName }}:&nbsp; &nbsp;</span>
<span :style="{ color: getActionTypeDisplay(item1.ActionType)?.color }">
{{ getActionTypeDisplay(item1.ActionType)?.name }}
</span>
</div>
</div>
</template>
<ExecutePlan
:action-id="item1.ID"
:node-id="item.StepName"
:execute-plans="agentsStore.executePlan"
/>
</el-collapse-item>
</el-collapse>
</el-card>
<el-card
class="card-item w-full relative output-object-card"
shadow="hover"
:class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''"
:id="`task-results-${item.Id}-1`"
@click="emit('setCurrentTask', item)"
>
<!-- <div class="text-[18px]">{{ item.OutputObject }}</div>-->
<el-collapse @change="handleCollapse">
<el-collapse-item
class="output-object"
:disabled="Boolean(!agentsStore.executePlan.length || loading)"
>
<template v-if="loading" #icon>
<SvgIcon icon-class="loading" size="20px" class="animate-spin" />
</template>
<template v-else-if="!agentsStore.executePlan.length" #icon>
<span></span>
</template>
<template #title>
<div class="text-[18px]">{{ item.OutputObject }}</div>
</template>
<ExecutePlan :node-id="item.OutputObject" :execute-plans="agentsStore.executePlan" />
</el-collapse-item>
</el-collapse>
</el-card>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
#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);
min-height: 41px;
line-height: 41px;
border-radius: 20px;
transition: border-radius 1ms;
position: relative;
.el-collapse-item__title {
background: var(--color-bg-secondary);
border-radius: 20px;
}
.el-icon {
font-size: 20px;
font-weight: bold;
}
&.is-active {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
}
.output-object {
.el-collapse-item__header {
background: none;
.el-collapse-item__title {
background: none;
}
}
.el-collapse-item__wrap {
background: none;
.card-item {
background: var(--color-bg-secondary);
padding: 25px;
padding-top: 10px;
border-radius: 7px;
}
}
}
.el-collapse-item__wrap {
border: none;
background: var(--color-bg-secondary);
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
}
}
:deep(.el-card) {
.el-card__body {
padding-right: 40px;
}
}
.output-object-card {
:deep(.el-card__body) {
padding-top: 0;
padding-bottom: 0;
padding-right: 0;
}
}
.active-card {
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;
}
}
</style>

View File

@@ -0,0 +1,31 @@
<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="absolute bottom-0 left-1/2 transform -translate-x-1/2 bg-[var(--color-bg-tertiary)] 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="absolute bottom-0 left-1/2 transform -translate-x-1/2 bg-[var(--color-bg-tertiary)] w-[15px] h-[15px] rounded-full"
></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
</script>

View File

@@ -0,0 +1,204 @@
<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 } from 'vue'
import { AnchorLocations } from '@jsplumb/browser-ui'
import MultiLineTooltip from '@/components/MultiLineTooltip/index.vue'
import Bg from './Bg.vue'
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'] ?? []
})
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()
}
defineExpose({
changeTask,
clear,
})
</script>
<template>
<div class="h-full flex flex-col">
<div class="text-[18px] font-bold mb-[18px]">任务大纲</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-[45%] h-[41px] flex justify-center relative z-99 items-center rounded-[20px] bg-[var(--color-bg-tertiary)]"
>
流程
</div>
</div>
<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)]"
>
产物
</div>
</div>
</div>
<div
v-for="item in collaborationProcess"
:key="item.Id"
class="card-item w-full flex items-center gap-[14%]"
>
<!-- 流程卡片 -->
<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"
: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-[#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>
</MultiLineTooltip>
<div class="h-[1px] w-full bg-[#494B51] 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="var(--color-text)"
size="24px"
/>
</div>
</el-tooltip>
</div>
</el-card>
<!-- 产物卡片 -->
<el-card
class="w-[43%] relative"
shadow="hover"
: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>
</div>
</template>
<style lang="scss" scoped>
.task-syllabus-flow-card {
:deep(.el-card__body) {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: auto;
}
}
</style>

View File

@@ -0,0 +1,122 @@
<script setup lang="ts">
import AgentRepo from './AgentRepo/index.vue'
import TaskSyllabus from './TaskSyllabus/index.vue'
import TaskResult from './TaskResult/index.vue'
import { Jsplumb } from './utils.ts'
import { type IRawStepTask, useAgentsStore } from '@/stores'
import { BezierConnector } from '@jsplumb/browser-ui'
const agentsStore = useAgentsStore()
// 智能体库
const agentRepoJsplumb = new Jsplumb('task-template', {
connector: {
type: BezierConnector.type,
options: {
curviness: 30, // 曲线弯曲程度
stub: 20, // 添加连接点与端点的距离
alwaysRespectStubs: true,
},
},
})
// 任务流程
const taskSyllabusRef = ref<{
changeTask: (task?: IRawStepTask, isEmit?: boolean) => void
clear: () => void
}>()
// 执行结果
const taskResultRef = ref<{
createInternalLine: () => void
clear: () => void
}>()
const taskResultJsplumb = new Jsplumb('task-template')
function scrollToElementTop(elementId: string) {
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)
}
function handleTaskResultCurrentTask(task: IRawStepTask) {
scrollToElementTop(`task-syllabus-flow-${task.Id}`)
agentsStore.setCurrentTask(task)
// 更新任务大纲内部的线
taskSyllabusRef.value?.changeTask(task, false)
}
function changeTask() {
taskResultRef.value?.createInternalLine()
taskSyllabusRef.value?.changeTask()
}
function resetAgentRepoLine() {
agentRepoJsplumb.repaintEverything()
taskResultJsplumb.repaintEverything()
}
function clear() {
taskSyllabusRef.value?.clear()
taskResultRef.value?.clear()
agentRepoJsplumb.repaintEverything()
taskResultJsplumb.repaintEverything()
}
defineExpose({
changeTask,
resetAgentRepoLine,
clear,
})
</script>
<template>
<div
class="task-template flex gap-6 items-center h-[calc(100%-84px)] relative overflow-hidden"
id="task-template"
>
<!-- 智能体库 -->
<div class="w-[9.5%] min-w-[179px] h-full relative flex-shrink-0">
<AgentRepo @resetAgentRepoLine="agentRepoJsplumb.repaintEverything" />
</div>
<!-- 任务大纲 -->
<div class="w-[35.5%] min-w-[600px] h-full px-[20px] flex-shrink-0">
<TaskSyllabus
ref="taskSyllabusRef"
@resetAgentRepoLine="resetAgentRepoLine"
@set-current-task="handleTaskSyllabusCurrentTask"
/>
</div>
<!-- 执行结果 -->
<div class="flex-1 h-full">
<TaskResult
ref="taskResultRef"
@refresh-line="taskResultJsplumb.repaintEverything"
@set-current-task="handleTaskResultCurrentTask"
/>
</div>
</div>
</template>
<style scoped lang="scss">
.task-template {
& > div {
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.8);
border-radius: 24px;
border: 1px solid #414752;
background: var(--color-bg-quinary);
padding-top: 20px;
padding-bottom: 20px;
}
}
</style>

View File

@@ -0,0 +1,169 @@
import type {
AnchorSpec,
ConnectorSpec,
ConnectParams,
EndpointOptions,
JsPlumbInstance,
} from '@jsplumb/browser-ui'
import { BezierConnector, DotEndpoint, newInstance } from '@jsplumb/browser-ui'
export interface JsplumbConfig {
connector?: ConnectorSpec
type?: 'input' | 'output'
stops?: [[number, string], [number, string]]
// 连接线条是否变透明一些
transparent?: boolean
}
export interface ConnectArg {
sourceId: string
targetId: string
anchor: AnchorSpec
config?: JsplumbConfig
}
const defaultConfig: JsplumbConfig = {
connector: {
type: BezierConnector.type,
options: {
curviness: 70,
stub: 10,
},
},
type: 'input',
}
export class Jsplumb {
instance!: JsPlumbInstance
containerId: string
config: JsplumbConfig
constructor(eleId: string, config = {} as JsplumbConfig) {
this.containerId = eleId
this.config = { ...defaultConfig, ...config }
onMounted(() => {
this.init()
})
}
init = () => {
if (this.instance) {
return
}
this.instance = newInstance({
container: document.querySelector(`#${this.containerId}`)!, // 或指定共同的父容器
})
}
getStops = (type?: 'input' | 'output'): [[number, string], [number, string]] => {
if (type === 'input') {
return [
[0, '#FF6161'],
[1, '#D76976'],
]
}
return [
[0, '#0093EB'],
[1, '#00D2D1'],
]
}
_connect = (
sourceId: string,
targetId: string,
anchor: AnchorSpec,
_config = {} as JsplumbConfig,
) => {
const config = {
...defaultConfig,
...this.config,
..._config,
}
this.init()
// 连接两个元素
const sourceElement = document.querySelector(`#${sourceId}`)
const targetElement = document.querySelector(`#${targetId}`)
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'
}
if (targetElement && sourceElement) {
this.instance.connect({
source: sourceElement,
target: targetElement,
connector: config.connector,
anchor: anchor,
paintStyle: {
stroke: stops[0][1],
strokeWidth: 2.5,
dashstyle: '0',
zIndex: 100,
opacity: 0.9,
gradient: {
stops: stops,
type: 'linear',
},
},
sourceEndpointStyle: { fill: stops[0][1] },
endpoint: {
type: DotEndpoint.type,
options: { radius: 5 },
},
cssClass: `jtk-connector-${config.type}`
} as unknown as ConnectParams<unknown>)
// 为源元素添加端点
this.instance.addEndpoint(sourceElement, {
anchor: (anchor as [AnchorSpec, AnchorSpec])[0],
paintStyle: { fill: stops[0][1], zIndex: 100 }, // source端点颜色
} as unknown as EndpointOptions)
// 为目标元素添加端点
this.instance.addEndpoint(targetElement, {
anchor: (anchor as [AnchorSpec, AnchorSpec])[1],
paintStyle: { fill: stops[1][1], zIndex: 100 }, // target端点颜色
} as unknown as EndpointOptions)
}
}
connect = (
sourceId: string,
targetId: string,
anchor: AnchorSpec,
config = {} as JsplumbConfig,
) => {
this._connect(sourceId, targetId, anchor, config)
}
connects = (args: ConnectArg[]) => {
this.instance.batch(() => {
args.forEach(({ sourceId, targetId, anchor, config }) => {
this._connect(sourceId, targetId, anchor, config)
})
})
}
repaintEverything = () => {
// 重新验证元素位置
const container = document.querySelector(`#${this.containerId}`)
if (container) {
const elements = container.querySelectorAll('[id^="task-results-"]')
elements.forEach((element) => {
this.instance.revalidate(element)
})
}
this.instance.repaintEverything()
}
reset = () => {
this.instance.deleteEveryConnection()
const allEndpoints = this.instance.selectEndpoints()
allEndpoints.each((endpoint) => {
this.instance.deleteEndpoint(endpoint)
})
}
}