feat: 全新智能体功能
- 基于先进智能体框架,增加智能体编排功能 - 增加智能体聊天,并对接持久化
This commit is contained in:
@@ -0,0 +1,380 @@
|
||||
<script setup lang="ts">
|
||||
import type {AgentDraftState, AgentOption} from '../../types';
|
||||
import type {
|
||||
AgentStudioCanvasSize,
|
||||
AgentStudioConnectionView,
|
||||
AgentStudioNodeData,
|
||||
AgentStudioNodeView,
|
||||
AgentStudioViewport,
|
||||
} from './types';
|
||||
|
||||
import {
|
||||
computed,
|
||||
createApp,
|
||||
h,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
reactive,
|
||||
ref,
|
||||
shallowRef,
|
||||
watch,
|
||||
} from 'vue';
|
||||
|
||||
import {usePreferences} from '@easyflow/preferences';
|
||||
|
||||
import {Tinyflow} from '@tinyflow-ai/vue';
|
||||
|
||||
import {useAgentStudioLayout} from '../../composables/agent-studio/useAgentStudioLayout';
|
||||
import {useAgentStudioModel} from '../../composables/agent-studio/useAgentStudioModel';
|
||||
import AgentStudioBaseNode from './AgentStudioBaseNode.vue';
|
||||
import AgentStudioCapabilityNode from './AgentStudioCapabilityNode.vue';
|
||||
import AgentStudioEdgeLayer from './AgentStudioEdgeLayer.vue';
|
||||
|
||||
import '@tinyflow-ai/vue/dist/index.css';
|
||||
|
||||
const props = defineProps<{
|
||||
knowledgeOptions?: AgentOption[];
|
||||
selectedNodeId: string;
|
||||
state: AgentDraftState;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [nodeId: string];
|
||||
}>();
|
||||
|
||||
const { isDark } = usePreferences();
|
||||
const { capture, layout } = useAgentStudioLayout(props.state);
|
||||
const canvasRef = ref<HTMLElement>();
|
||||
const canvasSize = ref<AgentStudioCanvasSize>();
|
||||
const canvasModel = useAgentStudioModel(
|
||||
props.state,
|
||||
() => props.selectedNodeId,
|
||||
layout,
|
||||
() => canvasSize.value,
|
||||
() => props.knowledgeOptions || [],
|
||||
);
|
||||
const liveNodes = ref<AgentStudioNodeView[]>([]);
|
||||
const liveViewport = ref<AgentStudioViewport>({ x: 250, y: 100, zoom: 1 });
|
||||
const flowData = computed(() => {
|
||||
const liveNodeMap = new Map(liveNodes.value.map((node) => [node.id, node]));
|
||||
return {
|
||||
edges: canvasModel.value.edges.map((edge) => ({ ...edge })),
|
||||
nodes: canvasModel.value.nodes.map((node) => {
|
||||
const liveNode = liveNodeMap.get(node.id);
|
||||
return {
|
||||
...node,
|
||||
data: { ...node.data },
|
||||
position: { ...(liveNode?.position || node.position) },
|
||||
};
|
||||
}),
|
||||
viewport: layout.viewport ? { ...layout.viewport } : undefined,
|
||||
};
|
||||
});
|
||||
const lastCaptureSignature = ref('');
|
||||
const lastFlowDataSignature = ref('');
|
||||
|
||||
const customNodes = computed(() => ({
|
||||
agentStudioBase: {
|
||||
presentation: 'plain',
|
||||
render: (parent: HTMLElement, node: any) => {
|
||||
renderVueNode(parent, AgentStudioBaseNode, node.data);
|
||||
},
|
||||
onUpdate: (parent: HTMLElement, node: any) => {
|
||||
renderVueNode(parent, AgentStudioBaseNode, node.data);
|
||||
},
|
||||
},
|
||||
agentStudioCapability: {
|
||||
presentation: 'plain',
|
||||
render: (parent: HTMLElement, node: any) => {
|
||||
renderVueNode(parent, AgentStudioCapabilityNode, node.data);
|
||||
},
|
||||
onUpdate: (parent: HTMLElement, node: any) => {
|
||||
renderVueNode(parent, AgentStudioCapabilityNode, node.data);
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const connections = computed<AgentStudioConnectionView[]>(() => {
|
||||
const nodes =
|
||||
liveNodes.value.length > 0
|
||||
? liveNodes.value
|
||||
: (canvasModel.value.nodes as AgentStudioNodeView[]);
|
||||
const nodeMap = new Map(nodes.map((node) => [node.id, node]));
|
||||
return canvasModel.value.edges
|
||||
.map((edge) => {
|
||||
const source = nodeMap.get(edge.source);
|
||||
const target = nodeMap.get(edge.target);
|
||||
if (!source || !target) return undefined;
|
||||
return {
|
||||
active: false,
|
||||
id: edge.id,
|
||||
path: buildConnectionPath(source, target, liveViewport.value),
|
||||
sourceId: edge.source,
|
||||
targetId: edge.target,
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as AgentStudioConnectionView[];
|
||||
});
|
||||
|
||||
function renderVueNode(
|
||||
parent: HTMLElement,
|
||||
component: any,
|
||||
data: AgentStudioNodeData,
|
||||
) {
|
||||
const current = (parent as any).__agentStudioNodeView as
|
||||
| undefined
|
||||
| {
|
||||
app: ReturnType<typeof createApp>;
|
||||
component: any;
|
||||
data: AgentStudioNodeData;
|
||||
signature: string;
|
||||
};
|
||||
const signature = JSON.stringify(data);
|
||||
if (current && current.component === component) {
|
||||
if (current.signature === signature) {
|
||||
return;
|
||||
}
|
||||
Object.assign(current.data, data);
|
||||
current.signature = signature;
|
||||
return;
|
||||
}
|
||||
|
||||
current?.app.unmount();
|
||||
parent.textContent = '';
|
||||
|
||||
const reactiveData = reactive({ ...data }) as AgentStudioNodeData;
|
||||
const app = createApp({
|
||||
render: () => h(component, { data: reactiveData }),
|
||||
});
|
||||
app.mount(parent);
|
||||
(parent as any).__agentStudioNodeView = {
|
||||
app,
|
||||
component,
|
||||
data: reactiveData,
|
||||
signature,
|
||||
};
|
||||
}
|
||||
|
||||
function getNodeSize(node: AgentStudioNodeView) {
|
||||
return {
|
||||
height: node.height || (node.data.kind === 'base' ? 104 : 78),
|
||||
width: node.width || (node.data.kind === 'base' ? 268 : 212),
|
||||
};
|
||||
}
|
||||
|
||||
function toScreenPoint(
|
||||
point: { x: number; y: number },
|
||||
viewport: AgentStudioViewport,
|
||||
) {
|
||||
return {
|
||||
x: point.x * viewport.zoom + viewport.x,
|
||||
y: point.y * viewport.zoom + viewport.y,
|
||||
};
|
||||
}
|
||||
|
||||
function buildConnectionPath(
|
||||
source: AgentStudioNodeView,
|
||||
target: AgentStudioNodeView,
|
||||
viewport: AgentStudioViewport,
|
||||
) {
|
||||
const sourceSize = getNodeSize(source);
|
||||
const targetSize = getNodeSize(target);
|
||||
const sourceCenter = {
|
||||
x: source.position.x + sourceSize.width / 2,
|
||||
y: source.position.y + sourceSize.height / 2,
|
||||
};
|
||||
const targetCenter = {
|
||||
x: target.position.x + targetSize.width / 2,
|
||||
y: target.position.y + targetSize.height / 2,
|
||||
};
|
||||
const sourceOnLeft = targetCenter.x < sourceCenter.x;
|
||||
const start = {
|
||||
x: source.position.x + (sourceOnLeft ? 0 : sourceSize.width),
|
||||
y: sourceCenter.y,
|
||||
};
|
||||
const end = {
|
||||
x: target.position.x + (sourceOnLeft ? targetSize.width : 0),
|
||||
y: targetCenter.y,
|
||||
};
|
||||
const screenStart = toScreenPoint(start, viewport);
|
||||
const screenEnd = toScreenPoint(end, viewport);
|
||||
const midX = (screenStart.x + screenEnd.x) / 2;
|
||||
return [
|
||||
`M ${screenStart.x} ${screenStart.y}`,
|
||||
`L ${midX} ${screenStart.y}`,
|
||||
`L ${midX} ${screenEnd.y}`,
|
||||
`L ${screenEnd.x} ${screenEnd.y}`,
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
function handleDataChange(data: any) {
|
||||
const signature = buildFlowDataSignature(data);
|
||||
if (signature === lastCaptureSignature.value) {
|
||||
return;
|
||||
}
|
||||
lastCaptureSignature.value = signature;
|
||||
|
||||
if (Array.isArray(data?.nodes)) {
|
||||
liveNodes.value = data.nodes as AgentStudioNodeView[];
|
||||
}
|
||||
if (data?.viewport) {
|
||||
liveViewport.value = data.viewport as AgentStudioViewport;
|
||||
}
|
||||
capture({
|
||||
nodes: data?.nodes as AgentStudioNodeView[] | undefined,
|
||||
viewport: data?.viewport as AgentStudioViewport | undefined,
|
||||
});
|
||||
const selected = data?.nodes?.find((node: any) => node.selected);
|
||||
if (selected?.id && selected.id !== props.selectedNodeId) {
|
||||
emit('select', selected.id);
|
||||
}
|
||||
}
|
||||
|
||||
function buildFlowDataSignature(data: any) {
|
||||
const nodes = Array.isArray(data?.nodes)
|
||||
? data.nodes.map((node: any) => ({
|
||||
data: node.data,
|
||||
id: node.id,
|
||||
position: node.position,
|
||||
selected: node.selected,
|
||||
type: node.type,
|
||||
}))
|
||||
: [];
|
||||
const edges = Array.isArray(data?.edges)
|
||||
? data.edges.map((edge: any) => ({
|
||||
id: edge.id,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
}))
|
||||
: [];
|
||||
return JSON.stringify({
|
||||
edges,
|
||||
nodes,
|
||||
viewport: data?.viewport,
|
||||
});
|
||||
}
|
||||
|
||||
const stableFlowData = shallowRef(flowData.value);
|
||||
let resizeObserver: ResizeObserver | undefined;
|
||||
|
||||
function syncFlowData() {
|
||||
const next = flowData.value;
|
||||
const signature = buildFlowDataSignature(next);
|
||||
if (signature === lastFlowDataSignature.value) {
|
||||
return;
|
||||
}
|
||||
lastFlowDataSignature.value = signature;
|
||||
stableFlowData.value = next;
|
||||
}
|
||||
|
||||
syncFlowData();
|
||||
|
||||
watch(flowData, syncFlowData);
|
||||
|
||||
function updateCanvasSize() {
|
||||
const rect = canvasRef.value?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
const next = {
|
||||
height: rect.height,
|
||||
width: rect.width,
|
||||
};
|
||||
if (
|
||||
canvasSize.value?.height === next.height &&
|
||||
canvasSize.value.width === next.width
|
||||
) {
|
||||
return;
|
||||
}
|
||||
canvasSize.value = next;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateCanvasSize();
|
||||
if (typeof ResizeObserver === 'undefined' || !canvasRef.value) return;
|
||||
resizeObserver = new ResizeObserver(updateCanvasSize);
|
||||
resizeObserver.observe(canvasRef.value);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resizeObserver?.disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="canvasRef" class="agent-studio-canvas">
|
||||
<Tinyflow
|
||||
class="agent-studio-canvas__flow"
|
||||
:data="stableFlowData"
|
||||
:theme="isDark ? 'dark' : 'light'"
|
||||
:custom-nodes="customNodes"
|
||||
:hide-bottom-dock="true"
|
||||
:hide-edge-panel="true"
|
||||
:hide-mini-map="true"
|
||||
:hide-node-handles="true"
|
||||
:hide-node-picker="true"
|
||||
:hide-node-setting="true"
|
||||
:hide-node-toolbar="true"
|
||||
:hide-edge-markers="true"
|
||||
:edge-animated="false"
|
||||
:edge-interaction-width="0"
|
||||
:nodes-draggable="true"
|
||||
:nodes-connectable="false"
|
||||
:elements-selectable="true"
|
||||
:drop-enabled="false"
|
||||
:on-data-change="handleDataChange"
|
||||
/>
|
||||
<AgentStudioEdgeLayer :connections="connections" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.agent-studio-canvas {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.agent-studio-canvas__flow {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.agent-studio-canvas :deep(.tinyflow),
|
||||
.agent-studio-canvas :deep(.agentsflow) {
|
||||
width: 100%;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.agent-studio-canvas :deep(.svelte-flow__node) {
|
||||
z-index: 2 !important;
|
||||
border: 0;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.agent-studio-canvas :deep(.svelte-flow__node::after) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.agent-studio-canvas :deep(.svelte-flow__node:hover),
|
||||
.agent-studio-canvas :deep(.svelte-flow__node.selectable.selected) {
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.agent-studio-canvas :deep(.svelte-flow__handle),
|
||||
.agent-studio-canvas :deep(.svelte-flow__minimap),
|
||||
.agent-studio-canvas :deep(.tf-left-dock),
|
||||
.agent-studio-canvas :deep(.tf-top-dock),
|
||||
.agent-studio-canvas :deep(.tf-bottom-dock),
|
||||
.agent-studio-canvas :deep(.tf-node-toolbar) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.agent-studio-canvas :deep(.tf-flow-line-path) {
|
||||
stroke: color-mix(
|
||||
in srgb,
|
||||
var(--el-color-primary) 30%,
|
||||
var(--el-border-color)
|
||||
);
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user