feat: improve workflow collaboration trace UX
This commit is contained in:
@@ -217,6 +217,7 @@ def _make_execution_log_entry(
|
|||||||
node_type: str,
|
node_type: str,
|
||||||
**extra: Any,
|
**extra: Any,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
|
extra.setdefault('timestamp', datetime.now().isoformat())
|
||||||
metadata = dict(extra.pop('metadata', {}) or {})
|
metadata = dict(extra.pop('metadata', {}) or {})
|
||||||
collaboration = _node_collaboration_metadata(node_map, node_id, node_type)
|
collaboration = _node_collaboration_metadata(node_map, node_id, node_type)
|
||||||
branch_id = extra.get('branch') or extra.get('branch_id')
|
branch_id = extra.get('branch') or extra.get('branch_id')
|
||||||
@@ -238,6 +239,7 @@ def _make_execution_log_entry(
|
|||||||
output=extra.get('output'),
|
output=extra.get('output'),
|
||||||
error=extra.get('error'),
|
error=extra.get('error'),
|
||||||
)
|
)
|
||||||
|
metadata['communication']['timestamp'] = extra.get('timestamp')
|
||||||
return {
|
return {
|
||||||
'node_id': node_id,
|
'node_id': node_id,
|
||||||
'node_type': node_type,
|
'node_type': node_type,
|
||||||
@@ -982,9 +984,18 @@ class AIWorkflowService:
|
|||||||
current_id = next_nodes[0] if next_nodes else None
|
current_id = next_nodes[0] if next_nodes else None
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 执行节点(异步)
|
node_inputs = _snapshot_node_inputs(branch_context)
|
||||||
|
|
||||||
|
# 执行节点(异步)。并行分支不能共享同一个 AsyncSession,否则模型/Provider 查询会并发抢占连接。
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
result = await node_instance.execute_async(branch_context)
|
if context.db_session is not None:
|
||||||
|
from app.database import AsyncSessionLocal
|
||||||
|
|
||||||
|
async with AsyncSessionLocal() as branch_db:
|
||||||
|
branch_context.db_session = branch_db
|
||||||
|
result = await node_instance.execute_async(branch_context)
|
||||||
|
else:
|
||||||
|
result = await node_instance.execute_async(branch_context)
|
||||||
elapsed = int((time.time() - start_time) * 1000)
|
elapsed = int((time.time() - start_time) * 1000)
|
||||||
target_node_id = _resolve_handoff_target(
|
target_node_id = _resolve_handoff_target(
|
||||||
current_id,
|
current_id,
|
||||||
@@ -1004,6 +1015,7 @@ class AIWorkflowService:
|
|||||||
elapsed_time=elapsed,
|
elapsed_time=elapsed,
|
||||||
tokens_used=result.tokens_used,
|
tokens_used=result.tokens_used,
|
||||||
metadata=_node_result_metadata(result),
|
metadata=_node_result_metadata(result),
|
||||||
|
inputs=copy.deepcopy(node_inputs),
|
||||||
branch=branch_id,
|
branch=branch_id,
|
||||||
branch_label=branch_labels.get(branch_id, branch_id),
|
branch_label=branch_labels.get(branch_id, branch_id),
|
||||||
target_node_id=target_node_id,
|
target_node_id=target_node_id,
|
||||||
@@ -1027,6 +1039,12 @@ class AIWorkflowService:
|
|||||||
branch_context.variables.update(result.output_variables)
|
branch_context.variables.update(result.output_variables)
|
||||||
branch_context.previous_output = result.output
|
branch_context.previous_output = result.output
|
||||||
branch_output = result.output
|
branch_output = result.output
|
||||||
|
|
||||||
|
# 支持分支内后续节点使用 {{node_id.output_variable}} 引用当前节点输出。
|
||||||
|
branch_context.variables[f'_node_{current_id}'] = {
|
||||||
|
'output': result.output,
|
||||||
|
**result.output_variables,
|
||||||
|
}
|
||||||
|
|
||||||
# 下一个节点
|
# 下一个节点
|
||||||
if result.next_node_id:
|
if result.next_node_id:
|
||||||
|
|||||||
@@ -281,6 +281,7 @@ export interface ExecutionLogEntry {
|
|||||||
error?: string;
|
error?: string;
|
||||||
elapsed_time?: number;
|
elapsed_time?: number;
|
||||||
tokens_used?: number;
|
tokens_used?: number;
|
||||||
|
timestamp?: string;
|
||||||
branch?: string;
|
branch?: string;
|
||||||
branch_label?: string;
|
branch_label?: string;
|
||||||
metadata?: Record<string, any>;
|
metadata?: Record<string, any>;
|
||||||
|
|||||||
@@ -13,11 +13,16 @@ type WindowWithIdleCallback = Window & {
|
|||||||
|
|
||||||
let prefetched = false;
|
let prefetched = false;
|
||||||
|
|
||||||
const prefetchTasks = [
|
const criticalPrefetchTasks = [
|
||||||
() => import('#/views/ai-platform/agent/index.vue'),
|
|
||||||
() => import('#/views/_core/agent-chat/index.vue'),
|
() => import('#/views/_core/agent-chat/index.vue'),
|
||||||
() => import('#/views/ai-platform/workflow/index.vue'),
|
|
||||||
() => import('#/views/ai-platform/workflow-runs/index.vue'),
|
() => import('#/views/ai-platform/workflow-runs/index.vue'),
|
||||||
|
() => import('#/components/ai-chat-panel/AiChatPanel.vue'),
|
||||||
|
() => import('#/views/ai-platform/workflow-runs/modules/detail-dialog.vue'),
|
||||||
|
];
|
||||||
|
|
||||||
|
const deferredPrefetchTasks = [
|
||||||
|
() => import('#/views/ai-platform/agent/index.vue'),
|
||||||
|
() => import('#/views/ai-platform/workflow/index.vue'),
|
||||||
() => import('#/views/ai-platform/model/index.vue'),
|
() => import('#/views/ai-platform/model/index.vue'),
|
||||||
() => import('#/views/ai-platform/knowledge/index.vue'),
|
() => import('#/views/ai-platform/knowledge/index.vue'),
|
||||||
];
|
];
|
||||||
@@ -39,8 +44,11 @@ function prefetchAiPlatformPages() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prefetched = true;
|
prefetched = true;
|
||||||
|
window.setTimeout(() => {
|
||||||
|
void Promise.allSettled(criticalPrefetchTasks.map((task) => task()));
|
||||||
|
}, 300);
|
||||||
scheduleIdle(() => {
|
scheduleIdle(() => {
|
||||||
void Promise.allSettled(prefetchTasks.map((task) => task()));
|
void Promise.allSettled(deferredPrefetchTasks.map((task) => task()));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,16 +104,27 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page auto-content-height>
|
<Page auto-content-height>
|
||||||
<div v-loading="loading" class="flex h-full">
|
<div class="flex h-full">
|
||||||
|
<template v-if="loading">
|
||||||
|
<div class="agent-chat-loading flex flex-1 items-center justify-center">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="mx-auto mb-3 h-8 w-8 animate-spin rounded-full border-2 border-[var(--el-border-color)] border-t-[var(--el-color-primary)]"></div>
|
||||||
|
<div class="text-muted-foreground text-sm">
|
||||||
|
正在打开智能体对话...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- 错误 -->
|
<!-- 错误 -->
|
||||||
<template v-if="!loading && (error || !agent)">
|
<template v-else-if="error || !agent">
|
||||||
<div class="flex flex-1 items-center justify-center">
|
<div class="flex flex-1 items-center justify-center">
|
||||||
<ElEmpty :description="error || $t('ai-platform.agent.notFound')" />
|
<ElEmpty :description="error || $t('ai-platform.agent.notFound')" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 主内容 -->
|
<!-- 主内容 -->
|
||||||
<template v-else-if="!loading && agent">
|
<template v-else>
|
||||||
<!-- 左侧历史记录面板 -->
|
<!-- 左侧历史记录面板 -->
|
||||||
<div
|
<div
|
||||||
v-if="showHistoryPanel"
|
v-if="showHistoryPanel"
|
||||||
|
|||||||
+52
-3
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Edge, Node } from '@vue-flow/core';
|
import type { Edge, Node } from '@vue-flow/core';
|
||||||
|
|
||||||
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
import { useVueFlow, VueFlow } from '@vue-flow/core';
|
import { useVueFlow, VueFlow } from '@vue-flow/core';
|
||||||
import { MiniMap } from '@vue-flow/minimap';
|
import { MiniMap } from '@vue-flow/minimap';
|
||||||
@@ -13,6 +13,7 @@ import '@vue-flow/core/dist/style.css';
|
|||||||
import '@vue-flow/core/dist/theme-default.css';
|
import '@vue-flow/core/dist/theme-default.css';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
activeNodeId?: string;
|
||||||
edges: Edge[];
|
edges: Edge[];
|
||||||
fitNodeId?: string;
|
fitNodeId?: string;
|
||||||
nodes: Node[];
|
nodes: Node[];
|
||||||
@@ -21,6 +22,38 @@ const props = defineProps<{
|
|||||||
const containerRef = ref<HTMLElement>();
|
const containerRef = ref<HTMLElement>();
|
||||||
const { fitView } = useVueFlow();
|
const { fitView } = useVueFlow();
|
||||||
|
|
||||||
|
const displayNodes = computed(() =>
|
||||||
|
props.nodes.map((node) => ({
|
||||||
|
...node,
|
||||||
|
selected: node.id === props.activeNodeId,
|
||||||
|
class: [
|
||||||
|
node.class,
|
||||||
|
node.id === props.activeNodeId ? 'is-run-focus-node' : '',
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' '),
|
||||||
|
data: {
|
||||||
|
...(node.data || {}),
|
||||||
|
isRunFocus: node.id === props.activeNodeId,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const displayEdges = computed(() =>
|
||||||
|
props.edges.map((edge) => ({
|
||||||
|
...edge,
|
||||||
|
class: [
|
||||||
|
edge.class,
|
||||||
|
props.activeNodeId &&
|
||||||
|
(edge.source === props.activeNodeId || edge.target === props.activeNodeId)
|
||||||
|
? 'is-run-focus-edge'
|
||||||
|
: '',
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' '),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
async function refreshView(nodeId?: string) {
|
async function refreshView(nodeId?: string) {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
if (!containerRef.value?.clientHeight) return;
|
if (!containerRef.value?.clientHeight) return;
|
||||||
@@ -71,8 +104,8 @@ onUnmounted(() => {
|
|||||||
class="workflow-readonly-canvas bg-muted/30 h-full w-full min-h-[320px]"
|
class="workflow-readonly-canvas bg-muted/30 h-full w-full min-h-[320px]"
|
||||||
>
|
>
|
||||||
<VueFlow
|
<VueFlow
|
||||||
:nodes="props.nodes"
|
:nodes="displayNodes"
|
||||||
:edges="props.edges"
|
:edges="displayEdges"
|
||||||
:node-types="workflowNodeTypes"
|
:node-types="workflowNodeTypes"
|
||||||
:edge-types="workflowEdgeTypes"
|
:edge-types="workflowEdgeTypes"
|
||||||
:nodes-draggable="false"
|
:nodes-draggable="false"
|
||||||
@@ -115,4 +148,20 @@ onUnmounted(() => {
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workflow-readonly-canvas :deep(.vue-flow__node.is-run-focus-node) {
|
||||||
|
z-index: 10 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-readonly-canvas :deep(.vue-flow__node.is-run-focus-node > *) {
|
||||||
|
border-color: var(--el-color-primary) !important;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 3px var(--el-color-primary-light-7),
|
||||||
|
0 10px 24px rgb(59 130 246 / 22%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-readonly-canvas :deep(.vue-flow__edge.is-run-focus-edge path) {
|
||||||
|
stroke: var(--el-color-primary) !important;
|
||||||
|
stroke-width: 3px !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Edge, Node } from '@vue-flow/core';
|
|||||||
|
|
||||||
import type { WorkflowRun } from '#/api/ai-platform/ai-platform';
|
import type { WorkflowRun } from '#/api/ai-platform/ai-platform';
|
||||||
|
|
||||||
import { computed, defineAsyncComponent, ref } from 'vue';
|
import { computed, defineAsyncComponent, nextTick, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { ExternalLink } from '@vben/icons';
|
import { ExternalLink } from '@vben/icons';
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
ElButton,
|
ElButton,
|
||||||
ElCollapse,
|
ElCollapse,
|
||||||
ElCollapseItem,
|
ElCollapseItem,
|
||||||
|
ElDialog,
|
||||||
ElScrollbar,
|
ElScrollbar,
|
||||||
ElTag,
|
ElTag,
|
||||||
} from 'element-plus';
|
} from 'element-plus';
|
||||||
@@ -44,13 +45,18 @@ const triggerOptions = getTriggerOptions();
|
|||||||
|
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const canvasLoading = ref(false);
|
||||||
const runId = ref('');
|
const runId = ref('');
|
||||||
const run = ref<WorkflowRun | null>(null);
|
const run = ref<WorkflowRun | null>(null);
|
||||||
const nodes = ref<Node[]>([]);
|
const nodes = ref<Node[]>([]);
|
||||||
const edges = ref<Edge[]>([]);
|
const edges = ref<Edge[]>([]);
|
||||||
const activeNodeId = ref('');
|
const activeNodeId = ref('');
|
||||||
|
const activeLogIndex = ref(-1);
|
||||||
const definitionFallback = ref(false);
|
const definitionFallback = ref(false);
|
||||||
const activeLogNames = ref<string[]>([]);
|
const activeLogNames = ref<string[]>([]);
|
||||||
|
const valueDialogVisible = ref(false);
|
||||||
|
const valueDialogTitle = ref('');
|
||||||
|
const valueDialogContent = ref('');
|
||||||
|
|
||||||
const dialogTitle = computed(() => run.value?.workflow_name || $t('ai-platform.workflowRuns.detail.runId'));
|
const dialogTitle = computed(() => run.value?.workflow_name || $t('ai-platform.workflowRuns.detail.runId'));
|
||||||
const executionLogs = computed(() => run.value?.execution_log || []);
|
const executionLogs = computed(() => run.value?.execution_log || []);
|
||||||
@@ -83,6 +89,12 @@ const finalOutput = computed(() => {
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
return previewValue(nodeValues.at(-1) || outputs);
|
return previewValue(nodeValues.at(-1) || outputs);
|
||||||
});
|
});
|
||||||
|
const selectedNodeLabel = computed(() => {
|
||||||
|
const nodeId = activeNodeId.value;
|
||||||
|
if (!nodeId) return '';
|
||||||
|
const log = executionLogs.value.find((item) => item.node_id === nodeId);
|
||||||
|
return log?.node_label || log?.node_id || nodeId;
|
||||||
|
});
|
||||||
const agentSummaries = computed(() => {
|
const agentSummaries = computed(() => {
|
||||||
const agents = new Map<
|
const agents = new Map<
|
||||||
string,
|
string,
|
||||||
@@ -114,6 +126,7 @@ const agentSummaries = computed(() => {
|
|||||||
return [...agents.values()];
|
return [...agents.values()];
|
||||||
});
|
});
|
||||||
const collaborationTimeline = computed(() => {
|
const collaborationTimeline = computed(() => {
|
||||||
|
let estimatedOffset = 0;
|
||||||
return executionLogs.value
|
return executionLogs.value
|
||||||
.map((log, index) => {
|
.map((log, index) => {
|
||||||
const meta = getLogMeta(log);
|
const meta = getLogMeta(log);
|
||||||
@@ -137,6 +150,11 @@ const collaborationTimeline = computed(() => {
|
|||||||
const model = meta.model || collaboration.model || meta.model_id || collaboration.model_id;
|
const model = meta.model || collaboration.model || meta.model_id || collaboration.model_id;
|
||||||
const targetName =
|
const targetName =
|
||||||
target.agent_name || target.agent_code || target.node_label || '';
|
target.agent_name || target.agent_code || target.node_label || '';
|
||||||
|
const timestamp =
|
||||||
|
log.timestamp ||
|
||||||
|
communication.timestamp ||
|
||||||
|
estimateLogTimestamp(estimatedOffset);
|
||||||
|
estimatedOffset += Number(log.elapsed_time || 0);
|
||||||
const title =
|
const title =
|
||||||
agentName ||
|
agentName ||
|
||||||
subflowName ||
|
subflowName ||
|
||||||
@@ -154,6 +172,7 @@ const collaborationTimeline = computed(() => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
key: `${log.node_id || 'node'}-${index}`,
|
key: `${log.node_id || 'node'}-${index}`,
|
||||||
|
index,
|
||||||
nodeId: log.node_id,
|
nodeId: log.node_id,
|
||||||
title,
|
title,
|
||||||
subtitle: log.node_label || log.node_id,
|
subtitle: log.node_label || log.node_id,
|
||||||
@@ -161,19 +180,121 @@ const collaborationTimeline = computed(() => {
|
|||||||
summary: summaryParts.join(' · '),
|
summary: summaryParts.join(' · '),
|
||||||
message: communication.message || previewValue(log.error || log.output),
|
message: communication.message || previewValue(log.error || log.output),
|
||||||
elapsed: log.elapsed_time,
|
elapsed: log.elapsed_time,
|
||||||
|
timestamp,
|
||||||
tokens: log.tokens_used || 0,
|
tokens: log.tokens_used || 0,
|
||||||
hasSignal: Boolean(agentName || branch || subflowName || model || communication.channel),
|
hasSignal: Boolean(agentName || branch || subflowName || model || communication.channel),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((item) => item.hasSignal);
|
.filter((item) => item.hasSignal);
|
||||||
});
|
});
|
||||||
|
const collaborationEdges = computed(() => {
|
||||||
|
const edges: Array<{
|
||||||
|
channel: string;
|
||||||
|
count: number;
|
||||||
|
from: string;
|
||||||
|
key: string;
|
||||||
|
to: string;
|
||||||
|
}> = [];
|
||||||
|
const seen = new Map<string, number>();
|
||||||
|
|
||||||
|
for (let index = 0; index < collaborationTimeline.value.length; index += 1) {
|
||||||
|
const item = collaborationTimeline.value[index];
|
||||||
|
if (!item) continue;
|
||||||
|
const log = executionLogs.value[item.index];
|
||||||
|
if (!log) continue;
|
||||||
|
const target = getTargetName(log);
|
||||||
|
const next = collaborationTimeline.value[index + 1];
|
||||||
|
const to = target || next?.title || '';
|
||||||
|
if (!item.title || !to || item.title === to) continue;
|
||||||
|
|
||||||
|
const channel = getCommunication(log).channel || 'workflow_edge';
|
||||||
|
const key = `${item.title}->${to}:${channel}`;
|
||||||
|
const count = (seen.get(key) || 0) + 1;
|
||||||
|
seen.set(key, count);
|
||||||
|
edges.push({
|
||||||
|
key: `${key}:${count}`,
|
||||||
|
from: item.title,
|
||||||
|
to,
|
||||||
|
channel,
|
||||||
|
count,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return edges;
|
||||||
|
});
|
||||||
|
const collaborationSupportSummary = computed(() => {
|
||||||
|
const modes = new Set<string>();
|
||||||
|
const traceText = executionLogs.value
|
||||||
|
.map((log) =>
|
||||||
|
[
|
||||||
|
getActorName(log),
|
||||||
|
getTargetName(log),
|
||||||
|
log.node_label,
|
||||||
|
log.node_type,
|
||||||
|
log.branch_label,
|
||||||
|
previewValue(log.output),
|
||||||
|
previewValue(log.error),
|
||||||
|
].join(' '),
|
||||||
|
)
|
||||||
|
.join(' ')
|
||||||
|
.toLowerCase();
|
||||||
|
for (const log of executionLogs.value) {
|
||||||
|
const collaboration = getCollaboration(log);
|
||||||
|
const communication = getCommunication(log);
|
||||||
|
if (collaboration.agent_code) modes.add('agent');
|
||||||
|
if (collaboration.collaboration_mode) modes.add(collaboration.collaboration_mode);
|
||||||
|
if (communication.channel) modes.add(communication.channel);
|
||||||
|
if (log.node_type) modes.add(log.node_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
const supportsParallel =
|
||||||
|
modes.has('parallel') ||
|
||||||
|
modes.has('parallel_branch') ||
|
||||||
|
executionLogs.value.some((log) => log.branch || log.branch_label);
|
||||||
|
const supportsHandoff = executionLogs.value.some((log, index) => {
|
||||||
|
if (getTargetName(log)) return true;
|
||||||
|
const current = getActorName(log);
|
||||||
|
const next = executionLogs.value[index + 1]
|
||||||
|
? getActorName(executionLogs.value[index + 1])
|
||||||
|
: '';
|
||||||
|
return current && next && current !== next;
|
||||||
|
});
|
||||||
|
const supportsLoop =
|
||||||
|
modes.has('loop') ||
|
||||||
|
executionLogs.value.some((log) => String(log.node_type).includes('loop')) ||
|
||||||
|
/返工|rework|不通过|阻塞/.test(traceText);
|
||||||
|
const supportsSubflow = modes.has('subflow');
|
||||||
|
const supportsHumanInput =
|
||||||
|
modes.has('human_input') ||
|
||||||
|
executionLogs.value.some((log) => log.status === 'waiting');
|
||||||
|
const supportsPm = /\bpm\b|产品|分诊|拆解|汇总|收口/.test(traceText);
|
||||||
|
const supportsDev = /dev|developer|backend|后端|开发|数据|架构/.test(traceText);
|
||||||
|
const supportsFrontend = /frontend|前端/.test(traceText);
|
||||||
|
const supportsQa = /\bqa\b|测试|复验|质量/.test(traceText);
|
||||||
|
const supportsAcceptance = /验收|闭环|完成/.test(traceText);
|
||||||
|
|
||||||
|
return [
|
||||||
|
supportsPm ? 'PM 分发' : '',
|
||||||
|
supportsParallel ? '并行分支' : '',
|
||||||
|
supportsHandoff ? 'Agent 交接' : '',
|
||||||
|
supportsDev ? 'Dev 实现' : '',
|
||||||
|
supportsFrontend ? 'Frontend 介入' : '',
|
||||||
|
supportsQa ? 'QA 复验' : '',
|
||||||
|
supportsLoop ? '返工闭环' : '',
|
||||||
|
supportsAcceptance ? '验收收口' : '',
|
||||||
|
supportsSubflow ? '子流程' : '',
|
||||||
|
supportsHumanInput ? '人工确认' : '',
|
||||||
|
].filter(Boolean);
|
||||||
|
});
|
||||||
|
|
||||||
async function loadDetail() {
|
async function loadDetail() {
|
||||||
if (!runId.value) return;
|
if (!runId.value) return;
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
canvasLoading.value = false;
|
||||||
definitionFallback.value = false;
|
definitionFallback.value = false;
|
||||||
activeNodeId.value = '';
|
activeNodeId.value = '';
|
||||||
|
activeLogIndex.value = -1;
|
||||||
run.value = null;
|
run.value = null;
|
||||||
nodes.value = [];
|
nodes.value = [];
|
||||||
edges.value = [];
|
edges.value = [];
|
||||||
@@ -181,6 +302,14 @@ async function loadDetail() {
|
|||||||
try {
|
try {
|
||||||
const detail = await getWorkflowRunDetailApi(runId.value);
|
const detail = await getWorkflowRunDetailApi(runId.value);
|
||||||
run.value = detail;
|
run.value = detail;
|
||||||
|
activeLogNames.value = executionLogs.value.slice(0, 1).map((_, index) => String(index));
|
||||||
|
if (executionLogs.value[0]?.node_id) {
|
||||||
|
activeNodeId.value = executionLogs.value[0].node_id;
|
||||||
|
activeLogIndex.value = 0;
|
||||||
|
}
|
||||||
|
loading.value = false;
|
||||||
|
canvasLoading.value = true;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
let definition = detail.definition_snapshot;
|
let definition = detail.definition_snapshot;
|
||||||
if (!definition?.nodes?.length) {
|
if (!definition?.nodes?.length) {
|
||||||
@@ -200,11 +329,11 @@ async function loadDetail() {
|
|||||||
const replayed = applyRunReplay(base.nodes, base.edges, detail);
|
const replayed = applyRunReplay(base.nodes, base.edges, detail);
|
||||||
nodes.value = replayed.nodes;
|
nodes.value = replayed.nodes;
|
||||||
edges.value = replayed.edges;
|
edges.value = replayed.edges;
|
||||||
activeLogNames.value = executionLogs.value.slice(0, 1).map((_, index) => String(index));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
canvasLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,6 +390,36 @@ function previewValue(value: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function compactValue(value: any, maxLength = 360) {
|
||||||
|
const text = previewValue(value);
|
||||||
|
if (text.length <= maxLength) return text;
|
||||||
|
return `${text.slice(0, maxLength)}...`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openValueDialog(title: string, value: any) {
|
||||||
|
valueDialogTitle.value = title;
|
||||||
|
valueDialogContent.value = previewValue(value);
|
||||||
|
valueDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function estimateLogTimestamp(offset: number) {
|
||||||
|
if (!run.value?.started_at) return '';
|
||||||
|
const start = new Date(run.value.started_at).getTime();
|
||||||
|
if (!Number.isFinite(start)) return '';
|
||||||
|
return new Date(start + offset).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimelineTime(value?: string) {
|
||||||
|
if (!value) return '-';
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return '-';
|
||||||
|
return date.toLocaleTimeString([], {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getLogMeta(log: any) {
|
function getLogMeta(log: any) {
|
||||||
return log?.metadata || {};
|
return log?.metadata || {};
|
||||||
}
|
}
|
||||||
@@ -294,9 +453,19 @@ function getTargetName(log: any) {
|
|||||||
return target.agent_name || target.agent_code || target.node_label || '';
|
return target.agent_name || target.agent_code || target.node_label || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectLog(nodeId?: string) {
|
function selectLog(nodeId?: string, index?: number) {
|
||||||
if (!nodeId) return;
|
if (!nodeId) return;
|
||||||
activeNodeId.value = nodeId;
|
activeNodeId.value = nodeId;
|
||||||
|
const resolvedIndex =
|
||||||
|
typeof index === 'number'
|
||||||
|
? index
|
||||||
|
: executionLogs.value.findIndex((item) => item.node_id === nodeId);
|
||||||
|
if (resolvedIndex >= 0) {
|
||||||
|
activeLogIndex.value = resolvedIndex;
|
||||||
|
if (!activeLogNames.value.includes(String(resolvedIndex))) {
|
||||||
|
activeLogNames.value = [String(resolvedIndex), ...activeLogNames.value];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({ open });
|
defineExpose({ open });
|
||||||
@@ -307,7 +476,7 @@ defineExpose({ open });
|
|||||||
v-model="visible"
|
v-model="visible"
|
||||||
class="workflow-run-detail-dialog"
|
class="workflow-run-detail-dialog"
|
||||||
:title="dialogTitle"
|
:title="dialogTitle"
|
||||||
:loading="loading"
|
:loading="false"
|
||||||
default-fullscreen
|
default-fullscreen
|
||||||
:draggable="false"
|
:draggable="false"
|
||||||
:show-footer="false"
|
:show-footer="false"
|
||||||
@@ -384,19 +553,30 @@ defineExpose({ open });
|
|||||||
|
|
||||||
<div class="run-report-grid grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_360px]">
|
<div class="run-report-grid grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_360px]">
|
||||||
<div class="border-border bg-muted/30 rounded-lg border p-3">
|
<div class="border-border bg-muted/30 rounded-lg border p-3">
|
||||||
<div class="mb-2 text-sm font-medium">
|
<div class="mb-2 flex items-center justify-between gap-2">
|
||||||
|
<span class="text-sm font-medium">
|
||||||
{{ $t('ai-platform.workflowRuns.detail.taskInput') }}
|
{{ $t('ai-platform.workflowRuns.detail.taskInput') }}
|
||||||
|
</span>
|
||||||
|
<ElButton link type="primary" @click="openValueDialog('任务输入', run.inputs)">
|
||||||
|
查看全部
|
||||||
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
<pre class="run-json run-report-json">{{ taskSummary }}</pre>
|
<pre class="run-json run-report-json is-clickable" @click="openValueDialog('任务输入', run.inputs)">{{ compactValue(taskSummary) }}</pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="border-border bg-muted/30 rounded-lg border p-3">
|
<div class="border-border bg-muted/30 rounded-lg border p-3">
|
||||||
<div class="mb-2 text-sm font-medium">
|
<div class="mb-2 flex items-center justify-between gap-2">
|
||||||
|
<span class="text-sm font-medium">
|
||||||
{{ $t('ai-platform.workflowRuns.detail.finalOutput') }}
|
{{ $t('ai-platform.workflowRuns.detail.finalOutput') }}
|
||||||
|
</span>
|
||||||
|
<ElButton link type="primary" @click="openValueDialog('最终输出', run.error_message || run.outputs)">
|
||||||
|
查看全部
|
||||||
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
<pre
|
<pre
|
||||||
:class="run.status === 'failed' ? 'text-destructive' : ''"
|
:class="run.status === 'failed' ? 'text-destructive' : ''"
|
||||||
class="run-json run-report-json"
|
class="run-json run-report-json is-clickable"
|
||||||
>{{ run.error_message || finalOutput }}</pre>
|
@click="openValueDialog('最终输出', run.error_message || run.outputs)"
|
||||||
|
>{{ compactValue(run.error_message || finalOutput) }}</pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="border-border bg-muted/30 rounded-lg border p-3">
|
<div class="border-border bg-muted/30 rounded-lg border p-3">
|
||||||
<div class="mb-2 flex items-center justify-between">
|
<div class="mb-2 flex items-center justify-between">
|
||||||
@@ -435,8 +615,9 @@ defineExpose({ open });
|
|||||||
v-for="item in collaborationTimeline"
|
v-for="item in collaborationTimeline"
|
||||||
:key="item.key"
|
:key="item.key"
|
||||||
class="collaboration-item"
|
class="collaboration-item"
|
||||||
|
:class="{ 'is-active': item.nodeId === activeNodeId }"
|
||||||
type="button"
|
type="button"
|
||||||
@click="selectLog(item.nodeId)"
|
@click="selectLog(item.nodeId, item.index)"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="collaboration-dot"
|
class="collaboration-dot"
|
||||||
@@ -450,8 +631,9 @@ defineExpose({ open });
|
|||||||
{{ item.summary || item.subtitle }}
|
{{ item.summary || item.subtitle }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="text-muted-foreground shrink-0 text-xs">
|
<span class="text-muted-foreground shrink-0 text-right text-xs">
|
||||||
{{ formatDuration(item.elapsed) }}
|
<span class="block">{{ formatTimelineTime(item.timestamp) }}</span>
|
||||||
|
<span class="block">{{ formatDuration(item.elapsed) }}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -462,11 +644,65 @@ defineExpose({ open });
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="collaborationTimeline.length"
|
||||||
|
class="border-border bg-muted/20 rounded-lg border p-3"
|
||||||
|
>
|
||||||
|
<div class="mb-3 flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium">多智能体协作链路</div>
|
||||||
|
<div class="text-muted-foreground mt-1 text-xs">
|
||||||
|
支持 {{ collaborationSupportSummary.join('、') || '工作流编排' }};点击任一节点会定位到画布和右侧日志。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ElTag v-if="selectedNodeLabel" type="primary">
|
||||||
|
当前定位:{{ selectedNodeLabel }}
|
||||||
|
</ElTag>
|
||||||
|
</div>
|
||||||
|
<div class="collaboration-flow">
|
||||||
|
<template
|
||||||
|
v-for="(item, index) in collaborationTimeline"
|
||||||
|
:key="`flow-${item.key}`"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="collaboration-flow-node"
|
||||||
|
:class="{ 'is-active': item.nodeId === activeNodeId }"
|
||||||
|
type="button"
|
||||||
|
@click="selectLog(item.nodeId, item.index)"
|
||||||
|
>
|
||||||
|
<span class="flow-time">{{ formatTimelineTime(item.timestamp) }}</span>
|
||||||
|
<span class="flow-title">{{ item.title }}</span>
|
||||||
|
<span class="flow-summary">{{ item.summary || item.subtitle }}</span>
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
v-if="index < collaborationTimeline.length - 1"
|
||||||
|
class="collaboration-flow-arrow"
|
||||||
|
>
|
||||||
|
→
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div v-if="collaborationEdges.length" class="collaboration-edge-list">
|
||||||
|
<span
|
||||||
|
v-for="edge in collaborationEdges"
|
||||||
|
:key="edge.key"
|
||||||
|
class="collaboration-edge-item"
|
||||||
|
>
|
||||||
|
{{ edge.from }} → {{ edge.to }}
|
||||||
|
<small>{{ edge.channel }}{{ edge.count > 1 ? ` #${edge.count}` : '' }}</small>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="run-detail-main grid min-h-0 flex-1 grid-cols-[minmax(0,1fr)_420px] gap-4">
|
<div class="run-detail-main grid min-h-0 flex-1 grid-cols-[minmax(0,1fr)_420px] gap-4">
|
||||||
<div class="run-detail-canvas border-border min-h-0 overflow-hidden rounded-lg border">
|
<div
|
||||||
|
v-loading="canvasLoading"
|
||||||
|
class="run-detail-canvas border-border min-h-0 overflow-hidden rounded-lg border"
|
||||||
|
>
|
||||||
<WorkflowReadonlyCanvas
|
<WorkflowReadonlyCanvas
|
||||||
:nodes="nodes"
|
:nodes="nodes"
|
||||||
:edges="edges"
|
:edges="edges"
|
||||||
|
:active-node-id="activeNodeId"
|
||||||
:fit-node-id="activeNodeId"
|
:fit-node-id="activeNodeId"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -495,7 +731,8 @@ defineExpose({ open });
|
|||||||
<template #title>
|
<template #title>
|
||||||
<div
|
<div
|
||||||
class="flex min-w-0 flex-1 items-center gap-2"
|
class="flex min-w-0 flex-1 items-center gap-2"
|
||||||
@click="selectLog(log.node_id)"
|
:class="{ 'text-primary': activeLogIndex === index }"
|
||||||
|
@click="selectLog(log.node_id, index)"
|
||||||
>
|
>
|
||||||
<ElTag :type="getLogStatusType(log.status)" size="small">
|
<ElTag :type="getLogStatusType(log.status)" size="small">
|
||||||
{{ getLogStatusText(log.status) }}
|
{{ getLogStatusText(log.status) }}
|
||||||
@@ -585,17 +822,23 @@ defineExpose({ open });
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="run-io-block">
|
||||||
<div class="text-muted-foreground mb-1 font-medium">
|
<div class="text-muted-foreground mb-1 flex items-center justify-between gap-2 font-medium">
|
||||||
{{ $t('ai-platform.workflowRuns.detail.inputsOutputs') }} - 输入
|
<span>{{ $t('ai-platform.workflowRuns.detail.inputsOutputs') }} - 输入</span>
|
||||||
|
<ElButton link type="primary" @click="openValueDialog(`${log.node_label || log.node_id} 输入`, log.inputs)">
|
||||||
|
查看全部
|
||||||
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
<pre class="run-json">{{ previewValue(log.inputs) }}</pre>
|
<pre class="run-json is-clickable" @click="openValueDialog(`${log.node_label || log.node_id} 输入`, log.inputs)">{{ compactValue(log.inputs) }}</pre>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="run-io-block">
|
||||||
<div class="text-muted-foreground mb-1 font-medium">
|
<div class="text-muted-foreground mb-1 flex items-center justify-between gap-2 font-medium">
|
||||||
{{ $t('ai-platform.workflowRuns.detail.inputsOutputs') }} - 输出
|
<span>{{ $t('ai-platform.workflowRuns.detail.inputsOutputs') }} - 输出</span>
|
||||||
|
<ElButton link type="primary" @click="openValueDialog(`${log.node_label || log.node_id} 输出`, log.output)">
|
||||||
|
查看全部
|
||||||
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
<pre class="run-json">{{ previewValue(log.output) }}</pre>
|
<pre class="run-json is-clickable" @click="openValueDialog(`${log.node_label || log.node_id} 输出`, log.output)">{{ compactValue(log.output) }}</pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-muted-foreground flex gap-3">
|
<div class="text-muted-foreground flex gap-3">
|
||||||
<span>
|
<span>
|
||||||
@@ -612,6 +855,23 @@ defineExpose({ open });
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="flex min-h-[420px] items-center justify-center">
|
||||||
|
<div class="text-muted-foreground text-sm">
|
||||||
|
{{ loading ? '正在加载运行详情...' : '暂无运行详情' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElDialog
|
||||||
|
v-model="valueDialogVisible"
|
||||||
|
append-to-body
|
||||||
|
class="run-value-dialog"
|
||||||
|
destroy-on-close
|
||||||
|
:title="valueDialogTitle"
|
||||||
|
width="78vw"
|
||||||
|
>
|
||||||
|
<pre class="run-value-full">{{ valueDialogContent }}</pre>
|
||||||
|
</ElDialog>
|
||||||
</ZqDialog>
|
</ZqDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -659,8 +919,21 @@ defineExpose({ open });
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workflow-run-detail-dialog .run-json.is-clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background 0.15s ease,
|
||||||
|
box-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-run-detail-dialog .run-json.is-clickable:hover {
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
box-shadow: inset 0 0 0 1px var(--el-color-primary-light-5);
|
||||||
|
}
|
||||||
|
|
||||||
.workflow-run-detail-dialog .run-report-json {
|
.workflow-run-detail-dialog .run-report-json {
|
||||||
max-height: 140px;
|
min-height: 120px;
|
||||||
|
max-height: 240px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workflow-run-detail-dialog .collaboration-timeline {
|
.workflow-run-detail-dialog .collaboration-timeline {
|
||||||
@@ -688,6 +961,12 @@ defineExpose({ open });
|
|||||||
background: var(--el-fill-color-light);
|
background: var(--el-fill-color-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workflow-run-detail-dialog .collaboration-item.is-active {
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
background: var(--el-color-primary-light-9);
|
||||||
|
box-shadow: 0 0 0 2px var(--el-color-primary-light-7);
|
||||||
|
}
|
||||||
|
|
||||||
.workflow-run-detail-dialog .collaboration-dot {
|
.workflow-run-detail-dialog .collaboration-dot {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
@@ -711,10 +990,138 @@ defineExpose({ open });
|
|||||||
background: var(--el-color-primary);
|
background: var(--el-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workflow-run-detail-dialog .collaboration-flow {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-run-detail-dialog .collaboration-flow-node {
|
||||||
|
flex: 0 0 190px;
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--el-fill-color-blank);
|
||||||
|
padding: 10px;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
border-color 0.15s ease,
|
||||||
|
box-shadow 0.15s ease,
|
||||||
|
background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-run-detail-dialog .collaboration-flow-node:hover {
|
||||||
|
border-color: var(--el-color-primary-light-5);
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-run-detail-dialog .collaboration-flow-node.is-active {
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
background: var(--el-color-primary-light-9);
|
||||||
|
box-shadow: 0 0 0 2px var(--el-color-primary-light-7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-run-detail-dialog .collaboration-flow-arrow {
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 18px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-run-detail-dialog .flow-time {
|
||||||
|
display: block;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-run-detail-dialog .flow-title {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-run-detail-dialog .flow-summary {
|
||||||
|
display: -webkit-box;
|
||||||
|
margin-top: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-run-detail-dialog .collaboration-edge-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-run-detail-dialog .collaboration-edge-item {
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
padding: 3px 8px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-run-detail-dialog .collaboration-edge-item small {
|
||||||
|
margin-left: 4px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-run-detail-dialog .run-io-block .el-button {
|
||||||
|
height: auto;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-value-dialog .el-dialog__body {
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-value-full {
|
||||||
|
max-height: 70vh;
|
||||||
|
margin: 0;
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
padding: 14px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.workflow-run-detail-dialog .run-report-grid,
|
.workflow-run-detail-dialog .run-report-grid,
|
||||||
.workflow-run-detail-dialog .run-detail-main {
|
.workflow-run-detail-dialog .run-detail-main {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workflow-run-detail-dialog .collaboration-flow {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
overflow-x: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-run-detail-dialog .collaboration-flow-node {
|
||||||
|
flex: 1 1 180px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user