feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
67
frontend/packages/workflow/test-run-next/trace/README.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# @coze-workflow/test-run-trace
|
||||
|
||||
Workflow TestRun Form
|
||||
|
||||
## Overview
|
||||
|
||||
This package is part of the Coze Studio monorepo and provides workflow functionality. It includes component.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Installation
|
||||
|
||||
Add this package to your `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@coze-workflow/test-run-trace": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
rush update
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```typescript
|
||||
import { /* exported functions/components */ } from '@coze-workflow/test-run-trace';
|
||||
|
||||
// Example usage
|
||||
// TODO: Add specific usage examples
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Component
|
||||
|
||||
## API Reference
|
||||
|
||||
### Exports
|
||||
|
||||
- `TraceListPanel`
|
||||
- `TraceDetailPanel`
|
||||
|
||||
|
||||
For detailed API documentation, please refer to the TypeScript definitions.
|
||||
|
||||
## Development
|
||||
|
||||
This package is built with:
|
||||
|
||||
- TypeScript
|
||||
- Modern JavaScript
|
||||
- Vitest for testing
|
||||
- ESLint for code quality
|
||||
|
||||
## Contributing
|
||||
|
||||
This package is part of the Coze Studio monorepo. Please follow the monorepo contribution guidelines.
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["./dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dupCheck": {
|
||||
"ignoreGlobPatterns": ["src/**/*"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
const { defineConfig } = require('@coze-arch/eslint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
packageRoot: __dirname,
|
||||
preset: 'web',
|
||||
rules: {
|
||||
'@coze-arch/max-line-per-function': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@coze-arch/zustand/prefer-shallow': 'off',
|
||||
'@coze-arch/no-deep-relative-import': 'off',
|
||||
'@typescript-eslint/naming-convention': 'off',
|
||||
},
|
||||
});
|
||||
47
frontend/packages/workflow/test-run-next/trace/package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "@coze-workflow/test-run-trace",
|
||||
"version": "0.0.1",
|
||||
"description": "Workflow TestRun Form",
|
||||
"author": "jiangxujin@bytedance.com",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "exit 0",
|
||||
"lint": "eslint ./ --cache",
|
||||
"test": "exit 0",
|
||||
"test:cov": "exit 0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coze-arch/bot-api": "workspace:*",
|
||||
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
|
||||
"@coze-arch/i18n": "workspace:*",
|
||||
"@coze-workflow/base": "workspace:*",
|
||||
"@coze-workflow/test-run-shared": "workspace:*",
|
||||
"@textea/json-viewer": "^3.0.0",
|
||||
"@visactor/vgrammar": "0.12.5-alpha.4",
|
||||
"ahooks": "^3.7.8",
|
||||
"clsx": "^1.2.1",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"dayjs": "^1.11.7",
|
||||
"json-bigint": "~1.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@coze-arch/bot-typings": "workspace:*",
|
||||
"@coze-arch/eslint-config": "workspace:*",
|
||||
"@coze-arch/ts-config": "workspace:*",
|
||||
"@emotion/react": "11.11.1",
|
||||
"@emotion/styled": "11.11.0",
|
||||
"@mui/material": "^5",
|
||||
"@types/lodash-es": "^4.17.10",
|
||||
"@types/react": "18.2.37",
|
||||
"@types/react-dom": "18.2.15",
|
||||
"react": "~18.2.0",
|
||||
"react-dom": "~18.2.0",
|
||||
"typescript": "~5.8.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 React, { useMemo } from 'react';
|
||||
|
||||
import { IconCozFocus } from '@coze-arch/coze-design/icons';
|
||||
import { IconButton } from '@coze-arch/coze-design';
|
||||
import { type Span } from '@coze-arch/bot-api/workflow_api';
|
||||
|
||||
import { getStrFromSpan } from '../../utils';
|
||||
|
||||
export const FocusButton: React.FC<{
|
||||
span: Span;
|
||||
onClick: (span: Span) => void;
|
||||
}> = ({ span, onClick }) => {
|
||||
const nodeId = useMemo(
|
||||
() => getStrFromSpan(span, 'workflow_node_id'),
|
||||
[span],
|
||||
);
|
||||
|
||||
if (!nodeId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
icon={<IconCozFocus />}
|
||||
size="mini"
|
||||
onClick={() => onClick(span)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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 { FocusButton } from './focus-button';
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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 React, { useMemo } from 'react';
|
||||
|
||||
import { clsx } from 'clsx';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
IconCozCheckMarkCircleFillPalette,
|
||||
IconCozCrossCircleFillPalette,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import { Tag } from '@coze-arch/coze-design';
|
||||
|
||||
interface StatusTagProps {
|
||||
status?: number;
|
||||
className?: string;
|
||||
type?: 'normal' | 'icon';
|
||||
}
|
||||
|
||||
export const StatusIcon: React.FC<{ status?: number; className?: string }> = ({
|
||||
status,
|
||||
className,
|
||||
}) =>
|
||||
status === 0 ? (
|
||||
<IconCozCheckMarkCircleFillPalette
|
||||
className={clsx(className, 'coz-fg-hglt-green')}
|
||||
/>
|
||||
) : (
|
||||
<IconCozCrossCircleFillPalette
|
||||
className={clsx(className, 'coz-fg-hglt-red')}
|
||||
/>
|
||||
);
|
||||
|
||||
export const StatusTag: React.FC<StatusTagProps> = ({
|
||||
status,
|
||||
className,
|
||||
type = 'normal',
|
||||
}) => {
|
||||
const children = useMemo(() => {
|
||||
if (type === 'icon') {
|
||||
return null;
|
||||
}
|
||||
return status === 0
|
||||
? I18n.t('debug_asyn_task_task_status_success')
|
||||
: I18n.t('debug_asyn_task_task_status_failed');
|
||||
}, [status, type]);
|
||||
|
||||
return (
|
||||
<Tag
|
||||
prefixIcon={
|
||||
status === 0 ? (
|
||||
<IconCozCheckMarkCircleFillPalette />
|
||||
) : (
|
||||
<IconCozCrossCircleFillPalette />
|
||||
)
|
||||
}
|
||||
color={status === 0 ? 'green' : 'red'}
|
||||
className={className}
|
||||
size="mini"
|
||||
>
|
||||
{children}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
@@ -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 { TraceDetailPanel } from './trace-detail-panel';
|
||||
@@ -0,0 +1,12 @@
|
||||
.pay-block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 2px;
|
||||
}
|
||||
|
||||
.pay-blocks {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 2px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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 { Divider, Typography } from '@coze-arch/coze-design';
|
||||
|
||||
import styles from './pay-block.module.less';
|
||||
|
||||
interface PayBlockProps {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const PayBlock: React.FC<PayBlockProps> = ({ label, value }) => (
|
||||
<div className={styles['pay-block']}>
|
||||
<Typography.Text type="secondary" size="small">
|
||||
{label}:
|
||||
</Typography.Text>
|
||||
<Typography.Text strong size="small">
|
||||
{value}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
|
||||
interface PayBlocksProps {
|
||||
options: PayBlockProps[];
|
||||
}
|
||||
|
||||
export const PayBlocks: React.FC<PayBlocksProps> = ({ options }) => (
|
||||
<div className={styles['pay-blocks']}>
|
||||
{options.flatMap((item, idx) =>
|
||||
idx < options.length - 1
|
||||
? [
|
||||
<PayBlock key={item.label} {...item} />,
|
||||
<Divider layout="vertical" margin={4} style={{ height: '10px' }} />,
|
||||
]
|
||||
: [<PayBlock key={item.label} {...item} />],
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,32 @@
|
||||
.trace-detail-panel {
|
||||
width: 360px;
|
||||
min-width:360px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.trace-detail {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 4px;
|
||||
margin-bottom: 4px;
|
||||
:global .coz-icon-button {
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
.log-id {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
:global .coz-icon-button {
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.json-viewer {
|
||||
margin-bottom: 16px;
|
||||
margin-top: -4px;
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* 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 React, { useMemo } from 'react';
|
||||
|
||||
import { isUndefined } from 'lodash-es';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { BottomPanel } from '@coze-workflow/test-run-shared';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type TraceFrontendSpan } from '@coze-arch/bot-api/workflow_api';
|
||||
import { IconCozCopy } from '@coze-arch/coze-design/icons';
|
||||
import { Divider, IconButton, Toast, Typography } from '@coze-arch/coze-design';
|
||||
|
||||
import { StatusTag } from '../status-tag';
|
||||
import { FocusButton } from '../focus-button';
|
||||
import {
|
||||
formatDuration,
|
||||
getTokensFromSpan,
|
||||
getGotoNodeParams,
|
||||
} from '../../utils';
|
||||
import { type GotoParams } from '../../types';
|
||||
import {
|
||||
MessagePanel,
|
||||
ObservationModules,
|
||||
type MessagePanelProps,
|
||||
} from '../../observation-components';
|
||||
import { PayBlocks } from './pay-block';
|
||||
|
||||
import styles from './trace-detail-panel.module.less';
|
||||
|
||||
const ResultViewer: React.FC<
|
||||
Omit<MessagePanelProps, 'i18nMapping'>
|
||||
> = props => (
|
||||
<MessagePanel
|
||||
{...props}
|
||||
className={styles['json-viewer']}
|
||||
i18nMapping={
|
||||
{
|
||||
[ObservationModules.INPUT]: {
|
||||
title: I18n.t('workflow_detail_node_input'),
|
||||
},
|
||||
[ObservationModules.OUTPUT]: {
|
||||
title: I18n.t('workflow_detail_node_output'),
|
||||
},
|
||||
} as unknown as MessagePanelProps['i18nMapping']
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
interface TraceDetailPanelProps {
|
||||
span: TraceFrontendSpan;
|
||||
onClose: () => void;
|
||||
onGotoNode: (params: GotoParams) => void;
|
||||
}
|
||||
|
||||
export const TraceDetailPanel: React.FC<TraceDetailPanelProps> = ({
|
||||
span,
|
||||
onClose,
|
||||
onGotoNode,
|
||||
}) => {
|
||||
const pays = useMemo(() => {
|
||||
const temp = [
|
||||
{
|
||||
label: I18n.t('analytic_query_detail_key_latency'),
|
||||
value: span.duration
|
||||
? formatDuration(span.duration as unknown as number)
|
||||
: '0ms',
|
||||
},
|
||||
];
|
||||
const tokens = getTokensFromSpan(span);
|
||||
if (!isUndefined(tokens)) {
|
||||
temp.push({
|
||||
label: I18n.t('analytic_query_table_title_tokens'),
|
||||
value: `${tokens}`,
|
||||
});
|
||||
}
|
||||
return temp;
|
||||
}, [span]);
|
||||
|
||||
const handleCopy = () => {
|
||||
try {
|
||||
copy(span.log_id || '');
|
||||
Toast.success({ content: I18n.t('copy_success'), showClose: false });
|
||||
} catch {
|
||||
Toast.error(I18n.t('copy_failed'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
onGotoNode(getGotoNodeParams(span));
|
||||
};
|
||||
|
||||
return (
|
||||
<BottomPanel
|
||||
header={I18n.t('workflow_running_results')}
|
||||
onClose={onClose}
|
||||
className={styles['trace-detail-panel']}
|
||||
>
|
||||
<div className={styles['trace-detail']}>
|
||||
<div className={styles['detail-title']}>
|
||||
<Typography.Text strong>{span.alias_name}</Typography.Text>
|
||||
<StatusTag status={span.status_code} />
|
||||
<FocusButton span={span} onClick={handleScroll} />
|
||||
</div>
|
||||
<PayBlocks options={pays} />
|
||||
{span.log_id ? (
|
||||
<div className={styles['log-id']}>
|
||||
<Typography.Text type="secondary" size="small">
|
||||
LogId: {span.log_id}
|
||||
</Typography.Text>
|
||||
<IconButton
|
||||
icon={<IconCozCopy />}
|
||||
size="mini"
|
||||
onClick={handleCopy}
|
||||
color="secondary"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<Divider margin={16} />
|
||||
{span.input?.content ? (
|
||||
<ResultViewer
|
||||
content={span.input?.content}
|
||||
category={ObservationModules.INPUT}
|
||||
jsonViewerProps={{
|
||||
displayDataTypes: false,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{span.output?.content ? (
|
||||
<ResultViewer
|
||||
content={span.output?.content}
|
||||
category={ObservationModules.OUTPUT}
|
||||
jsonViewerProps={{
|
||||
displayDataTypes: false,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</BottomPanel>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
.trace-graph {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
:global .coz-icon-button-mini {
|
||||
line-height: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.graph-part {
|
||||
width: 50%;
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.part-tree {
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.part-chart {
|
||||
border-left: 1px solid var(--coz-stroke-plus);
|
||||
}
|
||||
|
||||
.trace-charts {
|
||||
height: 100%;
|
||||
}
|
||||
.chart-header {
|
||||
height: 48px;
|
||||
padding: 0 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--coz-stroke-plus);
|
||||
|
||||
.mode-select {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
height: calc(100% - 48px);
|
||||
padding: 4px 8px;
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* 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, useState } from 'react';
|
||||
|
||||
import { clsx } from 'clsx';
|
||||
import { gotoDebugFlow } from '@coze-workflow/test-run-shared';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
type Span,
|
||||
type TraceFrontendSpan,
|
||||
} from '@coze-arch/bot-api/workflow_api';
|
||||
import { IconCozExit } from '@coze-arch/coze-design/icons';
|
||||
import { Typography, Select, IconButton } from '@coze-arch/coze-design';
|
||||
|
||||
import { FocusButton } from '../focus-button';
|
||||
import { getGotoNodeParams } from '../../utils';
|
||||
import { type GotoParams } from '../../types';
|
||||
import {
|
||||
TraceTree,
|
||||
TraceFlameThread,
|
||||
spans2SpanNodes,
|
||||
} from '../../observation-components';
|
||||
import { useTraceListStore } from '../../contexts';
|
||||
import { TraceChartsMode } from '../../constants';
|
||||
import { useTrace } from './use-trace';
|
||||
import { EmptyTemplate, LoadingTemplate } from './template';
|
||||
import { TraceTable } from './table';
|
||||
|
||||
import css from './graph.module.less';
|
||||
|
||||
interface TraceGraphProps {
|
||||
onOpenDetail: (span: TraceFrontendSpan) => void;
|
||||
onGotoNode: (params: GotoParams) => void;
|
||||
}
|
||||
|
||||
const MODE_OPTIONS = [
|
||||
{
|
||||
label: I18n.t('analytic_query_detail_left_panel_flamethread'),
|
||||
value: TraceChartsMode.FlameThread,
|
||||
},
|
||||
{
|
||||
label: I18n.t('Starling_filebox_api_list'),
|
||||
value: TraceChartsMode.Table,
|
||||
},
|
||||
];
|
||||
|
||||
export const TraceGraph: React.FC<TraceGraphProps> = ({
|
||||
onOpenDetail,
|
||||
onGotoNode,
|
||||
}) => {
|
||||
const [mode, setMode] = useState(TraceChartsMode.FlameThread);
|
||||
const { ready, spaceId, isInOp } = useTraceListStore(store => ({
|
||||
ready: store.ready,
|
||||
spaceId: store.spaceId,
|
||||
isInOp: store.isInOp,
|
||||
}));
|
||||
const { spans, loading } = useTrace();
|
||||
|
||||
const tree = useMemo(() => spans2SpanNodes(spans || ([] as any)), [spans]);
|
||||
|
||||
const handleFocusNode = (span: Span) => {
|
||||
onGotoNode(getGotoNodeParams(span));
|
||||
};
|
||||
|
||||
const jumpToDebugFlow = (span: Span) => {
|
||||
const params = getGotoNodeParams(span);
|
||||
gotoDebugFlow(
|
||||
{
|
||||
...params,
|
||||
spaceId,
|
||||
},
|
||||
isInOp,
|
||||
);
|
||||
};
|
||||
|
||||
if (!ready || loading) {
|
||||
return <LoadingTemplate />;
|
||||
}
|
||||
|
||||
if (!spans) {
|
||||
return <EmptyTemplate />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={css['trace-graph']}>
|
||||
<div className={clsx(css['graph-part'], css['part-tree'])}>
|
||||
<TraceTree
|
||||
spans={tree.roots}
|
||||
renderGraphNodeConfig={{
|
||||
traceTreeCustomRenderer: {
|
||||
renderExtra: span =>
|
||||
span.parent_id && span.parent_id !== '0' ? (
|
||||
<FocusButton span={span} onClick={handleFocusNode} />
|
||||
) : (
|
||||
<IconButton
|
||||
size="mini"
|
||||
icon={<IconCozExit />}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
jumpToDebugFlow(span);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
}}
|
||||
onSelect={v => {
|
||||
const span = (v.node.extra as any)?.spanNode;
|
||||
if (span) {
|
||||
onOpenDetail(span);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={clsx(css['graph-part'], css['part-chart'])}>
|
||||
<div className={css['trace-charts']}>
|
||||
<div className={css['chart-header']}>
|
||||
<Typography.Text strong fontSize="16px">
|
||||
{I18n.t('store_bot_detail_title_mobile')}
|
||||
</Typography.Text>
|
||||
<Select
|
||||
size="small"
|
||||
className={css['mode-select']}
|
||||
optionList={MODE_OPTIONS}
|
||||
value={mode}
|
||||
onChange={(v: any) => setMode(v)}
|
||||
/>
|
||||
</div>
|
||||
<div className={css['chart-content']}>
|
||||
{mode === TraceChartsMode.Table && <TraceTable spans={spans} />}
|
||||
{mode === TraceChartsMode.FlameThread && (
|
||||
<TraceFlameThread spans={spans} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 { TraceGraph } from './graph';
|
||||
@@ -0,0 +1,20 @@
|
||||
.trace-table {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
:global {
|
||||
.semi-table-tbody > .semi-table-row > .semi-table-row-cell {
|
||||
height: 32px !important;
|
||||
padding-top: 4px !important;
|
||||
padding-bottom: 4px !important;
|
||||
}
|
||||
.semi-table-row-head {
|
||||
font-size: 12px !important;
|
||||
height: 32px !important;
|
||||
padding-top: 4px !important;
|
||||
padding-bottom: 4px !important;
|
||||
}
|
||||
.coz-table-wrapper .coz-table-list .semi-table-fixed-header table {
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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 { isUndefined } from 'lodash-es';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Table } from '@coze-arch/coze-design';
|
||||
import { type TraceFrontendSpan } from '@coze-arch/bot-api/workflow_api';
|
||||
|
||||
import { formatDuration, getTokensFromSpan } from '../../utils';
|
||||
|
||||
import css from './table.module.less';
|
||||
|
||||
interface TraceTableProps {
|
||||
spans: TraceFrontendSpan[];
|
||||
}
|
||||
|
||||
export const TraceTable: React.FC<TraceTableProps> = ({ spans }) => {
|
||||
const columns = [
|
||||
{
|
||||
title: I18n.t('platfrom_trigger_creat_name'),
|
||||
dataIndex: 'name',
|
||||
},
|
||||
{
|
||||
title: I18n.t('debug_asyn_task_task_status'),
|
||||
dataIndex: 'status_code',
|
||||
render: data =>
|
||||
data === 0
|
||||
? I18n.t('debug_asyn_task_task_status_success')
|
||||
: I18n.t('debug_asyn_task_task_status_failed'),
|
||||
width: 78,
|
||||
},
|
||||
{
|
||||
title: I18n.t('analytic_query_table_title_tokens'),
|
||||
render: (_, row) => {
|
||||
const v = getTokensFromSpan(row);
|
||||
return isUndefined(v) ? '-' : v;
|
||||
},
|
||||
width: 78,
|
||||
},
|
||||
{
|
||||
title: I18n.t('db_add_table_field_type_time'),
|
||||
dataIndex: 'duration',
|
||||
render: formatDuration,
|
||||
width: 78,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={css['trace-table']}>
|
||||
<Table
|
||||
tableProps={{
|
||||
dataSource: spans,
|
||||
rowKey: 'span_id',
|
||||
columns,
|
||||
size: 'small',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
.full-template {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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 { IconCozIllusEmpty } from '@coze-arch/coze-design/illustrations';
|
||||
import { Spin } from '@coze-arch/coze-design';
|
||||
|
||||
import css from './template.module.less';
|
||||
|
||||
export const EmptyTemplate = () => (
|
||||
<div className={css['full-template']}>
|
||||
<IconCozIllusEmpty width="100px" height="100px" />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const LoadingTemplate = () => (
|
||||
<div className={css['full-template']}>
|
||||
<Spin />
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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, useState } from 'react';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { workflowApi } from '@coze-workflow/base';
|
||||
import { type TraceFrontendSpan } from '@coze-arch/bot-api/workflow_api';
|
||||
|
||||
import { sortSpans } from '../../utils';
|
||||
import { useTraceListStore } from '../../contexts';
|
||||
import { MAX_TRACE_TIME } from '../../constants';
|
||||
|
||||
export const useTrace = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [spans, setSpans] = useState<TraceFrontendSpan[] | null>(null);
|
||||
|
||||
const { span } = useTraceListStore(store => ({
|
||||
span: store.span,
|
||||
}));
|
||||
|
||||
const fetch = useMemoizedFn(async (logId: string) => {
|
||||
setLoading(true);
|
||||
/** 查询日志时,开始结束时间必传,由于用户可查范围为 7 天内,所以直接伪造 7 天时间间隔即可 */
|
||||
const now = dayjs().endOf('day').valueOf();
|
||||
const end = dayjs()
|
||||
.subtract(MAX_TRACE_TIME, 'day')
|
||||
.startOf('day')
|
||||
.valueOf();
|
||||
|
||||
try {
|
||||
const { data } = await workflowApi.GetTraceSDK({
|
||||
log_id: logId,
|
||||
start_at: end,
|
||||
end_at: now,
|
||||
});
|
||||
if (!data || !data.spans) {
|
||||
return;
|
||||
}
|
||||
const next = sortSpans(data.spans);
|
||||
setSpans(next);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (span?.log_id) {
|
||||
fetch(span.log_id);
|
||||
}
|
||||
}, [span, fetch]);
|
||||
|
||||
return { spans, loading };
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
.trace-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 8px;
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
.trace-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--coz-fg-hglt);
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.header-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import { TraceSelect } from '../trace-select';
|
||||
|
||||
import css from './header.module.less';
|
||||
|
||||
export const TraceListPanelHeader: React.FC = () => (
|
||||
<div className={css['trace-panel-header']}>
|
||||
<div className={css['header-tabs']}>
|
||||
<div className={css['trace-title']}>{I18n.t('debug_btn')}</div>
|
||||
</div>
|
||||
<TraceSelect />
|
||||
</div>
|
||||
);
|
||||
@@ -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 { TraceListPanel } from './list-panel';
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
|
||||
import { BottomPanel } from '@coze-workflow/test-run-shared';
|
||||
import { type TraceFrontendSpan } from '@coze-arch/bot-api/workflow_api';
|
||||
|
||||
import { TraceGraph } from '../trace-graph';
|
||||
import { type GotoParams } from '../../types';
|
||||
import { TraceListProvider } from '../../contexts';
|
||||
import { TraceListPanelHeader } from './header';
|
||||
|
||||
interface TraceListPanelProps {
|
||||
spaceId: string;
|
||||
workflowId: string;
|
||||
maxHeight: number;
|
||||
isInOp?: boolean;
|
||||
onOpenDetail: (span: TraceFrontendSpan) => void;
|
||||
onClose: () => void;
|
||||
onGotoNode: (params: GotoParams) => void;
|
||||
}
|
||||
|
||||
export const TraceListPanel: React.FC<TraceListPanelProps> = ({
|
||||
spaceId,
|
||||
workflowId,
|
||||
isInOp,
|
||||
maxHeight,
|
||||
onOpenDetail,
|
||||
onGotoNode,
|
||||
onClose,
|
||||
}) => (
|
||||
<TraceListProvider spaceId={spaceId} workflowId={workflowId} isInOp={isInOp}>
|
||||
<BottomPanel
|
||||
header={<TraceListPanelHeader />}
|
||||
height={300}
|
||||
resizable={{
|
||||
min: 300,
|
||||
max: maxHeight,
|
||||
}}
|
||||
onClose={onClose}
|
||||
>
|
||||
<TraceGraph onOpenDetail={onOpenDetail} onGotoNode={onGotoNode} />
|
||||
</BottomPanel>
|
||||
</TraceListProvider>
|
||||
);
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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 { useCallback } from 'react';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import { IconCozCalendar } from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
DatePicker as DatePickerCore,
|
||||
type DatePickerProps as DatePickerCoreProps,
|
||||
IconButton,
|
||||
} from '@coze-arch/coze-design';
|
||||
|
||||
type DatePickerProps = Pick<DatePickerCoreProps, 'value'> & {
|
||||
onChange: (v: [Date, Date]) => void;
|
||||
};
|
||||
|
||||
export const DatePicker: React.FC<DatePickerProps> = ({
|
||||
onChange,
|
||||
...props
|
||||
}) => {
|
||||
const disabledDate = (date?: Date) => {
|
||||
if (!date) {
|
||||
return false;
|
||||
}
|
||||
const current = date.getTime();
|
||||
const end = dayjs().endOf('day').valueOf();
|
||||
const start = dayjs().subtract(6, 'day').startOf('day').valueOf();
|
||||
|
||||
return current < start || current > end;
|
||||
};
|
||||
|
||||
const triggerRender = useCallback(
|
||||
() => (
|
||||
<IconButton icon={<IconCozCalendar />} color="secondary" size="small" />
|
||||
),
|
||||
[],
|
||||
);
|
||||
const handleChange = (v: any) => {
|
||||
onChange(v);
|
||||
};
|
||||
|
||||
return (
|
||||
<DatePickerCore
|
||||
type="dateRange"
|
||||
triggerRender={triggerRender}
|
||||
disabledDate={disabledDate}
|
||||
onChange={handleChange}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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 { TraceSelect } from './trace-select';
|
||||
@@ -0,0 +1,24 @@
|
||||
.select-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
justify-content: space-between;
|
||||
column-gap: 8px;
|
||||
padding: 0 8px;
|
||||
|
||||
.time {
|
||||
width: 128px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 242px;
|
||||
column-gap: 4px;
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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 { gotoDebugFlow } from '@coze-workflow/test-run-shared';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type Span } from '@coze-arch/bot-api/workflow_api';
|
||||
import { IconCozExit } from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
Typography,
|
||||
Tag,
|
||||
IconButton,
|
||||
type ButtonProps,
|
||||
} from '@coze-arch/coze-design';
|
||||
|
||||
import { StatusIcon } from '../status-tag';
|
||||
import {
|
||||
getTimeFromSpan,
|
||||
isTriggerFromSpan,
|
||||
getGotoNodeParams,
|
||||
} from '../../utils';
|
||||
import { useTraceListStore } from '../../contexts';
|
||||
|
||||
import css from './select-option.module.less';
|
||||
|
||||
interface SelectOptionProps {
|
||||
span: Span;
|
||||
}
|
||||
|
||||
export const SelectOption: React.FC<SelectOptionProps> = ({ span }) => {
|
||||
const time = useMemo(() => getTimeFromSpan(span), [span]);
|
||||
const isTrigger = useMemo(() => isTriggerFromSpan(span), [span]);
|
||||
const { spaceId, isInOp } = useTraceListStore(store => ({
|
||||
spaceId: store.spaceId,
|
||||
isInOp: store.isInOp,
|
||||
}));
|
||||
const jumpToDebugFlow: ButtonProps['onClick'] = e => {
|
||||
e.stopPropagation();
|
||||
const params = getGotoNodeParams(span);
|
||||
gotoDebugFlow(
|
||||
{
|
||||
...params,
|
||||
spaceId,
|
||||
},
|
||||
isInOp,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={css['select-option']}>
|
||||
<div className={css.title}>
|
||||
<StatusIcon status={span.status_code} className={css.icon} />
|
||||
<Typography.Text ellipsis={{ showTooltip: true }}>
|
||||
{time}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
{isTrigger ? (
|
||||
<Tag
|
||||
style={{
|
||||
color: 'var(--coz-fg-hglt)',
|
||||
backgroundColor: 'var(--coz-mg-hglt)',
|
||||
}}
|
||||
size={'mini'}
|
||||
>
|
||||
{I18n.t('workflow_start_trigger_triggername')}
|
||||
</Tag>
|
||||
) : null}
|
||||
<IconButton
|
||||
size="mini"
|
||||
icon={<IconCozExit />}
|
||||
onClick={jumpToDebugFlow}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 { useCallback } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozFilter } from '@coze-arch/coze-design/icons';
|
||||
import { Select, IconButton } from '@coze-arch/coze-design';
|
||||
import { SpanStatus } from '@coze-arch/bot-api/workflow_api';
|
||||
|
||||
interface StatusSelectProps {
|
||||
value: SpanStatus;
|
||||
onChange: (v: SpanStatus) => void;
|
||||
}
|
||||
|
||||
export const StatusSelect: React.FC<StatusSelectProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const triggerRender = useCallback(
|
||||
() => (
|
||||
<IconButton icon={<IconCozFilter />} color="secondary" size="small" />
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={value}
|
||||
triggerRender={triggerRender}
|
||||
optionList={[
|
||||
{
|
||||
value: SpanStatus.Unknown,
|
||||
label: I18n.t('query_status_all'),
|
||||
},
|
||||
{
|
||||
value: SpanStatus.Fail,
|
||||
label: I18n.t('query_status_failed'),
|
||||
},
|
||||
{
|
||||
value: SpanStatus.Success,
|
||||
label: I18n.t('query_status_completed'),
|
||||
},
|
||||
]}
|
||||
onChange={onChange as any}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
.trace-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.trace-filter {
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 4px;
|
||||
padding: 0 4px;
|
||||
border: 1px solid var(--coz-stroke-plus);
|
||||
border-right: 0;
|
||||
border-top-left-radius: 8px;
|
||||
border-bottom-left-radius: 8px;
|
||||
}
|
||||
|
||||
.main-select {
|
||||
width: 200px;
|
||||
border-top-left-radius: 0px!important;
|
||||
border-bottom-left-radius: 0px!important;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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 { useCallback, useMemo } from 'react';
|
||||
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { Select } from '@coze-arch/coze-design';
|
||||
|
||||
import { getTimeFromSpan } from '../../utils';
|
||||
import { useTraceListStore } from '../../contexts';
|
||||
import { useOptions } from './use-options';
|
||||
import { StatusSelect } from './status-select';
|
||||
import { SelectOption } from './select-option';
|
||||
import { DatePicker } from './date-picker';
|
||||
|
||||
import css from './trace-select.module.less';
|
||||
|
||||
export const TraceSelect: React.FC = () => {
|
||||
const { span, workflowId, patch } = useTraceListStore(store => ({
|
||||
span: store.span,
|
||||
workflowId: store.workflowId,
|
||||
patch: store.patch,
|
||||
}));
|
||||
|
||||
const {
|
||||
date,
|
||||
status,
|
||||
setStatus,
|
||||
options,
|
||||
optionsCacheRef,
|
||||
fetch,
|
||||
onDateChange,
|
||||
} = useOptions(workflowId);
|
||||
|
||||
const optionList = useMemo(() => {
|
||||
const temp = options.map(i => ({
|
||||
...i,
|
||||
value: i.log_id,
|
||||
label: <SelectOption span={i} />,
|
||||
}));
|
||||
return temp;
|
||||
}, [options]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(v: any) => {
|
||||
patch({
|
||||
span:
|
||||
v && optionsCacheRef.current.has(v)
|
||||
? optionsCacheRef.current.get(v)
|
||||
: null,
|
||||
});
|
||||
},
|
||||
[optionsCacheRef, patch],
|
||||
);
|
||||
|
||||
const handleDropdownVisibleChange = useMemoizedFn((v: boolean) => {
|
||||
if (v) {
|
||||
fetch();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={css['trace-select']}>
|
||||
<div className={css['trace-filter']}>
|
||||
<DatePicker value={date} onChange={onDateChange} />
|
||||
<StatusSelect value={status} onChange={setStatus} />
|
||||
</div>
|
||||
|
||||
<Select
|
||||
className={css['main-select']}
|
||||
optionList={optionList}
|
||||
value={span?.log_id}
|
||||
onChange={handleChange}
|
||||
renderSelectedItem={e => {
|
||||
const current = optionsCacheRef.current.get(e.value);
|
||||
return current ? getTimeFromSpan(current) : '-';
|
||||
}}
|
||||
onDropdownVisibleChange={handleDropdownVisibleChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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 complexity */
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { workflowApi } from '@coze-workflow/base';
|
||||
import { SpanStatus, type Span } from '@coze-arch/bot-api/workflow_api';
|
||||
|
||||
import { useTraceListStore } from '../../contexts';
|
||||
import { MAX_TRACE_LENGTH, MAX_TRACE_TIME } from '../../constants';
|
||||
|
||||
const getDefaultDate = (): [Date, Date] => {
|
||||
const end = dayjs().endOf('day').toDate();
|
||||
const start = dayjs()
|
||||
.subtract(MAX_TRACE_TIME - 1, 'day')
|
||||
.startOf('day')
|
||||
.toDate();
|
||||
|
||||
return [start, end];
|
||||
};
|
||||
|
||||
export const useOptions = (workflowId: string) => {
|
||||
const [date, setDate] = useState<[Date, Date]>(getDefaultDate());
|
||||
const [status, setStatus] = useState<SpanStatus>(SpanStatus.Unknown);
|
||||
const [options, setOptions] = useState<Span[]>([]);
|
||||
|
||||
const optionsCacheRef = useRef(new Map<string, Span>());
|
||||
|
||||
const { ready, span, patch } = useTraceListStore(store => ({
|
||||
span: store.span,
|
||||
ready: store.ready,
|
||||
patch: store.patch,
|
||||
}));
|
||||
|
||||
const fetch = useMemoizedFn(async () => {
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const executeMode = searchParams.get('execute_mode');
|
||||
const executeId = searchParams.get('execute_id');
|
||||
|
||||
const { spans } = await workflowApi.ListRootSpans({
|
||||
workflow_id: workflowId,
|
||||
limit: MAX_TRACE_LENGTH,
|
||||
offset: 0,
|
||||
start_at: date[0].getTime(),
|
||||
end_at: date[1].getTime(),
|
||||
status: status === SpanStatus.Unknown ? undefined : status,
|
||||
execute_mode: executeMode ? Number(executeMode) : undefined,
|
||||
});
|
||||
const next = spans || [];
|
||||
let maybeInitialSpan = next[0];
|
||||
if (executeId && !ready && !span) {
|
||||
try {
|
||||
const { data } = await workflowApi.GetTraceSDK({
|
||||
execute_id: executeId,
|
||||
workflow_id: workflowId,
|
||||
start_at: date[0].getTime(),
|
||||
end_at: date[1].getTime(),
|
||||
});
|
||||
const first = data?.spans?.[0];
|
||||
if (first?.log_id) {
|
||||
maybeInitialSpan = first;
|
||||
const urlSpan = next.find(i => i.log_id === first.log_id);
|
||||
if (!urlSpan) {
|
||||
next.unshift(first);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line @coze-arch/no-empty-catch -- 无需报错
|
||||
} catch {
|
||||
// 无需报错
|
||||
}
|
||||
}
|
||||
next.forEach(s => {
|
||||
if (s.log_id) {
|
||||
optionsCacheRef.current.set(s.log_id, s);
|
||||
}
|
||||
});
|
||||
setOptions(next);
|
||||
// 如果没有初始化,就初始化一次
|
||||
if (!ready && !span && maybeInitialSpan) {
|
||||
patch({ span: maybeInitialSpan });
|
||||
}
|
||||
if (!ready) {
|
||||
patch({ ready: true });
|
||||
}
|
||||
});
|
||||
|
||||
const handleDateChange = useCallback(
|
||||
(next: [Date, Date]) => {
|
||||
const [start, end] = next;
|
||||
// 时间选择器选择的日期是当天 0 点,需要转化为当天 11 点 59 分 59 秒
|
||||
setDate([start, dayjs(end).endOf('day').toDate()] as [Date, Date]);
|
||||
},
|
||||
[setDate],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetch();
|
||||
}, [date, status, fetch]);
|
||||
|
||||
return {
|
||||
date,
|
||||
status,
|
||||
setStatus,
|
||||
options,
|
||||
optionsCacheRef,
|
||||
fetch,
|
||||
onDateChange: handleDateChange,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 enum TraceChartsMode {
|
||||
Table,
|
||||
FlameThread,
|
||||
}
|
||||
|
||||
export const MAX_TRACE_LENGTH = 50;
|
||||
/** 日志最多查询 7 天 */
|
||||
export const MAX_TRACE_TIME = 7;
|
||||
@@ -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 { TraceListProvider, useTraceListStore } from './trace-list';
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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 { createContext, useMemo, useContext } from 'react';
|
||||
|
||||
import { createWithEqualityFn } from 'zustand/traditional';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { type Span } from '@coze-arch/bot-api/workflow_api';
|
||||
|
||||
export interface TraceListState {
|
||||
spaceId: string;
|
||||
workflowId: string;
|
||||
isInOp?: boolean;
|
||||
/** 初次打开时需要先请求列表,然后选中第一项 */
|
||||
ready: boolean;
|
||||
/** 当前选中的 span */
|
||||
span: Span | null;
|
||||
}
|
||||
|
||||
export interface TraceListAction {
|
||||
/** 更新状态 */
|
||||
patch: (next: Partial<TraceListState>) => void;
|
||||
}
|
||||
|
||||
const createTraceListStore = (
|
||||
params: Pick<TraceListState, 'spaceId' | 'workflowId' | 'isInOp'>,
|
||||
) =>
|
||||
createWithEqualityFn<TraceListState & TraceListAction>(
|
||||
set => ({
|
||||
...params,
|
||||
ready: false,
|
||||
span: null,
|
||||
patch: next => set(() => next),
|
||||
}),
|
||||
shallow,
|
||||
);
|
||||
|
||||
type TraceListStore = ReturnType<typeof createTraceListStore>;
|
||||
|
||||
export const TraceListContext = createContext<TraceListStore>(
|
||||
{} as unknown as TraceListStore,
|
||||
);
|
||||
|
||||
export const TraceListProvider: React.FC<
|
||||
React.PropsWithChildren<
|
||||
Pick<TraceListState, 'spaceId' | 'workflowId' | 'isInOp'>
|
||||
>
|
||||
> = ({ spaceId, workflowId, isInOp, children }) => {
|
||||
const store = useMemo(
|
||||
() => createTraceListStore({ spaceId, workflowId, isInOp }),
|
||||
[spaceId, workflowId, isInOp],
|
||||
);
|
||||
|
||||
return (
|
||||
<TraceListContext.Provider value={store}>
|
||||
{children}
|
||||
</TraceListContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTraceListStore = <T,>(
|
||||
selector: (s: TraceListState & TraceListAction) => T,
|
||||
) => {
|
||||
const store = useContext(TraceListContext);
|
||||
|
||||
return store(selector);
|
||||
};
|
||||
26
frontend/packages/workflow/test-run-next/trace/src/global.d.ts
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
declare module '*.otf' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.ttf' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
18
frontend/packages/workflow/test-run-next/trace/src/index.ts
Normal file
@@ -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 { TraceListPanel } from './components/trace-list-panel';
|
||||
export { TraceDetailPanel } from './components/trace-detail-panel';
|
||||
@@ -0,0 +1,12 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6630_32796)">
|
||||
<rect width="16" height="16" fill="#C6C6CD"/>
|
||||
<path d="M5.17163 5.17163L10.8285 10.8285" stroke="white" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M5.17163 10.8284L10.8285 5.17151" stroke="white" stroke-width="2" stroke-linecap="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6630_32796">
|
||||
<rect width="16" height="16" rx="4" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 504 B |
@@ -0,0 +1,11 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6630_28191)">
|
||||
<rect width="16" height="16" fill="#9D63E9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.39481 12.54L3.94296 11.7556C3.05772 10.8459 2.49198 9.63934 2.42025 8.30568C2.41452 8.20465 2.41162 8.10289 2.41162 8.00047C2.41162 4.9859 4.9259 2.54211 8.02741 2.54211H8.12991C9.48724 2.54211 10.7289 3.03755 11.6838 3.85744C12.4251 4.47606 12.9984 5.27939 13.3284 6.194C13.3397 6.22544 13.5883 7.25554 13.5883 8.00048C13.5883 10.7944 11.4891 13.098 8.78198 13.4203L8.78068 13.4204C8.56729 13.4458 8.35012 13.4588 8.12991 13.4588C7.18894 13.4569 6.24796 13.4575 5.30697 13.4582C4.83648 13.4585 4.36599 13.4588 3.8955 13.4588C3.4097 13.4588 3.12185 12.9306 3.39481 12.54ZM9.78585 7.6439C9.78585 7.24941 10.1056 6.92962 10.5001 6.92962C10.8946 6.92962 11.2144 7.24941 11.2144 7.6439V8.35817C11.2144 8.75266 10.8946 9.07246 10.5001 9.07246C10.1056 9.07246 9.78585 8.75266 9.78585 8.35817V7.6439ZM5.50013 6.92962C5.10564 6.92962 4.78585 7.24941 4.78585 7.6439V8.35817C4.78585 8.75266 5.10564 9.07246 5.50013 9.07246C5.89462 9.07246 6.21442 8.75266 6.21442 8.35817V7.6439C6.21442 7.24941 5.89462 6.92962 5.50013 6.92962Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6630_28191">
|
||||
<rect width="16" height="16" rx="4.8" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,11 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6630_28180)">
|
||||
<rect width="16" height="16" fill="#4D53E8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.08308 6.73795C3.2876 5.19559 4.53161 4.09102 6.0675 3.84254C6.63905 3.75007 7.29268 3.68494 8.00002 3.68494C8.70736 3.68494 9.36099 3.75007 9.93254 3.84254C11.4684 4.09102 12.7124 5.19558 12.9169 6.73791C12.9679 7.12251 13 7.55376 13 8.03013C13 8.64596 12.9464 9.18219 12.8676 9.63774C12.6425 10.9391 11.611 11.8792 10.3117 12.116C9.68458 12.2302 8.9108 12.3159 8.00002 12.3159C7.08925 12.3159 6.31547 12.2302 5.6883 12.116C4.38898 11.8792 3.35747 10.9391 3.1324 9.63769C3.05361 9.18216 3 8.64594 3 8.03013C3 7.55377 3.03208 7.12253 3.08308 6.73795ZM9.7832 8.3875C9.7832 7.99301 10.103 7.67322 10.4975 7.67322C10.892 7.67322 11.2118 7.99301 11.2118 8.3875V9.10178C11.2118 9.49626 10.892 9.81606 10.4975 9.81606C10.103 9.81606 9.7832 9.49626 9.7832 9.10178V8.3875ZM5.49749 7.67322C5.103 7.67322 4.7832 7.99301 4.7832 8.3875V9.10178C4.7832 9.49626 5.103 9.81606 5.49749 9.81606C5.89198 9.81606 6.21177 9.49626 6.21177 9.10177V8.3875C6.21177 7.99301 5.89198 7.67322 5.49749 7.67322Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6630_28180">
|
||||
<rect width="16" height="16" rx="4" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="4" fill="#3EC254"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 4C3.44772 4 3 4.44772 3 5V11C3 11.5523 3.44772 12 4 12H12C12.5523 12 13 11.5523 13 11V5C13 4.44772 12.5523 4 12 4H4ZM10.5 5C10.2239 5 10 5.22386 10 5.5V6.5C10 6.77614 10.2239 7 10.5 7H11.5C11.7761 7 12 6.77614 12 6.5V5.5C12 5.22386 11.7761 5 11.5 5H10.5ZM5 7.5C5 7.22386 5.22386 7 5.5 7H6.5C6.77614 7 7 7.22386 7 7.5C7 7.77614 6.77614 8 6.5 8H5.5C5.22386 8 5 7.77614 5 7.5ZM5.5 9C5.22386 9 5 9.22386 5 9.5C5 9.77614 5.22386 10 5.5 10H8.5C8.77614 10 9 9.77614 9 9.5C9 9.22386 8.77614 9 8.5 9H5.5Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 720 B |
@@ -0,0 +1,13 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6630_28184)">
|
||||
<rect width="16" height="16" fill="#28CAC8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.42782 5.16279C5.68817 5.42314 5.68817 5.84525 5.42782 6.1056L3.53013 8.00329L5.42782 9.90099C5.68817 10.1613 5.68817 10.5834 5.42782 10.8438C5.16747 11.1041 4.74536 11.1041 4.48501 10.8438L2.11592 8.4747C1.85557 8.21435 1.85557 7.79224 2.11592 7.53189L4.48501 5.16279C4.74536 4.90244 5.16747 4.90244 5.42782 5.16279Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5724 5.16279C10.3121 5.42314 10.3121 5.84525 10.5724 6.1056L12.4701 8.00329L10.5724 9.90099C10.3121 10.1613 10.3121 10.5834 10.5724 10.8438C10.8328 11.1041 11.2549 11.1041 11.5152 10.8438L13.8843 8.4747C14.1447 8.21435 14.1447 7.79224 13.8843 7.53189L11.5152 5.16279C11.2549 4.90244 10.8328 4.90244 10.5724 5.16279Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.84004 2.51694C9.20398 2.57272 9.4538 2.91296 9.39803 3.2769L7.91948 12.9249C7.86371 13.2888 7.52346 13.5386 7.15952 13.4828C6.79558 13.4271 6.54576 13.0868 6.60154 12.7229L8.08008 3.07493C8.13586 2.71099 8.4761 2.46117 8.84004 2.51694Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6630_28184">
|
||||
<rect width="16" height="16" rx="4" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,12 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6630_28183)">
|
||||
<rect width="16" height="16" fill="#FF9600"/>
|
||||
<path d="M8.61818 12.9549V7.07966H7.63672V5.78008H8.61818V5.09644C8.61818 3.69533 9.28151 3.04553 10.8383 3.04553C11.17 3.04553 11.5287 3.07261 11.7385 3.11322V4.29097C11.6032 4.27066 11.4136 4.25713 11.2309 4.25713C10.5743 4.25713 10.2697 4.56172 10.2697 5.14382V5.78008H11.7115V7.07966H10.3036V12.9549H8.61818Z" fill="white"/>
|
||||
<path d="M4.85474 12.9547V3.1875H6.60105V12.9547H4.85474Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6630_28183">
|
||||
<rect width="16" height="16" rx="4" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 707 B |
@@ -0,0 +1,18 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6630_28181)">
|
||||
<rect width="16" height="16" rx="4" fill="#FFB016"/>
|
||||
<mask id="mask0_6630_28181" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
|
||||
<rect x="0.796875" y="0.799438" width="14.4" height="14.4" fill="#D9D9D9"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_6630_28181)">
|
||||
<ellipse cx="7.997" cy="5.47946" rx="4.32" ry="2.16" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.3142 6.55945C12.3128 6.55945 12.3116 6.56054 12.3114 6.56197C12.1962 7.70346 10.3086 8.61126 7.997 8.61126C5.68539 8.61126 3.79784 7.70346 3.68258 6.56197C3.68243 6.56054 3.68123 6.55945 3.6798 6.55945C3.67825 6.55945 3.677 6.5607 3.677 6.56224V7.85266C3.677 7.8542 3.67825 7.85545 3.67979 7.85545C3.68123 7.85545 3.68243 7.85654 3.68257 7.85797C3.79735 8.99968 5.68511 9.90773 7.99704 9.90773C10.309 9.90773 12.1968 8.99966 12.3115 7.85793C12.3117 7.85652 12.3128 7.85545 12.3143 7.85545C12.3158 7.85545 12.317 7.85422 12.317 7.8527V7.75702L12.317 7.74773L12.317 7.73845V6.56224C12.317 6.5607 12.3158 6.55945 12.3142 6.55945Z" fill="white" fill-opacity="0.8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.3142 8.71948C12.3128 8.71948 12.3115 8.72058 12.3114 8.72201C12.1959 9.86339 10.3085 10.7711 7.997 10.7711C5.68555 10.7711 3.79809 9.86339 3.6826 8.72201C3.68246 8.72058 3.68125 8.71948 3.67981 8.71948C3.67826 8.71948 3.677 8.72074 3.677 8.72229V10.0127C3.677 10.0142 3.67826 10.0155 3.6798 10.0155C3.68124 10.0155 3.68245 10.0166 3.68259 10.018C3.7976 11.1596 5.68526 12.0675 7.99704 12.0675C10.3088 12.0675 12.1965 11.1596 12.3115 10.018C12.3116 10.0166 12.3128 10.0155 12.3142 10.0155C12.3158 10.0155 12.317 10.0142 12.317 10.0127V9.91684L12.317 9.90755L12.317 9.89826V8.72229C12.317 8.72074 12.3157 8.71948 12.3142 8.71948Z" fill="white" fill-opacity="0.6"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6630_28181">
|
||||
<rect width="16" height="16" rx="4" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,14 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6630_28185)">
|
||||
<rect width="16" height="16" rx="4" fill="#E85883"/>
|
||||
<path d="M5.44737 4.17578L5.44871 10.3804C5.44894 11.4439 6.31114 12.3059 7.37464 12.3059H11.8345C12.1004 12.3059 12.316 12.0903 12.316 11.8244V5.62005C12.316 4.55638 11.4537 3.69411 10.39 3.69412L5.92884 3.69419C5.66289 3.69419 5.44731 3.90982 5.44737 4.17578Z" fill="white" stroke="white" stroke-width="1.34815"/>
|
||||
<path opacity="0.7" d="M4.13155 4.78784L3.92237 4.78792C3.78941 4.78796 3.68167 4.89577 3.68171 5.02873L3.68338 10.8024C3.68345 11.0498 3.88409 11.2504 4.13155 11.2503V11.2503" stroke="white" stroke-width="1.34815"/>
|
||||
<path d="M7.19727 7.08081H10.5701" stroke="#E47258" stroke-width="1.34815" stroke-linecap="round"/>
|
||||
<path d="M7.19727 8.91931H9.20845" stroke="#E47258" stroke-width="1.34815" stroke-linecap="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6630_28185">
|
||||
<rect width="16" height="16" rx="4" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="4" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.29237 10.4214C1.70987 9.24042 1.52187 8.20551 2.69902 8.49609L3.80143 8.73235V6.94021C3.80143 4.57997 5.69282 2.66663 8.02596 2.66663C10.3591 2.66663 12.2505 4.57997 12.2505 6.94021V8.73235L13.2694 8.49609C14.424 8.18008 14.3458 9.12728 13.7012 10.5161C12.5075 12.4102 10.4119 13.6666 8.02608 13.6666C5.60072 13.6666 3.47526 12.3683 2.29237 10.4214ZM6.72488 10.6684C6.56522 10.5069 6.56522 10.2451 6.72488 10.0836C6.88454 9.92204 7.14339 9.92204 7.30305 10.0836C7.70219 10.4873 8.34932 10.4873 8.74846 10.0836C8.90812 9.92204 9.16697 9.92204 9.32663 10.0836C9.48629 10.2451 9.48629 10.5069 9.32663 10.6684C8.60818 11.3952 7.44333 11.3952 6.72488 10.6684ZM5.8204 7.14647C5.8204 6.91806 6.00343 6.7329 6.22922 6.7329C6.45501 6.7329 6.63805 6.91806 6.63805 7.14647V7.97362C6.63805 8.20203 6.45501 8.38719 6.22922 8.38719C6.00343 8.38719 5.8204 8.20203 5.8204 7.97362V7.14647ZM9.82263 6.7329C9.59685 6.7329 9.41381 6.91806 9.41381 7.14647V7.97362C9.41381 8.20203 9.59685 8.38719 9.82263 8.38719C10.0484 8.38719 10.2315 8.20203 10.2315 7.97362V7.14647C10.2315 6.91806 10.0484 6.7329 9.82263 6.7329Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="4" fill="#33B0FF"/>
|
||||
<path d="M4.64248 11.2101L4.17384 11.8808C3.94046 12.2147 4.18657 12.6663 4.60191 12.6663H7.89131C7.89131 12.6663 7.89294 12.6662 7.89598 12.6661C7.92105 12.6656 8.0417 12.6659 8.13215 12.6662L8.22214 12.6663C8.41041 12.6663 8.59608 12.6552 8.77852 12.6335L8.77963 12.6334C11.0941 12.3579 12.8888 10.3883 12.8888 7.99967C12.8888 7.36279 12.6763 6.48209 12.6666 6.45522C12.3845 5.67326 11.8943 4.98645 11.2606 4.45756C10.4441 3.75658 9.3826 3.33301 8.22214 3.33301C8.20773 3.33301 8.19333 3.33307 8.17895 3.3332C8.16415 3.33307 8.14934 3.33301 8.13451 3.33301C5.48285 3.33301 3.33325 5.42234 3.33325 7.99967C3.33325 8.08724 3.33573 8.17424 3.34063 8.26061C3.40195 9.40084 3.88564 10.4324 4.64248 11.2101ZM10.2221 7.11079C10.2221 7.33301 10.0503 7.55523 9.7777 7.55523H6.67527C6.39902 7.55523 6.22214 7.33301 6.22214 7.11079C6.22214 6.84342 6.44436 6.66634 6.66659 6.66634H9.7777C9.99992 6.66634 10.2221 6.84342 10.2221 7.11079ZM8.43915 8.44412C8.71346 8.44412 8.88881 8.66634 8.88881 8.88856C8.88881 9.11079 8.71346 9.33301 8.44436 9.33301H6.66659C6.44436 9.33301 6.22214 9.15602 6.22214 8.88856C6.22214 8.62111 6.44436 8.44412 6.66659 8.44412H8.43915Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,12 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6630_32795)">
|
||||
<rect width="16" height="16" fill="#C6C6CD"/>
|
||||
<path d="M5 5L10.6569 10.6569" stroke="white" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M5 11L10.6569 5.34315" stroke="white" stroke-width="2" stroke-linecap="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6630_32795">
|
||||
<rect width="16" height="16" rx="4" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 481 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="4" fill="#4D53E8"/>
|
||||
<path d="M4.34843 4.63774L5.77403 3.81814C7.1922 3.00281 8.93816 3.00832 10.3512 3.8326L11.788 4.6708C12.1656 4.89107 12.1656 5.43666 11.788 5.65693L9.22553 7.15177C8.51019 7.56907 7.62498 7.56632 6.91225 7.14459L4.34225 5.62388C3.966 5.40125 3.96942 4.85564 4.34843 4.63774Z" fill="white" fill-opacity="0.6"/>
|
||||
<path d="M4.34417 6.76575L7.38859 8.42635C7.57198 8.52638 7.68608 8.71858 7.68608 8.92748V12.6493C7.68608 13.0645 7.25698 13.3408 6.87904 13.169L4.83847 12.2414C4.02334 11.8709 3.5 11.0582 3.5 10.1628V7.26688C3.5 6.83359 3.96379 6.55827 4.34417 6.76575Z" fill="white" fill-opacity="0.8"/>
|
||||
<path d="M11.7891 6.76575L8.74471 8.42635C8.56133 8.52638 8.44722 8.71858 8.44722 8.92748V12.6493C8.44722 13.0645 8.87632 13.3408 9.25426 13.169L11.2948 12.2414C12.11 11.8709 12.6333 11.0582 12.6333 10.1628V7.26688C12.6333 6.83359 12.1695 6.55827 11.7891 6.76575Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="LLM Call">
|
||||
<rect width="16" height="16" rx="4" fill="#888892"/>
|
||||
<path id="Union" d="M8.00008 10.8959C7.63189 10.8959 7.33341 11.1943 7.33341 11.5625C7.33341 11.9307 7.63189 12.2292 8.00008 12.2292C8.36827 12.2292 8.66675 11.9307 8.66675 11.5625C8.66675 11.1943 8.36827 10.8959 8.00008 10.8959Z" fill="white"/>
|
||||
<path id="Vector 2757" d="M5.8855 5.84377C5.8855 5.84377 6.30216 4.66669 8.00008 4.66669C9.17717 4.66669 9.99752 5.31229 9.91675 6.21877C9.82099 7.29336 8.62717 7.49585 8.10425 8.54169C7.90841 8.93335 8.00008 9.54169 8.00008 9.54169" stroke="white" stroke-width="1.33333" stroke-linecap="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 722 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="4" fill="#0075FF"/>
|
||||
<path d="M11.7058 10.666V6.6268H12.6546V7.32413H12.6737C12.7994 6.83638 13.1348 6.54297 13.5997 6.54297C13.7178 6.54297 13.8283 6.56202 13.9007 6.58107V7.43845C13.8207 7.40796 13.6759 7.3851 13.5158 7.3851C12.9785 7.3851 12.6546 7.72424 12.6546 8.32249V10.666H11.7058Z" fill="#EBF5FF"/>
|
||||
<path d="M8.75289 10.7346C7.94505 10.7346 7.40015 10.2354 7.40015 9.5152V9.50757C7.40015 8.79119 7.95649 8.3606 8.9358 8.29963L9.99132 8.23485V7.95668C9.99132 7.54895 9.72458 7.30127 9.24445 7.30127C8.79481 7.30127 8.52045 7.51085 8.45948 7.80807L8.45186 7.84237H7.57162L7.57543 7.79664C7.63259 7.08026 8.25371 6.54297 9.27875 6.54297C10.2885 6.54297 10.9401 7.07645 10.9401 7.88809V10.666H9.99132V10.0449H9.96846C9.73601 10.4678 9.28637 10.7346 8.75289 10.7346ZM8.34516 9.47709C8.34516 9.80861 8.61952 10.0106 9.03487 10.0106C9.57978 10.0106 9.99132 9.64856 9.99132 9.16844V8.86359L9.06917 8.92075C8.60047 8.95123 8.34516 9.15319 8.34516 9.47328V9.47709Z" fill="#EBF5FF"/>
|
||||
<path d="M4.5686 10.6666L2.66333 5.16797H3.73409L5.12875 9.61869H5.14781L6.54628 5.16797H7.61704L5.70796 10.6666H4.5686Z" fill="#EBF5FF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="4" fill="#4D53E8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.37196 6.96875C7.722 6.38822 8.35863 6 9.08594 6C10.1905 6 11.0859 6.89543 11.0859 8C11.0859 9.10457 10.1905 10 9.08594 10C8.33283 10 7.67694 9.58374 7.3358 8.96875L3.24739 8.96875C2.69511 8.96875 2.24739 8.52103 2.24739 7.96875C2.24739 7.41646 2.69511 6.96875 3.24739 6.96875L7.37196 6.96875Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.49363 5.40697C6.75396 5.6673 7.17328 5.65923 7.48443 5.46245C8.64393 4.72914 10.1965 4.86787 11.2073 5.87864C12.3789 7.05021 12.3789 8.94971 11.2073 10.1213C10.1964 11.1322 8.64348 11.2708 7.48392 10.5371C7.17285 10.3403 6.75362 10.3322 6.49325 10.5924C6.23274 10.8527 6.23013 11.28 6.52712 11.4978C8.22249 12.741 10.6176 12.5966 12.1501 11.0641C13.8424 9.37182 13.8424 6.6281 12.1501 4.93583C10.6177 3.40339 8.22301 3.25865 6.5277 4.50163C6.23074 4.71935 6.23326 5.14659 6.49363 5.40697Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="4" fill="#4D53E8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.62804 6.96875C8.278 6.38822 7.64137 6 6.91406 6C5.80949 6 4.91406 6.89543 4.91406 8C4.91406 9.10457 5.80949 10 6.91406 10C7.66717 10 8.32306 9.58374 8.6642 8.96875L12.7526 8.96875C13.3049 8.96875 13.7526 8.52103 13.7526 7.96875C13.7526 7.41646 13.3049 6.96875 12.7526 6.96875L8.62804 6.96875Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.50637 5.40697C9.24604 5.6673 8.82672 5.65923 8.51557 5.46245C7.35607 4.72914 5.80348 4.86787 4.79271 5.87864C3.62113 7.05021 3.62113 8.94971 4.79271 10.1213C5.80363 11.1322 7.35652 11.2708 8.51608 10.5371C8.82715 10.3403 9.24638 10.3322 9.50675 10.5924C9.76726 10.8527 9.76987 11.28 9.47288 11.4978C7.77751 12.741 5.38242 12.5966 3.8499 11.0641C2.15763 9.37182 2.15763 6.6281 3.8499 4.93583C5.38234 3.40339 7.77699 3.25865 9.4723 4.50163C9.76926 4.71935 9.76674 5.14659 9.50637 5.40697Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,21 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6630_28189)">
|
||||
<rect width="16" height="16" rx="4" fill="#3EC254"/>
|
||||
<mask id="mask0_6630_28189" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
|
||||
<rect x="0.800293" y="0.799438" width="14.4" height="14.4" fill="#555B61"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_6630_28189)">
|
||||
<rect x="8.70117" y="7.30066" width="3.50049" height="3.50082" rx="1.0502" transform="rotate(-90 8.70117 7.30066)" fill="white" fill-opacity="0.8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.70152 5.49671L6.13574 7.36256L7.06223 8.6366L9.15582 7.11413C8.88137 6.92467 8.70152 6.60796 8.70152 6.24924L8.70152 5.49671Z" fill="white" fill-opacity="0.6"/>
|
||||
<rect x="3.7998" y="10.1002" width="4.20059" height="3.50082" rx="1.0502" transform="rotate(-90 3.7998 10.1002)" fill="white"/>
|
||||
<rect x="8.70117" y="12.2004" width="3.50049" height="3.50082" rx="1.0502" transform="rotate(-90 8.70117 12.2004)" fill="white" fill-opacity="0.8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.70055 10.3485L8.70055 9.75018C8.70055 9.34972 8.92469 9.00161 9.25436 8.82447L7.03489 7.34496L6.16113 8.65572L8.70055 10.3485Z" fill="white" fill-opacity="0.6"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.13452 7.36386L8.7003 5.49802L8.7003 6.25055C8.7003 6.60927 8.88015 6.92598 9.1546 7.11544L7.97634 7.97227L7.0353 7.34497L6.59792 8.0011L6.13452 7.36386Z" fill="white" fill-opacity="0.6"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6630_28189">
|
||||
<rect width="16" height="16" rx="4" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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: [4, 2],
|
||||
lineDash: [],
|
||||
},
|
||||
hover: {
|
||||
// fill: "#6690F2",
|
||||
// stroke: "#000",
|
||||
lineWidth: 1,
|
||||
lineDash: [],
|
||||
},
|
||||
select: {
|
||||
lineWidth: 1,
|
||||
lineDash: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const defaultGlobalStyle: GlobalStyle = {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
// padding: {
|
||||
// top: 50,
|
||||
// right: 60,
|
||||
// bottom: 80,
|
||||
// left: 60,
|
||||
// },
|
||||
// padding: {
|
||||
// top: 30,
|
||||
// right: 0,
|
||||
// bottom: 0,
|
||||
// left: 0,
|
||||
// },
|
||||
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}`;
|
||||
|
||||
// xScale的padding(解决hover后rect边框被截断问题)
|
||||
export const scrollbarMargin = 10;
|
||||
export const datazoomHeight = 20;
|
||||
export const datazoomDecimals = 0;
|
||||
export const datazoomPaddingBottom = 18;
|
||||
@@ -0,0 +1,7 @@
|
||||
.flame-thread-canvas-wrapper {
|
||||
display: flex;
|
||||
|
||||
canvas {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,705 @@
|
||||
/*
|
||||
* 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 max-lines */
|
||||
/* eslint-disable max-lines-per-function */
|
||||
|
||||
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';
|
||||
|
||||
import styles from './index.module.less';
|
||||
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 -- 计算需要
|
||||
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]);
|
||||
|
||||
// 此参数含义: 可视窗口Height / 火焰图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: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
update: (_scale: any, _element: any, params: any) => {
|
||||
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 -- 计算需要
|
||||
(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);
|
||||
// 特化逻辑: 隐藏0刻度
|
||||
if (dx > 0 && value === 0) {
|
||||
return '';
|
||||
}
|
||||
return value !== 0 && axisLabelSuffix !== undefined
|
||||
? `${value}${axisLabelSuffix}`
|
||||
: value;
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: GrammarMarkType.component,
|
||||
componentType: ComponentEnum.grid,
|
||||
tickCount: visibleColumnCount, // vgrammer库的类型写的不严谨,实际是可用的
|
||||
scale: 'xScale',
|
||||
gridType: 'line',
|
||||
gridShape: 'line',
|
||||
dependency: ['viewBox'],
|
||||
// dependency: ["viewBox"],
|
||||
encode: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
update: (_scale: any, _element: any, params: any) => ({
|
||||
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: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
update: (_scale: any, _element: IElement, params: any) => ({
|
||||
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 -- 尺寸计算,无须处理
|
||||
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 -- 尺寸计算,无须定义常量
|
||||
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 -- 尺寸计算,无须定义常量
|
||||
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, // vgrammer库的类型写的不严谨,实际是可用的
|
||||
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: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
update: (_scale: any, _element: any, params: any) => ({
|
||||
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: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
update: (_scale: any, _element: any, params: any) => {
|
||||
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],
|
||||
);
|
||||
|
||||
// 创建/更新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点击事件
|
||||
rectElm?.addEventListener('click', ((event, element) => {
|
||||
onClick?.(event, element);
|
||||
}) as InteractionEventHandler);
|
||||
|
||||
rectElm?.addEventListener('touchstart', ((event, element) => {
|
||||
onClick?.(event, element);
|
||||
}) as InteractionEventHandler);
|
||||
|
||||
// rect hover高亮
|
||||
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
|
||||
className={styles['flame-thread-canvas-wrapper']}
|
||||
style={{
|
||||
...pick(globalStyle, ['width', 'height']),
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
ref={containerRef}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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'>;
|
||||
// 其他字段,会透传
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
@import '../../variables.less';
|
||||
|
||||
.message-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
height: 20px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.message-title-text {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: @text-gray-0;
|
||||
|
||||
:global(.semi-typography-action-copied) {
|
||||
vertical-align: middle
|
||||
}
|
||||
|
||||
svg {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.node-detail-title-description {
|
||||
font-size: 12px;
|
||||
color: @text-gray-3;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
.common-text-content {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
color: @icon-color;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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 { IconCozCopy, IconCozCopyCheck } from '@coze-arch/coze-design/icons';
|
||||
import { Typography } from '@coze-arch/coze-design';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface MessageTitleProps {
|
||||
text: string;
|
||||
copyContent?: string;
|
||||
description?: string;
|
||||
onCopyClick?: (text: string) => void;
|
||||
}
|
||||
|
||||
export const MessageTitle = (props: MessageTitleProps) => {
|
||||
const { text, copyContent, description, onCopyClick } = props;
|
||||
|
||||
return (
|
||||
<div className={styles['message-title']}>
|
||||
<Text
|
||||
className={styles['message-title-text']}
|
||||
copyable={
|
||||
copyContent
|
||||
? {
|
||||
content: copyContent,
|
||||
icon: <IconCozCopy className={styles['copy-icon']} />,
|
||||
successTip: <IconCozCopyCheck />,
|
||||
onCopy: () => onCopyClick?.(copyContent),
|
||||
}
|
||||
: false
|
||||
}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
{description ? (
|
||||
<div className={styles['node-detail-title-description']}>
|
||||
{description}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
.time-tag {
|
||||
background-color: #d8d8d8;
|
||||
font-weight: 600;
|
||||
border-radius: 12px;
|
||||
padding: 6px;
|
||||
margin-left: 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 } from 'react';
|
||||
|
||||
import { isUndefined } from 'lodash-es';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const TimeTag: FC<{ duration: number | string | undefined }> = ({
|
||||
duration,
|
||||
}) =>
|
||||
!isUndefined(duration) ? (
|
||||
<span className={styles['time-tag']}>
|
||||
{duration.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')} ms
|
||||
</span>
|
||||
) : null;
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
.tree {
|
||||
// padding: 10px;
|
||||
display: flex;
|
||||
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;
|
||||
|
||||
/* stylelint-disable-next-line max-nesting-depth */
|
||||
.tree-node-box {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
/*
|
||||
* 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 -- 本组件中会有很多位置计算的数字,无须处理*/
|
||||
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,
|
||||
};
|
||||
|
||||
export const Tree: FC<TreeProps> = ({
|
||||
treeData,
|
||||
selectedKey,
|
||||
indentDisabled = false,
|
||||
lineStyle: gLineStyle,
|
||||
globalStyle,
|
||||
className,
|
||||
onMouseMove,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
onClick,
|
||||
onSelect,
|
||||
}) => {
|
||||
const [hoverKey, setHoverKey] = useState<string>('');
|
||||
|
||||
const { indent, verticalInterval, nodeBoxHeight, offsetX } = useMemo(
|
||||
() =>
|
||||
Object.assign(
|
||||
{},
|
||||
defaultGlobalStyle,
|
||||
globalStyle,
|
||||
) as Required<GlobalStyle>,
|
||||
[globalStyle],
|
||||
);
|
||||
|
||||
/**
|
||||
* 使得指定的selectKey的Line置于顶层。
|
||||
* 通过调整line顺序,来实现z-index效果:key为${selectKey}的line在最上层
|
||||
*/
|
||||
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 === hoverKey) {
|
||||
hoverLine = lines.splice(i, 1)[0];
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// 支持根据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, hoverKey],
|
||||
);
|
||||
|
||||
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],
|
||||
);
|
||||
|
||||
/**
|
||||
* 根据line信息生成svg path。 colNo, rowNum都从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;
|
||||
|
||||
// 起始点
|
||||
const startX = startColNo * indent + offsetX;
|
||||
const startY =
|
||||
startRowNo * nodeHeight + (nodeBoxHeight + verticalInterval / 2);
|
||||
|
||||
if (startColNo === endColNo) {
|
||||
// 竖线的长度
|
||||
const lineASize =
|
||||
(endRowNo - startRowNo - 1) * nodeHeight + verticalInterval;
|
||||
// 移动到起始点
|
||||
const moveToStartPoint = `M ${startX} ${startY + lineGap}`;
|
||||
// 竖线
|
||||
const lineA = `L ${startX} ${startY + lineASize}`;
|
||||
return `${moveToStartPoint} ${lineA}`;
|
||||
} else {
|
||||
// 竖线的长度
|
||||
const lineASize =
|
||||
(endRowNo - startRowNo - 1) * nodeHeight +
|
||||
verticalInterval / 2 +
|
||||
nodeHeight / 2 -
|
||||
lineRadius;
|
||||
// 横线的长度
|
||||
const lineBSize =
|
||||
(endColNo - startColNo) * indent - offsetX - lineRadius;
|
||||
// 结束点的坐标
|
||||
const endX = startX + lineBSize + lineRadius;
|
||||
const endY = startY + lineASize + lineRadius;
|
||||
|
||||
// 移动到起始点
|
||||
const moveToStartPoint = `M ${startX} ${startY + lineGap}`;
|
||||
// 竖线
|
||||
const lineA = `L ${startX} ${startY + lineASize}`;
|
||||
// 二次贝塞尔曲线
|
||||
const qbc = `Q ${startX} ${endY} ${startX + lineRadius} ${endY}`;
|
||||
// 横线
|
||||
const lineB = `L ${endX - lineGap} ${endY}`;
|
||||
return `${moveToStartPoint} ${lineA} ${qbc} ${lineB}`;
|
||||
}
|
||||
},
|
||||
[genLineStyle, indent, nodeBoxHeight, offsetX, verticalInterval],
|
||||
);
|
||||
|
||||
const genLineAttrs = useCallback(
|
||||
(nodeKey: string, lineStyle: LineStyle) => {
|
||||
if (hoverKey !== selectedKey) {
|
||||
if (nodeKey === hoverKey) {
|
||||
return mergeWith({}, lineStyle.normal, lineStyle.hover);
|
||||
}
|
||||
if (nodeKey === selectedKey) {
|
||||
return mergeWith({}, lineStyle.normal, lineStyle.select);
|
||||
}
|
||||
return lineStyle.normal;
|
||||
} else {
|
||||
if (nodeKey === hoverKey) {
|
||||
return mergeWith(
|
||||
{},
|
||||
lineStyle.normal,
|
||||
lineStyle.select,
|
||||
lineStyle.hover,
|
||||
);
|
||||
} else {
|
||||
return lineStyle.normal;
|
||||
}
|
||||
}
|
||||
},
|
||||
[hoverKey, 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: hoverKey === 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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
import { type SpanNode } from '../../typings/graph';
|
||||
|
||||
export type LineAttrs = Pick<
|
||||
SVGAttributes<unknown>,
|
||||
'stroke' | 'strokeDasharray' | 'strokeWidth'
|
||||
> & {
|
||||
lineRadius?: number; // line圆角半径 注意:这个数值不要大于 indent/2
|
||||
lineGap?: number; // line距离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; // 默认值 true
|
||||
indentDisabled?: boolean; // 关闭缩进。 仅针对如下场景生效:子节点中的最后一个节点
|
||||
lineStyle?: LineStyle; // 当指定了此属性时,会覆盖全局的lineStyle
|
||||
children?: TreeNode[];
|
||||
zIndex?: number;
|
||||
// 其他字段,会透传
|
||||
extra?: unknown;
|
||||
}
|
||||
|
||||
export interface TreeNodeInfo {
|
||||
isCollapsed?: boolean;
|
||||
isKeyNode?: boolean;
|
||||
keyNodeParentId?: string;
|
||||
}
|
||||
|
||||
export interface FilteredTreeNode extends SpanNode, TreeNodeInfo {
|
||||
children: FilteredTreeNode[];
|
||||
}
|
||||
|
||||
export type TreeNodeExtra = Omit<TreeNode, 'children'> & {
|
||||
colNo: number;
|
||||
rowNo: number;
|
||||
unindented: boolean; // 相对于父节点,是否未缩进
|
||||
selected: boolean; // 是否被选中
|
||||
hover: boolean; // 是否hover
|
||||
};
|
||||
|
||||
// 拉平后的TreeNode信息
|
||||
export type TreeNodeFlatten = Omit<TreeNodeExtra, 'selected' | 'hover'>;
|
||||
|
||||
export interface Line {
|
||||
startNode: TreeNodeFlatten;
|
||||
endNode: TreeNodeFlatten;
|
||||
}
|
||||
|
||||
export interface GlobalStyle {
|
||||
indent?: number; // 父节点和子节点的缩进距离
|
||||
verticalInterval?: number; // node节点的垂直间距
|
||||
nodeBoxHeight?: number; // node-box节点的高度
|
||||
offsetX?: number;
|
||||
}
|
||||
|
||||
export interface MouseEventParams {
|
||||
event: React.MouseEvent<HTMLDivElement>;
|
||||
node: TreeNodeExtra;
|
||||
}
|
||||
|
||||
export interface TreeProps {
|
||||
treeData: TreeNode;
|
||||
selectedKey?: string;
|
||||
indentDisabled?: boolean; // 关闭缩进。 仅针对如下场景生效:最后一个节点
|
||||
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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
* 基于TreeData生成:
|
||||
*
|
||||
* @param treeData tree原始数据
|
||||
* @param options.indentDisabled 是否取消缩进。仅针对下述场景有效:异常节点+最后一个节点
|
||||
*
|
||||
* @returns
|
||||
* 1. nodes, 拉平后的node节点信息
|
||||
* 2. lines, 用于将node进行连接
|
||||
*/
|
||||
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, // 未缩进
|
||||
};
|
||||
nodes.push(nodeFlatten);
|
||||
if (fatherNodeFlatten !== undefined) {
|
||||
lines.push({
|
||||
startNode: fatherNodeFlatten,
|
||||
endNode: nodeFlatten,
|
||||
});
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
const childNodes = node.children;
|
||||
|
||||
childNodes.forEach((childNode, index) => {
|
||||
// 取消缩进。 生效场景:异常节点+最后一个节点
|
||||
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,35 @@
|
||||
/*
|
||||
* 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 Value, TagType } from '../typings/idl';
|
||||
|
||||
export enum StatusCode {
|
||||
SUCCESS = 0,
|
||||
ERROR = 1,
|
||||
}
|
||||
|
||||
export const META_TAGS_VALUE_TYPE_MAP: Record<TagType, keyof Value> = {
|
||||
[TagType.STRING]: 'v_str',
|
||||
[TagType.DOUBLE]: 'v_double',
|
||||
[TagType.BOOL]: 'v_bool',
|
||||
[TagType.LONG]: 'v_long',
|
||||
[TagType.BYTES]: 'v_bytes',
|
||||
};
|
||||
|
||||
export enum RegionMap {
|
||||
CN = 'cn',
|
||||
I18N = 'i18N',
|
||||
}
|
||||
@@ -0,0 +1,621 @@
|
||||
/*
|
||||
* 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 max-lines */
|
||||
import {
|
||||
type TraceFlamethreadProps,
|
||||
type NodePresetConfig,
|
||||
type TraceTreeStyleDefaultProps,
|
||||
} from '../typings/graph';
|
||||
import { type LineStyle } from '../common/tree';
|
||||
import { type RectStyle } from '../common/flamethread';
|
||||
import { ReactComponent as WorkflowIcon } from '../assets/graph/span-type/icon-workflow.svg';
|
||||
import { ReactComponent as WorkflowStartIcon } from '../assets/graph/span-type/icon-workflow-start.svg';
|
||||
import { ReactComponent as WorkflowEndIcon } from '../assets/graph/span-type/icon-workflow-end.svg';
|
||||
import { ReactComponent as PluginToolIcon } from '../assets/graph/span-type/icon-plugin-tool.svg';
|
||||
import { ReactComponent as MessageIcon } from '../assets/graph/span-type/icon-message.svg';
|
||||
import { ReactComponent as LlmCallIcon } from '../assets/graph/span-type/icon-llm-call.svg';
|
||||
import { ReactComponent as KnowledgeIcon } from '../assets/graph/span-type/icon-knowledge.svg';
|
||||
import { ReactComponent as ConditionIcon } from '../assets/graph/span-type/icon-condition.svg';
|
||||
import { ReactComponent as CodeIcon } from '../assets/graph/span-type/icon-code.svg';
|
||||
import { ReactComponent as CardIcon } from '../assets/graph/span-type/icon-card.svg';
|
||||
import { ReactComponent as BotIcon } from '../assets/graph/span-type/icon-bot.svg';
|
||||
import { ReactComponent as AgentIcon } from '../assets/graph/span-type/icon-agent.svg';
|
||||
import { StatusCode } from './basic';
|
||||
|
||||
export const TRACE_TREE_STYLE_DEFAULT_PROPS: TraceTreeStyleDefaultProps = {
|
||||
lineStyle: {
|
||||
normal: {
|
||||
stroke: '#C6C6CD',
|
||||
strokeWidth: 1,
|
||||
},
|
||||
hover: {
|
||||
stroke: '#B4BAF6',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
select: {
|
||||
stroke: '#B4BAF6',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
},
|
||||
globalStyle: {
|
||||
nodeBoxHeight: 20,
|
||||
verticalInterval: 12,
|
||||
offsetX: 10,
|
||||
},
|
||||
};
|
||||
|
||||
export const SPAN_STATUS_CONFIG: Record<
|
||||
StatusCode,
|
||||
{
|
||||
lineStyle?: LineStyle;
|
||||
}
|
||||
> = {
|
||||
[StatusCode.SUCCESS]: {},
|
||||
[StatusCode.ERROR]: {
|
||||
lineStyle: {
|
||||
normal: {
|
||||
stroke: '#FF441E',
|
||||
},
|
||||
hover: {
|
||||
stroke: '#FF441E',
|
||||
},
|
||||
select: {
|
||||
stroke: '#FF441E',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export enum PresetSpanType {
|
||||
UserInput = 'UserInput',
|
||||
ThirdParty = 'ThirdParty',
|
||||
ScheduledTasks = 'ScheduledTasks',
|
||||
OpenDialog = 'OpenDialog',
|
||||
InvokeAgent = 'InvokeAgent',
|
||||
RestartAgent = 'RestartAgent',
|
||||
SwitchAgent = 'SwitchAgent',
|
||||
LLMCall = 'LLMCall',
|
||||
LLMBatchCall = 'LLMBatchCall',
|
||||
Workflow = 'Workflow',
|
||||
WorkflowStart = 'WorkflowStart',
|
||||
WorkflowEnd = 'WorkflowEnd',
|
||||
PluginTool = 'PluginTool',
|
||||
PluginToolBatch = 'PluginToolBatch',
|
||||
Knowledge = 'Knowledge',
|
||||
Code = 'Code',
|
||||
CodeBatch = 'CodeBatch',
|
||||
Condition = 'Condition',
|
||||
Chain = 'Chain',
|
||||
Card = 'Card',
|
||||
WorkflowMessage = 'WorkflowMessage',
|
||||
WorkflowLLMCall = 'WorkflowLLMCall',
|
||||
WorkflowLLMBatchCall = 'WorkflowLLMBatchCall',
|
||||
WorkflowCode = 'WorkflowCode',
|
||||
WorkflowCodeBatch = 'WorkflowCodeBatch',
|
||||
WorkflowCondition = 'WorkflowCondition',
|
||||
WorkflowPluginTool = 'WorkflowPluginTool',
|
||||
WorkflowPluginToolBatch = 'WorkflowPluginToolBatch',
|
||||
WorkflowKnowledge = 'WorkflowKnowledge',
|
||||
}
|
||||
|
||||
export const NODE_PRESET_CONFIG_MAP: Record<PresetSpanType, NodePresetConfig> =
|
||||
{
|
||||
[PresetSpanType.UserInput]: {
|
||||
icon: <BotIcon />,
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(254, 242, 237)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(254, 221, 210)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(253, 183, 165)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[PresetSpanType.ThirdParty]: {
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(253, 236, 239)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(251, 207, 216)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(246, 160, 181)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[PresetSpanType.ScheduledTasks]: {
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(247, 233, 247)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(239, 202, 240)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(221, 155, 224)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[PresetSpanType.OpenDialog]: {
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(243, 237, 249)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(226, 209, 244)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(196, 167, 233)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[PresetSpanType.InvokeAgent]: {
|
||||
icon: <AgentIcon />,
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(236, 239, 248)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(209, 216, 240)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(167, 179, 225)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[PresetSpanType.RestartAgent]: {
|
||||
icon: <AgentIcon />,
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(234, 245, 255)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(203, 231, 254)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(152, 205, 253)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[PresetSpanType.SwitchAgent]: {
|
||||
icon: <AgentIcon />,
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(233, 247, 253)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(201, 236, 252)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(149, 216, 248)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
[PresetSpanType.LLMCall]: {
|
||||
icon: <LlmCallIcon />,
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(229, 247, 248)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(194, 239, 240)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(138, 221, 226)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
[PresetSpanType.LLMBatchCall]: {
|
||||
icon: <LlmCallIcon />,
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(228, 247, 244)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(192, 240, 232)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(135, 224, 211)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
[PresetSpanType.Workflow]: {
|
||||
icon: <WorkflowIcon />,
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(236, 247, 236)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(208, 240, 209)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(164, 224, 167)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[PresetSpanType.WorkflowStart]: {
|
||||
icon: <WorkflowStartIcon />,
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(243, 248, 236)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(227, 240, 208)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(200, 226, 165)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[PresetSpanType.WorkflowEnd]: {
|
||||
icon: <WorkflowEndIcon />,
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(242, 250, 230)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(227, 246, 197)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(203, 237, 142)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[PresetSpanType.PluginTool]: {
|
||||
icon: <PluginToolIcon />,
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(255, 253, 234)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(254, 251, 203)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(253, 243, 152)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[PresetSpanType.PluginToolBatch]: {
|
||||
icon: <PluginToolIcon />,
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(254, 251, 235)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(252, 245, 206)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(249, 232, 158)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[PresetSpanType.Knowledge]: {
|
||||
icon: <KnowledgeIcon />,
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(255, 248, 234)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(254, 238, 204)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(254, 217, 152)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// from
|
||||
[PresetSpanType.Code]: {
|
||||
icon: <CodeIcon />,
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(254, 242, 237)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(254, 221, 210)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(253, 183, 165)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
[PresetSpanType.CodeBatch]: {
|
||||
icon: <CodeIcon />,
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(253, 236, 239)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(251, 207, 216)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(246, 160, 181)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
[PresetSpanType.Condition]: {
|
||||
icon: <ConditionIcon />,
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(247, 233, 247)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(239, 202, 240)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(221, 155, 224)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[PresetSpanType.Chain]: {
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(243, 237, 249)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(226, 209, 244)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(196, 167, 233)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[PresetSpanType.Card]: {
|
||||
icon: <CardIcon />,
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(236, 239, 248)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(209, 216, 240)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(167, 179, 225)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[PresetSpanType.WorkflowMessage]: {
|
||||
icon: <MessageIcon />,
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(234, 245, 255)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(203, 231, 254)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(152, 205, 253)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[PresetSpanType.WorkflowLLMCall]: {
|
||||
icon: <LlmCallIcon />,
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(233, 247, 253)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(201, 236, 252)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(149, 216, 248)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[PresetSpanType.WorkflowLLMBatchCall]: {
|
||||
icon: <LlmCallIcon />,
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(229, 247, 248)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(194, 239, 240)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(138, 221, 226)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
[PresetSpanType.WorkflowCode]: {
|
||||
icon: <CodeIcon />,
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(228, 247, 244)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(192, 240, 232)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(135, 224, 211)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
[PresetSpanType.WorkflowCodeBatch]: {
|
||||
icon: <CodeIcon />,
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(236, 247, 236)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(208, 240, 209)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(164, 224, 167)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
[PresetSpanType.WorkflowCondition]: {
|
||||
icon: <ConditionIcon />,
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(243, 248, 236)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(227, 240, 208)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(200, 226, 165)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[PresetSpanType.WorkflowPluginTool]: {
|
||||
icon: <PluginToolIcon />,
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(242, 250, 230)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(227, 246, 197)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(203, 237, 142)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[PresetSpanType.WorkflowPluginToolBatch]: {
|
||||
icon: <PluginToolIcon />,
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(255, 253, 234)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(254, 251, 203)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(253, 243, 152)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[PresetSpanType.WorkflowKnowledge]: {
|
||||
icon: <KnowledgeIcon />,
|
||||
flamethread: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: 'rgb(254, 251, 235)',
|
||||
},
|
||||
hover: {
|
||||
fill: 'rgb(252, 245, 206)',
|
||||
},
|
||||
select: {
|
||||
fill: 'rgb(249, 232, 158)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const FLAME_THREAD_DEFAULT_CONFIG: Pick<
|
||||
TraceFlamethreadProps,
|
||||
'rectStyle' | 'labelStyle' | 'rowHeight' | 'globalStyle' | 'datazoomDecimals'
|
||||
> = {
|
||||
labelStyle: {
|
||||
position: 'inside-left',
|
||||
fontSize: 12,
|
||||
fill: '#1D1C23CC',
|
||||
},
|
||||
rowHeight: 50,
|
||||
globalStyle: {},
|
||||
datazoomDecimals: 1,
|
||||
};
|
||||
|
||||
export const FLAME_THREAD_DEFAULT_RECT_STYLE: RectStyle = {
|
||||
normal: {
|
||||
fill: '#F7F7FA',
|
||||
},
|
||||
hover: {
|
||||
fill: '#F0F0F5',
|
||||
},
|
||||
select: {
|
||||
fill: '#C6C6CD',
|
||||
},
|
||||
};
|
||||
|
||||
export const FLAME_THREAD_DEFAULT_TOOLTIP_STYLE = {
|
||||
fill: '#212629',
|
||||
shape: {
|
||||
symbolType: 'square',
|
||||
fill: '#212629',
|
||||
size: 5,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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 STATUS_CODE_KEY = '_status_code';
|
||||
|
||||
export enum ObservationModules {
|
||||
RUN_TREE = 'run_tree',
|
||||
FLAME_THREAD = 'flame_thread',
|
||||
INPUT = 'input',
|
||||
OUTPUT = 'output',
|
||||
META_TAGS = 'meta_tags',
|
||||
SPAN_DETAIL = 'span_detail',
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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 { isString } from 'lodash-es';
|
||||
import {
|
||||
JsonViewer,
|
||||
type JsonViewerProps,
|
||||
type Path,
|
||||
} from '@textea/json-viewer';
|
||||
|
||||
const objectToCopyableString = (value: any): string => {
|
||||
if (isString(value)) {
|
||||
return value;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(value, null, 4);
|
||||
} catch (_err) {
|
||||
return String(value);
|
||||
}
|
||||
};
|
||||
|
||||
export const CustomJsonViewer = <T,>(props: JsonViewerProps<T>) => {
|
||||
const { onCopy } = props;
|
||||
return (
|
||||
<JsonViewer
|
||||
style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
fontSize: '12px',
|
||||
}}
|
||||
rootName={false}
|
||||
{...props}
|
||||
onCopy={(
|
||||
_path: Path,
|
||||
value: unknown,
|
||||
copy: (value: string) => Promise<void>,
|
||||
) => {
|
||||
copy(objectToCopyableString(value));
|
||||
onCopy?.(_path, objectToCopyableString(value), copy);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export type { JsonViewerProps };
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 从 @flow-devops/observation-components copy
|
||||
* 暂不去理解其中逻辑
|
||||
*/
|
||||
export { TraceTree } from './trace-tree';
|
||||
export { TraceFlameThread } from './trace-flame-thread';
|
||||
export { MessagePanel, type MessagePanelProps } from './message-panel';
|
||||
export { spans2SpanNodes } from './utils/graph';
|
||||
export { ObservationModules } from './consts';
|
||||
@@ -0,0 +1,54 @@
|
||||
@import '../variables.less';
|
||||
@import '../scroll-bar.less';
|
||||
|
||||
.message-panel-content {
|
||||
.webkit-scrollbar_mixin();
|
||||
|
||||
overflow: auto;
|
||||
|
||||
max-height: 340px;
|
||||
padding: 8px 4px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
|
||||
border: 1px solid @border-color;
|
||||
border-radius: 8px;
|
||||
|
||||
.message-panel-content-detail-text {
|
||||
font-size: 12px;
|
||||
color: @text-gray-0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.message-panel-content-json-container {
|
||||
:global(.string-value) {
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-panel-content_error {
|
||||
border: 1px solid @error-color;
|
||||
}
|
||||
|
||||
.encryption {
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 0.5rem;
|
||||
|
||||
max-height: 600px;
|
||||
padding: 8px 4px;
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
border: 1px solid #1d1c2314;
|
||||
border-radius: 8px;
|
||||
|
||||
}
|
||||
|
||||
.encryption-tip {
|
||||
margin-top: 8px;
|
||||
font-weight: 600;
|
||||
color: rgb(239,68,68);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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 ReactJson from 'react-json-view';
|
||||
|
||||
import { type CSSProperties } from 'react';
|
||||
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
import { jsonParse, textWithFallback } from '../utils';
|
||||
import { type I18nMapping } from '../typings/basic';
|
||||
import { CustomJsonViewer, type JsonViewerProps } from '../custom-json-viewer';
|
||||
import { type ObservationModules } from '../consts';
|
||||
import { MessageTitle } from '../common/message-title';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface MessagePanelProps {
|
||||
content: string;
|
||||
category: ObservationModules;
|
||||
i18nMapping: I18nMapping;
|
||||
className?: string;
|
||||
isError?: boolean;
|
||||
onCopyClick?: (text: string) => void;
|
||||
panelStyle?: CSSProperties;
|
||||
encrypted?: boolean;
|
||||
jsonViewerProps?: Partial<JsonViewerProps<Record<string, unknown> | string>>;
|
||||
}
|
||||
|
||||
export const MessagePanel = (props: MessagePanelProps) => {
|
||||
const {
|
||||
content,
|
||||
category,
|
||||
i18nMapping,
|
||||
isError,
|
||||
onCopyClick,
|
||||
panelStyle,
|
||||
encrypted,
|
||||
jsonViewerProps = {},
|
||||
} = props;
|
||||
const i18nInfo = i18nMapping[category];
|
||||
const inputValue = jsonParse(textWithFallback(content));
|
||||
|
||||
return (
|
||||
<>
|
||||
<MessageTitle
|
||||
text={i18nInfo?.title ?? ''}
|
||||
copyContent={
|
||||
encrypted
|
||||
? ''
|
||||
: typeof inputValue === 'string'
|
||||
? inputValue
|
||||
: JSON.stringify(inputValue)
|
||||
}
|
||||
onCopyClick={onCopyClick}
|
||||
/>
|
||||
{encrypted ? (
|
||||
<div className={styles.encryption}>
|
||||
<span>安全提示</span>
|
||||
<span className={styles['encryption-tip']}>
|
||||
当前数据已被加密,无法查看
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={clsx(
|
||||
styles['message-panel-content'],
|
||||
isError && styles['message-panel-content_error'],
|
||||
)}
|
||||
style={panelStyle}
|
||||
>
|
||||
{typeof inputValue === 'string' ? (
|
||||
<div className={styles['message-panel-content-detail-text']}>
|
||||
{inputValue}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles['message-panel-content-json-container']}>
|
||||
<CustomJsonViewer
|
||||
{...jsonViewerProps}
|
||||
className={styles.content}
|
||||
value={inputValue}
|
||||
defaultInspectDepth={5}
|
||||
onCopy={(_, txt) => onCopyClick?.(txt as string)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
@scrollbar-size: 11px;
|
||||
@scrollbar-padding: 2px;
|
||||
@transition-timing-function-standard: cubic-bezier(0.34, 0.69, 0.1, 1);
|
||||
|
||||
.webkit-scrollbar_mixin() {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(125, 125, 125, 30%);
|
||||
background-clip: padding-box;
|
||||
border: @scrollbar-padding solid transparent;
|
||||
border-radius: 9999px;
|
||||
|
||||
transition: background .2s @transition-timing-function-standard;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: @scrollbar-size;
|
||||
height: @scrollbar-size;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
background-color: rgba(125, 125, 125, 60%) !important;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar:hover {
|
||||
width: @scrollbar-size;
|
||||
height: @scrollbar-size;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-corner {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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, useMemo, useState } from 'react';
|
||||
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
|
||||
import { spanData2flamethreadData } from '../utils/graph';
|
||||
import { type SpanNode, type TraceFlamethreadProps } from '../typings/graph';
|
||||
import {
|
||||
FLAME_THREAD_DEFAULT_CONFIG,
|
||||
FLAME_THREAD_DEFAULT_TOOLTIP_STYLE,
|
||||
} from '../consts/graph';
|
||||
import {
|
||||
Flamethread,
|
||||
type RectStyle,
|
||||
type RectNode,
|
||||
type Tooltip,
|
||||
type LabelText,
|
||||
} from '../common/flamethread';
|
||||
|
||||
export const TraceFlameThread = (props: TraceFlamethreadProps) => {
|
||||
const [flamethreadData, setFlamethreadData] = useState<RectNode[]>([]);
|
||||
|
||||
const {
|
||||
spans,
|
||||
selectedSpanId,
|
||||
rectStyle: _rectStyle,
|
||||
labelStyle: _labelStyle,
|
||||
globalStyle: _globalStyle,
|
||||
visibleColumnCount,
|
||||
datazoomDecimals = FLAME_THREAD_DEFAULT_CONFIG.datazoomDecimals,
|
||||
axisLabelSuffix,
|
||||
disableViewScroll,
|
||||
enableAutoFit,
|
||||
onClick,
|
||||
renderGraphNodeConfig,
|
||||
} = props;
|
||||
|
||||
const { traceFlamethreadCustomRenderer, customTypeConfigMap } =
|
||||
renderGraphNodeConfig || {};
|
||||
|
||||
useEffect(() => {
|
||||
const rectNodes = spanData2flamethreadData(spans, customTypeConfigMap);
|
||||
setFlamethreadData(rectNodes);
|
||||
}, [spans]);
|
||||
|
||||
const rectStyle = useMemo((): RectStyle => {
|
||||
const defaultRectStyle = FLAME_THREAD_DEFAULT_CONFIG.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, FLAME_THREAD_DEFAULT_CONFIG.labelStyle),
|
||||
[_labelStyle],
|
||||
);
|
||||
|
||||
const globalStyle = useMemo(
|
||||
() =>
|
||||
Object.assign({}, _globalStyle, FLAME_THREAD_DEFAULT_CONFIG.globalStyle),
|
||||
[_globalStyle],
|
||||
);
|
||||
|
||||
const tooltip: Tooltip = useMemo(
|
||||
() => ({
|
||||
title: {
|
||||
value: () => ({}),
|
||||
},
|
||||
content: (datum: RectNode) => {
|
||||
const { span } = (datum.extra ?? {}) as { span: SpanNode };
|
||||
if (!span) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const tips =
|
||||
traceFlamethreadCustomRenderer?.renderTooltip?.(span) ?? [];
|
||||
|
||||
return tips.map(({ key, value, fill }) => ({
|
||||
key: {
|
||||
text: key,
|
||||
fill: fill ?? FLAME_THREAD_DEFAULT_TOOLTIP_STYLE.fill,
|
||||
},
|
||||
value: {
|
||||
text: value ?? '',
|
||||
fill: fill ?? FLAME_THREAD_DEFAULT_TOOLTIP_STYLE.fill,
|
||||
},
|
||||
shape: FLAME_THREAD_DEFAULT_TOOLTIP_STYLE.shape,
|
||||
}));
|
||||
},
|
||||
}),
|
||||
[traceFlamethreadCustomRenderer],
|
||||
);
|
||||
|
||||
const labelText: LabelText = useMemoizedFn((datum: RectNode) => {
|
||||
const { span } = (datum.extra ?? {}) as { span: SpanNode };
|
||||
return span.name ?? '';
|
||||
});
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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 { createContext, useContext } from 'react';
|
||||
|
||||
import { type TreeNodeInfo } from '../common/tree/typing';
|
||||
|
||||
interface TraceTreeContext {
|
||||
treeMap: Record<string, TreeNodeInfo>;
|
||||
onCollapse: (id: string, collapsed: boolean) => void;
|
||||
}
|
||||
|
||||
export const traceTreeContext = createContext<TraceTreeContext>({
|
||||
treeMap: {},
|
||||
onCollapse: (id, collapsed) => '',
|
||||
});
|
||||
|
||||
export const useTraceTree = () => useContext(traceTreeContext);
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* 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 { clsx } from 'clsx';
|
||||
import {
|
||||
IconCozClockFill,
|
||||
IconCozArrowLeftFill,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import { Tag, Tooltip, IconButton } from '@coze-arch/coze-design';
|
||||
|
||||
import { formatDuration } from '../utils/time';
|
||||
import { isSuccessStatus } from '../utils/basic';
|
||||
import {
|
||||
type SpanNode,
|
||||
type NodeConfig,
|
||||
type NodePresetConfig,
|
||||
type SpanNodeRenderOptions,
|
||||
type RenderGraphNodeConfig,
|
||||
} from '../typings/graph';
|
||||
import { NODE_PRESET_CONFIG_MAP, type PresetSpanType } from '../consts/graph';
|
||||
import { type TreeNodeExtra } from '../common/tree';
|
||||
import { ReactComponent as IconBroken } from '../assets/graph/icon-broken.svg';
|
||||
import { useTraceTree } from './context';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export function getNodeConfig(
|
||||
params: Pick<SpanNode, 'name' | 'type'> & {
|
||||
customTypeConfigMap?: Record<string | number, NodePresetConfig | undefined>;
|
||||
},
|
||||
): NodeConfig {
|
||||
const { name, type, customTypeConfigMap } = params;
|
||||
const nodeConfig: NodeConfig = {
|
||||
character: name?.charAt(0),
|
||||
color: '#b4baf6',
|
||||
};
|
||||
|
||||
if (!type) {
|
||||
return nodeConfig;
|
||||
} else {
|
||||
const presetNodeConfig =
|
||||
customTypeConfigMap?.[type] ??
|
||||
NODE_PRESET_CONFIG_MAP[type as PresetSpanType];
|
||||
|
||||
return {
|
||||
...nodeConfig,
|
||||
...presetNodeConfig,
|
||||
};
|
||||
}
|
||||
}
|
||||
interface CustomTreeNodeProps {
|
||||
renderGraphNodeConfig?: RenderGraphNodeConfig;
|
||||
nodeData: TreeNodeExtra;
|
||||
}
|
||||
|
||||
export const CustomTreeNode = ({
|
||||
renderGraphNodeConfig,
|
||||
nodeData,
|
||||
}: CustomTreeNodeProps) => {
|
||||
const { treeMap, onCollapse } = useTraceTree();
|
||||
|
||||
const { selected, hover, key } = nodeData;
|
||||
const { spanNode } = nodeData?.extra as { spanNode: SpanNode };
|
||||
const { type, name, status_code, isBroken, span_id, duration } = spanNode;
|
||||
const { isCollapsed } = treeMap[key] || {};
|
||||
|
||||
const isLeaf = !spanNode.children || spanNode.children.length === 0;
|
||||
|
||||
const { customTypeConfigMap, traceTreeCustomRenderer } =
|
||||
renderGraphNodeConfig || {};
|
||||
|
||||
const { icon, character, color } = getNodeConfig({
|
||||
type,
|
||||
name,
|
||||
customTypeConfigMap,
|
||||
});
|
||||
|
||||
const customTreeNodeContent = useMemo(
|
||||
() => (
|
||||
<div
|
||||
className={clsx(styles['tree-custom-node-content'], {
|
||||
[styles.selected]: selected,
|
||||
[styles.hover]: hover,
|
||||
[styles.error]: !isSuccessStatus(status_code),
|
||||
})}
|
||||
>
|
||||
<span className={styles.title}>{name}</span>
|
||||
</div>
|
||||
),
|
||||
[selected, hover, status_code, name],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles['tree-custom-node']}>
|
||||
<div className={styles['tree-custom-node-icon']}>
|
||||
{isBroken ? <IconBroken /> : null}
|
||||
{icon ?? (
|
||||
<span
|
||||
className={styles['tree-custom-node-icon-default']}
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
}}
|
||||
>
|
||||
{character}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{traceTreeCustomRenderer?.renderTooltip ? (
|
||||
<Tooltip
|
||||
position="right"
|
||||
{...traceTreeCustomRenderer.renderTooltip?.(spanNode)}
|
||||
>
|
||||
{customTreeNodeContent}
|
||||
</Tooltip>
|
||||
) : (
|
||||
customTreeNodeContent
|
||||
)}
|
||||
<Tag
|
||||
color={
|
||||
Number(duration) > 60000
|
||||
? 'red'
|
||||
: Number(duration) > 10000
|
||||
? 'yellow'
|
||||
: 'green'
|
||||
}
|
||||
type="light"
|
||||
className={styles['node-tag']}
|
||||
size="mini"
|
||||
prefixIcon={<IconCozClockFill />}
|
||||
>
|
||||
{formatDuration(Number(duration))}
|
||||
</Tag>
|
||||
{traceTreeCustomRenderer?.renderExtra?.(spanNode)}
|
||||
{!isLeaf && (
|
||||
<Tooltip content={isCollapsed ? '展开' : '收起'} position="right">
|
||||
<IconButton
|
||||
color="secondary"
|
||||
size="mini"
|
||||
icon={
|
||||
<IconCozArrowLeftFill
|
||||
className={clsx(!isCollapsed && styles['is-open'])}
|
||||
/>
|
||||
}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onCollapse?.(span_id || '', !isCollapsed);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const renderCustomTreeNode =
|
||||
({ renderGraphNodeConfig }: SpanNodeRenderOptions) =>
|
||||
(nodeData: TreeNodeExtra) => (
|
||||
<CustomTreeNode
|
||||
nodeData={nodeData}
|
||||
renderGraphNodeConfig={renderGraphNodeConfig}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,133 @@
|
||||
/* stylelint-disable max-nesting-depth */
|
||||
@import '../scroll-bar.less';
|
||||
@import '../variables.less';
|
||||
|
||||
|
||||
.trace-tree {
|
||||
overflow: visible;
|
||||
|
||||
}
|
||||
|
||||
.trace-trees-wrapper {
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex:1;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
justify-content: flex-start;
|
||||
|
||||
min-height: 0;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
.webkit-scrollbar_mixin();
|
||||
}
|
||||
|
||||
.trace-tree-layout {
|
||||
display: flex;
|
||||
flex:1;
|
||||
flex-direction: column;
|
||||
gap:8px;
|
||||
height:100%;
|
||||
}
|
||||
|
||||
.tree-custom-node {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
|
||||
.tree-custom-node-icon {
|
||||
display: inline-flex;
|
||||
gap: 3px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.tree-custom-node-icon-default {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: @white;
|
||||
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.tree-custom-node-content {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
.title {
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
|
||||
// max-width: 268px;
|
||||
padding: 1px 8px;
|
||||
|
||||
font-size: 12px;
|
||||
color: @text-gray-0;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
.title {
|
||||
background: #F1F2FD;
|
||||
}
|
||||
}
|
||||
|
||||
&.hover {
|
||||
.title {
|
||||
background: #F1F2FD;
|
||||
}
|
||||
}
|
||||
|
||||
&.error {
|
||||
.title {
|
||||
color: @error-color;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
.title {
|
||||
background: #FFE0D2;
|
||||
}
|
||||
}
|
||||
|
||||
&.hover {
|
||||
.title {
|
||||
background: #FFF3EE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: default;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tree-data-declare {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.node-tag {
|
||||
max-width: 100%;
|
||||
height: 16px;
|
||||
padding-right:4px;
|
||||
padding-left:4px;
|
||||
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.is-open {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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, useMemo, useState } from 'react';
|
||||
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
import { geSpanNodeInfoMap, spanNode2TreeNode } from '../utils/graph';
|
||||
import { type TraceTreeProps } from '../typings/graph';
|
||||
import { TRACE_TREE_STYLE_DEFAULT_PROPS } from '../consts/graph';
|
||||
import { type TreeNodeInfo } from '../common/tree/typing';
|
||||
import { Tree, type TreeNode } from '../common/tree';
|
||||
import { traceTreeContext } from './context';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const TraceTree = (props: TraceTreeProps) => {
|
||||
const {
|
||||
spans: spanNodes,
|
||||
selectedSpanId,
|
||||
indentDisabled,
|
||||
lineStyle: _lineStyle,
|
||||
globalStyle: _globalStyle,
|
||||
className,
|
||||
onCollapseChange,
|
||||
renderGraphNodeConfig,
|
||||
defaultDisplayMode,
|
||||
hostUser,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const [treeData, setTreeData] = useState<TreeNode[]>();
|
||||
const [treeMap, setTreeMap] = useState<Record<string, TreeNodeInfo>>(() =>
|
||||
geSpanNodeInfoMap(spanNodes),
|
||||
);
|
||||
|
||||
const lineStyle = useMemo(
|
||||
() => ({
|
||||
normal: Object.assign(
|
||||
{},
|
||||
TRACE_TREE_STYLE_DEFAULT_PROPS.lineStyle?.normal,
|
||||
_lineStyle?.normal,
|
||||
),
|
||||
select: Object.assign(
|
||||
{},
|
||||
TRACE_TREE_STYLE_DEFAULT_PROPS.lineStyle?.select,
|
||||
_lineStyle?.select,
|
||||
),
|
||||
hover: Object.assign(
|
||||
{},
|
||||
TRACE_TREE_STYLE_DEFAULT_PROPS.lineStyle?.hover,
|
||||
_lineStyle?.hover,
|
||||
),
|
||||
}),
|
||||
[_lineStyle],
|
||||
);
|
||||
|
||||
const globalStyle = useMemo(
|
||||
() =>
|
||||
Object.assign(
|
||||
{},
|
||||
TRACE_TREE_STYLE_DEFAULT_PROPS.globalStyle,
|
||||
_globalStyle,
|
||||
),
|
||||
[_globalStyle],
|
||||
);
|
||||
const handleCollapse = (id: string, collapsed: boolean) => {
|
||||
setTreeMap({
|
||||
...treeMap,
|
||||
[id]: {
|
||||
...treeMap[id],
|
||||
isCollapsed: collapsed,
|
||||
},
|
||||
});
|
||||
onCollapseChange?.(id);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTreeMap(geSpanNodeInfoMap(spanNodes));
|
||||
}, [spanNodes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (spanNodes) {
|
||||
const treeNodes = spanNode2TreeNode(spanNodes, treeMap, {
|
||||
renderGraphNodeConfig,
|
||||
showKeyNodeOnly: false,
|
||||
});
|
||||
setTreeData(treeNodes);
|
||||
}
|
||||
}, [treeMap]);
|
||||
|
||||
return (
|
||||
<traceTreeContext.Provider
|
||||
value={{
|
||||
treeMap,
|
||||
onCollapse: handleCollapse,
|
||||
}}
|
||||
>
|
||||
<div className={styles['trace-tree-layout']}>
|
||||
<div
|
||||
className={clsx(
|
||||
styles['trace-trees-wrapper'],
|
||||
'trace-trees-scroll-ref',
|
||||
)}
|
||||
>
|
||||
{treeData?.map(tree => (
|
||||
<Tree
|
||||
key={tree.key}
|
||||
className={clsx(
|
||||
styles['trace-tree'],
|
||||
styles['trace-graph-panel-content-trace-tree'],
|
||||
)}
|
||||
treeData={tree}
|
||||
selectedKey={selectedSpanId}
|
||||
indentDisabled={indentDisabled}
|
||||
lineStyle={lineStyle}
|
||||
globalStyle={globalStyle}
|
||||
{...restProps}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</traceTreeContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import { type TagType, type Value } from '../typings/idl';
|
||||
import { type StatusCode } from '../consts/basic';
|
||||
import { type ObservationModules } from '../consts';
|
||||
|
||||
export interface StatusBasicInfo {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface I18NInfo {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface StatusConfigInfo {
|
||||
icon: ReactNode;
|
||||
color: string;
|
||||
backgroundColor: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export type StatusBasicInfoMapping = Record<
|
||||
number,
|
||||
StatusBasicInfo | undefined
|
||||
>;
|
||||
|
||||
export type StatusInfo = StatusBasicInfo & StatusConfigInfo;
|
||||
|
||||
export type I18nMapping = Record<ObservationModules, I18NInfo | undefined>;
|
||||
|
||||
export interface PresetInfoMappings {
|
||||
i18nMapping: I18nMapping;
|
||||
statusBasicInfoMapping: StatusBasicInfoMapping;
|
||||
}
|
||||
|
||||
export interface MetaTagsFieldParams {
|
||||
tagType: TagType;
|
||||
value?: Value;
|
||||
canCopy?: boolean;
|
||||
statusBasicInfoMapping?: StatusBasicInfoMapping;
|
||||
}
|
||||
|
||||
export type MetaTagsFieldRenderer = (
|
||||
params: MetaTagsFieldParams,
|
||||
) => string | ReactNode | undefined;
|
||||
|
||||
export interface BasicExtraRenderParams {
|
||||
duration?: number;
|
||||
tokens?: number;
|
||||
status?: StatusCode;
|
||||
placement: 'vertical' | 'horizontal';
|
||||
icon?: ReactNode;
|
||||
statusBasicInfoMapping: StatusBasicInfoMapping;
|
||||
}
|
||||
|
||||
export type BasicExtraRender = (params: BasicExtraRenderParams) => ReactNode;
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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 { type TooltipProps } from '@coze-arch/coze-design';
|
||||
|
||||
import { type TraceFrontendSpan } from '../typings/idl';
|
||||
import { type TreeProps } from '../common/tree';
|
||||
import {
|
||||
type RectStyle,
|
||||
type FlamethreadProps,
|
||||
type LabelStyle,
|
||||
} from '../common/flamethread';
|
||||
|
||||
export type TraceTreeProps = {
|
||||
spans: SpanNode[];
|
||||
selectedSpanId?: string;
|
||||
renderGraphNodeConfig?: RenderGraphNodeConfig;
|
||||
onCollapseChange?: (id: string) => void;
|
||||
defaultDisplayMode?: DisplayMode;
|
||||
/**
|
||||
* 隐藏模式选择器
|
||||
*/
|
||||
hideModeSelect?: boolean;
|
||||
/** 宿主用户信息 */
|
||||
hostUser?: {
|
||||
/** 用户邮箱 */
|
||||
email?: string;
|
||||
/** user id */
|
||||
id?: string;
|
||||
};
|
||||
} & Pick<
|
||||
TreeProps,
|
||||
| 'indentDisabled'
|
||||
| 'lineStyle'
|
||||
| 'globalStyle'
|
||||
| 'onSelect'
|
||||
| 'onClick'
|
||||
| 'onMouseMove'
|
||||
| 'onMouseEnter'
|
||||
| 'onMouseLeave'
|
||||
| 'className'
|
||||
>;
|
||||
|
||||
export type TraceFlamethreadProps = {
|
||||
spans: SpanNode[];
|
||||
selectedSpanId?: string;
|
||||
renderGraphNodeConfig?: RenderGraphNodeConfig;
|
||||
} & Pick<
|
||||
FlamethreadProps,
|
||||
| 'rectStyle'
|
||||
| 'labelStyle'
|
||||
| 'globalStyle'
|
||||
| 'rowHeight'
|
||||
| 'visibleColumnCount'
|
||||
| 'datazoomDecimals'
|
||||
| 'axisLabelSuffix'
|
||||
| 'disableViewScroll'
|
||||
| 'enableAutoFit'
|
||||
| 'onClick'
|
||||
>;
|
||||
|
||||
export type SpanNode = TraceFrontendSpan & {
|
||||
children?: SpanNode[];
|
||||
isBroken?: boolean;
|
||||
};
|
||||
|
||||
export type TraceTreeStyleDefaultProps = Pick<
|
||||
TraceTreeProps,
|
||||
'lineStyle' | 'globalStyle'
|
||||
>;
|
||||
|
||||
export interface FlamethreadStyleConfig {
|
||||
rectStyle?: RectStyle;
|
||||
labelStyle?: LabelStyle;
|
||||
}
|
||||
|
||||
export interface NodePresetConfig {
|
||||
icon?: ReactNode;
|
||||
flamethread?: FlamethreadStyleConfig;
|
||||
}
|
||||
|
||||
export interface NodeConfig extends NodePresetConfig {
|
||||
character?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface TraceTreeCustomRenderer {
|
||||
renderTooltip?: (spanNode: SpanNode) => TooltipProps;
|
||||
renderExtra?: (spanNode: SpanNode) => ReactNode;
|
||||
}
|
||||
|
||||
export interface TraceFlamethreadCustomRenderer {
|
||||
renderTooltip?: (spanNode: SpanNode) => FlamethreadTooltipConfig[];
|
||||
}
|
||||
|
||||
export interface RenderGraphNodeConfig {
|
||||
customTypeConfigMap?: Record<string | number, NodePresetConfig | undefined>;
|
||||
traceTreeCustomRenderer?: TraceTreeCustomRenderer;
|
||||
traceFlamethreadCustomRenderer?: TraceFlamethreadCustomRenderer;
|
||||
}
|
||||
|
||||
export interface FlamethreadTooltipConfig {
|
||||
key: string;
|
||||
value?: string;
|
||||
fill?: string;
|
||||
}
|
||||
|
||||
export interface SpanNodeRenderOptions {
|
||||
renderGraphNodeConfig?: RenderGraphNodeConfig;
|
||||
showKeyNodeOnly?: boolean;
|
||||
}
|
||||
|
||||
export type DisplayMode = 'default' | 'simplification';
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
export type Int64 = string | number;
|
||||
|
||||
export enum TagType {
|
||||
STRING = 0,
|
||||
DOUBLE = 1,
|
||||
BOOL = 2,
|
||||
LONG = 3,
|
||||
BYTES = 4,
|
||||
}
|
||||
|
||||
export interface Value {
|
||||
v_str?: string;
|
||||
v_double?: number;
|
||||
v_bool?: boolean;
|
||||
v_long?: Int64;
|
||||
v_bytes?: Blob;
|
||||
}
|
||||
|
||||
export enum FrontedTagType {
|
||||
/** 文本 */
|
||||
TEXT = 0,
|
||||
/** 时间,用时间戳,单位是微秒 */
|
||||
TIME = 1,
|
||||
/** 时间间隔,单位是微秒 */
|
||||
TIME_DURATION = 2,
|
||||
}
|
||||
|
||||
export interface FrontendTag {
|
||||
key: string;
|
||||
/** 多语,如无配置时值沿用 key */
|
||||
key_alias?: string;
|
||||
tag_type: TagType;
|
||||
value?: Value;
|
||||
/** 用于自定义渲染 */
|
||||
element?: ReactNode;
|
||||
/** 前端类型,用于前端处理 */
|
||||
frontend_tag_type?: FrontedTagType;
|
||||
/** 是否可复制 */
|
||||
can_copy?: boolean;
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
key?: string;
|
||||
tag_type?: TagType;
|
||||
value?: Value;
|
||||
}
|
||||
|
||||
export enum InputOutputType {
|
||||
/** 文本类型 */
|
||||
TEXT = 0,
|
||||
}
|
||||
|
||||
export interface SpanSummary {
|
||||
tags?: Array<FrontendTag>;
|
||||
}
|
||||
|
||||
export interface SpanInputOutput {
|
||||
/** TEXT */
|
||||
type?: InputOutputType;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
export interface TraceFrontendSpan {
|
||||
trace_id?: string;
|
||||
log_id?: string;
|
||||
span_id?: string;
|
||||
type?: string;
|
||||
name?: string;
|
||||
alias_name?: string;
|
||||
parent_id?: string;
|
||||
/** 单位是微秒 */
|
||||
duration?: Int64;
|
||||
/** 单位是微秒 */
|
||||
start_time?: Int64;
|
||||
status_code?: number;
|
||||
product_line?: string;
|
||||
is_entry?: boolean;
|
||||
is_key_span?: boolean;
|
||||
owner_list?: Array<string>;
|
||||
rundown_doc_url?: string;
|
||||
tags?: Array<Tag>;
|
||||
/** 节点详情 */
|
||||
summary?: SpanSummary;
|
||||
input?: SpanInputOutput;
|
||||
output?: SpanInputOutput;
|
||||
}
|
||||
|
||||
export interface TraceHeader {
|
||||
/** 单位是微秒 */
|
||||
duration?: Int64;
|
||||
/** 输入消耗token数 */
|
||||
tokens?: number;
|
||||
status_code?: number;
|
||||
tags?: Array<FrontendTag>;
|
||||
}
|
||||
|
||||
export interface TraceFrontend {
|
||||
spans?: Array<TraceFrontendSpan>;
|
||||
header?: TraceHeader;
|
||||
}
|
||||
export interface MessageRenderOptions {
|
||||
expandable?: boolean;
|
||||
defaultExpand?: boolean;
|
||||
showRole?: boolean;
|
||||
rawMessageItem?: boolean;
|
||||
}
|
||||
|
||||
export enum MessageRole {
|
||||
User = 'User',
|
||||
Assistant = 'Assistant',
|
||||
System = 'System',
|
||||
Tool = 'Tool',
|
||||
}
|
||||
|
||||
export enum MessageScene {
|
||||
Input = 'Input',
|
||||
Output = 'Output',
|
||||
History = 'History',
|
||||
System = 'System',
|
||||
Tool = 'Tool',
|
||||
}
|
||||
|
||||
export enum KeySceneField {
|
||||
Status = '状态',
|
||||
System = 'System',
|
||||
History = '消息列表',
|
||||
Input = '输入',
|
||||
Output = '输出',
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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 { StatusCode } from '../consts/basic';
|
||||
|
||||
export const getStandardStatusCode = (status: number) =>
|
||||
status === StatusCode.SUCCESS ? StatusCode.SUCCESS : StatusCode.ERROR;
|
||||
|
||||
export const isSuccessStatus = (status?: number) =>
|
||||
status === StatusCode.SUCCESS;
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* 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 { uniqBy } from 'lodash-es';
|
||||
|
||||
import { type TraceFrontendSpan } from '../typings/idl';
|
||||
import {
|
||||
type SpanNode,
|
||||
type RenderGraphNodeConfig,
|
||||
type SpanNodeRenderOptions,
|
||||
} from '../typings/graph';
|
||||
import { renderCustomTreeNode } from '../trace-tree/graph-render';
|
||||
import {
|
||||
FLAME_THREAD_DEFAULT_RECT_STYLE,
|
||||
SPAN_STATUS_CONFIG,
|
||||
} from '../consts/graph';
|
||||
import { NODE_PRESET_CONFIG_MAP, type PresetSpanType } from '../consts/graph';
|
||||
import { StatusCode } from '../consts/basic';
|
||||
import { type TreeNodeInfo } from '../common/tree/typing';
|
||||
import { type TreeNode } from '../common/tree';
|
||||
import { type RectStyle, type RectNode } from '../common/flamethread';
|
||||
import { getStandardStatusCode } from './basic';
|
||||
|
||||
export function spans2SpanNodes(spans: TraceFrontendSpan[]): {
|
||||
roots: SpanNode[];
|
||||
spans: TraceFrontendSpan[];
|
||||
} {
|
||||
if (spans.length === 0) {
|
||||
return {
|
||||
roots: [],
|
||||
spans: [],
|
||||
};
|
||||
}
|
||||
|
||||
const roots: SpanNode[] = [];
|
||||
const map: Record<string, SpanNode> = {};
|
||||
|
||||
const sortedSpans = uniqBy(spans, span => span.span_id).sort((a, b) => {
|
||||
const startA = a.start_time ? Number(a.start_time) : Infinity;
|
||||
const startB = b.start_time ? Number(b.start_time) : Infinity;
|
||||
return startA - startB;
|
||||
});
|
||||
|
||||
sortedSpans.forEach(span => {
|
||||
const currentSpan: SpanNode = {
|
||||
...span,
|
||||
children: [],
|
||||
};
|
||||
const { span_id } = span;
|
||||
if (span_id) {
|
||||
map[span_id] = currentSpan;
|
||||
}
|
||||
});
|
||||
|
||||
sortedSpans.forEach(span => {
|
||||
const { span_id, parent_id } = span;
|
||||
if (span_id) {
|
||||
const spanNode = map[span_id];
|
||||
const parentSpanNode = parent_id ? map[parent_id] : undefined;
|
||||
if (parentSpanNode === undefined) {
|
||||
roots.push({
|
||||
...spanNode,
|
||||
isBroken: parent_id !== '0',
|
||||
});
|
||||
} else {
|
||||
parentSpanNode.children = parentSpanNode.children ?? [];
|
||||
parentSpanNode.children.push(spanNode);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
roots,
|
||||
spans: sortedSpans,
|
||||
};
|
||||
}
|
||||
|
||||
export const getSpanNodeInfo = (
|
||||
spanNode: SpanNode,
|
||||
map: Record<string, TreeNodeInfo> = {},
|
||||
) => {
|
||||
const { isKeyNode, keyNodeParentId } = map[spanNode.parent_id || ''] || {};
|
||||
if (spanNode.span_id) {
|
||||
map[spanNode.span_id] = {
|
||||
isCollapsed: false,
|
||||
isKeyNode: spanNode.is_key_span,
|
||||
keyNodeParentId: isKeyNode ? spanNode.parent_id : keyNodeParentId,
|
||||
};
|
||||
}
|
||||
|
||||
spanNode.children?.forEach(childSpan => getSpanNodeInfo(childSpan, map));
|
||||
};
|
||||
|
||||
export const geSpanNodeInfoMap = (spanNodes: SpanNode[]) => {
|
||||
const map: Record<string, TreeNodeInfo> = {};
|
||||
spanNodes.map(spanNode => getSpanNodeInfo(spanNode, map));
|
||||
return map;
|
||||
};
|
||||
|
||||
export const spanNode2TreeNode = (
|
||||
spanNodes: SpanNode[],
|
||||
map: Record<string, TreeNodeInfo> = {},
|
||||
options: SpanNodeRenderOptions = {},
|
||||
): TreeNode[] => {
|
||||
const { showKeyNodeOnly } = options;
|
||||
return (
|
||||
spanNodes
|
||||
.map(spanNode => {
|
||||
const { status_code = StatusCode.SUCCESS } = spanNode;
|
||||
const lineStyle =
|
||||
SPAN_STATUS_CONFIG[getStandardStatusCode(status_code)]?.lineStyle;
|
||||
|
||||
const treeNode: TreeNode = {
|
||||
key: spanNode.span_id || '',
|
||||
title: renderCustomTreeNode(options),
|
||||
lineStyle,
|
||||
zIndex: spanNode.status_code === StatusCode.ERROR ? 1 : 0,
|
||||
extra: {
|
||||
spanNode,
|
||||
},
|
||||
};
|
||||
|
||||
const { isCollapsed, isKeyNode } = map[treeNode.key] || {};
|
||||
|
||||
if (showKeyNodeOnly && !isKeyNode) {
|
||||
return (
|
||||
spanNode2TreeNode(spanNode.children || [], map, options).flat() ??
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
if (!isCollapsed) {
|
||||
treeNode.children =
|
||||
spanNode2TreeNode(spanNode.children || [], map, options).flat() ??
|
||||
[];
|
||||
}
|
||||
return treeNode;
|
||||
})
|
||||
.flat() || []
|
||||
);
|
||||
};
|
||||
|
||||
const genRectStyle = (
|
||||
span: SpanNode,
|
||||
customTypeConfigMap?: RenderGraphNodeConfig['customTypeConfigMap'],
|
||||
): RectStyle => {
|
||||
const { type = '' } = span;
|
||||
|
||||
const rectStyle =
|
||||
customTypeConfigMap?.[type]?.flamethread?.rectStyle ??
|
||||
NODE_PRESET_CONFIG_MAP[type as PresetSpanType]?.flamethread?.rectStyle ??
|
||||
FLAME_THREAD_DEFAULT_RECT_STYLE;
|
||||
|
||||
return {
|
||||
normal: rectStyle?.normal,
|
||||
hover: rectStyle?.hover,
|
||||
select: rectStyle?.select,
|
||||
};
|
||||
};
|
||||
|
||||
const genRectNode = (info: {
|
||||
span: TraceFrontendSpan;
|
||||
startSpan?: TraceFrontendSpan;
|
||||
rowNo: number;
|
||||
customTypeConfigMap?: RenderGraphNodeConfig['customTypeConfigMap'];
|
||||
}): RectNode => {
|
||||
const { span, startSpan, rowNo, customTypeConfigMap } = info;
|
||||
const start =
|
||||
(span.start_time as number) -
|
||||
(startSpan ? (startSpan.start_time as number) : 0);
|
||||
const end = start + (span.duration as number);
|
||||
return {
|
||||
key: span.span_id ?? '',
|
||||
rowNo,
|
||||
start,
|
||||
end,
|
||||
rectStyle: genRectStyle(span, customTypeConfigMap),
|
||||
extra: {
|
||||
span,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const spanData2flamethreadData = (
|
||||
spans: TraceFrontendSpan[],
|
||||
customTypeConfigMap?: RenderGraphNodeConfig['customTypeConfigMap'],
|
||||
): RectNode[] => {
|
||||
const startSpan = spans?.[0] as TraceFrontendSpan | undefined;
|
||||
return spans.map((span, index) =>
|
||||
genRectNode({
|
||||
span,
|
||||
startSpan,
|
||||
rowNo: index,
|
||||
customTypeConfigMap,
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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 JSONBig from 'json-bigint';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const jsonBig = JSONBig({ storeAsString: true });
|
||||
export const textWithFallback = (text?: string | number) =>
|
||||
text && text !== '' ? text.toString() : '-';
|
||||
|
||||
export const formatTime = (timestamp?: number | string) =>
|
||||
dayjs(Number(timestamp)).format('YYYY-MM-DD HH:mm:ss.SSS');
|
||||
|
||||
export const isJsonString = (str: string) => {
|
||||
try {
|
||||
const jsonData = JSON.parse(str);
|
||||
if (
|
||||
Object.prototype.toString.call(jsonData) !== '[object Object]' &&
|
||||
Object.prototype.toString.call(jsonData) !== '[object Array]'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const jsonParseWithBigNumber = (jsonString: string) =>
|
||||
JSON.parse(JSON.stringify(jsonBig.parse(jsonString)));
|
||||
|
||||
export const jsonParse = (
|
||||
jsonString: string,
|
||||
): Record<string, unknown> | string => {
|
||||
if (isJsonString(jsonString)) {
|
||||
return jsonParseWithBigNumber(jsonString);
|
||||
} else {
|
||||
return jsonString;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const DECIMAL = 60;
|
||||
const UNIT_MILLISECOND = 1000;
|
||||
const UNIT_SECOND = DECIMAL * UNIT_MILLISECOND;
|
||||
const UNIT_MINUTES = DECIMAL * UNIT_SECOND;
|
||||
const UNIT_HOUR = DECIMAL * UNIT_MINUTES;
|
||||
|
||||
export function formatDuration(time: number) {
|
||||
if (time < UNIT_MILLISECOND) {
|
||||
return `${time}ms`;
|
||||
} else if (time < UNIT_SECOND) {
|
||||
return `${(time / UNIT_MILLISECOND).toFixed(2)}s`;
|
||||
} else if (time < UNIT_MINUTES) {
|
||||
return `${(time / UNIT_SECOND).toFixed(2)}min`;
|
||||
} else if (time < UNIT_HOUR) {
|
||||
return `${(time / UNIT_MINUTES).toFixed(2)}h`;
|
||||
} else {
|
||||
return `${(time / UNIT_HOUR).toFixed(2)}d`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
@black: #000000;
|
||||
@white: #FFFFFF;
|
||||
@blue-0: #4D53E8;
|
||||
|
||||
@text-gray-0: #1D1C23;
|
||||
@text-gray-1: #1D1C23CC;
|
||||
@text-gray-2: #1D1C2399;
|
||||
@text-gray-3: #1D1C2359;
|
||||
@text-blue-0: #4D53E8;
|
||||
|
||||
|
||||
@background-gray-0: #F7F7FA;
|
||||
@background-gray-1: #F0F0F5;
|
||||
@background-blue-0: #F1F2FD;
|
||||
|
||||
@icon-color: #6B6B75;
|
||||
@border-color: #1D1C2314;
|
||||
@error-color: #FF441E;
|
||||
22
frontend/packages/workflow/test-run-next/trace/src/types.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 interface GotoParams {
|
||||
nodeId: string;
|
||||
workflowId: string;
|
||||
executeId: string;
|
||||
subExecuteId: string;
|
||||
}
|
||||
92
frontend/packages/workflow/test-run-next/trace/src/utils.ts
Normal file
@@ -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 { uniqBy } from 'lodash-es';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
type Span,
|
||||
type Int64,
|
||||
type TraceFrontendSpan,
|
||||
} from '@coze-arch/bot-api/workflow_api';
|
||||
|
||||
import { type GotoParams } from './types';
|
||||
|
||||
export const sortSpans = (spans: TraceFrontendSpan[]) =>
|
||||
uniqBy(spans, span => span.span_id).sort((a, b) => {
|
||||
const startA = a.start_time ? Number(a.start_time) : Infinity;
|
||||
const startB = b.start_time ? Number(b.start_time) : Infinity;
|
||||
return startA - startB;
|
||||
});
|
||||
|
||||
export const getTimeFromSpan = (span: Span) =>
|
||||
span.start_time ? dayjs(span.start_time).format('YYYY-MM-DD HH:mm:ss') : '-';
|
||||
|
||||
export const isTriggerFromSpan = (span: Span) =>
|
||||
span.tags?.find(t => t.key === 'is_trigger');
|
||||
|
||||
export const getStrFromSpan = (span: Span, key: string): string => {
|
||||
const { tags } = span;
|
||||
if (!Array.isArray(tags) || !tags.length) {
|
||||
return '';
|
||||
}
|
||||
const target = tags.find(t => t.key === key);
|
||||
return target?.value?.v_str || '';
|
||||
};
|
||||
export const getLongFromSpan = (span: Span, key: string): Int64 | undefined => {
|
||||
const { tags } = span;
|
||||
if (!Array.isArray(tags) || !tags.length) {
|
||||
return '';
|
||||
}
|
||||
const target = tags.find(t => t.key === key);
|
||||
return target?.value?.v_long;
|
||||
};
|
||||
|
||||
export const getTokensFromSpan = (span: Span) =>
|
||||
getLongFromSpan(span, 'tokens');
|
||||
|
||||
const DECIMAL = 60;
|
||||
const UNIT_MILLISECOND = 1000;
|
||||
const UNIT_SECOND = DECIMAL * UNIT_MILLISECOND;
|
||||
const UNIT_MINUTES = DECIMAL * UNIT_SECOND;
|
||||
const UNIT_HOUR = DECIMAL * UNIT_MINUTES;
|
||||
|
||||
export function formatDuration(time: number) {
|
||||
if (time < UNIT_MILLISECOND) {
|
||||
return `${time}ms`;
|
||||
} else if (time < UNIT_SECOND) {
|
||||
return `${(time / UNIT_MILLISECOND).toFixed(2)}s`;
|
||||
} else if (time < UNIT_MINUTES) {
|
||||
return `${(time / UNIT_SECOND).toFixed(2)}min`;
|
||||
} else if (time < UNIT_HOUR) {
|
||||
return `${(time / UNIT_MINUTES).toFixed(2)}h`;
|
||||
} else {
|
||||
return `${(time / UNIT_HOUR).toFixed(2)}d`;
|
||||
}
|
||||
}
|
||||
|
||||
export const getGotoNodeParams = (span: Span): GotoParams => {
|
||||
const workflowId = getStrFromSpan(span, 'workflow_id');
|
||||
const nodeId = getStrFromSpan(span, 'workflow_node_id');
|
||||
const executeId = getStrFromSpan(span, 'execute_id');
|
||||
const subExecuteId = getStrFromSpan(span, 'sub_execute_id');
|
||||
|
||||
return {
|
||||
workflowId,
|
||||
nodeId,
|
||||
executeId,
|
||||
subExecuteId,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@coze-arch/ts-config/tsconfig.web.json",
|
||||
"compilerOptions": {
|
||||
"types": [],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib-ts",
|
||||
"tsBuildInfoFile": "./lib-ts/tsconfig.build.tsbuildinfo"
|
||||
},
|
||||
"include": ["./src", "./src/**/*.json"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../arch/bot-api/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../arch/bot-typings/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../arch/i18n/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../base/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../../config/eslint-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../../config/ts-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../shared/tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
frontend/packages/workflow/test-run-next/trace/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"exclude": ["**/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.misc.json"
|
||||
}
|
||||
]
|
||||
}
|
||||