2023-10-02 08:33:43 -07:00
|
|
|
<template>
|
|
|
|
<div v-if="aiData" :class="$style.container">
|
|
|
|
<div :class="{ [$style.tree]: true, [$style.slim]: slim }">
|
2023-12-28 00:49:58 -08:00
|
|
|
<ElTree
|
2023-10-02 08:33:43 -07:00
|
|
|
:data="executionTree"
|
|
|
|
:props="{ label: 'node' }"
|
|
|
|
default-expand-all
|
|
|
|
:indent="12"
|
|
|
|
:expand-on-click-node="false"
|
2023-11-28 07:47:28 -08:00
|
|
|
data-test-id="lm-chat-logs-tree"
|
2023-12-28 00:49:58 -08:00
|
|
|
@node-click="onItemClick"
|
2023-10-02 08:33:43 -07:00
|
|
|
>
|
|
|
|
<template #default="{ node, data }">
|
|
|
|
<div
|
|
|
|
:class="{
|
|
|
|
[$style.treeNode]: true,
|
|
|
|
[$style.isSelected]: isTreeNodeSelected(data),
|
|
|
|
}"
|
|
|
|
:data-tree-depth="data.depth"
|
|
|
|
:style="{ '--item-depth': data.depth }"
|
|
|
|
>
|
|
|
|
<button
|
|
|
|
v-if="data.children.length"
|
2023-12-28 00:49:58 -08:00
|
|
|
:class="$style.treeToggle"
|
2023-10-02 08:33:43 -07:00
|
|
|
@click="toggleTreeItem(node)"
|
|
|
|
>
|
|
|
|
<font-awesome-icon :icon="node.expanded ? 'angle-down' : 'angle-up'" />
|
|
|
|
</button>
|
|
|
|
<n8n-tooltip :disabled="!slim" placement="right">
|
|
|
|
<template #content>
|
|
|
|
{{ node.label }}
|
|
|
|
</template>
|
|
|
|
<span :class="$style.leafLabel">
|
2023-12-28 00:49:58 -08:00
|
|
|
<NodeIcon :node-type="getNodeType(data.node)!" :size="17" />
|
|
|
|
<span v-if="!slim" v-text="node.label" />
|
2023-10-02 08:33:43 -07:00
|
|
|
</span>
|
|
|
|
</n8n-tooltip>
|
|
|
|
</div>
|
|
|
|
</template>
|
2023-12-28 00:49:58 -08:00
|
|
|
</ElTree>
|
2023-10-02 08:33:43 -07:00
|
|
|
</div>
|
|
|
|
<div :class="$style.runData">
|
|
|
|
<div v-if="selectedRun.length === 0" :class="$style.empty">
|
|
|
|
<n8n-text size="large">
|
|
|
|
{{
|
|
|
|
$locale.baseText('ndv.output.ai.empty', {
|
|
|
|
interpolate: {
|
|
|
|
node: props.node.name,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}}
|
|
|
|
</n8n-text>
|
|
|
|
</div>
|
2023-11-28 07:47:28 -08:00
|
|
|
<div
|
|
|
|
v-for="(data, index) in selectedRun"
|
|
|
|
:key="`${data.node}__${data.runIndex}__index`"
|
|
|
|
data-test-id="lm-chat-logs-entry"
|
|
|
|
>
|
2023-12-28 00:49:58 -08:00
|
|
|
<RunDataAiContent :input-data="data" :content-index="index" />
|
2023-10-02 08:33:43 -07:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
|
|
|
import type { Ref } from 'vue';
|
|
|
|
import { computed, ref, watch } from 'vue';
|
|
|
|
import type { ITaskSubRunMetadata, ITaskDataConnections } from 'n8n-workflow';
|
|
|
|
import { NodeConnectionType } from 'n8n-workflow';
|
|
|
|
import type { IAiData, IAiDataContent, INodeUi } from '@/Interface';
|
2023-11-28 03:15:08 -08:00
|
|
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
|
|
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
2023-10-02 08:33:43 -07:00
|
|
|
import NodeIcon from '@/components/NodeIcon.vue';
|
|
|
|
import RunDataAiContent from './RunDataAiContent.vue';
|
|
|
|
import { ElTree } from 'element-plus';
|
|
|
|
|
|
|
|
interface AIResult {
|
|
|
|
node: string;
|
|
|
|
runIndex: number;
|
|
|
|
data: IAiDataContent | undefined;
|
|
|
|
}
|
|
|
|
interface TreeNode {
|
|
|
|
node: string;
|
|
|
|
id: string;
|
|
|
|
children: TreeNode[];
|
|
|
|
depth: number;
|
|
|
|
startTime: number;
|
|
|
|
runIndex: number;
|
|
|
|
}
|
|
|
|
export interface Props {
|
|
|
|
node: INodeUi;
|
|
|
|
runIndex: number;
|
|
|
|
hideTitle?: boolean;
|
|
|
|
slim?: boolean;
|
|
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), { runIndex: 0 });
|
|
|
|
const workflowsStore = useWorkflowsStore();
|
|
|
|
const nodeTypesStore = useNodeTypesStore();
|
|
|
|
const selectedRun: Ref<IAiData[]> = ref([]);
|
|
|
|
|
|
|
|
function isTreeNodeSelected(node: TreeNode) {
|
|
|
|
return selectedRun.value.some((run) => run.node === node.node && run.runIndex === node.runIndex);
|
|
|
|
}
|
|
|
|
|
|
|
|
function getReferencedData(
|
|
|
|
reference: ITaskSubRunMetadata,
|
|
|
|
withInput: boolean,
|
|
|
|
withOutput: boolean,
|
|
|
|
): IAiDataContent[] {
|
|
|
|
const resultData = workflowsStore.getWorkflowResultDataByNodeName(reference.node);
|
|
|
|
|
|
|
|
if (!resultData?.[reference.runIndex]) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
const taskData = resultData[reference.runIndex];
|
|
|
|
|
|
|
|
if (!taskData) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
const returnData: IAiDataContent[] = [];
|
|
|
|
|
|
|
|
function addFunction(data: ITaskDataConnections | undefined, inOut: 'input' | 'output') {
|
|
|
|
if (!data) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
Object.keys(data).map((type) => {
|
|
|
|
returnData.push({
|
|
|
|
data: data[type][0],
|
|
|
|
inOut,
|
|
|
|
type: type as NodeConnectionType,
|
|
|
|
metadata: {
|
|
|
|
executionTime: taskData.executionTime,
|
|
|
|
startTime: taskData.startTime,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (withInput) {
|
|
|
|
addFunction(taskData.inputOverride, 'input');
|
|
|
|
}
|
|
|
|
if (withOutput) {
|
|
|
|
addFunction(taskData.data, 'output');
|
|
|
|
}
|
|
|
|
|
|
|
|
return returnData;
|
|
|
|
}
|
|
|
|
|
|
|
|
function toggleTreeItem(node: { expanded: boolean }) {
|
|
|
|
node.expanded = !node.expanded;
|
|
|
|
}
|
|
|
|
|
|
|
|
function onItemClick(data: TreeNode) {
|
|
|
|
const matchingRun = aiData.value?.find(
|
|
|
|
(run) => run.node === data.node && run.runIndex === data.runIndex,
|
|
|
|
);
|
|
|
|
if (!matchingRun) {
|
|
|
|
selectedRun.value = [];
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
selectedRun.value = [
|
|
|
|
{
|
|
|
|
node: data.node,
|
|
|
|
runIndex: data.runIndex,
|
|
|
|
data: getReferencedData(
|
|
|
|
{
|
|
|
|
node: data.node,
|
|
|
|
runIndex: data.runIndex,
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
true,
|
|
|
|
),
|
|
|
|
},
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
function getNodeType(nodeName: string) {
|
|
|
|
const node = workflowsStore.getNodeByName(nodeName);
|
|
|
|
if (!node) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
const nodeType = nodeTypesStore.getNodeType(node?.type);
|
|
|
|
|
|
|
|
return nodeType;
|
|
|
|
}
|
|
|
|
|
|
|
|
function selectFirst() {
|
|
|
|
if (executionTree.value.length && executionTree.value[0].children.length) {
|
|
|
|
onItemClick(executionTree.value[0].children[0]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const createNode = (
|
|
|
|
nodeName: string,
|
|
|
|
currentDepth: number,
|
|
|
|
r?: AIResult,
|
|
|
|
children: TreeNode[] = [],
|
|
|
|
): TreeNode => ({
|
|
|
|
node: nodeName,
|
|
|
|
id: nodeName,
|
|
|
|
depth: currentDepth,
|
|
|
|
startTime: r?.data?.metadata?.startTime ?? 0,
|
|
|
|
runIndex: r?.runIndex ?? 0,
|
|
|
|
children,
|
|
|
|
});
|
|
|
|
|
|
|
|
function getTreeNodeData(nodeName: string, currentDepth: number): TreeNode[] {
|
|
|
|
const { connectionsByDestinationNode } = workflowsStore.getCurrentWorkflow();
|
|
|
|
const connections = connectionsByDestinationNode[nodeName];
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
|
|
const resultData = aiData.value?.filter((data) => data.node === nodeName) ?? [];
|
|
|
|
|
|
|
|
if (!connections) {
|
|
|
|
return resultData.map((d) => createNode(nodeName, currentDepth, d));
|
|
|
|
}
|
|
|
|
|
|
|
|
const nonMainConnectionsKeys = Object.keys(connections).filter(
|
|
|
|
(key) => key !== NodeConnectionType.Main,
|
|
|
|
);
|
|
|
|
const children = nonMainConnectionsKeys.flatMap((key) =>
|
|
|
|
connections[key][0].flatMap((node) => getTreeNodeData(node.node, currentDepth + 1)),
|
|
|
|
);
|
|
|
|
|
|
|
|
if (resultData.length) {
|
|
|
|
return resultData.map((r) => createNode(nodeName, currentDepth, r, children));
|
|
|
|
}
|
|
|
|
|
|
|
|
children.sort((a, b) => a.startTime - b.startTime);
|
|
|
|
|
|
|
|
return [createNode(nodeName, currentDepth, undefined, children)];
|
|
|
|
}
|
|
|
|
|
|
|
|
const aiData = computed<AIResult[] | undefined>(() => {
|
|
|
|
const resultData = workflowsStore.getWorkflowResultDataByNodeName(props.node.name);
|
|
|
|
|
|
|
|
if (!resultData || !Array.isArray(resultData)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const subRun = resultData[props.runIndex].metadata?.subRun;
|
|
|
|
if (!Array.isArray(subRun)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Extend the subRun with the data and sort by adding execution time + startTime and comparing them
|
|
|
|
const subRunWithData = subRun.flatMap((run) =>
|
|
|
|
getReferencedData(run, false, true).map((data) => ({ ...run, data })),
|
|
|
|
);
|
|
|
|
|
|
|
|
subRunWithData.sort((a, b) => {
|
|
|
|
const aTime = a.data?.metadata?.startTime || 0;
|
|
|
|
const bTime = b.data?.metadata?.startTime || 0;
|
|
|
|
return aTime - bTime;
|
|
|
|
});
|
|
|
|
|
|
|
|
return subRunWithData;
|
|
|
|
});
|
|
|
|
|
|
|
|
const executionTree = computed<TreeNode[]>(() => {
|
|
|
|
const rootNode = props.node;
|
|
|
|
|
|
|
|
const tree = getTreeNodeData(rootNode.name, 0);
|
|
|
|
return tree || [];
|
|
|
|
});
|
|
|
|
|
|
|
|
watch(() => props.runIndex, selectFirst, { immediate: true });
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="scss" module>
|
|
|
|
.treeToggle {
|
|
|
|
border: none;
|
|
|
|
background-color: transparent;
|
|
|
|
padding: 0 var(--spacing-3xs);
|
|
|
|
margin: 0 calc(-1 * var(--spacing-3xs));
|
|
|
|
cursor: pointer;
|
|
|
|
}
|
|
|
|
.leafLabel {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
gap: var(--spacing-3xs);
|
|
|
|
}
|
|
|
|
.empty {
|
|
|
|
padding: var(--spacing-l);
|
|
|
|
}
|
|
|
|
.title {
|
|
|
|
font-size: var(--font-size-s);
|
|
|
|
margin-bottom: var(--spacing-xs);
|
|
|
|
}
|
|
|
|
.tree {
|
|
|
|
flex-shrink: 0;
|
|
|
|
min-width: 12.8rem;
|
|
|
|
height: 100%;
|
|
|
|
border-right: 1px solid var(--color-foreground-base);
|
|
|
|
padding-right: var(--spacing-xs);
|
|
|
|
padding-left: var(--spacing-2xs);
|
|
|
|
&.slim {
|
|
|
|
min-width: auto;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.runData {
|
|
|
|
width: 100%;
|
|
|
|
height: 100%;
|
|
|
|
overflow: auto;
|
|
|
|
}
|
|
|
|
.container {
|
|
|
|
height: 100%;
|
|
|
|
padding: 0 var(--spacing-xs);
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
:global(.el-tree > .el-tree-node) {
|
|
|
|
position: relative;
|
|
|
|
&:after {
|
|
|
|
content: '';
|
|
|
|
position: absolute;
|
|
|
|
top: 2rem;
|
|
|
|
bottom: 1.2rem;
|
|
|
|
left: 0.75rem;
|
|
|
|
width: 0.125rem;
|
|
|
|
background-color: var(--color-foreground-base);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
:global(.el-tree-node__expand-icon) {
|
|
|
|
display: none;
|
|
|
|
}
|
|
|
|
:global(.el-tree) {
|
|
|
|
margin-left: calc(-1 * var(--spacing-xs));
|
|
|
|
}
|
|
|
|
:global(.el-tree-node__content) {
|
|
|
|
margin-left: var(--spacing-xs);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.isSelected {
|
|
|
|
background-color: var(--color-foreground-base);
|
|
|
|
}
|
|
|
|
.treeNode {
|
|
|
|
display: inline-flex;
|
|
|
|
border-radius: var(--border-radius-base);
|
|
|
|
align-items: center;
|
|
|
|
gap: var(--spacing-3xs);
|
|
|
|
padding: var(--spacing-4xs) var(--spacing-3xs);
|
|
|
|
font-size: var(--font-size-xs);
|
|
|
|
color: var(--color-text-dark);
|
|
|
|
margin-bottom: var(--spacing-3xs);
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
background-color: var(--color-foreground-base);
|
|
|
|
}
|
|
|
|
&[data-tree-depth='0'] {
|
|
|
|
margin-left: calc(-1 * var(--spacing-2xs));
|
|
|
|
}
|
|
|
|
|
|
|
|
&:after {
|
|
|
|
content: '';
|
|
|
|
position: absolute;
|
|
|
|
margin: auto;
|
|
|
|
background-color: var(--color-foreground-base);
|
|
|
|
height: 0.125rem;
|
|
|
|
left: 0.75rem;
|
|
|
|
width: calc(var(--item-depth) * 0.625rem);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</style>
|