feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
BIN
frontend/packages/common/biz-components/src/assets/rspack.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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, { useState, type FC } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
Switch,
|
||||
TextArea,
|
||||
Button,
|
||||
type ButtonProps,
|
||||
} from '@coze-arch/coze-design';
|
||||
|
||||
interface IValue {
|
||||
isOpen?: boolean;
|
||||
replyText?: string;
|
||||
}
|
||||
|
||||
interface AsyncFormProps {
|
||||
value?: IValue;
|
||||
onChange?: (value: IValue) => void;
|
||||
switchStatus?: 'default' | 'hidden' | 'disabled';
|
||||
disabled?: boolean;
|
||||
textAreaVisible?: boolean;
|
||||
saveButtonProps?: ButtonProps;
|
||||
}
|
||||
|
||||
const REPLY_MAX_LENGTH = 1000;
|
||||
const validate = (value: IValue, needReply: boolean): string => {
|
||||
if (!needReply) {
|
||||
return '';
|
||||
}
|
||||
if (!value.replyText) {
|
||||
return I18n.t('asyn_task_reply_need');
|
||||
}
|
||||
if (value.replyText.length > REPLY_MAX_LENGTH) {
|
||||
return I18n.t('asyn_task_reply_toolong');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
export const AsyncSettingUI: FC<AsyncFormProps> = props => {
|
||||
const {
|
||||
onChange,
|
||||
switchStatus = 'default',
|
||||
textAreaVisible = true,
|
||||
saveButtonProps = {},
|
||||
disabled,
|
||||
} = props;
|
||||
|
||||
const [value, setValue] = useState<IValue>(props.value ?? {});
|
||||
const [error, setError] = useState('');
|
||||
return (
|
||||
<div className="flex flex-col h-full gap-[12px] text-lg">
|
||||
<div className="flex flex-col gap-[4px]">
|
||||
<div className="flex">
|
||||
<div className="flex-1 font-semibold coz-fg-primary">
|
||||
{I18n.t('asyn_task_setting_title')}
|
||||
</div>
|
||||
{switchStatus === 'hidden' ? null : (
|
||||
<Switch
|
||||
size="small"
|
||||
disabled={switchStatus === 'disabled' || disabled}
|
||||
checked={value?.isOpen}
|
||||
onChange={(v: boolean) => {
|
||||
setValue({
|
||||
...value,
|
||||
isOpen: v,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="coz-fg-secondary">
|
||||
{I18n.t('asyn_task_setting_desc')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{textAreaVisible && value?.isOpen ? (
|
||||
<div className="flex flex-col flex-1 gap-[12px]">
|
||||
<div className="font-semibold coz-fg-primary">
|
||||
{I18n.t('asyn_task_setting_response_title')}
|
||||
<span className="coz-fg-hglt-red">*</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<TextArea
|
||||
disabled={disabled}
|
||||
error={!!error}
|
||||
className="h-[135px]"
|
||||
suffix={
|
||||
<div>{`${
|
||||
value?.replyText?.length || 0
|
||||
}/${REPLY_MAX_LENGTH}`}</div>
|
||||
}
|
||||
value={value?.replyText}
|
||||
onChange={(v: string) => {
|
||||
const newValue = {
|
||||
...value,
|
||||
replyText: v,
|
||||
};
|
||||
setValue(newValue);
|
||||
setError(validate(newValue, textAreaVisible));
|
||||
}}
|
||||
placeholder={I18n.t('asyn_task_setting_response_content')}
|
||||
/>
|
||||
{error ? (
|
||||
<div className="coz-fg-hglt-red text-base">{error}</div>
|
||||
) : undefined}
|
||||
</div>
|
||||
</div>
|
||||
) : undefined}
|
||||
|
||||
<div className="flex justify-end mt-auto">
|
||||
<Button
|
||||
{...saveButtonProps}
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
setError(validate(value, textAreaVisible));
|
||||
if (!error) {
|
||||
onChange?.(value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{I18n.t('Save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
.banner-preview {
|
||||
@apply w-full;
|
||||
|
||||
.label {
|
||||
@apply coz-fg-white-dim text-xxl font-medium;
|
||||
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
& path {
|
||||
@apply coz-fg-hglt-plus-dim;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
@apply coz-fg-white-dim text-xxl font-medium underline;
|
||||
|
||||
line-height: 22px;
|
||||
|
||||
&:visited,&:active,&:focus {
|
||||
@apply coz-fg-white-dim;
|
||||
}
|
||||
}
|
||||
}
|
||||
80
frontend/packages/common/biz-components/src/banner/index.tsx
Normal file
@@ -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 React, { type CSSProperties, forwardRef } from 'react';
|
||||
|
||||
import DOMPurify from 'dompurify';
|
||||
import classNames from 'classnames';
|
||||
import { IconCozCrossFill } from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
Banner as CozeDesignBanner,
|
||||
type BannerProps as CozeDesignBannerProps,
|
||||
} from '@coze-arch/coze-design';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface BannerProps {
|
||||
label?: string;
|
||||
backgroundColor?: string;
|
||||
showClose?: boolean;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
labelClassName?: string;
|
||||
labelStyle?: CSSProperties;
|
||||
bannerProps?: CozeDesignBannerProps;
|
||||
}
|
||||
|
||||
export const Banner = forwardRef<HTMLDivElement, BannerProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
style,
|
||||
label,
|
||||
backgroundColor,
|
||||
showClose = true,
|
||||
labelClassName,
|
||||
labelStyle,
|
||||
bannerProps,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const description = (
|
||||
<span
|
||||
className={classNames(labelClassName, styles.label)}
|
||||
style={labelStyle}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(label || '', {
|
||||
ALLOWED_ATTR: ['href', 'target'],
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div ref={ref} className={className} style={style}>
|
||||
<CozeDesignBanner
|
||||
icon={null}
|
||||
className={styles['banner-preview']}
|
||||
style={{ backgroundColor }}
|
||||
closeIcon={
|
||||
showClose ? <IconCozCrossFill className={styles.icon} /> : null
|
||||
}
|
||||
description={description}
|
||||
{...bannerProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,4 @@
|
||||
.container {
|
||||
padding: 8px;
|
||||
background: var(--Bg-COZ-bg-max, #FFF);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export const Container = ({ children }: { children: React.ReactNode }) => (
|
||||
<div className={s.container}>{children}</div>
|
||||
);
|
||||
172
frontend/packages/common/biz-components/src/coachmark/index.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* 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 Joyride, {
|
||||
type Props,
|
||||
ACTIONS,
|
||||
EVENTS,
|
||||
type CallBackProps,
|
||||
} from 'react-joyride';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
import { typeSafeJSONParse } from '@coze-arch/bot-utils';
|
||||
import { localStorageService } from '@coze-foundation/local-storage';
|
||||
|
||||
import { Tooltip, type IExtraAction } from './tooltip';
|
||||
import { StepCard } from './step-card';
|
||||
|
||||
export { type Placement, type Step } from 'react-joyride';
|
||||
|
||||
const COACHMARK_KEY = 'coachmark';
|
||||
const COACHMARK_END = 10000;
|
||||
|
||||
export default function Coachmark({
|
||||
steps,
|
||||
extraAction,
|
||||
showProgress = true,
|
||||
caseId,
|
||||
itemIndex = 0,
|
||||
}: {
|
||||
steps: Props['steps'];
|
||||
showProgress?: Props['showProgress'];
|
||||
extraAction?: IExtraAction;
|
||||
caseId: string;
|
||||
itemIndex?: number;
|
||||
}) {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [stepIndex, setStepIndex] = useState(itemIndex);
|
||||
|
||||
const initVisible = async (cid: string) => {
|
||||
const coachMarkStorage =
|
||||
await localStorageService.getValueSync(COACHMARK_KEY);
|
||||
// readStep 代表已读的step index
|
||||
const readStep = (
|
||||
typeSafeJSONParse(coachMarkStorage) as Record<string, number> | undefined
|
||||
)?.[cid];
|
||||
|
||||
// 如果没有读过,或者读过的step index 小于当前项的index,则展示。
|
||||
const shouldShow = readStep === undefined || itemIndex > readStep;
|
||||
setVisible(shouldShow);
|
||||
};
|
||||
|
||||
// 设置已读的step index
|
||||
const setCoachmarkReadStep = useCallback(
|
||||
(step: number) => {
|
||||
const coachmarkStorage =
|
||||
localStorageService.getValue(COACHMARK_KEY) ?? '{}';
|
||||
const coachmarkValue: Record<string, number | undefined> =
|
||||
(typeSafeJSONParse(coachmarkStorage) ?? {}) as unknown as Record<
|
||||
string,
|
||||
number | undefined
|
||||
>;
|
||||
|
||||
// 如果没有读过,或者要设置的index大于已读的step index 才设置,否则忽略。
|
||||
if (
|
||||
coachmarkValue[caseId] === undefined ||
|
||||
step > Number(coachmarkValue[caseId])
|
||||
) {
|
||||
localStorageService.setValue(
|
||||
COACHMARK_KEY,
|
||||
JSON.stringify({
|
||||
...coachmarkValue,
|
||||
[caseId]: step,
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
[caseId],
|
||||
);
|
||||
|
||||
const handleJoyrideCallback = (data: CallBackProps) => {
|
||||
const { action, index, type } = data;
|
||||
|
||||
if (
|
||||
[EVENTS.STEP_AFTER, EVENTS.TARGET_NOT_FOUND].includes(
|
||||
type as 'step:after' | 'error:target_not_found',
|
||||
)
|
||||
) {
|
||||
const nextIndex = index + (action === ACTIONS.PREV ? -1 : 1);
|
||||
// 设置已经读过的step index
|
||||
setCoachmarkReadStep(index);
|
||||
setStepIndex(nextIndex);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initVisible(caseId);
|
||||
|
||||
return () => {
|
||||
setCoachmarkReadStep(itemIndex);
|
||||
};
|
||||
}, [caseId, setCoachmarkReadStep, itemIndex]);
|
||||
|
||||
return visible ? (
|
||||
<Joyride
|
||||
steps={steps}
|
||||
tooltipComponent={props => (
|
||||
<Tooltip
|
||||
{...props}
|
||||
extraAction={extraAction}
|
||||
showProgress={showProgress}
|
||||
onClose={() => {
|
||||
setVisible(false);
|
||||
setCoachmarkReadStep(COACHMARK_END);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
continuous
|
||||
disableOverlay
|
||||
disableScrollParentFix
|
||||
stepIndex={stepIndex}
|
||||
callback={handleJoyrideCallback}
|
||||
spotlightPadding={-6}
|
||||
styles={{
|
||||
options: {
|
||||
zIndex: 100,
|
||||
primaryColor: '#4E40E5',
|
||||
},
|
||||
buttonClose: {
|
||||
display: 'none',
|
||||
},
|
||||
tooltip: {
|
||||
width: 300,
|
||||
padding: 8,
|
||||
borderRadius: 12,
|
||||
},
|
||||
tooltipContent: {
|
||||
padding: 0,
|
||||
},
|
||||
buttonBack: {
|
||||
display: 'none', // 隐藏返回按钮
|
||||
},
|
||||
}}
|
||||
floaterProps={{
|
||||
styles: {
|
||||
arrow: {
|
||||
length: 7,
|
||||
spread: 14,
|
||||
margin: 40,
|
||||
},
|
||||
floater: {
|
||||
filter: 'none',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
|
||||
export { StepCard };
|
||||
@@ -0,0 +1,29 @@
|
||||
.title {
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
|
||||
width: 100%;
|
||||
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
color: var(--Fg-COZ-fg-plus, #060709F5);
|
||||
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
margin: 4px 0;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: var(--Fg-COZ-fg-primary, #060709CC);
|
||||
text-align: left;
|
||||
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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 { Container } from '../container';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export const StepCard = (props: {
|
||||
content: string;
|
||||
title: string;
|
||||
imgSrc?: string;
|
||||
}) => {
|
||||
const { imgSrc, content, title } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{imgSrc ? <img className={s.image} src={imgSrc} /> : null}
|
||||
|
||||
<Container>
|
||||
<div className={s.title}>{title}</div>
|
||||
<div className={s.content}>{content}</div>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
.tooltip {
|
||||
overflow: hidden;
|
||||
|
||||
width: 300px;
|
||||
|
||||
font-size: 14px;
|
||||
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 16px 48px 0 #00000014, 0 8px 24px 0 #00000029;
|
||||
|
||||
&-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
||||
&-index {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 32px;
|
||||
color: var(--Fg-COZ-fg-secondary, rgba(6, 7, 9, 50%));
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* 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 TooltipRenderProps, type StepMerged } from 'react-joyride';
|
||||
import { type FC, type MouseEvent, useCallback } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Button } from '@coze-arch/coze-design';
|
||||
|
||||
import { Container } from '../container';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export interface IExtraAction {
|
||||
content: string;
|
||||
onClick: (step: StepMerged) => void;
|
||||
}
|
||||
|
||||
export const Tooltip: FC<
|
||||
TooltipRenderProps & {
|
||||
showProgress?: boolean;
|
||||
extraAction?: IExtraAction;
|
||||
onClose?: () => void;
|
||||
}
|
||||
> = props => {
|
||||
const {
|
||||
index,
|
||||
isLastStep,
|
||||
primaryProps,
|
||||
skipProps,
|
||||
closeProps,
|
||||
step,
|
||||
showProgress,
|
||||
size,
|
||||
extraAction,
|
||||
onClose,
|
||||
} = props;
|
||||
|
||||
const handleClose = useCallback(
|
||||
(e: MouseEvent<HTMLElement, globalThis.MouseEvent>) => {
|
||||
onClose?.();
|
||||
closeProps.onClick(e);
|
||||
},
|
||||
[closeProps, onClose],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={s.tooltip} data-testid="coachmark_tooltip">
|
||||
<Container>
|
||||
{step.content}
|
||||
<Container>
|
||||
<div className={s['tooltip-footer']}>
|
||||
<div className={s['action-btn-index']}>
|
||||
{showProgress ? `${index + 1}/${size}` : ''}
|
||||
</div>
|
||||
<div className={s['action-btn']}>
|
||||
{isLastStep ? (
|
||||
extraAction ? (
|
||||
<>
|
||||
<Button
|
||||
size="default"
|
||||
color="secondary"
|
||||
{...closeProps}
|
||||
onClick={handleClose}
|
||||
data-testid="coachmark_tooltip_got_it"
|
||||
>
|
||||
{I18n.t('upgrade_guide_got_it')}
|
||||
</Button>
|
||||
<Button
|
||||
size="default"
|
||||
color="highlight"
|
||||
onClick={() => {
|
||||
extraAction.onClick(step);
|
||||
onClose?.();
|
||||
}}
|
||||
>
|
||||
{extraAction.content}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
size="default"
|
||||
color="highlight"
|
||||
{...closeProps}
|
||||
onClick={handleClose}
|
||||
data-testid="coachmark_tooltip_got_it"
|
||||
>
|
||||
{I18n.t('upgrade_guide_got_it')}
|
||||
</Button>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
size="default"
|
||||
color={'secondary'}
|
||||
{...skipProps}
|
||||
onClick={handleClose}
|
||||
data-testid="coachmark_tooltip_got_it"
|
||||
>
|
||||
{I18n.t('upgrade_guide_got_it')}
|
||||
</Button>
|
||||
<Button
|
||||
size="default"
|
||||
color={'highlight'}
|
||||
onClick={e => {
|
||||
step.data?.nextAction?.();
|
||||
primaryProps.onClick(e);
|
||||
}}
|
||||
>
|
||||
{I18n.t('upgrade_guide_next')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
31
frontend/packages/common/biz-components/src/index.tsx
Normal file
@@ -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.
|
||||
*/
|
||||
|
||||
export { ParamTypeAlias } from './parameters';
|
||||
export type {
|
||||
ParametersProps,
|
||||
ParameterValue,
|
||||
ParametersError,
|
||||
} from './parameters';
|
||||
|
||||
export { AsyncSettingUI } from './async-setting';
|
||||
export { Banner } from './banner';
|
||||
// export { PictureUpload, customUploadRequest } from './picture-upload';
|
||||
export type { UploadValue, GenerateInfo } from './picture-upload';
|
||||
|
||||
export { type Placement } from './coachmark';
|
||||
|
||||
export { UpdateUserAvatar } from './update-user-avatar';
|
||||
@@ -0,0 +1,35 @@
|
||||
.popup_container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
.nano {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-self: stretch;
|
||||
pointer-events: none;
|
||||
:global {
|
||||
.semi-portal-inner {
|
||||
left: 50% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.tooltip {
|
||||
&.top-level {
|
||||
word-break: break-word;
|
||||
border-radius: 6px;
|
||||
background: var(--light-color-grey-grey-7, #41414C);
|
||||
max-width: 400px;
|
||||
color: var(--light-usage-bg-color-bg-0, #FFF);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
}
|
||||
& > svg > path {
|
||||
fill: var(--light-color-grey-grey-7, #41414C);
|
||||
}
|
||||
}
|
||||
@@ -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 React, { type PropsWithChildren, useRef } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { type TooltipProps } from '@coze-arch/bot-semi/Tooltip';
|
||||
import { Tooltip } from '@coze-arch/bot-semi';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
type AutoSizeTooltipProps = PropsWithChildren<
|
||||
{
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
containerClassName?: string;
|
||||
containerStyle?: React.CSSProperties;
|
||||
tooltipClassName?: string;
|
||||
tooltipStyle?: React.CSSProperties;
|
||||
} & Omit<TooltipProps, 'className' | 'style'>
|
||||
>;
|
||||
|
||||
export default function AutoSizeTooltip({
|
||||
children,
|
||||
className,
|
||||
style,
|
||||
tooltipClassName,
|
||||
tooltipStyle,
|
||||
containerClassName,
|
||||
containerStyle,
|
||||
...rest
|
||||
}: AutoSizeTooltipProps) {
|
||||
const nanoRef = useRef<HTMLDivElement | null>(null);
|
||||
const renderContent = () => (
|
||||
<>
|
||||
<div
|
||||
ref={nanoRef}
|
||||
className={classNames(styles.nano, containerClassName)}
|
||||
style={containerStyle}
|
||||
/>
|
||||
<Tooltip
|
||||
{...rest}
|
||||
className={classNames(
|
||||
styles.tooltip,
|
||||
styles['top-level'],
|
||||
tooltipClassName,
|
||||
)}
|
||||
style={{ left: 0, ...tooltipStyle }}
|
||||
>
|
||||
{children}
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.popup_container, className)}
|
||||
style={style}
|
||||
>
|
||||
{renderContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
/** 基本组件样式 */
|
||||
.dot {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: gray;
|
||||
}
|
||||
|
||||
.empty-block {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.help-line-block {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background: gray;
|
||||
}
|
||||
}
|
||||
|
||||
/** 业务组件样式 */
|
||||
.half-top-root {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-left: 1px solid gray;
|
||||
border-bottom: 1px solid gray;
|
||||
border-radius: 0 0 0 4px;
|
||||
width: 80%;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.children {
|
||||
.dot {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
left: 70%;
|
||||
transform: translate3d(0, -60%, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.half-bottom-root {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.line {
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
left: 0;
|
||||
border-left: 1px solid gray;
|
||||
border-top: 1px solid gray;
|
||||
border-radius: 4px 0 0;
|
||||
width: 80%;
|
||||
height: calc(100% - 12px);
|
||||
}
|
||||
|
||||
.dot {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.children {
|
||||
.dot {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
left: 70%;
|
||||
transform: translate3d(0, -40%, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.full-root {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.top-line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 80%;
|
||||
height: 22px;
|
||||
border-left: 1px solid gray;
|
||||
border-bottom: 1px solid gray;
|
||||
border-radius: 0 0 0 4px;
|
||||
}
|
||||
|
||||
.bottom-line {
|
||||
position: absolute;
|
||||
top: 19px;
|
||||
left: 0;
|
||||
width: 1px;
|
||||
height: calc(100% - 19px);
|
||||
background: gray;
|
||||
}
|
||||
|
||||
.dot {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.children {
|
||||
.dot {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
left: 70%;
|
||||
transform: translate3d(0, -60%, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.half-top-child {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-left: 1px solid gray;
|
||||
border-bottom: 1px solid gray;
|
||||
border-radius: 0 0 0 4px;
|
||||
width: 80%;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.children {
|
||||
.dot {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
left: 70%;
|
||||
transform: translate3d(0, -60%, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.full-child {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.top-line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 80%;
|
||||
height: 22px;
|
||||
border-left: 1px solid gray;
|
||||
border-bottom: 1px solid gray;
|
||||
border-radius: 0 0 0 4px;
|
||||
}
|
||||
|
||||
.bottom-line {
|
||||
position: absolute;
|
||||
top: 19px;
|
||||
left: 0;
|
||||
width: 1px;
|
||||
height: calc(100% - 19px);
|
||||
background: gray;
|
||||
}
|
||||
|
||||
.dot {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.children {
|
||||
.dot {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
left: 70%;
|
||||
transform: translate3d(0, -60%, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.multiline {
|
||||
.line {
|
||||
position: absolute;
|
||||
top: -51px;
|
||||
height: 73px;
|
||||
}
|
||||
|
||||
.top-line {
|
||||
position: absolute;
|
||||
top: -51px;
|
||||
height: 73px;
|
||||
}
|
||||
|
||||
&.with-name-error {
|
||||
.line {
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
height: 29px;
|
||||
}
|
||||
|
||||
.top-line {
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
height: 29px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
/*
|
||||
* 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 classNames from 'classnames';
|
||||
|
||||
import { LineShowResult, getLineShowResult } from '@/parameters/utils/utils';
|
||||
|
||||
import { type TreeNodeCustomData } from '../../type';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface IconComponentProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export function Dot({ className, style }: IconComponentProps) {
|
||||
return <div className={classNames(styles.dot, className)} style={style} />;
|
||||
}
|
||||
|
||||
export function EmptyBlock({ className, style }: IconComponentProps) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles['empty-block'], className)}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function HelpLineBlock({ className, style }: IconComponentProps) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles['help-line-block'], className)}
|
||||
style={style}
|
||||
>
|
||||
<div className={styles.line} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HalfTopRoot({ className, style }: IconComponentProps) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles['half-top-root'], className)}
|
||||
style={style}
|
||||
>
|
||||
<div className={styles.line} />
|
||||
<Dot className={styles.dot} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HalfTopRootWithChildren({
|
||||
className,
|
||||
style,
|
||||
}: IconComponentProps) {
|
||||
return (
|
||||
<HalfTopRoot
|
||||
className={classNames(styles.children, className)}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function HalfBottomRoot({ className, style }: IconComponentProps) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles['half-bottom-root'], className)}
|
||||
style={style}
|
||||
>
|
||||
<div className={styles.line} />
|
||||
<Dot className={styles.dot} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HalfBottomRootWithChildren({
|
||||
className,
|
||||
style,
|
||||
}: IconComponentProps) {
|
||||
return (
|
||||
<HalfBottomRoot
|
||||
className={classNames(styles.children, className)}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function FullRoot({ className, style }: IconComponentProps) {
|
||||
return (
|
||||
<div className={classNames(styles['full-root'], className)} style={style}>
|
||||
<div className={styles['top-line']} />
|
||||
<div className={styles['bottom-line']} />
|
||||
<Dot className={styles.dot} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FullRootWithChildren({ className, style }: IconComponentProps) {
|
||||
return (
|
||||
<FullRoot
|
||||
className={classNames(styles.children, className)}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function HalfTopChild({ className, style }: IconComponentProps) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles['half-top-child'], className)}
|
||||
style={style}
|
||||
>
|
||||
<div className={styles.line} />
|
||||
<Dot className={styles.dot} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HalfTopChildWithChildren({
|
||||
className,
|
||||
style,
|
||||
}: IconComponentProps) {
|
||||
return (
|
||||
<HalfTopChild
|
||||
className={classNames(styles.children, className)}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function FullChild({ className, style }: IconComponentProps) {
|
||||
return (
|
||||
<div className={classNames(styles['full-child'], className)} style={style}>
|
||||
<div className={styles['top-line']} />
|
||||
<div className={styles['bottom-line']} />
|
||||
<Dot className={styles.dot} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FullChildWithChildren({
|
||||
className,
|
||||
style,
|
||||
}: IconComponentProps) {
|
||||
return (
|
||||
<FullChild
|
||||
className={classNames(styles.children, className)}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface LevelLineProps {
|
||||
level: number;
|
||||
data: TreeNodeCustomData;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
multiInfo?: {
|
||||
multiline: boolean;
|
||||
withNameError?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export default function LevelLine({
|
||||
level,
|
||||
data,
|
||||
className,
|
||||
style,
|
||||
multiInfo = { multiline: false },
|
||||
}: LevelLineProps) {
|
||||
// getLineShowResult 返回数据,暂时没涉及到 root 画线
|
||||
const lineShowResult = getLineShowResult({ level, data });
|
||||
const showMap: Record<LineShowResult, React.ReactNode> = {
|
||||
[LineShowResult.HalfTopRoot]: (
|
||||
<HalfTopRoot className={className} style={style} />
|
||||
),
|
||||
[LineShowResult.HalfTopRootWithChildren]: (
|
||||
<HalfTopRootWithChildren className={className} style={style} />
|
||||
),
|
||||
[LineShowResult.HalfBottomRoot]: (
|
||||
<HalfBottomRoot className={className} style={style} />
|
||||
),
|
||||
[LineShowResult.HalfBottomRootWithChildren]: (
|
||||
<HalfBottomRootWithChildren className={className} style={style} />
|
||||
),
|
||||
[LineShowResult.FullRoot]: <FullRoot className={className} style={style} />,
|
||||
[LineShowResult.FullRootWithChildren]: (
|
||||
<FullRootWithChildren className={className} style={style} />
|
||||
),
|
||||
// 在 output tree 中,暂时没涉及到 root 画线
|
||||
[LineShowResult.HalfTopChild]: (
|
||||
<HalfTopChild
|
||||
className={classNames(
|
||||
multiInfo?.multiline ? styles.multiline : null,
|
||||
multiInfo?.withNameError ? styles['with-name-error'] : null,
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
/>
|
||||
),
|
||||
[LineShowResult.HalfTopChildWithChildren]: (
|
||||
<HalfTopChildWithChildren
|
||||
className={classNames(
|
||||
multiInfo?.multiline ? styles.multiline : null,
|
||||
multiInfo?.withNameError ? styles['with-name-error'] : null,
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
/>
|
||||
),
|
||||
[LineShowResult.FullChild]: (
|
||||
<FullChild
|
||||
className={classNames(
|
||||
multiInfo?.multiline ? styles.multiline : null,
|
||||
multiInfo?.withNameError ? styles['with-name-error'] : null,
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
/>
|
||||
),
|
||||
[LineShowResult.FullChildWithChildren]: (
|
||||
<FullChildWithChildren
|
||||
className={classNames(
|
||||
multiInfo?.multiline ? styles.multiline : null,
|
||||
multiInfo?.withNameError ? styles['with-name-error'] : null,
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
/>
|
||||
),
|
||||
[LineShowResult.EmptyBlock]: (
|
||||
<EmptyBlock className={className} style={style} />
|
||||
),
|
||||
[LineShowResult.HelpLineBlock]: (
|
||||
<HelpLineBlock className={className} style={style} />
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{lineShowResult.map((item, index) => (
|
||||
<React.Fragment key={index}>{showMap[item]}</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
align-self: stretch;
|
||||
flex-direction: column;
|
||||
margin-left: 8px;
|
||||
|
||||
.desc-not-focus {
|
||||
textarea {
|
||||
padding: 0 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.desc-not-focus-with-value {
|
||||
textarea {
|
||||
padding: 0 12px;
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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, { useState } from 'react';
|
||||
|
||||
import cs from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import WorkflowSLTextArea from '../workflow-sl-textarea';
|
||||
import { type TreeNodeCustomData } from '../../type';
|
||||
import { DescriptionLine } from '../../constants';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface ParamNameProps {
|
||||
data: TreeNodeCustomData;
|
||||
disabled?: boolean;
|
||||
onChange: (desc: string) => void;
|
||||
onLineChange?: (type: DescriptionLine) => void;
|
||||
hasObjectLike?: boolean;
|
||||
}
|
||||
|
||||
export default function ParamDescription({
|
||||
data,
|
||||
disabled,
|
||||
onChange,
|
||||
onLineChange,
|
||||
hasObjectLike,
|
||||
}: ParamNameProps) {
|
||||
const [inputFocus, setInputFocus] = useState(false);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<WorkflowSLTextArea
|
||||
className={cs(
|
||||
inputFocus
|
||||
? null
|
||||
: data.description
|
||||
? styles['desc-not-focus-with-value']
|
||||
: styles['desc-not-focus'],
|
||||
styles.desc,
|
||||
hasObjectLike ? styles['desc-object-like'] : null,
|
||||
)}
|
||||
value={data.description}
|
||||
ellipsis={true}
|
||||
// 好像不生效
|
||||
disabled={disabled}
|
||||
handleBlur={() => {
|
||||
setInputFocus(false);
|
||||
onLineChange?.(DescriptionLine.Single);
|
||||
}}
|
||||
handleChange={(desc: string) => {
|
||||
onChange(desc);
|
||||
}}
|
||||
handleFocus={() => {
|
||||
setInputFocus(true);
|
||||
onLineChange?.(DescriptionLine.Multi);
|
||||
}}
|
||||
textAreaProps={
|
||||
inputFocus
|
||||
? {
|
||||
placeholder: I18n.t('workflow_detail_llm_output_decription'),
|
||||
maxLength: 50,
|
||||
rows: 2,
|
||||
autosize: false,
|
||||
maxCount: 50,
|
||||
}
|
||||
: {
|
||||
placeholder: I18n.t('workflow_detail_llm_output_decription'),
|
||||
rows: 1,
|
||||
autosize: false,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
.container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-self: stretch;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
&.withDescription {
|
||||
flex: auto;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.name {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
|
||||
&>div:first-child {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 React, { useState } from 'react';
|
||||
import type { CSSProperties } from 'react';
|
||||
|
||||
import cx from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import useErrorMessage from '@/parameters/hooks/use-error-message';
|
||||
import useConfig from '@/parameters/hooks/use-config';
|
||||
|
||||
import WorkflowSLInput from '../workflow-sl-input';
|
||||
import { type TreeNodeCustomData } from '../../type';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface ParamNameProps {
|
||||
data: TreeNodeCustomData;
|
||||
disabled?: boolean;
|
||||
style?: CSSProperties;
|
||||
onChange: (name: string) => void;
|
||||
}
|
||||
|
||||
export default function ParamName({
|
||||
disabled,
|
||||
data,
|
||||
style,
|
||||
onChange,
|
||||
}: ParamNameProps) {
|
||||
const errorMessage = useErrorMessage('name');
|
||||
const [slient, setSlient] = useState(true);
|
||||
const showError = slient === false && errorMessage;
|
||||
const { withDescription } = useConfig();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(styles.container, {
|
||||
[styles.withDescription]: withDescription,
|
||||
})}
|
||||
style={style}
|
||||
>
|
||||
<WorkflowSLInput
|
||||
className={styles.name}
|
||||
value={data.name || ''}
|
||||
disabled={disabled}
|
||||
handleBlur={() => setSlient(false)}
|
||||
handleChange={(name: string) => {
|
||||
setSlient(true);
|
||||
onChange(name);
|
||||
}}
|
||||
inputProps={{
|
||||
size: 'small',
|
||||
placeholder: I18n.t('workflow_detail_end_output_entername'),
|
||||
disabled,
|
||||
}}
|
||||
errorMsg={showError ? errorMessage : ''}
|
||||
validateStatus={showError ? 'error' : 'default'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 React from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { UIIconButton } from '@coze-arch/bot-semi';
|
||||
import { IconAdd } from '@coze-arch/bot-icons';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
type AddOperationProps = React.PropsWithChildren<{
|
||||
readonly?: boolean;
|
||||
onClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
disabled?: boolean;
|
||||
}>;
|
||||
|
||||
export default function AddOperation({
|
||||
readonly,
|
||||
onClick,
|
||||
className,
|
||||
style,
|
||||
disabled,
|
||||
}: AddOperationProps) {
|
||||
if (readonly) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<UIIconButton
|
||||
onClick={onClick}
|
||||
className={classNames(
|
||||
styles.container,
|
||||
disabled ? styles.disabled : null,
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
icon={<IconAdd className={styles.icon} />}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
align-self: stretch;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-left: 8px;
|
||||
|
||||
.icon-no {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
color: #888D92;
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&>svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add {
|
||||
margin-left: 8px;
|
||||
color: #4D53E8;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
@@ -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 React from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Tooltip } from '@coze-arch/bot-semi';
|
||||
import { IconNo } from '@coze-arch/bot-icons';
|
||||
|
||||
import { OperatorLargeSize, OperatorSmallSize } from '@/parameters/constants';
|
||||
|
||||
import { type TreeNodeCustomData } from '../../type';
|
||||
import { ObjectLikeTypes } from '../../constants';
|
||||
import AddOperation from './add-operation';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface ParamOperatorProps {
|
||||
data: TreeNodeCustomData;
|
||||
level: number;
|
||||
onAppend: () => void;
|
||||
onDelete: () => void;
|
||||
disableDelete: boolean;
|
||||
hasObjectLike?: boolean;
|
||||
}
|
||||
|
||||
export default function ParamOperator({
|
||||
data,
|
||||
level,
|
||||
onDelete,
|
||||
onAppend,
|
||||
disableDelete,
|
||||
hasObjectLike,
|
||||
}: ParamOperatorProps) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
const isLimited = level >= 3;
|
||||
// 是否展示新增子项的按钮
|
||||
const needRenderAppendChild =
|
||||
ObjectLikeTypes.includes(data.type) && !isLimited;
|
||||
const computedOperatorStyle = (): React.CSSProperties => {
|
||||
if (!hasObjectLike) {
|
||||
return { width: OperatorSmallSize };
|
||||
}
|
||||
return { width: OperatorLargeSize };
|
||||
};
|
||||
return (
|
||||
<div className={styles.container} style={computedOperatorStyle()}>
|
||||
<div
|
||||
className={styles['icon-no']}
|
||||
onClick={() => {
|
||||
if (disableDelete) {
|
||||
return;
|
||||
}
|
||||
onDelete();
|
||||
}}
|
||||
>
|
||||
<IconNo
|
||||
className={classNames({
|
||||
[styles.icon]: true,
|
||||
[styles.disabled]: disableDelete,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
{needRenderAppendChild && (
|
||||
<div className={styles.add}>
|
||||
<Tooltip content={I18n.t('workflow_detail_node_output_add_subitem')}>
|
||||
<div>
|
||||
<AddOperation onClick={onAppend} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
.container {
|
||||
display: flex;
|
||||
// align-items: center;
|
||||
position: relative;
|
||||
align-self: stretch;
|
||||
flex-direction: column;
|
||||
width: 155px;
|
||||
margin-left: 8px;
|
||||
|
||||
|
||||
|
||||
.pop-container {
|
||||
align-self: self-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.param-type {
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
padding: 0 1px;
|
||||
}
|
||||
|
||||
// 已经无用,可以删除,添加子项按钮已经放在单独组件中了
|
||||
.param-operator {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
align-self: stretch;
|
||||
width: 86px;
|
||||
height: 24px;
|
||||
margin-left: 8px;
|
||||
|
||||
.icon-no {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
color: #888D92;
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&>svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add {
|
||||
margin-left: 8px;
|
||||
color: #4D53E8;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 React from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type SelectProps } from '@coze-arch/bot-semi/Select';
|
||||
import { Select } from '@coze-arch/bot-semi';
|
||||
|
||||
import convertMapToOptions from '@/parameters/utils/convert-map-to-options';
|
||||
import { PARAM_TYPE_ALIAS_MAP, ParamTypeAlias } from '@/parameters/types';
|
||||
|
||||
import PopupContainer from '../popup-container';
|
||||
import { type TreeNodeCustomData } from '../../type';
|
||||
import { ObjectLikeTypes } from '../../constants';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface ParamTypeProps {
|
||||
data: TreeNodeCustomData;
|
||||
level: number;
|
||||
onSelectChange?: SelectProps['onChange'];
|
||||
disabled?: boolean;
|
||||
// 不支持使用的类型
|
||||
disabledTypes?: ParamTypeAlias[];
|
||||
}
|
||||
|
||||
export default function ParamType({
|
||||
data,
|
||||
onSelectChange,
|
||||
level,
|
||||
disabled,
|
||||
disabledTypes = [],
|
||||
}: ParamTypeProps) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
const isLimited = level >= 3;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<PopupContainer className={styles['pop-container']}>
|
||||
<Select
|
||||
placeholder={I18n.t('workflow_detail_start_variable_type')}
|
||||
disabled={disabled}
|
||||
onChange={val => {
|
||||
onSelectChange?.(val);
|
||||
}}
|
||||
className={styles['param-type']}
|
||||
optionList={convertMapToOptions(PARAM_TYPE_ALIAS_MAP, {
|
||||
computedValue: Number,
|
||||
passItem: item => item === ParamTypeAlias.List.toString(),
|
||||
}).map(item => ({
|
||||
...item,
|
||||
disabled:
|
||||
disabledTypes?.includes(Number(item.value)) ||
|
||||
(isLimited && ObjectLikeTypes.includes(Number(item.value))),
|
||||
}))}
|
||||
value={data.type}
|
||||
/>
|
||||
</PopupContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
.popup-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.popup-container-id {
|
||||
position: relative;
|
||||
:global {
|
||||
.semi-portal-inner {
|
||||
top: 2px !important;
|
||||
left: 0 !important;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 React, {
|
||||
type PropsWithChildren,
|
||||
type ReactElement,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export const PopupContainer: React.FC<
|
||||
PropsWithChildren<{
|
||||
className?: string;
|
||||
containerName?: string;
|
||||
containerClassName?: string;
|
||||
containerStyle?: React.CSSProperties;
|
||||
}>
|
||||
> = ({
|
||||
className,
|
||||
children,
|
||||
containerName,
|
||||
containerClassName,
|
||||
containerStyle,
|
||||
}) => {
|
||||
const _nanoid = useMemo(
|
||||
() => `${containerName || 'popup_container'}_${nanoid()}`,
|
||||
[containerName],
|
||||
);
|
||||
const _children = React.cloneElement(children as unknown as ReactElement, {
|
||||
getPopupContainer: () => document.getElementById(_nanoid) as HTMLElement,
|
||||
});
|
||||
return (
|
||||
<div className={classNames(s['popup-container'], className)}>
|
||||
{_children}
|
||||
<div
|
||||
id={_nanoid}
|
||||
style={containerStyle}
|
||||
className={classNames([
|
||||
'nowheel',
|
||||
s['popup-container-id'],
|
||||
containerClassName,
|
||||
])}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopupContainer;
|
||||
@@ -0,0 +1,47 @@
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
>* {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
span {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.error-content {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.error-float {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 14px;
|
||||
font-family: "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
line-height: 22px;
|
||||
color: var(--semi-color-danger)
|
||||
}
|
||||
|
||||
.input {
|
||||
input {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.limit-count {
|
||||
padding-left: 8px;
|
||||
padding-right: 12px;
|
||||
overflow: hidden;
|
||||
color: var(--light-usage-text-color-text-3, rgba(28, 31, 35, 0.35));
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* 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, {
|
||||
type ComponentProps,
|
||||
useRef,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useEffect,
|
||||
type ForwardedRef,
|
||||
} from 'react';
|
||||
|
||||
import isNumber from 'lodash-es/isNumber';
|
||||
import cs from 'classnames';
|
||||
import { useReactive } from 'ahooks';
|
||||
import { type TooltipProps } from '@coze-arch/bot-semi/Tooltip';
|
||||
import { type InputProps } from '@coze-arch/bot-semi/Input';
|
||||
import { UIInput } from '@coze-arch/bot-semi';
|
||||
|
||||
import AutoSizeTooltip from '../auto-size-tooltip';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export interface WorkflowSLInputRefType {
|
||||
triggerFocus?: () => void;
|
||||
}
|
||||
|
||||
export type WorkflowSLInputProps = ComponentProps<typeof UIInput> & {
|
||||
value: string | undefined;
|
||||
onRef?: ForwardedRef<WorkflowSLInputRefType>;
|
||||
ellipsis?: boolean;
|
||||
handleChange?: (v: string) => void;
|
||||
handleBlur?: (v: string) => void;
|
||||
handleFocus?: (v: string) => void;
|
||||
ellipsisTooltipProps?: TooltipProps;
|
||||
onFocusTooltipProps?: TooltipProps;
|
||||
tooltipProps?: TooltipProps;
|
||||
inputProps?: InputProps;
|
||||
errorMsg?: string;
|
||||
errorMsgFloat?: boolean;
|
||||
maxCount?: number;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
const SL_INPUT_TIMEOUT = 10;
|
||||
|
||||
export default function WorkflowSLInput(props: WorkflowSLInputProps) {
|
||||
const { ellipsis = true, maxCount } = props;
|
||||
const showCount = isNumber(maxCount) && maxCount > 0;
|
||||
useImperativeHandle(props.onRef, () => ({
|
||||
triggerFocus,
|
||||
}));
|
||||
const $state = useReactive({
|
||||
value: props.value,
|
||||
inputOnFocus: false,
|
||||
inputEle: false,
|
||||
});
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const triggerFocus = () => {
|
||||
$state.inputEle = true;
|
||||
inputRef?.current?.focus();
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
$state.inputOnFocus = true;
|
||||
$state.inputEle = true;
|
||||
props?.handleFocus?.($state.value || '');
|
||||
};
|
||||
|
||||
const onBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
$state.inputOnFocus = false;
|
||||
props?.handleBlur?.($state.value || '');
|
||||
props?.onBlur?.(e);
|
||||
$state.inputEle = false;
|
||||
};
|
||||
|
||||
const onChange = (v: string) => {
|
||||
$state.value = v;
|
||||
props?.handleChange?.(v);
|
||||
};
|
||||
|
||||
const onclick = () => {
|
||||
if (!$state.inputEle) {
|
||||
setTimeout(() => {
|
||||
inputRef?.current?.focus();
|
||||
}, SL_INPUT_TIMEOUT);
|
||||
}
|
||||
$state.inputEle = true;
|
||||
};
|
||||
const hasEllipsis = useMemo(() => {
|
||||
const clientWidth = inputRef.current?.clientWidth || 0;
|
||||
const scrollWidth = inputRef.current?.scrollWidth || 0;
|
||||
return clientWidth < scrollWidth - 1;
|
||||
}, [
|
||||
ellipsis,
|
||||
$state.inputOnFocus,
|
||||
$state.value,
|
||||
inputRef.current?.clientWidth,
|
||||
inputRef.current?.scrollWidth,
|
||||
$state.inputEle,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
$state.value = props.value;
|
||||
}, [props.value]);
|
||||
|
||||
const LimitCountNode = (
|
||||
<span className={s['limit-count']}>
|
||||
{$state.value?.length || 0}/{maxCount}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cs(s['input-wrapper'], props.className)}
|
||||
style={props.style}
|
||||
>
|
||||
{!$state.inputEle && hasEllipsis ? (
|
||||
<AutoSizeTooltip
|
||||
content={$state.value}
|
||||
position={'top'}
|
||||
showArrow
|
||||
mouseEnterDelay={300}
|
||||
{...props.tooltipProps}
|
||||
>
|
||||
<div
|
||||
className={cs(props?.errorMsg ? s['error-wrapper'] : null)}
|
||||
onClick={onclick}
|
||||
>
|
||||
<UIInput
|
||||
{...props.inputProps}
|
||||
validateStatus={props.validateStatus}
|
||||
ref={inputRef}
|
||||
value={$state.value}
|
||||
className={ellipsis ? s.input : ''}
|
||||
suffix={showCount ? LimitCountNode : undefined}
|
||||
></UIInput>
|
||||
</div>
|
||||
</AutoSizeTooltip>
|
||||
) : (
|
||||
<div className={cs(props?.errorMsg ? s['error-wrapper'] : null)}>
|
||||
<AutoSizeTooltip
|
||||
{...props.onFocusTooltipProps}
|
||||
trigger="custom"
|
||||
visible={
|
||||
Boolean(props.onFocusTooltipProps?.content) && $state.inputOnFocus
|
||||
}
|
||||
showArrow
|
||||
>
|
||||
<UIInput
|
||||
{...props.inputProps}
|
||||
validateStatus={props.validateStatus}
|
||||
ref={inputRef}
|
||||
value={$state.value}
|
||||
className={ellipsis ? s.input : ''}
|
||||
onChange={onChange}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
suffix={showCount ? LimitCountNode : undefined}
|
||||
></UIInput>
|
||||
</AutoSizeTooltip>
|
||||
</div>
|
||||
)}
|
||||
{props?.errorMsg && (
|
||||
<div
|
||||
className={cs(
|
||||
s['error-content'],
|
||||
props?.errorMsgFloat ? s['error-float'] : null,
|
||||
)}
|
||||
>
|
||||
<div className={s['error-text']}>{props?.errorMsg}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// todo: 这里需要修复为正确的值
|
||||
@error-red: #ddd;
|
||||
|
||||
.input-wrapper {
|
||||
width: 100%;
|
||||
|
||||
span {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.error-wrapper {
|
||||
:global {
|
||||
.semi-input-wrapper {
|
||||
border: 1px solid @error-red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-content {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.error-float {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
position: absolute;
|
||||
|
||||
padding-top: 2px;
|
||||
padding-left: 12px;
|
||||
|
||||
font-size: 12px;
|
||||
color: @error-red;
|
||||
}
|
||||
|
||||
.textarea-pd {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.inputting {
|
||||
textarea {
|
||||
.textarea-pd;
|
||||
}
|
||||
}
|
||||
|
||||
.input-blur {
|
||||
textarea {
|
||||
.textarea-pd;
|
||||
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* 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 @coze-arch/max-line-per-function */
|
||||
import React, {
|
||||
type ComponentProps,
|
||||
useRef,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useEffect,
|
||||
type ForwardedRef,
|
||||
} from 'react';
|
||||
|
||||
import cs from 'classnames';
|
||||
import { useReactive } from 'ahooks';
|
||||
import { type TooltipProps } from '@coze-arch/bot-semi/Tooltip';
|
||||
import { type TextAreaProps } from '@coze-arch/bot-semi/Input';
|
||||
import { TextArea } from '@coze-arch/bot-semi';
|
||||
|
||||
import AutoSizeTooltip from '../auto-size-tooltip';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export interface WorkflowSLTextAreaRefType {
|
||||
triggerFocus?: () => void;
|
||||
}
|
||||
|
||||
export type WorkflowSLTextAreaProps = ComponentProps<typeof TextArea> & {
|
||||
value: string | undefined;
|
||||
onRef?: ForwardedRef<WorkflowSLTextAreaRefType>;
|
||||
ellipsis?: boolean;
|
||||
handleChange?: (v: string) => void;
|
||||
handleBlur?: (v: string) => void;
|
||||
handleFocus?: (v: string) => void;
|
||||
ellipsisTooltipProps?: TooltipProps;
|
||||
onFocusTooltipProps?: TooltipProps;
|
||||
textAreaProps?: TextAreaProps;
|
||||
errorMsg?: string;
|
||||
errorMsgFloat?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* @component TextArea 在 Workflow 场景下的二次封装;
|
||||
* focus(inputting) 的时候提供多行滚动输入能力,blur 的时候提供 ellipsis 和 tooltip 提示能力
|
||||
*/
|
||||
export default function WorkflowSLTextArea(props: WorkflowSLTextAreaProps) {
|
||||
const { ellipsis = true } = props;
|
||||
useImperativeHandle(props.onRef, () => ({
|
||||
triggerFocus,
|
||||
}));
|
||||
const $state = useReactive({
|
||||
value: props.value,
|
||||
inputOnFocus: false,
|
||||
inputHover: false,
|
||||
});
|
||||
|
||||
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const triggerFocus = () => {
|
||||
$state.inputOnFocus = true;
|
||||
textAreaRef?.current?.focus();
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
$state.inputOnFocus = true;
|
||||
props?.handleFocus?.($state.value || '');
|
||||
};
|
||||
|
||||
const onBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
|
||||
$state.inputOnFocus = false;
|
||||
props?.handleBlur?.($state.value || '');
|
||||
props?.onBlur?.(e);
|
||||
// 失焦的时候,滚动到最顶端
|
||||
if (textAreaRef?.current) {
|
||||
textAreaRef.current.scrollTop = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (v: string) => {
|
||||
$state.value = v;
|
||||
props?.handleChange?.(v);
|
||||
};
|
||||
|
||||
// 输入法输入结束
|
||||
const onCompositionEnd = (e: React.CompositionEvent<HTMLTextAreaElement>) => {
|
||||
const target = e.target as HTMLTextAreaElement;
|
||||
|
||||
if (
|
||||
props.textAreaProps?.maxCount &&
|
||||
(target.textLength || 0) > props.textAreaProps?.maxCount
|
||||
) {
|
||||
const v = target.value?.slice(0, props.textAreaProps?.maxCount);
|
||||
$state.value = v;
|
||||
props?.handleChange?.(v);
|
||||
}
|
||||
};
|
||||
|
||||
const hasEllipsis = useMemo(() => {
|
||||
const clientHeight = textAreaRef.current?.clientHeight || 0;
|
||||
const scrollHeight = textAreaRef.current?.scrollHeight || 0;
|
||||
return clientHeight < scrollHeight - 1;
|
||||
}, [
|
||||
ellipsis,
|
||||
$state.inputOnFocus,
|
||||
$state.value,
|
||||
textAreaRef.current?.clientHeight,
|
||||
textAreaRef.current?.scrollHeight,
|
||||
props.textAreaProps?.rows,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
$state.value = props.value;
|
||||
}, [props.value]);
|
||||
|
||||
/** 是否处于失焦缩略状态 */
|
||||
const ellipsisWithBlur = useMemo(
|
||||
() => !$state.inputOnFocus && hasEllipsis,
|
||||
[hasEllipsis, $state.inputOnFocus],
|
||||
);
|
||||
|
||||
const showTooltip = useMemo(
|
||||
() =>
|
||||
ellipsisWithBlur
|
||||
? Boolean($state.value) && $state.inputHover
|
||||
: Boolean(props.onFocusTooltipProps?.content) && $state.inputOnFocus,
|
||||
[
|
||||
ellipsisWithBlur,
|
||||
$state.inputHover,
|
||||
$state.inputOnFocus,
|
||||
props.onFocusTooltipProps?.content,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cs(s['input-wrapper'], props.className)}>
|
||||
<AutoSizeTooltip
|
||||
content={
|
||||
<article
|
||||
style={{
|
||||
maxWidth: 200,
|
||||
wordWrap: 'break-word',
|
||||
wordBreak: 'normal',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
{$state.value}
|
||||
</article>
|
||||
}
|
||||
position={'top'}
|
||||
showArrow
|
||||
mouseEnterDelay={300}
|
||||
trigger="custom"
|
||||
visible={showTooltip}
|
||||
{...(ellipsisWithBlur
|
||||
? props.ellipsisTooltipProps
|
||||
: props.onFocusTooltipProps)}
|
||||
>
|
||||
<div
|
||||
className={cs(props?.errorMsg ? s['error-wrapper'] : null)}
|
||||
onMouseEnter={() => {
|
||||
$state.inputHover = true;
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
$state.inputHover = false;
|
||||
}}
|
||||
>
|
||||
<TextArea
|
||||
{...props.textAreaProps}
|
||||
ref={textAreaRef}
|
||||
value={$state.value}
|
||||
className={
|
||||
ellipsis
|
||||
? !$state.inputOnFocus
|
||||
? s['input-blur']
|
||||
: s.inputting
|
||||
: ''
|
||||
}
|
||||
onChange={onChange}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
disabled={props.disabled}
|
||||
onCompositionEnd={onCompositionEnd}
|
||||
></TextArea>
|
||||
</div>
|
||||
</AutoSizeTooltip>
|
||||
|
||||
{props?.errorMsg && (
|
||||
<div
|
||||
className={cs(
|
||||
s['error-content'],
|
||||
props?.errorMsgFloat ? s['error-float'] : null,
|
||||
)}
|
||||
>
|
||||
<div className={s['error-text']}>{props?.errorMsg}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 { ParamTypeAlias } from '../../types';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const ObjectLikeTypes = [
|
||||
ParamTypeAlias.Object,
|
||||
ParamTypeAlias.ArrayObject,
|
||||
];
|
||||
|
||||
export enum ChangeMode {
|
||||
Update,
|
||||
Delete,
|
||||
Append,
|
||||
DeleteChildren,
|
||||
}
|
||||
|
||||
export enum DescriptionLine {
|
||||
Single = 'singleline',
|
||||
Multi = 'multiline',
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
.font-normal {
|
||||
color: rgba(28, 31, 35, 0.80);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center !important;
|
||||
position: relative;
|
||||
|
||||
// 下面是兼容线的样式的,不要动
|
||||
&:first-child {
|
||||
& > div:first-child {
|
||||
& > div {
|
||||
& > div:first-child {
|
||||
top: 12px;
|
||||
}
|
||||
& > div:last-child {
|
||||
top: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
& > div:last-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
:global {
|
||||
.semi-form-field {
|
||||
flex: 1;
|
||||
}
|
||||
.semi-tree-option-expand-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
.level-icon {
|
||||
display: flex;
|
||||
align-self: stretch;
|
||||
}
|
||||
.wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
.readonly-icon-container {
|
||||
margin-top: 10px;
|
||||
&.more-level {
|
||||
cursor: default;
|
||||
& > span,& > div {
|
||||
cursor: pointer;
|
||||
}
|
||||
&:hover, &:active {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.readonly-container {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
.name {
|
||||
.font-normal();
|
||||
word-break: keep-all;
|
||||
}
|
||||
.tag {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
background: rgba(230, 232, 234, 0.76);
|
||||
margin-left: 8px;
|
||||
.label {
|
||||
.font-normal();
|
||||
color: rgba(28, 31, 35, 0.60);
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
* 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 @coze-arch/max-line-per-function */
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
|
||||
import isNumber from 'lodash-es/isNumber';
|
||||
import classNames from 'classnames';
|
||||
import { type RenderFullLabelProps } from '@coze-arch/bot-semi/Tree';
|
||||
|
||||
import { PARAM_TYPE_ALIAS_MAP, type ParamTypeAlias } from '../../types';
|
||||
import useConfig from '../../hooks/use-config';
|
||||
import NodeContext from '../../context/node-context';
|
||||
import { type TreeNodeCustomData, type ActiveMultiInfo } from './type';
|
||||
import { ChangeMode, ObjectLikeTypes, DescriptionLine } from './constants';
|
||||
import ParamType from './components/param-type';
|
||||
import ParamOperator from './components/param-operator';
|
||||
import ParamName from './components/param-name';
|
||||
import ParamDescription from './components/param-description';
|
||||
import LevelLine from './components/line-component';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface CustomTreeNodeProps extends RenderFullLabelProps {
|
||||
onChange: (mode: ChangeMode, param: TreeNodeCustomData) => void;
|
||||
// Description 组件变换为多行时,其下面第一个 child 需被记录
|
||||
onActiveMultiInfoChange?: (info: ActiveMultiInfo) => void;
|
||||
activeMultiInfo?: ActiveMultiInfo;
|
||||
// 不支持使用的类型
|
||||
disabledTypes?: ParamTypeAlias[];
|
||||
}
|
||||
|
||||
const LEVEL_LINE_STEP_WIDTH = 15;
|
||||
|
||||
export default function CustomTreeNode(props: CustomTreeNodeProps) {
|
||||
const {
|
||||
data,
|
||||
onExpand,
|
||||
expandIcon,
|
||||
className,
|
||||
level,
|
||||
onChange,
|
||||
onActiveMultiInfoChange,
|
||||
activeMultiInfo,
|
||||
disabledTypes = [],
|
||||
} = props;
|
||||
const { allowValueEmpty, readonly, hasObjectLike, withDescription } =
|
||||
useConfig();
|
||||
// 当前值
|
||||
const value = data as TreeNodeCustomData;
|
||||
const isTopLevel = level === 0;
|
||||
const isOnlyOneData = value.isSingle && isTopLevel;
|
||||
const IndentationWidth = level * LEVEL_LINE_STEP_WIDTH;
|
||||
const paramNameWidth = 181;
|
||||
|
||||
const treeNodeRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const disableDelete = Boolean(
|
||||
!allowValueEmpty && isOnlyOneData && isTopLevel,
|
||||
);
|
||||
// 删除时
|
||||
const onDelete = () => {
|
||||
onChange(ChangeMode.Delete, value);
|
||||
};
|
||||
// 新增子项时
|
||||
const onAppend = () => {
|
||||
onChange(ChangeMode.Append, value);
|
||||
};
|
||||
// 类型切换时
|
||||
const onSelectChange = (
|
||||
val?: string | number | Array<unknown> | Record<string, unknown>,
|
||||
) => {
|
||||
if (val === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNumber(val)) {
|
||||
const isObjectLike = ObjectLikeTypes.includes(val);
|
||||
if (!isObjectLike) {
|
||||
// 如果不是类Object,判断是否有children,如果有,删除掉
|
||||
if (value.children && value.children.length > 0) {
|
||||
delete value.children;
|
||||
}
|
||||
}
|
||||
|
||||
onChange(ChangeMode.Update, { ...value, type: val });
|
||||
}
|
||||
|
||||
// 更新type
|
||||
};
|
||||
// 更新
|
||||
const onNameChange = (name: string) => {
|
||||
onChange(ChangeMode.Update, { ...value, name });
|
||||
};
|
||||
|
||||
// 更新
|
||||
const onDescriptionChange = useCallback(
|
||||
(description: string) => {
|
||||
onChange(ChangeMode.Update, { ...value, description });
|
||||
},
|
||||
[onChange, value],
|
||||
);
|
||||
|
||||
/**
|
||||
* Description 组件单行 / 多行变换时,其下面第一个 child 的竖线需要缩短 / 延长
|
||||
*/
|
||||
const onDescriptionLineChange = useCallback(
|
||||
(type: DescriptionLine) => {
|
||||
const errorDoms = treeNodeRef.current?.getElementsByClassName(
|
||||
'output-param-name-error-text',
|
||||
);
|
||||
|
||||
if (type === DescriptionLine.Multi && value.children?.[0]?.field) {
|
||||
onActiveMultiInfoChange?.({
|
||||
activeMultiKey: value.children[0].field,
|
||||
withNameError: Boolean(errorDoms?.length || 0),
|
||||
// withNameError: Boolean(nameError || ''),
|
||||
});
|
||||
} else {
|
||||
onActiveMultiInfoChange?.({
|
||||
activeMultiKey: '',
|
||||
});
|
||||
}
|
||||
},
|
||||
[onActiveMultiInfoChange, value],
|
||||
);
|
||||
|
||||
if (readonly) {
|
||||
return (
|
||||
// 提高class的css 权重
|
||||
<div
|
||||
className={classNames(
|
||||
styles['readonly-icon-container'],
|
||||
styles['more-level'],
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{expandIcon}
|
||||
<div className={styles['readonly-container']} onClick={onExpand}>
|
||||
<span className={styles.name}>{value.name || '-'}</span>
|
||||
<div className={styles.tag}>
|
||||
<span className={styles.label}>
|
||||
{PARAM_TYPE_ALIAS_MAP[value.type] || '-'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NodeContext.Provider value={{ field: data.field }}>
|
||||
<div
|
||||
className={classNames({
|
||||
[styles.container]: true,
|
||||
[className]: Boolean(className),
|
||||
})}
|
||||
ref={treeNodeRef}
|
||||
>
|
||||
{/* 每增加一级多15长度 */}
|
||||
<div
|
||||
style={{ width: IndentationWidth }}
|
||||
className={styles['level-icon']}
|
||||
>
|
||||
<LevelLine
|
||||
level={level}
|
||||
data={value}
|
||||
multiInfo={{
|
||||
multiline: activeMultiInfo?.activeMultiKey === value.field,
|
||||
withNameError: activeMultiInfo?.withNameError,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.wrapper}>
|
||||
<ParamName
|
||||
style={{ width: paramNameWidth - IndentationWidth }}
|
||||
data={value}
|
||||
onChange={onNameChange}
|
||||
/>
|
||||
<ParamType
|
||||
data={value}
|
||||
onSelectChange={onSelectChange}
|
||||
level={level}
|
||||
disabledTypes={disabledTypes}
|
||||
/>
|
||||
{/* LLM 节点输出才有 description */}
|
||||
{withDescription ? (
|
||||
<ParamDescription
|
||||
data={value}
|
||||
onChange={onDescriptionChange}
|
||||
onLineChange={onDescriptionLineChange}
|
||||
hasObjectLike={hasObjectLike}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<ParamOperator
|
||||
data={value}
|
||||
level={level}
|
||||
onDelete={onDelete}
|
||||
onAppend={onAppend}
|
||||
disableDelete={disableDelete}
|
||||
hasObjectLike={hasObjectLike}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</NodeContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -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 { type TreeNodeData } from '@coze-arch/bot-semi/Tree';
|
||||
|
||||
import { type RecursedParamDefinition } from '../../types';
|
||||
import { type ChangeMode } from './constants';
|
||||
|
||||
export type TreeNodeCustomData = TreeNodeData &
|
||||
Pick<
|
||||
RecursedParamDefinition,
|
||||
| 'name'
|
||||
| 'type'
|
||||
| 'isQuote'
|
||||
| 'fixedValue'
|
||||
| 'quotedValue'
|
||||
| 'fieldRandomKey'
|
||||
> & {
|
||||
// 行唯一值
|
||||
key: string;
|
||||
// Form的field
|
||||
field?: string;
|
||||
// 是否是第一项
|
||||
isFirst?: boolean;
|
||||
// 是否是最后一项
|
||||
isLast?: boolean;
|
||||
// 是否只有该项一条数据
|
||||
isSingle?: boolean;
|
||||
// 该项的嵌套层级,从0开始
|
||||
level?: number;
|
||||
// 辅助线展示的字段
|
||||
helpLineShow?: Array<boolean>;
|
||||
children?: Array<TreeNodeCustomData>;
|
||||
// 变量描述,用于作为隐藏的引导
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export interface CustomTreeNodeFuncRef {
|
||||
data: TreeNodeCustomData;
|
||||
level: number;
|
||||
readonly: boolean;
|
||||
// 通用change方法
|
||||
onChange: (mode: ChangeMode, param: TreeNodeCustomData) => void;
|
||||
// 定制的类型改变的change方法,主要用于自定义render使用
|
||||
// 添加子项
|
||||
onAppend: () => void;
|
||||
// 删除该项
|
||||
onDelete: () => void;
|
||||
// 删除该项下面的所有子项
|
||||
onDeleteChildren: () => void;
|
||||
// 类型改变时内部的调用方法,主要用于从类Object类型转为其他类型时需要删除所有子项
|
||||
onSelectChange: (
|
||||
val?: string | number | Array<unknown> | Record<string, unknown>,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface ActiveMultiInfo {
|
||||
// 当前行是否处于多行状态,多行状态竖线需要延长
|
||||
activeMultiKey: string;
|
||||
// 当前行paramName数据是否出现错误信息
|
||||
withNameError?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.type {
|
||||
margin-left: 8px;
|
||||
width: 155px;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-left: 8px;
|
||||
width: 312px;
|
||||
}
|
||||
|
||||
&.withDescription {
|
||||
.name {
|
||||
flex: auto;
|
||||
width: 181px;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
color: var(--light-usage-text-color-text-3, rgb(28 31 35 / 35%));
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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 cx from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import useConfig from '../../hooks/use-config';
|
||||
import {
|
||||
OperatorLargeSize,
|
||||
OperatorSmallSize,
|
||||
SpacingSize,
|
||||
OperatorTypeBaseWidth,
|
||||
} from '../../constants';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export default function Header() {
|
||||
const { readonly, withDescription, hasObjectLike } = useConfig();
|
||||
|
||||
if (readonly) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(styles.header, {
|
||||
[styles.withDescription]: withDescription,
|
||||
})}
|
||||
>
|
||||
{/* name */}
|
||||
<div className={styles.name}>
|
||||
<span className={styles.text}>
|
||||
{I18n.t('workflow_detail_end_output_name')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* type */}
|
||||
<div
|
||||
className={styles.type}
|
||||
style={
|
||||
withDescription
|
||||
? {
|
||||
width: OperatorTypeBaseWidth,
|
||||
}
|
||||
: !hasObjectLike
|
||||
? { width: OperatorSmallSize + SpacingSize + OperatorTypeBaseWidth }
|
||||
: { width: OperatorLargeSize + SpacingSize + OperatorTypeBaseWidth }
|
||||
}
|
||||
>
|
||||
<span className={styles.text}>
|
||||
{I18n.t('workflow_detail_start_variable_type')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* description 目前只在 LLM 的 output 中存在 */}
|
||||
{withDescription ? (
|
||||
<div className={styles.description}>
|
||||
<span className={styles.text}>
|
||||
{I18n.t('workflow_detail_llm_output_decription_title')}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
// 类型选择控件基础宽度
|
||||
export const OperatorTypeBaseWidth = 155;
|
||||
|
||||
// 61 = 删除按钮 + 添加按钮 的上层容器宽度
|
||||
export const OperatorLargeSize = 61;
|
||||
// 31 = 删除按钮 的上层容器宽度
|
||||
export const OperatorSmallSize = 31;
|
||||
// 8 = 删除按钮与变量类型中间的 margin 距离
|
||||
export const SpacingSize = 8;
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { createContext } from 'react';
|
||||
|
||||
import type { ParametersProps } from '../types';
|
||||
|
||||
export type Configs = Omit<
|
||||
ParametersProps,
|
||||
'value' | 'onChange' | 'className' | 'style' | 'disabledTypes'
|
||||
> & { hasObjectLike?: boolean };
|
||||
|
||||
const ConfigContext = createContext<Configs>({});
|
||||
|
||||
export default ConfigContext;
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { createContext } from 'react';
|
||||
|
||||
export interface Node {
|
||||
field?: string;
|
||||
}
|
||||
|
||||
const NodeContext = createContext<Node>({});
|
||||
|
||||
export default NodeContext;
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import { useContext } from 'react';
|
||||
|
||||
import ConfigContext from '../context/config-context';
|
||||
import type { Configs } from '../context/config-context';
|
||||
|
||||
export default function useConfig(): Configs {
|
||||
const config = useContext(ConfigContext);
|
||||
return config;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import resolvePath from '../utils/resolve-path';
|
||||
import useNode from './use-node';
|
||||
import useParametersConfig from './use-config';
|
||||
|
||||
export default function useErrorMessage(key: string): string {
|
||||
const { errors = [] } = useParametersConfig();
|
||||
const { field = '' } = useNode();
|
||||
const pathSearched = resolvePath(field, key);
|
||||
|
||||
const error = errors.find(({ path }) => pathSearched === path);
|
||||
|
||||
return error?.message || '';
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import { useContext } from 'react';
|
||||
|
||||
import NodeContext, { type Node } from '../context/node-context';
|
||||
|
||||
export default function useNode(): Node {
|
||||
const node = useContext(NodeContext);
|
||||
return node;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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 { Parameters } from './parameters';
|
||||
export { ParamTypeAlias } from './types';
|
||||
export type { ParameterValue, ParametersError, ParametersProps } from './types';
|
||||
@@ -0,0 +1,46 @@
|
||||
.container {
|
||||
position: relative;
|
||||
|
||||
.content {
|
||||
overflow-x: auto;
|
||||
|
||||
&.readonly {
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.add-hot-area {
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-tree-option-list {
|
||||
overflow: initial;
|
||||
|
||||
&>div:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-tree-option-list {
|
||||
overflow: inherit;
|
||||
|
||||
.semi-tree-option {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-tree-option-list-block .semi-tree-option {
|
||||
cursor: default;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* 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 @coze-arch/max-line-per-function */
|
||||
import React, { type PropsWithChildren, useState } from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type TreeNodeData } from '@coze-arch/bot-semi/Tree';
|
||||
import { Toast, Tree } from '@coze-arch/bot-semi';
|
||||
|
||||
import { findCustomTreeNodeDataResult, formatTreeData } from './utils/utils';
|
||||
import { traverse } from './utils/traverse';
|
||||
import { ParamTypeAlias } from './types';
|
||||
import type { ParametersProps } from './types';
|
||||
import ConfigContext from './context/config-context';
|
||||
import Header from './components/header';
|
||||
import {
|
||||
type TreeNodeCustomData,
|
||||
type ActiveMultiInfo,
|
||||
} from './components/custom-tree-node/type';
|
||||
import { ChangeMode } from './components/custom-tree-node/constants';
|
||||
import CustomTreeNode from './components/custom-tree-node';
|
||||
|
||||
import styles from './parameters.module.less';
|
||||
|
||||
const getDefaultAppendValue = () => ({
|
||||
fieldRandomKey: nanoid(),
|
||||
type: ParamTypeAlias.String,
|
||||
});
|
||||
|
||||
export function Parameters(props: PropsWithChildren<ParametersProps>) {
|
||||
const {
|
||||
value,
|
||||
readonly = false,
|
||||
withDescription = false,
|
||||
disabledTypes = [],
|
||||
className = '',
|
||||
style = {},
|
||||
errors = [],
|
||||
allowValueEmpty = true,
|
||||
onChange,
|
||||
} = props;
|
||||
// 监听该值的变化
|
||||
const isValueEmpty = !value || value.length === 0;
|
||||
const { data: formattedTreeData, hasObjectLike } = formatTreeData(
|
||||
cloneDeep(value) as TreeNodeCustomData[],
|
||||
);
|
||||
|
||||
/**
|
||||
* 表示当前哪一行的父亲节点的 description 处于多行状态(LLM节点)
|
||||
* 用于渲染树形竖线,处于多行文本的下一行竖线应该延长
|
||||
* 若 param name 有错误信息,竖线从错误信息下方延展,长度有所变化
|
||||
*/
|
||||
const [activeMultiInfo, setActiveMultiInfo] = useState<ActiveMultiInfo>({
|
||||
activeMultiKey: '',
|
||||
});
|
||||
|
||||
// 该组件的 change 方法
|
||||
const onValueChange = (freshValue?: Array<TreeNodeCustomData>) => {
|
||||
if (onChange) {
|
||||
freshValue = (freshValue || []).concat([]);
|
||||
// 清理掉无用字段
|
||||
traverse<TreeNodeCustomData>(freshValue, node => {
|
||||
const { key, name, type, description, children } = node;
|
||||
// eslint-disable-next-line guard-for-in
|
||||
for (const prop in node) {
|
||||
delete node[prop];
|
||||
}
|
||||
node.key = key;
|
||||
node.name = name;
|
||||
node.type = type;
|
||||
node.description = description;
|
||||
|
||||
if (children) {
|
||||
node.children = children;
|
||||
}
|
||||
});
|
||||
onChange(freshValue);
|
||||
}
|
||||
};
|
||||
|
||||
// 树节点的 change 方法
|
||||
const onTreeNodeChange = (mode: ChangeMode, param: TreeNodeCustomData) => {
|
||||
// 先clone一份,因为Tree内部会对treeData执行isEqual,克隆一份一定是false
|
||||
const cloneDeepTreeData = cloneDeep(
|
||||
formattedTreeData,
|
||||
) as Array<TreeNodeCustomData>;
|
||||
const findResult = findCustomTreeNodeDataResult(
|
||||
cloneDeepTreeData,
|
||||
param.field as string,
|
||||
);
|
||||
if (findResult) {
|
||||
switch (mode) {
|
||||
case ChangeMode.Append: {
|
||||
// 新增不可以用 parentData 做标准,要在当前 data 下新增
|
||||
const { data } = findResult;
|
||||
const currentChildren = data.children || [];
|
||||
// @ts-expect-error 有些值不需要此时指定,因为在 rerender 的时候会执行 format
|
||||
data.children = currentChildren.concat({
|
||||
...getDefaultAppendValue(),
|
||||
// 增加 field
|
||||
field: `${data.field}.children[${currentChildren.length}]`,
|
||||
});
|
||||
onValueChange(cloneDeepTreeData);
|
||||
break;
|
||||
}
|
||||
case ChangeMode.Update: {
|
||||
const targetArray = findResult.isRoot
|
||||
? cloneDeepTreeData
|
||||
: findResult.parentData?.children;
|
||||
const index = targetArray?.findIndex(item => item.key === param.key);
|
||||
|
||||
if (index !== undefined) {
|
||||
targetArray?.splice(index, 1, param);
|
||||
onValueChange(cloneDeepTreeData);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ChangeMode.Delete: {
|
||||
if (findResult.isRoot) {
|
||||
const freshValue = (cloneDeepTreeData || []).filter(
|
||||
item => item.key !== param.key,
|
||||
);
|
||||
onValueChange(freshValue);
|
||||
} else {
|
||||
const parentData = findResult.parentData as TreeNodeData;
|
||||
parentData.children = (parentData.children || []).filter(
|
||||
item => item.key !== param.key,
|
||||
);
|
||||
onValueChange(cloneDeepTreeData);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ChangeMode.DeleteChildren: {
|
||||
const { data } = findResult;
|
||||
data.children = [];
|
||||
onValueChange(cloneDeepTreeData);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
}
|
||||
} else {
|
||||
Toast.error(I18n.t('workflow_detail_node_output_parsingfailed'));
|
||||
}
|
||||
};
|
||||
|
||||
if (readonly && isValueEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfigContext.Provider
|
||||
value={{
|
||||
errors,
|
||||
allowValueEmpty,
|
||||
withDescription,
|
||||
readonly,
|
||||
hasObjectLike,
|
||||
}}
|
||||
>
|
||||
<div className={`${styles.container} ${className}`} style={style}>
|
||||
<Header />
|
||||
<Tree
|
||||
expandAll={!readonly}
|
||||
style={readonly ? {} : { overflow: 'inherit' }}
|
||||
motion={false}
|
||||
className={classNames({
|
||||
[styles.content]: true,
|
||||
[styles.readonly]: readonly,
|
||||
[styles['content-fix-pop-container']]: !readonly,
|
||||
})}
|
||||
renderFullLabel={renderFullLabelProps => (
|
||||
<CustomTreeNode
|
||||
{...renderFullLabelProps}
|
||||
onChange={onTreeNodeChange}
|
||||
onActiveMultiInfoChange={setActiveMultiInfo}
|
||||
activeMultiInfo={activeMultiInfo}
|
||||
disabledTypes={disabledTypes}
|
||||
/>
|
||||
)}
|
||||
treeData={formattedTreeData}
|
||||
/>
|
||||
</div>
|
||||
</ConfigContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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 ParamTypeAlias {
|
||||
String = 1,
|
||||
Integer,
|
||||
Boolean,
|
||||
Number,
|
||||
/** 理论上没有 List 了,此项仅作兼容 */
|
||||
List = 5,
|
||||
Object = 6,
|
||||
// 上面是 api 中定义的 InputType。下面是整合后的。从 99 开始,避免和后端定义撞车
|
||||
ArrayString = 99,
|
||||
ArrayInteger,
|
||||
ArrayBoolean,
|
||||
ArrayNumber,
|
||||
ArrayObject,
|
||||
}
|
||||
|
||||
export const PARAM_TYPE_ALIAS_MAP: Record<ParamTypeAlias, string> = {
|
||||
[ParamTypeAlias.String]: 'String',
|
||||
[ParamTypeAlias.Integer]: 'Integer',
|
||||
[ParamTypeAlias.Boolean]: 'Boolean',
|
||||
[ParamTypeAlias.Number]: 'Number',
|
||||
[ParamTypeAlias.List]: 'List',
|
||||
[ParamTypeAlias.Object]: 'Object',
|
||||
[ParamTypeAlias.ArrayString]: 'Array<String>',
|
||||
[ParamTypeAlias.ArrayInteger]: 'Array<Integer>',
|
||||
[ParamTypeAlias.ArrayBoolean]: 'Array<Boolean>',
|
||||
[ParamTypeAlias.ArrayNumber]: 'Array<Number>',
|
||||
[ParamTypeAlias.ArrayObject]: 'Array<Object>',
|
||||
};
|
||||
|
||||
export enum ParamValueType {
|
||||
QUOTE = 'quote',
|
||||
FIXED = 'fixed',
|
||||
}
|
||||
|
||||
export interface RecursedParamDefinition {
|
||||
name?: string;
|
||||
/** Tree 组件要求每一个节点都有 key,而 key 不适合用名称(前后缀)等任何方式赋值,最终确定由接口转换层一次性提供随机 key */
|
||||
fieldRandomKey?: string;
|
||||
desc?: string;
|
||||
required?: boolean;
|
||||
type: ParamTypeAlias;
|
||||
children?: RecursedParamDefinition[];
|
||||
// region 参数值定义
|
||||
// 输入参数的值可以来自上游变量引用,也可以是用户输入的定值(复杂类型则只允许引用)
|
||||
// 如果是定值,传 fixedValue
|
||||
// 如果是引用,传 quotedValue
|
||||
isQuote?: ParamValueType;
|
||||
/** 参数定值 */
|
||||
fixedValue?: string;
|
||||
/** 参数引用 */
|
||||
quotedValue?: [nodeId: string, ...path: string[]]; // string[]
|
||||
// endregion
|
||||
}
|
||||
|
||||
export interface ParameterValue {
|
||||
key: string;
|
||||
name?: string;
|
||||
type: ParamTypeAlias;
|
||||
description?: string;
|
||||
children?: ParameterValue[];
|
||||
}
|
||||
|
||||
export interface ParametersError {
|
||||
path: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ParametersProps {
|
||||
value: Array<ParameterValue>;
|
||||
onChange?: (value: Array<ParameterValue>) => void;
|
||||
readonly?: boolean;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
withDescription?: boolean;
|
||||
// 不支持使用的类型
|
||||
disabledTypes?: ParamTypeAlias[];
|
||||
errors?: ParametersError[];
|
||||
// 支持空值 & 空数组
|
||||
allowValueEmpty?: boolean;
|
||||
}
|
||||
@@ -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 { isFunction } from 'lodash-es';
|
||||
|
||||
/**
|
||||
* 将 { value: label } 形式的结构体转成Select需要的options Array<{ label, value }>
|
||||
* computedValue:将value值转化一次作为options的value
|
||||
* passItem:判断当前value值是否需要跳过遍历
|
||||
*/
|
||||
export default function convertMaptoOptions<Value = number>(
|
||||
map: Record<string, unknown>,
|
||||
convertOptions: {
|
||||
computedValue?: (val: unknown) => Value;
|
||||
passItem?: (val: unknown) => boolean;
|
||||
/**
|
||||
* 由于 i18n 的实现方式问题,写成常量的文案需要惰性加载
|
||||
* 因此涉及到 i18n 的 { value: label } 结构一律需要写成 { value: () => label }
|
||||
* 该属性启用时,会额外进行一次惰性加载
|
||||
* @default false
|
||||
* @link
|
||||
*/
|
||||
i18n?: boolean;
|
||||
} = {},
|
||||
) {
|
||||
const res: Array<{ label: string; value: Value }> = [];
|
||||
for (const [value, label] of Object.entries(map)) {
|
||||
const pass = convertOptions.passItem
|
||||
? convertOptions.passItem(value)
|
||||
: false;
|
||||
if (pass) {
|
||||
continue;
|
||||
}
|
||||
const computedValue = convertOptions.computedValue
|
||||
? convertOptions.computedValue(value)
|
||||
: (value as Value);
|
||||
|
||||
const finalLabel: string = convertOptions.i18n
|
||||
? isFunction(label)
|
||||
? label()
|
||||
: label
|
||||
: label;
|
||||
res.push({ label: finalLabel, value: computedValue });
|
||||
}
|
||||
return res;
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
export default function resolvePath(path1: string, path2: string): string {
|
||||
if (path1 && path2) {
|
||||
return `${path1}.${path2}`;
|
||||
}
|
||||
|
||||
return path2 || '';
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
interface TreeNode<T> {
|
||||
children?: T[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export function traverse<T extends TreeNode<T>>(
|
||||
nodeOrNodes: T | T[],
|
||||
action: (node: T) => void,
|
||||
) {
|
||||
const nodes = Array.isArray(nodeOrNodes) ? nodeOrNodes : [nodeOrNodes];
|
||||
|
||||
nodes.forEach(node => {
|
||||
action(node);
|
||||
|
||||
if (node.children && node.children.length > 0) {
|
||||
traverse(node.children, action);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* 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 { nanoid } from 'nanoid';
|
||||
|
||||
import { type TreeNodeCustomData } from '../components/custom-tree-node/type';
|
||||
import { ObjectLikeTypes } from '../components/custom-tree-node/constants';
|
||||
|
||||
interface RootFindResult {
|
||||
isRoot: true;
|
||||
data: TreeNodeCustomData;
|
||||
parentData: null;
|
||||
}
|
||||
interface ChildrenFindResult {
|
||||
isRoot: false;
|
||||
parentData: TreeNodeCustomData;
|
||||
data: TreeNodeCustomData;
|
||||
}
|
||||
|
||||
export type FindDataResult = RootFindResult | ChildrenFindResult | null;
|
||||
/**
|
||||
* 根据target数组,找到key在该项的值和位置,主要是获取位置,方便操作parent的children
|
||||
*/
|
||||
export function findCustomTreeNodeDataResult(
|
||||
target: Array<TreeNodeCustomData>,
|
||||
findField: string,
|
||||
): FindDataResult {
|
||||
const dataInRoot = target.find(item => item.field === findField);
|
||||
if (dataInRoot) {
|
||||
// 如果是根节点
|
||||
return {
|
||||
isRoot: true,
|
||||
parentData: null,
|
||||
data: dataInRoot,
|
||||
};
|
||||
}
|
||||
function findDataInChildrenLoop(
|
||||
customChildren: Array<TreeNodeCustomData>,
|
||||
parentData?: TreeNodeCustomData,
|
||||
): FindDataResult {
|
||||
function findDataLoop(
|
||||
customData: TreeNodeCustomData,
|
||||
_parentData: TreeNodeCustomData,
|
||||
): FindDataResult {
|
||||
if (customData.field === findField) {
|
||||
return {
|
||||
isRoot: false,
|
||||
parentData: _parentData,
|
||||
data: customData,
|
||||
};
|
||||
}
|
||||
if (customData.children && customData.children.length > 0) {
|
||||
return findDataInChildrenLoop(
|
||||
customData.children as Array<TreeNodeCustomData>,
|
||||
customData,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
for (const child of customChildren) {
|
||||
const childResult = findDataLoop(child, parentData || child);
|
||||
if (childResult) {
|
||||
return childResult;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return findDataInChildrenLoop(target);
|
||||
}
|
||||
|
||||
const MAX_LINE_LEVEL = 2;
|
||||
export function formatTreeData(data: Array<TreeNodeCustomData>) {
|
||||
let hasObjectLike = false;
|
||||
function resolveActionParamList(
|
||||
list: Array<TreeNodeCustomData>,
|
||||
field: string,
|
||||
// 主要是用来辅助展示线的判断的
|
||||
{
|
||||
parentData,
|
||||
level,
|
||||
}: {
|
||||
parentData?: TreeNodeCustomData;
|
||||
level: number;
|
||||
},
|
||||
) {
|
||||
list?.forEach((item, index) => {
|
||||
const keyField = field ? `${field}.${index}` : `${index}`;
|
||||
hasObjectLike = hasObjectLike || ObjectLikeTypes.includes(item.type);
|
||||
// 赋值children
|
||||
item.key = item.key ?? item.fieldRandomKey ?? nanoid();
|
||||
item.field = keyField;
|
||||
item.isFirst = index === 0;
|
||||
item.isLast = index === list.length - 1;
|
||||
item.isSingle = item.isFirst && item.isLast;
|
||||
item.level = level;
|
||||
// 第一级不展示辅助线,需要判断level
|
||||
// 也就是第二级(level = 1)只需要自身的层级线
|
||||
// 在第三级(level = 2)之后需要辅助线展示上一级的辅助线
|
||||
item.helpLineShow =
|
||||
parentData && level >= MAX_LINE_LEVEL
|
||||
? (parentData.helpLineShow || []).concat(!parentData.isLast)
|
||||
: [];
|
||||
if (item.children) {
|
||||
resolveActionParamList(
|
||||
item.children as Array<TreeNodeCustomData>,
|
||||
`${keyField}.children`,
|
||||
{
|
||||
parentData: item,
|
||||
level: level + 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resolveActionParamList(data as TreeNodeCustomData[], '', { level: 0 });
|
||||
|
||||
return { data, hasObjectLike };
|
||||
}
|
||||
|
||||
export enum LineShowResult {
|
||||
HalfTopRoot,
|
||||
HalfTopRootWithChildren,
|
||||
HalfBottomRoot,
|
||||
HalfBottomRootWithChildren,
|
||||
FullRoot,
|
||||
FullRootWithChildren,
|
||||
HalfTopChild,
|
||||
HalfTopChildWithChildren,
|
||||
FullChild,
|
||||
FullChildWithChildren,
|
||||
EmptyBlock,
|
||||
HelpLineBlock,
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
export function getLineShowResult({
|
||||
level,
|
||||
data,
|
||||
}: {
|
||||
level: number;
|
||||
data: TreeNodeCustomData;
|
||||
}): Array<LineShowResult> {
|
||||
const isRootWithChildren = level === 0 && (data.children || []).length > 0;
|
||||
const isRootWithoutChildren =
|
||||
level === 0 && (data.children || []).length === 0;
|
||||
const isChildWithChildren = level > 0 && (data.children || []).length > 0;
|
||||
const isChildWithoutChildren =
|
||||
level > 0 && (data.children || []).length === 0;
|
||||
const res: Array<LineShowResult> =
|
||||
data.helpLineShow?.map(item =>
|
||||
item ? LineShowResult.HelpLineBlock : LineShowResult.EmptyBlock,
|
||||
) || [];
|
||||
const isRoot = isRootWithoutChildren || isRootWithChildren;
|
||||
// 根节点不需要展示线,只有非根节点才需要辅助线
|
||||
if (!isRoot) {
|
||||
if (isChildWithChildren) {
|
||||
if (data.isLast) {
|
||||
res.push(LineShowResult.HalfTopChildWithChildren);
|
||||
} else if (data.isFirst) {
|
||||
res.push(LineShowResult.FullChildWithChildren);
|
||||
} else {
|
||||
res.push(LineShowResult.FullChildWithChildren);
|
||||
}
|
||||
} else if (isChildWithoutChildren) {
|
||||
if (data.isLast) {
|
||||
res.push(LineShowResult.HalfTopChild);
|
||||
} else if (data.isFirst) {
|
||||
res.push(LineShowResult.FullChild);
|
||||
} else {
|
||||
res.push(LineShowResult.FullChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 569 B |
@@ -0,0 +1,176 @@
|
||||
.generate-list-wrap {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
|
||||
box-sizing: border-box;
|
||||
height: 32px;
|
||||
|
||||
border-radius: 8px;
|
||||
|
||||
|
||||
.hidden-element {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-image: url('../assets/bot-generate-loading-sprite.png'), url('../assets/bot-generate-dis-sprite.png')
|
||||
}
|
||||
|
||||
.split-line{
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
margin: 0 12px;
|
||||
background-color: var(--coz-stroke-plus);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
position: relative;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
|
||||
&.checked::after {
|
||||
content: '';
|
||||
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
background: url('../assets/list-checked-bold.png') no-repeat center center/cover;
|
||||
}
|
||||
|
||||
.loading-mask {
|
||||
cursor: pointer;
|
||||
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background-color: transparent;
|
||||
border-radius: 8px;
|
||||
|
||||
&.loading {
|
||||
background-image: url('../assets/bot-generate-loading-sprite.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: -2px;
|
||||
background-size: 2190px 73px;
|
||||
|
||||
animation: loading 1.5s steps(30) infinite;
|
||||
}
|
||||
|
||||
&.finish {
|
||||
background-image: url('../assets/bot-generate-dis-sprite.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: -2px;
|
||||
background-size: 730px 73px;
|
||||
|
||||
animation: finish .5s steps(10) forwards;
|
||||
}
|
||||
|
||||
.mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
color: #FFF;
|
||||
|
||||
background-color: var(--coz-mg-mask);
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
filter: brightness(1.5) blur(6px);
|
||||
mix-blend-mode: hard-light;
|
||||
animation: fade-in .8s .2s forwards;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.generate-btn {
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
|
||||
font-size: 12px;
|
||||
color: #4D53E8;
|
||||
|
||||
background-color: var(--coz-mg-primary);
|
||||
border: 1px solid var(--coz-stroke-primary);
|
||||
border-radius: 8px;
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
color: #B4BAF6;
|
||||
|
||||
svg {
|
||||
opacity: .4;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
from {
|
||||
background-position: -2px -2px;
|
||||
}
|
||||
|
||||
to {
|
||||
background-position: -2192px -2px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes finish {
|
||||
from {
|
||||
background-position: -2px -2px;
|
||||
}
|
||||
|
||||
to {
|
||||
background-position: -730px -2px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
filter: brightness(1.5) blur(6px);
|
||||
mix-blend-mode: hard-light;
|
||||
}
|
||||
|
||||
30% {
|
||||
mix-blend-mode: unset;
|
||||
}
|
||||
|
||||
100% {
|
||||
filter: brightness(1) blur(0);
|
||||
mix-blend-mode: unset;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { isFunction } from 'lodash-es';
|
||||
import classNames from 'classnames';
|
||||
import axios, { type CancelTokenSource } from 'axios';
|
||||
import { useHover } from 'ahooks';
|
||||
import {
|
||||
REPORT_EVENTS as ReportEventNames,
|
||||
createReportEvent,
|
||||
} from '@coze-arch/report-events';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
IconCozCheckMark,
|
||||
IconCozCrossCircle,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
Tooltip,
|
||||
Toast,
|
||||
Image,
|
||||
AIButton,
|
||||
Space,
|
||||
} from '@coze-arch/coze-design';
|
||||
import { loadImage } from '@coze-arch/bot-utils';
|
||||
import { DeveloperApi } from '@coze-arch/bot-api';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
type UploadValue = { uid: string; url: string }[];
|
||||
interface GenerateInfo {
|
||||
name: string;
|
||||
desc?: string;
|
||||
}
|
||||
interface AutoGenerateProps {
|
||||
onChange: (value?: UploadValue) => void;
|
||||
generateInfo?: GenerateInfo | (() => GenerateInfo);
|
||||
generateTooltip?: {
|
||||
generateBtnText?: string;
|
||||
contentNotLegalText?: string;
|
||||
};
|
||||
showAiAvatar: boolean;
|
||||
/**
|
||||
* 最多允许多少个候选
|
||||
* @default 5
|
||||
*/
|
||||
maxCandidateCount?: number;
|
||||
}
|
||||
|
||||
interface PictureItem {
|
||||
url: string;
|
||||
uid: string;
|
||||
}
|
||||
|
||||
// 自动生成头像错误码
|
||||
enum ErrorCode {
|
||||
OVER_QUOTA_PER_DAY = 700012034,
|
||||
CONTENT_NOT_LEGAL = 700012050,
|
||||
}
|
||||
|
||||
const MAX_CANDIDATE_COUNT = 5;
|
||||
const MAX_TOTAL_COUNT = 25;
|
||||
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function
|
||||
export const AutoGenerate = (props: AutoGenerateProps) => {
|
||||
const {
|
||||
onChange,
|
||||
generateInfo,
|
||||
showAiAvatar,
|
||||
generateTooltip,
|
||||
maxCandidateCount = MAX_CANDIDATE_COUNT,
|
||||
} = props;
|
||||
const cancelTokenSource = useRef<CancelTokenSource>();
|
||||
const hoverCount = useRef(0);
|
||||
const loadingRef = useRef<HTMLDivElement>(null);
|
||||
const loadingHover = useHover(loadingRef);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [showGenerateBtn, setShowGenerateBtn] = useState(true);
|
||||
const [pictureList, setPictureList] = useState<PictureItem[]>([]);
|
||||
const [checkedId, setCheckedId] = useState(-1);
|
||||
|
||||
const tooltipContent = useMemo(() => {
|
||||
if (totalCount >= MAX_TOTAL_COUNT && pictureList.length === 0) {
|
||||
return I18n.t('bot_edit_profile_pircture_autogen_quota_tooltip');
|
||||
}
|
||||
|
||||
const defaultText = IS_OVERSEA
|
||||
? I18n.t('bot_edit_profile_pircture_autogen_tooltip')
|
||||
: I18n.t('bot_edit_profile_pircture_autogen_tooltip_cn');
|
||||
return generateTooltip?.generateBtnText || defaultText;
|
||||
}, [totalCount, pictureList.length, generateTooltip?.generateBtnText]);
|
||||
|
||||
const allowGenerate = useMemo(
|
||||
() =>
|
||||
totalCount < MAX_TOTAL_COUNT &&
|
||||
(isFunction(generateInfo) ? generateInfo?.().name : generateInfo?.name),
|
||||
[generateInfo, totalCount],
|
||||
);
|
||||
|
||||
const cancelGenerate = () => {
|
||||
cancelTokenSource.current?.cancel('cancel generate picture');
|
||||
};
|
||||
|
||||
const updateParentValue = (id: number, value: UploadValue) => {
|
||||
setCheckedId(id);
|
||||
onChange(value);
|
||||
};
|
||||
|
||||
const getPicture = async () => {
|
||||
hoverCount.current = 1;
|
||||
setLoading(true);
|
||||
|
||||
const newList = [
|
||||
...pictureList,
|
||||
{
|
||||
url: '',
|
||||
uid: '',
|
||||
},
|
||||
];
|
||||
setPictureList(newList);
|
||||
const reportEvent = createReportEvent({
|
||||
eventName: ReportEventNames.botGetAiGenerateAvatar,
|
||||
});
|
||||
try {
|
||||
cancelTokenSource.current = axios.CancelToken.source();
|
||||
const { name, desc } = isFunction(generateInfo)
|
||||
? generateInfo()
|
||||
: generateInfo || {};
|
||||
const { data } = await DeveloperApi.GenerateIcon(
|
||||
{
|
||||
bot_name: name,
|
||||
description: desc,
|
||||
},
|
||||
{
|
||||
__disableErrorToast: true,
|
||||
cancelToken: cancelTokenSource.current.token,
|
||||
},
|
||||
);
|
||||
setTotalCount(Number(data?.count));
|
||||
await loadImage(String(data?.icon_url));
|
||||
setLoading(false);
|
||||
setPictureList(prevList => {
|
||||
prevList[prevList.length - 1] = {
|
||||
url: String(data?.icon_url),
|
||||
uid: String(data?.icon_uri),
|
||||
};
|
||||
return prevList;
|
||||
});
|
||||
updateParentValue(newList.length - 1, [
|
||||
{
|
||||
url: String(data?.icon_url),
|
||||
uid: String(data?.icon_uri),
|
||||
},
|
||||
]);
|
||||
reportEvent.success();
|
||||
} catch (error) {
|
||||
onChange();
|
||||
setLoading(false);
|
||||
setPictureList(list => {
|
||||
list.pop();
|
||||
return list;
|
||||
});
|
||||
const codeNumber = Number((error as { code: number })?.code);
|
||||
if (codeNumber === ErrorCode.OVER_QUOTA_PER_DAY) {
|
||||
// 超过单日次数上限
|
||||
setTotalCount(MAX_TOTAL_COUNT);
|
||||
Toast.error({
|
||||
content: I18n.t('bot_edit_profile_pircture_autogen_quota_tooltip'),
|
||||
showClose: false,
|
||||
});
|
||||
reportEvent.error({
|
||||
reason: 'The number of times in a day exceeded the upper limit',
|
||||
error: error instanceof Error ? error : void 0,
|
||||
});
|
||||
} else if (codeNumber === ErrorCode.CONTENT_NOT_LEGAL) {
|
||||
Toast.error({
|
||||
content:
|
||||
generateTooltip?.contentNotLegalText ||
|
||||
I18n.t('generate_bot_icon_content_filter'),
|
||||
showClose: false,
|
||||
});
|
||||
reportEvent.error({
|
||||
reason:
|
||||
"The bot's name or description contains inappropriate content",
|
||||
error: error instanceof Error ? error : void 0,
|
||||
});
|
||||
} else if (codeNumber > 0) {
|
||||
Toast.error({
|
||||
content: I18n.t('error'),
|
||||
showClose: false,
|
||||
});
|
||||
reportEvent.error({
|
||||
reason: 'Failed to generate profile picture',
|
||||
error: error instanceof Error ? error : void 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// 获取当日总生成次数
|
||||
DeveloperApi.GetGenerateIconInfo()
|
||||
.then(({ data }) => {
|
||||
setTotalCount(Number(data?.current_day_count));
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
pictureList.length >= maxCandidateCount ||
|
||||
(totalCount >= MAX_TOTAL_COUNT && pictureList.length > 0)
|
||||
) {
|
||||
setShowGenerateBtn(false);
|
||||
} else {
|
||||
setShowGenerateBtn(true);
|
||||
}
|
||||
}, [pictureList.length, totalCount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loadingRef.current) {
|
||||
hoverCount.current++;
|
||||
}
|
||||
}, [loadingHover]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showAiAvatar) {
|
||||
setCheckedId(-1);
|
||||
if (loading) {
|
||||
cancelGenerate();
|
||||
}
|
||||
}
|
||||
}, [showAiAvatar]);
|
||||
|
||||
return (
|
||||
<div className={s['generate-list-wrap']}>
|
||||
<div className={s['hidden-element']} />
|
||||
<div className={s['split-line']} />
|
||||
<Space spacing={4}>
|
||||
{(pictureList || []).map((picture, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={classNames(s.avatar)}
|
||||
onClick={() => {
|
||||
if (picture.url) {
|
||||
updateParentValue(idx, [
|
||||
{ url: picture.url, uid: String(picture.uid) },
|
||||
]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={loadingRef}
|
||||
className={classNames(s['loading-mask'], {
|
||||
[s.loading]: !picture.url,
|
||||
[s.finish]: picture.url,
|
||||
[s['loading-hover']]: loadingHover && !picture.url,
|
||||
})}
|
||||
>
|
||||
{/* 二次hover展示取消 */}
|
||||
{hoverCount.current > 1 && loadingHover && !picture.url ? (
|
||||
<div
|
||||
className={s.mask}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
cancelGenerate();
|
||||
}}
|
||||
>
|
||||
<IconCozCrossCircle />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* 选中图片蒙版 */}
|
||||
{checkedId === idx && (
|
||||
<div className={s.mask}>
|
||||
<IconCozCheckMark className="text-[16]" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Image
|
||||
className={picture.url && s['avatar-img']}
|
||||
preview={false}
|
||||
src={picture.url}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{showGenerateBtn ? (
|
||||
<Tooltip position="topLeft" content={tooltipContent}>
|
||||
<AIButton
|
||||
onlyIcon
|
||||
color="aihglt"
|
||||
disabled={!allowGenerate}
|
||||
loading={loading}
|
||||
onClick={() => {
|
||||
if (allowGenerate) {
|
||||
getPicture();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
.upload {
|
||||
overflow: hidden;
|
||||
width: fit-content;
|
||||
height: 64px;
|
||||
margin: auto;
|
||||
|
||||
.circle {
|
||||
:global {
|
||||
.semi-upload-picture-file-card-uploading::before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 14px;
|
||||
|
||||
>img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-skeleton-image {
|
||||
background-color: #fff;
|
||||
border: 1px solid rgb(29 28 35 / 8%);
|
||||
border-radius: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-button-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.upload-button {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.avatar-wrap {
|
||||
position: relative;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
|
||||
.mask {
|
||||
cursor: pointer;
|
||||
|
||||
&.full-center {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
color: rgba(255, 255, 255, 0%);
|
||||
|
||||
visibility: hidden;
|
||||
background-color: rgba(22, 22, 26, 0%);
|
||||
border-radius: 14px;
|
||||
|
||||
transition: all 0.1s;
|
||||
}
|
||||
|
||||
&.right-bottom {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 4px 0 0 4px;
|
||||
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.mask {
|
||||
&.full-center {
|
||||
color: #fff;
|
||||
visibility: visible;
|
||||
background-color: var(--coz-mg-mask);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-with-auto-generate {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
.upload {
|
||||
height: 64px;
|
||||
margin: 0;
|
||||
|
||||
.avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-wrap {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
export {
|
||||
PictureUpload,
|
||||
type GenerateInfo,
|
||||
type UploadValue,
|
||||
} from './picture-upload';
|
||||
export { default as customUploadRequest } from './utils/custom-upload-request';
|
||||
export { type RenderAutoGenerateParams } from './picture-upload';
|
||||
@@ -0,0 +1,296 @@
|
||||
/*
|
||||
* 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 */
|
||||
/* eslint-disable @typescript-eslint/no-magic-numbers */
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { useMemo, useRef, useState, type FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useMount } from 'ahooks';
|
||||
import { CommonE2e } from '@coze-data/e2e';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozEdit } from '@coze-arch/coze-design/icons';
|
||||
import { type FileItem, type UploadProps } from '@coze-arch/bot-semi/Upload';
|
||||
import { type CommonFieldProps } from '@coze-arch/bot-semi/Form';
|
||||
import { UIButton, Toast, withField, Image, Upload } from '@coze-arch/bot-semi';
|
||||
import { IconAvatarEditMask } from '@coze-arch/bot-icons';
|
||||
import { type FileBizType, IconType } from '@coze-arch/bot-api/developer_api';
|
||||
import { DeveloperApi } from '@coze-arch/bot-api';
|
||||
|
||||
import customUploadRequest from './utils/custom-upload-request';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export type UploadValue = { uid: string | undefined; url: string }[];
|
||||
export interface GenerateInfo {
|
||||
name: string;
|
||||
desc?: string;
|
||||
}
|
||||
|
||||
export interface RenderAutoGenerateParams {
|
||||
uploadPicture: () => void;
|
||||
showAiAvatar: boolean;
|
||||
setShowAiAvatar: (show: boolean) => void;
|
||||
generateInfo?: GenerateInfo | (() => GenerateInfo);
|
||||
generateTooltip?: {
|
||||
generateBtnText?: string;
|
||||
contentNotLegalText?: string;
|
||||
};
|
||||
onChange?: (value: UploadValue) => void;
|
||||
maxCandidateCount?: number;
|
||||
}
|
||||
interface PackageUploadProps {
|
||||
value?: FileItem[];
|
||||
onChange?: (value: UploadValue) => void;
|
||||
fileBizType: FileBizType;
|
||||
uploadButtonText?: string;
|
||||
iconType?: IconType;
|
||||
disabled?: boolean;
|
||||
avatarClassName?: string;
|
||||
uploadClassName?: string;
|
||||
triggerClassName?: string;
|
||||
maskIcon?: React.ReactNode;
|
||||
/**
|
||||
* 编辑遮罩的展示模式
|
||||
* - full-center(默认): 整体覆盖黑色透明遮罩, Icon 居中展示. hover 展示
|
||||
* - right-bottom: 右下角遮罩, 长期展示
|
||||
*/
|
||||
maskMode?: 'full-center' | 'right-bottom';
|
||||
/** 编辑遮罩的 className */
|
||||
editMaskClassName?: string;
|
||||
/** max size */
|
||||
maxSize?: number;
|
||||
withAutoGenerate?: boolean;
|
||||
generateInfo?: GenerateInfo | (() => GenerateInfo);
|
||||
generateTooltip?: {
|
||||
generateBtnText?: string;
|
||||
contentNotLegalText?: string;
|
||||
};
|
||||
/**
|
||||
* 自动生成的最大候选数量
|
||||
* @default 5
|
||||
*/
|
||||
maxCandidateCount?: number;
|
||||
beforeUploadCustom?: () => void;
|
||||
afterUploadCustom?: () => void;
|
||||
accept?: string;
|
||||
onGenerateStaticImageClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
onGenerateGifClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
onSizeError?: () => void;
|
||||
// 自定义自定生成图片逻辑
|
||||
renderAutoGenerate?: (params: RenderAutoGenerateParams) => React.ReactNode;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function
|
||||
const _PictureUpload = (props: PackageUploadProps) => {
|
||||
// 业务
|
||||
const {
|
||||
onChange,
|
||||
value,
|
||||
fileBizType,
|
||||
uploadButtonText,
|
||||
iconType = IconType.Bot,
|
||||
disabled = false,
|
||||
avatarClassName,
|
||||
uploadClassName,
|
||||
triggerClassName,
|
||||
maskIcon,
|
||||
maskMode = 'full-center',
|
||||
editMaskClassName,
|
||||
withAutoGenerate = false,
|
||||
generateInfo,
|
||||
generateTooltip,
|
||||
beforeUploadCustom,
|
||||
afterUploadCustom,
|
||||
accept = 'image/*',
|
||||
maxCandidateCount,
|
||||
renderAutoGenerate,
|
||||
onSizeError,
|
||||
maxSize = 2 * 1024,
|
||||
testId,
|
||||
} = props;
|
||||
const uploadRef = useRef<Upload>(null);
|
||||
const pictureValue = value?.at(0);
|
||||
const [loadingIcon, setLoadingIcon] = useState(!pictureValue);
|
||||
const [showAiAvatar, setShowAiAvatar] = useState(withAutoGenerate);
|
||||
const maskIconInner = useMemo(() => {
|
||||
if (maskIcon) {
|
||||
return maskIcon;
|
||||
}
|
||||
|
||||
return (
|
||||
<IconCozEdit
|
||||
className={classNames(
|
||||
maskMode === 'right-bottom' ? 'text-[14px]' : 'text-[24px]',
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}, [maskIcon, maskMode]);
|
||||
|
||||
const getIcon = async () => {
|
||||
setLoadingIcon(true);
|
||||
try {
|
||||
const res = await DeveloperApi.GetIcon({
|
||||
icon_type: iconType,
|
||||
});
|
||||
const iconData = res.data?.icon_list?.[0];
|
||||
if (!iconData) {
|
||||
Toast.error({
|
||||
content: I18n.t('error'),
|
||||
showClose: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { url = '', uri = '' } = iconData;
|
||||
onChange?.([
|
||||
{
|
||||
url,
|
||||
uid: uri,
|
||||
},
|
||||
]);
|
||||
} catch (e) {
|
||||
Toast.error({
|
||||
content: I18n.t('error'),
|
||||
showClose: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useMount(() => {
|
||||
if (!pictureValue) {
|
||||
getIcon().then(() => setLoadingIcon(false));
|
||||
}
|
||||
});
|
||||
|
||||
const customRequest: UploadProps['customRequest'] = options => {
|
||||
customUploadRequest({
|
||||
...options,
|
||||
fileBizType,
|
||||
onSuccess: data => {
|
||||
if (withAutoGenerate) {
|
||||
setShowAiAvatar(false);
|
||||
}
|
||||
options.onSuccess(data);
|
||||
onChange?.([
|
||||
{
|
||||
uid: data?.upload_uri || '',
|
||||
url: data?.upload_url || '',
|
||||
},
|
||||
]);
|
||||
},
|
||||
beforeUploadCustom,
|
||||
afterUploadCustom,
|
||||
});
|
||||
};
|
||||
|
||||
const uploadPicture = () => {
|
||||
uploadRef.current?.openFileDialog();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={withAutoGenerate ? s['upload-with-auto-generate'] : ''}
|
||||
data-testid={CommonE2e.PictureUpload}
|
||||
>
|
||||
<Upload
|
||||
action=""
|
||||
className={classNames(s.upload, uploadClassName)}
|
||||
limit={1}
|
||||
customRequest={customRequest}
|
||||
fileList={value}
|
||||
accept={accept}
|
||||
showReplace={false}
|
||||
showUploadList={false}
|
||||
ref={uploadRef}
|
||||
disabled={disabled}
|
||||
maxSize={maxSize}
|
||||
onSizeError={() => {
|
||||
if (onSizeError) {
|
||||
onSizeError();
|
||||
return;
|
||||
}
|
||||
Toast.error({
|
||||
// starling 切换
|
||||
content: I18n.t(
|
||||
'dataset_upload_image_warning',
|
||||
{},
|
||||
'Please upload an image less than 2MB',
|
||||
),
|
||||
showClose: false,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
s['avatar-wrap'],
|
||||
'cursor-pointer',
|
||||
triggerClassName,
|
||||
)}
|
||||
data-testid={testId}
|
||||
>
|
||||
<Image
|
||||
preview={false}
|
||||
className={classNames(
|
||||
s.avatar,
|
||||
loadingIcon && s['avatar-loading'],
|
||||
avatarClassName,
|
||||
)}
|
||||
placeholder={
|
||||
<Image
|
||||
className={classNames(s.avatar, avatarClassName)}
|
||||
src={pictureValue?.url}
|
||||
preview={false}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<div className={classNames(s.mask, s[maskMode], editMaskClassName)}>
|
||||
{maskMode === 'right-bottom' && (
|
||||
<IconAvatarEditMask className="absolute inset-0 w-full h-full rounded-br-[14px] overflow-hidden" />
|
||||
)}
|
||||
<div className="relative inline-flex">{maskIconInner}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Upload>
|
||||
{uploadButtonText && !disabled ? (
|
||||
<div className={s['upload-button-wrap']}>
|
||||
<UIButton
|
||||
className={s['upload-button']}
|
||||
theme="borderless"
|
||||
type="primary"
|
||||
onClick={uploadPicture}
|
||||
>
|
||||
{uploadButtonText}
|
||||
</UIButton>
|
||||
</div>
|
||||
) : null}
|
||||
{withAutoGenerate && renderAutoGenerate
|
||||
? renderAutoGenerate({
|
||||
uploadPicture,
|
||||
showAiAvatar,
|
||||
setShowAiAvatar,
|
||||
generateInfo,
|
||||
generateTooltip,
|
||||
onChange,
|
||||
maxCandidateCount,
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const PictureUpload: FC<CommonFieldProps & PackageUploadProps> =
|
||||
withField(_PictureUpload);
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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 { REPORT_EVENTS as ReportEventNames } from '@coze-arch/report-events';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type customRequestArgs } from '@coze-arch/bot-semi/Upload';
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
import {
|
||||
type UploadFileData,
|
||||
type FileBizType,
|
||||
} from '@coze-arch/bot-api/developer_api';
|
||||
import { DeveloperApi } from '@coze-arch/bot-api';
|
||||
|
||||
import getBase64 from './get-base64';
|
||||
|
||||
function customUploadRequest(
|
||||
options: Omit<customRequestArgs, 'onSuccess'> & {
|
||||
fileBizType: FileBizType;
|
||||
onSuccess: (data?: UploadFileData) => void;
|
||||
beforeUploadCustom?: () => void;
|
||||
afterUploadCustom?: () => void;
|
||||
},
|
||||
): void {
|
||||
const {
|
||||
onSuccess,
|
||||
onError,
|
||||
file,
|
||||
beforeUploadCustom,
|
||||
afterUploadCustom,
|
||||
fileBizType,
|
||||
} = options;
|
||||
|
||||
if (typeof file === 'string') {
|
||||
return;
|
||||
}
|
||||
beforeUploadCustom?.();
|
||||
const getFileExtension = (name: string) => {
|
||||
const index = name.lastIndexOf('.');
|
||||
return name.slice(index + 1);
|
||||
};
|
||||
try {
|
||||
const { fileInstance } = file;
|
||||
|
||||
// 业务
|
||||
if (fileInstance) {
|
||||
const extension = getFileExtension(file.name);
|
||||
|
||||
// 业务
|
||||
(async () => {
|
||||
try {
|
||||
const base64 = await getBase64(fileInstance);
|
||||
const result = await DeveloperApi.UploadFile({
|
||||
file_head: {
|
||||
file_type: extension,
|
||||
biz_type: fileBizType,
|
||||
},
|
||||
data: base64,
|
||||
});
|
||||
onSuccess?.(result.data);
|
||||
afterUploadCustom?.();
|
||||
} catch (error) {
|
||||
// 如参数校验失败情况会走到catch
|
||||
afterUploadCustom?.();
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
afterUploadCustom?.();
|
||||
throw new CustomError(ReportEventNames.parmasValidation, I18n.t('error'));
|
||||
}
|
||||
} catch (e) {
|
||||
afterUploadCustom?.();
|
||||
onError?.({
|
||||
status: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default customUploadRequest;
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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 { REPORT_EVENTS as ReportEventNames } from '@coze-arch/report-events';
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
|
||||
function getBase64(file: Blob): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = event => {
|
||||
const result = event.target?.result;
|
||||
if (!result || typeof result !== 'string') {
|
||||
reject(
|
||||
new CustomError(ReportEventNames.parmasValidation, 'file read fail'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
resolve(result.replace(/^.*?,/, ''));
|
||||
};
|
||||
fileReader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
export default getBase64;
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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, { useRef, useState } from 'react';
|
||||
|
||||
import { type IntelligenceData } from '@coze-arch/idl/intelligence_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Modal, Search } from '@coze-arch/coze-design';
|
||||
|
||||
import { useIntelligenceSearch } from '../hooks/use-case/use-intelligence-search';
|
||||
import { IntelligenceList } from './intelligence-list';
|
||||
|
||||
export interface SelectIntelligenceModalProps {
|
||||
visible: boolean;
|
||||
spaceId: string;
|
||||
onSelect?: (intelligence: IntelligenceData) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const SelectIntelligenceModal: React.FC<
|
||||
SelectIntelligenceModalProps
|
||||
> = ({ visible, onCancel, onSelect, spaceId }) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
const { loading, data, loadingMore, noMore } = useIntelligenceSearch({
|
||||
spaceId,
|
||||
searchValue,
|
||||
containerRef,
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
onCancel={onCancel}
|
||||
width={640}
|
||||
height={588}
|
||||
className="[&_.semi-modal-header]:flex [&_.semi-modal-header]:items-center [&_.semi-modal-header]:px-3 [&_.semi-modal-content]:!px-3 [&_.semi-modal-content]:!coz-bg-max"
|
||||
footer={null}
|
||||
title={
|
||||
<div className="flex items-center justify-between w-full mr-4">
|
||||
<div className="coz-fg-plus text-[20px] font-medium">
|
||||
{I18n.t('select_agent_title')}
|
||||
</div>
|
||||
<Search
|
||||
placeholder={I18n.t('Search')}
|
||||
value={searchValue}
|
||||
onChange={setSearchValue}
|
||||
showClear
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div ref={containerRef} className="max-h-[480px] h-full overflow-auto">
|
||||
<IntelligenceList
|
||||
loading={loading}
|
||||
loadingMore={loadingMore}
|
||||
noMore={noMore}
|
||||
data={data}
|
||||
searchValue={searchValue}
|
||||
onSelect={intelligenceData => {
|
||||
onSelect?.(intelligenceData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -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 React from 'react';
|
||||
|
||||
import { type IntelligenceData } from '@coze-arch/idl/intelligence_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Typography } from '@coze-arch/coze-design';
|
||||
import { formatDate, getFormatDateType } from '@coze-arch/bot-utils';
|
||||
|
||||
import { highlightService } from '../services/use-case-services/highlight-text.service';
|
||||
|
||||
interface IntelligenceItemProps {
|
||||
intelligence: IntelligenceData;
|
||||
searchValue: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const IntelligenceItem: React.FC<IntelligenceItemProps> = ({
|
||||
intelligence,
|
||||
searchValue,
|
||||
onClick,
|
||||
}) => {
|
||||
const { basic_info } = intelligence;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="rounded-lg hover:coz-mg-secondary-hovered cursor-pointer h-[80px] box-border"
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="flex items-center gap-[14px] px-3 h-full">
|
||||
<img
|
||||
src={basic_info?.icon_url}
|
||||
className="flex-shrink-0 w-[52px] h-[52px] rounded-lg"
|
||||
alt={basic_info?.name}
|
||||
/>
|
||||
<div className="w-full overflow-hidden flex flex-col gap-1 justify-center border-b-[0.6px] border-solid border-0 coz-stroke-primary pb-3 pt-2 h-full">
|
||||
<div className="font-medium text-sm">
|
||||
<Typography.Text className="text-[16px] !font-medium w-full">
|
||||
{highlightService.highlightText(
|
||||
basic_info?.name || '',
|
||||
searchValue,
|
||||
)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
{basic_info?.description ? (
|
||||
<div className="text-sm leading-4 coz-fg-secondary">
|
||||
<Typography.Text
|
||||
className="text-sm w-full"
|
||||
ellipsis={{
|
||||
rows: 1,
|
||||
}}
|
||||
>
|
||||
{highlightService.highlightText(
|
||||
basic_info?.description || '',
|
||||
searchValue,
|
||||
)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="text-xs coz-fg-secondary flex items-center gap-1">
|
||||
<div className="text-xs coz-fg-secondary">
|
||||
{I18n.t('bot_list_rank_tag_edited')}
|
||||
</div>
|
||||
<div className="text-xs coz-fg-secondary">
|
||||
{formatDate(
|
||||
Number(basic_info?.update_time),
|
||||
getFormatDateType(Number(basic_info?.update_time)),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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 { type IntelligenceData } from '@coze-arch/idl/intelligence_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozLoading, IconCozEmpty } from '@coze-arch/coze-design/icons';
|
||||
import { Spin, IconButton } from '@coze-arch/coze-design';
|
||||
|
||||
import { IntelligenceItem } from './intelligence-item';
|
||||
|
||||
interface IntelligenceListProps {
|
||||
loading: boolean;
|
||||
loadingMore: boolean;
|
||||
noMore: boolean;
|
||||
data?: {
|
||||
list: IntelligenceData[];
|
||||
hasMore: boolean;
|
||||
};
|
||||
searchValue: string;
|
||||
onSelect: (intelligence: IntelligenceData) => void;
|
||||
}
|
||||
|
||||
export const IntelligenceList: React.FC<IntelligenceListProps> = ({
|
||||
loading,
|
||||
loadingMore,
|
||||
noMore,
|
||||
data,
|
||||
searchValue,
|
||||
onSelect,
|
||||
}) => {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center py-8">
|
||||
<Spin />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data?.list.length) {
|
||||
return (
|
||||
<div className="flex flex-col justify-center items-center w-full h-full">
|
||||
<IconCozEmpty className="w-[48px] h-[48px] coz-fg-dim" />
|
||||
<div className="text-sm coz-fg-primary mt-2">
|
||||
{I18n.t('select_agent_no_result')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative h-full">
|
||||
{/* 上遮罩 */}
|
||||
<div className="sticky top-0 left-0 right-0 h-[20px] bg-gradient-to-b from-[rgba(255,255,255,1)] to-transparent pointer-events-none z-10" />
|
||||
|
||||
{/* 列表内容 */}
|
||||
<div className="styled-scrollbar">
|
||||
{data.list.map(intelligence => (
|
||||
<IntelligenceItem
|
||||
key={intelligence.basic_info?.id}
|
||||
intelligence={intelligence}
|
||||
searchValue={searchValue}
|
||||
onClick={() => onSelect(intelligence)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{loadingMore ? (
|
||||
<div className="flex items-center justify-center h-[38px] my-[20px] text-[12px]">
|
||||
<IconButton icon={<IconCozLoading />} loading color="secondary" />
|
||||
<div>{I18n.t('Loading')}...</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{noMore && data.list.length > 0 ? (
|
||||
<div className="h-[38px] my-[20px]" />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* 下遮罩 */}
|
||||
<div className="sticky bottom-0 left-0 right-0 h-[20px] bg-gradient-to-t from-[rgba(255,255,255,1)] to-transparent pointer-events-none z-10" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 { useInfiniteScroll } from 'ahooks';
|
||||
|
||||
import { type IntelligenceList } from '../../services/use-case-services/intelligence-search.service';
|
||||
import { intelligenceSearchService } from '../../services/use-case-services/intelligence-search.service';
|
||||
|
||||
interface UseIntelligenceSearchProps {
|
||||
spaceId: string;
|
||||
searchValue: string;
|
||||
containerRef: React.RefObject<HTMLElement>;
|
||||
}
|
||||
|
||||
export const useIntelligenceSearch = ({
|
||||
spaceId,
|
||||
searchValue,
|
||||
containerRef,
|
||||
}: UseIntelligenceSearchProps) =>
|
||||
useInfiniteScroll<IntelligenceList>(
|
||||
async d =>
|
||||
await intelligenceSearchService.searchIntelligence({
|
||||
spaceId,
|
||||
searchValue,
|
||||
cursorId: d?.nextCursorId,
|
||||
}),
|
||||
{
|
||||
target: containerRef,
|
||||
isNoMore: d => !d?.hasMore,
|
||||
reloadDeps: [searchValue],
|
||||
},
|
||||
);
|
||||
@@ -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 { useState } from 'react';
|
||||
|
||||
import { type IntelligenceData } from '@coze-arch/idl/intelligence_api';
|
||||
|
||||
import { SelectIntelligenceModal } from '../../components';
|
||||
|
||||
interface ModalProps {
|
||||
spaceId: string;
|
||||
onSelect?: (intelligence: IntelligenceData) => void;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
export const useModal = (props: ModalProps) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const close = () => {
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const open = () => {
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
return {
|
||||
node: visible ? (
|
||||
<SelectIntelligenceModal
|
||||
visible={visible}
|
||||
spaceId={props.spaceId}
|
||||
onSelect={props.onSelect}
|
||||
onCancel={close}
|
||||
/>
|
||||
) : null,
|
||||
close,
|
||||
open,
|
||||
};
|
||||
};
|
||||
@@ -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 { SelectIntelligenceModal } from './components';
|
||||
export { useModal } from './hooks/use-case/use-modal';
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
export class HighlightTextService {
|
||||
highlightText(text: string, keyword: string) {
|
||||
if (!keyword) {
|
||||
return text;
|
||||
}
|
||||
|
||||
const parts = text.split(new RegExp(`(${keyword})`, 'gi'));
|
||||
|
||||
return (
|
||||
<>
|
||||
{parts.map((part, i) =>
|
||||
part.toLowerCase() === keyword.toLowerCase() ? (
|
||||
<span key={i} className="coz-fg-hglt-yellow">
|
||||
{part}
|
||||
</span>
|
||||
) : (
|
||||
part
|
||||
),
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const highlightService = new HighlightTextService();
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
search,
|
||||
IntelligenceStatus,
|
||||
type IntelligenceData,
|
||||
SearchScope,
|
||||
IntelligenceType,
|
||||
BotMode,
|
||||
} from '@coze-arch/idl/intelligence_api';
|
||||
import { intelligenceApi } from '@coze-arch/bot-api';
|
||||
|
||||
export interface IntelligenceList {
|
||||
list: IntelligenceData[];
|
||||
hasMore: boolean;
|
||||
nextCursorId?: string;
|
||||
}
|
||||
|
||||
export const intelligenceSearchService = {
|
||||
async searchIntelligence(params: {
|
||||
spaceId: string;
|
||||
searchValue: string;
|
||||
cursorId?: string;
|
||||
}): Promise<IntelligenceList> {
|
||||
const resp = await intelligenceApi.GetDraftIntelligenceList({
|
||||
space_id: params.spaceId,
|
||||
name: params.searchValue,
|
||||
size: 20,
|
||||
cursor_id: params.cursorId,
|
||||
order_by: search.OrderBy.UpdateTime,
|
||||
types: [IntelligenceType.Bot],
|
||||
status: [IntelligenceStatus.Using],
|
||||
search_scope: SearchScope.CreateByMe,
|
||||
option: {
|
||||
need_replica: true,
|
||||
},
|
||||
});
|
||||
const intelligenceList = resp?.data?.intelligences ?? [];
|
||||
// 只保留single mode bot
|
||||
const singleModeBotList = intelligenceList.filter(
|
||||
intelligence => intelligence.other_info?.bot_mode === BotMode.SingleMode,
|
||||
);
|
||||
|
||||
if (resp?.code === 0 && resp?.data) {
|
||||
return {
|
||||
list: singleModeBotList,
|
||||
hasMore: Boolean(resp.data.has_more),
|
||||
nextCursorId: resp.data.next_cursor_id,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
list: [],
|
||||
hasMore: false,
|
||||
};
|
||||
},
|
||||
};
|
||||
17
frontend/packages/common/biz-components/src/typings.d.ts
vendored
Normal file
@@ -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.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
@@ -0,0 +1,40 @@
|
||||
.avatar-wrap {
|
||||
position: relative;
|
||||
|
||||
.avatar {
|
||||
cursor: default;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.mask {
|
||||
cursor: pointer;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 0;
|
||||
height: 100%;
|
||||
|
||||
visibility: hidden;
|
||||
border-radius: 100%;
|
||||
|
||||
&:hover {
|
||||
visibility: visible;
|
||||
background-color: rgba(22, 22, 26, 60%);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.mask {
|
||||
color: #fff;
|
||||
visibility: visible;
|
||||
background-color: rgba(22, 22, 26, 60%);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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, type ForwardedRef, forwardRef } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useRafState } from 'ahooks';
|
||||
import { REPORT_EVENTS } from '@coze-arch/report-events';
|
||||
import { uploadAvatar } from '@coze-arch/foundation-sdk';
|
||||
import { type UploadProps } from '@coze-arch/bot-semi/Upload';
|
||||
import { Upload } from '@coze-arch/bot-semi';
|
||||
import { IconEditOutlined } from '@coze-arch/bot-icons';
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
import { CozAvatar } from '@coze-arch/coze-design';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export interface UpdateUserAvatarProps {
|
||||
value?: string;
|
||||
onChange?: (url: string) => void;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
isReadonly?: boolean;
|
||||
onError?: () => void;
|
||||
onSuccess?: (url: string) => void;
|
||||
}
|
||||
|
||||
export const UpdateUserAvatar = forwardRef(
|
||||
(
|
||||
{
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
style,
|
||||
isReadonly,
|
||||
onError,
|
||||
onSuccess,
|
||||
}: UpdateUserAvatarProps,
|
||||
ref: ForwardedRef<Upload>,
|
||||
) => {
|
||||
const [loading, setLoading] = useRafState(false);
|
||||
const customRequest: UploadProps['customRequest'] = async options => {
|
||||
const {
|
||||
onSuccess: onUpdateSuccess,
|
||||
onError: onUpdateError,
|
||||
file,
|
||||
} = options;
|
||||
|
||||
if (typeof file === 'string' || loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const { fileInstance } = file;
|
||||
|
||||
if (fileInstance) {
|
||||
// 业务
|
||||
const resp = await uploadAvatar(fileInstance);
|
||||
onChange?.(resp.web_uri);
|
||||
onUpdateSuccess?.(resp.web_uri);
|
||||
} else {
|
||||
throw new CustomError(
|
||||
REPORT_EVENTS.parmasValidation,
|
||||
'Upload failed',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
onUpdateError({
|
||||
status: 0,
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const avatarNode = <CozAvatar type="person" size="xxl" src={value} />;
|
||||
|
||||
return isReadonly ? (
|
||||
<div className={s['avatar-wrap']}>{avatarNode}</div>
|
||||
) : (
|
||||
<Upload
|
||||
action=""
|
||||
style={style}
|
||||
className={classNames(className)}
|
||||
limit={1}
|
||||
customRequest={customRequest}
|
||||
accept="image/*"
|
||||
showReplace={false}
|
||||
showUploadList={false}
|
||||
disabled={loading}
|
||||
ref={ref}
|
||||
onError={onError}
|
||||
onSuccess={onSuccess}
|
||||
>
|
||||
<div className={s['avatar-wrap']}>
|
||||
{avatarNode}
|
||||
<div className={s.mask}>
|
||||
<IconEditOutlined />
|
||||
</div>
|
||||
</div>
|
||||
</Upload>
|
||||
);
|
||||
},
|
||||
);
|
||||