chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1 @@
|
||||
公共组件目录
|
||||
@@ -0,0 +1 @@
|
||||
公共hook目录
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export {
|
||||
TraceFlamethread,
|
||||
TraceTree,
|
||||
useSpanTransform,
|
||||
} from './modules/query-trace';
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type GlobalStyle,
|
||||
type RectStyle,
|
||||
type LabelStyle,
|
||||
type LabelText,
|
||||
} from './typing';
|
||||
|
||||
export const defaultRectStyle: RectStyle = {
|
||||
normal: {
|
||||
fill: '#F7F7FA',
|
||||
stroke: '#1D1C2314',
|
||||
lineWidth: 1,
|
||||
lineDash: [],
|
||||
},
|
||||
hover: {
|
||||
lineWidth: 1,
|
||||
lineDash: [],
|
||||
},
|
||||
select: {
|
||||
lineWidth: 1,
|
||||
lineDash: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const defaultGlobalStyle: GlobalStyle = {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
padding: {
|
||||
top: 0,
|
||||
right: 24,
|
||||
bottom: 24,
|
||||
left: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const defaultDatazoomDecimals = 1;
|
||||
|
||||
export const defaultVisibleRowCount = 6;
|
||||
export const defaultRowHeight = 42;
|
||||
export const defaultVisibleColumnCount = 6; // 13 // 8
|
||||
|
||||
export const defaultLabelStyle: LabelStyle = {
|
||||
position: 'inside-left',
|
||||
fontSize: 12,
|
||||
fill: '#212629',
|
||||
};
|
||||
|
||||
export const defaultLabelText: LabelText = (datum, element, params) =>
|
||||
`${datum.start}-${datum.end}`;
|
||||
|
||||
// padding of xScale (solve the problem of rect border being truncated after hover)
|
||||
export const scrollbarMargin = 10;
|
||||
export const datazoomHeight = 20;
|
||||
export const datazoomDecimals = 0;
|
||||
export const datazoomPaddingBottom = 18;
|
||||
+692
@@ -0,0 +1,692 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
useRef,
|
||||
useEffect,
|
||||
type FC,
|
||||
useMemo,
|
||||
useCallback,
|
||||
useLayoutEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { pick, uniqWith } from 'lodash-es';
|
||||
import {
|
||||
View,
|
||||
type ViewSpec,
|
||||
ComponentEnum,
|
||||
GrammarMarkType,
|
||||
type GrammarScaleType,
|
||||
type MarkSpec,
|
||||
type IView,
|
||||
} from '@visactor/vgrammar';
|
||||
|
||||
import type {
|
||||
FlamethreadProps,
|
||||
RectNode,
|
||||
RectStyle,
|
||||
LabelStyle,
|
||||
LabelText,
|
||||
Tooltip,
|
||||
IElement,
|
||||
GlobalStyle,
|
||||
InteractionEventHandler,
|
||||
} from './typing';
|
||||
import {
|
||||
datazoomDecimals,
|
||||
datazoomHeight,
|
||||
datazoomPaddingBottom,
|
||||
defaultGlobalStyle,
|
||||
defaultLabelStyle,
|
||||
defaultLabelText,
|
||||
defaultRectStyle,
|
||||
defaultRowHeight,
|
||||
defaultVisibleColumnCount,
|
||||
scrollbarMargin,
|
||||
} from './config';
|
||||
|
||||
export type {
|
||||
FlamethreadProps,
|
||||
RectNode,
|
||||
RectStyle,
|
||||
LabelStyle,
|
||||
LabelText,
|
||||
Tooltip,
|
||||
IElement,
|
||||
GlobalStyle,
|
||||
InteractionEventHandler,
|
||||
};
|
||||
|
||||
export const Flamethread: FC<FlamethreadProps> = props => {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const viewRef = useRef<IView | null>(null);
|
||||
const [viewSize, setViewSize] = useState({ width: 0, height: 0 });
|
||||
const {
|
||||
flamethreadData,
|
||||
rowHeight = defaultRowHeight,
|
||||
visibleColumnCount = defaultVisibleColumnCount,
|
||||
tooltip,
|
||||
rectStyle: globalRectStyle,
|
||||
labelStyle: _globalLabelStyle,
|
||||
globalStyle: _globalStyle,
|
||||
axisLabelSuffix,
|
||||
labelText,
|
||||
selectedKey,
|
||||
disableViewScroll = false,
|
||||
enableAutoFit = false,
|
||||
onClick,
|
||||
} = props;
|
||||
|
||||
const genRectStyle = useCallback(
|
||||
(rectStyle?: RectStyle): RectStyle => ({
|
||||
normal: Object.assign(
|
||||
{},
|
||||
defaultRectStyle.normal,
|
||||
globalRectStyle?.normal,
|
||||
rectStyle?.normal,
|
||||
),
|
||||
hover: Object.assign(
|
||||
{},
|
||||
defaultRectStyle.hover,
|
||||
globalRectStyle?.hover,
|
||||
rectStyle?.hover,
|
||||
),
|
||||
select: Object.assign(
|
||||
{},
|
||||
defaultRectStyle.select,
|
||||
globalRectStyle?.select,
|
||||
rectStyle?.select,
|
||||
),
|
||||
}),
|
||||
[globalRectStyle],
|
||||
);
|
||||
|
||||
const genLabelStyle = useCallback(
|
||||
(labelStyle?: LabelStyle): LabelStyle =>
|
||||
Object.assign({}, defaultLabelStyle, _globalLabelStyle, labelStyle),
|
||||
[_globalLabelStyle],
|
||||
);
|
||||
|
||||
const globalLabelStyle = useMemo(
|
||||
() => Object.assign({}, defaultLabelStyle, _globalLabelStyle),
|
||||
[_globalLabelStyle],
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers -- calculation required
|
||||
const topOffset = datazoomHeight + datazoomPaddingBottom + 8;
|
||||
|
||||
const globalStyle: GlobalStyle = useMemo(
|
||||
() => Object.assign({}, defaultGlobalStyle, _globalStyle),
|
||||
[_globalStyle],
|
||||
);
|
||||
|
||||
const totalRowHeight = useMemo(() => {
|
||||
const rowCount = uniqWith(
|
||||
flamethreadData,
|
||||
(node0: RectNode, node1: RectNode) => node0.rowNo === node1.rowNo,
|
||||
).length;
|
||||
return rowCount * rowHeight;
|
||||
}, [flamethreadData]);
|
||||
|
||||
// Meaning of this parameter: Visual Window Height/Flame Map Height
|
||||
const yScaleRangeFactor = useMemo(() => {
|
||||
const rowCount = uniqWith(
|
||||
flamethreadData,
|
||||
(node0: RectNode, node1: RectNode) => node0.rowNo === node1.rowNo,
|
||||
).length;
|
||||
|
||||
return rowCount !== 0
|
||||
? ((viewRef.current?.getViewBox().height() || 300) - topOffset) /
|
||||
(rowCount * rowHeight)
|
||||
: 1;
|
||||
}, [flamethreadData, viewSize.height]);
|
||||
|
||||
const spec = useMemo(() => {
|
||||
const orgData = flamethreadData.map(node => {
|
||||
const rectStyle = genRectStyle(node.rectStyle);
|
||||
const labelStyle = genLabelStyle(node.labelStyle);
|
||||
return {
|
||||
...node,
|
||||
rectStyle,
|
||||
labelStyle,
|
||||
};
|
||||
});
|
||||
|
||||
const marks = [
|
||||
{
|
||||
type: GrammarMarkType.component,
|
||||
componentType: ComponentEnum.axis,
|
||||
id: 'xAxis',
|
||||
scale: 'xScale',
|
||||
axisType: 'line',
|
||||
tickCount: visibleColumnCount,
|
||||
dependency: ['viewBox'],
|
||||
encode: {
|
||||
update: (scale0, elment, params) => {
|
||||
const scale = params.xScale;
|
||||
const range = scale.range() as number[];
|
||||
const tickData = scale.tickData(visibleColumnCount);
|
||||
const dx =
|
||||
tickData.length > 1
|
||||
? // eslint-disable-next-line @typescript-eslint/no-magic-numbers -- calculation required
|
||||
(range[1] - range[0]) / (tickData.length - 1) / 2
|
||||
: 0;
|
||||
|
||||
return {
|
||||
verticalFactor: -1,
|
||||
x: params.viewBox.x1,
|
||||
y: params.viewBox.y1 + topOffset,
|
||||
start: { x: 0, y: 0 },
|
||||
end: { x: params.viewBox.width(), y: 0 },
|
||||
tick: { visible: false },
|
||||
label: {
|
||||
style: { dx: -dx },
|
||||
formatMethod: (_value: string) => {
|
||||
const value = Number(_value);
|
||||
// Specialized logic: hide 0 ticks
|
||||
if (dx > 0 && value === 0) {
|
||||
return '';
|
||||
}
|
||||
return value !== 0 && axisLabelSuffix !== undefined
|
||||
? `${value}${axisLabelSuffix}`
|
||||
: value;
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: GrammarMarkType.component,
|
||||
componentType: ComponentEnum.grid,
|
||||
tickCount: visibleColumnCount, // The types of the vgrammer library are not strictly written, but are actually usable
|
||||
scale: 'xScale',
|
||||
gridType: 'line',
|
||||
gridShape: 'line',
|
||||
dependency: ['viewBox'],
|
||||
// dependency: ["viewBox"],
|
||||
encode: {
|
||||
update: (scale, elment, params) => ({
|
||||
verticalFactor: -1,
|
||||
length: params.viewBox.height() - topOffset,
|
||||
x: params.viewBox.x1,
|
||||
x1: params.viewBox.x2,
|
||||
y: params.viewBox.y1 + topOffset,
|
||||
start: { x: 0, y: 0 },
|
||||
end: { x: params.viewBox.width(), y: 0 },
|
||||
style: { stroke: '#ccc', lineWidth: 1, lineDash: [] },
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
type: GrammarMarkType.group,
|
||||
dependency: ['viewBox'],
|
||||
encode: {
|
||||
update: (scale, elment: IElement, params) => ({
|
||||
x: params.viewBox.x1,
|
||||
y: params.viewBox.y1 + topOffset,
|
||||
width: params.viewBox.width(),
|
||||
height: params.viewBox.height() - topOffset,
|
||||
clip: true,
|
||||
}),
|
||||
},
|
||||
|
||||
marks: [
|
||||
{
|
||||
type: GrammarMarkType.rect,
|
||||
id: 'rect',
|
||||
from: { data: 'orgData' },
|
||||
groupBy: 'start',
|
||||
key: 'rowNo',
|
||||
encode: {
|
||||
update: {
|
||||
x: { scale: 'xScale', field: 'start' },
|
||||
x1: { scale: 'xScale', field: 'end' },
|
||||
y: { scale: 'yScale', field: 'rowNo', band: 0.07 },
|
||||
// height: { scale: 'yScale', band: 0.86 },
|
||||
height: rowHeight - 4,
|
||||
// height: { scale: "yScale", band: 0.7, offset: 0.15 },
|
||||
fill: (datum, element, params) =>
|
||||
datum?.rectStyle?.normal?.fill,
|
||||
innerBorder: (datum, element, params) => {
|
||||
const { stroke, lineWidth, lineDash } =
|
||||
datum.rectStyle.normal;
|
||||
return {
|
||||
stroke: lineWidth !== 0 ? stroke : null,
|
||||
lineWidth,
|
||||
lineDash,
|
||||
visible: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers -- size calculation, no processing required
|
||||
distance: lineWidth / 2,
|
||||
};
|
||||
},
|
||||
},
|
||||
hover2: {
|
||||
fill: (datum, element, params) =>
|
||||
datum?.rectStyle?.hover?.fill ??
|
||||
datum?.rectStyle?.normal?.fill,
|
||||
innerBorder: (datum, element, params) => {
|
||||
const { stroke, lineWidth, lineDash } = Object.assign(
|
||||
{},
|
||||
datum?.rectStyle?.normal,
|
||||
datum?.rectStyle?.hover,
|
||||
);
|
||||
return {
|
||||
stroke: lineWidth !== 0 ? stroke : null,
|
||||
lineWidth,
|
||||
lineDash,
|
||||
visible: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers -- size calculation, no need to define constants
|
||||
distance: lineWidth / 2,
|
||||
};
|
||||
},
|
||||
zIndex: 2,
|
||||
},
|
||||
select2: {
|
||||
fill: (datum, element, params) =>
|
||||
datum?.rectStyle?.select?.fill ??
|
||||
datum?.rectStyle?.normal?.fill,
|
||||
innerBorder: (datum, element, params) => {
|
||||
const { stroke, lineWidth, lineDash } = Object.assign(
|
||||
{},
|
||||
datum?.rectStyle?.normal,
|
||||
datum?.rectStyle?.select,
|
||||
);
|
||||
return {
|
||||
stroke: lineWidth !== 0 ? stroke : null,
|
||||
lineWidth,
|
||||
lineDash,
|
||||
visible: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers -- size calculation, no need to define constants
|
||||
distance: lineWidth / 2,
|
||||
};
|
||||
},
|
||||
zIndex: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: GrammarMarkType.component,
|
||||
componentType: ComponentEnum.label,
|
||||
target: 'rect',
|
||||
labelStyle: {
|
||||
position: globalLabelStyle.position,
|
||||
textStyle: {
|
||||
fontSize: globalLabelStyle.fontSize,
|
||||
},
|
||||
animation: false,
|
||||
overlap: {
|
||||
hideOnHit: false,
|
||||
clampForce: false,
|
||||
strategy: [{ type: 'position', position: ['top-left'] }],
|
||||
},
|
||||
},
|
||||
encode: {
|
||||
update: {
|
||||
pickable: false, // The type of vgrammer library is not strictly written
|
||||
text: labelText ?? defaultLabelText,
|
||||
fill: (datum, element, params) => datum?.labelStyle.fill,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
type: GrammarMarkType.component,
|
||||
componentType: ComponentEnum.datazoom,
|
||||
id: 'dataZoom',
|
||||
dependency: ['viewBox'],
|
||||
preview: {
|
||||
data: 'table',
|
||||
x: { scale: 'dataZoomXScale', field: ['start', 'end'] },
|
||||
y: { scale: 'dataZoomYScale', field: 'rowNo' },
|
||||
},
|
||||
encode: {
|
||||
update: (scale, elment, params) => ({
|
||||
showDetail: false,
|
||||
x: params.viewBox.x1,
|
||||
y: params.viewBox.y1,
|
||||
size: { width: params.viewBox.width(), height: datazoomHeight },
|
||||
// start: 0,
|
||||
// end: 1,
|
||||
// fill: '#ff0000',
|
||||
minSpan: 0.01,
|
||||
selectedBackgroundStyle: {
|
||||
fill: '#B4BAF6',
|
||||
},
|
||||
brushSelect: false,
|
||||
startHandlerStyle: {
|
||||
symbolType:
|
||||
'M-0.5-2.4h0.9c0.4,0,0.7,0.3,0.7,0.7v3.3c0,0.4-0.3,0.7-0.7,0.7h-0.9c-0.4,0-0.7-0.3-0.7-0.7v-3.3\nC-1.2-2-0.9-2.4-0.5-2.4z M-0.4-1.4L-0.4-1.4c0,0,0,0.1,0,0.1v2.6c0,0.1,0,0.1,0,0.1l0,0c0,0,0-0.1,0-0.1v-2.6\nC-0.4-1.4-0.4-1.4-0.4-1.4z M0.3-1.4L0.3-1.4c0,0,0,0.1,0,0.1v2.6c0,0.1,0,0.1,0,0.1l0,0c0,0,0-0.1,0-0.1v-2.6\nC0.3-1.4,0.3-1.4,0.3-1.4z;',
|
||||
fill: '#ffffff',
|
||||
scaleX: 1.2,
|
||||
scaleY: 1.2,
|
||||
stroke: '#aeb5be',
|
||||
lineWidth: 1,
|
||||
size: 20,
|
||||
},
|
||||
middleHandlerStyle: {
|
||||
visible: false,
|
||||
},
|
||||
endHandlerStyle: {
|
||||
symbolType:
|
||||
'M-0.5-2.4h0.9c0.4,0,0.7,0.3,0.7,0.7v3.3c0,0.4-0.3,0.7-0.7,0.7h-0.9c-0.4,0-0.7-0.3-0.7-0.7v-3.3\nC-1.2-2-0.9-2.4-0.5-2.4z M-0.4-1.4L-0.4-1.4c0,0,0,0.1,0,0.1v2.6c0,0.1,0,0.1,0,0.1l0,0c0,0,0-0.1,0-0.1v-2.6\nC-0.4-1.4-0.4-1.4-0.4-1.4z M0.3-1.4L0.3-1.4c0,0,0,0.1,0,0.1v2.6c0,0.1,0,0.1,0,0.1l0,0c0,0,0-0.1,0-0.1v-2.6\nC0.3-1.4,0.3-1.4,0.3-1.4z;',
|
||||
fill: '#ffffff',
|
||||
scaleX: 1.2,
|
||||
scaleY: 1.2,
|
||||
stroke: '#aeb5be',
|
||||
lineWidth: 1,
|
||||
size: 20,
|
||||
},
|
||||
startTextStyle: {
|
||||
padding: 8,
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
lineHeight: '130%',
|
||||
fill: '#606773',
|
||||
// fill: '#ff0000',
|
||||
},
|
||||
formatMethod: (value: number) => value.toFixed(datazoomDecimals),
|
||||
},
|
||||
endTextStyle: {
|
||||
padding: 8,
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
lineHeight: '130%',
|
||||
fill: '#606773',
|
||||
},
|
||||
formatMethod: (value: number) => value.toFixed(datazoomDecimals),
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
] as MarkSpec[];
|
||||
|
||||
const padding = {
|
||||
top: 3,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
};
|
||||
|
||||
if (yScaleRangeFactor < 1) {
|
||||
marks.unshift({
|
||||
type: GrammarMarkType.component,
|
||||
componentType: ComponentEnum.scrollbar,
|
||||
direction: 'vertical',
|
||||
id: 'verticalScrollbar',
|
||||
dependency: ['viewBox', 'yScale'],
|
||||
encode: {
|
||||
update: (scale, elment, params) => {
|
||||
const { yScale } = params;
|
||||
const curRangeFactor = yScale?.rangeFactor?.() ?? [
|
||||
0,
|
||||
yScaleRangeFactor,
|
||||
];
|
||||
|
||||
return {
|
||||
x: params.viewBox.x2 + scrollbarMargin,
|
||||
y: params.viewBox.y1 + topOffset,
|
||||
height: params.viewBox.height() - topOffset,
|
||||
range: [curRangeFactor[1], curRangeFactor[0]],
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
padding.right = 22;
|
||||
}
|
||||
|
||||
const spec0: ViewSpec = {
|
||||
padding,
|
||||
background: globalStyle.background,
|
||||
|
||||
data: [
|
||||
{
|
||||
id: 'orgData',
|
||||
values: orgData,
|
||||
},
|
||||
{
|
||||
id: 'markData',
|
||||
source: 'orgData',
|
||||
},
|
||||
],
|
||||
|
||||
scales: [
|
||||
{
|
||||
id: 'xScale',
|
||||
type: 'linear' as GrammarScaleType,
|
||||
domain: { data: 'markData', field: ['start', 'end'] },
|
||||
dependency: ['viewBox'],
|
||||
range: (scale, params) => [0, params.viewBox.width()],
|
||||
nice: true,
|
||||
},
|
||||
{
|
||||
id: 'yScale',
|
||||
type: 'band',
|
||||
domain: { data: 'markData', field: 'rowNo' },
|
||||
dependency: ['viewBox'],
|
||||
range: (scale, params) => {
|
||||
const vHeight = params.viewBox.height() - topOffset;
|
||||
const height = yScaleRangeFactor <= 1 ? vHeight : totalRowHeight;
|
||||
|
||||
return [0, height];
|
||||
},
|
||||
padding: 0,
|
||||
round: false,
|
||||
},
|
||||
{
|
||||
id: 'dataZoomXScale',
|
||||
type: 'linear',
|
||||
domain: { data: 'orgData', field: ['start', 'end'] },
|
||||
dependency: ['viewBox'],
|
||||
range: (scale, params) => [0, params.viewBox.width()],
|
||||
},
|
||||
{
|
||||
id: 'dataZoomYScale',
|
||||
type: 'band',
|
||||
domain: { data: 'orgData', field: 'rowNo' },
|
||||
dependency: ['viewBox'],
|
||||
range: (scale, params) => [params.viewBox.height(), 0],
|
||||
padding: 0.05,
|
||||
round: true,
|
||||
},
|
||||
],
|
||||
|
||||
marks,
|
||||
};
|
||||
return spec0;
|
||||
}, [
|
||||
flamethreadData,
|
||||
visibleColumnCount,
|
||||
globalLabelStyle.position,
|
||||
globalLabelStyle.fontSize,
|
||||
labelText,
|
||||
yScaleRangeFactor,
|
||||
totalRowHeight,
|
||||
globalStyle.padding,
|
||||
globalStyle.background,
|
||||
genRectStyle,
|
||||
genLabelStyle,
|
||||
axisLabelSuffix,
|
||||
]);
|
||||
|
||||
const updateSelectedKey = useCallback(
|
||||
(view: IView) => {
|
||||
const rectElm = view?.getMarkById('rect');
|
||||
const elements = rectElm?.elements;
|
||||
elements?.forEach(element => {
|
||||
element?.removeState('select2');
|
||||
});
|
||||
elements
|
||||
?.filter(element => {
|
||||
const datum = element.getDatum();
|
||||
return datum.key === selectedKey;
|
||||
})[0]
|
||||
?.addState('select2');
|
||||
},
|
||||
[selectedKey],
|
||||
);
|
||||
|
||||
// Create/update view
|
||||
useLayoutEffect(() => {
|
||||
const initializeYScale = (view: IView) => {
|
||||
const yScale = view?.getScaleById('yScale');
|
||||
yScale?.setRangeFactor([0, yScaleRangeFactor]);
|
||||
yScale?.commit();
|
||||
};
|
||||
|
||||
const initializeScale = (view: IView) => {
|
||||
initializeYScale(view);
|
||||
};
|
||||
|
||||
const registerEvent = (view: IView) => {
|
||||
const rectElm = view?.getMarkById('rect');
|
||||
// Rect click event
|
||||
rectElm?.addEventListener('click', ((event, element) => {
|
||||
onClick?.(event, element);
|
||||
}) as InteractionEventHandler);
|
||||
|
||||
// Rect hover highlight
|
||||
view?.interaction('element-highlight', {
|
||||
selector: 'rect',
|
||||
highlightState: 'hover2',
|
||||
});
|
||||
|
||||
view?.interaction('element-highlight', {
|
||||
trigger: 'click',
|
||||
// triggerOff: "view:click",
|
||||
triggerOff: 'swipe',
|
||||
selector: 'rect',
|
||||
highlightState: 'select2',
|
||||
});
|
||||
|
||||
if (!disableViewScroll) {
|
||||
view.interaction('view-scroll', {
|
||||
scaleY: 'yScale',
|
||||
});
|
||||
}
|
||||
|
||||
// Rect hover tooltip
|
||||
if (tooltip) {
|
||||
view?.interaction('tooltip', {
|
||||
selector: 'rect',
|
||||
...tooltip,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (containerRef.current && !viewRef.current) {
|
||||
const view = new View({
|
||||
autoFit: enableAutoFit,
|
||||
container: containerRef.current,
|
||||
});
|
||||
|
||||
view?.on('change', (...args) => {
|
||||
const event = args[0];
|
||||
const { start, end } = event.detail;
|
||||
const xScale = view.getScaleById('xScale');
|
||||
xScale?.setRangeFactor([start, end]);
|
||||
xScale?.commit();
|
||||
view?.run();
|
||||
});
|
||||
|
||||
view?.on('scrollDrag', e => {
|
||||
const direction = e?.target?.attribute?.direction;
|
||||
if (direction === 'vertical') {
|
||||
const range = e.detail.value;
|
||||
const yScale = view.getScaleById('yScale');
|
||||
yScale?.setRangeFactor(range);
|
||||
yScale?.commit();
|
||||
view.run();
|
||||
}
|
||||
});
|
||||
|
||||
view.parseSpec(spec);
|
||||
initializeScale(view);
|
||||
registerEvent(view);
|
||||
view.run();
|
||||
updateSelectedKey(view);
|
||||
|
||||
view.run();
|
||||
|
||||
viewRef.current = view;
|
||||
} else if (viewRef.current) {
|
||||
const view = viewRef.current;
|
||||
|
||||
view.updateSpec(spec);
|
||||
initializeScale(view);
|
||||
registerEvent(view);
|
||||
view.run({ reuse: false });
|
||||
updateSelectedKey(view);
|
||||
|
||||
view.run();
|
||||
}
|
||||
}, [
|
||||
spec,
|
||||
tooltip,
|
||||
yScaleRangeFactor,
|
||||
onClick,
|
||||
visibleColumnCount,
|
||||
flamethreadData,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (viewRef.current) {
|
||||
updateSelectedKey(viewRef.current);
|
||||
}
|
||||
}, [selectedKey]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (viewRef.current) {
|
||||
viewRef.current.release();
|
||||
}
|
||||
viewRef.current = null;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const resizeObserver = new ResizeObserver(params => {
|
||||
const width = params[0].target.clientWidth;
|
||||
const height = params[0].target.clientHeight;
|
||||
if (width !== undefined && height !== undefined && viewRef.current) {
|
||||
viewRef.current.resize(width, height);
|
||||
setViewSize({
|
||||
width,
|
||||
height,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (containerRef.current) {
|
||||
resizeObserver.observe(containerRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
...pick(globalStyle, ['width', 'height']),
|
||||
}}
|
||||
ref={containerRef}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Flamethread;
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type CSSProperties } from 'react';
|
||||
|
||||
import {
|
||||
type InteractionEventHandler,
|
||||
type TooltipSpec,
|
||||
type ViewSpec,
|
||||
type IElement,
|
||||
} from '@visactor/vgrammar';
|
||||
|
||||
export type { IElement, InteractionEventHandler, TooltipSpec };
|
||||
|
||||
export interface RectStyleAttrs {
|
||||
fill?: string;
|
||||
stroke?: string;
|
||||
lineWidth?: number;
|
||||
lineDash?: number[];
|
||||
}
|
||||
|
||||
export interface RectStyle {
|
||||
normal?: RectStyleAttrs;
|
||||
hover?: RectStyleAttrs;
|
||||
select?: RectStyleAttrs;
|
||||
}
|
||||
|
||||
export interface LabelStyle {
|
||||
position?: string;
|
||||
fontSize?: number;
|
||||
fill?: string;
|
||||
}
|
||||
|
||||
export interface RectNode {
|
||||
key: string;
|
||||
rowNo: number;
|
||||
start: number;
|
||||
end: number;
|
||||
rectStyle?: RectStyle;
|
||||
labelStyle?: Pick<LabelStyle, 'fill'>;
|
||||
// Other fields will be passed through
|
||||
extra?: unknown;
|
||||
}
|
||||
|
||||
export type Tooltip = Pick<TooltipSpec, 'title' | 'content'>;
|
||||
|
||||
export type GlobalStyle = Pick<CSSProperties, 'width' | 'height'> &
|
||||
Pick<ViewSpec, 'padding' | 'background'>;
|
||||
|
||||
export type LabelText = (
|
||||
datum: RectNode,
|
||||
element: IElement,
|
||||
params: unknown,
|
||||
) => string;
|
||||
|
||||
export interface FlamethreadProps {
|
||||
flamethreadData: RectNode[];
|
||||
rectStyle?: RectStyle;
|
||||
labelStyle?: LabelStyle;
|
||||
labelText?: LabelText;
|
||||
tooltip?: Tooltip;
|
||||
globalStyle?: GlobalStyle;
|
||||
rowHeight?: number;
|
||||
visibleColumnCount?: number;
|
||||
// valuePerColumn?: number;
|
||||
datazoomDecimals?: number;
|
||||
axisLabelSuffix?: string;
|
||||
selectedKey?: string;
|
||||
disableViewScroll?: boolean;
|
||||
enableAutoFit?: boolean;
|
||||
onClick?: InteractionEventHandler;
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type EdgeProps } from 'reactflow';
|
||||
|
||||
import { SpanCategory } from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import { CommonNode } from '../custom-nodes';
|
||||
import { CommonEdge } from '../custom-edges';
|
||||
import { NodeEdgeCategory } from '.';
|
||||
|
||||
export const CUSTOM_NODES = {
|
||||
[SpanCategory.Unknown]: CommonNode,
|
||||
[SpanCategory.Start]: CommonNode,
|
||||
[SpanCategory.Agent]: CommonNode,
|
||||
[SpanCategory.LLMCall]: CommonNode,
|
||||
[SpanCategory.Workflow]: CommonNode,
|
||||
[SpanCategory.WorkflowStart]: CommonNode,
|
||||
[SpanCategory.WorkflowEnd]: CommonNode,
|
||||
[SpanCategory.Plugin]: CommonNode,
|
||||
[SpanCategory.Knowledge]: CommonNode,
|
||||
[SpanCategory.Code]: CommonNode,
|
||||
[SpanCategory.Condition]: CommonNode,
|
||||
[SpanCategory.Card]: CommonNode,
|
||||
[SpanCategory.Message]: CommonNode,
|
||||
[SpanCategory.Loop]: CommonNode,
|
||||
[SpanCategory.LongTermMemory]: CommonNode,
|
||||
};
|
||||
|
||||
export const CUSTOM_EDGES: Record<NodeEdgeCategory, React.FC<EdgeProps>> = {
|
||||
[NodeEdgeCategory.Common]: CommonEdge,
|
||||
};
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
IconCozDatabaseFill,
|
||||
IconCozLongTermMemory,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
IconSpanAgent,
|
||||
IconSpanBMBatch,
|
||||
IconSpanBMConnector,
|
||||
IconSpanBMParallel,
|
||||
IconSpanCard,
|
||||
IconSpanCode,
|
||||
IconSpanCondition,
|
||||
IconSpanKnowledge,
|
||||
IconSpanLLMCall,
|
||||
IconSpanMessage,
|
||||
IconSpanPluginTool,
|
||||
IconSpanUnknown,
|
||||
IconSpanHook,
|
||||
IconSpanVar,
|
||||
IconSpanWorkflow,
|
||||
IconSpanWorkflowEnd,
|
||||
IconSpanWorkflowStart,
|
||||
} from '@coze-arch/bot-icons';
|
||||
import { SpanCategory, SpanType } from '@coze-arch/bot-api/ob_query_api';
|
||||
import { ResourceType } from '@coze-arch/bot-api/dp_manage_api';
|
||||
|
||||
import {
|
||||
type TopologicalStatusData,
|
||||
type TopologicalLayoutBizData,
|
||||
type TopologicalLayoutCommonData,
|
||||
} from '../typing';
|
||||
|
||||
export const TOPOLOGY_COMMON_NODE_TEXT_FONT = '14px SF Pro Display';
|
||||
export const TOPOLOGY_COMMON_NODE_TEXT_DEFAULT_WIDTH = 100;
|
||||
export const TOPOLOGY_COMMON_NODE_TEXT_HEIGHT = 24;
|
||||
export const TOPOLOGY_COMMON_NODE_TEXT_MAX_WIDTH = 200;
|
||||
export const TOPOLOGY_COMMON_NODE_TEXT_ADDITIONAL_WIDTH = 62;
|
||||
export const TOPOLOGY_COMMON_EDGE_OFFSET_WIDTH = 12;
|
||||
export const TOPOLOGY_DEFAULT_NODE_ICON = <IconSpanUnknown />;
|
||||
|
||||
export enum NodeEdgeCategory {
|
||||
Common = 'common',
|
||||
}
|
||||
|
||||
export enum TopologyLayoutDirection {
|
||||
TB = 'TB',
|
||||
LR = 'LR',
|
||||
}
|
||||
|
||||
export const TOPOLOGY_LAYOUT_RECORD: Partial<
|
||||
Record<SpanType, TopologyLayoutDirection>
|
||||
> = {
|
||||
[SpanType.InvokeAgent]: TopologyLayoutDirection.TB,
|
||||
[SpanType.UserInput]: TopologyLayoutDirection.TB,
|
||||
[SpanType.UserInputV2]: TopologyLayoutDirection.TB,
|
||||
[SpanType.Workflow]: TopologyLayoutDirection.LR,
|
||||
};
|
||||
|
||||
export const RESOURCE_TYPE_RECORD: Partial<Record<SpanType, ResourceType>> = {
|
||||
[SpanType.InvokeAgent]: ResourceType.Bot,
|
||||
[SpanType.UserInput]: ResourceType.Bot,
|
||||
[SpanType.UserInputV2]: ResourceType.Bot,
|
||||
[SpanType.Workflow]: ResourceType.Workflow,
|
||||
};
|
||||
|
||||
export enum NodeLayoutCategory {
|
||||
Common,
|
||||
}
|
||||
|
||||
export const TOPOLOGY_LAYOUT_COMMON_MAP: Record<
|
||||
NodeLayoutCategory,
|
||||
TopologicalLayoutCommonData
|
||||
> = {
|
||||
[NodeLayoutCategory.Common]: {
|
||||
height: TOPOLOGY_COMMON_NODE_TEXT_HEIGHT,
|
||||
},
|
||||
};
|
||||
|
||||
export const TOPOLOGY_LAYOUT_BIZ_MAP: Record<
|
||||
SpanCategory,
|
||||
TopologicalLayoutBizData
|
||||
> = {
|
||||
[SpanCategory.Unknown]: {
|
||||
icon: <IconSpanUnknown />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Start]: {
|
||||
icon: <IconSpanWorkflowStart />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Agent]: {
|
||||
icon: <IconSpanAgent />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.LLMCall]: {
|
||||
icon: <IconSpanLLMCall />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Workflow]: {
|
||||
icon: <IconSpanWorkflow />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
|
||||
[SpanCategory.WorkflowStart]: {
|
||||
icon: <IconSpanWorkflowStart />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.WorkflowEnd]: {
|
||||
icon: <IconSpanWorkflowEnd />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
|
||||
[SpanCategory.Plugin]: {
|
||||
icon: <IconSpanPluginTool />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
|
||||
[SpanCategory.Knowledge]: {
|
||||
icon: <IconSpanKnowledge />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
|
||||
[SpanCategory.Code]: {
|
||||
icon: <IconSpanCode />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Condition]: {
|
||||
icon: <IconSpanCondition />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Card]: {
|
||||
icon: <IconSpanCard />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Message]: {
|
||||
icon: <IconSpanMessage />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Variable]: {
|
||||
icon: <IconSpanVar />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Database]: {
|
||||
icon: <IconCozDatabaseFill />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.LongTermMemory]: {
|
||||
icon: <IconCozLongTermMemory />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Hook]: {
|
||||
icon: <IconSpanHook />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Batch]: {
|
||||
icon: <IconSpanBMBatch />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Loop]: {
|
||||
icon: <IconSpanBMBatch />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Parallel]: {
|
||||
icon: <IconSpanBMParallel />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Script]: {
|
||||
icon: <IconSpanCode />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.CallFlow]: {
|
||||
icon: <IconSpanWorkflow />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Connector]: {
|
||||
icon: <IconSpanBMConnector />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
};
|
||||
|
||||
export enum TopologyEdgeStatus {
|
||||
STATIC,
|
||||
DYNAMIC,
|
||||
ERROR,
|
||||
}
|
||||
|
||||
export const TOPOLOGY_EDGE_STATUS_MAP: Record<
|
||||
TopologyEdgeStatus,
|
||||
TopologicalStatusData
|
||||
> = {
|
||||
[TopologyEdgeStatus.STATIC]: {
|
||||
edgeColor: '#C8C8CA',
|
||||
nodeClassName: 'common-node-container_static',
|
||||
},
|
||||
[TopologyEdgeStatus.DYNAMIC]: {
|
||||
edgeColor: '#3EC254',
|
||||
nodeClassName: 'common-node-container_dynamic',
|
||||
},
|
||||
[TopologyEdgeStatus.ERROR]: {
|
||||
edgeColor: '#FF441E',
|
||||
nodeClassName: 'common-node-container_error',
|
||||
},
|
||||
};
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
.batch-edge-info-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
padding: 2px 4px;
|
||||
|
||||
font-size: 14px;
|
||||
color: rgb(29 28 35 / 80%);
|
||||
|
||||
background: #F7F7FA;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.batch-edge-info-container_error {
|
||||
font-weight: 600;
|
||||
color: #FF441E;
|
||||
background: #FFF3EE;
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type EdgeProps,
|
||||
BaseEdge,
|
||||
getBezierPath,
|
||||
Position,
|
||||
EdgeLabelRenderer,
|
||||
} from 'reactflow';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Tooltip } from '@coze-arch/bot-semi';
|
||||
import { SpanStatus } from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import { getTopologyItemStatus } from '../util';
|
||||
import {
|
||||
type TopologicalBatchNodeExecutionInfo,
|
||||
type EdgeData,
|
||||
} from '../typing';
|
||||
import { TOPOLOGY_EDGE_STATUS_MAP } from '../constant';
|
||||
import { type SpanNode } from '../../../utils/cspan-graph';
|
||||
import { checkIsBatchBasicCSpan } from '../../../utils/cspan';
|
||||
import { type CSPanBatch } from '../../../typings/cspan';
|
||||
|
||||
import s from './common.module.less';
|
||||
|
||||
const isVerticalEdge = (position: Position) =>
|
||||
position === Position.Top || position === Position.Bottom;
|
||||
|
||||
const getBatchNodeExecutionInfo = (
|
||||
spanNode?: SpanNode,
|
||||
): TopologicalBatchNodeExecutionInfo => {
|
||||
const batchNodeExecutionInfo: TopologicalBatchNodeExecutionInfo = {
|
||||
isBatch: false,
|
||||
isError: false,
|
||||
errorNumber: 0,
|
||||
totalNumber: 0,
|
||||
};
|
||||
if (!spanNode || !checkIsBatchBasicCSpan(spanNode)) {
|
||||
return batchNodeExecutionInfo;
|
||||
}
|
||||
const { spans, status: batchNodeStatus } = spanNode as CSPanBatch;
|
||||
|
||||
batchNodeExecutionInfo.isBatch = true;
|
||||
batchNodeExecutionInfo.isError = batchNodeStatus === SpanStatus.Error;
|
||||
|
||||
spans.forEach(span => {
|
||||
const { status } = span;
|
||||
batchNodeExecutionInfo.totalNumber++;
|
||||
if (status === SpanStatus.Error) {
|
||||
batchNodeExecutionInfo.errorNumber++;
|
||||
}
|
||||
});
|
||||
|
||||
return batchNodeExecutionInfo;
|
||||
};
|
||||
|
||||
interface BatchEdgeInfoProps {
|
||||
batchNodeExecutionInfo: TopologicalBatchNodeExecutionInfo;
|
||||
}
|
||||
|
||||
const BatchEdgeInfo = (props: BatchEdgeInfoProps) => {
|
||||
const { batchNodeExecutionInfo } = props;
|
||||
const { isError, totalNumber, errorNumber } = batchNodeExecutionInfo;
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
s['batch-edge-info-container'],
|
||||
isError && s['batch-edge-info-container_error'],
|
||||
)}
|
||||
>
|
||||
{!isError || totalNumber === errorNumber ? (
|
||||
<>{totalNumber}</>
|
||||
) : (
|
||||
<Tooltip
|
||||
content={I18n.t('analytic_query_detail_topology_tooltip', {
|
||||
errorCount: errorNumber,
|
||||
callCount: totalNumber,
|
||||
})}
|
||||
>
|
||||
{errorNumber}
|
||||
<span style={{ color: '#1D1C23' }}>
|
||||
<span style={{ margin: '0 3px' }}>/</span>
|
||||
{totalNumber}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CommonEdge = (props: EdgeProps<EdgeData>) => {
|
||||
const {
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
markerEnd,
|
||||
data,
|
||||
} = props;
|
||||
|
||||
const batchNodeExecutionInfo = useMemo(
|
||||
() => getBatchNodeExecutionInfo(data?.tailDynamicSpanNode),
|
||||
[data?.tailDynamicSpanNode],
|
||||
);
|
||||
|
||||
const topologyEdgeStatus = getTopologyItemStatus(data?.tailDynamicSpanNode);
|
||||
|
||||
// When the vertical type line segment is laid out, the node position is used for positioning, so that the start and end points of the line segment are positioned at the beginning of the node
|
||||
const adaptedSourceX = isVerticalEdge(sourcePosition)
|
||||
? data?.layoutInfo?.customSourceX ?? sourceX
|
||||
: sourceX;
|
||||
const adaptedTargetX = isVerticalEdge(targetPosition)
|
||||
? data?.layoutInfo?.customTargetX ?? targetX
|
||||
: targetX;
|
||||
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX: adaptedSourceX,
|
||||
sourceY,
|
||||
targetX: adaptedTargetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseEdge
|
||||
id={id}
|
||||
path={edgePath}
|
||||
markerEnd={markerEnd}
|
||||
style={{
|
||||
strokeWidth: 2,
|
||||
stroke: TOPOLOGY_EDGE_STATUS_MAP[topologyEdgeStatus].edgeColor,
|
||||
}}
|
||||
/>
|
||||
{batchNodeExecutionInfo.isBatch && (
|
||||
<EdgeLabelRenderer>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
|
||||
pointerEvents: 'all',
|
||||
}}
|
||||
>
|
||||
<BatchEdgeInfo batchNodeExecutionInfo={batchNodeExecutionInfo} />
|
||||
</div>
|
||||
</EdgeLabelRenderer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { CommonEdge } from './common';
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
.common-node {
|
||||
width: auto;
|
||||
height: 24px;
|
||||
|
||||
.common-node-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: 24px;
|
||||
padding: 2px 4px;
|
||||
|
||||
|
||||
font-size: 14px;
|
||||
color: rgb(29 28 35 / 80%);
|
||||
|
||||
border-radius: 4px;
|
||||
|
||||
.common-node-container-text {
|
||||
max-width: 200px;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.common-node-container_static {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.common-node-container_dynamic {
|
||||
background: #EDF9EE;
|
||||
}
|
||||
|
||||
.common-node-container_error {
|
||||
font-weight: 600;
|
||||
color: #FF441E;
|
||||
background: #FFF3EE;
|
||||
}
|
||||
|
||||
|
||||
:global(.react-flow__handle) {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Handle,
|
||||
type NodeProps,
|
||||
Position,
|
||||
useUpdateNodeInternals,
|
||||
} from 'reactflow';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useUpdateEffect } from 'ahooks';
|
||||
import { Typography } from '@coze-arch/bot-semi';
|
||||
import { SpanCategory } from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import { getTopologyItemStatus } from '../util';
|
||||
import { type NodeData } from '../typing';
|
||||
import {
|
||||
TOPOLOGY_EDGE_STATUS_MAP,
|
||||
TopologyEdgeStatus,
|
||||
TopologyLayoutDirection,
|
||||
} from '../constant';
|
||||
|
||||
import s from './common.module.less';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const CommonNode = (props: NodeProps<NodeData>) => {
|
||||
const {
|
||||
id,
|
||||
type,
|
||||
data: { name, icon, layoutDirection, dynamicSpanNode },
|
||||
} = props;
|
||||
|
||||
// Specialization logic: There are no workflow_start nodes in dynamic tracing, workflow_start nodes in topo are highlighted by default
|
||||
const topologyNodeStatus =
|
||||
Number(type) === SpanCategory.WorkflowStart
|
||||
? TopologyEdgeStatus.DYNAMIC
|
||||
: getTopologyItemStatus(dynamicSpanNode);
|
||||
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
|
||||
useUpdateEffect(() => {
|
||||
updateNodeInternals(id);
|
||||
}, [layoutDirection]);
|
||||
|
||||
return (
|
||||
<div className={s['common-node']}>
|
||||
<Handle
|
||||
type="target"
|
||||
position={
|
||||
layoutDirection === TopologyLayoutDirection.LR
|
||||
? Position.Left
|
||||
: Position.Top
|
||||
}
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={
|
||||
layoutDirection === TopologyLayoutDirection.LR
|
||||
? Position.Right
|
||||
: Position.Bottom
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
s['common-node-container'],
|
||||
s[TOPOLOGY_EDGE_STATUS_MAP[topologyNodeStatus].nodeClassName],
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
<Text
|
||||
className={s['common-node-container-text']}
|
||||
ellipsis={{ showTooltip: true }}
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { CommonNode } from './common';
|
||||
+333
@@ -0,0 +1,333 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useReactFlow, useStoreApi } from 'reactflow';
|
||||
import { type RefObject, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useAsyncEffect, useSize, useThrottle } from 'ahooks';
|
||||
import {
|
||||
Env,
|
||||
type ResourceType,
|
||||
type GetTopoInfoReq,
|
||||
type TopoInfo,
|
||||
TopoType,
|
||||
} from '@coze-arch/bot-api/dp_manage_api';
|
||||
import { dpManageApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { buildTraceTree } from '../../utils/cspan-graph';
|
||||
import { DataSourceTypeEnum } from '../../typings/graph';
|
||||
import {
|
||||
type CSpanAttrUserInput,
|
||||
type CSpanAttrInvokeAgent,
|
||||
type CSpanAttrWorkflow,
|
||||
} from '../../typings/cspan';
|
||||
import {
|
||||
completeDynamicTopologyInfo,
|
||||
extractOriginDynamicNodeMap,
|
||||
filterObjectByKeys,
|
||||
findNearestTopologyRootSpanNode,
|
||||
generateStaticTopologyDataMapKey,
|
||||
generateTopologyMetaInfo,
|
||||
getAllUpstreamTopologyNodeIds,
|
||||
getLayoutedMeta,
|
||||
getNodeResourceId,
|
||||
getTopologyAgentRootType,
|
||||
} from './util';
|
||||
import {
|
||||
type DynamicNodeMap,
|
||||
type DynamicTopologyData,
|
||||
type StaticTopologyDataCache,
|
||||
type UseGenerateTopologyHookData,
|
||||
type TopologicalData,
|
||||
type ProcessedGetTopoInfoReq,
|
||||
} from './typing';
|
||||
import {
|
||||
RESOURCE_TYPE_RECORD,
|
||||
TOPOLOGY_LAYOUT_RECORD,
|
||||
type TopologyLayoutDirection,
|
||||
} from './constant';
|
||||
|
||||
export const useLayoutTopology = (
|
||||
topologicalData: TopologicalData | undefined,
|
||||
): [RefObject<HTMLDivElement>] => {
|
||||
const topologyFlowDomRef = useRef<HTMLDivElement>(null);
|
||||
const topologyFlowBoxSize = useSize(topologyFlowDomRef);
|
||||
const throttledTopologyFlowBoxSize = useThrottle(topologyFlowBoxSize);
|
||||
const { setCenter } = useReactFlow();
|
||||
const store = useStoreApi();
|
||||
|
||||
useEffect(() => {
|
||||
if (topologicalData && throttledTopologyFlowBoxSize) {
|
||||
const { nodeInternals } = store.getState();
|
||||
const nodes = Array.from(nodeInternals).map(([, node]) => node);
|
||||
if (nodes.length > 0) {
|
||||
const node = nodes[0];
|
||||
const zoom = 0.7;
|
||||
const { height, width } = throttledTopologyFlowBoxSize;
|
||||
const x = node.position.x + (node.width || 0) / 2 + (width / 2) * zoom;
|
||||
const y =
|
||||
node.position.y + (node.height || 0) / 2 + (height / 2) * zoom;
|
||||
setCenter(x, y, { zoom, duration: 1000 });
|
||||
}
|
||||
}
|
||||
}, [topologicalData, setCenter, store, throttledTopologyFlowBoxSize]);
|
||||
|
||||
return [topologyFlowDomRef];
|
||||
};
|
||||
|
||||
const notShowTopo = (topoInfo: TopoInfo | undefined): topoInfo is undefined => {
|
||||
if (!topoInfo) {
|
||||
return true;
|
||||
}
|
||||
if (!topoInfo.nodes) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!topoInfo.topo_type || topoInfo.topo_type === TopoType.AgentFlow) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const useGenerateTopology = (
|
||||
data: UseGenerateTopologyHookData,
|
||||
): [boolean, TopologicalData | undefined] => {
|
||||
const { botId, entityId, spaceId, dataSource, selectedSpanId } = data;
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [topologicalData, setTopologicalData] = useState<TopologicalData>();
|
||||
|
||||
// Static topo interface original data source and computing data cache
|
||||
const staticTopologyDataRef = useRef<Record<string, StaticTopologyDataCache>>(
|
||||
{},
|
||||
);
|
||||
// The nearest upstream of a span can be queried to the span node cache of topo
|
||||
const nearestTopologyRootSpanMapRef = useRef<DynamicNodeMap>({});
|
||||
|
||||
const resetStatus = () => {
|
||||
setTopologicalData(undefined);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const fetchStaticTopologyData = async (req: GetTopoInfoReq) => {
|
||||
const resp = await dpManageApi.GetTopoInfo({
|
||||
...req,
|
||||
});
|
||||
return resp.data;
|
||||
};
|
||||
|
||||
const getOriginDynamicData = (): DynamicTopologyData | undefined => {
|
||||
const { type, spanData = [] } = dataSource;
|
||||
if (type === DataSourceTypeEnum.SpanData) {
|
||||
const traceTree = buildTraceTree(spanData, false);
|
||||
return extractOriginDynamicNodeMap(traceTree, botId || entityId || '');
|
||||
} else {
|
||||
// The TraceId type is not yet implemented
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
try {
|
||||
if (!selectedSpanId) {
|
||||
resetStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
/**
|
||||
* Step 1
|
||||
* Get dynamic tracing tree data and find the currently selected node
|
||||
*/
|
||||
const originDynamicData = getOriginDynamicData();
|
||||
const currentSelectedSpanNode =
|
||||
originDynamicData?.originDynamicNodeMap?.[selectedSpanId];
|
||||
|
||||
if (!originDynamicData || !currentSelectedSpanNode) {
|
||||
resetStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
const getNearestTopologyRootSpanNode = () => {
|
||||
// hit cache
|
||||
if (nearestTopologyRootSpanMapRef.current[selectedSpanId]) {
|
||||
return nearestTopologyRootSpanMapRef.current[selectedSpanId];
|
||||
}
|
||||
const node = findNearestTopologyRootSpanNode(currentSelectedSpanNode);
|
||||
if (!node) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
nearestTopologyRootSpanMapRef.current[selectedSpanId] = node;
|
||||
return node;
|
||||
};
|
||||
|
||||
/**
|
||||
* Step 2
|
||||
* Find the nearest drawable topo node starting from the current node and request static raw topo data
|
||||
*/
|
||||
const nearestTopologyRootSpanNode = getNearestTopologyRootSpanNode() as
|
||||
| CSpanAttrInvokeAgent
|
||||
| CSpanAttrWorkflow
|
||||
| CSpanAttrUserInput
|
||||
| undefined;
|
||||
|
||||
if (!nearestTopologyRootSpanNode) {
|
||||
resetStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
const { type } = nearestTopologyRootSpanNode;
|
||||
|
||||
// Currently supports InvokeAgent and Workflow types, taking the id and version of bot and workflow respectively
|
||||
const getStaticTopologyMetaData = (): Partial<
|
||||
Pick<GetTopoInfoReq, 'resource_id' | 'version'>
|
||||
> => {
|
||||
if (
|
||||
getTopologyAgentRootType().includes(nearestTopologyRootSpanNode.type)
|
||||
) {
|
||||
return {
|
||||
resource_id: botId || entityId || '',
|
||||
version: nearestTopologyRootSpanNode.extra?.bot_version,
|
||||
};
|
||||
} else {
|
||||
const typedNearestTopologyRootSpanNode =
|
||||
nearestTopologyRootSpanNode as CSpanAttrWorkflow;
|
||||
return {
|
||||
resource_id: typedNearestTopologyRootSpanNode.extra?.workflow_id,
|
||||
version: typedNearestTopologyRootSpanNode.extra?.workflow_version,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const { resource_id, version } = getStaticTopologyMetaData();
|
||||
|
||||
if (!resource_id || !version) {
|
||||
resetStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
const processedGetTopoInfoReq: ProcessedGetTopoInfoReq = {
|
||||
space_id: spaceId,
|
||||
env: Env.Online,
|
||||
resource_type: RESOURCE_TYPE_RECORD[type] as ResourceType,
|
||||
resource_id: resource_id ?? '',
|
||||
version: version ?? '',
|
||||
};
|
||||
const staticTopologyDataMapKey = generateStaticTopologyDataMapKey(
|
||||
processedGetTopoInfoReq,
|
||||
);
|
||||
|
||||
const staticTopologyDataCache = staticTopologyDataRef.current[
|
||||
staticTopologyDataMapKey
|
||||
] as StaticTopologyDataCache | undefined;
|
||||
|
||||
const topoInfo =
|
||||
// Read from cache first
|
||||
staticTopologyDataCache?.topoInfoMap ??
|
||||
(await fetchStaticTopologyData(processedGetTopoInfoReq));
|
||||
|
||||
if (notShowTopo(topoInfo)) {
|
||||
resetStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 3
|
||||
* Filter out the nodes in the current static topo node that need to display the dynamic call link
|
||||
*/
|
||||
const topoMetaInfo =
|
||||
// Read from cache first
|
||||
staticTopologyDataCache?.topoMetaInfo ??
|
||||
generateTopologyMetaInfo(topoInfo);
|
||||
|
||||
const upstreamNodeMap = staticTopologyDataCache?.upstreamNodeMap ?? {};
|
||||
|
||||
// Determine whether the current node itself has topo information (that is, the root node of the topo that needs to be displayed at present)
|
||||
const isSelectedNodeTopologyRoot =
|
||||
currentSelectedSpanNode.id === nearestTopologyRootSpanNode.id;
|
||||
|
||||
const currentSelectedSpanNodeTopoNodeId =
|
||||
topoMetaInfo.nodeIdMap[
|
||||
getNodeResourceId(currentSelectedSpanNode, botId || entityId || '')
|
||||
];
|
||||
|
||||
// If the current node is the topo root node, then all dynamic node information is displayed.
|
||||
// Otherwise, filter out all upstream nodes of the current node in the static topo and display only those upstream nodes
|
||||
const currentDynamicNodeMap = isSelectedNodeTopologyRoot
|
||||
? originDynamicData.dynamicNodeMap
|
||||
: filterObjectByKeys(
|
||||
originDynamicData.dynamicNodeMap,
|
||||
[
|
||||
currentSelectedSpanNodeTopoNodeId,
|
||||
...getAllUpstreamTopologyNodeIds(
|
||||
currentSelectedSpanNodeTopoNodeId,
|
||||
topoMetaInfo.topoGraph,
|
||||
upstreamNodeMap,
|
||||
),
|
||||
].map(nodeId => topoMetaInfo.resourceIdMap[nodeId]),
|
||||
);
|
||||
|
||||
const layoutDirection = TOPOLOGY_LAYOUT_RECORD[
|
||||
type
|
||||
] as TopologyLayoutDirection;
|
||||
|
||||
/**
|
||||
* Step 4
|
||||
* Complement dynamic node information & layout information to static topo
|
||||
*/
|
||||
const originalTopologicalData = completeDynamicTopologyInfo(
|
||||
topoInfo,
|
||||
currentDynamicNodeMap,
|
||||
layoutDirection,
|
||||
);
|
||||
|
||||
const layoutTopologicalData = getLayoutedMeta(
|
||||
originalTopologicalData,
|
||||
layoutDirection,
|
||||
);
|
||||
|
||||
// cache
|
||||
staticTopologyDataRef.current[staticTopologyDataMapKey] = {
|
||||
topoInfoMap: topoInfo,
|
||||
topoMetaInfo,
|
||||
upstreamNodeMap,
|
||||
};
|
||||
|
||||
setTopologicalData(layoutTopologicalData);
|
||||
// eslint-disable-next-line @coze-arch/use-error-in-catch
|
||||
} catch (e) {
|
||||
resetStatus();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [botId, entityId, spaceId, dataSource, selectedSpanId]);
|
||||
|
||||
useEffect(
|
||||
// Clear cache on destruction
|
||||
() => () => {
|
||||
staticTopologyDataRef.current = {
|
||||
topoInfoMap: {},
|
||||
upstreamNodeMap: {},
|
||||
};
|
||||
nearestTopologyRootSpanMapRef.current = {};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return [loading, topologicalData];
|
||||
};
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
.topology-flow {
|
||||
.topology-flow-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
:global(.semi-spin-children) {
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.topology-flow-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.topology-flow-container-flow {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.topology-flow_default {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
padding: 24px;
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ReactFlow, { ReactFlowProvider } from 'reactflow';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Spin } from '@coze-arch/bot-semi';
|
||||
|
||||
import { type TopologyFlowProps } from './typing';
|
||||
|
||||
import 'reactflow/dist/style.css';
|
||||
import { useGenerateTopology, useLayoutTopology } from './hook';
|
||||
import { CUSTOM_EDGES, CUSTOM_NODES } from './constant/flow';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
const TopologyFlowContent = (props: TopologyFlowProps) => {
|
||||
const { style, className, renderHeader, ...restProps } = props;
|
||||
|
||||
// Calculate topo data
|
||||
const [loading, topologicalData] = useGenerateTopology({
|
||||
...restProps,
|
||||
});
|
||||
|
||||
// After each topo data change, calculate the topo layout information
|
||||
const [topologyFlowDomRef] = useLayoutTopology(topologicalData);
|
||||
|
||||
// Rendering external custom header implementations (with business semantics)
|
||||
const topologyHeader = useMemo(() => {
|
||||
if (!renderHeader || !topologicalData) {
|
||||
return null;
|
||||
}
|
||||
return renderHeader(topologicalData.topoType);
|
||||
}, [renderHeader, topologicalData]);
|
||||
|
||||
return topologicalData ? (
|
||||
<div
|
||||
className={classNames(
|
||||
s['topology-flow'],
|
||||
className ?? s['topology-flow_default'],
|
||||
)}
|
||||
style={style}
|
||||
ref={topologyFlowDomRef}
|
||||
>
|
||||
{loading ? (
|
||||
<div className={s['topology-flow-loading']}>
|
||||
<Spin />
|
||||
</div>
|
||||
) : (
|
||||
<div className={s['topology-flow-container']}>
|
||||
{topologyHeader}
|
||||
<div className={s['topology-flow-container-flow']}>
|
||||
<ReactFlow
|
||||
// @ts-expect-error Use the number type to enumerate SpanType as a custom type, the error can be ignored
|
||||
nodes={topologicalData.nodes}
|
||||
edges={topologicalData.edges}
|
||||
nodeTypes={CUSTOM_NODES}
|
||||
edgeTypes={CUSTOM_EDGES}
|
||||
proOptions={{
|
||||
hideAttribution: true,
|
||||
}}
|
||||
nodesDraggable={false}
|
||||
nodesConnectable={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const TopologyFlow = (props: TopologyFlowProps) => (
|
||||
<ReactFlowProvider>
|
||||
<TopologyFlowContent {...props} />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
|
||||
export default TopologyFlow;
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Node, type Edge } from 'reactflow';
|
||||
import { type CSSProperties } from 'react';
|
||||
|
||||
import { type SpanCategory } from '@coze-arch/bot-api/ob_query_api';
|
||||
import { type LogBizScene } from '@coze-arch/bot-api/ob_data';
|
||||
import {
|
||||
type TopoInfo,
|
||||
type GetTopoInfoReq,
|
||||
type TopoType,
|
||||
} from '@coze-arch/bot-api/dp_manage_api';
|
||||
|
||||
import { type SpanNode } from '../../utils/cspan-graph';
|
||||
import { type DataSource } from '../../typings/graph';
|
||||
import { type TopologyLayoutDirection } from './constant';
|
||||
|
||||
export type ProcessedGetTopoInfoReq = Omit<GetTopoInfoReq, 'Base'>;
|
||||
|
||||
export interface TopologyFlowProps {
|
||||
spaceId: string;
|
||||
botId?: string;
|
||||
entityId?: string;
|
||||
entityType?: LogBizScene;
|
||||
dataSource: DataSource;
|
||||
selectedSpanId?: string;
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
renderHeader?: (topologyType: TopoType) => React.ReactNode;
|
||||
}
|
||||
|
||||
export interface UseGenerateTopologyHookData {
|
||||
spaceId: string;
|
||||
botId?: string;
|
||||
entityId?: string;
|
||||
entityType?: LogBizScene;
|
||||
dataSource: DataSource;
|
||||
selectedSpanId?: string;
|
||||
}
|
||||
|
||||
// @ts-expect-error Use the number type to enumerate SpanType as a custom type, the error can be ignored
|
||||
export type TopologicalNode = Node<NodeData, SpanCategory>;
|
||||
|
||||
export type TopologicalEdge = Edge<EdgeData>;
|
||||
|
||||
export interface NodeData {
|
||||
name: string;
|
||||
icon: React.ReactNode;
|
||||
layoutDirection: TopologyLayoutDirection;
|
||||
dynamicSpanNode?: SpanNode;
|
||||
}
|
||||
|
||||
export interface EdgeData {
|
||||
tailDynamicSpanNode?: SpanNode;
|
||||
layoutInfo?: {
|
||||
customSourceX: number;
|
||||
customTargetX: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type DynamicNodeMap = Record<string, SpanNode>;
|
||||
export type DynamicEdgeMap = Record<string, SpanNode>;
|
||||
|
||||
export interface DynamicTopologyData {
|
||||
dynamicNodeMap: DynamicNodeMap;
|
||||
originDynamicNodeMap: DynamicNodeMap;
|
||||
}
|
||||
|
||||
export interface TopologicalData {
|
||||
topoType: TopoType;
|
||||
nodes: TopologicalNode[];
|
||||
edges: TopologicalEdge[];
|
||||
}
|
||||
|
||||
export interface TopologicalLayoutCommonData {
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface TopologicalLayoutBizData extends TopologicalLayoutCommonData {
|
||||
icon: React.ReactElement;
|
||||
}
|
||||
|
||||
export interface TopologicalStatusData {
|
||||
edgeColor: string;
|
||||
nodeClassName: string;
|
||||
}
|
||||
|
||||
export interface TopologicalBatchNodeExecutionInfo {
|
||||
isBatch: boolean;
|
||||
isError: boolean;
|
||||
errorNumber: number;
|
||||
totalNumber: number;
|
||||
}
|
||||
|
||||
export interface TopoMetaInfo {
|
||||
topoGraph: Record<string, string[]>;
|
||||
resourceIdMap: Record<string, string>;
|
||||
nodeIdMap: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface StaticTopologyDataCache {
|
||||
topoInfoMap?: TopoInfo;
|
||||
topoMetaInfo?: TopoMetaInfo;
|
||||
upstreamNodeMap?: Record<string, string[]>;
|
||||
}
|
||||
+497
@@ -0,0 +1,497 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { MarkerType } from 'reactflow';
|
||||
|
||||
import { getFlags } from '@coze-arch/bot-flags';
|
||||
import {
|
||||
SpanCategory,
|
||||
SpanStatus,
|
||||
SpanType,
|
||||
} from '@coze-arch/bot-api/ob_query_api';
|
||||
import {
|
||||
TopoType,
|
||||
type TopoInfo,
|
||||
type Node as TopoNode,
|
||||
} from '@coze-arch/bot-api/dp_manage_api';
|
||||
import Dagre from '@dagrejs/dagre';
|
||||
|
||||
import {
|
||||
getSpanTitle as getDynamicSpanTitle,
|
||||
type SpanNode,
|
||||
} from '../../utils/cspan-graph';
|
||||
import {
|
||||
type CSpanAttrInvokeAgent,
|
||||
type CSpan,
|
||||
type CSpanAttrWorkflow,
|
||||
type CSpanAttrPluginTool,
|
||||
type CSpanAttrKnowledge,
|
||||
type CSpanAttrCondition,
|
||||
type CSPanBatch,
|
||||
type CSpanAttrLLMCall,
|
||||
} from '../../typings/cspan';
|
||||
import { rootBreakSpanId } from '../../constant';
|
||||
import { spanCategoryConfigMap } from '../../config/cspan';
|
||||
import {
|
||||
type DynamicNodeMap,
|
||||
type DynamicTopologyData,
|
||||
type TopologicalData,
|
||||
type TopologicalNode,
|
||||
type TopologicalEdge,
|
||||
type ProcessedGetTopoInfoReq,
|
||||
type TopoMetaInfo,
|
||||
} from './typing';
|
||||
import {
|
||||
NodeEdgeCategory,
|
||||
TOPOLOGY_COMMON_EDGE_OFFSET_WIDTH,
|
||||
TOPOLOGY_COMMON_NODE_TEXT_ADDITIONAL_WIDTH,
|
||||
TOPOLOGY_COMMON_NODE_TEXT_DEFAULT_WIDTH,
|
||||
TOPOLOGY_COMMON_NODE_TEXT_FONT,
|
||||
TOPOLOGY_COMMON_NODE_TEXT_HEIGHT,
|
||||
TOPOLOGY_COMMON_NODE_TEXT_MAX_WIDTH,
|
||||
TOPOLOGY_DEFAULT_NODE_ICON,
|
||||
TOPOLOGY_EDGE_STATUS_MAP,
|
||||
TOPOLOGY_LAYOUT_BIZ_MAP,
|
||||
TopologyEdgeStatus,
|
||||
type TopologyLayoutDirection,
|
||||
} from './constant';
|
||||
|
||||
const assignRecordIfNotExists = <T extends object>(
|
||||
object: T,
|
||||
key: keyof T,
|
||||
value: T[keyof T],
|
||||
) => {
|
||||
if (!object[key]) {
|
||||
object[key] = value;
|
||||
}
|
||||
};
|
||||
|
||||
export const filterObjectByKeys = <T extends object>(
|
||||
object: T,
|
||||
targetList: string[],
|
||||
) =>
|
||||
Object.keys(object)
|
||||
.filter(key => targetList.includes(key))
|
||||
.reduce<T>((pre, cur) => {
|
||||
pre[cur] = object[cur];
|
||||
return pre;
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
}, {} as T);
|
||||
|
||||
/**
|
||||
* Static topo data resource_id different and different data field mapping according to span type
|
||||
* @param spanNode current span
|
||||
* @param entityId
|
||||
* @Returns the id mapped to the static topo
|
||||
*/
|
||||
export const getNodeResourceId = (
|
||||
spanNode: SpanNode,
|
||||
entityId: string,
|
||||
): string => {
|
||||
const { category, id } = spanNode;
|
||||
const workflowNodeId =
|
||||
// Compatible with merged Batch Nodes
|
||||
(spanNode as CSPanBatch).workflow_node_id ??
|
||||
(spanNode as CSpanAttrCondition).extra?.workflow_node_id;
|
||||
if (workflowNodeId) {
|
||||
return workflowNodeId;
|
||||
}
|
||||
switch (category) {
|
||||
case SpanCategory.Agent:
|
||||
case SpanCategory.Start:
|
||||
return (spanNode as CSpanAttrInvokeAgent).extra?.agent_id || entityId;
|
||||
case SpanCategory.Workflow:
|
||||
return (spanNode as CSpanAttrWorkflow).extra?.workflow_id || id;
|
||||
case SpanCategory.Plugin:
|
||||
return (spanNode as CSpanAttrPluginTool).extra?.plugin_id || id;
|
||||
case SpanCategory.Knowledge:
|
||||
return (spanNode as CSpanAttrKnowledge).extra?.knowledge_id || id;
|
||||
case SpanCategory.LLMCall:
|
||||
return (spanNode as CSpanAttrLLMCall).extra?.model || id;
|
||||
default:
|
||||
return id;
|
||||
}
|
||||
};
|
||||
|
||||
export const generateStaticTopologyDataMapKey = (
|
||||
info: ProcessedGetTopoInfoReq,
|
||||
) => {
|
||||
const { space_id, resource_id, version, env, resource_type } = info;
|
||||
return `${space_id}-${resource_id}-${version}-${env}-${resource_type}`;
|
||||
};
|
||||
|
||||
// The SpanType. InvokeAgent node will be removed from the trace under a single agent, and the topology root may be SpanType. UserInput.
|
||||
export const getTopologyAgentRootType = () => {
|
||||
const FLAGS = getFlags();
|
||||
return FLAGS['bot.devops.use_user_input_as_agent']
|
||||
? [SpanType.InvokeAgent, SpanType.UserInput, SpanType.UserInputV2]
|
||||
: [SpanType.InvokeAgent];
|
||||
};
|
||||
|
||||
export const isTopologyRootSpan = (span: CSpan) => {
|
||||
// Only the base workflow shows the topology
|
||||
if (span.type === SpanType.Workflow) {
|
||||
return (span as CSpanAttrWorkflow).extra?.workflow_schema_type === 1;
|
||||
}
|
||||
|
||||
return [...getTopologyAgentRootType()].includes(span.type);
|
||||
};
|
||||
|
||||
export const extractOriginDynamicNodeMap = (
|
||||
spanNode: SpanNode,
|
||||
entityId: string,
|
||||
): DynamicTopologyData => {
|
||||
const dynamicNodeMap: DynamicNodeMap = {};
|
||||
const originDynamicNodeMap: DynamicNodeMap = {};
|
||||
const queue = [spanNode];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const currentNode = queue.shift() as SpanNode;
|
||||
// Filter out broken nodes
|
||||
if (currentNode.id === rootBreakSpanId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
originDynamicNodeMap[currentNode.id] = currentNode;
|
||||
assignRecordIfNotExists(
|
||||
dynamicNodeMap,
|
||||
getNodeResourceId(currentNode, entityId),
|
||||
currentNode,
|
||||
);
|
||||
|
||||
for (const childNode of currentNode.children ?? []) {
|
||||
queue.push(childNode);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
dynamicNodeMap,
|
||||
originDynamicNodeMap,
|
||||
};
|
||||
};
|
||||
|
||||
export const findNearestTopologyRootSpanNode = (
|
||||
spanNode?: SpanNode,
|
||||
): SpanNode | undefined => {
|
||||
let currentSpanNode = spanNode;
|
||||
while (currentSpanNode) {
|
||||
if (isTopologyRootSpan(currentSpanNode)) {
|
||||
return currentSpanNode;
|
||||
}
|
||||
currentSpanNode = currentSpanNode.parent;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
function findUserInputRootNode(dynamicNodeMap: DynamicNodeMap) {
|
||||
return Object.values(dynamicNodeMap).find(
|
||||
item =>
|
||||
item.type === SpanType.UserInput || item.type === SpanType.UserInputV2,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the dynamic nodes to the corresponding static nodes and connections to complete their dynamic operation information
|
||||
* @param staticTopoInfo
|
||||
* @Param dynamicNodeMap The dynamic node map that needs to be displayed at present
|
||||
* @param layoutDirection
|
||||
* @returns
|
||||
*/
|
||||
export const completeDynamicTopologyInfo = (
|
||||
staticTopoInfo: TopoInfo,
|
||||
dynamicNodeMap: DynamicNodeMap,
|
||||
layoutDirection: TopologyLayoutDirection,
|
||||
): TopologicalData => {
|
||||
const FLAGS = getFlags();
|
||||
const {
|
||||
nodes: staticNodes = [],
|
||||
edges: staticEdges = [],
|
||||
topo_type = TopoType.AgentFlow,
|
||||
} = staticTopoInfo;
|
||||
const nodeInfoMap: Record<
|
||||
string,
|
||||
{
|
||||
node: TopoNode;
|
||||
resourceId: string;
|
||||
}
|
||||
> = {};
|
||||
const nodes: TopologicalNode[] = staticNodes.map(item => {
|
||||
const {
|
||||
node_id = '',
|
||||
resource_id = '',
|
||||
resource_kind,
|
||||
resource_name = '',
|
||||
} = item;
|
||||
nodeInfoMap[node_id] = {
|
||||
node: item,
|
||||
resourceId: resource_id,
|
||||
};
|
||||
|
||||
const typedResourceKind: SpanCategory =
|
||||
resource_kind && resource_kind in SpanCategory
|
||||
? Number(resource_kind)
|
||||
: SpanCategory.Unknown;
|
||||
|
||||
let dynamicSpanNode = dynamicNodeMap[resource_id] as SpanNode | undefined;
|
||||
|
||||
// Specialized logic: In a single agent scenario, the node information will not include the agent node, and it needs to be replaced with userInput.
|
||||
if (
|
||||
typedResourceKind === SpanCategory.Agent &&
|
||||
!dynamicSpanNode &&
|
||||
FLAGS['bot.devops.use_user_input_as_agent']
|
||||
) {
|
||||
dynamicSpanNode = findUserInputRootNode(dynamicNodeMap) as
|
||||
| SpanNode
|
||||
| undefined;
|
||||
}
|
||||
|
||||
let title = '';
|
||||
if (dynamicSpanNode) {
|
||||
title = getDynamicSpanTitle(dynamicSpanNode);
|
||||
} else {
|
||||
title = getStaticSpanTitle(typedResourceKind, resource_name);
|
||||
}
|
||||
|
||||
return {
|
||||
id: node_id,
|
||||
type: typedResourceKind,
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
data: {
|
||||
name: title,
|
||||
icon:
|
||||
TOPOLOGY_LAYOUT_BIZ_MAP[typedResourceKind]?.icon ??
|
||||
TOPOLOGY_DEFAULT_NODE_ICON,
|
||||
dynamicSpanNode,
|
||||
layoutDirection,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const edges: TopologicalEdge[] = staticEdges.map(item => {
|
||||
const { edge_id = '', source_node_id = '', target_node_id = '' } = item;
|
||||
|
||||
const sourceNodeInfo = nodeInfoMap[source_node_id];
|
||||
// Specialized logic: there is no workflow_start node in dynamic tracing, the dynamic node downstream is filled in by default
|
||||
const isWorkflowStartNode =
|
||||
Number(sourceNodeInfo?.node?.resource_kind) ===
|
||||
SpanCategory.WorkflowStart;
|
||||
|
||||
let sourceNode: SpanNode | undefined =
|
||||
dynamicNodeMap[sourceNodeInfo?.resourceId];
|
||||
const targetNode: SpanNode | undefined =
|
||||
dynamicNodeMap[nodeInfoMap[target_node_id]?.resourceId];
|
||||
|
||||
// Specialized logic: In a single agent scenario, the node information will not include the agent node, and it needs to be replaced with userInput.
|
||||
if (
|
||||
sourceNodeInfo.node.resource_kind === SpanCategory.Agent &&
|
||||
!sourceNode &&
|
||||
FLAGS['bot.devops.use_user_input_as_agent']
|
||||
) {
|
||||
sourceNode = findUserInputRootNode(dynamicNodeMap) as
|
||||
| SpanNode
|
||||
| undefined;
|
||||
}
|
||||
|
||||
let tailDynamicSpanNode: SpanNode | undefined;
|
||||
if (isWorkflowStartNode || (sourceNode && targetNode)) {
|
||||
tailDynamicSpanNode = targetNode;
|
||||
}
|
||||
|
||||
const topologyEdgeStatus = getTopologyItemStatus(tailDynamicSpanNode);
|
||||
return {
|
||||
id: edge_id,
|
||||
source: source_node_id,
|
||||
target: target_node_id,
|
||||
type: NodeEdgeCategory.Common,
|
||||
markerEnd: {
|
||||
type: MarkerType.Arrow,
|
||||
color: TOPOLOGY_EDGE_STATUS_MAP[topologyEdgeStatus].edgeColor,
|
||||
height: 17,
|
||||
width: 17,
|
||||
},
|
||||
data: {
|
||||
tailDynamicSpanNode,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
nodes,
|
||||
edges,
|
||||
topoType: topo_type,
|
||||
};
|
||||
};
|
||||
|
||||
const measureTextCanvas = document.createElement('canvas');
|
||||
const measureTextContext = measureTextCanvas.getContext('2d');
|
||||
|
||||
const getTextWidth = (text: string) => {
|
||||
if (!measureTextContext) {
|
||||
return TOPOLOGY_COMMON_NODE_TEXT_DEFAULT_WIDTH;
|
||||
}
|
||||
measureTextContext.font = TOPOLOGY_COMMON_NODE_TEXT_FONT;
|
||||
return (
|
||||
Math.min(
|
||||
Math.round(measureTextContext.measureText(text).width),
|
||||
TOPOLOGY_COMMON_NODE_TEXT_MAX_WIDTH,
|
||||
) + TOPOLOGY_COMMON_NODE_TEXT_ADDITIONAL_WIDTH
|
||||
);
|
||||
};
|
||||
|
||||
const getStaticSpanTitle = (category: SpanCategory, name: string) => {
|
||||
const typeName = spanCategoryConfigMap[category]?.label ?? '';
|
||||
if (name && name !== typeName) {
|
||||
return `${typeName} ${name}`;
|
||||
} else {
|
||||
return typeName;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Layout and style processing of raw topo data
|
||||
* @param originTopologicalData
|
||||
* @param layoutDirection
|
||||
* @returns
|
||||
*/
|
||||
export const getLayoutedMeta = (
|
||||
originTopologicalData: TopologicalData,
|
||||
layoutDirection: TopologyLayoutDirection,
|
||||
): TopologicalData => {
|
||||
const graphInstance = new Dagre.graphlib.Graph().setDefaultEdgeLabel(
|
||||
() => ({}),
|
||||
);
|
||||
graphInstance.setGraph({
|
||||
rankdir: layoutDirection,
|
||||
align: 'UL',
|
||||
});
|
||||
const { edges, nodes, topoType } = originTopologicalData;
|
||||
|
||||
edges.forEach(edge => graphInstance.setEdge(edge.source, edge.target));
|
||||
|
||||
nodes.forEach(node => {
|
||||
const { type = SpanCategory.Unknown, data } = node;
|
||||
const { name } = data;
|
||||
|
||||
graphInstance.setNode(node.id, {
|
||||
label: name,
|
||||
height:
|
||||
TOPOLOGY_LAYOUT_BIZ_MAP[type]?.height ??
|
||||
TOPOLOGY_COMMON_NODE_TEXT_HEIGHT,
|
||||
width: getTextWidth(name),
|
||||
});
|
||||
});
|
||||
|
||||
Dagre.layout(graphInstance);
|
||||
|
||||
// Acquire the positioning information of the node for positioning when drawing vertical types of connections
|
||||
const nodeXAxisMap: Record<string, number> = {};
|
||||
|
||||
const layoutNodes: TopologicalNode[] = nodes.map(node => {
|
||||
const { x, y } = graphInstance.node(node.id);
|
||||
|
||||
nodeXAxisMap[node.id] = x;
|
||||
|
||||
return { ...node, position: { x, y } };
|
||||
});
|
||||
|
||||
const layoutEdges: TopologicalEdge[] = edges.map(edge => {
|
||||
const { source, target } = edge;
|
||||
return {
|
||||
...edge,
|
||||
data: {
|
||||
...edge.data,
|
||||
layoutInfo: {
|
||||
customSourceX:
|
||||
nodeXAxisMap[source] + TOPOLOGY_COMMON_EDGE_OFFSET_WIDTH,
|
||||
customTargetX:
|
||||
nodeXAxisMap[target] + TOPOLOGY_COMMON_EDGE_OFFSET_WIDTH,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
topoType,
|
||||
nodes: layoutNodes,
|
||||
edges: layoutEdges,
|
||||
};
|
||||
};
|
||||
|
||||
export const getTopologyItemStatus = (spanNode?: SpanNode) => {
|
||||
if (!spanNode) {
|
||||
return TopologyEdgeStatus.STATIC;
|
||||
}
|
||||
if (spanNode.status === SpanStatus.Error) {
|
||||
return TopologyEdgeStatus.ERROR;
|
||||
}
|
||||
return TopologyEdgeStatus.DYNAMIC;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate graphs and id maps of upstream nodes for each node using static topo data
|
||||
* @param topoInfo
|
||||
* @Returns meta information for graphs and maps
|
||||
*/
|
||||
export const generateTopologyMetaInfo = (topoInfo: TopoInfo): TopoMetaInfo => {
|
||||
const { nodes = [], edges = [] } = topoInfo;
|
||||
const resourceIdMap: Record<string, string> = {};
|
||||
const nodeIdMap: Record<string, string> = {};
|
||||
const topoGraph: Record<string, string[]> = {};
|
||||
for (const { node_id = '', resource_id = '' } of nodes) {
|
||||
resourceIdMap[node_id] = resource_id;
|
||||
nodeIdMap[resource_id] = node_id;
|
||||
}
|
||||
for (const { source_node_id = '', target_node_id = '' } of edges) {
|
||||
if (!topoGraph[target_node_id]) {
|
||||
topoGraph[target_node_id] = [];
|
||||
}
|
||||
topoGraph[target_node_id].push(source_node_id);
|
||||
}
|
||||
return {
|
||||
resourceIdMap,
|
||||
nodeIdMap,
|
||||
topoGraph,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Query all upstream nodes of a node in a static topo graph, using DP to reduce complexity
|
||||
* @param selectedNodeId Current span id
|
||||
* @Param topoGraph holds a graph that records the upstream nodes of each node
|
||||
* @Param upstreamNodeMap holds a map that records all upstream nodes of a node
|
||||
* @Returns the id list of all nodes upstream
|
||||
*/
|
||||
export const getAllUpstreamTopologyNodeIds = (
|
||||
selectedNodeId: string,
|
||||
topoGraph: Record<string, string[]>,
|
||||
upstreamNodeMap: Record<string, string[]>,
|
||||
) => {
|
||||
if (upstreamNodeMap[selectedNodeId]) {
|
||||
return upstreamNodeMap[selectedNodeId];
|
||||
}
|
||||
const upstreamNodeIds = topoGraph[selectedNodeId] ?? [];
|
||||
const allUpstreamNodeIds: string[] = [
|
||||
...upstreamNodeIds,
|
||||
...upstreamNodeIds.flatMap(nodeId =>
|
||||
getAllUpstreamTopologyNodeIds(nodeId, topoGraph, upstreamNodeMap),
|
||||
),
|
||||
];
|
||||
upstreamNodeMap[selectedNodeId] = allUpstreamNodeIds;
|
||||
return allUpstreamNodeIds;
|
||||
};
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { SpanStatus, SpanCategory } from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import { type LabelStyle, type RectStyle } from '../flamethread';
|
||||
import { type TraceFlamethreadProps } from './typing';
|
||||
|
||||
type DefaultProps = Pick<
|
||||
TraceFlamethreadProps,
|
||||
'rectStyle' | 'labelStyle' | 'rowHeight' | 'globalStyle' | 'datazoomDecimals'
|
||||
>;
|
||||
|
||||
export const defaultProps: DefaultProps = {
|
||||
labelStyle: {
|
||||
position: 'inside-left',
|
||||
fontSize: 12,
|
||||
fill: '#1D1C23CC',
|
||||
},
|
||||
rowHeight: 50,
|
||||
globalStyle: {},
|
||||
datazoomDecimals: 1,
|
||||
};
|
||||
|
||||
interface SpanCategoryConfig {
|
||||
[spanCategory: number]:
|
||||
| {
|
||||
rectStyle?: RectStyle;
|
||||
labelStyle?: LabelStyle;
|
||||
name?: string;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
|
||||
export const spanCategoryConfig: SpanCategoryConfig = {
|
||||
[SpanCategory.Unknown]: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: '#F7F7FA',
|
||||
},
|
||||
hover: {
|
||||
fill: '#F0F0F5',
|
||||
},
|
||||
select: {
|
||||
fill: '#C6C6CD',
|
||||
},
|
||||
},
|
||||
},
|
||||
[SpanCategory.Start]: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: '#F1F2FD',
|
||||
},
|
||||
hover: {
|
||||
fill: '#D9DCFA',
|
||||
},
|
||||
select: {
|
||||
fill: '#B4BAF6',
|
||||
},
|
||||
},
|
||||
labelStyle: {},
|
||||
name: 'start',
|
||||
},
|
||||
[SpanCategory.Agent]: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: '#F6EFFC',
|
||||
},
|
||||
hover: {
|
||||
fill: '#E9D6F9',
|
||||
},
|
||||
select: {
|
||||
fill: '#D1AEF4',
|
||||
},
|
||||
},
|
||||
name: 'invoke agent',
|
||||
},
|
||||
[SpanCategory.LLMCall]: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: '#F7F7FA',
|
||||
},
|
||||
hover: {
|
||||
fill: '#F0F0F5',
|
||||
},
|
||||
select: {
|
||||
fill: '#C6C6CD',
|
||||
},
|
||||
},
|
||||
name: 'invoke llm',
|
||||
},
|
||||
[SpanCategory.Workflow]: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: '#EDF9EE',
|
||||
},
|
||||
hover: {
|
||||
fill: '#D2F3D5',
|
||||
},
|
||||
select: {
|
||||
fill: '#CFECAC',
|
||||
},
|
||||
},
|
||||
name: 'invoke workflow',
|
||||
},
|
||||
|
||||
[SpanCategory.WorkflowStart]: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: '#EDF9EE',
|
||||
},
|
||||
hover: {
|
||||
fill: '#D2F3D5',
|
||||
},
|
||||
select: {
|
||||
fill: '#CFECAC',
|
||||
},
|
||||
},
|
||||
},
|
||||
[SpanCategory.WorkflowEnd]: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: '#EDF9EE',
|
||||
},
|
||||
hover: {
|
||||
fill: '#D2F3D5',
|
||||
},
|
||||
select: {
|
||||
fill: '#CFECAC',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
[SpanCategory.Plugin]: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: '#F1F2FD',
|
||||
},
|
||||
hover: {
|
||||
fill: '#D9DCFA',
|
||||
},
|
||||
select: {
|
||||
fill: '#B4BAF6',
|
||||
},
|
||||
},
|
||||
name: 'invoke plugin',
|
||||
},
|
||||
[SpanCategory.Knowledge]: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: '#FFEEEF',
|
||||
},
|
||||
hover: {
|
||||
fill: '#FFD2D7',
|
||||
},
|
||||
select: {
|
||||
fill: '#FFA5B2',
|
||||
},
|
||||
},
|
||||
name: 'invoke knowledage',
|
||||
},
|
||||
|
||||
[SpanCategory.Code]: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: '#E5F8F7',
|
||||
},
|
||||
hover: {
|
||||
fill: '#C1F2EF',
|
||||
},
|
||||
select: {
|
||||
fill: '#89E5E0',
|
||||
},
|
||||
},
|
||||
name: 'execute code',
|
||||
},
|
||||
[SpanCategory.Condition]: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: '#FFFAEB',
|
||||
},
|
||||
hover: {
|
||||
fill: '#FFF1CC',
|
||||
},
|
||||
select: {
|
||||
fill: '#FFDF99',
|
||||
},
|
||||
},
|
||||
name: 'if condition',
|
||||
},
|
||||
[SpanCategory.Card]: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: '#FFFAEB',
|
||||
},
|
||||
hover: {
|
||||
fill: '#FFF1CC',
|
||||
},
|
||||
select: {
|
||||
fill: '#FFDF99',
|
||||
},
|
||||
},
|
||||
name: 'card',
|
||||
},
|
||||
[SpanCategory.Message]: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: '#FFFAEB',
|
||||
},
|
||||
hover: {
|
||||
fill: '#FFF1CC',
|
||||
},
|
||||
select: {
|
||||
fill: '#FFDF99',
|
||||
},
|
||||
},
|
||||
name: 'message',
|
||||
},
|
||||
};
|
||||
|
||||
interface SpanStatusConfig {
|
||||
[spanStatus: string]: {
|
||||
tooltip?: {
|
||||
fill?: string;
|
||||
};
|
||||
rectStyle?: RectStyle;
|
||||
};
|
||||
}
|
||||
|
||||
export const spanStatusConfig: SpanStatusConfig = {
|
||||
[SpanStatus.Success]: {
|
||||
tooltip: {
|
||||
fill: '#3EC254',
|
||||
},
|
||||
rectStyle: {
|
||||
normal: {
|
||||
stroke: '#1D1C2314',
|
||||
},
|
||||
hover: {},
|
||||
select: {},
|
||||
},
|
||||
},
|
||||
[SpanStatus.Error]: {
|
||||
tooltip: {
|
||||
fill: '#FF441E',
|
||||
},
|
||||
rectStyle: {
|
||||
normal: {
|
||||
stroke: '#1D1C2314',
|
||||
fill: '#FFF3EE',
|
||||
},
|
||||
hover: {
|
||||
fill: '#FFE0D2',
|
||||
},
|
||||
select: {
|
||||
fill: '#FFBDA5',
|
||||
},
|
||||
},
|
||||
},
|
||||
[SpanStatus.Broken]: {
|
||||
tooltip: {
|
||||
fill: '#FF9600',
|
||||
},
|
||||
},
|
||||
[SpanStatus.Unknown]: {
|
||||
tooltip: {
|
||||
fill: '#6B6B75',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const tooltipStyle = {
|
||||
fill: '#212629',
|
||||
shape: {
|
||||
symbolType: 'square',
|
||||
fill: '#212629',
|
||||
size: 5,
|
||||
},
|
||||
};
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, type FC, useMemo, useState, useCallback } from 'react';
|
||||
|
||||
import { type IElement } from '@visactor/vgrammar';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import Flamethread, {
|
||||
type LabelText,
|
||||
type RectNode,
|
||||
type RectStyle,
|
||||
type Tooltip,
|
||||
} from '../flamethread';
|
||||
import {
|
||||
getSpanDataByTraceId,
|
||||
getSpanTitle,
|
||||
getStatusLabel,
|
||||
} from '../../utils/cspan-graph';
|
||||
import { getTokens } from '../../utils/cspan';
|
||||
import { DataSourceTypeEnum } from '../../typings/graph';
|
||||
import { type CSpan } from '../../typings/cspan';
|
||||
import { spanData2flamethreadData } from './util';
|
||||
import { type TraceFlamethreadProps } from './typing';
|
||||
import { defaultProps, spanStatusConfig, tooltipStyle } from './config';
|
||||
|
||||
const TraceFlamethread: FC<TraceFlamethreadProps> = props => {
|
||||
const [flamethreadData, setFlamethreadData] = useState<RectNode[]>([]);
|
||||
|
||||
const {
|
||||
dataSource: { type: dataType, spanData, traceId },
|
||||
rectStyle: _rectStyle,
|
||||
labelStyle: _labelStyle,
|
||||
globalStyle: _globalStyle,
|
||||
visibleColumnCount,
|
||||
datazoomDecimals = defaultProps.datazoomDecimals,
|
||||
axisLabelSuffix,
|
||||
selectedSpanId,
|
||||
spanTypeConfigMap,
|
||||
spanStatusConfigMap,
|
||||
disableViewScroll,
|
||||
enableAutoFit,
|
||||
onClick,
|
||||
} = props;
|
||||
|
||||
// Initialize flamethreadData
|
||||
useEffect(() => {
|
||||
if (dataType === DataSourceTypeEnum.SpanData && spanData) {
|
||||
if (spanData?.length === 0 && flamethreadData.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rectNodes = spanData2flamethreadData(spanData);
|
||||
setFlamethreadData(rectNodes);
|
||||
} else if (dataType === DataSourceTypeEnum.TraceId && traceId) {
|
||||
const spans = getSpanDataByTraceId(traceId);
|
||||
const rectNodes = spanData2flamethreadData(spans);
|
||||
setFlamethreadData(rectNodes);
|
||||
}
|
||||
}, [dataType, spanData, traceId]);
|
||||
|
||||
const rectStyle = useMemo((): RectStyle => {
|
||||
const defaultRectStyle = defaultProps.rectStyle;
|
||||
return {
|
||||
normal: Object.assign({}, defaultRectStyle?.normal, _rectStyle?.normal),
|
||||
hover: Object.assign({}, defaultRectStyle?.hover, _rectStyle?.hover),
|
||||
select: Object.assign({}, defaultRectStyle?.select, _rectStyle?.select),
|
||||
};
|
||||
}, [_rectStyle]);
|
||||
|
||||
const labelStyle = useMemo(
|
||||
() => Object.assign({}, _labelStyle, defaultProps.labelStyle),
|
||||
[_labelStyle],
|
||||
);
|
||||
|
||||
const globalStyle = useMemo(
|
||||
() => Object.assign({}, _globalStyle, defaultProps.globalStyle),
|
||||
[_globalStyle],
|
||||
);
|
||||
|
||||
const tooltip: Tooltip = useMemo(
|
||||
() => ({
|
||||
title: {
|
||||
value: (datum: RectNode, element: IElement, params) => ({}),
|
||||
},
|
||||
content: (datum: RectNode, element: IElement, params) => {
|
||||
const { span } = (datum.extra ?? {}) as { span: CSpan };
|
||||
if (!span) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { status, latency } = span;
|
||||
const statusConfig = spanStatusConfig[status];
|
||||
|
||||
const tips = [
|
||||
{
|
||||
key: I18n.t('analytic_query_status'),
|
||||
value: getStatusLabel(span, spanStatusConfigMap),
|
||||
},
|
||||
{
|
||||
key: I18n.t('analytic_query_latency'),
|
||||
value: latency ? `${latency}ms` : '-',
|
||||
},
|
||||
];
|
||||
const { input_tokens: inputTokens, output_tokens: outputTokens } =
|
||||
getTokens(span);
|
||||
if (inputTokens !== undefined && outputTokens !== undefined) {
|
||||
tips.push({
|
||||
key: I18n.t('analytic_query_tokens'),
|
||||
value: `${inputTokens + outputTokens}`,
|
||||
});
|
||||
}
|
||||
|
||||
return tips.map(({ key, value }) => ({
|
||||
key: {
|
||||
text: key,
|
||||
fill: tooltipStyle.fill,
|
||||
},
|
||||
value: {
|
||||
text: value ?? '',
|
||||
fill:
|
||||
key === I18n.t('analytic_query_status')
|
||||
? statusConfig?.tooltip?.fill
|
||||
: tooltipStyle.fill,
|
||||
},
|
||||
shape: tooltipStyle.shape,
|
||||
}));
|
||||
},
|
||||
}),
|
||||
[spanStatusConfigMap],
|
||||
);
|
||||
|
||||
const labelText: LabelText = useCallback(
|
||||
(datum: RectNode, element: IElement, params) => {
|
||||
const { span } = (datum.extra ?? {}) as { span: CSpan };
|
||||
return getSpanTitle(span, spanTypeConfigMap);
|
||||
},
|
||||
[spanTypeConfigMap],
|
||||
);
|
||||
|
||||
return flamethreadData ? (
|
||||
<Flamethread
|
||||
flamethreadData={flamethreadData}
|
||||
tooltip={tooltip}
|
||||
rectStyle={rectStyle}
|
||||
labelStyle={labelStyle}
|
||||
globalStyle={globalStyle}
|
||||
labelText={labelText}
|
||||
datazoomDecimals={datazoomDecimals}
|
||||
visibleColumnCount={visibleColumnCount}
|
||||
axisLabelSuffix={axisLabelSuffix}
|
||||
selectedKey={selectedSpanId}
|
||||
disableViewScroll={disableViewScroll}
|
||||
enableAutoFit={enableAutoFit}
|
||||
onClick={onClick}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default TraceFlamethread;
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FlamethreadProps } from '../flamethread';
|
||||
import { type DataSource } from '../../typings/graph';
|
||||
import {
|
||||
type SpanStatusConfigMap,
|
||||
type SpanTypeConfigMap,
|
||||
} from '../../typings/config';
|
||||
|
||||
export type TraceFlamethreadProps = {
|
||||
dataSource: DataSource;
|
||||
selectedSpanId?: string;
|
||||
spanTypeConfigMap?: SpanTypeConfigMap;
|
||||
spanStatusConfigMap?: SpanStatusConfigMap;
|
||||
} & Pick<
|
||||
FlamethreadProps,
|
||||
| 'rectStyle'
|
||||
| 'labelStyle'
|
||||
| 'globalStyle'
|
||||
| 'rowHeight'
|
||||
| 'visibleColumnCount'
|
||||
| 'datazoomDecimals'
|
||||
| 'axisLabelSuffix'
|
||||
| 'disableViewScroll'
|
||||
| 'enableAutoFit'
|
||||
| 'onClick'
|
||||
>;
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { sortBy } from 'lodash-es';
|
||||
import { SpanCategory } from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import { type RectNode, type RectStyle } from '../flamethread';
|
||||
import {
|
||||
buildCallTrees,
|
||||
getBreakSpans,
|
||||
getRootSpan,
|
||||
type SpanNode,
|
||||
} from '../../utils/cspan-graph';
|
||||
import { isVisibleSpan } from '../../utils/cspan';
|
||||
import { type CSpan } from '../../typings/cspan';
|
||||
import { spanCategoryConfig, spanStatusConfig } from './config';
|
||||
|
||||
const genRectStyle = (span: CSpan): RectStyle => {
|
||||
const { status, category = SpanCategory.Unknown } = span;
|
||||
const categoryRectStyle = spanCategoryConfig[category]?.rectStyle;
|
||||
const statusRectStyle = spanStatusConfig[status]?.rectStyle;
|
||||
|
||||
return {
|
||||
normal: Object.assign(
|
||||
{},
|
||||
categoryRectStyle?.normal,
|
||||
statusRectStyle?.normal,
|
||||
),
|
||||
hover: Object.assign({}, categoryRectStyle?.hover, statusRectStyle?.hover),
|
||||
select: Object.assign(
|
||||
{},
|
||||
categoryRectStyle?.select,
|
||||
statusRectStyle?.select,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const genRectNode = (info: {
|
||||
span: CSpan;
|
||||
startSpan: CSpan;
|
||||
rowNo: number;
|
||||
}): RectNode => {
|
||||
const { span, startSpan, rowNo } = info;
|
||||
const start = span.start_time - startSpan.start_time;
|
||||
return {
|
||||
key: span.id,
|
||||
rowNo,
|
||||
start,
|
||||
end: start + span.latency,
|
||||
rectStyle: genRectStyle(span),
|
||||
extra: {
|
||||
span,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const spanData2flamethreadData = (spanData: CSpan[]): RectNode[] => {
|
||||
// 1. According to spans, assemble call trees
|
||||
const callTrees = buildCallTrees(spanData);
|
||||
|
||||
// 2. Generate tartSpan
|
||||
const startSpan: SpanNode = getRootSpan(callTrees, false);
|
||||
|
||||
// 3. Get the break node (all non-start root nodes are breakSpan)
|
||||
const breakSpans: SpanNode[] = getBreakSpans(callTrees, false);
|
||||
|
||||
let rstSpans: SpanNode[] = [];
|
||||
|
||||
// Preorder search to ensure that the parent node is in front
|
||||
const walk = (spans: SpanNode[]) => {
|
||||
rstSpans = rstSpans.concat(spans);
|
||||
spans.forEach(span => {
|
||||
if (span.children) {
|
||||
walk(span.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
if (startSpan.children) {
|
||||
walk(startSpan.children);
|
||||
}
|
||||
walk(breakSpans);
|
||||
|
||||
// Filter out undisplayed span nodes
|
||||
rstSpans = rstSpans.filter(span => isVisibleSpan(span));
|
||||
|
||||
// Sort by start_time
|
||||
const sortedSpans = sortBy(rstSpans, o => o.start_time);
|
||||
|
||||
// Add follow node
|
||||
sortedSpans.unshift(startSpan);
|
||||
|
||||
const rectNodes: RectNode[] = [];
|
||||
sortedSpans.forEach((span, index) => {
|
||||
rectNodes.push(genRectNode({ span, startSpan, rowNo: index }));
|
||||
});
|
||||
|
||||
return rectNodes;
|
||||
};
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import {
|
||||
IconSpanAgent,
|
||||
IconSpanCard,
|
||||
IconSpanCode,
|
||||
IconSpanCondition,
|
||||
IconSpanKnowledge,
|
||||
IconSpanVar,
|
||||
IconSpanLLMCall,
|
||||
IconSpanHook,
|
||||
IconSpanMessage,
|
||||
IconSpanPluginTool,
|
||||
IconSpanUnknown,
|
||||
IconSpanWorkflow,
|
||||
IconSpanWorkflowEnd,
|
||||
IconSpanWorkflowStart,
|
||||
IconSpanBMConnector,
|
||||
IconSpanBMParallel,
|
||||
IconSpanBMBatch,
|
||||
} from '@coze-arch/bot-icons';
|
||||
import { SpanStatus, SpanCategory } from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import { type LineStyle } from '../tree';
|
||||
import { type TraceTreeProps } from './typing';
|
||||
|
||||
type DefaultProps = Pick<TraceTreeProps, 'lineStyle' | 'globalStyle'>;
|
||||
|
||||
export const defaultProps: DefaultProps = {
|
||||
lineStyle: {
|
||||
normal: {
|
||||
stroke: '#C6C6CD',
|
||||
strokeWidth: 1,
|
||||
},
|
||||
hover: {
|
||||
stroke: '#C6C6CD',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
select: {
|
||||
stroke: '#C6C6CD',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
type SpanCategoryConfig = Record<
|
||||
number,
|
||||
| {
|
||||
icon: ReactNode;
|
||||
title: string;
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
|
||||
export const spanCategoryConfig: SpanCategoryConfig = {
|
||||
[SpanCategory.Unknown]: {
|
||||
icon: <IconSpanUnknown />,
|
||||
title: 'Unknown',
|
||||
},
|
||||
[SpanCategory.Start]: {
|
||||
icon: <IconSpanWorkflowStart />,
|
||||
title: 'Start',
|
||||
},
|
||||
[SpanCategory.Agent]: {
|
||||
icon: <IconSpanAgent />,
|
||||
title: 'Agent',
|
||||
},
|
||||
[SpanCategory.LLMCall]: {
|
||||
icon: <IconSpanLLMCall />,
|
||||
title: 'Invoke LLM',
|
||||
},
|
||||
[SpanCategory.Workflow]: {
|
||||
icon: <IconSpanWorkflow />,
|
||||
title: 'Invoke Workflow',
|
||||
},
|
||||
|
||||
[SpanCategory.WorkflowStart]: {
|
||||
icon: <IconSpanWorkflowStart />,
|
||||
title: 'Workflow Start',
|
||||
},
|
||||
[SpanCategory.WorkflowEnd]: {
|
||||
icon: <IconSpanWorkflowEnd />,
|
||||
title: 'Workflow End',
|
||||
},
|
||||
|
||||
[SpanCategory.Plugin]: {
|
||||
icon: <IconSpanPluginTool />,
|
||||
title: 'Invoke Plugin',
|
||||
},
|
||||
|
||||
[SpanCategory.Knowledge]: {
|
||||
icon: <IconSpanKnowledge />,
|
||||
title: 'Recall Knowledage',
|
||||
},
|
||||
|
||||
[SpanCategory.Code]: {
|
||||
icon: <IconSpanCode />,
|
||||
title: 'Execute Code',
|
||||
},
|
||||
[SpanCategory.Condition]: {
|
||||
icon: <IconSpanCondition />,
|
||||
title: 'If Condition',
|
||||
},
|
||||
[SpanCategory.Card]: {
|
||||
icon: <IconSpanCard />,
|
||||
title: 'Card',
|
||||
},
|
||||
[SpanCategory.Message]: {
|
||||
icon: <IconSpanMessage />,
|
||||
title: 'Message',
|
||||
},
|
||||
[SpanCategory.Variable]: {
|
||||
icon: <IconSpanVar />,
|
||||
title: 'Variable',
|
||||
},
|
||||
[SpanCategory.Hook]: {
|
||||
icon: <IconSpanHook />,
|
||||
title: 'Hook',
|
||||
},
|
||||
[SpanCategory.Batch]: {
|
||||
icon: <IconSpanBMBatch />,
|
||||
title: 'Batch',
|
||||
},
|
||||
[SpanCategory.Loop]: {
|
||||
icon: <IconSpanBMBatch />,
|
||||
title: 'Loop',
|
||||
},
|
||||
[SpanCategory.Parallel]: {
|
||||
icon: <IconSpanBMParallel />,
|
||||
title: 'Parallel',
|
||||
},
|
||||
[SpanCategory.Script]: {
|
||||
icon: <IconSpanCode />,
|
||||
title: 'Script',
|
||||
},
|
||||
[SpanCategory.CallFlow]: {
|
||||
icon: <IconSpanWorkflow />,
|
||||
title: 'CallFlow',
|
||||
},
|
||||
[SpanCategory.Connector]: {
|
||||
icon: <IconSpanBMConnector />,
|
||||
title: 'Connector',
|
||||
},
|
||||
};
|
||||
|
||||
interface SpanStatusConfig {
|
||||
[spanStatus: string]: {
|
||||
lineStyle?: LineStyle;
|
||||
};
|
||||
}
|
||||
|
||||
export const spanStatusConfig: SpanStatusConfig = {
|
||||
[SpanStatus.Success]: {},
|
||||
[SpanStatus.Error]: {
|
||||
lineStyle: {
|
||||
normal: {
|
||||
stroke: '#FF441E',
|
||||
},
|
||||
hover: {
|
||||
stroke: '#FF441E',
|
||||
},
|
||||
select: {
|
||||
stroke: '#FF441E',
|
||||
},
|
||||
},
|
||||
},
|
||||
[SpanStatus.Broken]: {},
|
||||
[SpanStatus.Unknown]: {},
|
||||
};
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
.trace-tree {
|
||||
.trace-tree-node {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
margin-left: 4px;
|
||||
padding: 0 4px;
|
||||
|
||||
font-size: 12px;
|
||||
color: #1D1C23;
|
||||
white-space: nowrap;
|
||||
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&:global(.selected) {
|
||||
.title {
|
||||
background: #2E2E381F;
|
||||
}
|
||||
}
|
||||
|
||||
&:global(.hover) {
|
||||
.title {
|
||||
background: #2E2E3814;
|
||||
}
|
||||
}
|
||||
|
||||
&:global(.error) {
|
||||
.title {
|
||||
color: #FF441E;
|
||||
}
|
||||
|
||||
&:global(.selected) {
|
||||
.title {
|
||||
background: #FFE0D2;
|
||||
}
|
||||
}
|
||||
|
||||
&:global(.hover) {
|
||||
.title {
|
||||
background: #FFF3EE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line no-descending-specificity */
|
||||
&:global(.disabled) {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { SceneType, usePageJumpService } from '@coze-arch/bot-hooks';
|
||||
|
||||
import Tree, { type TreeNode } from '../tree';
|
||||
import { getSpanDataByTraceId } from '../../utils/cspan-graph';
|
||||
import { DataSourceTypeEnum } from '../../typings/graph';
|
||||
import { spanData2treeData } from './util';
|
||||
import { type WorkflowJumpParams, type TraceTreeProps } from './typing';
|
||||
import { defaultProps } from './config';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
const TraceTree: FC<TraceTreeProps> = props => {
|
||||
const [treeData, setTreeData] = useState<TreeNode>();
|
||||
const [hoverNodeKey, setHoverNodeKey] = useState('');
|
||||
const { jump } = usePageJumpService();
|
||||
const {
|
||||
dataSource: { type: dataType, spanData, traceId },
|
||||
spaceId,
|
||||
selectedSpanId,
|
||||
spanTypeConfigMap,
|
||||
indentDisabled,
|
||||
lineStyle: _lineStyle,
|
||||
globalStyle: _globalStyle,
|
||||
className,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const lineStyle = useMemo(
|
||||
() => ({
|
||||
normal: Object.assign(
|
||||
{},
|
||||
defaultProps.lineStyle?.normal,
|
||||
_lineStyle?.normal,
|
||||
),
|
||||
select: Object.assign(
|
||||
{},
|
||||
defaultProps.lineStyle?.select,
|
||||
_lineStyle?.select,
|
||||
),
|
||||
hover: Object.assign(
|
||||
{},
|
||||
defaultProps.lineStyle?.hover,
|
||||
_lineStyle?.hover,
|
||||
),
|
||||
}),
|
||||
[_lineStyle],
|
||||
);
|
||||
|
||||
const globalStyle = useMemo(
|
||||
() => Object.assign({}, defaultProps.globalStyle, _globalStyle),
|
||||
[_globalStyle],
|
||||
);
|
||||
const handleJumpToWorkflow = ({
|
||||
workflowID,
|
||||
executeID,
|
||||
workflowNodeID,
|
||||
workflowVersion,
|
||||
subExecuteID,
|
||||
}: WorkflowJumpParams) => {
|
||||
if (!spaceId) {
|
||||
return;
|
||||
}
|
||||
jump(SceneType.BOT__VIEW__WORKFLOW, {
|
||||
workflowID,
|
||||
spaceID: spaceId,
|
||||
botID: '',
|
||||
executeID,
|
||||
workflowNodeID,
|
||||
workflowVersion,
|
||||
subExecuteID,
|
||||
newWindow: true,
|
||||
});
|
||||
};
|
||||
// Initialize flamethreadData
|
||||
useEffect(() => {
|
||||
if (dataType === DataSourceTypeEnum.SpanData && spanData) {
|
||||
if (spanData?.length === 0 && treeData === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const treeNode = spanData2treeData(spanData, spanTypeConfigMap, {
|
||||
spaceId,
|
||||
onHoverChange: setHoverNodeKey,
|
||||
onJumpToWorkflow: handleJumpToWorkflow,
|
||||
});
|
||||
setTreeData(treeNode);
|
||||
} else if (dataType === DataSourceTypeEnum.TraceId && traceId) {
|
||||
const spans = getSpanDataByTraceId(traceId);
|
||||
const treeNode = spanData2treeData(spans, spanTypeConfigMap, {
|
||||
onHoverChange: setHoverNodeKey,
|
||||
onJumpToWorkflow: handleJumpToWorkflow,
|
||||
});
|
||||
setTreeData(treeNode);
|
||||
}
|
||||
}, [dataType, spanData, traceId, spanTypeConfigMap]);
|
||||
|
||||
return treeData ? (
|
||||
<Tree
|
||||
className={classNames(styles['trace-tree'], className)}
|
||||
treeData={treeData}
|
||||
disableDefaultHover={true}
|
||||
hoverKey={hoverNodeKey}
|
||||
selectedKey={selectedSpanId}
|
||||
indentDisabled={indentDisabled}
|
||||
lineStyle={lineStyle}
|
||||
globalStyle={globalStyle}
|
||||
{...restProps}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default TraceTree;
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type TreeProps } from '../tree';
|
||||
import { type DataSource } from '../../typings/graph';
|
||||
import { type SpanTypeConfigMap } from '../../typings/config';
|
||||
|
||||
export type TraceTreeProps = {
|
||||
dataSource: DataSource;
|
||||
spaceId?: string;
|
||||
selectedSpanId?: string;
|
||||
spanTypeConfigMap?: SpanTypeConfigMap;
|
||||
} & Pick<
|
||||
TreeProps,
|
||||
| 'indentDisabled'
|
||||
| 'lineStyle'
|
||||
| 'globalStyle'
|
||||
| 'onSelect'
|
||||
| 'onClick'
|
||||
| 'onMouseMove'
|
||||
| 'onMouseEnter'
|
||||
| 'onMouseLeave'
|
||||
| 'className'
|
||||
>;
|
||||
|
||||
export interface SpanDetail {
|
||||
isCozeWorkflowNode: boolean;
|
||||
workflowLevel: number; // Workflow Hierarchy
|
||||
workflowVersion?: string; // Parent node passes through to sub-node
|
||||
}
|
||||
|
||||
export interface WorkflowJumpParams {
|
||||
workflowID: string;
|
||||
executeID?: string;
|
||||
workflowNodeID?: string;
|
||||
workflowVersion?: string;
|
||||
subExecuteID?: string;
|
||||
}
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozFocus } from '@coze-arch/coze-design/icons';
|
||||
import { Button } from '@coze-arch/coze-design';
|
||||
import { Tooltip } from '@coze-arch/bot-semi';
|
||||
import { IconpanNodeDamaged } from '@coze-arch/bot-icons';
|
||||
import {
|
||||
SpanCategory,
|
||||
SpanStatus,
|
||||
SpanType,
|
||||
} from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import { type TreeNode, type TreeNodeExtra } from '../tree';
|
||||
import {
|
||||
buildTraceTree,
|
||||
getSpanTitle,
|
||||
type SpanNode,
|
||||
} from '../../utils/cspan-graph';
|
||||
import { getTokens, getSpanProp } from '../../utils/cspan';
|
||||
import { type CSpan } from '../../typings/cspan';
|
||||
import { type SpanTypeConfigMap } from '../../typings/config';
|
||||
import { rootBreakSpanId } from '../../constant';
|
||||
import { type WorkflowJumpParams, type SpanDetail } from './typing';
|
||||
import { spanStatusConfig, spanCategoryConfig } from './config';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
const genTitleRender = ({
|
||||
spanTypeConfigMap,
|
||||
spanInfoMap,
|
||||
spaceId,
|
||||
onHoverChange,
|
||||
onJumpToWorkflow,
|
||||
}: {
|
||||
spanTypeConfigMap?: SpanTypeConfigMap;
|
||||
spanInfoMap?: Record<string, SpanDetail | undefined>;
|
||||
spaceId?: string;
|
||||
onHoverChange?: (key: string) => void;
|
||||
onJumpToWorkflow?: (params: WorkflowJumpParams) => void;
|
||||
}) => {
|
||||
const titleRender = (nodeData: TreeNodeExtra): ReactNode => {
|
||||
const { selected, unindented, hover, key } = nodeData;
|
||||
const { span } = nodeData?.extra as { span: CSpan };
|
||||
const { status, latency, category = SpanCategory.Unknown } = span;
|
||||
const title = getSpanTitle(span, spanTypeConfigMap);
|
||||
|
||||
const isCozeWorkflow =
|
||||
spanInfoMap?.[span.id]?.isCozeWorkflowNode &&
|
||||
Boolean(getSpanProp(span, 'workflow_id')) &&
|
||||
spaceId;
|
||||
const workflowID = getSpanProp(span, 'workflow_id') as string;
|
||||
const workflowVersion =
|
||||
getSpanProp(span, 'workflow_version')?.toString() ||
|
||||
spanInfoMap?.[span.id]?.workflowVersion;
|
||||
|
||||
const handleJumpToWorkflow = () => {
|
||||
const executeID = getSpanProp(span, 'execute_id') as string | undefined;
|
||||
const workflowNodeID = getSpanProp(span, 'workflow_node_id')?.toString();
|
||||
const subExecuteID = getSpanProp(span, 'sub_execute_id')?.toString();
|
||||
|
||||
onJumpToWorkflow?.({
|
||||
workflowID,
|
||||
executeID,
|
||||
workflowVersion,
|
||||
workflowNodeID,
|
||||
subExecuteID,
|
||||
});
|
||||
};
|
||||
|
||||
const { input_tokens: inputTokens, output_tokens: outputTokens } =
|
||||
getTokens(span);
|
||||
let content = '';
|
||||
|
||||
if (inputTokens !== undefined && outputTokens !== undefined) {
|
||||
const tokensStr = inputTokens + outputTokens;
|
||||
content = `${I18n.t('analytic_query_latency')}: ${latency}ms | ${I18n.t(
|
||||
'analytic_query_tokens',
|
||||
)}: ${tokensStr}`;
|
||||
} else {
|
||||
content = `${I18n.t('analytic_query_latency')}: ${latency}ms`;
|
||||
}
|
||||
const config = spanCategoryConfig[category];
|
||||
|
||||
// Virtual broken root node
|
||||
if (span.id === rootBreakSpanId) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles['trace-tree-node'], {
|
||||
selected: false,
|
||||
unindented,
|
||||
hover,
|
||||
error: status === SpanStatus.Error,
|
||||
disabled: true,
|
||||
})}
|
||||
>
|
||||
<IconpanNodeDamaged />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<Tooltip position="right" content={content} trigger="hover">
|
||||
<div
|
||||
className={classNames(styles['trace-tree-node'], {
|
||||
selected,
|
||||
unindented,
|
||||
hover,
|
||||
error: status === SpanStatus.Error,
|
||||
})}
|
||||
onMouseEnter={() => onHoverChange?.(key)}
|
||||
onMouseLeave={() => onHoverChange?.('')}
|
||||
>
|
||||
{config?.icon}
|
||||
<span className={styles.title}>{title}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{isCozeWorkflow &&
|
||||
workflowID !== undefined &&
|
||||
workflowVersion !== undefined ? (
|
||||
<Tooltip position="top" content={I18n.t('view_workflow_details')}>
|
||||
<Button
|
||||
icon={<IconCozFocus />}
|
||||
style={{ width: 16, height: 16, marginLeft: 4 }}
|
||||
color="secondary"
|
||||
size="mini"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
handleJumpToWorkflow();
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
return titleRender;
|
||||
};
|
||||
|
||||
export const spanData2treeData = (
|
||||
spanData: CSpan[],
|
||||
spanTypeConfigMap?: SpanTypeConfigMap,
|
||||
options?: {
|
||||
spaceId?: string;
|
||||
onHoverChange?: (key: string) => void;
|
||||
onJumpToWorkflow?: (params: WorkflowJumpParams) => void;
|
||||
},
|
||||
): TreeNode | undefined => {
|
||||
const traceTree = buildTraceTree(spanData);
|
||||
const spanInfoMap = getSpanInfoMap(traceTree);
|
||||
|
||||
const walk = (span: SpanNode): TreeNode => {
|
||||
const lineStyle = spanStatusConfig[span.status]?.lineStyle;
|
||||
|
||||
let treeNode: TreeNode = {
|
||||
key: span.id,
|
||||
title: genTitleRender({ spanTypeConfigMap, spanInfoMap, ...options }),
|
||||
selectEnabled: true,
|
||||
indentDisabled: false,
|
||||
lineStyle,
|
||||
zIndex: span.status === SpanStatus.Error ? 1 : 0,
|
||||
extra: {
|
||||
span,
|
||||
},
|
||||
};
|
||||
// breakSpan node
|
||||
if (span.id === rootBreakSpanId) {
|
||||
treeNode = {
|
||||
...treeNode,
|
||||
selectEnabled: false,
|
||||
indentDisabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
treeNode.children = span.children?.map(childSpan => walk(childSpan)) ?? [];
|
||||
|
||||
return treeNode;
|
||||
};
|
||||
|
||||
return walk(traceTree);
|
||||
};
|
||||
|
||||
export const getSpanInfoMap = (root: SpanNode) => {
|
||||
const spanInfoMap: Record<string, SpanDetail | undefined> = {};
|
||||
|
||||
const bfs = (node: SpanNode) => {
|
||||
// Coze workflow settings isCozeWorkflowNode
|
||||
if (
|
||||
node.type === SpanType.Workflow &&
|
||||
getSpanProp(node, 'workflow_schema_type') === 1
|
||||
) {
|
||||
const parentLevel = spanInfoMap[node.parent_id]?.workflowLevel || 0;
|
||||
spanInfoMap[node.id] = {
|
||||
isCozeWorkflowNode: true,
|
||||
workflowLevel: parentLevel + 1,
|
||||
workflowVersion:
|
||||
parentLevel <= 1
|
||||
? getSpanProp(node, 'workflow_version')?.toString() ||
|
||||
spanInfoMap[node.parent_id]?.workflowVersion
|
||||
: undefined,
|
||||
};
|
||||
} else {
|
||||
// Coze workflow sub-node settings isCozeWorkflowNode
|
||||
const { isCozeWorkflowNode, workflowLevel = 0 } =
|
||||
spanInfoMap[node.parent_id] || {};
|
||||
|
||||
if (isCozeWorkflowNode) {
|
||||
spanInfoMap[node.id] = {
|
||||
isCozeWorkflowNode: true,
|
||||
workflowLevel: workflowLevel + 1,
|
||||
workflowVersion:
|
||||
workflowLevel <= 1
|
||||
? spanInfoMap[node.parent_id]?.workflowVersion
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// recursion
|
||||
for (const childNode of node.children || []) {
|
||||
bfs(childNode);
|
||||
}
|
||||
};
|
||||
|
||||
bfs(root);
|
||||
|
||||
return spanInfoMap;
|
||||
};
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type GlobalStyle, type LineStyle } from './typing';
|
||||
|
||||
export const defaultGlobalStyle: GlobalStyle = {
|
||||
indent: 24,
|
||||
verticalInterval: 16,
|
||||
nodeBoxHeight: 16,
|
||||
offsetX: 8,
|
||||
};
|
||||
|
||||
export const defaultLineStyle: LineStyle = {
|
||||
normal: {
|
||||
stroke: '#ccc',
|
||||
strokeDasharray: '[]',
|
||||
strokeWidth: 2,
|
||||
lineRadius: 6,
|
||||
lineGap: 0,
|
||||
},
|
||||
select: {
|
||||
stroke: '#333',
|
||||
},
|
||||
hover: {
|
||||
stroke: '#d25e5a',
|
||||
},
|
||||
};
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
.tree {
|
||||
display: flex;
|
||||
// padding: 10px;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
// background-color: #f1f1f1;
|
||||
|
||||
.tree-container {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
|
||||
.tree-path-list {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tree-node-list {
|
||||
position: relative;
|
||||
|
||||
.tree-node {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
.tree-node-box {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
// .tree-node-icon {
|
||||
// display: flex;
|
||||
// justify-content: center;
|
||||
// align-items: center;
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+322
@@ -0,0 +1,322 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-magic-numbers -- there will be many numbers calculated in this component, no need to deal with them*/
|
||||
import { type FC, useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { isFunction, mergeWith } from 'lodash-es';
|
||||
|
||||
import { flattenTreeData } from './util';
|
||||
import type {
|
||||
TreeProps,
|
||||
TreeNode,
|
||||
TreeNodeExtra,
|
||||
MouseEventParams,
|
||||
Line,
|
||||
LineStyle,
|
||||
GlobalStyle,
|
||||
} from './typing';
|
||||
import { defaultGlobalStyle, defaultLineStyle } from './config';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export type {
|
||||
TreeProps,
|
||||
TreeNode,
|
||||
TreeNodeExtra,
|
||||
MouseEventParams,
|
||||
LineStyle,
|
||||
GlobalStyle,
|
||||
};
|
||||
|
||||
const Tree: FC<TreeProps> = ({
|
||||
treeData,
|
||||
selectedKey,
|
||||
disableDefaultHover,
|
||||
hoverKey: customHoverKey,
|
||||
indentDisabled = false,
|
||||
lineStyle: gLineStyle,
|
||||
globalStyle,
|
||||
className,
|
||||
onMouseMove,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
onClick,
|
||||
onSelect,
|
||||
}) => {
|
||||
const [hoverKey, setHoverKey] = useState<string>('');
|
||||
|
||||
const controlledHoverKey = disableDefaultHover ? customHoverKey : hoverKey;
|
||||
|
||||
const { indent, verticalInterval, nodeBoxHeight, offsetX } = useMemo(
|
||||
() =>
|
||||
Object.assign(
|
||||
{},
|
||||
defaultGlobalStyle,
|
||||
globalStyle,
|
||||
) as Required<GlobalStyle>,
|
||||
[globalStyle],
|
||||
);
|
||||
|
||||
/**
|
||||
* Causes the Line of the specified selectKey to be placed at the top level.
|
||||
* By adjusting the line order, the z-index effect is achieved: the line with the key ${selectKey} is at the top
|
||||
*/
|
||||
const adjustLineOrder = useCallback(
|
||||
(lines: Line[]): Line[] => {
|
||||
let selectedLine, hoverLine;
|
||||
let i = 0;
|
||||
while (i < lines.length) {
|
||||
const line = lines[i];
|
||||
if (line.endNode.key === selectedKey) {
|
||||
selectedLine = lines.splice(i, 1)[0];
|
||||
} else if (line.endNode.key === controlledHoverKey) {
|
||||
hoverLine = lines.splice(i, 1)[0];
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// Support controlling height according to zIndex
|
||||
lines.sort((lineA, lineB) => {
|
||||
const zIndexA = lineA.endNode.zIndex ?? -1;
|
||||
const zIndexB = lineB.endNode.zIndex ?? -1;
|
||||
|
||||
if (zIndexA > zIndexB) {
|
||||
return 1;
|
||||
} else if (zIndexA < zIndexB) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
if (selectedLine) {
|
||||
lines.push(selectedLine);
|
||||
}
|
||||
if (hoverLine) {
|
||||
lines.push(hoverLine);
|
||||
}
|
||||
|
||||
return lines;
|
||||
},
|
||||
[selectedKey, controlledHoverKey],
|
||||
);
|
||||
|
||||
const genLineStyle = useCallback(
|
||||
(lineStyle?: LineStyle): LineStyle => ({
|
||||
normal: Object.assign(
|
||||
{},
|
||||
defaultLineStyle?.normal,
|
||||
gLineStyle?.normal,
|
||||
lineStyle?.normal,
|
||||
),
|
||||
select: Object.assign(
|
||||
{},
|
||||
defaultLineStyle?.select,
|
||||
gLineStyle?.select,
|
||||
lineStyle?.select,
|
||||
),
|
||||
hover: Object.assign(
|
||||
{},
|
||||
defaultLineStyle?.hover,
|
||||
gLineStyle?.hover,
|
||||
lineStyle?.hover,
|
||||
),
|
||||
}),
|
||||
[gLineStyle],
|
||||
);
|
||||
|
||||
/**
|
||||
* Generate svg path from line information. colNo, rowNum all start from 0
|
||||
*/
|
||||
const genSvgPath = useCallback(
|
||||
(line: Line): string => {
|
||||
const {
|
||||
startNode: { colNo: startColNo, rowNo: startRowNo },
|
||||
endNode: { colNo: endColNo, rowNo: endRowNo, lineStyle },
|
||||
} = line;
|
||||
|
||||
const { normal: normalLineStyle = {} } = genLineStyle(lineStyle);
|
||||
|
||||
const { lineRadius = 0, lineGap = 0 } = normalLineStyle;
|
||||
const nodeHeight = nodeBoxHeight + verticalInterval;
|
||||
|
||||
// starting point
|
||||
const startX = startColNo * indent + offsetX;
|
||||
const startY =
|
||||
startRowNo * nodeHeight + (nodeBoxHeight + verticalInterval / 2);
|
||||
|
||||
if (startColNo === endColNo) {
|
||||
// The length of the vertical line
|
||||
const lineASize =
|
||||
(endRowNo - startRowNo - 1) * nodeHeight + verticalInterval;
|
||||
// Move to the starting point
|
||||
const moveToStartPoint = `M ${startX} ${startY + lineGap}`;
|
||||
// vertical line
|
||||
const lineA = `L ${startX} ${startY + lineASize}`;
|
||||
return `${moveToStartPoint} ${lineA}`;
|
||||
} else {
|
||||
// The length of the vertical line
|
||||
const lineASize =
|
||||
(endRowNo - startRowNo - 1) * nodeHeight +
|
||||
verticalInterval / 2 +
|
||||
nodeHeight / 2 -
|
||||
lineRadius;
|
||||
// The length of the horizontal line
|
||||
const lineBSize =
|
||||
(endColNo - startColNo) * indent - offsetX - lineRadius;
|
||||
// Coordinates of the end point
|
||||
const endX = startX + lineBSize + lineRadius;
|
||||
const endY = startY + lineASize + lineRadius;
|
||||
|
||||
// Move to the starting point
|
||||
const moveToStartPoint = `M ${startX} ${startY + lineGap}`;
|
||||
// vertical line
|
||||
const lineA = `L ${startX} ${startY + lineASize}`;
|
||||
// Quadratic Bézier Curve
|
||||
const qbc = `Q ${startX} ${endY} ${startX + lineRadius} ${endY}`;
|
||||
// horizontal line
|
||||
const lineB = `L ${endX - lineGap} ${endY}`;
|
||||
return `${moveToStartPoint} ${lineA} ${qbc} ${lineB}`;
|
||||
}
|
||||
},
|
||||
[genLineStyle, indent, nodeBoxHeight, offsetX, verticalInterval],
|
||||
);
|
||||
|
||||
const genLineAttrs = useCallback(
|
||||
(nodeKey: string, lineStyle: LineStyle) => {
|
||||
if (controlledHoverKey !== selectedKey) {
|
||||
if (nodeKey === controlledHoverKey) {
|
||||
return mergeWith({}, lineStyle.normal, lineStyle.hover);
|
||||
}
|
||||
if (nodeKey === selectedKey) {
|
||||
return mergeWith({}, lineStyle.normal, lineStyle.select);
|
||||
}
|
||||
return lineStyle.normal;
|
||||
} else {
|
||||
if (nodeKey === controlledHoverKey) {
|
||||
return mergeWith(
|
||||
{},
|
||||
lineStyle.normal,
|
||||
lineStyle.select,
|
||||
lineStyle.hover,
|
||||
);
|
||||
} else {
|
||||
return lineStyle.normal;
|
||||
}
|
||||
}
|
||||
},
|
||||
[controlledHoverKey, selectedKey],
|
||||
);
|
||||
|
||||
const { nodes, lines: orgLines } = flattenTreeData(treeData, {
|
||||
indentDisabled,
|
||||
});
|
||||
const lines = adjustLineOrder(orgLines);
|
||||
|
||||
return (
|
||||
<div className={`${styles.tree} ${className ?? ''}`}>
|
||||
<div
|
||||
className={styles['tree-container']}
|
||||
style={{ marginTop: -verticalInterval / 2 }}
|
||||
>
|
||||
<div className={styles['tree-path-list']}>
|
||||
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
|
||||
{lines.map((line, index) => {
|
||||
const path = genSvgPath(line);
|
||||
const { lineStyle } = line.endNode;
|
||||
const lineStyle0 = genLineStyle(lineStyle);
|
||||
const attrs = genLineAttrs(line.endNode.key, lineStyle0);
|
||||
|
||||
return (
|
||||
<path
|
||||
d={path}
|
||||
stroke={attrs?.stroke}
|
||||
strokeWidth={attrs?.strokeWidth}
|
||||
strokeDasharray={attrs?.strokeDasharray}
|
||||
fill="none"
|
||||
// strokeLinecap="round"
|
||||
key={line.endNode.key}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
</div>
|
||||
<div className={styles['tree-node-list']}>
|
||||
{nodes.map(node => {
|
||||
const { key, title, selectEnabled = true, colNo } = node;
|
||||
const nodeExtra: TreeNodeExtra = {
|
||||
...node,
|
||||
selected: selectedKey === key,
|
||||
hover: controlledHoverKey === key,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles['tree-node']}
|
||||
style={{
|
||||
paddingTop: verticalInterval / 2,
|
||||
paddingBottom: verticalInterval / 2,
|
||||
}}
|
||||
key={node.key}
|
||||
>
|
||||
<div
|
||||
className={styles['tree-node-box']}
|
||||
style={{
|
||||
marginLeft: colNo * indent,
|
||||
height: nodeBoxHeight,
|
||||
}}
|
||||
onClick={event => {
|
||||
if (selectEnabled) {
|
||||
onSelect?.({ node: nodeExtra });
|
||||
}
|
||||
onClick?.({ event, node: nodeExtra });
|
||||
}}
|
||||
onMouseMove={event => {
|
||||
onMouseMove?.({ event, node: nodeExtra });
|
||||
}}
|
||||
onMouseEnter={event => {
|
||||
if (selectEnabled) {
|
||||
setHoverKey(key);
|
||||
}
|
||||
onMouseEnter?.({
|
||||
event,
|
||||
node: { ...nodeExtra, hover: true },
|
||||
});
|
||||
}}
|
||||
onMouseLeave={event => {
|
||||
if (selectEnabled) {
|
||||
setHoverKey('');
|
||||
}
|
||||
onMouseLeave?.({
|
||||
event,
|
||||
node: { ...nodeExtra, hover: false },
|
||||
});
|
||||
}}
|
||||
>
|
||||
{isFunction(title) ? title(nodeExtra) : title}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tree;
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReactNode, type SVGAttributes } from 'react';
|
||||
|
||||
export type LineAttrs = Pick<
|
||||
SVGAttributes<unknown>,
|
||||
'stroke' | 'strokeDasharray' | 'strokeWidth'
|
||||
> & {
|
||||
lineRadius?: number; // Line fillet radius, note: this value should not be greater than indent/2
|
||||
lineGap?: number; // Line distance box gap
|
||||
};
|
||||
|
||||
export interface LineStyle {
|
||||
normal?: LineAttrs;
|
||||
select?: LineAttrs;
|
||||
hover?: LineAttrs;
|
||||
}
|
||||
|
||||
// tree-node-box
|
||||
export interface TreeNode {
|
||||
key: string;
|
||||
title: ReactNode | ((nodeData: TreeNodeExtra) => ReactNode);
|
||||
selectEnabled?: boolean; // Default value true
|
||||
indentDisabled?: boolean; // Turn off indentation. Only works for the following scenarios: the last node in the sub-node
|
||||
lineStyle?: LineStyle; // When this property is specified, the global lineStyle is overridden.
|
||||
children?: TreeNode[];
|
||||
linePath?: PathEnum[];
|
||||
zIndex?: number;
|
||||
// Other fields will be passed through
|
||||
extra?: unknown;
|
||||
}
|
||||
|
||||
export enum PathEnum {
|
||||
Hidden = 0,
|
||||
Show = 1,
|
||||
Active = 2,
|
||||
}
|
||||
|
||||
export type TreeNodeExtra = Omit<TreeNode, 'children'> & {
|
||||
colNo: number;
|
||||
rowNo: number;
|
||||
unindented: boolean; // Is it unindented relative to the parent node?
|
||||
selected: boolean; // Is it selected?
|
||||
hover: boolean; // Whether to hover
|
||||
};
|
||||
|
||||
// Flattened TreeNode information
|
||||
export type TreeNodeFlatten = Omit<TreeNodeExtra, 'selected' | 'hover'>;
|
||||
|
||||
export interface Line {
|
||||
startNode: TreeNodeFlatten;
|
||||
endNode: TreeNodeFlatten;
|
||||
}
|
||||
|
||||
export interface GlobalStyle {
|
||||
indent?: number; // Indent distance of parent and child nodes
|
||||
verticalInterval?: number; // Vertical spacing of nodes
|
||||
nodeBoxHeight?: number; // The height of the node-box node
|
||||
offsetX?: number;
|
||||
}
|
||||
|
||||
export interface MouseEventParams {
|
||||
event: React.MouseEvent<HTMLDivElement>;
|
||||
node: TreeNodeExtra;
|
||||
}
|
||||
|
||||
export interface TreeProps {
|
||||
treeData: TreeNode;
|
||||
selectedKey?: string;
|
||||
hoverKey?: string;
|
||||
disableDefaultHover?: boolean;
|
||||
indentDisabled?: boolean; // Turn off indentation. Only works for the following scenarios: the last node
|
||||
lineStyle?: LineStyle;
|
||||
globalStyle?: GlobalStyle;
|
||||
className?: string;
|
||||
|
||||
onSelect?: (info: Pick<MouseEventParams, 'node'>) => void;
|
||||
onClick?: (info: MouseEventParams) => void;
|
||||
onMouseMove?: (info: MouseEventParams) => void;
|
||||
onMouseEnter?: (info: MouseEventParams) => void;
|
||||
onMouseLeave?: (info: MouseEventParams) => void;
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { omit } from 'lodash-es';
|
||||
|
||||
import { type TreeNodeFlatten, type TreeNode, type Line } from './typing';
|
||||
|
||||
/**
|
||||
* Generated based on TreeData:
|
||||
*
|
||||
* @param treeData tree original data source
|
||||
* @Param options.indentDisabled Whether to unindent. Valid only for the following scenarios: exception node + last node
|
||||
*
|
||||
* @returns
|
||||
* 1. nodes, node information after leveling
|
||||
* 2. lines, used to connect nodes
|
||||
*/
|
||||
export const flattenTreeData = (
|
||||
treeData: TreeNode,
|
||||
options: {
|
||||
indentDisabled: boolean;
|
||||
},
|
||||
): { nodes: TreeNodeFlatten[]; lines: Line[] } => {
|
||||
const nodes: TreeNodeFlatten[] = [];
|
||||
const lines: Line[] = [];
|
||||
const walk = (
|
||||
node: TreeNode,
|
||||
nodeColNo: number,
|
||||
fatherNodeFlatten?: TreeNodeFlatten,
|
||||
) => {
|
||||
const nodeFlatten: TreeNodeFlatten = {
|
||||
...omit(node, ['children']),
|
||||
colNo: nodeColNo,
|
||||
rowNo: nodes.length,
|
||||
unindented: fatherNodeFlatten?.colNo === nodeColNo, // Unindented
|
||||
};
|
||||
nodes.push(nodeFlatten);
|
||||
if (fatherNodeFlatten !== undefined) {
|
||||
lines.push({
|
||||
startNode: fatherNodeFlatten,
|
||||
endNode: nodeFlatten,
|
||||
});
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
const childNodes = node.children;
|
||||
|
||||
childNodes.forEach((childNode, index) => {
|
||||
// Cancel indentation. Effective scene: exception node + last node
|
||||
const indentDisabled =
|
||||
childNode.indentDisabled ?? options.indentDisabled;
|
||||
if (indentDisabled && childNodes.length - 1 === index) {
|
||||
walk(childNode, nodeColNo, nodeFlatten);
|
||||
} else {
|
||||
walk(childNode, nodeColNo + 1, nodeFlatten);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
walk(treeData, 0);
|
||||
return { nodes, lines };
|
||||
};
|
||||
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
SpanCategory,
|
||||
SpanStatus,
|
||||
SpanType,
|
||||
} from '@coze-arch/bot-api/ob_query_api';
|
||||
import {
|
||||
SpanCategoryConfigMap,
|
||||
SpanStatusConfigMap,
|
||||
SpanTypeConfigMap,
|
||||
} from '../typings/config';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { StreamingOutputStatus } from '../typings/cspan';
|
||||
|
||||
export const spanTypeConfigMap: SpanTypeConfigMap = {
|
||||
[SpanType.Unknown]: {
|
||||
label: I18n.t('analytic_query_subtype_value_unknown'),
|
||||
},
|
||||
[SpanType.UserInput]: {
|
||||
label: I18n.t('analytic_query_subtype_value_userinput'),
|
||||
},
|
||||
[SpanType.UserInputV2]: {
|
||||
label: I18n.t('analytic_query_subtype_value_userinput'),
|
||||
},
|
||||
[SpanType.ThirdParty]: {
|
||||
label: I18n.t('analytic_query_subtype_value_thirdparty'),
|
||||
},
|
||||
[SpanType.ScheduledTasks]: {
|
||||
label: I18n.t('analytic_query_subtype_value_scheduledtasks'),
|
||||
},
|
||||
[SpanType.OpenDialog]: {
|
||||
label: I18n.t('analytic_query_subtype_value_opendialog'),
|
||||
},
|
||||
[SpanType.InvokeAgent]: {
|
||||
label: I18n.t('analytic_query_subtype_value_invokeagent'),
|
||||
},
|
||||
[SpanType.RestartAgent]: {
|
||||
label: I18n.t('analytic_query_subtype_value_restartagent'),
|
||||
},
|
||||
[SpanType.SwitchAgent]: {
|
||||
label: I18n.t('analytic_query_subtype_value_switchagent'),
|
||||
},
|
||||
[SpanType.LLMCall]: {
|
||||
label: I18n.t('analytic_query_subtype_value_llmcall'),
|
||||
},
|
||||
[SpanType.LLMBatchCall]: {
|
||||
label: I18n.t('analytic_query_subtype_value_llmbatchcall'),
|
||||
},
|
||||
[SpanType.Workflow]: {
|
||||
label: I18n.t('analytic_query_subtype_value_workflow'),
|
||||
},
|
||||
[SpanType.WorkflowStart]: {
|
||||
label: I18n.t('analytic_query_subtype_value_workflowstart'),
|
||||
},
|
||||
[SpanType.WorkflowEnd]: {
|
||||
label: I18n.t('analytic_query_subtype_value_workflowend'),
|
||||
},
|
||||
[SpanType.PluginTool]: {
|
||||
label: I18n.t('analytic_query_subtype_value_plugintool'),
|
||||
},
|
||||
[SpanType.PluginToolBatch]: {
|
||||
label: I18n.t('analytic_query_subtype_value_plugintoolbatch'),
|
||||
},
|
||||
[SpanType.Knowledge]: {
|
||||
label: I18n.t('analytic_query_subtype_value_knowledge'),
|
||||
},
|
||||
[SpanType.Code]: {
|
||||
label: I18n.t('analytic_query_subtype_value_code'),
|
||||
},
|
||||
[SpanType.CodeBatch]: {
|
||||
label: I18n.t('analytic_query_subtype_value_codebatch'),
|
||||
},
|
||||
[SpanType.Condition]: {
|
||||
label: I18n.t('analytic_query_subtype_value_condition'),
|
||||
},
|
||||
[SpanType.Card]: {
|
||||
label: I18n.t('analytic_query_subtype_value_card'),
|
||||
},
|
||||
[SpanType.WorkflowMessage]: {
|
||||
label: I18n.t('analytic_query_subtype_value_workflow_message'),
|
||||
},
|
||||
|
||||
[SpanType.WorkflowLLMCall]: {
|
||||
label: I18n.t('analytic_query_subtype_value_llmcall'),
|
||||
},
|
||||
[SpanType.WorkflowLLMBatchCall]: {
|
||||
label: I18n.t('analytic_query_subtype_value_llmbatchcall'),
|
||||
},
|
||||
[SpanType.WorkflowCode]: {
|
||||
label: I18n.t('analytic_query_subtype_value_code'),
|
||||
},
|
||||
[SpanType.WorkflowCodeBatch]: {
|
||||
label: I18n.t('analytic_query_subtype_value_codebatch'),
|
||||
},
|
||||
[SpanType.WorkflowCondition]: {
|
||||
label: I18n.t('analytic_query_subtype_value_condition'),
|
||||
},
|
||||
[SpanType.WorkflowPluginTool]: {
|
||||
label: I18n.t('analytic_query_subtype_value_plugintool'),
|
||||
},
|
||||
[SpanType.WorkflowPluginToolBatch]: {
|
||||
label: I18n.t('analytic_query_subtype_value_plugintoolbatch'),
|
||||
},
|
||||
[SpanType.WorkflowKnowledge]: {
|
||||
label: I18n.t('analytic_query_subtype_value_knowledge'),
|
||||
},
|
||||
[SpanType.Chain]: {},
|
||||
// specific business
|
||||
[SpanType.Hook]: {
|
||||
label: I18n.t('analytics_query_invoke', {
|
||||
name: 'Hook',
|
||||
}),
|
||||
},
|
||||
[SpanType.BWStart]: { label: 'BWStart' },
|
||||
[SpanType.BWEnd]: { label: 'BWEnd' },
|
||||
[SpanType.BWBatch]: { label: 'BWBatch' },
|
||||
[SpanType.BWLoop]: { label: 'BWLoop' },
|
||||
[SpanType.BWCondition]: { label: 'BWCondition' },
|
||||
[SpanType.BWLLM]: { label: 'BWLLM' },
|
||||
[SpanType.BWParallel]: { label: 'BWParallel' },
|
||||
[SpanType.BWScript]: { label: 'BWScript' },
|
||||
[SpanType.BWVariable]: { label: 'BWVariable' },
|
||||
[SpanType.BWCallFlow]: { label: 'BWCallFlow' },
|
||||
[SpanType.BWConnector]: { label: 'BWConnector' },
|
||||
};
|
||||
|
||||
export const spanCategoryConfigMap: SpanCategoryConfigMap = {
|
||||
[SpanCategory.Unknown]: {
|
||||
label: I18n.t('analytic_query_type_value_unknown'),
|
||||
},
|
||||
[SpanCategory.Start]: {
|
||||
label: I18n.t('analytic_query_type_value_start'),
|
||||
},
|
||||
[SpanCategory.Agent]: {
|
||||
label: I18n.t('analytic_query_type_value_agent'),
|
||||
},
|
||||
[SpanCategory.LLMCall]: {
|
||||
label: I18n.t('analytic_query_type_value_llmcall'),
|
||||
},
|
||||
[SpanCategory.Workflow]: {
|
||||
label: I18n.t('analytic_query_type_value_workflow'),
|
||||
},
|
||||
[SpanCategory.WorkflowStart]: {
|
||||
label: I18n.t('analytic_query_type_value_workflowstart'),
|
||||
},
|
||||
[SpanCategory.WorkflowEnd]: {
|
||||
label: I18n.t('analytic_query_type_value_workflowend'),
|
||||
},
|
||||
[SpanCategory.Plugin]: {
|
||||
label: I18n.t('analytic_query_type_value_plugin'),
|
||||
},
|
||||
[SpanCategory.Knowledge]: {
|
||||
label: I18n.t('analytic_query_type_value_knowledge'),
|
||||
},
|
||||
[SpanCategory.Code]: {
|
||||
label: I18n.t('analytic_query_type_value_code'),
|
||||
},
|
||||
[SpanCategory.Condition]: {
|
||||
label: I18n.t('analytic_query_type_value_condition'),
|
||||
},
|
||||
[SpanCategory.Card]: {
|
||||
label: I18n.t('analytic_query_type_value_card'),
|
||||
},
|
||||
[SpanCategory.Message]: {
|
||||
label: I18n.t('analytic_query_type_value_message'),
|
||||
},
|
||||
[SpanCategory.Variable]: {
|
||||
label: I18n.t('analytics_query_type_variable'),
|
||||
},
|
||||
[SpanCategory.Hook]: {
|
||||
label: 'Hook',
|
||||
},
|
||||
[SpanCategory.Batch]: {
|
||||
label: 'Batch',
|
||||
},
|
||||
[SpanCategory.Loop]: {
|
||||
label: 'Loop',
|
||||
},
|
||||
[SpanCategory.Parallel]: {
|
||||
label: 'Parallel',
|
||||
},
|
||||
[SpanCategory.Script]: {
|
||||
label: 'Script',
|
||||
},
|
||||
[SpanCategory.CallFlow]: {
|
||||
label: 'CallFlow',
|
||||
},
|
||||
[SpanCategory.Connector]: {
|
||||
label: 'Connector',
|
||||
},
|
||||
};
|
||||
|
||||
export const spanStatusConfigMap: SpanStatusConfigMap = {
|
||||
[SpanStatus.Unknown]: {
|
||||
label: I18n.t('analytic_query_status_unknown'),
|
||||
},
|
||||
[SpanStatus.Success]: {
|
||||
label: I18n.t('analytic_query_status_success'),
|
||||
},
|
||||
[SpanStatus.Error]: {
|
||||
label: I18n.t('analytic_query_status_error'),
|
||||
},
|
||||
[SpanStatus.Broken]: {
|
||||
label: I18n.t('analytic_query_status_broken'),
|
||||
},
|
||||
};
|
||||
|
||||
export const streamingOutputStatusConfigMap: Record<
|
||||
StreamingOutputStatus,
|
||||
{ label?: string } | undefined
|
||||
> = {
|
||||
[StreamingOutputStatus.OPEN]: {
|
||||
label: I18n.t('analytic_streaming_output_status_open'),
|
||||
},
|
||||
[StreamingOutputStatus.CLOSE]: {
|
||||
label: I18n.t('analytic_streaming_output_status_close'),
|
||||
},
|
||||
[StreamingOutputStatus.UNDEFINED]: {},
|
||||
};
|
||||
|
||||
export const botEnvConfigMap: Record<string, { label?: string } | undefined> = {
|
||||
'0': {
|
||||
label: I18n.t('analytic_query_env_value_botmakerdebug'),
|
||||
},
|
||||
'1': {
|
||||
label: I18n.t('analytic_query_env_value_realuser'),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const virtualStartSpanId = '-10001';
|
||||
export const rootBreakSpanId = '-10002';
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import {
|
||||
SpanType,
|
||||
type Span,
|
||||
type TraceAdvanceInfo,
|
||||
SpanStatus,
|
||||
SpanCategory,
|
||||
} from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import { spans2CSpans } from '../utils/cspan-transform';
|
||||
import {
|
||||
type SpanNode,
|
||||
buildCallTrees,
|
||||
getRootSpan,
|
||||
} from '../utils/cspan-graph';
|
||||
import { genVirtualStart, getSpanProp } from '../utils/cspan';
|
||||
import {
|
||||
type SpanCategoryMeta,
|
||||
type CSpan,
|
||||
type CSpanAttrInvokeAgent,
|
||||
type CSPanBatch,
|
||||
type CTrace,
|
||||
type CSpanAttrUserInput,
|
||||
} from '../typings/cspan';
|
||||
|
||||
// Append traceAdvanceInfo to the root node
|
||||
const appendTraceAdvanceInfo = (
|
||||
spans: CSpan[],
|
||||
traceAdvanceInfo?: Omit<TraceAdvanceInfo, 'trace_id'>,
|
||||
): CSpan[] =>
|
||||
spans.map(span => {
|
||||
// Modify the state of the root node. The tokens and status of the root node are subject to the server level
|
||||
if (
|
||||
span.type === SpanType.UserInput ||
|
||||
span.type === SpanType.UserInputV2
|
||||
) {
|
||||
const span0 = span as CSpanAttrUserInput;
|
||||
return {
|
||||
...span0,
|
||||
status: traceAdvanceInfo?.status ?? SpanStatus.Unknown,
|
||||
input_tokens_sum: traceAdvanceInfo?.tokens.input,
|
||||
output_tokens_sum: traceAdvanceInfo?.tokens.output,
|
||||
};
|
||||
} else {
|
||||
return span;
|
||||
}
|
||||
});
|
||||
|
||||
interface TokensSum {
|
||||
input_tokens_sum: number;
|
||||
output_tokens_sum: number;
|
||||
}
|
||||
const appendSpans = (spans: CSpan[], callTrees: SpanNode[]) => {
|
||||
const tokensMap: {
|
||||
[spanId: string]: TokensSum | undefined;
|
||||
} = {};
|
||||
const calculateTokensSum = (span: SpanNode): TokensSum => {
|
||||
const { input_tokens: inputTokens, output_tokens: outputTokens } =
|
||||
getCSpanTokens(span);
|
||||
let inputTokensSumRst = inputTokens;
|
||||
let outputTokensSumRst = outputTokens;
|
||||
|
||||
span.children?.forEach((subSpan: SpanNode) => {
|
||||
const subTokensSum = calculateTokensSum(subSpan);
|
||||
inputTokensSumRst += subTokensSum.input_tokens_sum;
|
||||
outputTokensSumRst += subTokensSum.output_tokens_sum;
|
||||
return span;
|
||||
});
|
||||
const tokensSum = {
|
||||
input_tokens_sum: inputTokensSumRst,
|
||||
output_tokens_sum: outputTokensSumRst,
|
||||
};
|
||||
tokensMap[span.id] = tokensSum;
|
||||
return tokensSum;
|
||||
};
|
||||
|
||||
callTrees.forEach(callTree => {
|
||||
calculateTokensSum(callTree);
|
||||
});
|
||||
|
||||
return spans.map(span => {
|
||||
if (
|
||||
span.type === SpanType.UserInput ||
|
||||
span.type === SpanType.UserInputV2
|
||||
) {
|
||||
// The values input_tokens_sum and output_tokens_sum of the root node are subject to server level acquisition and are not calculated
|
||||
return span;
|
||||
} else {
|
||||
return {
|
||||
...span,
|
||||
input_tokens_sum: tokensMap[span.id]?.input_tokens_sum,
|
||||
output_tokens_sum: tokensMap[span.id]?.output_tokens_sum,
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Get the tokens information of the CSpan node
|
||||
const getCSpanTokens = (
|
||||
span: CSpan,
|
||||
): {
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
} => {
|
||||
if ('spans' in span) {
|
||||
const spanBatch = span as CSPanBatch;
|
||||
let inputTokensRst = 0;
|
||||
let outputTokensRst = 0;
|
||||
spanBatch.spans.forEach(subSpan => {
|
||||
const inputTokens = subSpan?.extra?.input_tokens;
|
||||
const outputTokens = subSpan?.extra?.output_tokens;
|
||||
if (inputTokens !== undefined) {
|
||||
inputTokensRst += inputTokens;
|
||||
}
|
||||
if (outputTokens !== undefined) {
|
||||
outputTokensRst += outputTokens;
|
||||
}
|
||||
});
|
||||
return {
|
||||
input_tokens: inputTokensRst,
|
||||
output_tokens: outputTokensRst,
|
||||
};
|
||||
} else {
|
||||
// SingleSpan Node
|
||||
return {
|
||||
input_tokens: (getSpanProp(span, 'input_tokens') as number) ?? 0,
|
||||
output_tokens: (getSpanProp(span, 'output_tokens') as number) ?? 0,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Append invokeAgentInfo's dialog_round and model fields
|
||||
const appendRootSpan = (info: { rootSpan: CSpan; spans: CSpan[] }): CTrace => {
|
||||
const { rootSpan, spans } = info;
|
||||
const rstSpan: CTrace = rootSpan;
|
||||
let { extra } = rstSpan;
|
||||
|
||||
const invokeAgentSpans = spans.filter(
|
||||
span => span.type === SpanType.InvokeAgent,
|
||||
);
|
||||
if (invokeAgentSpans.length > 0) {
|
||||
const invokeAgentSpan = invokeAgentSpans[0] as CSpanAttrInvokeAgent;
|
||||
extra = {
|
||||
...extra,
|
||||
dialog_round: invokeAgentSpan.extra?.dialog_round,
|
||||
model: invokeAgentSpan.extra?.model,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...rstSpan,
|
||||
extra,
|
||||
};
|
||||
};
|
||||
|
||||
interface UseSpanTransformProps {
|
||||
orgSpans: Span[];
|
||||
traceAdvanceInfo?: Omit<TraceAdvanceInfo, 'trace_id'>;
|
||||
spanCategoryMeta?: SpanCategoryMeta;
|
||||
messageId?: string;
|
||||
}
|
||||
interface UseSpanTransformReturn {
|
||||
rootSpan: CTrace;
|
||||
spans: CSpan[];
|
||||
}
|
||||
|
||||
// When the start node does not exist, generate a virtual start node
|
||||
export const appendVirtualStart = (spans: CSpan[]): CSpan[] => {
|
||||
const startSpans = spans.filter(rootSpan => {
|
||||
const { category } = rootSpan;
|
||||
return category === SpanCategory.Start;
|
||||
});
|
||||
|
||||
// Generate virtual spans
|
||||
if (startSpans.length > 0) {
|
||||
return spans;
|
||||
} else {
|
||||
const virtualStartSpan = genVirtualStart(spans);
|
||||
return spans.concat(virtualStartSpan);
|
||||
}
|
||||
};
|
||||
|
||||
export const useSpanTransform = (
|
||||
props: UseSpanTransformProps,
|
||||
): UseSpanTransformReturn => {
|
||||
const { orgSpans, traceAdvanceInfo, spanCategoryMeta, messageId } = props;
|
||||
|
||||
const rst = useMemo(() => {
|
||||
let spans = spans2CSpans(orgSpans, spanCategoryMeta);
|
||||
// Append virtual span
|
||||
spans = appendVirtualStart(spans);
|
||||
|
||||
// append traceAdvanceInfo
|
||||
spans = appendTraceAdvanceInfo(spans, traceAdvanceInfo);
|
||||
|
||||
// According to spans, assembling called trees
|
||||
let callTrees = buildCallTrees(spans);
|
||||
// If there is more than 1 root node, it needs to be filtered by message id.
|
||||
if (callTrees.length > 1 && messageId) {
|
||||
callTrees = callTrees.filter(
|
||||
root =>
|
||||
!('extra' in root) ||
|
||||
(root.extra && !('message_id' in root.extra)) ||
|
||||
// In the presence of message_id, filter the nodes with matching IDs
|
||||
root.extra?.message_id === messageId,
|
||||
);
|
||||
}
|
||||
const rootSpan = getRootSpan(callTrees, false);
|
||||
|
||||
// Root node adjustment of rootSpan: add invokeAgent information
|
||||
const rootSpanRst = appendRootSpan({
|
||||
rootSpan,
|
||||
spans,
|
||||
});
|
||||
|
||||
const visit = (targetId: string, root: SpanNode): boolean => {
|
||||
if (root.id === targetId) {
|
||||
return true;
|
||||
}
|
||||
return root.children?.some(subRoot => visit(targetId, subRoot)) ?? false;
|
||||
};
|
||||
|
||||
// Filter out nodes not in rootSpan
|
||||
spans = spans.filter(span => visit(span.id, rootSpan));
|
||||
|
||||
// Adjust the spans node: the accumulation calculation of tokens in the workflow node in spans
|
||||
const spansRst = appendSpans(spans, callTrees);
|
||||
return {
|
||||
rootSpan: rootSpanRst,
|
||||
spans: spansRst,
|
||||
};
|
||||
}, [orgSpans, traceAdvanceInfo, spanCategoryMeta, messageId]);
|
||||
|
||||
return rst;
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { default as TraceFlamethread } from './components/trace-flamethread';
|
||||
export { default as TraceTree } from './components/trace-tree';
|
||||
export { default as TopologyFlow } from './components/topology-flow';
|
||||
export {
|
||||
default as Flamethread,
|
||||
type InteractionEventHandler,
|
||||
} from './components/flamethread';
|
||||
export { default as Tree, type MouseEventParams } from './components/tree';
|
||||
export { useSpanTransform } from './hooks/use-span-transform';
|
||||
// Parameter types for Tree and FlamethRead
|
||||
export { DataSourceTypeEnum } from './typings/graph';
|
||||
|
||||
export {
|
||||
// useSpanTransform related types
|
||||
type SpanCategoryMeta,
|
||||
// useSpanTransform generated custom spans
|
||||
type CSpan,
|
||||
type CTrace,
|
||||
type CSpanSingle,
|
||||
type CSPanBatch,
|
||||
type CSpanAttrUserInput,
|
||||
type CSpanAttrInvokeAgent,
|
||||
type CSpanAttrRestartAgent,
|
||||
type CSpanAttrSwitchAgent,
|
||||
type CSpanAttrLLMCall,
|
||||
type CSpanAttrLLMBatchCall,
|
||||
type CSpanAttrWorkflow,
|
||||
type CSpanAttrWorkflowEnd,
|
||||
type CSpanAttrCode,
|
||||
type CSpanAttrCodeBatch,
|
||||
type CSpanAttrCondition,
|
||||
type CSpanAttrPluginTool,
|
||||
type CSpanAttrPluginToolBatch,
|
||||
type CSpanAttrKnowledge,
|
||||
type CSpanAttrChain,
|
||||
StreamingOutputStatus,
|
||||
} from './typings/cspan';
|
||||
|
||||
export {
|
||||
spanTypeConfigMap,
|
||||
botEnvConfigMap,
|
||||
spanCategoryConfigMap,
|
||||
streamingOutputStatusConfigMap,
|
||||
} from './config/cspan';
|
||||
|
||||
export {
|
||||
isBatchSpanType,
|
||||
isVisibleSpan,
|
||||
checkIsBatchBasicCSpan,
|
||||
getTokens,
|
||||
getSpanProp,
|
||||
} from './utils/cspan';
|
||||
|
||||
export { span2CSpan } from './utils/cspan-transform';
|
||||
|
||||
export { fieldItemHandlers, type FieldItem } from './utils/field-item-handler';
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
interface SpanTypeConfig {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/** key: SpanType */
|
||||
export type SpanTypeConfigMap = Record<number, SpanTypeConfig | undefined>;
|
||||
|
||||
interface SpanCategoryConfig {
|
||||
label: string;
|
||||
}
|
||||
|
||||
/** key: SpanCategory */
|
||||
export type SpanCategoryConfigMap = Record<
|
||||
number,
|
||||
SpanCategoryConfig | undefined
|
||||
>;
|
||||
|
||||
interface SpanStatusConfig {
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface SpanStatusConfigMap {
|
||||
[x: number]: SpanStatusConfig | undefined;
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type Span,
|
||||
type AttrUserInput,
|
||||
type AttrInvokeAgent,
|
||||
type AttrCode,
|
||||
type AttrCodeBatch,
|
||||
type AttrCondition,
|
||||
type AttrKnowledge,
|
||||
type AttrLLMBatchCall,
|
||||
type AttrLLMCall,
|
||||
type AttrPluginTool,
|
||||
type AttrPluginToolBatch,
|
||||
type AttrRestartAgent,
|
||||
type AttrSwitchAgent,
|
||||
type AttrWorkflow,
|
||||
type AttrWorkflowEnd,
|
||||
type SpanCategory,
|
||||
type SpanType,
|
||||
type SpanStatus,
|
||||
type AttrChain,
|
||||
type AttrWorkflowMessage,
|
||||
type AttrCard,
|
||||
type AttrWorkflowLLMCall,
|
||||
type AttrWorkflowCode,
|
||||
type AttrWorkflowCodeBatch,
|
||||
type AttrWorkflowCondition,
|
||||
type AttrWorkflowKnowledge,
|
||||
type AttrWorkflowLLMBatchCall,
|
||||
type AttrWorkflowPluginTool,
|
||||
type AttrWorkflowPluginToolBatch,
|
||||
type AttrBWStart,
|
||||
type AttrBWEnd,
|
||||
type AttrBWBatch,
|
||||
type AttrBWLoop,
|
||||
type AttrBWCondition,
|
||||
type AttrBWLLM,
|
||||
type AttrBWParallel,
|
||||
type AttrBWScript,
|
||||
type AttrBWVariable,
|
||||
type AttrBWCallFlow,
|
||||
type AttrBWConnector,
|
||||
type AttrHook,
|
||||
} from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
type CSpanCommonProp = Pick<
|
||||
Span,
|
||||
'trace_id' | 'id' | 'parent_id' | 'name' | 'type' | 'status'
|
||||
> & {
|
||||
start_time: number; // The default is Int64, which is inconvenient to use
|
||||
latency: number; // The default is Int64, which is inconvenient to use
|
||||
category?: SpanCategory; // Empty only when Meta fails to load
|
||||
input_tokens_sum?: number; // Extended field to store the sum of sub-node input_tokens
|
||||
output_tokens_sum?: number; // Extended field to store the sum of sub-node output_tokens
|
||||
};
|
||||
|
||||
type GenCSpan<T> = CSpanCommonProp & {
|
||||
extra?: T;
|
||||
};
|
||||
|
||||
export type CSpanAttrUserInput = GenCSpan<AttrUserInput>;
|
||||
export type CSpanAttrInvokeAgent = GenCSpan<AttrInvokeAgent>;
|
||||
export type CSpanAttrRestartAgent = GenCSpan<AttrRestartAgent>;
|
||||
export type CSpanAttrSwitchAgent = GenCSpan<AttrSwitchAgent>;
|
||||
export type CSpanAttrLLMCall = GenCSpan<AttrLLMCall>;
|
||||
export type CSpanAttrWorkflowLLMCall = GenCSpan<AttrWorkflowLLMCall>;
|
||||
export type CSpanAttrLLMBatchCall = GenCSpan<AttrLLMBatchCall>;
|
||||
export type CSpanAttrWorkflowLLMBatchCall = GenCSpan<AttrWorkflowLLMBatchCall>;
|
||||
export type CSpanAttrWorkflow = GenCSpan<AttrWorkflow>;
|
||||
export type CSpanAttrWorkflowEnd = GenCSpan<AttrWorkflowEnd>;
|
||||
export type CSpanAttrCode = GenCSpan<AttrCode>;
|
||||
export type CSpanAttrWorkflowCode = GenCSpan<AttrWorkflowCode>;
|
||||
export type CSpanAttrCodeBatch = GenCSpan<AttrCodeBatch>;
|
||||
export type CSpanAttrWorkflowCodeBatch = GenCSpan<AttrWorkflowCodeBatch>;
|
||||
export type CSpanAttrCondition = GenCSpan<AttrCondition>;
|
||||
export type CSpanAttrWorkflowCondition = GenCSpan<AttrWorkflowCondition>;
|
||||
export type CSpanAttrPluginTool = GenCSpan<AttrPluginTool>;
|
||||
export type CSpanAttrWorkflowPluginTool = GenCSpan<AttrWorkflowPluginTool>;
|
||||
export type CSpanAttrPluginToolBatch = GenCSpan<AttrPluginToolBatch>;
|
||||
export type CSpanAttrWorkflowPluginToolBatch =
|
||||
GenCSpan<AttrWorkflowPluginToolBatch>;
|
||||
export type CSpanAttrKnowledge = GenCSpan<AttrKnowledge>;
|
||||
export type CSpanAttrWorkflowKnowledge = GenCSpan<AttrWorkflowKnowledge>;
|
||||
export type CSpanAttrChain = GenCSpan<AttrChain>;
|
||||
export type CSpanAttrCard = GenCSpan<AttrCard>;
|
||||
export type CSpanAttrWorkflowMessage = GenCSpan<AttrWorkflowMessage>;
|
||||
export type CSpanAttrHook = GenCSpan<AttrHook>;
|
||||
export type CSpanAttrBWStart = GenCSpan<AttrBWStart>;
|
||||
export type CSpanAttrBWEnd = GenCSpan<AttrBWEnd>;
|
||||
export type CSpanAttrBWBatch = GenCSpan<AttrBWBatch>;
|
||||
export type CSpanAttrBWLoop = GenCSpan<AttrBWLoop>;
|
||||
export type CSpanAttrBWCondition = GenCSpan<AttrBWCondition>;
|
||||
export type CSpanAttrBWLLM = GenCSpan<AttrBWLLM>;
|
||||
export type CSpanAttrBWParallel = GenCSpan<AttrBWParallel>;
|
||||
export type CSpanAttrBWScript = GenCSpan<AttrBWScript>;
|
||||
export type CSpanAttrBWVariable = GenCSpan<AttrBWVariable>;
|
||||
export type CSpanAttrBWCallFlow = GenCSpan<AttrBWCallFlow>;
|
||||
export type CSpanAttrBWConnector = GenCSpan<AttrBWConnector>;
|
||||
|
||||
export type CSpanSingle =
|
||||
| CSpanAttrUserInput
|
||||
| CSpanAttrInvokeAgent
|
||||
| CSpanAttrRestartAgent
|
||||
| CSpanAttrSwitchAgent
|
||||
| CSpanAttrLLMCall
|
||||
| CSpanAttrLLMBatchCall
|
||||
| CSpanAttrWorkflow
|
||||
| CSpanAttrWorkflowEnd
|
||||
| CSpanAttrCode
|
||||
| CSpanAttrCodeBatch
|
||||
| CSpanAttrCondition
|
||||
| CSpanAttrPluginTool
|
||||
| CSpanAttrPluginToolBatch
|
||||
| CSpanAttrKnowledge
|
||||
| CSpanAttrChain
|
||||
| CSpanAttrCard
|
||||
| CSpanAttrWorkflowMessage
|
||||
| CSpanAttrWorkflowLLMCall
|
||||
| CSpanAttrWorkflowLLMBatchCall
|
||||
| CSpanAttrWorkflowCode
|
||||
| CSpanAttrWorkflowCodeBatch
|
||||
| CSpanAttrWorkflowCondition
|
||||
| CSpanAttrWorkflowPluginTool
|
||||
| CSpanAttrWorkflowPluginToolBatch
|
||||
| CSpanAttrWorkflowKnowledge
|
||||
| CSpanAttrHook
|
||||
| CSpanAttrBWStart
|
||||
| CSpanAttrBWEnd
|
||||
| CSpanAttrBWBatch
|
||||
| CSpanAttrBWLoop
|
||||
| CSpanAttrBWCondition
|
||||
| CSpanAttrBWLLM
|
||||
| CSpanAttrBWParallel
|
||||
| CSpanAttrBWScript
|
||||
| CSpanAttrBWVariable
|
||||
| CSpanAttrBWCallFlow
|
||||
| CSpanAttrBWConnector;
|
||||
|
||||
export type CSpanSingleForBatch =
|
||||
| CSpanAttrLLMBatchCall
|
||||
| CSpanAttrWorkflowLLMBatchCall
|
||||
| CSpanAttrCodeBatch
|
||||
| CSpanAttrWorkflowCodeBatch
|
||||
| CSpanAttrPluginToolBatch
|
||||
| CSpanAttrWorkflowPluginToolBatch;
|
||||
|
||||
export type CSPanBatch = CSpanCommonProp & {
|
||||
spans: CSpanSingleForBatch[];
|
||||
workflow_node_id?: string;
|
||||
};
|
||||
|
||||
export type CSpan = CSpanSingle | CSPanBatch;
|
||||
|
||||
type AttrUserInputExtra = Partial<CSpanAttrUserInput['extra']> & {
|
||||
dialog_round?: number;
|
||||
model?: string;
|
||||
input_tokens?: number;
|
||||
output_tokens?: number;
|
||||
};
|
||||
|
||||
export type CTrace = Omit<CSpanAttrUserInput, 'extra' | 'status'> & {
|
||||
status?: SpanStatus;
|
||||
extra?: AttrUserInputExtra;
|
||||
};
|
||||
|
||||
export type SpanCategoryMeta = Record<SpanCategory, SpanType[] | undefined>;
|
||||
|
||||
/** key: SpanCategory */
|
||||
export type SpanCategoryMap = Record<number, SpanCategory>;
|
||||
|
||||
export enum StreamingOutputStatus {
|
||||
OPEN = 'open',
|
||||
CLOSE = 'close',
|
||||
UNDEFINED = 'undefined',
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type CSpan } from './cspan';
|
||||
|
||||
export enum DataSourceTypeEnum {
|
||||
SpanData = 'SpanData',
|
||||
TraceId = 'TraceId',
|
||||
}
|
||||
|
||||
export interface DataSource {
|
||||
// When the value is traceId, the component queries SpanData based on traceId.
|
||||
type: DataSourceTypeEnum;
|
||||
spanData?: CSpan[]; // When type is spanData, unique fields
|
||||
traceId?: string; // When type is traceId, unique field
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { omit } from 'lodash-es';
|
||||
import {
|
||||
SpanCategory,
|
||||
SpanStatus,
|
||||
SpanType,
|
||||
} from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import { type CSpan } from '../typings/cspan';
|
||||
import { rootBreakSpanId } from '../constant';
|
||||
import {
|
||||
spanStatusConfigMap as defaultSpanStatusConfigMap,
|
||||
spanTypeConfigMap as defaultSpanTypeConfigMap,
|
||||
} from '../config/cspan';
|
||||
import { genVirtualStart, isVisibleSpan } from './cspan';
|
||||
|
||||
export type SpanNode = CSpan & {
|
||||
parent?: SpanNode;
|
||||
children?: SpanNode[];
|
||||
};
|
||||
|
||||
export const getSpanDataByTraceId = (traceId: string): CSpan[] => [];
|
||||
|
||||
// Get the heel node of the tree
|
||||
export const buildCallTrees = (
|
||||
spans: CSpan[],
|
||||
splitBatchSpan = true,
|
||||
): SpanNode[] => {
|
||||
const roots: SpanNode[] = [];
|
||||
|
||||
const map: {
|
||||
[spanId: string]: SpanNode;
|
||||
} = {};
|
||||
|
||||
spans.forEach(span => {
|
||||
const curSpan = { ...span, children: [] };
|
||||
// Batch Node
|
||||
if ('spans' in span && splitBatchSpan) {
|
||||
span.spans.forEach(subSpan => {
|
||||
map[subSpan.id] = curSpan;
|
||||
});
|
||||
} else {
|
||||
const { id: spanId } = span;
|
||||
map[spanId] = curSpan;
|
||||
}
|
||||
});
|
||||
|
||||
spans.forEach(span => {
|
||||
const { id: spanId, parent_id: parentSpanId } = span;
|
||||
const spanNode = map[spanId];
|
||||
const parentSpanNode = map[parentSpanId];
|
||||
|
||||
if (parentSpanId === '' || parentSpanNode === undefined) {
|
||||
roots.push(spanNode);
|
||||
} else {
|
||||
parentSpanNode.children = parentSpanNode.children ?? [];
|
||||
parentSpanNode.children.push(spanNode);
|
||||
spanNode.parent = parentSpanNode;
|
||||
}
|
||||
});
|
||||
|
||||
return roots;
|
||||
};
|
||||
|
||||
export const getRootSpan = (spans: SpanNode[], needBuildTrees = true) => {
|
||||
const rootSpans = needBuildTrees ? buildCallTrees(spans) : spans;
|
||||
|
||||
const startSpans: SpanNode[] = [];
|
||||
rootSpans.forEach(rootSpan => {
|
||||
const { category } = rootSpan;
|
||||
if (category === SpanCategory.Start) {
|
||||
startSpans.push(rootSpan);
|
||||
}
|
||||
});
|
||||
|
||||
// Scenarios without start: virtual one StartSpan (for multiple parties to use, flame map, tree map, detail map to ensure consistency); multiple StartSpans, take the first one
|
||||
return startSpans.length > 0 ? startSpans[0] : genVirtualStart(rootSpans);
|
||||
};
|
||||
|
||||
export const getBreakSpans = (spans: SpanNode[], needBuildTrees = true) => {
|
||||
const rootSpans = needBuildTrees ? buildCallTrees(spans) : spans;
|
||||
|
||||
const breakSpans: SpanNode[] = [];
|
||||
rootSpans.forEach(rootSpan => {
|
||||
const { category } = rootSpan;
|
||||
if (category !== SpanCategory.Start) {
|
||||
breakSpans.push(rootSpan);
|
||||
}
|
||||
});
|
||||
return breakSpans;
|
||||
};
|
||||
|
||||
export const compareByStartAt = (spanA: SpanNode, spanB: SpanNode) => {
|
||||
const startAtA = spanA.start_time;
|
||||
const startAtB = spanB.start_time;
|
||||
if (startAtA > startAtB) {
|
||||
return 1;
|
||||
} else if (startAtA < startAtB) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
export const compareByEndAt = (spanA: SpanNode, spanB: SpanNode) => {
|
||||
const endAtA = spanA.start_time + spanA.latency;
|
||||
const endAtB = spanB.start_time + spanB.latency;
|
||||
if (endAtA > endAtB) {
|
||||
return 1;
|
||||
} else if (endAtA < endAtB) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
export const getSpanTitle = (
|
||||
span: CSpan,
|
||||
spanTypeConfigMap = defaultSpanTypeConfigMap,
|
||||
) => {
|
||||
const { type, name = '' } = span;
|
||||
const typeName = spanTypeConfigMap[type]?.label ?? '';
|
||||
if (name && name !== typeName) {
|
||||
return `${typeName} ${name}`;
|
||||
} else {
|
||||
return typeName;
|
||||
}
|
||||
};
|
||||
|
||||
export const getStatusLabel = (
|
||||
span: CSpan,
|
||||
spanStatusConfigMap = defaultSpanStatusConfigMap,
|
||||
) => {
|
||||
const { status } = span;
|
||||
return spanStatusConfigMap[status]?.label ?? '';
|
||||
};
|
||||
|
||||
// When the start node does not exist, generate a virtual start node
|
||||
const getRootBreakSpan = (breakSpans: SpanNode[]): SpanNode => ({
|
||||
id: rootBreakSpanId,
|
||||
parent_id: '',
|
||||
trace_id: '',
|
||||
name: '',
|
||||
type: SpanType.UserInput,
|
||||
status: SpanStatus.Broken,
|
||||
start_time: -1,
|
||||
latency: -1,
|
||||
children: breakSpans,
|
||||
});
|
||||
|
||||
// Create a parent-child relationship according to switchAgent/reStartAgent
|
||||
const handleAgent = (spans: SpanNode[]): SpanNode[] => {
|
||||
const getAgent = (startAt: number, agents: SpanNode[]) => {
|
||||
const len = agents.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
const curAgent = agents[i];
|
||||
const nextAgent = agents[i + 1];
|
||||
const curEndAt = curAgent.start_time + curAgent.latency;
|
||||
const nextEndAt = nextAgent
|
||||
? nextAgent.start_time + nextAgent.latency
|
||||
: Number.POSITIVE_INFINITY;
|
||||
|
||||
if (startAt >= curEndAt && startAt <= nextEndAt) {
|
||||
return agents[i];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const agentSpans = spans
|
||||
.filter(
|
||||
span =>
|
||||
span.type === SpanType.SwitchAgent ||
|
||||
span.type === SpanType.RestartAgent,
|
||||
)
|
||||
.sort(compareByEndAt);
|
||||
|
||||
const normalSpans = spans
|
||||
.filter(
|
||||
span =>
|
||||
span.type !== SpanType.SwitchAgent &&
|
||||
span.type !== SpanType.RestartAgent,
|
||||
)
|
||||
.sort(compareByStartAt);
|
||||
|
||||
const rstSpans: SpanNode[] = [];
|
||||
normalSpans.forEach(span => {
|
||||
const agent = getAgent(span.start_time, agentSpans);
|
||||
if (agent) {
|
||||
agent.children = agent.children ?? [];
|
||||
agent.children?.push(span);
|
||||
span.parent = agent;
|
||||
} else {
|
||||
rstSpans.push(span);
|
||||
}
|
||||
});
|
||||
|
||||
return [...rstSpans, ...agentSpans];
|
||||
};
|
||||
|
||||
// Only special node types can be used as root nodes
|
||||
const isTreeRootSpanType = (type: SpanType) =>
|
||||
[
|
||||
SpanType.InvokeAgent,
|
||||
SpanType.Workflow,
|
||||
SpanType.LLMBatchCall,
|
||||
SpanType.LLMCall,
|
||||
SpanType.WorkflowLLMCall,
|
||||
SpanType.WorkflowLLMBatchCall,
|
||||
// All BlockWise's are put here
|
||||
SpanType.BWStart,
|
||||
SpanType.BWEnd,
|
||||
SpanType.BWBatch,
|
||||
SpanType.BWLoop,
|
||||
SpanType.BWCondition,
|
||||
SpanType.BWLLM,
|
||||
SpanType.BWParallel,
|
||||
SpanType.BWScript,
|
||||
SpanType.BWVariable,
|
||||
SpanType.BWCallFlow,
|
||||
SpanType.BWConnector,
|
||||
// New types all support hierarchies
|
||||
SpanType.Hook,
|
||||
].includes(type);
|
||||
|
||||
// Build TraceTree based on call tree
|
||||
const callTree2TraceTree = (rootSpan: SpanNode): SpanNode => {
|
||||
const rstSpans: SpanNode[] = [];
|
||||
const walk = (span: SpanNode) => {
|
||||
span.children?.forEach(subSpan => {
|
||||
const { type } = subSpan;
|
||||
if (isTreeRootSpanType(type)) {
|
||||
rstSpans.push(callTree2TraceTree(subSpan));
|
||||
} else {
|
||||
if (isVisibleSpan(subSpan)) {
|
||||
// The current node is added to rootSpan.children
|
||||
rstSpans.push(omit(subSpan, 'children'));
|
||||
}
|
||||
// Recursive sub-node (current node). Note: The type of hidden node should also be recursive. The current node is hidden, and its sub-node may be displayed
|
||||
walk(subSpan);
|
||||
}
|
||||
});
|
||||
};
|
||||
walk(rootSpan);
|
||||
|
||||
return {
|
||||
...rootSpan,
|
||||
children: handleAgent(rstSpans),
|
||||
};
|
||||
};
|
||||
|
||||
export const buildTraceTree = (spans: SpanNode[], splitBatchSpan?: boolean) => {
|
||||
// 1. According to spans, assemble call trees
|
||||
const callTrees = buildCallTrees(spans, splitBatchSpan);
|
||||
|
||||
// 2. Generate the gastSpan
|
||||
const startSpan: SpanNode = getRootSpan(callTrees, false);
|
||||
|
||||
// 3. Get the break node (all non-start root nodes are breakSpan)
|
||||
const breakSpans: SpanNode[] = getBreakSpans(callTrees, false);
|
||||
|
||||
// 4. According to the call tree, generate the Tree in PRD (ie, the Tree in PRD)
|
||||
const treeStartSpan = callTree2TraceTree(startSpan);
|
||||
|
||||
if (breakSpans.length > 0) {
|
||||
// 5. Mount all breakSpans under the rootBreakSpan node
|
||||
const breakSpan: SpanNode = getRootBreakSpan(breakSpans);
|
||||
// 6. Generate TraceTree according to the call tree
|
||||
const treeBreakSpan = callTree2TraceTree(breakSpan);
|
||||
|
||||
// 7. Hang treeBreakSpan under treeStartSpan
|
||||
treeStartSpan.children = treeStartSpan.children ?? [];
|
||||
treeStartSpan.children.push(treeBreakSpan);
|
||||
treeBreakSpan.parent = treeStartSpan;
|
||||
}
|
||||
return treeStartSpan;
|
||||
};
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { pick, uniqBy } from 'lodash-es';
|
||||
import {
|
||||
type Span,
|
||||
SpanStatus,
|
||||
type SpanCategory,
|
||||
} from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import {
|
||||
type CSpanSingle,
|
||||
type CSpan,
|
||||
type CSPanBatch,
|
||||
type SpanCategoryMeta,
|
||||
type SpanCategoryMap,
|
||||
type CSpanSingleForBatch,
|
||||
} from '../typings/cspan';
|
||||
import { isBatchSpanType } from './cspan';
|
||||
|
||||
const compareByStartAt = (
|
||||
a: CSpanSingleForBatch,
|
||||
b: CSpanSingleForBatch,
|
||||
): number => {
|
||||
if (a.start_time > b.start_time) {
|
||||
return 1;
|
||||
} else if (a.start_time < b.start_time) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const compareByTaskIndex = (
|
||||
a: CSpanSingleForBatch,
|
||||
b: CSpanSingleForBatch,
|
||||
): number => {
|
||||
const taskIndexA = a.extra?.task_index ?? 0;
|
||||
const taskIndexB = a.extra?.task_index ?? 0;
|
||||
if (taskIndexA > taskIndexB) {
|
||||
return 1;
|
||||
} else if (taskIndexA < taskIndexB) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const getStartAtForBatch = (spans: CSpanSingleForBatch[]): number => {
|
||||
let startAt = Number.POSITIVE_INFINITY;
|
||||
spans.forEach(span => {
|
||||
startAt = Math.min(span.start_time, startAt);
|
||||
});
|
||||
return startAt;
|
||||
};
|
||||
|
||||
const getLatencyForBatch = (spans: CSpanSingleForBatch[]): number => {
|
||||
let endAt = Number.NEGATIVE_INFINITY;
|
||||
spans.forEach(span => {
|
||||
endAt = Math.max(span.start_time + span.latency, endAt);
|
||||
});
|
||||
const startAt = getStartAtForBatch(spans);
|
||||
return endAt - startAt;
|
||||
};
|
||||
|
||||
const getStatusForBatch = (spans: CSpanSingleForBatch[]): SpanStatus => {
|
||||
let isSuccess = true;
|
||||
spans.forEach(span => {
|
||||
if (span.status === SpanStatus.Error) {
|
||||
isSuccess = false;
|
||||
}
|
||||
});
|
||||
return isSuccess ? SpanStatus.Success : SpanStatus.Error;
|
||||
};
|
||||
|
||||
// Spans are directly polymerized to generate batchSpan.
|
||||
const genBatchSpan = function (
|
||||
spans: CSpanSingleForBatch[],
|
||||
spanCategoryMap?: SpanCategoryMap,
|
||||
): CSPanBatch | undefined {
|
||||
if (spans.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
// legality check
|
||||
const taskTotal = spans[0].extra?.task_total;
|
||||
const spans0 = spans.filter(curSpan => {
|
||||
const curTaskTotal = curSpan.extra?.task_total;
|
||||
return curTaskTotal !== taskTotal;
|
||||
});
|
||||
// taskTotal is not all equal, the data is invalid
|
||||
if (spans0.length > 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
...pick(spans[0], ['trace_id', 'id', 'parent_id', 'name', 'type']),
|
||||
category: spanCategoryMap?.[spans[0].type],
|
||||
status: getStatusForBatch(spans),
|
||||
start_time: getStartAtForBatch(spans),
|
||||
latency: getLatencyForBatch(spans),
|
||||
spans: spans.sort(compareByTaskIndex),
|
||||
workflow_node_id: spans[0].extra?.workflow_node_id,
|
||||
};
|
||||
};
|
||||
|
||||
const aggregationBatchSpan = function (
|
||||
spans: CSpanSingleForBatch[],
|
||||
spanCategoryMap?: SpanCategoryMap,
|
||||
) {
|
||||
const batchSpans: CSPanBatch[] = [];
|
||||
|
||||
// Sorting spans by workflowNodeId + type
|
||||
const map: {
|
||||
[key: string]: CSpanSingleForBatch[];
|
||||
} = {};
|
||||
spans.forEach(span => {
|
||||
const { type } = span;
|
||||
const workflowNodeId = span.extra?.workflow_node_id;
|
||||
if (!workflowNodeId) {
|
||||
return;
|
||||
}
|
||||
map[type + workflowNodeId] = map[type + workflowNodeId] ?? [];
|
||||
map[type + workflowNodeId].push(span);
|
||||
});
|
||||
|
||||
// Further classify according to time + serial number to generate CSpanBatch
|
||||
Object.keys(map).forEach(key => {
|
||||
const workflowSpans = map[key];
|
||||
|
||||
// Sort by: Time
|
||||
workflowSpans.sort(compareByStartAt);
|
||||
|
||||
// Aggregate according to task_index
|
||||
let curTaskIndexs: number[] = [];
|
||||
let curSpans: CSpanSingleForBatch[] = [];
|
||||
workflowSpans.forEach(span => {
|
||||
const taskIndex = span.extra?.task_index;
|
||||
if (taskIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (curTaskIndexs.includes(taskIndex)) {
|
||||
// If the serial number exists, open a new set.
|
||||
const batchSpan = genBatchSpan(curSpans, spanCategoryMap);
|
||||
if (batchSpan) {
|
||||
batchSpans.push(batchSpan);
|
||||
}
|
||||
curTaskIndexs = [];
|
||||
curSpans = [];
|
||||
}
|
||||
curTaskIndexs.push(taskIndex);
|
||||
curSpans.push(span);
|
||||
});
|
||||
|
||||
const batchSpan = genBatchSpan(curSpans, spanCategoryMap);
|
||||
if (batchSpan) {
|
||||
batchSpans.push(batchSpan);
|
||||
}
|
||||
});
|
||||
|
||||
return batchSpans;
|
||||
};
|
||||
|
||||
/**
|
||||
* Process raw Span (unify extra node fields to extra)
|
||||
* @param span Span
|
||||
* @returns CSpanSingle
|
||||
*/
|
||||
// eslint-disable-next-line complexity -- too many parameters
|
||||
export const span2CSpan = function (
|
||||
span: Span,
|
||||
spanCategoryMap?: SpanCategoryMap,
|
||||
): CSpanSingle {
|
||||
const cspan: CSpanSingle = {
|
||||
trace_id: span.trace_id,
|
||||
id: span.id,
|
||||
parent_id: span.parent_id,
|
||||
name: span.name,
|
||||
type: span.type,
|
||||
start_time: Number(span.start_time),
|
||||
latency: Number(span.latency),
|
||||
status: span.status,
|
||||
category: spanCategoryMap?.[span.type],
|
||||
};
|
||||
cspan.extra =
|
||||
span.attr_user_input ??
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(span as any).attr_user_input_v2 ??
|
||||
span.attr_invoke_agent ??
|
||||
span.attr_restart_agent ??
|
||||
span.attr_switch_agent ??
|
||||
span.attr_llm_call ??
|
||||
span.attr_workflow_llm_call ??
|
||||
span.attr_llm_batch_call ??
|
||||
span.attr_workflow_llm_batch_call ??
|
||||
span.attr_workflow ??
|
||||
span.attr_workflow_end ??
|
||||
span.attr_code ??
|
||||
span.attr_workflow_code ??
|
||||
span.attr_code_batch ??
|
||||
span.attr_workflow_code_batch ??
|
||||
span.attr_condition ??
|
||||
span.attr_workflow_condition ??
|
||||
span.attr_plugin_tool ??
|
||||
span.attr_workflow_plugin_tool ??
|
||||
span.attr_plugin_tool_batch ??
|
||||
span.attr_workflow_plugin_tool_batch ??
|
||||
span.attr_knowledge ??
|
||||
span.attr_workflow_knowledge ??
|
||||
span.attr_card ??
|
||||
span.attr_workflow_message ??
|
||||
span.attr_chain ??
|
||||
span.attr_hook ??
|
||||
span.attr_bw_start ??
|
||||
span.attr_bw_end ??
|
||||
span.attr_bw_batch ??
|
||||
span.attr_bw_loop ??
|
||||
span.attr_bw_condition ??
|
||||
span.attr_bw_llm ??
|
||||
span.attr_bw_parallel ??
|
||||
span.attr_bw_variable ??
|
||||
span.attr_bw_call_flow ??
|
||||
span.attr_bw_connector ??
|
||||
span.attr_bw_script;
|
||||
return cspan;
|
||||
};
|
||||
|
||||
const genSpanCategoryMap = (
|
||||
spanCategoryMeta: SpanCategoryMeta,
|
||||
): SpanCategoryMap => {
|
||||
const map: SpanCategoryMap = {};
|
||||
Object.keys(spanCategoryMeta).forEach(key => {
|
||||
const spanCategoryValue = Number(key) as SpanCategory;
|
||||
const spanTypes = spanCategoryMeta[spanCategoryValue];
|
||||
spanTypes?.forEach(spanTypeValue => {
|
||||
map[spanTypeValue] = spanCategoryValue;
|
||||
});
|
||||
});
|
||||
return map as SpanCategoryMap;
|
||||
};
|
||||
|
||||
export const spans2CSpans = function (
|
||||
spans: Span[],
|
||||
spanCategoryMeta?: SpanCategoryMeta,
|
||||
): CSpan[] {
|
||||
const spanCategoryMap = spanCategoryMeta
|
||||
? genSpanCategoryMap(spanCategoryMeta)
|
||||
: undefined;
|
||||
|
||||
// Deduplicate according to span.id
|
||||
const uniqSpans = uniqBy(spans, 'id');
|
||||
|
||||
// Span -> CSpanSingle
|
||||
const cSpans = uniqSpans.map(span => span2CSpan(span, spanCategoryMap));
|
||||
|
||||
const singleCSpans = cSpans.filter(({ type }) => !isBatchSpanType(type));
|
||||
// CSpanSingle[] -> CSpanBatch[]
|
||||
const batchCSpans = aggregationBatchSpan(
|
||||
cSpans.filter(({ type }) => isBatchSpanType(type)) as CSpanSingleForBatch[],
|
||||
spanCategoryMap,
|
||||
);
|
||||
|
||||
return [...singleCSpans, ...batchCSpans];
|
||||
};
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
SpanCategory,
|
||||
SpanStatus,
|
||||
SpanType,
|
||||
} from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import {
|
||||
type CTrace,
|
||||
type CSPanBatch,
|
||||
type CSpan,
|
||||
type CSpanSingle,
|
||||
} from '../typings/cspan';
|
||||
import { virtualStartSpanId } from '../constant';
|
||||
|
||||
export const getSpanProp = (span: CSpan | CTrace, key: string) => {
|
||||
if (checkIsBatchBasicCSpan(span)) {
|
||||
const batchSpan = span as CSPanBatch;
|
||||
return (
|
||||
batchSpan[key as keyof CSPanBatch] ??
|
||||
batchSpan.spans[0]?.extra?.[key as keyof CSPanBatch['spans'][0]['extra']]
|
||||
);
|
||||
} else {
|
||||
const singleSpan = span as CSpanSingle;
|
||||
return (
|
||||
singleSpan[key as keyof CSpanSingle] ??
|
||||
singleSpan.extra?.[key as keyof CSpanSingle['extra']]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const getTokens = (
|
||||
span: CSpan,
|
||||
): {
|
||||
input_tokens?: number;
|
||||
output_tokens?: number;
|
||||
} => {
|
||||
if ('spans' in span) {
|
||||
const spanBatch = span as CSPanBatch;
|
||||
let inputTokensSum = 0;
|
||||
let outputTokensSum = 0;
|
||||
spanBatch.spans.forEach(subSpan => {
|
||||
const inputTokens = subSpan?.extra?.input_tokens ?? 0;
|
||||
const outputTokens = subSpan?.extra?.output_tokens ?? 0;
|
||||
inputTokensSum += inputTokens;
|
||||
outputTokensSum += outputTokens;
|
||||
});
|
||||
return {
|
||||
input_tokens: inputTokensSum,
|
||||
output_tokens: outputTokensSum,
|
||||
};
|
||||
} else if (
|
||||
span.type === SpanType.UserInput ||
|
||||
span.type === SpanType.UserInputV2 ||
|
||||
span.type === SpanType.Workflow
|
||||
) {
|
||||
// SingleSpan Node - Workflow
|
||||
const inputTokens = getSpanProp(span, 'input_tokens_sum') as number;
|
||||
const outputTokens = getSpanProp(span, 'output_tokens_sum') as number;
|
||||
|
||||
return {
|
||||
input_tokens: inputTokens,
|
||||
output_tokens: outputTokens,
|
||||
};
|
||||
} else {
|
||||
// SingleSpan Node - Non-workflow Node
|
||||
const inputTokens = getSpanProp(span, 'input_tokens') as number;
|
||||
const outputTokens = getSpanProp(span, 'output_tokens') as number;
|
||||
|
||||
return {
|
||||
input_tokens: inputTokens,
|
||||
output_tokens: outputTokens,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const isBatchSpanType = (type: SpanType): boolean => {
|
||||
const whites = [
|
||||
SpanType.LLMBatchCall,
|
||||
SpanType.WorkflowLLMBatchCall,
|
||||
SpanType.PluginToolBatch,
|
||||
SpanType.WorkflowPluginToolBatch,
|
||||
SpanType.CodeBatch,
|
||||
SpanType.WorkflowCodeBatch,
|
||||
];
|
||||
return whites.includes(type);
|
||||
};
|
||||
|
||||
export const checkIsBatchBasicCSpan = (span: CSpan | CTrace) => 'spans' in span;
|
||||
|
||||
export const isVisibleSpan = (span: CSpan) => {
|
||||
const whites = [
|
||||
SpanType.Unknown,
|
||||
SpanType.UserInput,
|
||||
SpanType.UserInputV2,
|
||||
SpanType.ThirdParty,
|
||||
SpanType.ScheduledTasks,
|
||||
SpanType.OpenDialog,
|
||||
SpanType.InvokeAgent,
|
||||
SpanType.RestartAgent,
|
||||
SpanType.SwitchAgent,
|
||||
SpanType.LLMCall,
|
||||
SpanType.WorkflowLLMCall,
|
||||
SpanType.LLMBatchCall,
|
||||
SpanType.WorkflowLLMBatchCall,
|
||||
SpanType.Workflow,
|
||||
SpanType.WorkflowStart,
|
||||
SpanType.WorkflowEnd,
|
||||
SpanType.PluginTool,
|
||||
SpanType.WorkflowPluginTool,
|
||||
SpanType.PluginToolBatch,
|
||||
SpanType.WorkflowPluginToolBatch,
|
||||
SpanType.Knowledge,
|
||||
SpanType.WorkflowKnowledge,
|
||||
SpanType.Code,
|
||||
SpanType.WorkflowCode,
|
||||
SpanType.CodeBatch,
|
||||
SpanType.WorkflowCodeBatch,
|
||||
SpanType.Condition,
|
||||
SpanType.WorkflowCondition,
|
||||
SpanType.Card,
|
||||
SpanType.WorkflowMessage,
|
||||
SpanType.Hook,
|
||||
SpanType.BWStart,
|
||||
SpanType.BWEnd,
|
||||
SpanType.BWBatch,
|
||||
SpanType.BWLoop,
|
||||
SpanType.BWCondition,
|
||||
SpanType.BWLLM,
|
||||
SpanType.BWParallel,
|
||||
SpanType.BWScript,
|
||||
SpanType.BWVariable,
|
||||
SpanType.BWCallFlow,
|
||||
SpanType.BWConnector,
|
||||
];
|
||||
|
||||
return whites.includes(span.type);
|
||||
};
|
||||
|
||||
export const genVirtualStart = (spans: CSpan[]): CSpan => {
|
||||
let startAt = Number.POSITIVE_INFINITY;
|
||||
let endAt = Number.NEGATIVE_INFINITY;
|
||||
spans.forEach(span => {
|
||||
startAt = Math.min(startAt, span.start_time);
|
||||
endAt = Math.max(endAt, span.start_time + span.latency);
|
||||
});
|
||||
if (
|
||||
startAt === Number.POSITIVE_INFINITY ||
|
||||
endAt === Number.NEGATIVE_INFINITY
|
||||
) {
|
||||
startAt = 0;
|
||||
endAt = 0;
|
||||
}
|
||||
return {
|
||||
id: virtualStartSpanId,
|
||||
parent_id: '',
|
||||
trace_id: '',
|
||||
name: '',
|
||||
type: SpanType.UserInput,
|
||||
category: SpanCategory.Start,
|
||||
status: SpanStatus.Unknown,
|
||||
start_time: startAt,
|
||||
latency: endAt - startAt,
|
||||
};
|
||||
};
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import {
|
||||
type CSpanAttrInvokeAgent,
|
||||
type CSpan,
|
||||
type StreamingOutputStatus,
|
||||
} from '../typings/cspan';
|
||||
import {
|
||||
botEnvConfigMap,
|
||||
spanCategoryConfigMap,
|
||||
spanTypeConfigMap,
|
||||
streamingOutputStatusConfigMap,
|
||||
} from '../config/cspan';
|
||||
import { formatTime } from './format-time';
|
||||
import { getSpanProp } from './cspan';
|
||||
|
||||
export interface FieldItem {
|
||||
key?: string | ReactNode;
|
||||
value?: string | number | boolean | ReactNode;
|
||||
}
|
||||
|
||||
const getFieldCategory = (span: CSpan): FieldItem => {
|
||||
const { category } = span;
|
||||
const categoryConfig =
|
||||
category !== undefined ? spanCategoryConfigMap[category] : undefined;
|
||||
return {
|
||||
key: I18n.t('analytic_query_type'),
|
||||
value: categoryConfig?.label,
|
||||
};
|
||||
};
|
||||
|
||||
const getFieldType = (span: CSpan): FieldItem => {
|
||||
const { type } = span;
|
||||
const typeConfig = spanTypeConfigMap[type];
|
||||
return {
|
||||
key: I18n.t('analytic_query_subtype'),
|
||||
value: typeConfig?.label,
|
||||
};
|
||||
};
|
||||
|
||||
const getFieldOS = (span: CSpan): FieldItem => {
|
||||
const os = getSpanProp(span, 'os');
|
||||
const osVersion = getSpanProp(span, 'os_version');
|
||||
|
||||
return {
|
||||
key: I18n.t('analytic_query_os'),
|
||||
value: os ? `${os}${osVersion ? ` ${osVersion}` : ''}` : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const getFieldLatency = (span: CSpan): FieldItem => {
|
||||
const { latency } = span;
|
||||
return {
|
||||
key: I18n.t('analytic_query_latency'),
|
||||
value: latency !== undefined ? `${latency}ms` : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const getFieldName = (span: CSpan): FieldItem => {
|
||||
const { name } = span;
|
||||
return {
|
||||
key: I18n.t('analytic_query_name'),
|
||||
value: name,
|
||||
};
|
||||
};
|
||||
|
||||
const getFieldOffline = (span: CSpan): FieldItem => {
|
||||
const botEnv = getSpanProp(span, 'bot_env') as string;
|
||||
const botEnvConfig =
|
||||
botEnv !== undefined ? botEnvConfigMap[botEnv] : undefined;
|
||||
return {
|
||||
key: I18n.t('analytic_query_env'),
|
||||
value: botEnvConfig?.label,
|
||||
};
|
||||
};
|
||||
|
||||
const getFieldStartTime = (span: CSpan): FieldItem => {
|
||||
const { start_time } = span;
|
||||
return {
|
||||
key: I18n.t('analytic_query_starttime'),
|
||||
value: formatTime(start_time),
|
||||
};
|
||||
};
|
||||
|
||||
const getFieldEndTime = (span: CSpan): FieldItem => {
|
||||
const startAt = span?.start_time;
|
||||
const latency = span?.latency;
|
||||
return {
|
||||
key: I18n.t('analytic_query_endtime'),
|
||||
value:
|
||||
startAt !== undefined && latency !== undefined
|
||||
? formatTime(Number(startAt) + Number(latency))
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const getFieldCallType = (span: CSpan): FieldItem => ({
|
||||
key: I18n.t('analytic_query_calltype'),
|
||||
value: getSpanProp(span, 'call_type') as string,
|
||||
});
|
||||
|
||||
const getFieldAgentType = (_span: CSpan): FieldItem => {
|
||||
const span = _span as CSpanAttrInvokeAgent;
|
||||
return {
|
||||
key: I18n.t('analytic_query_agenttype'),
|
||||
value: span.extra?.agent_type,
|
||||
};
|
||||
};
|
||||
|
||||
const getFieldModel = (span: CSpan): FieldItem => ({
|
||||
key: I18n.t('analytic_query_model'),
|
||||
value: getSpanProp(span, 'model') as string,
|
||||
});
|
||||
|
||||
const getFieldTemperature = (span: CSpan): FieldItem => ({
|
||||
key: I18n.t('analytic_query_temperature'),
|
||||
value: getSpanProp(span, 'temperature') as string,
|
||||
});
|
||||
|
||||
const getFieldDialogRound = (_span: CSpan): FieldItem => {
|
||||
const span = _span as CSpanAttrInvokeAgent;
|
||||
return {
|
||||
key: I18n.t('analytic_query_diagloground'),
|
||||
value: span.extra?.dialog_round,
|
||||
};
|
||||
};
|
||||
|
||||
const getFieldMaxLengthResp = (span: CSpan): FieldItem => ({
|
||||
key: I18n.t('analytic_query_resmaxlen'),
|
||||
value: getSpanProp(span, 'max_length_resp') as string,
|
||||
});
|
||||
|
||||
const getFieldChannel = (span: CSpan): FieldItem => ({
|
||||
key: I18n.t('analytic_query_channel'),
|
||||
value: getSpanProp(span, 'channel') as string,
|
||||
});
|
||||
|
||||
const getFieldInputType = (span: CSpan): FieldItem => ({
|
||||
key: I18n.t('analytic_query_inputtype'),
|
||||
value: getSpanProp(span, 'query_input_method') as string,
|
||||
});
|
||||
|
||||
const getFieldInput = (span: CSpan): FieldItem => ({
|
||||
key: I18n.t('analytic_query_input'),
|
||||
value: getSpanProp(span, 'input') as string,
|
||||
});
|
||||
|
||||
const getStreamOutput = (span: CSpan): FieldItem => {
|
||||
const value = getSpanProp(span, 'streaming_output') as StreamingOutputStatus;
|
||||
// Key to starling key
|
||||
return {
|
||||
key: I18n.t('query_stream_output'),
|
||||
value: streamingOutputStatusConfigMap[value]?.label,
|
||||
};
|
||||
};
|
||||
|
||||
const getCardId = (span: CSpan): FieldItem => {
|
||||
const value = getSpanProp(span, 'card_id') as string;
|
||||
return { key: I18n.t('query_card_id'), value };
|
||||
};
|
||||
|
||||
const getBranchName = (span: CSpan): FieldItem => {
|
||||
const value = getSpanProp(span, 'branch_name') as string;
|
||||
return { key: 'branch_name', value };
|
||||
};
|
||||
|
||||
const getNodeType = (span: CSpan): FieldItem => {
|
||||
const value = getSpanProp(span, 'node_type') as string;
|
||||
return { key: 'node_type', value };
|
||||
};
|
||||
|
||||
const getHookType = (span: CSpan): FieldItem => {
|
||||
const value = getSpanProp(span, 'hook_type') as string;
|
||||
return {
|
||||
key: I18n.t('codedev_hook_hook_type'),
|
||||
value,
|
||||
};
|
||||
};
|
||||
|
||||
const getHookUri = (span: CSpan): FieldItem => {
|
||||
const value = getSpanProp(span, 'hook_uri') as string;
|
||||
return {
|
||||
key: 'Hook Uri',
|
||||
value,
|
||||
};
|
||||
};
|
||||
|
||||
const getAgentId = (span: CSpan): FieldItem => {
|
||||
const value = getSpanProp(span, 'agent_id') as string;
|
||||
return {
|
||||
key: 'AgentId',
|
||||
value,
|
||||
};
|
||||
};
|
||||
|
||||
const getHookRespCode = (span: CSpan): FieldItem => {
|
||||
const value = getSpanProp(span, 'hook_resp_code')?.toString() as string;
|
||||
return {
|
||||
key: I18n.t('analytic_query_hook_resp_code'),
|
||||
value,
|
||||
};
|
||||
};
|
||||
|
||||
const getIsStream = (span: CSpan): FieldItem => {
|
||||
const value = getSpanProp(span, 'is_stream')?.toString() as string;
|
||||
return {
|
||||
key: I18n.t('query_stream_output'),
|
||||
value,
|
||||
};
|
||||
};
|
||||
|
||||
export const fieldItemHandlers = {
|
||||
category: getFieldCategory,
|
||||
type: getFieldType,
|
||||
os: getFieldOS,
|
||||
latency: getFieldLatency,
|
||||
offline: getFieldOffline,
|
||||
name: getFieldName,
|
||||
start_time: getFieldStartTime,
|
||||
end_time: getFieldEndTime,
|
||||
call_type: getFieldCallType,
|
||||
agent_type: getFieldAgentType,
|
||||
model: getFieldModel,
|
||||
temperature: getFieldTemperature,
|
||||
dialog_round: getFieldDialogRound,
|
||||
max_length_resp: getFieldMaxLengthResp,
|
||||
channel: getFieldChannel,
|
||||
input_type: getFieldInputType,
|
||||
input: getFieldInput,
|
||||
card_id: getCardId,
|
||||
stream_output: getStreamOutput,
|
||||
branch_name: getBranchName,
|
||||
node_type: getNodeType,
|
||||
hook_type: getHookType,
|
||||
hook_uri: getHookUri,
|
||||
agent_id: getAgentId,
|
||||
hook_resp_code: getHookRespCode,
|
||||
is_stream: getIsStream,
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export const formatTime = (timestamp?: number | string) =>
|
||||
dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss');
|
||||
@@ -0,0 +1 @@
|
||||
公共API目录, 基于bam接口的二次封装
|
||||
@@ -0,0 +1 @@
|
||||
公共样式目录
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
declare module '*.less' {
|
||||
const resource: { [key: string]: string };
|
||||
export = resource;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
公共类型目录
|
||||
@@ -0,0 +1 @@
|
||||
公共util目录
|
||||
Reference in New Issue
Block a user