finish auto detection
This commit is contained in:
parent
92c8cf90ed
commit
9294275874
@ -5,7 +5,27 @@
|
|||||||
<span>Tool Diagram</span>
|
<span>Tool Diagram</span>
|
||||||
 
|
 
|
||||||
<el-button size="small" type="primary" @click="() => context.reset()">重置</el-button>
|
<el-button size="small" type="primary" @click="() => context.reset()">重置</el-button>
|
||||||
<el-button size="small" type="primary" @click="() => startTest()">开启自检程序</el-button>
|
<!-- 自检程序弹出表单 -->
|
||||||
|
<el-popover placement="top" width="350" trigger="click" v-model:visible="testFormVisible">
|
||||||
|
<template #reference>
|
||||||
|
<el-button size="small" type="primary">
|
||||||
|
开启自检程序
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-input type="textarea" v-model="testPrompt" :rows="2" style="margin-bottom: 8px;"
|
||||||
|
placeholder="请输入 prompt" />
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 8px;">
|
||||||
|
<el-switch v-model="enableXmlWrapper" style="margin-right: 8px;" />
|
||||||
|
<span style="opacity: 0.7;">enableXmlWrapper</span>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: right;">
|
||||||
|
<el-button size="small" @click="testFormVisible = false">取消</el-button>
|
||||||
|
<el-button size="small" type="primary" @click="onTestConfirm">
|
||||||
|
确认
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-popover>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-scrollbar height="80vh">
|
<el-scrollbar height="80vh">
|
||||||
@ -17,16 +37,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
<!-- <el-button @click="showDiagram = true" type="primary" style="margin-bottom: 16px;">
|
|
||||||
Show Tool Diagram
|
|
||||||
</el-button> -->
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { nextTick, provide, ref } from 'vue';
|
import { nextTick, provide, ref } from 'vue';
|
||||||
import Diagram from './diagram.vue';
|
import Diagram from './diagram.vue';
|
||||||
import { topoSortParallel, type DiagramState } from './diagram';
|
import { makeNodeTest, topoSortParallel, type DiagramContext, type DiagramState } from './diagram';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
const showDiagram = ref(true);
|
const showDiagram = ref(true);
|
||||||
|
|
||||||
const caption = ref('');
|
const caption = ref('');
|
||||||
@ -45,35 +63,40 @@ function setCaption(text: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DiagramContext {
|
|
||||||
reset: () => void,
|
|
||||||
state?: DiagramState,
|
|
||||||
setCaption: (value: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const context: DiagramContext = {
|
const context: DiagramContext = {
|
||||||
reset: () => {},
|
reset: () => {},
|
||||||
|
render: () => {},
|
||||||
state: undefined,
|
state: undefined,
|
||||||
setCaption
|
setCaption
|
||||||
};
|
};
|
||||||
|
|
||||||
provide('context', context);
|
provide('context', context);
|
||||||
|
|
||||||
async function startTest() {
|
// 新增:自检参数表单相关
|
||||||
|
const testFormVisible = ref(false);
|
||||||
|
const enableXmlWrapper = ref(false);
|
||||||
|
const testPrompt = ref('please call the tool {tool} to make some test');
|
||||||
|
|
||||||
|
async function onTestConfirm() {
|
||||||
|
testFormVisible.value = false;
|
||||||
|
// 这里可以将 enableXmlWrapper.value 和 testPrompt.value 传递给自检逻辑
|
||||||
const state = context.state;
|
const state = context.state;
|
||||||
if (state) {
|
if (state) {
|
||||||
const dispatches = topoSortParallel(state);
|
const dispatches = topoSortParallel(state);
|
||||||
// for (const layer of dispatches) {
|
for (const nodeIds of dispatches) {
|
||||||
// await Promise.all(
|
await Promise.all(
|
||||||
// layer.map(nodeId => state.nodes[nodeId].run())
|
nodeIds.map(id => {
|
||||||
// );
|
const node = state.dataView.get(id);
|
||||||
// }
|
if (node) {
|
||||||
|
return makeNodeTest(node, enableXmlWrapper.value, testPrompt.value, context)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error('error');
|
ElMessage.error('error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@ -90,7 +113,7 @@ async function startTest() {
|
|||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
background: rgba(245, 247, 250, 0.05);
|
background: rgba(245, 247, 250, 0.05);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 8px 0 rgba(0,0,0,0.06);
|
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.06);
|
||||||
color: var(--main-color);
|
color: var(--main-color);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -3,6 +3,7 @@ import { TaskLoop } from '../chat/core/task-loop';
|
|||||||
import type { Reactive } from 'vue';
|
import type { Reactive } from 'vue';
|
||||||
import type { ChatStorage } from '../chat/chat-box/chat';
|
import type { ChatStorage } from '../chat/chat-box/chat';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
|
import type { ToolItem } from '@/hook/type';
|
||||||
|
|
||||||
export interface Edge {
|
export interface Edge {
|
||||||
id: string;
|
id: string;
|
||||||
@ -19,6 +20,7 @@ export interface DiagramState {
|
|||||||
nodes: Node[];
|
nodes: Node[];
|
||||||
edges: Edge[];
|
edges: Edge[];
|
||||||
selectedNodeId: string | null;
|
selectedNodeId: string | null;
|
||||||
|
dataView: Map<string, NodeDataView>;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,6 +29,19 @@ export interface CanConnectResult {
|
|||||||
reason?: string;
|
reason?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NodeDataView {
|
||||||
|
tool: ToolItem;
|
||||||
|
status: 'default' | 'running' | 'waiting' | 'success' | 'error';
|
||||||
|
result: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiagramContext {
|
||||||
|
reset: () => void,
|
||||||
|
render: () => void,
|
||||||
|
state?: DiagramState,
|
||||||
|
setCaption: (value: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 判断两个节点是否可以连接
|
* @description 判断两个节点是否可以连接
|
||||||
*/
|
*/
|
||||||
@ -133,25 +148,31 @@ export function topoSortParallel(state: DiagramState): string[][] {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function makeNodeTest(dataView: Reactive<any>, enableXmlWrapper: boolean, prompt: string | null = null) {
|
export async function makeNodeTest(
|
||||||
|
dataView: Reactive<NodeDataView>,
|
||||||
|
enableXmlWrapper: boolean,
|
||||||
|
prompt: string | null = null,
|
||||||
|
context: DiagramContext
|
||||||
|
) {
|
||||||
if (!dataView.tool.inputSchema) {
|
if (!dataView.tool.inputSchema) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dataView.loading = true;
|
dataView.status = 'running';
|
||||||
|
context.render();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const loop = new TaskLoop({ maxEpochs: 1 });
|
const loop = new TaskLoop({ maxEpochs: 1 });
|
||||||
const usePrompt = prompt || `please call the tool ${dataView.too.name} to make some test`;
|
const usePrompt = (prompt || 'please call the tool {tool} to make some test').replace('{tool}', dataView.tool.name);
|
||||||
const chatStorage = {
|
const chatStorage = {
|
||||||
messages: [],
|
messages: [],
|
||||||
settings: {
|
settings: {
|
||||||
temperature: 0.6,
|
temperature: 0.6,
|
||||||
systemPrompt: '',
|
systemPrompt: '',
|
||||||
enableTools: [{
|
enableTools: [{
|
||||||
name: dataView.too.name,
|
name: dataView.tool.name,
|
||||||
description: dataView.too.description,
|
description: dataView.tool.description,
|
||||||
inputSchema: dataView.too.inputSchema,
|
inputSchema: dataView.tool.inputSchema,
|
||||||
enabled: true
|
enabled: true
|
||||||
}],
|
}],
|
||||||
enableWebSearch: false,
|
enableWebSearch: false,
|
||||||
@ -168,15 +189,21 @@ export async function makeNodeTest(dataView: Reactive<any>, enableXmlWrapper: bo
|
|||||||
loop.registerOnToolCall(toolCall => {
|
loop.registerOnToolCall(toolCall => {
|
||||||
console.log(toolCall);
|
console.log(toolCall);
|
||||||
|
|
||||||
if (toolCall.function?.name === dataView.too?.name) {
|
if (toolCall.function?.name === dataView.tool?.name) {
|
||||||
try {
|
try {
|
||||||
const toolArgs = JSON.parse(toolCall.function?.arguments || '{}');
|
const toolArgs = JSON.parse(toolCall.function?.arguments || '{}');
|
||||||
aiMockJson = toolArgs;
|
aiMockJson = toolArgs;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ElMessage.error('AI 生成的 JSON 解析错误');
|
// ElMessage.error('AI 生成的 JSON 解析错误');
|
||||||
|
dataView.status = 'error';
|
||||||
|
dataView.result = 'AI 生成的 JSON 解析错误';
|
||||||
|
context.render();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// ElMessage.error('AI 调用了未知的工具');
|
// ElMessage.error('AI 调用了未知的工具');
|
||||||
|
dataView.status = 'error';
|
||||||
|
dataView.result = 'AI 调用了未知的工具 ' + toolCall.function?.name;
|
||||||
|
context.render();
|
||||||
}
|
}
|
||||||
loop.abort();
|
loop.abort();
|
||||||
return toolCall;
|
return toolCall;
|
||||||
@ -189,6 +216,9 @@ export async function makeNodeTest(dataView: Reactive<any>, enableXmlWrapper: bo
|
|||||||
await loop.start(chatStorage, usePrompt);
|
await loop.start(chatStorage, usePrompt);
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
dataView.loading = false;
|
if (dataView.status === 'running') {
|
||||||
|
dataView.status = 'success';
|
||||||
|
context.render();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
@ -1,6 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div style="display: flex; align-items: center; gap: 16px;">
|
<div style="display: flex; align-items: center; gap: 16px;">
|
||||||
<div ref="svgContainer" class="diagram-container"></div>
|
<div ref="svgContainer" class="diagram-container"></div>
|
||||||
|
|
||||||
|
<!-- <template v-for="(node, index) in state.nodes" :key="node.id + '-popup'">
|
||||||
|
<div
|
||||||
|
v-if="state.hoverNodeId === node.id"
|
||||||
|
:style="getNodePopupStyle(node)"
|
||||||
|
class="node-popup"
|
||||||
|
>
|
||||||
|
<div>节点:{{ node.labels?.[0]?.text || node.id }}</div>
|
||||||
|
<div>宽: {{ node.width }}, 高: {{ node.height }}</div>
|
||||||
|
</div>
|
||||||
|
</template> -->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -9,7 +20,7 @@ import { ref, onMounted, nextTick, reactive, inject } from 'vue';
|
|||||||
import * as d3 from 'd3';
|
import * as d3 from 'd3';
|
||||||
import ELK from 'elkjs/lib/elk.bundled.js';
|
import ELK from 'elkjs/lib/elk.bundled.js';
|
||||||
import { mcpClientAdapter } from '@/views/connect/core';
|
import { mcpClientAdapter } from '@/views/connect/core';
|
||||||
import { invalidConnectionDetector, type Edge, type Node } from './diagram';
|
import { invalidConnectionDetector, type Edge, type Node, type NodeDataView } from './diagram';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
const svgContainer = ref<HTMLDivElement | null>(null);
|
const svgContainer = ref<HTMLDivElement | null>(null);
|
||||||
@ -21,7 +32,9 @@ const state = reactive({
|
|||||||
edges: [] as any[],
|
edges: [] as any[],
|
||||||
selectedNodeId: null as string | null,
|
selectedNodeId: null as string | null,
|
||||||
draggingNodeId: null as string | null,
|
draggingNodeId: null as string | null,
|
||||||
offset: { x: 0, y: 0 }
|
hoverNodeId: null as string | null,
|
||||||
|
offset: { x: 0, y: 0 },
|
||||||
|
dataView: new Map<string, NodeDataView>
|
||||||
});
|
});
|
||||||
|
|
||||||
const getAllTools = async () => {
|
const getAllTools = async () => {
|
||||||
@ -66,7 +79,7 @@ const drawDiagram = async () => {
|
|||||||
const nodes = [] as Node[];
|
const nodes = [] as Node[];
|
||||||
const edges = [] as Edge[];
|
const edges = [] as Edge[];
|
||||||
|
|
||||||
for (let i = 0; i < tools.length - 1; ++ i) {
|
for (let i = 0; i < tools.length - 1; ++i) {
|
||||||
const prev = tools[i];
|
const prev = tools[i];
|
||||||
const next = tools[i + 1];
|
const next = tools[i + 1];
|
||||||
edges.push({
|
edges.push({
|
||||||
@ -79,10 +92,16 @@ const drawDiagram = async () => {
|
|||||||
for (const tool of tools) {
|
for (const tool of tools) {
|
||||||
nodes.push({
|
nodes.push({
|
||||||
id: tool.name,
|
id: tool.name,
|
||||||
width: 160,
|
width: 200,
|
||||||
height: 48,
|
height: 64, // 增加高度
|
||||||
labels: [{ text: tool.name || 'Tool' }]
|
labels: [{ text: tool.name || 'Tool' }]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
state.dataView.set(tool.name, {
|
||||||
|
tool,
|
||||||
|
status: 'waiting',
|
||||||
|
result: null
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
state.edges = edges;
|
state.edges = edges;
|
||||||
@ -273,6 +292,7 @@ function renderSvg() {
|
|||||||
state.draggingNodeId = null;
|
state.draggingNodeId = null;
|
||||||
})
|
})
|
||||||
.on('mouseover', function (event, d) {
|
.on('mouseover', function (event, d) {
|
||||||
|
state.hoverNodeId = d.id;
|
||||||
d3.select(this).select('rect')
|
d3.select(this).select('rect')
|
||||||
.transition()
|
.transition()
|
||||||
.duration(200)
|
.duration(200)
|
||||||
@ -280,9 +300,8 @@ function renderSvg() {
|
|||||||
.attr('stroke-width', 2);
|
.attr('stroke-width', 2);
|
||||||
})
|
})
|
||||||
.on('mouseout', function (event, d) {
|
.on('mouseout', function (event, d) {
|
||||||
if (state.selectedNodeId === d.id) {
|
state.hoverNodeId = null;
|
||||||
return;
|
if (state.selectedNodeId === d.id) return;
|
||||||
}
|
|
||||||
d3.select(this).select('rect')
|
d3.select(this).select('rect')
|
||||||
.transition()
|
.transition()
|
||||||
.duration(200)
|
.duration(200)
|
||||||
@ -298,15 +317,91 @@ function renderSvg() {
|
|||||||
.attr('stroke', d => state.selectedNodeId === d.id ? 'var(--main-color)' : 'var(--main-light-color-10)')
|
.attr('stroke', d => state.selectedNodeId === d.id ? 'var(--main-color)' : 'var(--main-light-color-10)')
|
||||||
.attr('stroke-width', 2);
|
.attr('stroke-width', 2);
|
||||||
|
|
||||||
|
// 节点文字
|
||||||
nodeGroupEnter.append('text')
|
nodeGroupEnter.append('text')
|
||||||
.attr('x', d => d.width / 2)
|
.attr('x', d => d.width / 2)
|
||||||
.attr('y', d => d.height / 2 + 6)
|
.attr('y', d => d.height / 2 - 6) // 上移一点
|
||||||
.attr('text-anchor', 'middle')
|
.attr('text-anchor', 'middle')
|
||||||
.attr('font-size', 16)
|
.attr('font-size', 16)
|
||||||
.attr('fill', 'var(--main-color)')
|
.attr('fill', 'var(--main-color)')
|
||||||
.attr('font-weight', 600)
|
.attr('font-weight', 600)
|
||||||
.text(d => d.labels?.[0]?.text || 'Tool');
|
.text(d => d.labels?.[0]?.text || 'Tool');
|
||||||
|
|
||||||
|
// 状态条
|
||||||
|
nodeGroupEnter.append('g')
|
||||||
|
.attr('class', 'node-status')
|
||||||
|
.each(function (d) {
|
||||||
|
const status = state.dataView.get(d.id)?.status || 'waiting';
|
||||||
|
const g = d3.select(this);
|
||||||
|
if (status === 'running') {
|
||||||
|
// 动画圆环+文字
|
||||||
|
g.append('circle')
|
||||||
|
.attr('cx', d.width / 2 - 32)
|
||||||
|
.attr('cy', d.height - 16)
|
||||||
|
.attr('r', 6) // 半径更小
|
||||||
|
.attr('fill', 'none')
|
||||||
|
.attr('stroke', 'var(--main-color)') // 使用主题色
|
||||||
|
.attr('stroke-width', 3)
|
||||||
|
.attr('stroke-dasharray', 20)
|
||||||
|
.attr('stroke-dashoffset', 0)
|
||||||
|
.append('animateTransform')
|
||||||
|
.attr('attributeName', 'transform')
|
||||||
|
.attr('attributeType', 'XML')
|
||||||
|
.attr('type', 'rotate')
|
||||||
|
.attr('from', `0 ${(d.width / 2 - 32)} ${(d.height - 16)}`)
|
||||||
|
.attr('to', `360 ${(d.width / 2 - 32)} ${(d.height - 16)}`)
|
||||||
|
.attr('dur', '1s')
|
||||||
|
.attr('repeatCount', 'indefinite');
|
||||||
|
g.append('text')
|
||||||
|
.attr('x', d.width / 2 - 16)
|
||||||
|
.attr('y', d.height - 12)
|
||||||
|
.attr('font-size', 13)
|
||||||
|
.attr('fill', 'var(--main-color)')
|
||||||
|
.text('running');
|
||||||
|
} else if (status === 'waiting') {
|
||||||
|
g.append('circle')
|
||||||
|
.attr('cx', d.width / 2 - 32)
|
||||||
|
.attr('cy', d.height - 16)
|
||||||
|
.attr('r', 6)
|
||||||
|
.attr('fill', 'none')
|
||||||
|
.attr('stroke', '#bdbdbd')
|
||||||
|
.attr('stroke-width', 3);
|
||||||
|
g.append('text')
|
||||||
|
.attr('x', d.width / 2 - 16)
|
||||||
|
.attr('y', d.height - 12)
|
||||||
|
.attr('font-size', 13)
|
||||||
|
.attr('fill', '#bdbdbd')
|
||||||
|
.text('waiting');
|
||||||
|
} else if (status === 'success') {
|
||||||
|
g.append('circle')
|
||||||
|
.attr('cx', d.width / 2 - 32)
|
||||||
|
.attr('cy', d.height - 16)
|
||||||
|
.attr('r', 6) // 保持和 waiting 一致
|
||||||
|
.attr('fill', 'none')
|
||||||
|
.attr('stroke', '#4caf50')
|
||||||
|
.attr('stroke-width', 3);
|
||||||
|
g.append('text')
|
||||||
|
.attr('x', d.width / 2 - 16)
|
||||||
|
.attr('y', d.height - 12)
|
||||||
|
.attr('font-size', 13)
|
||||||
|
.attr('fill', '#4caf50')
|
||||||
|
.text('success');
|
||||||
|
} else if (status === 'error') {
|
||||||
|
g.append('circle')
|
||||||
|
.attr('cx', d.width / 2 - 32)
|
||||||
|
.attr('cy', d.height - 16)
|
||||||
|
.attr('r', 6) // 保持和 waiting 一致
|
||||||
|
.attr('fill', 'none')
|
||||||
|
.attr('stroke', '#f44336')
|
||||||
|
.attr('stroke-width', 3);
|
||||||
|
g.append('text')
|
||||||
|
.attr('x', d.width / 2 - 16)
|
||||||
|
.attr('y', d.height - 12)
|
||||||
|
.attr('font-size', 13)
|
||||||
|
.attr('fill', '#f44336')
|
||||||
|
.text('error');
|
||||||
|
}
|
||||||
|
});
|
||||||
// 节点 enter 动画
|
// 节点 enter 动画
|
||||||
nodeGroupEnter
|
nodeGroupEnter
|
||||||
.transition()
|
.transition()
|
||||||
@ -375,7 +470,7 @@ function renderSvg() {
|
|||||||
function resetConnections() {
|
function resetConnections() {
|
||||||
if (!state.nodes.length) return;
|
if (!state.nodes.length) return;
|
||||||
const edges = [];
|
const edges = [];
|
||||||
for (let i = 0; i < state.nodes.length - 1; ++ i) {
|
for (let i = 0; i < state.nodes.length - 1; ++i) {
|
||||||
const prev = state.nodes[i];
|
const prev = state.nodes[i];
|
||||||
const next = state.nodes[i + 1];
|
const next = state.nodes[i + 1];
|
||||||
edges.push({
|
edges.push({
|
||||||
@ -391,16 +486,32 @@ function resetConnections() {
|
|||||||
const context = inject('context') as any;
|
const context = inject('context') as any;
|
||||||
context.reset = resetConnections;
|
context.reset = resetConnections;
|
||||||
context.state = state;
|
context.state = state;
|
||||||
|
context.render = renderSvg;
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
nextTick(drawDiagram);
|
nextTick(drawDiagram);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 4. 计算窗口位置
|
||||||
|
function getNodePopupStyle(node: any): any {
|
||||||
|
// 节点的 svg 坐标转为容器内绝对定位
|
||||||
|
// 注意:这里假设 offsetX、node.x、node.y 已经是最新的
|
||||||
|
console.log(node);
|
||||||
|
|
||||||
|
const left = (node.x || 0) + (node.width || 160) - 120; // 节点右侧
|
||||||
|
const top = (node.y || 0) + 30; // 节点顶部对齐
|
||||||
|
return {
|
||||||
|
position: 'absolute',
|
||||||
|
left: `${left}px`,
|
||||||
|
top: `${top}px`,
|
||||||
|
};
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.diagram-container {
|
.diagram-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 300px;
|
min-height: 200px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@ -408,4 +519,28 @@ onMounted(() => {
|
|||||||
padding: 24px 0;
|
padding: 24px 0;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.node-popup {
|
||||||
|
position: absolute;
|
||||||
|
background: var(--background);
|
||||||
|
border: 1px solid var(--main-color);
|
||||||
|
width: 240px;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
white-space: nowrap;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 旋转动画 */
|
||||||
|
.status-running-circle {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
2215
resources/changelog/index.html
Normal file
2215
resources/changelog/index.html
Normal file
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user