feat: improve workflow collaboration trace UX

This commit is contained in:
2026-06-16 07:02:10 +08:00
parent d5f1a4510f
commit 56c1eec051
6 changed files with 529 additions and 35 deletions
@@ -217,6 +217,7 @@ def _make_execution_log_entry(
node_type: str,
**extra: Any,
) -> dict:
extra.setdefault('timestamp', datetime.now().isoformat())
metadata = dict(extra.pop('metadata', {}) or {})
collaboration = _node_collaboration_metadata(node_map, node_id, node_type)
branch_id = extra.get('branch') or extra.get('branch_id')
@@ -238,6 +239,7 @@ def _make_execution_log_entry(
output=extra.get('output'),
error=extra.get('error'),
)
metadata['communication']['timestamp'] = extra.get('timestamp')
return {
'node_id': node_id,
'node_type': node_type,
@@ -982,8 +984,17 @@ class AIWorkflowService:
current_id = next_nodes[0] if next_nodes else None
continue
# 执行节点(异步)
node_inputs = _snapshot_node_inputs(branch_context)
# 执行节点(异步)。并行分支不能共享同一个 AsyncSession,否则模型/Provider 查询会并发抢占连接。
start_time = time.time()
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)
target_node_id = _resolve_handoff_target(
@@ -1004,6 +1015,7 @@ class AIWorkflowService:
elapsed_time=elapsed,
tokens_used=result.tokens_used,
metadata=_node_result_metadata(result),
inputs=copy.deepcopy(node_inputs),
branch=branch_id,
branch_label=branch_labels.get(branch_id, branch_id),
target_node_id=target_node_id,
@@ -1028,6 +1040,12 @@ class AIWorkflowService:
branch_context.previous_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:
current_id = result.next_node_id
@@ -281,6 +281,7 @@ export interface ExecutionLogEntry {
error?: string;
elapsed_time?: number;
tokens_used?: number;
timestamp?: string;
branch?: string;
branch_label?: string;
metadata?: Record<string, any>;
+12 -4
View File
@@ -13,11 +13,16 @@ type WindowWithIdleCallback = Window & {
let prefetched = false;
const prefetchTasks = [
() => import('#/views/ai-platform/agent/index.vue'),
const criticalPrefetchTasks = [
() => import('#/views/_core/agent-chat/index.vue'),
() => import('#/views/ai-platform/workflow/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/knowledge/index.vue'),
];
@@ -39,8 +44,11 @@ function prefetchAiPlatformPages() {
}
prefetched = true;
window.setTimeout(() => {
void Promise.allSettled(criticalPrefetchTasks.map((task) => task()));
}, 300);
scheduleIdle(() => {
void Promise.allSettled(prefetchTasks.map((task) => task()));
void Promise.allSettled(deferredPrefetchTasks.map((task) => task()));
});
}
@@ -104,16 +104,27 @@ onMounted(() => {
<template>
<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">
<ElEmpty :description="error || $t('ai-platform.agent.notFound')" />
</div>
</template>
<!-- 主内容 -->
<template v-else-if="!loading && agent">
<template v-else>
<!-- 左侧历史记录面板 -->
<div
v-if="showHistoryPanel"
@@ -1,7 +1,7 @@
<script setup lang="ts">
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 { MiniMap } from '@vue-flow/minimap';
@@ -13,6 +13,7 @@ import '@vue-flow/core/dist/style.css';
import '@vue-flow/core/dist/theme-default.css';
const props = defineProps<{
activeNodeId?: string;
edges: Edge[];
fitNodeId?: string;
nodes: Node[];
@@ -21,6 +22,38 @@ const props = defineProps<{
const containerRef = ref<HTMLElement>();
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) {
await nextTick();
if (!containerRef.value?.clientHeight) return;
@@ -71,8 +104,8 @@ onUnmounted(() => {
class="workflow-readonly-canvas bg-muted/30 h-full w-full min-h-[320px]"
>
<VueFlow
:nodes="props.nodes"
:edges="props.edges"
:nodes="displayNodes"
:edges="displayEdges"
:node-types="workflowNodeTypes"
:edge-types="workflowEdgeTypes"
:nodes-draggable="false"
@@ -115,4 +148,20 @@ onUnmounted(() => {
display: none !important;
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>
@@ -3,7 +3,7 @@ import type { Edge, Node } from '@vue-flow/core';
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 { ExternalLink } from '@vben/icons';
@@ -14,6 +14,7 @@ import {
ElButton,
ElCollapse,
ElCollapseItem,
ElDialog,
ElScrollbar,
ElTag,
} from 'element-plus';
@@ -44,13 +45,18 @@ const triggerOptions = getTriggerOptions();
const visible = ref(false);
const loading = ref(false);
const canvasLoading = ref(false);
const runId = ref('');
const run = ref<WorkflowRun | null>(null);
const nodes = ref<Node[]>([]);
const edges = ref<Edge[]>([]);
const activeNodeId = ref('');
const activeLogIndex = ref(-1);
const definitionFallback = ref(false);
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 executionLogs = computed(() => run.value?.execution_log || []);
@@ -83,6 +89,12 @@ const finalOutput = computed(() => {
.filter(Boolean);
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 agents = new Map<
string,
@@ -114,6 +126,7 @@ const agentSummaries = computed(() => {
return [...agents.values()];
});
const collaborationTimeline = computed(() => {
let estimatedOffset = 0;
return executionLogs.value
.map((log, index) => {
const meta = getLogMeta(log);
@@ -137,6 +150,11 @@ const collaborationTimeline = computed(() => {
const model = meta.model || collaboration.model || meta.model_id || collaboration.model_id;
const targetName =
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 =
agentName ||
subflowName ||
@@ -154,6 +172,7 @@ const collaborationTimeline = computed(() => {
return {
key: `${log.node_id || 'node'}-${index}`,
index,
nodeId: log.node_id,
title,
subtitle: log.node_label || log.node_id,
@@ -161,19 +180,121 @@ const collaborationTimeline = computed(() => {
summary: summaryParts.join(' · '),
message: communication.message || previewValue(log.error || log.output),
elapsed: log.elapsed_time,
timestamp,
tokens: log.tokens_used || 0,
hasSignal: Boolean(agentName || branch || subflowName || model || communication.channel),
};
})
.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() {
if (!runId.value) return;
loading.value = true;
canvasLoading.value = false;
definitionFallback.value = false;
activeNodeId.value = '';
activeLogIndex.value = -1;
run.value = null;
nodes.value = [];
edges.value = [];
@@ -181,6 +302,14 @@ async function loadDetail() {
try {
const detail = await getWorkflowRunDetailApi(runId.value);
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;
if (!definition?.nodes?.length) {
@@ -200,11 +329,11 @@ async function loadDetail() {
const replayed = applyRunReplay(base.nodes, base.edges, detail);
nodes.value = replayed.nodes;
edges.value = replayed.edges;
activeLogNames.value = executionLogs.value.slice(0, 1).map((_, index) => String(index));
} catch (error) {
console.error(error);
} finally {
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) {
return log?.metadata || {};
}
@@ -294,9 +453,19 @@ function getTargetName(log: any) {
return target.agent_name || target.agent_code || target.node_label || '';
}
function selectLog(nodeId?: string) {
function selectLog(nodeId?: string, index?: number) {
if (!nodeId) return;
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 });
@@ -307,7 +476,7 @@ defineExpose({ open });
v-model="visible"
class="workflow-run-detail-dialog"
:title="dialogTitle"
:loading="loading"
:loading="false"
default-fullscreen
:draggable="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="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') }}
</span>
<ElButton link type="primary" @click="openValueDialog('任务输入', run.inputs)">
查看全部
</ElButton>
</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 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') }}
</span>
<ElButton link type="primary" @click="openValueDialog('最终输出', run.error_message || run.outputs)">
查看全部
</ElButton>
</div>
<pre
:class="run.status === 'failed' ? 'text-destructive' : ''"
class="run-json run-report-json"
>{{ run.error_message || finalOutput }}</pre>
class="run-json run-report-json is-clickable"
@click="openValueDialog('最终输出', run.error_message || run.outputs)"
>{{ compactValue(run.error_message || finalOutput) }}</pre>
</div>
<div class="border-border bg-muted/30 rounded-lg border p-3">
<div class="mb-2 flex items-center justify-between">
@@ -435,8 +615,9 @@ defineExpose({ open });
v-for="item in collaborationTimeline"
:key="item.key"
class="collaboration-item"
:class="{ 'is-active': item.nodeId === activeNodeId }"
type="button"
@click="selectLog(item.nodeId)"
@click="selectLog(item.nodeId, item.index)"
>
<span
class="collaboration-dot"
@@ -450,8 +631,9 @@ defineExpose({ open });
{{ item.summary || item.subtitle }}
</span>
</span>
<span class="text-muted-foreground shrink-0 text-xs">
{{ formatDuration(item.elapsed) }}
<span class="text-muted-foreground shrink-0 text-right text-xs">
<span class="block">{{ formatTimelineTime(item.timestamp) }}</span>
<span class="block">{{ formatDuration(item.elapsed) }}</span>
</span>
</button>
</div>
@@ -462,11 +644,65 @@ defineExpose({ open });
</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-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
:nodes="nodes"
:edges="edges"
:active-node-id="activeNodeId"
:fit-node-id="activeNodeId"
/>
</div>
@@ -495,7 +731,8 @@ defineExpose({ open });
<template #title>
<div
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">
{{ getLogStatusText(log.status) }}
@@ -585,17 +822,23 @@ defineExpose({ open });
</div>
</div>
</div>
<div>
<div class="text-muted-foreground mb-1 font-medium">
{{ $t('ai-platform.workflowRuns.detail.inputsOutputs') }} - 输入
<div class="run-io-block">
<div class="text-muted-foreground mb-1 flex items-center justify-between gap-2 font-medium">
<span>{{ $t('ai-platform.workflowRuns.detail.inputsOutputs') }} - 输入</span>
<ElButton link type="primary" @click="openValueDialog(`${log.node_label || log.node_id} 输入`, log.inputs)">
查看全部
</ElButton>
</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 class="text-muted-foreground mb-1 font-medium">
{{ $t('ai-platform.workflowRuns.detail.inputsOutputs') }} - 输出
<div class="run-io-block">
<div class="text-muted-foreground mb-1 flex items-center justify-between gap-2 font-medium">
<span>{{ $t('ai-platform.workflowRuns.detail.inputsOutputs') }} - 输出</span>
<ElButton link type="primary" @click="openValueDialog(`${log.node_label || log.node_id} 输出`, log.output)">
查看全部
</ElButton>
</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 class="text-muted-foreground flex gap-3">
<span>
@@ -612,6 +855,23 @@ defineExpose({ open });
</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>
</template>
@@ -659,8 +919,21 @@ defineExpose({ open });
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 {
max-height: 140px;
min-height: 120px;
max-height: 240px;
}
.workflow-run-detail-dialog .collaboration-timeline {
@@ -688,6 +961,12 @@ defineExpose({ open });
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 {
width: 8px;
height: 8px;
@@ -711,10 +990,138 @@ defineExpose({ open });
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) {
.workflow-run-detail-dialog .run-report-grid,
.workflow-run-detail-dialog .run-detail-main {
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>