Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import type { ExecutionLogEntry } from '#/api/ai-platform/ai-platform';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElEmpty, ElScrollbar, ElTimeline, ElTimelineItem } from 'element-plus';
|
||||
|
||||
import RunStatusTag from './RunStatusTag.vue';
|
||||
|
||||
defineProps<{
|
||||
activeNodeId?: string;
|
||||
logs: ExecutionLogEntry[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [nodeId: string];
|
||||
}>();
|
||||
|
||||
function formatDuration(ms?: number) {
|
||||
if (!ms && ms !== 0) return '-';
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
return `${(ms / 1000).toFixed(2)}s`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElScrollbar class="h-full">
|
||||
<ElEmpty
|
||||
v-if="logs.length === 0"
|
||||
:description="$t('ai-platform.workflowRuns.detail.noLogs')"
|
||||
/>
|
||||
<ElTimeline v-else class="px-2 py-3">
|
||||
<ElTimelineItem
|
||||
v-for="(log, index) in logs"
|
||||
:key="`${log.node_id}-${index}`"
|
||||
:timestamp="formatDuration(log.elapsed_time)"
|
||||
placement="top"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="hover:bg-muted w-full rounded border px-3 py-2 text-left transition-colors"
|
||||
:class="activeNodeId === log.node_id ? 'border-primary bg-primary/5' : 'border-border'"
|
||||
@click="emit('select', log.node_id)"
|
||||
>
|
||||
<div class="mb-1 flex items-center justify-between gap-2">
|
||||
<span class="text-foreground text-sm font-medium">
|
||||
{{ log.node_label || log.node_id }}
|
||||
</span>
|
||||
<RunStatusTag :status="log.status" />
|
||||
</div>
|
||||
<div class="text-muted-foreground text-xs">
|
||||
{{ log.node_type }}
|
||||
</div>
|
||||
<div
|
||||
v-if="log.error"
|
||||
class="text-destructive mt-1 line-clamp-2 text-xs"
|
||||
>
|
||||
{{ log.error }}
|
||||
</div>
|
||||
</button>
|
||||
</ElTimelineItem>
|
||||
</ElTimeline>
|
||||
</ElScrollbar>
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElTag } from 'element-plus';
|
||||
|
||||
const props = defineProps<{ status: string }>();
|
||||
|
||||
const tagType = computed(() => {
|
||||
switch (props.status) {
|
||||
case 'completed':
|
||||
return 'success';
|
||||
case 'failed':
|
||||
return 'danger';
|
||||
case 'running':
|
||||
return 'primary';
|
||||
case 'waiting':
|
||||
return 'warning';
|
||||
case 'stopped':
|
||||
return 'info';
|
||||
default:
|
||||
return 'info';
|
||||
}
|
||||
});
|
||||
|
||||
const label = computed(() => {
|
||||
const key = `ai-platform.workflowRuns.status.${props.status}`;
|
||||
const translated = $t(key);
|
||||
return translated === key ? props.status : translated;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElTag :type="tagType" size="small">{{ label }}</ElTag>
|
||||
</template>
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
<script setup lang="ts">
|
||||
import type { Edge, Node } from '@vue-flow/core';
|
||||
|
||||
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
import { useVueFlow, VueFlow } from '@vue-flow/core';
|
||||
import { MiniMap } from '@vue-flow/minimap';
|
||||
|
||||
import { workflowEdgeTypes, workflowNodeTypes } from '../../workflow/shared/workflowNodeTypes';
|
||||
|
||||
import '@vue-flow/minimap/dist/style.css';
|
||||
import '@vue-flow/core/dist/style.css';
|
||||
import '@vue-flow/core/dist/theme-default.css';
|
||||
|
||||
const props = defineProps<{
|
||||
edges: Edge[];
|
||||
fitNodeId?: string;
|
||||
nodes: Node[];
|
||||
}>();
|
||||
|
||||
const containerRef = ref<HTMLElement>();
|
||||
const { fitView } = useVueFlow();
|
||||
|
||||
async function refreshView(nodeId?: string) {
|
||||
await nextTick();
|
||||
if (!containerRef.value?.clientHeight) return;
|
||||
if (nodeId) {
|
||||
fitView({ nodes: [nodeId], padding: 0.4, duration: 200 });
|
||||
return;
|
||||
}
|
||||
fitView({ padding: 0.2, duration: 200 });
|
||||
}
|
||||
|
||||
function onNodesInitialized() {
|
||||
refreshView(props.fitNodeId);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.fitNodeId,
|
||||
(nodeId) => {
|
||||
if (nodeId) refreshView(nodeId);
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => [props.nodes, props.edges],
|
||||
() => refreshView(props.fitNodeId),
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
let resizeObserver: ResizeObserver | undefined;
|
||||
|
||||
onMounted(() => {
|
||||
if (!containerRef.value) return;
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
if (containerRef.value && containerRef.value.clientHeight > 0) {
|
||||
refreshView(props.fitNodeId);
|
||||
}
|
||||
});
|
||||
resizeObserver.observe(containerRef.value);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
resizeObserver?.disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="workflow-readonly-canvas bg-muted/30 h-full w-full min-h-[320px]"
|
||||
>
|
||||
<VueFlow
|
||||
:nodes="props.nodes"
|
||||
:edges="props.edges"
|
||||
:node-types="workflowNodeTypes"
|
||||
:edge-types="workflowEdgeTypes"
|
||||
:nodes-draggable="false"
|
||||
:nodes-connectable="false"
|
||||
:elements-selectable="false"
|
||||
:pan-on-drag="[1, 2]"
|
||||
:zoom-on-scroll="true"
|
||||
@nodes-initialized="onNodesInitialized"
|
||||
>
|
||||
<MiniMap pannable zoomable />
|
||||
</VueFlow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.workflow-readonly-canvas :deep(.vue-flow) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.workflow-readonly-canvas :deep(.vue-flow__container) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.workflow-readonly-canvas :deep(.vue-flow__node) {
|
||||
cursor: default;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.workflow-readonly-canvas :deep(.nopan) {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.workflow-readonly-canvas :deep(.vue-flow__panel.top) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.workflow-readonly-canvas :deep(.vue-flow__edge button) {
|
||||
display: none !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,160 @@
|
||||
import type { Column } from 'element-plus';
|
||||
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { getWorkflowListApi } from '#/api/ai-platform/ai-platform';
|
||||
|
||||
type TagType = 'danger' | 'info' | 'primary' | 'success' | 'warning';
|
||||
|
||||
export function getStatusOptions() {
|
||||
return [
|
||||
{ label: $t('ai-platform.workflowRuns.status.pending'), value: 'pending', type: 'info' as TagType },
|
||||
{ label: $t('ai-platform.workflowRuns.status.running'), value: 'running', type: 'primary' as TagType },
|
||||
{ label: $t('ai-platform.workflowRuns.status.waiting'), value: 'waiting', type: 'warning' as TagType },
|
||||
{ label: $t('ai-platform.workflowRuns.status.completed'), value: 'completed', type: 'success' as TagType },
|
||||
{ label: $t('ai-platform.workflowRuns.status.failed'), value: 'failed', type: 'danger' as TagType },
|
||||
{ label: $t('ai-platform.workflowRuns.status.stopped'), value: 'stopped', type: 'info' as TagType },
|
||||
];
|
||||
}
|
||||
|
||||
export function getTriggerOptions() {
|
||||
return [
|
||||
{ label: $t('ai-platform.workflowRuns.trigger.editor_draft'), value: 'editor_draft', type: 'warning' as TagType },
|
||||
{ label: $t('ai-platform.workflowRuns.trigger.editor_published'), value: 'editor_published', type: 'primary' as TagType },
|
||||
{ label: $t('ai-platform.workflowRuns.trigger.agent'), value: 'agent', type: 'success' as TagType },
|
||||
{ label: $t('ai-platform.workflowRuns.trigger.api'), value: 'api', type: 'info' as TagType },
|
||||
{ label: $t('ai-platform.workflowRuns.trigger.form_button'), value: 'form_button', type: 'info' as TagType },
|
||||
];
|
||||
}
|
||||
|
||||
export function getTagType(value: string, options: Array<{ type?: TagType; value: string }>): TagType {
|
||||
const option = options.find((item) => item.value === value);
|
||||
return option?.type || 'info';
|
||||
}
|
||||
|
||||
export function getTagLabel(value: string, options: Array<{ label: string; value: string }>): string {
|
||||
const option = options.find((item) => item.value === value);
|
||||
return option?.label || value;
|
||||
}
|
||||
|
||||
export function formatDuration(ms?: number) {
|
||||
if (!ms && ms !== 0) return '-';
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
return `${(ms / 1000).toFixed(2)}s`;
|
||||
}
|
||||
|
||||
export function formatTime(value?: string) {
|
||||
if (!value) return '-';
|
||||
return new Date(value).toLocaleString();
|
||||
}
|
||||
|
||||
export function useSearchFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
fieldName: 'workflowId',
|
||||
label: $t('ai-platform.workflowRuns.filters.workflow'),
|
||||
componentProps: {
|
||||
api: async () => {
|
||||
const res = await getWorkflowListApi({ page: 1, pageSize: 200 });
|
||||
return (res.items || []).map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}));
|
||||
},
|
||||
placeholder: $t('ai-platform.workflowRuns.filters.allWorkflows'),
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'status',
|
||||
label: $t('ai-platform.workflowRuns.filters.status'),
|
||||
componentProps: {
|
||||
placeholder: $t('ai-platform.workflowRuns.filters.allStatus'),
|
||||
options: getStatusOptions(),
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'triggerType',
|
||||
label: $t('ai-platform.workflowRuns.filters.trigger'),
|
||||
componentProps: {
|
||||
placeholder: $t('ai-platform.workflowRuns.filters.allTriggers'),
|
||||
options: getTriggerOptions(),
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function useZqTableColumns(): Column[] {
|
||||
return [
|
||||
{
|
||||
key: 'workflow_name',
|
||||
dataKey: 'workflow_name',
|
||||
title: $t('ai-platform.workflowRuns.columns.workflow'),
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
title: $t('ai-platform.workflowRuns.columns.status'),
|
||||
width: 110,
|
||||
align: 'center' as const,
|
||||
slots: { default: 'cell-status' },
|
||||
},
|
||||
{
|
||||
key: 'trigger_type',
|
||||
title: $t('ai-platform.workflowRuns.columns.trigger'),
|
||||
width: 140,
|
||||
align: 'center' as const,
|
||||
slots: { default: 'cell-trigger_type' },
|
||||
},
|
||||
{
|
||||
key: 'total_steps',
|
||||
dataKey: 'total_steps',
|
||||
title: $t('ai-platform.workflowRuns.columns.steps'),
|
||||
width: 80,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
key: 'total_tokens',
|
||||
dataKey: 'total_tokens',
|
||||
title: $t('ai-platform.workflowRuns.columns.tokens'),
|
||||
width: 90,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
key: 'elapsed_time',
|
||||
title: $t('ai-platform.workflowRuns.columns.duration'),
|
||||
width: 100,
|
||||
align: 'center' as const,
|
||||
slots: { default: 'cell-elapsed_time' },
|
||||
},
|
||||
{
|
||||
key: 'started_at',
|
||||
title: $t('ai-platform.workflowRuns.columns.startedAt'),
|
||||
width: 180,
|
||||
slots: { default: 'cell-started_at' },
|
||||
},
|
||||
{
|
||||
key: 'error_message',
|
||||
dataKey: 'error_message',
|
||||
title: $t('ai-platform.workflowRuns.columns.error'),
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true,
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
title: $t('ai-platform.workflowRuns.columns.actions'),
|
||||
width: 120,
|
||||
fixed: true,
|
||||
align: 'center' as const,
|
||||
slots: { default: 'cell-actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<script lang="ts" setup>
|
||||
import type { WorkflowRunListItem } from '#/api/ai-platform/ai-platform';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Eye } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElButton, ElTag } from 'element-plus';
|
||||
|
||||
import { getAllWorkflowRunsApi } from '#/api/ai-platform/ai-platform';
|
||||
import { useZqTable } from '#/components/zq-table';
|
||||
|
||||
import RunStatusTag from './components/RunStatusTag.vue';
|
||||
import {
|
||||
formatDuration,
|
||||
formatTime,
|
||||
getTagLabel,
|
||||
getTagType,
|
||||
getTriggerOptions,
|
||||
useSearchFormSchema,
|
||||
useZqTableColumns,
|
||||
} from './data';
|
||||
import DetailDialog from './modules/detail-dialog.vue';
|
||||
|
||||
defineOptions({ name: 'WorkflowRunHistory' });
|
||||
|
||||
const detailRef = ref<InstanceType<typeof DetailDialog>>();
|
||||
const triggerOptions = getTriggerOptions();
|
||||
|
||||
const fetchRunList = async (params: any) => {
|
||||
const res = await getAllWorkflowRunsApi({
|
||||
page: params.page.currentPage,
|
||||
pageSize: params.page.pageSize,
|
||||
workflowId: params.form?.workflowId || undefined,
|
||||
status: params.form?.status || undefined,
|
||||
triggerType: params.form?.triggerType || undefined,
|
||||
});
|
||||
return {
|
||||
items: res.items,
|
||||
total: res.total,
|
||||
};
|
||||
};
|
||||
|
||||
const [Grid] = useZqTable({
|
||||
gridOptions: {
|
||||
columns: useZqTableColumns(),
|
||||
border: true,
|
||||
stripe: true,
|
||||
showIndex: true,
|
||||
proxyConfig: {
|
||||
autoLoad: true,
|
||||
ajax: {
|
||||
query: fetchRunList,
|
||||
},
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
pageSize: 20,
|
||||
},
|
||||
toolbarConfig: {
|
||||
search: true,
|
||||
refresh: true,
|
||||
zoom: true,
|
||||
custom: true,
|
||||
},
|
||||
},
|
||||
formOptions: {
|
||||
schema: useSearchFormSchema(),
|
||||
showCollapseButton: true,
|
||||
submitOnChange: true,
|
||||
},
|
||||
});
|
||||
|
||||
function openDetail(row: WorkflowRunListItem) {
|
||||
detailRef.value?.open(row.id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<DetailDialog ref="detailRef" />
|
||||
|
||||
<Grid>
|
||||
<template #cell-status="{ row }">
|
||||
<RunStatusTag :status="row.status" />
|
||||
</template>
|
||||
|
||||
<template #cell-trigger_type="{ row }">
|
||||
<ElTag
|
||||
:type="getTagType(row.trigger_type, triggerOptions)"
|
||||
size="small"
|
||||
>
|
||||
{{ getTagLabel(row.trigger_type, triggerOptions) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
|
||||
<template #cell-elapsed_time="{ row }">
|
||||
{{ formatDuration(row.elapsed_time) }}
|
||||
</template>
|
||||
|
||||
<template #cell-started_at="{ row }">
|
||||
{{ formatTime(row.started_at) }}
|
||||
</template>
|
||||
|
||||
<template #cell-actions="{ row }">
|
||||
<ElButton link type="primary" :icon="Eye" @click.stop="openDetail(row)">
|
||||
{{ $t('common.view') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,236 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Edge, Node } from '@vue-flow/core';
|
||||
|
||||
import type { WorkflowRun } from '#/api/ai-platform/ai-platform';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { ExternalLink } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElAlert, ElButton, ElTag } from 'element-plus';
|
||||
|
||||
import { getWorkflowDetailApi, getWorkflowRunDetailApi } from '#/api/ai-platform/ai-platform';
|
||||
import { ZqDesc, ZqDescItem } from '#/components/zq-desc';
|
||||
import { ZqDialog } from '#/components/zq-dialog';
|
||||
import { useAppContextStore } from '#/store/app-context';
|
||||
|
||||
import { definitionToFlowElements } from '../../workflow/shared/loadDefinition';
|
||||
import { applyRunReplay } from '../../workflow/shared/useRunReplay';
|
||||
import RunStatusTag from '../components/RunStatusTag.vue';
|
||||
import WorkflowReadonlyCanvas from '../components/WorkflowReadonlyCanvas.vue';
|
||||
import {
|
||||
formatDuration,
|
||||
formatTime,
|
||||
getTagLabel,
|
||||
getTagType,
|
||||
getTriggerOptions,
|
||||
} from '../data';
|
||||
|
||||
const router = useRouter();
|
||||
const appContextStore = useAppContextStore();
|
||||
const triggerOptions = getTriggerOptions();
|
||||
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const runId = ref('');
|
||||
const run = ref<WorkflowRun | null>(null);
|
||||
const nodes = ref<Node[]>([]);
|
||||
const edges = ref<Edge[]>([]);
|
||||
const activeNodeId = ref('');
|
||||
const definitionFallback = ref(false);
|
||||
|
||||
const dialogTitle = computed(() => run.value?.workflow_name || $t('ai-platform.workflowRuns.detail.runId'));
|
||||
|
||||
async function loadDetail() {
|
||||
if (!runId.value) return;
|
||||
|
||||
loading.value = true;
|
||||
definitionFallback.value = false;
|
||||
activeNodeId.value = '';
|
||||
run.value = null;
|
||||
nodes.value = [];
|
||||
edges.value = [];
|
||||
|
||||
try {
|
||||
const detail = await getWorkflowRunDetailApi(runId.value);
|
||||
run.value = detail;
|
||||
|
||||
let definition = detail.definition_snapshot;
|
||||
if (!definition?.nodes?.length) {
|
||||
definitionFallback.value = true;
|
||||
try {
|
||||
const workflow = await getWorkflowDetailApi(detail.workflow_id);
|
||||
definition = detail.use_draft
|
||||
? workflow.definition
|
||||
: workflow.published_definition || workflow.definition;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
definition = { nodes: [], edges: [] };
|
||||
}
|
||||
}
|
||||
|
||||
const base = definitionToFlowElements(definition || { nodes: [], edges: [] });
|
||||
const replayed = applyRunReplay(base.nodes, base.edges, detail);
|
||||
nodes.value = replayed.nodes;
|
||||
edges.value = replayed.edges;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function open(id: string) {
|
||||
runId.value = id;
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
function handleOpen() {
|
||||
loadDetail();
|
||||
}
|
||||
|
||||
function openEditor() {
|
||||
if (!run.value?.workflow_id) return;
|
||||
visible.value = false;
|
||||
router.push(
|
||||
appContextStore.getContextPath(
|
||||
`/ai-platform/workflow/editor/${run.value.workflow_id}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function triggerLabel(value?: string) {
|
||||
if (!value) return '-';
|
||||
return getTagLabel(value, triggerOptions);
|
||||
}
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ZqDialog
|
||||
v-model="visible"
|
||||
class="workflow-run-detail-dialog"
|
||||
:title="dialogTitle"
|
||||
:loading="loading"
|
||||
default-fullscreen
|
||||
:draggable="false"
|
||||
:show-footer="false"
|
||||
@open="handleOpen"
|
||||
>
|
||||
<template #title>
|
||||
<span class="flex items-center gap-2">
|
||||
{{ dialogTitle }}
|
||||
<RunStatusTag v-if="run" :status="run.status" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #header-extra>
|
||||
<ElButton
|
||||
v-if="run?.workflow_id"
|
||||
:icon="ExternalLink"
|
||||
@click="openEditor"
|
||||
>
|
||||
{{ $t('ai-platform.workflowRuns.detail.openEditor') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
|
||||
<div v-if="run" class="run-detail-layout flex h-full min-h-0 flex-col gap-4">
|
||||
<ElAlert
|
||||
v-if="definitionFallback"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
:title="$t('ai-platform.workflowRuns.detail.snapshotMissing')"
|
||||
/>
|
||||
|
||||
<ZqDesc :column="4">
|
||||
<ZqDescItem :label="$t('ai-platform.workflowRuns.detail.runId')">
|
||||
{{ runId }}
|
||||
</ZqDescItem>
|
||||
<ZqDescItem :label="$t('ai-platform.workflowRuns.columns.trigger')">
|
||||
<ElTag
|
||||
:type="getTagType(run.trigger_type, triggerOptions)"
|
||||
size="small"
|
||||
>
|
||||
{{ triggerLabel(run.trigger_type) }}
|
||||
</ElTag>
|
||||
</ZqDescItem>
|
||||
<ZqDescItem :label="$t('ai-platform.workflowRuns.columns.steps')">
|
||||
{{ run.total_steps }}
|
||||
</ZqDescItem>
|
||||
<ZqDescItem :label="$t('ai-platform.workflowRuns.columns.tokens')">
|
||||
{{ run.total_tokens }}
|
||||
</ZqDescItem>
|
||||
<ZqDescItem :label="$t('ai-platform.workflowRuns.columns.duration')">
|
||||
{{ formatDuration(run.elapsed_time) }}
|
||||
</ZqDescItem>
|
||||
<ZqDescItem :label="$t('ai-platform.workflowRuns.columns.startedAt')">
|
||||
{{ formatTime(run.started_at) }}
|
||||
</ZqDescItem>
|
||||
<ZqDescItem :label="$t('ai-platform.workflowRuns.detail.completedAt')">
|
||||
{{ formatTime(run.completed_at) }}
|
||||
</ZqDescItem>
|
||||
<ZqDescItem :label="$t('ai-platform.workflowRuns.detail.version')">
|
||||
{{
|
||||
run.use_draft
|
||||
? $t('ai-platform.workflowRuns.detail.draft')
|
||||
: run.workflow_version || '-'
|
||||
}}
|
||||
</ZqDescItem>
|
||||
<ZqDescItem
|
||||
v-if="run.error_message"
|
||||
:label="$t('ai-platform.workflowRuns.columns.error')"
|
||||
:span="4"
|
||||
>
|
||||
<span class="text-destructive">{{ run.error_message }}</span>
|
||||
</ZqDescItem>
|
||||
</ZqDesc>
|
||||
|
||||
<div class="run-detail-canvas border-border overflow-hidden rounded-lg border">
|
||||
<WorkflowReadonlyCanvas
|
||||
:nodes="nodes"
|
||||
:edges="edges"
|
||||
:fit-node-id="activeNodeId"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ZqDialog>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.workflow-run-detail-dialog.is-fullscreen .el-dialog__body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.workflow-run-detail-dialog .zq-dialog-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.workflow-run-detail-dialog.is-fullscreen .zq-dialog-body .el-scrollbar,
|
||||
.workflow-run-detail-dialog.is-fullscreen .zq-dialog-body .el-scrollbar__wrap,
|
||||
.workflow-run-detail-dialog.is-fullscreen .zq-dialog-body .el-scrollbar__view,
|
||||
.workflow-run-detail-dialog.is-fullscreen .zq-dialog-body .el-scrollbar__view > div {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.workflow-run-detail-dialog .run-detail-layout {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.workflow-run-detail-dialog .run-detail-canvas {
|
||||
flex: 1;
|
||||
min-height: 320px;
|
||||
height: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user