feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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 PropsWithChildren } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { typeSafeAudioStaticToastVariants } from './variant';
|
||||
|
||||
export interface AudioStaticToastProps {
|
||||
theme?: 'danger' | 'primary' | 'background';
|
||||
className?: string;
|
||||
color?: 'primary' | 'danger';
|
||||
}
|
||||
|
||||
export const AudioStaticToast: React.FC<
|
||||
PropsWithChildren<AudioStaticToastProps>
|
||||
> = ({ children, theme = 'primary', color = 'primary', className }) => {
|
||||
const cvaClassNames = typeSafeAudioStaticToastVariants({ theme, color });
|
||||
return <div className={classNames(cvaClassNames, className)}>{children}</div>;
|
||||
};
|
||||
@@ -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 { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
const audioStaticToastVariants = cva(['px-24px', 'py-10px', 'rounded-[99px]'], {
|
||||
variants: {
|
||||
theme: {
|
||||
primary: ['bg-[#F2F3F7]'],
|
||||
danger: ['bg-[#FFEFF1]'],
|
||||
background: ['coz-bg-image-bots'],
|
||||
},
|
||||
color: {
|
||||
primary: ['coz-fg-primary'],
|
||||
danger: ['coz-fg-hglt-red'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export type AudioStaticToastVariantsProps = Required<
|
||||
VariantProps<typeof audioStaticToastVariants>
|
||||
>;
|
||||
export const typeSafeAudioStaticToastVariants: (
|
||||
props: AudioStaticToastVariantsProps,
|
||||
) => string = audioStaticToastVariants;
|
||||
@@ -0,0 +1,19 @@
|
||||
.container {
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
|
||||
.bar {
|
||||
width: 4px;
|
||||
min-height: 6px;
|
||||
max-height: 24px;
|
||||
|
||||
background-color: #AAA;
|
||||
border-radius: 8px;
|
||||
|
||||
transition: height 0.1s ease-in-out; /* 设置过渡效果 */
|
||||
}
|
||||
}
|
||||
@@ -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 classNames from 'classnames';
|
||||
import { Space } from '@coze-arch/coze-design';
|
||||
|
||||
import { getBarBgColor, getBarHeights } from './utils';
|
||||
import { type AudioWaveProps } from './type';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
const waveBarNumberMap = {
|
||||
large: 41,
|
||||
medium: 29,
|
||||
small: 4,
|
||||
};
|
||||
export const AudioWave = ({
|
||||
size = 'medium',
|
||||
volumeNumber = 0,
|
||||
type = 'default',
|
||||
wrapperClassName,
|
||||
waveClassName,
|
||||
}: AudioWaveProps) => {
|
||||
const volumeRealNumber = Math.max(Math.min(volumeNumber, 100), 0);
|
||||
const waveBarNumber = waveBarNumberMap[size] || 29;
|
||||
const waveBarHeights = getBarHeights(waveBarNumber, volumeRealNumber);
|
||||
|
||||
return (
|
||||
<Space
|
||||
spacing={3}
|
||||
align="center"
|
||||
className={classNames(styles.container, wrapperClassName)}
|
||||
>
|
||||
{waveBarHeights.map((height, index) => (
|
||||
<div
|
||||
className={classNames(
|
||||
styles[`audio-wave-${index}`],
|
||||
styles[type],
|
||||
styles.bar,
|
||||
styles[size],
|
||||
waveClassName,
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: getBarBgColor(index, waveBarNumber, type),
|
||||
height,
|
||||
}}
|
||||
key={`${type}_${index}`}
|
||||
/>
|
||||
))}
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export interface AudioWaveProps {
|
||||
size: 'small' | 'medium' | 'large';
|
||||
type: 'default' | 'primary' | 'warning';
|
||||
/** 0 ~ 100 */
|
||||
volumeNumber: number;
|
||||
wrapperClassName?: string;
|
||||
waveClassName?: string;
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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 AudioWaveProps } from './type';
|
||||
|
||||
export const getBarHeights = (
|
||||
waveBarNumber: number,
|
||||
volumeRealNumber: number,
|
||||
) => {
|
||||
if (volumeRealNumber <= 0) {
|
||||
return new Array(waveBarNumber).fill(8);
|
||||
}
|
||||
const waveBarHeights = new Array(waveBarNumber)
|
||||
.fill(0)
|
||||
.map((_item, index) =>
|
||||
getBarHeight3(index, waveBarNumber, volumeRealNumber),
|
||||
);
|
||||
const minHeight = Math.min(...waveBarHeights);
|
||||
const maxHeight = Math.max(...waveBarHeights);
|
||||
const heightSpan = maxHeight - minHeight;
|
||||
return waveBarHeights.map(
|
||||
item =>
|
||||
8 +
|
||||
((item - minHeight) / (maxHeight - minHeight)) * Math.min(12, heightSpan),
|
||||
);
|
||||
};
|
||||
|
||||
export const getBarHeight3 = (
|
||||
index: number,
|
||||
maxNumber: number,
|
||||
volumeNumber: number,
|
||||
) => {
|
||||
const percent = index / maxNumber;
|
||||
const maxHeight = 24;
|
||||
let baseHeight = 2;
|
||||
let randomMin = -2;
|
||||
let randomMax = 2;
|
||||
if (percent < 1 / 6) {
|
||||
baseHeight = 1 + (4 - 1) * percent * 6;
|
||||
randomMin = 0.1 + (-0.8 - 0.1) * percent * 6;
|
||||
randomMax = 0.3 + (0.6 - 0.3) * percent * 6;
|
||||
} else if (percent < 2 / 6) {
|
||||
baseHeight = 4 + (2 - 4) * (percent - 1 / 6) * 6;
|
||||
randomMin = -0.8 + (-0.0 + 0.8) * (percent - 1 / 6) * 6;
|
||||
randomMax = 0.6 + (0.6 - 0.6) * (percent - 1 / 6) * 6;
|
||||
} else if (percent < 3 / 6) {
|
||||
baseHeight = 2 + (8 - 2) * (percent - 2 / 6) * 6;
|
||||
randomMin = 0.0 + (Number(-1.6) - 0.0) * (percent - 2 / 6) * 6;
|
||||
randomMax = 0.6 + (1.2 - 0.6) * (percent - 2 / 6) * 6;
|
||||
} else if (percent < 4 / 6) {
|
||||
baseHeight = 8 + (2 - 8) * (percent - 3 / 6) * 6;
|
||||
randomMin = -1.6 + (0.0 + 1.6) * (percent - 3 / 6) * 6;
|
||||
randomMax = 1.2 + (0.6 - 1.2) * (percent - 3 / 6) * 6;
|
||||
} else if (percent < 5 / 6) {
|
||||
baseHeight = 2 + (4 - 2) * (percent - 4 / 6) * 6;
|
||||
randomMin = 0.0 + (Number(-0.8) - 0.0) * (percent - 4 / 6) * 6;
|
||||
randomMax = 0.6 + (0.6 - 0.6) * (percent - 4 / 6) * 6;
|
||||
} else if (percent < 1) {
|
||||
baseHeight = 4 + (1 - 4) * (percent - 5 / 6) * 6;
|
||||
randomMin = -0.8 + (0.1 + 0.8) * (percent - 5 / 6) * 6;
|
||||
randomMax = 0.1 + (0.3 - 0.6) * (percent - 5 / 6) * 6;
|
||||
}
|
||||
const height =
|
||||
baseHeight +
|
||||
volumeNumber *
|
||||
(Math.random() * (randomMax - randomMin) + randomMin) *
|
||||
(maxHeight - baseHeight);
|
||||
return height;
|
||||
};
|
||||
|
||||
export const getBarBgColor = (
|
||||
index: number,
|
||||
maxNumber: number,
|
||||
type: AudioWaveProps['type'],
|
||||
) => {
|
||||
let bgColor = '#FFF';
|
||||
switch (type) {
|
||||
case 'primary':
|
||||
{
|
||||
/*
|
||||
* implement : fill: linear-gradient(90deg, rgba(83, 71, 255, 0.20) 0%, #5347FF 20%, #B125F1 80%, rgba(177, 37, 241, 0.20) 100%);
|
||||
*/
|
||||
let opacity = 0;
|
||||
let rColor = 0;
|
||||
let gColor = 0;
|
||||
let bColor = 0;
|
||||
const percent = index / maxNumber;
|
||||
if (percent < 0.2) {
|
||||
opacity = 0.2 + ((1 - 0.2) * percent) / 0.2;
|
||||
rColor = 83;
|
||||
gColor = 71;
|
||||
bColor = 255;
|
||||
} else if (percent < 0.8) {
|
||||
opacity = 1;
|
||||
rColor = Math.round(83 + ((177 - 83) * (percent - 0.2)) / 0.6);
|
||||
gColor = Math.round(71 + ((37 - 71) * (percent - 0.2)) / 0.6);
|
||||
bColor = Math.round(255 + ((241 - 255) * (percent - 0.2)) / 0.6);
|
||||
} else {
|
||||
opacity = 1 - ((1 - 0.2) * (percent - 0.8)) / 0.2;
|
||||
rColor = 177;
|
||||
gColor = 37;
|
||||
bColor = 241;
|
||||
}
|
||||
bgColor = `rgba(${rColor}, ${gColor}, ${bColor}, ${opacity.toFixed(
|
||||
2,
|
||||
)})`;
|
||||
}
|
||||
break;
|
||||
case 'warning':
|
||||
{
|
||||
bgColor = '#FF0030';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
{
|
||||
/*
|
||||
* implement : fill: linear-gradient(90deg, rgba(255, 255, 255, 0.20) 0%, #FFF 20%, rgba(255, 255, 255, 0.90) 80%, rgba(255, 255, 255, 0.20) 100%);
|
||||
*/
|
||||
let opacity = 0;
|
||||
const percent = index / maxNumber;
|
||||
if (percent < 0.2) {
|
||||
opacity = 0.2 + ((1 - 0.2) * percent) / 0.2;
|
||||
} else if (percent < 0.8) {
|
||||
opacity = 1 - ((1 - 0.9) * (percent - 0.2)) / 0.6;
|
||||
} else {
|
||||
opacity = 0.9 - ((0.9 - 0.2) * (percent - 0.8)) / 0.2;
|
||||
}
|
||||
bgColor = `rgba(255, 255, 255, ${opacity.toFixed(2)})`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return bgColor;
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { forwardRef, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useThrottleFn } from 'ahooks';
|
||||
import { type AudioRecordProps, Layout } from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { type AudioWaveProps } from './audio-wave/type';
|
||||
import { AudioWave } from './audio-wave';
|
||||
|
||||
export const AudioRecord = forwardRef<
|
||||
HTMLDivElement,
|
||||
AudioRecordProps & { layout: Layout }
|
||||
>(({ isRecording, getVolume, isPointerMoveOut, layout, text }, ref) => {
|
||||
const [volumeNumber, setVolumeNumber] = useState(0);
|
||||
const animationIdRef = useRef<number | null>(null);
|
||||
const { run, flush } = useThrottleFn(
|
||||
() => {
|
||||
setVolumeNumber(getVolume?.() ?? 0);
|
||||
animationIdRef.current = requestAnimationFrame(run);
|
||||
},
|
||||
{ wait: 100 },
|
||||
);
|
||||
|
||||
const getAudioWaveTheme = (): AudioWaveProps['type'] => {
|
||||
if (layout === Layout.MOBILE) {
|
||||
return 'default';
|
||||
}
|
||||
if (isPointerMoveOut) {
|
||||
return 'warning';
|
||||
}
|
||||
return 'primary';
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRecording) {
|
||||
return;
|
||||
}
|
||||
|
||||
run();
|
||||
|
||||
return () => {
|
||||
flush();
|
||||
if (typeof animationIdRef.current !== 'number') {
|
||||
return;
|
||||
}
|
||||
cancelAnimationFrame(animationIdRef.current);
|
||||
animationIdRef.current = null;
|
||||
};
|
||||
}, [isRecording]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className="w-full h-32px relative">
|
||||
<div className="w-full h-full flex items-center justify-center pointer-events-none">
|
||||
{isRecording ? (
|
||||
<AudioWave
|
||||
size="medium"
|
||||
type={getAudioWaveTheme()}
|
||||
volumeNumber={volumeNumber}
|
||||
/>
|
||||
) : (
|
||||
<div className="coz-fg-primary text-lg font-medium leading-[20px]">
|
||||
{text}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export { AudioWave };
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReactNode, type FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { IconCozChatPlus } from '@coze-arch/coze-design/icons';
|
||||
import { Layout } from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { UIKitTooltip } from '../../../../common/tooltips';
|
||||
import { OutlinedIconButton } from '../../../../common';
|
||||
|
||||
interface IProps {
|
||||
isDisabled?: boolean;
|
||||
tooltipContent?: ReactNode;
|
||||
onClick: () => void;
|
||||
layout: Layout;
|
||||
className: string;
|
||||
showBackground: boolean;
|
||||
}
|
||||
|
||||
const ClearContextButton: FC<IProps> = props => {
|
||||
const {
|
||||
isDisabled,
|
||||
tooltipContent,
|
||||
onClick,
|
||||
layout,
|
||||
className,
|
||||
showBackground,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<UIKitTooltip
|
||||
content={tooltipContent}
|
||||
hideToolTip={layout === Layout.MOBILE}
|
||||
>
|
||||
<OutlinedIconButton
|
||||
data-testid="chat-input-clear-context-button"
|
||||
showBackground={showBackground}
|
||||
disabled={isDisabled}
|
||||
icon={<IconCozChatPlus className="text-18px" />}
|
||||
size="default"
|
||||
onClick={onClick}
|
||||
className={classNames('mr-12px', '!rounded-full', className)}
|
||||
/>
|
||||
</UIKitTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClearContextButton;
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReactNode, type FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { IconCozBroom } from '@coze-arch/coze-design/icons';
|
||||
import { Layout } from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { UIKitTooltip } from '../../../../common/tooltips';
|
||||
import { OutlinedIconButton } from '../../../../common';
|
||||
|
||||
interface IProps {
|
||||
isDisabled?: boolean;
|
||||
tooltipContent?: ReactNode;
|
||||
onClick: () => void;
|
||||
layout: Layout;
|
||||
className: string;
|
||||
showBackground: boolean;
|
||||
}
|
||||
|
||||
const ClearHistoryButton: FC<IProps> = props => {
|
||||
const {
|
||||
isDisabled,
|
||||
tooltipContent,
|
||||
onClick,
|
||||
layout,
|
||||
className,
|
||||
showBackground,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<UIKitTooltip
|
||||
content={tooltipContent}
|
||||
hideToolTip={layout === Layout.MOBILE}
|
||||
>
|
||||
<OutlinedIconButton
|
||||
data-testid="bot-edit-debug-chat-clear-button"
|
||||
showBackground={showBackground}
|
||||
disabled={isDisabled}
|
||||
icon={<IconCozBroom className="text-18px" />}
|
||||
size="default"
|
||||
onClick={onClick}
|
||||
className={classNames('mr-12px', '!rounded-full', className)}
|
||||
/>
|
||||
</UIKitTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClearHistoryButton;
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReactNode, type FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { IconCozPlusCircle } from '@coze-arch/coze-design/icons';
|
||||
import { IconButton } from '@coze-arch/coze-design';
|
||||
import { Layout } from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { UIKitTooltip } from '../../../../common/tooltips';
|
||||
|
||||
interface IProps {
|
||||
isDisabled?: boolean;
|
||||
tooltipContent?: ReactNode;
|
||||
layout: Layout;
|
||||
}
|
||||
|
||||
const MoreButton: FC<IProps> = props => {
|
||||
const { isDisabled, tooltipContent, layout } = props;
|
||||
|
||||
return (
|
||||
<UIKitTooltip
|
||||
// 为了点调起选择文件的事件时收起 tooltip
|
||||
disableFocusListener
|
||||
content={tooltipContent}
|
||||
hideToolTip={layout === Layout.MOBILE}
|
||||
>
|
||||
<IconButton
|
||||
className="!rounded-full"
|
||||
data-testid="chat-area.chat-upload-button"
|
||||
color="secondary"
|
||||
disabled={isDisabled}
|
||||
icon={
|
||||
<IconCozPlusCircle
|
||||
className={classNames(
|
||||
isDisabled ? 'coz-fg-dim' : 'coz-fg-primary',
|
||||
'text-18px',
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</UIKitTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default MoreButton;
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { IconCozSendFill } from '@coze-arch/coze-design/icons';
|
||||
import { IconButton } from '@coze-arch/coze-design';
|
||||
import { Layout, type SendButtonProps } from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { UIKitTooltip } from '../../../../common/tooltips';
|
||||
|
||||
const SendButton: FC<SendButtonProps> = props => {
|
||||
const { isDisabled, tooltipContent, onClick, layout } = props;
|
||||
return (
|
||||
<UIKitTooltip
|
||||
content={tooltipContent}
|
||||
hideToolTip={layout === Layout.MOBILE}
|
||||
>
|
||||
<IconButton
|
||||
className={classNames('!rounded-full', !isDisabled && '!coz-fg-hglt')}
|
||||
disabled={isDisabled}
|
||||
data-testid="bot-home-chart-send-button"
|
||||
size="default"
|
||||
color="secondary"
|
||||
icon={<IconCozSendFill className="text-18px" />}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</UIKitTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default SendButton;
|
||||
@@ -0,0 +1,184 @@
|
||||
.input-wrap {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@keyframes move-left-fade-out {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes move-right-fade-in {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@fade-easing-time: 300ms;
|
||||
|
||||
@fade-easing-function: cubic-bezier(0.65, 0, 0.35, 1);
|
||||
|
||||
.animate-left {
|
||||
animation: move-left-fade-out forwards @fade-easing-time @fade-easing-function;
|
||||
}
|
||||
|
||||
.animate-left-revert {
|
||||
animation: move-right-fade-in forwards @fade-easing-time @fade-easing-function;
|
||||
}
|
||||
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.input-tooltip-anchor {
|
||||
position: absolute;
|
||||
top: -10%;
|
||||
}
|
||||
|
||||
.left-actions-container {
|
||||
align-self: flex-end;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.textarea-with-top-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: 24px;
|
||||
|
||||
transition: width @fade-easing-time;
|
||||
}
|
||||
|
||||
.background-theme {
|
||||
textarea {
|
||||
// 背景图模式覆盖样式
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
.textarea-with-actions-container {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.input-focus {
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line selector-class-pattern */
|
||||
.textarea-with-actions-container__col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line selector-class-pattern */
|
||||
.textarea-with-actions-container__row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.textarea-actions-container {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.textarea-actions-container-transition {
|
||||
transition: opacity @fade-easing-time @fade-easing-function
|
||||
}
|
||||
|
||||
.textarea-actions-left {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.textarea-actions-right {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
display: flex;
|
||||
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
min-height: 20px;
|
||||
margin: 0;
|
||||
|
||||
font-family: inherit;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
outline: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #e6e6e7;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.textarea::placeholder {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.textarea:disabled {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.bottom-tips {
|
||||
width: 100%;
|
||||
padding: 8px 24px;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.danger-mask-transition {
|
||||
transition: opacity 500ms cubic-bezier(0.33, 1, 0.68, 1);
|
||||
}
|
||||
|
||||
.mobile-audio-bg {
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
background: linear-gradient(117deg, #5448FF 4.74%, #B026F1 131.42%) !important;
|
||||
}
|
||||
|
||||
.mobile-audio-bg-danger {
|
||||
@apply !coz-mg-hglt-plus-red;
|
||||
}
|
||||
@@ -0,0 +1,527 @@
|
||||
/*
|
||||
* 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 {
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useState,
|
||||
type Dispatch,
|
||||
type ForwardRefExoticComponent,
|
||||
type PropsWithChildren,
|
||||
type PropsWithoutRef,
|
||||
type RefAttributes,
|
||||
type RefObject,
|
||||
type SetStateAction,
|
||||
useRef,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
|
||||
import Textarea, { type TextAreaRef } from 'rc-textarea';
|
||||
import { merge } from 'lodash-es';
|
||||
import classNames from 'classnames';
|
||||
import { useControllableValue, useUpdateEffect } from 'ahooks';
|
||||
import {
|
||||
IconCozKeyboard,
|
||||
IconCozMicrophone,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import { Divider, IconButton } from '@coze-arch/coze-design';
|
||||
import {
|
||||
type IChatInputProps,
|
||||
type InputMode,
|
||||
Layout,
|
||||
} from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { AudioRecord } from '../audio-record';
|
||||
import { UIKitTooltip } from '../../common';
|
||||
import { useAudioRecordInteraction } from '../../../hooks/use-audio-record-interaction';
|
||||
import { ChatUpload } from '../../../components/chat/chat-upload';
|
||||
import { useTextSend } from './use-text-area';
|
||||
import BuiltinSendButton from './components/send-button';
|
||||
import MoreButton from './components/more-button';
|
||||
import ClearHistoryButton from './components/clear-history-button';
|
||||
import ClearContextButton from './components/clear-context-button';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
const DEFAULT_HEIGHT = 24;
|
||||
|
||||
export interface UiKitChatInputButtonConfig {
|
||||
isSendButtonVisible: boolean;
|
||||
isClearHistoryButtonVisible: boolean;
|
||||
isMoreButtonVisible: boolean;
|
||||
isClearContextButtonVisible: boolean;
|
||||
}
|
||||
|
||||
export type ChatInputTooltipComponent = ForwardRefExoticComponent<
|
||||
PropsWithoutRef<PropsWithChildren> &
|
||||
RefAttributes<{
|
||||
close: () => void;
|
||||
}>
|
||||
>;
|
||||
|
||||
export interface InputRefObject {
|
||||
input: RefObject<TextAreaRef>;
|
||||
setValue: Dispatch<SetStateAction<string>>;
|
||||
sendMessage: IChatInputProps['onSendMessage'];
|
||||
}
|
||||
|
||||
export const ChatInput = forwardRef<InputRefObject, IChatInputProps>(
|
||||
// eslint-disable-next-line complexity, @coze-arch/max-line-per-function, max-lines-per-function
|
||||
(props, ref) => {
|
||||
const {
|
||||
onBeforeSubmit,
|
||||
onFocus,
|
||||
onBlur,
|
||||
isReadonly,
|
||||
leftActions,
|
||||
rightActions,
|
||||
addonTop,
|
||||
addonLeft,
|
||||
aboveOutside,
|
||||
buildInButtonConfig,
|
||||
buildInButtonStatus,
|
||||
copywritingConfig,
|
||||
onSendMessage,
|
||||
onClearHistory,
|
||||
onClearContext,
|
||||
onUpload,
|
||||
onInputClick,
|
||||
hasOtherContentToSend,
|
||||
layout,
|
||||
isFileCountExceedsLimit,
|
||||
inputTooltip: InputTooltip,
|
||||
showBackground,
|
||||
limitFileCount,
|
||||
onPaste,
|
||||
CustomSendButton,
|
||||
isInputReadonly,
|
||||
inputNativeCallbacks,
|
||||
audioRecordEvents = {},
|
||||
audioRecordState = {},
|
||||
audioRecordOptions,
|
||||
className: _className,
|
||||
wrapperClassName,
|
||||
...inputModeProps
|
||||
} = props;
|
||||
|
||||
const {
|
||||
isSendButtonVisible = true,
|
||||
isClearHistoryButtonVisible = true,
|
||||
isMoreButtonVisible = true,
|
||||
isClearContextButtonVisible = false,
|
||||
} = buildInButtonConfig ?? {};
|
||||
|
||||
const {
|
||||
isClearHistoryButtonDisabled,
|
||||
isMoreButtonDisabled,
|
||||
isSendButtonDisabled,
|
||||
isClearContextButtonDisabled,
|
||||
} = buildInButtonStatus ?? {};
|
||||
const { tooltip, inputPlaceholder, uploadConfig, bottomTips } =
|
||||
copywritingConfig ?? {};
|
||||
const {
|
||||
sendButtonTooltipContent,
|
||||
clearHistoryButtonTooltipContent,
|
||||
clearContextButtonTooltipContent,
|
||||
moreButtonTooltipContent,
|
||||
audioButtonTooltipContent,
|
||||
keyboardButtonTooltipContent,
|
||||
} = tooltip ?? {};
|
||||
const [isMultiLines, setIsMultiLines] = useState(false);
|
||||
const [breakLength, setBreakLength] = useState(0);
|
||||
const [isFocus, setIsFocus] = useState(false);
|
||||
const [mode, setMode] = useControllableValue<InputMode>(inputModeProps, {
|
||||
defaultValue: 'input',
|
||||
valuePropName: 'inputMode',
|
||||
trigger: 'onInputModeChange',
|
||||
});
|
||||
const audioButtonWrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useAudioRecordInteraction({
|
||||
target: audioButtonWrapperRef,
|
||||
events: audioRecordEvents,
|
||||
options: merge({}, audioRecordOptions, {
|
||||
enabled: mode === 'audio' && audioRecordOptions?.enabled,
|
||||
}),
|
||||
});
|
||||
|
||||
const inputSizeRef = useRef<HTMLDivElement>(null);
|
||||
const [inputWidth, setInputWidth] = useState<'100%' | number>('100%');
|
||||
useEffect(() => {
|
||||
const target = inputSizeRef.current;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
const observer = new ResizeObserver(() => {
|
||||
setInputWidth(target.clientWidth);
|
||||
});
|
||||
observer.observe(target);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const isInputMode = mode === 'input';
|
||||
const isAudioMode = mode === 'audio';
|
||||
|
||||
const handleClickContextButtonClick = () => {
|
||||
onClearContext?.();
|
||||
};
|
||||
/**
|
||||
* 处理清除历史记录按钮点击事件
|
||||
*/
|
||||
const handleClearHistoryButtonClick = () => {
|
||||
onClearHistory?.();
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理用户发送消息
|
||||
* @param text 用户发送消息的文本
|
||||
*/
|
||||
const handleSendMessage = (text: string) => {
|
||||
onSendMessage?.({
|
||||
text,
|
||||
mentionList: [],
|
||||
});
|
||||
setIsMultiLines(false);
|
||||
setBreakLength(0);
|
||||
};
|
||||
|
||||
const {
|
||||
onChange,
|
||||
setIsComposing,
|
||||
onKeyDown,
|
||||
inputText,
|
||||
setInputText,
|
||||
submit: handleSendButtonClick,
|
||||
rcTextareaRef,
|
||||
updateSelectPos,
|
||||
onKeyUp,
|
||||
} = useTextSend({
|
||||
onSubmit: handleSendMessage,
|
||||
onBeforeSubmit,
|
||||
isDisabled: isSendButtonDisabled,
|
||||
allowEmpty: hasOtherContentToSend,
|
||||
inputNativeCallbacks,
|
||||
});
|
||||
const buttonClass = showBackground ? '!coz-fg-images-white' : '';
|
||||
|
||||
const handleResize = ({ height }: { height: number }) => {
|
||||
if (
|
||||
!isMultiLines &&
|
||||
height > DEFAULT_HEIGHT &&
|
||||
// 通过 inputText 长度判断,排除 placeholder 导致的 resize 不处理
|
||||
inputText?.trim()?.length !== 0
|
||||
) {
|
||||
setIsMultiLines(true);
|
||||
setBreakLength(inputText.length);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnChange = (evt: { target: { value: string } }) => {
|
||||
onChange(evt);
|
||||
if (isMultiLines && evt.target.value.length < breakLength) {
|
||||
setIsMultiLines(false);
|
||||
}
|
||||
};
|
||||
|
||||
/** 计算出复合条件的readonly值 */
|
||||
const finalClearHistoryButtonDisable =
|
||||
isClearHistoryButtonDisabled || isReadonly;
|
||||
const finalSendButtonDisable =
|
||||
(Boolean(!inputText?.trim()) && !hasOtherContentToSend) ||
|
||||
isSendButtonDisabled ||
|
||||
isReadonly;
|
||||
const finalMoreButtonDisable = isMoreButtonDisabled || isReadonly;
|
||||
const finalClearContextButtonDisable =
|
||||
isClearContextButtonDisabled || isReadonly;
|
||||
|
||||
const getFinalSendButtonVisible = () => {
|
||||
const visibleCondition = isSendButtonVisible && isInputMode;
|
||||
if (audioRecordOptions?.enabled) {
|
||||
return visibleCondition && inputText;
|
||||
}
|
||||
return visibleCondition;
|
||||
};
|
||||
useUpdateEffect(() => {
|
||||
if (mode !== 'input') {
|
||||
return;
|
||||
}
|
||||
rcTextareaRef.current?.focus();
|
||||
}, [mode]);
|
||||
useImperativeHandle(ref, () => ({
|
||||
input: rcTextareaRef,
|
||||
setValue: setInputText,
|
||||
sendMessage: onSendMessage,
|
||||
}));
|
||||
|
||||
const SendButton = CustomSendButton ?? BuiltinSendButton;
|
||||
|
||||
return (
|
||||
<div className={styles['input-container']}>
|
||||
<div
|
||||
className={classNames(
|
||||
styles['input-wrap'],
|
||||
'py-0',
|
||||
layout === Layout.MOBILE ? 'px-[16px]' : 'px-[24px]',
|
||||
'input-wraper-for-reset',
|
||||
)}
|
||||
>
|
||||
{aboveOutside}
|
||||
<div
|
||||
className={classNames(
|
||||
styles['left-actions-container'],
|
||||
['mb-8px'],
|
||||
typeof audioRecordState.isRecording === 'boolean' &&
|
||||
audioRecordState.isRecording
|
||||
? [styles['animate-left'], '!w-0']
|
||||
: styles['animate-left-revert'],
|
||||
)}
|
||||
>
|
||||
{leftActions}
|
||||
{isClearContextButtonVisible ? (
|
||||
<ClearContextButton
|
||||
showBackground={Boolean(showBackground)}
|
||||
tooltipContent={clearContextButtonTooltipContent}
|
||||
isDisabled={finalClearContextButtonDisable}
|
||||
onClick={handleClickContextButtonClick}
|
||||
data-testid="bot-edit-debug-chat-clear-button"
|
||||
layout={layout}
|
||||
className={buttonClass}
|
||||
></ClearContextButton>
|
||||
) : null}
|
||||
{isClearHistoryButtonVisible ? (
|
||||
<ClearHistoryButton
|
||||
showBackground={Boolean(showBackground)}
|
||||
tooltipContent={clearHistoryButtonTooltipContent}
|
||||
isDisabled={finalClearHistoryButtonDisable}
|
||||
onClick={handleClearHistoryButtonClick}
|
||||
data-testid="bot-edit-debug-chat-clear-button"
|
||||
layout={layout}
|
||||
className={buttonClass}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
ref={inputSizeRef}
|
||||
className="flex-[1] flex justify-end overflow-hidden"
|
||||
>
|
||||
<div
|
||||
style={{ width: inputWidth }}
|
||||
className={classNames(
|
||||
styles['textarea-with-top-rows'],
|
||||
['coz-bg-max', 'coz-stroke-plus', 'relative'],
|
||||
isFocus && styles['input-focus'],
|
||||
(isFocus || audioRecordState.isRecording) && [
|
||||
'!coz-stroke-hglt',
|
||||
],
|
||||
audioRecordState.isRecording &&
|
||||
audioRecordState.isPointerMoveOut &&
|
||||
'!coz-stroke-hglt-red',
|
||||
audioRecordState.isRecording
|
||||
? 'overflow-visible'
|
||||
: 'overflow-hidden',
|
||||
mode === 'audio' && 'cursor-pointer',
|
||||
(mode === 'audio' || audioRecordState.isRecording) &&
|
||||
'hover:coz-stroke-hglt',
|
||||
showBackground && styles['background-theme'],
|
||||
{ '!coz-bg-image-bots': showBackground },
|
||||
layout === Layout.MOBILE &&
|
||||
audioRecordState.isRecording &&
|
||||
(audioRecordState.isPointerMoveOut
|
||||
? styles['mobile-audio-bg-danger']
|
||||
: styles['mobile-audio-bg']),
|
||||
wrapperClassName,
|
||||
)}
|
||||
>
|
||||
{addonTop}
|
||||
<div
|
||||
className={classNames(
|
||||
styles['textarea-with-actions-container'],
|
||||
styles['coz-textarea-with-actions-container-padding'],
|
||||
'py-8px',
|
||||
'pr-8px',
|
||||
'pl-20px',
|
||||
{
|
||||
[styles['textarea-with-actions-container__row']]:
|
||||
!isMultiLines,
|
||||
[styles['textarea-with-actions-container__col']]:
|
||||
isMultiLines,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{InputTooltip ? (
|
||||
<InputTooltip>
|
||||
<i className={styles['input-tooltip-anchor']} />
|
||||
</InputTooltip>
|
||||
) : null}
|
||||
{addonLeft}
|
||||
{isInputMode ? (
|
||||
<Textarea
|
||||
data-testid="bot.ide.chat_area.chat_input.textarea"
|
||||
disabled={isInputReadonly || isReadonly}
|
||||
className={classNames(
|
||||
styles.textarea,
|
||||
[
|
||||
'coz-fg-primary',
|
||||
'coz-bg-max',
|
||||
'disabled:coz-bg-max',
|
||||
'placeholder:coz-fg-dim',
|
||||
],
|
||||
{
|
||||
[styles['textarea--with-margin']]: isMultiLines,
|
||||
},
|
||||
)}
|
||||
autoSize={{
|
||||
minRows: 1,
|
||||
maxRows: 5,
|
||||
}}
|
||||
classNames={{
|
||||
textarea: classNames(
|
||||
styles.textarea,
|
||||
layout === Layout.MOBILE
|
||||
? 'text-[16px]'
|
||||
: 'text-[14px]',
|
||||
),
|
||||
}}
|
||||
ref={rcTextareaRef}
|
||||
placeholder={inputPlaceholder}
|
||||
onChange={handleOnChange}
|
||||
value={inputText}
|
||||
onCompositionStart={evt => setIsComposing(evt, true)}
|
||||
onCompositionEnd={evt => setIsComposing(evt, false)}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
onResize={handleResize}
|
||||
onSelect={updateSelectPos}
|
||||
onClick={onInputClick}
|
||||
onFocus={e => {
|
||||
onFocus?.(e);
|
||||
setIsFocus(true);
|
||||
}}
|
||||
onBlur={e => {
|
||||
onBlur?.(e);
|
||||
setIsFocus(false);
|
||||
}}
|
||||
onPaste={onPaste}
|
||||
/>
|
||||
) : null}
|
||||
{isAudioMode ? (
|
||||
<AudioRecord
|
||||
ref={audioButtonWrapperRef}
|
||||
{...audioRecordState}
|
||||
layout={layout}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
audioRecordState.isRecording
|
||||
? 'opacity-0 w-0'
|
||||
: 'opacity-100',
|
||||
styles['textarea-actions-container'],
|
||||
styles['textarea-actions-container-transition'],
|
||||
)}
|
||||
>
|
||||
<div className={styles['textarea-actions-right']}>
|
||||
{getFinalSendButtonVisible() ? (
|
||||
<SendButton
|
||||
tooltipContent={sendButtonTooltipContent}
|
||||
isDisabled={finalSendButtonDisable}
|
||||
onClick={handleSendButtonClick}
|
||||
layout={layout}
|
||||
/>
|
||||
) : null}
|
||||
{!inputText &&
|
||||
!isAudioMode &&
|
||||
audioRecordOptions?.enabled ? (
|
||||
<UIKitTooltip
|
||||
content={audioButtonTooltipContent}
|
||||
hideToolTip={layout === Layout.MOBILE}
|
||||
>
|
||||
<IconButton
|
||||
className="!rounded-full"
|
||||
color="secondary"
|
||||
icon={<IconCozMicrophone className="text-18px" />}
|
||||
onClick={() => {
|
||||
setMode('audio');
|
||||
}}
|
||||
/>
|
||||
</UIKitTooltip>
|
||||
) : null}
|
||||
{isAudioMode ? (
|
||||
<UIKitTooltip
|
||||
content={keyboardButtonTooltipContent}
|
||||
hideToolTip={layout === Layout.MOBILE}
|
||||
>
|
||||
<IconButton
|
||||
className="!rounded-full"
|
||||
color="secondary"
|
||||
icon={<IconCozKeyboard className="text-18px" />}
|
||||
onClick={() => {
|
||||
setMode('input');
|
||||
}}
|
||||
/>
|
||||
</UIKitTooltip>
|
||||
) : null}
|
||||
</div>
|
||||
{rightActions || isMoreButtonVisible ? (
|
||||
<>
|
||||
<Divider layout="vertical" style={{ height: '14px' }} />
|
||||
<div className={styles['textarea-actions-left']}>
|
||||
{rightActions}
|
||||
{isMoreButtonVisible ? (
|
||||
<ChatUpload
|
||||
onUpload={(uploadType, file) =>
|
||||
onUpload?.(uploadType, { file, mentionList: [] })
|
||||
}
|
||||
isFileCountExceedsLimit={isFileCountExceedsLimit}
|
||||
copywritingConfig={uploadConfig}
|
||||
isDisabled={finalMoreButtonDisable}
|
||||
limitFileCount={limitFileCount}
|
||||
>
|
||||
<MoreButton
|
||||
tooltipContent={moreButtonTooltipContent}
|
||||
isDisabled={finalMoreButtonDisable}
|
||||
layout={layout}
|
||||
/>
|
||||
</ChatUpload>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{bottomTips ? (
|
||||
<div
|
||||
className={classNames(
|
||||
styles['bottom-tips'],
|
||||
'coz-fg-dim',
|
||||
showBackground && '!coz-fg-images-secondary',
|
||||
)}
|
||||
>
|
||||
<span>{bottomTips}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ChatInput.displayName = 'UiKitChatInput';
|
||||
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* 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,
|
||||
type KeyboardEvent,
|
||||
useRef,
|
||||
useEffect,
|
||||
type KeyboardEventHandler,
|
||||
type CompositionEvent,
|
||||
} from 'react';
|
||||
|
||||
import { type TextAreaRef } from 'rc-textarea';
|
||||
import {
|
||||
type InputNativeCallbacks,
|
||||
type InputController,
|
||||
type InputState,
|
||||
} from '@coze-common/chat-uikit-shared';
|
||||
import { useImperativeLayoutEffect } from '@coze-common/chat-hooks';
|
||||
|
||||
type KeyboardGeneralEvent = KeyboardEvent<HTMLElement>;
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function, @coze-arch/max-line-per-function -- x.x
|
||||
export const useTextSend = ({
|
||||
onSubmit,
|
||||
defaultValue = '',
|
||||
allowEmpty = false,
|
||||
onBeforeSubmit,
|
||||
isDisabled = false,
|
||||
inputNativeCallbacks = {},
|
||||
}: {
|
||||
onSubmit: (text: string) => void;
|
||||
defaultValue?: string;
|
||||
/**
|
||||
* 是否允许空字符串提交
|
||||
* @default false
|
||||
*/
|
||||
allowEmpty?: boolean;
|
||||
onBeforeSubmit?: () => boolean;
|
||||
isDisabled?: boolean;
|
||||
inputNativeCallbacks?: InputNativeCallbacks;
|
||||
}) => {
|
||||
const [inputText, setInputText] = useState(defaultValue);
|
||||
const composingRef = useRef(false);
|
||||
const rcTextareaRef = useRef<TextAreaRef>(null);
|
||||
const selectionRef = useRef<{ start: number; end: number }>({
|
||||
start: 0,
|
||||
end: 0,
|
||||
});
|
||||
const getTextarea = () => rcTextareaRef.current?.resizableTextArea.textArea;
|
||||
const waitAndUpdateSelectPos = useImperativeLayoutEffect(() =>
|
||||
updateSelectPos(),
|
||||
);
|
||||
|
||||
const readState = (): InputState => {
|
||||
const state: InputState = {
|
||||
inputText,
|
||||
isComposing: composingRef.current,
|
||||
isDisabled,
|
||||
selection: selectionRef.current,
|
||||
hasSelection: selectionRef.current.start !== selectionRef.current.end,
|
||||
};
|
||||
return state;
|
||||
};
|
||||
const readStateRef = useRef<() => InputState>();
|
||||
readStateRef.current = readState;
|
||||
|
||||
useEffect(() => {
|
||||
if (!inputNativeCallbacks.getController) {
|
||||
return;
|
||||
}
|
||||
const controller: InputController = {
|
||||
requireSetMousePosition,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- .
|
||||
readState: () => readStateRef.current!(),
|
||||
setInputText: (updater: string | ((pre: string) => string)) => {
|
||||
setInputText(updater);
|
||||
waitAndUpdateSelectPos();
|
||||
},
|
||||
focus: () => {
|
||||
getTextarea()?.focus();
|
||||
},
|
||||
};
|
||||
inputNativeCallbacks.getController(controller);
|
||||
}, [inputNativeCallbacks.getController]);
|
||||
|
||||
const requireSetMousePosition = useImperativeLayoutEffect((pos: number) => {
|
||||
const textarea = getTextarea();
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
textarea.focus();
|
||||
textarea.setSelectionRange(pos, pos);
|
||||
});
|
||||
|
||||
const updateSelectPos = () => {
|
||||
const textarea = getTextarea();
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
selectionRef.current = { start, end };
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
if (!allowEmpty && !inputText.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDisabled) {
|
||||
return;
|
||||
}
|
||||
if (onBeforeSubmit && !onBeforeSubmit()) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit(inputText);
|
||||
|
||||
setInputText('');
|
||||
};
|
||||
|
||||
const onKeydownToSubmit = (evt: KeyboardGeneralEvent) => {
|
||||
if (evt.code !== 'Enter') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (composingRef.current || isPressEnterToChangeLine(evt)) {
|
||||
return;
|
||||
}
|
||||
evt.preventDefault();
|
||||
|
||||
submit();
|
||||
};
|
||||
|
||||
const onKeyUp: KeyboardEventHandler<HTMLTextAreaElement> = e => {
|
||||
updateSelectPos();
|
||||
inputNativeCallbacks.onAfterProcessKeyUp?.(e);
|
||||
};
|
||||
|
||||
const onKeyDown = (evt: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
updateSelectPos();
|
||||
const res = inputNativeCallbacks.onBeforeProcessKeyDown?.(evt);
|
||||
if (res?.exit) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((evt.metaKey || evt.altKey || evt.ctrlKey) && evt.code === 'Enter') {
|
||||
handleNewLine();
|
||||
return;
|
||||
}
|
||||
|
||||
onKeydownToSubmit(evt);
|
||||
};
|
||||
|
||||
const handleNewLine = () => {
|
||||
const textarea = getTextarea();
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算光标的当前位置
|
||||
const cursorPosition = textarea.selectionStart;
|
||||
|
||||
// 在光标位置插入新行
|
||||
const newValue = `${inputText.substring(
|
||||
0,
|
||||
cursorPosition,
|
||||
)}\n${inputText.substring(cursorPosition)}`;
|
||||
|
||||
setInputText(newValue);
|
||||
|
||||
setTimeout(() => {
|
||||
textarea.selectionStart = cursorPosition + 1;
|
||||
textarea.selectionEnd = cursorPosition + 1;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const onChange = (evt: { target: { value: string } }) => {
|
||||
updateSelectPos();
|
||||
const val = evt.target.value;
|
||||
setInputText(val);
|
||||
Promise.resolve().then(() => {
|
||||
inputNativeCallbacks?.onAfterOnChange?.();
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
onChange,
|
||||
setIsComposing: (
|
||||
_: CompositionEvent<HTMLTextAreaElement>,
|
||||
composing: boolean,
|
||||
) => {
|
||||
composingRef.current = composing;
|
||||
},
|
||||
submit,
|
||||
onKeyDown,
|
||||
inputText,
|
||||
setInputText,
|
||||
rcTextareaRef,
|
||||
updateSelectPos,
|
||||
onKeyUp,
|
||||
};
|
||||
};
|
||||
|
||||
const isPressEnterToChangeLine = (evt: KeyboardGeneralEvent) => {
|
||||
if (evt.code !== 'Enter') {
|
||||
return false;
|
||||
}
|
||||
return evt.shiftKey || evt.altKey || evt.metaKey;
|
||||
};
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import {
|
||||
FILE_TYPE_CONFIG,
|
||||
FileTypeEnum,
|
||||
} from '@coze-common/chat-core/shared/const';
|
||||
import { Toast, Upload } from '@coze-arch/coze-design';
|
||||
import {
|
||||
type IChatUploadCopywritingConfig,
|
||||
DEFAULT_MAX_FILE_SIZE,
|
||||
UploadType,
|
||||
} from '@coze-common/chat-uikit-shared';
|
||||
|
||||
interface IChatUploadProps {
|
||||
/**
|
||||
* 上传事件回调
|
||||
* @param uploadType 上传类型 [IMAGE=0 FILE=1]
|
||||
* @param file 文件
|
||||
* @returns void
|
||||
*/
|
||||
onUpload: (uploadType: UploadType, file: File) => void;
|
||||
/**
|
||||
* 文案信息配置
|
||||
*/
|
||||
copywritingConfig?: IChatUploadCopywritingConfig;
|
||||
/**
|
||||
* 文件最大尺寸(单位byte)
|
||||
*/
|
||||
maxFileSize?: number;
|
||||
isDisabled?: boolean;
|
||||
children: JSX.Element;
|
||||
limitFileCount?: number;
|
||||
isFileCountExceedsLimit: (fileCount: number) => boolean;
|
||||
}
|
||||
|
||||
const findFileTypeConfig = (file: File) =>
|
||||
FILE_TYPE_CONFIG.find(
|
||||
cnf => cnf.judge?.(file) || cnf.accept.some(ext => file.name.endsWith(ext)),
|
||||
);
|
||||
|
||||
export const ChatUpload: FC<IChatUploadProps> = props => {
|
||||
const {
|
||||
copywritingConfig = {},
|
||||
maxFileSize = DEFAULT_MAX_FILE_SIZE,
|
||||
children,
|
||||
onUpload,
|
||||
isDisabled,
|
||||
isFileCountExceedsLimit,
|
||||
limitFileCount = 1,
|
||||
} = props;
|
||||
|
||||
/**
|
||||
* 处理上传
|
||||
* @param fileList 文件List
|
||||
* @returns void
|
||||
*/
|
||||
const handleUpload = (fileList: File[]) => {
|
||||
const { fileSizeReachLimitToast, fileExceedsLimitToast, fileEmptyToast } =
|
||||
copywritingConfig;
|
||||
|
||||
if (isFileCountExceedsLimit(fileList.length)) {
|
||||
Toast.warning({
|
||||
showClose: false,
|
||||
content: fileExceedsLimitToast,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fileList.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 是否存在超出大小的文件
|
||||
const hasOverflowLimitFileSize = fileList.some(
|
||||
file => file.size > maxFileSize,
|
||||
);
|
||||
const hasEmptyFile = fileList.some(file => file.size <= 0);
|
||||
|
||||
// 文件大小超过预期大小的错误处理
|
||||
if (hasOverflowLimitFileSize) {
|
||||
Toast.warning({
|
||||
showClose: false,
|
||||
content: fileSizeReachLimitToast,
|
||||
});
|
||||
}
|
||||
|
||||
if (hasEmptyFile) {
|
||||
Toast.warning({
|
||||
showClose: false,
|
||||
content: fileEmptyToast,
|
||||
});
|
||||
}
|
||||
|
||||
const verifiedFileTypeConfigList = fileList
|
||||
.filter(file => file.size <= maxFileSize && file.size > 0)
|
||||
.map(file => ({
|
||||
file,
|
||||
fileTypeConfig: findFileTypeConfig(file),
|
||||
}));
|
||||
|
||||
for (const fileConfig of verifiedFileTypeConfigList) {
|
||||
if (fileConfig.fileTypeConfig?.fileType === FileTypeEnum.IMAGE) {
|
||||
onUpload?.(UploadType.IMAGE, fileConfig.file);
|
||||
} else {
|
||||
onUpload?.(UploadType.FILE, fileConfig.file);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Upload
|
||||
limit={limitFileCount === 1 ? 1 : undefined}
|
||||
draggable={false}
|
||||
action=""
|
||||
fileList={[]}
|
||||
onFileChange={handleUpload}
|
||||
disabled={isDisabled}
|
||||
multiple={limitFileCount > 1}
|
||||
>
|
||||
{children}
|
||||
</Upload>
|
||||
);
|
||||
};
|
||||
|
||||
ChatUpload.displayName = 'UiKitChatUpload';
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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 classNames from 'classnames';
|
||||
import { Divider, Typography } from '@coze-arch/coze-design';
|
||||
|
||||
interface ContextDividerProps {
|
||||
className?: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export const ContextDivider = ({ text, className }: ContextDividerProps) => (
|
||||
<Divider className={classNames(className, 'w-full my-24px ')} align="center">
|
||||
<Typography.Paragraph
|
||||
ellipsis={{
|
||||
showTooltip: {
|
||||
opts: {
|
||||
content: text,
|
||||
style: {
|
||||
wordBreak: 'break-word',
|
||||
},
|
||||
},
|
||||
},
|
||||
rows: 2,
|
||||
}}
|
||||
className="coz-fg-dim whitespace-pre-wrap text-center text-base leading-[16px] font-normal break-words"
|
||||
>
|
||||
{text}
|
||||
</Typography.Paragraph>
|
||||
</Divider>
|
||||
);
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './chat-input';
|
||||
export * from './chat-upload';
|
||||
export * from './on-boarding';
|
||||
export * from './stop-respond-button';
|
||||
export * from './thinking-placeholder';
|
||||
export * from './context-divider';
|
||||
export * from './with-rule-img-background';
|
||||
export * from './audio-record';
|
||||
|
||||
export type { UiKitChatInputButtonConfig } from './chat-input/index';
|
||||
@@ -0,0 +1,63 @@
|
||||
.chat-uikit-on-boarding {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
// max-width: 576px;
|
||||
height: 100%;
|
||||
padding: 0 10px;
|
||||
|
||||
&__bot {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
column-gap: 20px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
|
||||
&.chat-uikit-on-boarding__bot__with__onboarding {
|
||||
row-gap: 12px;
|
||||
// justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
&__prologue-sug {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
&__prologue {
|
||||
user-select: text;
|
||||
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
margin-top: 16px;
|
||||
|
||||
|
||||
font-style: normal;
|
||||
word-break: break-word;
|
||||
|
||||
}
|
||||
|
||||
&__suggestions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: flex-start;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// pc store页面开场白视觉与输入框对齐,整体左偏差
|
||||
.chat-uikit-on-boarding-pc {
|
||||
.chat-uikit-on-boarding__bot {
|
||||
padding-right: 14px;
|
||||
padding-left: 58px;
|
||||
}
|
||||
|
||||
.chat-uikit-on-boarding__prologue-sug {
|
||||
padding-right: 14px;
|
||||
padding-left: 58px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
* 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,
|
||||
useState,
|
||||
forwardRef,
|
||||
type FC,
|
||||
useContext,
|
||||
} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useUpdateEffect } from 'ahooks';
|
||||
import { Avatar, Typography } from '@coze-arch/coze-design';
|
||||
import {
|
||||
MdBoxLazy,
|
||||
type MdBoxLazyProps,
|
||||
} from '@coze-arch/bot-md-box-adapter/lazy';
|
||||
import {
|
||||
Layout,
|
||||
type IMessage,
|
||||
type IEventCallbacks,
|
||||
} from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { CozeLink } from '../../md-box-slots/link';
|
||||
import {
|
||||
type CozeImageProps,
|
||||
CozeImageWithPreview,
|
||||
} from '../../md-box-slots/coze-image';
|
||||
import { SuggestionItem } from '../../contents/suggestion-content/components/suggestion-item';
|
||||
import { OnboardingContext } from '../../../context/onboarding';
|
||||
import { NO_MESSAGE_ID_MARK } from '../../../constants/grab';
|
||||
import defaultAvatar from '../../../assets/default-square-avatar.png';
|
||||
import {
|
||||
typeSafeBotInfoNameVariants,
|
||||
type BotInfoVariantProps,
|
||||
} from './variants';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface OnBoardingProps {
|
||||
avatar?: string;
|
||||
name?: string;
|
||||
prologue?: string;
|
||||
suggestionList?: IMessage[];
|
||||
/**
|
||||
* suggestionList是否换行展示, 默认false
|
||||
*/
|
||||
suggestionsWrap?: boolean;
|
||||
readonly?: boolean;
|
||||
suggestionListWithString?: string[];
|
||||
/**
|
||||
* suggestionListWithString是否换行展示, 默认false
|
||||
*/
|
||||
suggestionsWithStringWrap?: boolean;
|
||||
onSuggestionClick?: (content: string) => void;
|
||||
className?: string;
|
||||
prologueClassName?: string;
|
||||
mdBoxProps?: Pick<MdBoxLazyProps, 'insertedElements' | 'slots'>;
|
||||
style?: CSSProperties;
|
||||
showBackground?: boolean;
|
||||
layout?: Layout;
|
||||
enableAutoSizeImage?: boolean;
|
||||
imageAutoSizeContainerWidth?: number;
|
||||
eventCallbacks?: IEventCallbacks;
|
||||
suggestionItemColor?: 'white' | 'grey';
|
||||
}
|
||||
|
||||
interface BotInfoProps {
|
||||
wrapperClassName?: string;
|
||||
avatar: string | undefined;
|
||||
onError: () => void;
|
||||
name: string | undefined;
|
||||
}
|
||||
|
||||
const BotInfo: React.FC<BotInfoProps & BotInfoVariantProps> = ({
|
||||
avatar,
|
||||
wrapperClassName,
|
||||
onError,
|
||||
name,
|
||||
showBackground,
|
||||
}) => (
|
||||
<div className={wrapperClassName}>
|
||||
<Avatar
|
||||
className={classNames('h-[64px] w-[64px]', 'rounded-[16px]')}
|
||||
src={avatar}
|
||||
shape="square"
|
||||
onError={onError}
|
||||
></Avatar>
|
||||
{name ? (
|
||||
<Typography.Text
|
||||
ellipsis
|
||||
className={typeSafeBotInfoNameVariants({
|
||||
showBackground: Boolean(showBackground),
|
||||
})}
|
||||
>
|
||||
{name}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const OnBoarding = forwardRef<HTMLDivElement, OnBoardingProps>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
avatar,
|
||||
name,
|
||||
prologue,
|
||||
suggestionList,
|
||||
readonly,
|
||||
suggestionListWithString,
|
||||
onSuggestionClick,
|
||||
className,
|
||||
prologueClassName,
|
||||
mdBoxProps,
|
||||
style,
|
||||
showBackground,
|
||||
layout,
|
||||
enableAutoSizeImage,
|
||||
imageAutoSizeContainerWidth,
|
||||
eventCallbacks,
|
||||
suggestionsWrap = false,
|
||||
suggestionsWithStringWrap = false,
|
||||
suggestionItemColor,
|
||||
} = props;
|
||||
const [botAvatar, setBotAvatar] = useState(avatar || defaultAvatar);
|
||||
const suggestions = suggestionList || suggestionListWithString;
|
||||
const isOnboardingEmpty = !prologue && !suggestions?.length;
|
||||
useUpdateEffect(() => {
|
||||
setBotAvatar(avatar || defaultAvatar);
|
||||
}, [avatar]);
|
||||
return (
|
||||
<OnboardingContext.Provider
|
||||
value={{
|
||||
imageAutoSizeContainerWidth,
|
||||
eventCallbacks,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
className={classNames('chat-uikit-on-boarding', className, {
|
||||
'chat-uikit-on-boarding-pc': layout === Layout.PC,
|
||||
})}
|
||||
style={style}
|
||||
>
|
||||
<BotInfo
|
||||
wrapperClassName={classNames(
|
||||
'chat-uikit-on-boarding__bot',
|
||||
!isOnboardingEmpty &&
|
||||
'chat-uikit-on-boarding__bot__with__onboarding',
|
||||
)}
|
||||
avatar={botAvatar}
|
||||
name={name}
|
||||
showBackground={showBackground}
|
||||
onError={() => setBotAvatar(defaultAvatar)}
|
||||
/>
|
||||
<div className={classNames('chat-uikit-on-boarding__prologue-sug')}>
|
||||
{prologue ? (
|
||||
<div
|
||||
className={classNames(
|
||||
[
|
||||
'py-12px',
|
||||
'px-16px',
|
||||
layout === Layout.MOBILE ? 'text-[16px]' : 'text-lg',
|
||||
'leading-[20px]',
|
||||
'rounded-normal',
|
||||
'bg-[var(--coz-mg-primary)]',
|
||||
],
|
||||
'chat-uikit-on-boarding__prologue',
|
||||
prologueClassName,
|
||||
{
|
||||
'!coz-bg-image-bots !coz-stroke-image-bots': showBackground,
|
||||
},
|
||||
)}
|
||||
data-grab-mark={NO_MESSAGE_ID_MARK}
|
||||
>
|
||||
<MdBoxLazy
|
||||
markDown={prologue}
|
||||
autoFixSyntax={{ autoFixEnding: false }}
|
||||
slots={{
|
||||
Image: enableAutoSizeImage
|
||||
? CozeImageWithSizeProps
|
||||
: undefined,
|
||||
Link: CozeLink,
|
||||
}}
|
||||
{...mdBoxProps}
|
||||
></MdBoxLazy>
|
||||
</div>
|
||||
) : null}
|
||||
{Boolean(suggestionList?.length) && (
|
||||
<div
|
||||
className={classNames(
|
||||
'chat-uikit-on-boarding__suggestions',
|
||||
'mt-8px',
|
||||
{
|
||||
'flex-wrap !flex-row gap-2': suggestionsWrap,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{suggestionList?.map((message, index) => (
|
||||
<SuggestionItem
|
||||
key={index}
|
||||
className={classNames({
|
||||
'!mb-0': suggestionsWrap,
|
||||
})}
|
||||
message={message}
|
||||
readonly={readonly}
|
||||
onSuggestionClick={({ text }) => onSuggestionClick?.(text)}
|
||||
showBackground={showBackground}
|
||||
color={suggestionItemColor}
|
||||
></SuggestionItem>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{Boolean(suggestionListWithString?.length) && (
|
||||
<div
|
||||
className={classNames(
|
||||
'chat-uikit-on-boarding__suggestions',
|
||||
'mt-8px',
|
||||
{
|
||||
'flex-wrap !flex-row gap-2': suggestionsWithStringWrap,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{suggestionListWithString?.map((content, index) => (
|
||||
<SuggestionItem
|
||||
key={index}
|
||||
className={classNames({
|
||||
'!mb-0': suggestionsWithStringWrap,
|
||||
})}
|
||||
content={content}
|
||||
readonly={readonly}
|
||||
onSuggestionClick={({ text }) => onSuggestionClick?.(text)}
|
||||
showBackground={showBackground}
|
||||
color={suggestionItemColor}
|
||||
></SuggestionItem>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</OnboardingContext.Provider>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const CozeImageWithSizeProps: FC<CozeImageProps> = props => {
|
||||
const { imageAutoSizeContainerWidth } = useContext(OnboardingContext);
|
||||
return (
|
||||
<CozeImageWithPreview
|
||||
{...props}
|
||||
imageAutoSizeContainerWidth={imageAutoSizeContainerWidth}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
CozeImageWithSizeProps.displayName = 'CozeImageWithSizeProps';
|
||||
@@ -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 { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
export const botInfoNameVariants = cva(
|
||||
['leading-[28px]', 'font-medium', 'text-20px'],
|
||||
{
|
||||
variants: {
|
||||
showBackground: {
|
||||
true: ['coz-fg-images-user-name'],
|
||||
false: ['coz-fg-plus'],
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export type BotInfoVariantProps = VariantProps<typeof botInfoNameVariants>;
|
||||
|
||||
export const typeSafeBotInfoNameVariants: (
|
||||
props: BotInfoVariantProps,
|
||||
) => string = botInfoNameVariants;
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { IconCozStopCircle } from '@coze-arch/coze-design/icons';
|
||||
import { Button } from '@coze-arch/coze-design';
|
||||
|
||||
interface IProps {
|
||||
className?: string;
|
||||
content: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const StopRespondButton: FC<IProps> = props => {
|
||||
const { content, onClick, className } = props;
|
||||
return (
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={onClick}
|
||||
className={classNames(
|
||||
'coz-stroke-primary',
|
||||
'coz-fg-primary',
|
||||
'border-[1px]',
|
||||
'border-solid',
|
||||
'coz-shadow-default',
|
||||
className,
|
||||
)}
|
||||
icon={<IconCozStopCircle />}
|
||||
>
|
||||
{content}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
StopRespondButton.displayName = 'StopRespondButton';
|
||||
@@ -0,0 +1,62 @@
|
||||
@keyframes chat-uikit-thinking-placeholder-animation {
|
||||
0% {
|
||||
background-color: var(--coz-fg-plus);
|
||||
}
|
||||
|
||||
50% {
|
||||
background-color: var(--coz-fg-dim);
|
||||
}
|
||||
|
||||
100% {
|
||||
background-color: var(--coz-fg-plus);
|
||||
}
|
||||
}
|
||||
|
||||
.chat-uikit-coz-thinking-placeholder {
|
||||
position: relative;
|
||||
|
||||
overflow: visible;
|
||||
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin: 0 10px;
|
||||
|
||||
@apply coz-fg-dim;
|
||||
|
||||
background-color: var(--coz-fg-dim);
|
||||
border-radius: 100%;
|
||||
|
||||
animation: chat-uikit-thinking-placeholder-animation 0.8s infinite alternate;
|
||||
animation-timing-function: ease;
|
||||
animation-delay: -0.2s;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
display: inline-block;
|
||||
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
|
||||
|
||||
background-color: var(--coz-fg-dim);
|
||||
border-radius: 100%;
|
||||
|
||||
animation: chat-uikit-thinking-placeholder-animation 0.8s infinite alternate;
|
||||
animation-timing-function: ease;
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: -10px;
|
||||
animation-delay: -0.4s;
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: 10px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
}
|
||||
@@ -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 { type FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {
|
||||
type ThinkingPlaceholderVariantProps,
|
||||
typeSafeThinkingPlaceholderVariants,
|
||||
} from './variant';
|
||||
import { type IThinkingPlaceholderProps } from './type';
|
||||
import './animation.less';
|
||||
|
||||
const getVariantByProps = ({
|
||||
theme,
|
||||
showBackground,
|
||||
}: {
|
||||
theme: IThinkingPlaceholderProps['theme'];
|
||||
showBackground: boolean;
|
||||
}): ThinkingPlaceholderVariantProps => {
|
||||
if (showBackground) {
|
||||
return { backgroundColor: 'withBackground' };
|
||||
}
|
||||
if (!theme) {
|
||||
return { backgroundColor: null };
|
||||
}
|
||||
return { backgroundColor: theme };
|
||||
};
|
||||
|
||||
export const ThinkingPlaceholder: FC<IThinkingPlaceholderProps> = props => {
|
||||
const { className, theme = 'none', showBackground } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
typeSafeThinkingPlaceholderVariants(
|
||||
getVariantByProps({ showBackground: Boolean(showBackground), theme }),
|
||||
),
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="chat-uikit-coz-thinking-placeholder"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export interface IThinkingPlaceholderProps {
|
||||
className?: string;
|
||||
theme?: 'primary' | 'whiteness' | 'none' | 'grey';
|
||||
showBackground?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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 { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
const thinkingPlaceholderVariants = cva(
|
||||
[
|
||||
'h-[44px]',
|
||||
'w-fit',
|
||||
'flex',
|
||||
'justify-center',
|
||||
'items-center',
|
||||
'py-12px',
|
||||
'px-16px',
|
||||
'rounded-normal',
|
||||
],
|
||||
{
|
||||
variants: {
|
||||
backgroundColor: {
|
||||
whiteness: ['bg-[var(--coz-mg-card)]'],
|
||||
grey: ['bg-[var(--coz-mg-primary)]'],
|
||||
primary: ['bg-[var(coz-mg-hglt-plus)]'],
|
||||
withBackground: ['coz-bg-image-bots', 'coz-stroke-image-bots'],
|
||||
none: ['coz-stroke-primary'],
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export type ThinkingPlaceholderVariantProps = Required<
|
||||
VariantProps<typeof thinkingPlaceholderVariants>
|
||||
>;
|
||||
export const typeSafeThinkingPlaceholderVariants: (
|
||||
props: ThinkingPlaceholderVariantProps,
|
||||
) => string = thinkingPlaceholderVariants;
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const MODE_CONFIG = {
|
||||
pc: {
|
||||
size: {
|
||||
width: 486,
|
||||
height: 346,
|
||||
},
|
||||
centerWidth: 346,
|
||||
},
|
||||
mobile: {
|
||||
size: {
|
||||
width: 248,
|
||||
height: 346,
|
||||
},
|
||||
centerWidth: 206,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useRef } from 'react';
|
||||
|
||||
import { useSize } from 'ahooks';
|
||||
|
||||
import { getStandardRatio } from '../utils';
|
||||
import { type BackgroundImageInfo } from '../types';
|
||||
import { MODE_CONFIG } from '../const';
|
||||
|
||||
export const useGetResponsiveBackgroundInfo = ({
|
||||
backgroundInfo,
|
||||
}: {
|
||||
backgroundInfo?: BackgroundImageInfo;
|
||||
}) => {
|
||||
const targetRef = useRef(null);
|
||||
|
||||
const size = useSize(targetRef);
|
||||
const { width = 0, height = 0 } = size ?? {};
|
||||
|
||||
const isMobileMode = width / height <= getStandardRatio('mobile');
|
||||
|
||||
const mobileBackgroundInfo = backgroundInfo?.mobile_background_image;
|
||||
const pcBackgroundInfo = backgroundInfo?.web_background_image;
|
||||
|
||||
const currentBackgroundInfo = isMobileMode
|
||||
? mobileBackgroundInfo
|
||||
: pcBackgroundInfo;
|
||||
|
||||
const { theme_color } = currentBackgroundInfo ?? {};
|
||||
const { size: cropperSize } = MODE_CONFIG[isMobileMode ? 'mobile' : 'pc'];
|
||||
|
||||
return {
|
||||
targetRef,
|
||||
currentBackgroundInfo,
|
||||
targetWidth: width,
|
||||
targetHeight: height,
|
||||
currentThemeColor: theme_color,
|
||||
cropperSize,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* 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, useEffect } from 'react';
|
||||
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { addAlpha, computeShowGradient } from './utils';
|
||||
import {
|
||||
type CanvasPosition,
|
||||
type GradientPosition,
|
||||
type BackgroundImageInfo,
|
||||
} from './types';
|
||||
import { useGetResponsiveBackgroundInfo } from './hooks/use-get-background-info';
|
||||
|
||||
export interface WithRuleImgBackgroundProps {
|
||||
preview?: boolean;
|
||||
backgroundInfo?: BackgroundImageInfo;
|
||||
onError?: () => void;
|
||||
}
|
||||
|
||||
export const Gradient: React.FC<{
|
||||
position: number;
|
||||
preview: boolean;
|
||||
showGradient: boolean;
|
||||
background: string;
|
||||
direction: 'left' | 'right';
|
||||
}> = ({ position, preview, showGradient, background, direction }) => (
|
||||
<div
|
||||
className={classNames('absolute -translate-y-1/2 top-1/2 h-full z-10', {
|
||||
'transition-all duration-500': !preview,
|
||||
})}
|
||||
style={{
|
||||
[direction]: `${(position > 0 ? position : 0) * 100 - 0.1}%`, // 0.1为阴影补偿,防止出现间隙
|
||||
width: '10%',
|
||||
background,
|
||||
opacity: showGradient ? 1 : 0,
|
||||
}}
|
||||
></div>
|
||||
);
|
||||
const getGradient = (
|
||||
gradient: GradientPosition,
|
||||
canvasData: CanvasPosition,
|
||||
cropperWidth = 1,
|
||||
) => {
|
||||
const { left: cropperImgLeft = 0, width: cropperImgWidth = 0 } = canvasData;
|
||||
// 伪裁剪 兼容历史gradient不准的问题
|
||||
if (!isEmpty(canvasData)) {
|
||||
return {
|
||||
left: cropperImgLeft / cropperWidth,
|
||||
right: (cropperWidth - cropperImgWidth - cropperImgLeft) / cropperWidth,
|
||||
};
|
||||
} else {
|
||||
return gradient;
|
||||
}
|
||||
};
|
||||
export const WithRuleImgBackground: React.FC<WithRuleImgBackgroundProps> = ({
|
||||
preview = false,
|
||||
backgroundInfo,
|
||||
}) => {
|
||||
const {
|
||||
currentBackgroundInfo,
|
||||
targetHeight,
|
||||
targetWidth,
|
||||
targetRef,
|
||||
cropperSize,
|
||||
} = useGetResponsiveBackgroundInfo({
|
||||
backgroundInfo,
|
||||
});
|
||||
|
||||
const {
|
||||
theme_color,
|
||||
gradient_position = {},
|
||||
canvas_position = {},
|
||||
} = currentBackgroundInfo ?? {};
|
||||
const { left: gradientLeft = 0, right: gradientRight = 0 } = getGradient(
|
||||
gradient_position,
|
||||
canvas_position,
|
||||
cropperSize.width,
|
||||
);
|
||||
const { top: cropperImgTop = 0, height: cropperImgHeight = 0 } =
|
||||
canvas_position;
|
||||
|
||||
const [themeColor, setThemeColor] = useState(theme_color ?? 'transparent');
|
||||
|
||||
// 与裁剪框等比例算出图片渲染区域的宽度
|
||||
const imgWidth = (targetHeight * cropperSize.width) / cropperSize.height;
|
||||
|
||||
const mediumColor = addAlpha(themeColor, 0.95);
|
||||
|
||||
useEffect(() => {
|
||||
if (theme_color) {
|
||||
setThemeColor(theme_color);
|
||||
}
|
||||
}, [currentBackgroundInfo]);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="chat.with_rule_img_background"
|
||||
ref={targetRef}
|
||||
className={
|
||||
'rule-img-background absolute left-1/2 -translate-x-1/2 w-full h-full overflow-hidden pointer-events-none'
|
||||
}
|
||||
style={{
|
||||
background: preview ? 'none' : themeColor,
|
||||
zIndex: preview ? 100 : 0,
|
||||
}}
|
||||
>
|
||||
{/* 背景图上压黑阴影 */}
|
||||
<div className="bg-[rgba(0,0,0,0.12)] absolute w-full h-full z-[200] rounded-t-[6px]"></div>
|
||||
|
||||
<div className="relative w-fit h-fit left-1/2 -translate-x-1/2">
|
||||
{
|
||||
<Gradient
|
||||
preview={preview}
|
||||
showGradient={computeShowGradient(
|
||||
targetWidth,
|
||||
imgWidth,
|
||||
gradientLeft,
|
||||
)}
|
||||
position={gradientLeft}
|
||||
direction="left"
|
||||
background={`linear-gradient(90deg, ${themeColor} 10%, ${mediumColor} 28%, transparent 92.4%)`}
|
||||
/>
|
||||
}
|
||||
{preview ? (
|
||||
<div
|
||||
style={{
|
||||
height: targetHeight,
|
||||
width: targetWidth,
|
||||
}}
|
||||
></div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
width: imgWidth,
|
||||
height: targetHeight,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={currentBackgroundInfo?.origin_image_url}
|
||||
alt=""
|
||||
style={{
|
||||
height: `${(cropperImgHeight / cropperSize.height) * 100}%`,
|
||||
position: 'absolute',
|
||||
left: `${
|
||||
gradientLeft ? gradientLeft * 100 : -gradientRight * 2 * 100
|
||||
}%`,
|
||||
top: `${(cropperImgTop / cropperSize.height) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{
|
||||
<Gradient
|
||||
preview={preview}
|
||||
showGradient={computeShowGradient(
|
||||
targetWidth,
|
||||
imgWidth,
|
||||
gradientRight,
|
||||
)}
|
||||
position={gradientRight}
|
||||
direction="right"
|
||||
background={`linear-gradient(90deg, transparent 10% , ${mediumColor} 72%, ${themeColor} 92%)`}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export interface GradientPosition {
|
||||
left?: number;
|
||||
right?: number;
|
||||
}
|
||||
|
||||
export interface CanvasPosition {
|
||||
width?: number;
|
||||
height?: number;
|
||||
left?: number;
|
||||
top?: number;
|
||||
}
|
||||
|
||||
export interface BackgroundImageDetail {
|
||||
/** 原始图片 */
|
||||
origin_image_uri?: string;
|
||||
origin_image_url?: string;
|
||||
/** 实际使用图片 */
|
||||
image_uri?: string;
|
||||
image_url?: string;
|
||||
theme_color?: string;
|
||||
/** 渐变位置 */
|
||||
gradient_position?: GradientPosition;
|
||||
/** 裁剪画布位置 */
|
||||
canvas_position?: CanvasPosition;
|
||||
}
|
||||
|
||||
export interface BackgroundImageInfo {
|
||||
/** web端背景图 */
|
||||
web_background_image?: BackgroundImageDetail;
|
||||
/** 移动端背景图 */
|
||||
mobile_background_image?: BackgroundImageDetail;
|
||||
}
|
||||
@@ -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 { MODE_CONFIG } from './const';
|
||||
|
||||
// 输入透明度系数 和color 返回新的颜色
|
||||
export function addAlpha(color: string, alpha: number): string {
|
||||
const regex = /^rgba\((\d{1,3}),(\d{1,3}),(\d{1,3})\)$/;
|
||||
if (!regex.test(color)) {
|
||||
return color;
|
||||
}
|
||||
|
||||
const values: string[] = color.slice(5, -1).split(',');
|
||||
values.push(alpha.toString());
|
||||
|
||||
const newColor = `rgba(${values.join(',')})`;
|
||||
|
||||
return newColor;
|
||||
}
|
||||
|
||||
// 图片的宽高比
|
||||
export const getStandardRatio = (mode: 'pc' | 'mobile'): number =>
|
||||
MODE_CONFIG[mode].size.width / MODE_CONFIG[mode].size.height;
|
||||
|
||||
// 计算是否展示渐变阴影 = 屏幕宽度 > 图片宽度 * (1- 2 * 左/右阴影位置)
|
||||
export const computeShowGradient = (
|
||||
width: number,
|
||||
imgWidth: number,
|
||||
percent: number,
|
||||
): boolean => width > imgWidth * (1 - (percent > 0 ? percent : 0) * 2);
|
||||
@@ -0,0 +1,86 @@
|
||||
.button {
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
padding: 12px;
|
||||
|
||||
color: #4D53E8;
|
||||
|
||||
background: #FFF;
|
||||
border: 1px solid rgba(10, 17, 61, 6%);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: linear-gradient(0deg, rgba(0, 0, 0, 4%) 0%, rgba(0, 0, 0, 4%) 100%), #FFF;
|
||||
}
|
||||
|
||||
.button:active {
|
||||
background: linear-gradient(0deg, rgba(0, 0, 0, 8%) 0%, rgba(0, 0, 0, 8%) 100%), #FFF;
|
||||
}
|
||||
|
||||
.button-text {
|
||||
margin-left: 8px;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
color: #4D53E8;
|
||||
}
|
||||
|
||||
.outlined-icon-button {
|
||||
@apply !coz-bg-max;
|
||||
|
||||
&:hover {
|
||||
@apply !coz-mg-secondary-hovered;
|
||||
}
|
||||
|
||||
&:active {
|
||||
@apply !coz-mg-secondary-pressed;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@apply !coz-fg-dim bg-transparent;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
@apply !coz-fg-dim bg-transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.outlined-icon-button-background {
|
||||
&:hover {
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
background: rgba(249, 249, 249, 90%) !important;
|
||||
}
|
||||
|
||||
&:active {
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
background: rgba(249, 249, 249, 85%) !important;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@apply !coz-fg-dim bg-transparent;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
@apply !coz-fg-dim bg-transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.base-outlined-icon-button {
|
||||
@apply !coz-fg-primary !coz-stroke-plus;
|
||||
|
||||
width: 32px;
|
||||
// 和 ui 确认后的业务定制样式
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
border-style: solid !important;
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
border-width: 1px !important;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { IconButton, type ButtonProps } from '@coze-arch/coze-design';
|
||||
import { type Button as SemiButton } from '@douyinfe/semi-ui';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const OutlinedIconButton = forwardRef<
|
||||
SemiButton,
|
||||
ButtonProps & { showBackground: boolean }
|
||||
>(({ className, showBackground, ...restProps }, ref) => (
|
||||
<IconButton
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
className,
|
||||
showBackground
|
||||
? ['!coz-bg-image-bots', styles['outlined-icon-button-background']]
|
||||
: styles['outlined-icon-button'],
|
||||
styles['base-outlined-icon-button'],
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
));
|
||||
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReactNode, type FC } from 'react';
|
||||
|
||||
import {
|
||||
type IEventCallbacks,
|
||||
ContentBoxType,
|
||||
type MdBoxProps,
|
||||
type GetBotInfo,
|
||||
type Layout,
|
||||
type IContentConfigs,
|
||||
type IMessage,
|
||||
} from '@coze-common/chat-uikit-shared';
|
||||
import { ContentType } from '@coze-common/chat-core';
|
||||
|
||||
import { TextContent } from '../../contents/text-content';
|
||||
import { SingleImageContentWithAutoSize } from '../../contents/single-image-content/auto-size';
|
||||
import { SingleImageContent } from '../../contents/single-image-content';
|
||||
import { SimpleFunctionContent } from '../../contents/simple-function-content';
|
||||
import { PlainTextContent } from '../../contents/plain-text-content';
|
||||
import { MultimodalContent } from '../../contents/multimodal-content';
|
||||
import { ImageContent } from '../../contents/image-content';
|
||||
import { FileContent } from '../../contents/file-content';
|
||||
import { isImage } from '../../../utils/is-image';
|
||||
import { defaultEnable } from '../../../utils/default-enable';
|
||||
import { MESSAGE_TYPE_VALID_IN_TEXT_LIST } from '../../../constants/content-box';
|
||||
|
||||
export interface EnhancedContentConfig {
|
||||
rule: (params: {
|
||||
message: IMessage;
|
||||
contentType: ContentType;
|
||||
contentConfigs: IContentConfigs | undefined;
|
||||
}) => boolean;
|
||||
render: (params: {
|
||||
message: IMessage;
|
||||
eventCallbacks: IEventCallbacks | undefined;
|
||||
contentConfigs: IContentConfigs | undefined;
|
||||
options: {
|
||||
isCardDisabled?: boolean;
|
||||
isContentLoading?: boolean;
|
||||
showBackground: boolean;
|
||||
readonly?: boolean;
|
||||
};
|
||||
}) => ReactNode;
|
||||
}
|
||||
|
||||
export interface IContentBoxProps {
|
||||
/**
|
||||
* Core SDK的消息体内容
|
||||
*/
|
||||
message: IMessage;
|
||||
/**
|
||||
* 事件回调对象
|
||||
*/
|
||||
eventCallbacks?: IEventCallbacks;
|
||||
/**
|
||||
* content卡片配置的内容
|
||||
*/
|
||||
contentConfigs?: IContentConfigs;
|
||||
/**
|
||||
* 是否只读
|
||||
*/
|
||||
readonly?: boolean;
|
||||
getBotInfo: GetBotInfo;
|
||||
layout: Layout;
|
||||
/**
|
||||
* 在 mix 模式下,给 text 格式卡片加插槽
|
||||
*/
|
||||
multimodalTextContentAddonTop?: ReactNode;
|
||||
showBackground: boolean;
|
||||
/**
|
||||
* 启用自动适应图片能力
|
||||
*/
|
||||
enableAutoSizeImage?: boolean;
|
||||
|
||||
/**
|
||||
* mdBox的配置
|
||||
*/
|
||||
mdBoxProps?: MdBoxProps;
|
||||
/**
|
||||
* 卡片状态是否为disabled
|
||||
*/
|
||||
isCardDisabled?: boolean;
|
||||
isContentLoading?: boolean;
|
||||
enhancedContentConfigList?: EnhancedContentConfig[];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity, @coze-arch/max-line-per-function, @coze-arch/max-line-per-function
|
||||
export const ContentBox: FC<IContentBoxProps> = props => {
|
||||
const {
|
||||
message,
|
||||
contentConfigs,
|
||||
readonly,
|
||||
getBotInfo,
|
||||
layout,
|
||||
showBackground,
|
||||
enableAutoSizeImage,
|
||||
isCardDisabled,
|
||||
isContentLoading,
|
||||
enhancedContentConfigList,
|
||||
} = props;
|
||||
/**
|
||||
* Content内容启用配置 Start
|
||||
*/
|
||||
const isTextEnable = defaultEnable(
|
||||
contentConfigs?.[ContentBoxType.TEXT]?.enable,
|
||||
);
|
||||
const isImageEnable = defaultEnable(
|
||||
contentConfigs?.[ContentBoxType.IMAGE]?.enable,
|
||||
);
|
||||
const isFileEnable = contentConfigs?.[ContentBoxType.FILE]?.enable;
|
||||
|
||||
const isSimpleFunctionEnable =
|
||||
contentConfigs?.[ContentBoxType.SIMPLE_FUNCTION]?.enable;
|
||||
/**
|
||||
* Content内容启用配置 End
|
||||
*/
|
||||
|
||||
const enhancedContentConfig = enhancedContentConfigList?.find(config =>
|
||||
config.rule({ contentType: message.content_type, contentConfigs, message }),
|
||||
);
|
||||
|
||||
if (enhancedContentConfig) {
|
||||
return enhancedContentConfig.render({
|
||||
message,
|
||||
eventCallbacks: props.eventCallbacks,
|
||||
contentConfigs,
|
||||
options: { isCardDisabled, isContentLoading, showBackground, readonly },
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 文本类型的处理
|
||||
* 这里目前有两种情况,第一种message.type = 'follow_up' 代表是suggestion 第二种反之是普通文本消息
|
||||
*/
|
||||
if (message.content_type === ContentType.Text) {
|
||||
const { eventCallbacks, mdBoxProps } = props;
|
||||
const { onImageClick, onLinkClick } = eventCallbacks ?? {};
|
||||
if (
|
||||
MESSAGE_TYPE_VALID_IN_TEXT_LIST.includes(message.type) &&
|
||||
isTextEnable
|
||||
) {
|
||||
return message.role === 'user' ? (
|
||||
<PlainTextContent
|
||||
isContentLoading={isContentLoading}
|
||||
content={message.content}
|
||||
getBotInfo={getBotInfo}
|
||||
mentioned={message.mention_list.at(0)}
|
||||
/>
|
||||
) : (
|
||||
<TextContent
|
||||
message={message}
|
||||
readonly={readonly}
|
||||
onImageClick={onImageClick}
|
||||
onLinkClick={onLinkClick}
|
||||
enableAutoSizeImage={enableAutoSizeImage}
|
||||
mdBoxProps={mdBoxProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FIle类型的内容
|
||||
*/
|
||||
if (message.content_type === ContentType.File && isFileEnable) {
|
||||
const { copywriting, fileAttributeKeys } =
|
||||
contentConfigs[ContentBoxType.FILE] ?? {};
|
||||
const { eventCallbacks } = props;
|
||||
const { onCancelUpload, onCopyUpload, onRetryUpload } =
|
||||
eventCallbacks ?? {};
|
||||
return (
|
||||
<FileContent
|
||||
message={message}
|
||||
copywriting={copywriting}
|
||||
fileAttributeKeys={fileAttributeKeys}
|
||||
readonly={readonly}
|
||||
onCancel={onCancelUpload}
|
||||
onCopy={onCopyUpload}
|
||||
onRetry={onRetryUpload}
|
||||
layout={layout}
|
||||
showBackground={showBackground}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片类型的内容
|
||||
*/
|
||||
if (message.content_type === ContentType.Image && isImageEnable) {
|
||||
const { eventCallbacks } = props;
|
||||
const { onImageClick } = eventCallbacks ?? {};
|
||||
|
||||
if (!isImage(message.content_obj)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const UsedSingleImageContent = enableAutoSizeImage
|
||||
? SingleImageContentWithAutoSize
|
||||
: SingleImageContent;
|
||||
|
||||
const isMultipleImage = message.content_obj.image_list.length > 1;
|
||||
|
||||
if (isMultipleImage) {
|
||||
return <ImageContent message={message} onImageClick={onImageClick} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<UsedSingleImageContent message={message} onImageClick={onImageClick} />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* function类型
|
||||
*/
|
||||
if (message.type === 'function_call' && isSimpleFunctionEnable) {
|
||||
const { copywriting } =
|
||||
contentConfigs[ContentBoxType.SIMPLE_FUNCTION] ?? {};
|
||||
|
||||
return (
|
||||
<SimpleFunctionContent message={message} copywriting={copywriting} />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件文字同时发送的多模态消息
|
||||
*/
|
||||
if (
|
||||
message.content_type === ContentType.Mix &&
|
||||
isFileEnable &&
|
||||
isImageEnable &&
|
||||
isTextEnable
|
||||
) {
|
||||
const { copywriting, fileAttributeKeys } =
|
||||
contentConfigs[ContentBoxType.FILE] ?? {};
|
||||
const { eventCallbacks } = props;
|
||||
const { onCancelUpload, onCopyUpload, onRetryUpload, onImageClick } =
|
||||
eventCallbacks ?? {};
|
||||
return (
|
||||
<MultimodalContent
|
||||
isContentLoading={isContentLoading}
|
||||
renderTextContentAddonTop={props.multimodalTextContentAddonTop}
|
||||
message={message}
|
||||
getBotInfo={getBotInfo}
|
||||
fileAttributeKeys={fileAttributeKeys}
|
||||
copywriting={copywriting}
|
||||
readonly={readonly}
|
||||
onCancel={onCancelUpload}
|
||||
onCopy={onCopyUpload}
|
||||
onRetry={onRetryUpload}
|
||||
onImageClick={onImageClick}
|
||||
layout={layout}
|
||||
showBackground={showBackground}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <span>Not Support {message.content_type} Content</span>;
|
||||
};
|
||||
|
||||
ContentBox.displayName = 'UIKitContentBox';
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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 ComponentPropsWithRef, type FC } from 'react';
|
||||
|
||||
import { MdBoxLazy } from '@coze-arch/bot-md-box-adapter/lazy';
|
||||
|
||||
import { CozeLink } from '../../md-box-slots/link';
|
||||
import { CozeImage } from '../../md-box-slots/coze-image';
|
||||
|
||||
export const LazyCozeMdBox: FC<
|
||||
ComponentPropsWithRef<typeof MdBoxLazy>
|
||||
> = props => (
|
||||
<MdBoxLazy
|
||||
slots={{
|
||||
Image: CozeImage,
|
||||
Link: CozeLink,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
.full-width-aligner {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.full-width-aligner-inner-wrap {
|
||||
pointer-events: auto;
|
||||
}
|
||||
@@ -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 type { PropsWithChildren } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import './index.less';
|
||||
|
||||
/**
|
||||
* 套壳组件,默认宽度通栏,用于帮助孤立组件与 message box 保持宽度对齐
|
||||
*/
|
||||
export const FullWidthAligner = (
|
||||
props: PropsWithChildren<{
|
||||
alignWidth?: string;
|
||||
className?: string;
|
||||
innerWrapClassName?: string;
|
||||
}>,
|
||||
) => {
|
||||
const { alignWidth, children, className, innerWrapClassName } = props;
|
||||
return (
|
||||
<div
|
||||
className={classNames('full-width-aligner', className)}
|
||||
style={{
|
||||
width: alignWidth || '100%',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={classNames(
|
||||
'full-width-aligner-inner-wrap',
|
||||
innerWrapClassName,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FullWidthAligner.displayName = 'UIKitFullWidthAligner';
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './content-box';
|
||||
export * from './tooltips';
|
||||
export * from './message-box';
|
||||
export * from './button';
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type PropsWithChildren, type FC } from 'react';
|
||||
export const DefaultAvatarWrap: FC<PropsWithChildren> = ({ children }) => (
|
||||
<>{children}</>
|
||||
);
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useRef, useEffect, type ComponentType } from 'react';
|
||||
|
||||
import { type FallbackProps } from '@coze-arch/logger';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import { useUiKitMessageBoxContext } from '../../../context/message-box';
|
||||
export const FallbackComponent: ComponentType<FallbackProps> = ({ error }) => {
|
||||
const { onError } = useUiKitMessageBoxContext();
|
||||
|
||||
const reported = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!onError || !error) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (reported.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
onError(error);
|
||||
reported.current = true;
|
||||
}, [onError, error]);
|
||||
|
||||
return (
|
||||
<div className="p-[12px]">
|
||||
<span className="text-[14px] font-medium text-[#222222]">
|
||||
{I18n.t('message_content_error')}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import { Layout } from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { ContentBox } from '../content-box';
|
||||
import {
|
||||
type MessageBoxProps,
|
||||
type MessageBoxShellProps,
|
||||
type NormalMessageBoxProps,
|
||||
} from './type';
|
||||
import { MessageBoxWrap } from './message-box-wrap';
|
||||
|
||||
export const MessageBox: FC<
|
||||
MessageBoxShellProps | NormalMessageBoxProps
|
||||
> = props => {
|
||||
const {
|
||||
theme = 'none',
|
||||
renderFooter,
|
||||
hoverContent,
|
||||
senderInfo,
|
||||
showUserInfo,
|
||||
right,
|
||||
classname,
|
||||
|
||||
messageBubbleClassname,
|
||||
messageBubbleWrapperClassname,
|
||||
messageBoxWraperClassname,
|
||||
messageErrorWrapperClassname,
|
||||
isHoverShowUserInfo,
|
||||
|
||||
layout = Layout.PC,
|
||||
showBackground = false,
|
||||
topRightSlot,
|
||||
imageAutoSizeContainerWidth,
|
||||
enableImageAutoSize,
|
||||
messageId,
|
||||
eventCallbacks,
|
||||
onError,
|
||||
} = props ?? {};
|
||||
const { url, nickname, id, userLabel, userUniqueName } = senderInfo ?? {};
|
||||
|
||||
return (
|
||||
<MessageBoxWrap
|
||||
messageId={messageId}
|
||||
theme={theme}
|
||||
avatar={url}
|
||||
nickname={nickname}
|
||||
showUserInfo={showUserInfo}
|
||||
renderFooter={renderFooter}
|
||||
hoverContent={hoverContent}
|
||||
right={right}
|
||||
senderId={id || ''}
|
||||
classname={classname}
|
||||
messageBubbleWrapperClassname={messageBubbleWrapperClassname}
|
||||
messageBubbleClassname={messageBubbleClassname}
|
||||
messageBoxWraperClassname={messageBoxWraperClassname}
|
||||
messageErrorWrapperClassname={messageErrorWrapperClassname}
|
||||
isHoverShowUserInfo={isHoverShowUserInfo}
|
||||
layout={layout}
|
||||
contentTime={getMessageContentTime(props)}
|
||||
showBackground={showBackground}
|
||||
extendedUserInfo={{
|
||||
userLabel,
|
||||
userUniqueName,
|
||||
}}
|
||||
topRightSlot={topRightSlot}
|
||||
imageAutoSizeContainerWidth={imageAutoSizeContainerWidth}
|
||||
enableImageAutoSize={enableImageAutoSize}
|
||||
eventCallbacks={eventCallbacks}
|
||||
onError={onError}
|
||||
>
|
||||
{getMessageBoxContent(props)}
|
||||
</MessageBoxWrap>
|
||||
);
|
||||
};
|
||||
|
||||
const getMessageContentTime = (props: MessageBoxProps): number | undefined => {
|
||||
if ('message' in props) {
|
||||
return Number(props.message.content_time);
|
||||
}
|
||||
};
|
||||
|
||||
const getMessageBoxContent = (props: MessageBoxProps) => {
|
||||
if ('children' in props) {
|
||||
return props.children;
|
||||
}
|
||||
|
||||
const {
|
||||
message,
|
||||
contentConfigs,
|
||||
eventCallbacks,
|
||||
getBotInfo,
|
||||
layout = Layout.PC,
|
||||
showBackground = false,
|
||||
isContentLoading,
|
||||
isCardDisabled,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<ContentBox
|
||||
message={message}
|
||||
contentConfigs={contentConfigs}
|
||||
eventCallbacks={eventCallbacks}
|
||||
getBotInfo={getBotInfo}
|
||||
layout={layout}
|
||||
showBackground={showBackground}
|
||||
isContentLoading={isContentLoading}
|
||||
isCardDisabled={isCardDisabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
MessageBox.displayName = 'UIKitMessageBox';
|
||||
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
* 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 PropsWithChildren,
|
||||
type FC,
|
||||
useRef,
|
||||
useState,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
|
||||
import classnames from 'classnames';
|
||||
import { useClickAway, useHover, useUpdateEffect } from 'ahooks';
|
||||
import { ErrorBoundary } from '@coze-arch/logger';
|
||||
import {
|
||||
Layout,
|
||||
UIKitEvents,
|
||||
useUiKitEventCenter,
|
||||
} from '@coze-common/chat-uikit-shared';
|
||||
import { useEventCallback } from '@coze-common/chat-hooks';
|
||||
import { Avatar, Typography } from '@coze-arch/coze-design';
|
||||
|
||||
import { UserLabel, UserName } from '../user-label';
|
||||
import { MessageContentTime } from '../message-content-time';
|
||||
import { typeSafeMessageBoxInnerVariants } from '../../../variants/message-box-inner-variants';
|
||||
import { useObserveCardContainer } from '../../../hooks/use-observe-card-container';
|
||||
import { UIKitMessageBoxProvider } from '../../../context/message-box';
|
||||
import { useUIKitCustomComponent } from '../../../context/custom-components';
|
||||
import defaultAvatar from '../../../assets/default-avatar.png';
|
||||
import {
|
||||
typeSafeBotNicknameVariants,
|
||||
messageBoxContainerVariants,
|
||||
} from './variants';
|
||||
import { getMessageBoxInnerVariantsByTheme } from './utils';
|
||||
import { type MessageBoxWrapProps } from './type';
|
||||
import { FallbackComponent } from './fallback';
|
||||
import { DefaultAvatarWrap } from './default-avatar-wrap';
|
||||
import './message-box.less';
|
||||
|
||||
export const MessageBoxWrap: FC<
|
||||
PropsWithChildren<MessageBoxWrapProps>
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function
|
||||
> = props => {
|
||||
const {
|
||||
children,
|
||||
theme,
|
||||
nickname,
|
||||
avatar,
|
||||
showUserInfo,
|
||||
renderFooter,
|
||||
hoverContent,
|
||||
right,
|
||||
senderId,
|
||||
classname,
|
||||
messageBubbleClassname,
|
||||
messageBubbleWrapperClassname,
|
||||
messageBoxWraperClassname,
|
||||
messageErrorWrapperClassname,
|
||||
isHoverShowUserInfo = true,
|
||||
layout,
|
||||
contentTime,
|
||||
showBackground,
|
||||
extendedUserInfo,
|
||||
topRightSlot,
|
||||
imageAutoSizeContainerWidth,
|
||||
enableImageAutoSize,
|
||||
messageId,
|
||||
eventCallbacks,
|
||||
onError,
|
||||
} = props;
|
||||
const { userLabel, userUniqueName } = extendedUserInfo ?? {};
|
||||
const [botAvatar, setBotAvatar] = useState(avatar || defaultAvatar);
|
||||
const { MentionOperateTool = () => null, AvatarWrap = DefaultAvatarWrap } =
|
||||
useUIKitCustomComponent();
|
||||
const wrapRef = useRef<HTMLDivElement>(null);
|
||||
const messageContainerRef = useRef<HTMLDivElement>(null);
|
||||
const messageFooterRef = useRef<HTMLDivElement>(null);
|
||||
const eventCenter = useUiKitEventCenter();
|
||||
const isMobileLayout = layout === Layout.MOBILE;
|
||||
const refreshContainerWidthConditionally = useEventCallback(() => {
|
||||
if (!messageContainerRef.current || !messageFooterRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentMessageWidth = `${messageContainerRef.current.offsetWidth}px`;
|
||||
const currentFooterWidth = messageFooterRef.current.style.width;
|
||||
|
||||
if (currentFooterWidth === currentMessageWidth) {
|
||||
return;
|
||||
}
|
||||
|
||||
messageFooterRef.current.style.width = currentMessageWidth;
|
||||
});
|
||||
|
||||
useUpdateEffect(() => {
|
||||
setBotAvatar(avatar || defaultAvatar);
|
||||
}, [avatar]);
|
||||
useObserveCardContainer({
|
||||
messageId,
|
||||
cardContainerRef: messageContainerRef,
|
||||
onResize: refreshContainerWidthConditionally,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!eventCenter) {
|
||||
return;
|
||||
}
|
||||
eventCenter.on(
|
||||
UIKitEvents.WINDOW_RESIZE,
|
||||
refreshContainerWidthConditionally,
|
||||
);
|
||||
|
||||
return () => {
|
||||
eventCenter.off(
|
||||
UIKitEvents.WINDOW_RESIZE,
|
||||
refreshContainerWidthConditionally,
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const isHovering = useHover(() => wrapRef.current);
|
||||
|
||||
// 适配移动端 移动端没有hover效果,使用点击交互
|
||||
const [hoverContentVisible, setHoverContentVisible] =
|
||||
useState<boolean>(false);
|
||||
|
||||
useClickAway(() => {
|
||||
setHoverContentVisible(false);
|
||||
}, wrapRef);
|
||||
|
||||
return (
|
||||
<UIKitMessageBoxProvider
|
||||
value={{
|
||||
layout,
|
||||
imageAutoSizeContainerWidth,
|
||||
enableImageAutoSize,
|
||||
eventCallbacks,
|
||||
onError,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
// chat-uikit-message-box
|
||||
className={classnames('max-w-full', classname)}
|
||||
ref={wrapRef}
|
||||
onClick={() => {
|
||||
if (isMobileLayout) {
|
||||
setHoverContentVisible(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
// chat-uikit-message-box-container chat-uikit-message-box-container-pc
|
||||
className={classnames(
|
||||
messageBoxContainerVariants({ isMobileLayout }),
|
||||
messageBoxWraperClassname,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
// chat-uikit-message-box-container__avatar-wrap
|
||||
className="mr-[12px] w-32px h-32px"
|
||||
>
|
||||
{showUserInfo ? (
|
||||
<AvatarWrap>
|
||||
<Avatar
|
||||
// chat-uikit-message-box-container__avatar-wrap__avatar
|
||||
size="small"
|
||||
src={botAvatar}
|
||||
onError={() => setBotAvatar(defaultAvatar)}
|
||||
></Avatar>
|
||||
</AvatarWrap>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
// chat-uikit-message-box-container__message
|
||||
className="flex-1 max-w-[calc(100%-44px)]"
|
||||
>
|
||||
{/* TODO: 不支持一条消息内渲染多个 content */}
|
||||
<div
|
||||
// chat-uikit-message-box-container__message__message-box
|
||||
className="relative flex flex-col w-fit max-w-full"
|
||||
>
|
||||
{showUserInfo && nickname ? (
|
||||
<div
|
||||
// chat-uikit-message-box-container__message__nickname
|
||||
className="flex"
|
||||
>
|
||||
<Typography.Text
|
||||
ellipsis={{
|
||||
showTooltip: {
|
||||
opts: {
|
||||
content: nickname,
|
||||
},
|
||||
},
|
||||
}}
|
||||
// chat-uikit-message-box-container__message__nickname-text
|
||||
className={typeSafeBotNicknameVariants({
|
||||
showBackground: Boolean(showBackground),
|
||||
})}
|
||||
>
|
||||
{nickname}
|
||||
</Typography.Text>
|
||||
<UserLabel userLabel={userLabel} />
|
||||
<div
|
||||
// chat-uikit-message-box-container__message__nickname-partner
|
||||
className="flex shrink w-full h-[20px]"
|
||||
>
|
||||
{isHovering && isHoverShowUserInfo ? (
|
||||
<>
|
||||
<UserName
|
||||
userUniqueName={userUniqueName}
|
||||
showBackground={showBackground}
|
||||
/>
|
||||
<MentionOperateTool senderId={senderId} />
|
||||
<MessageContentTime
|
||||
contentTime={contentTime}
|
||||
showBackground={Boolean(showBackground)}
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<div className="flex gap-x-[8px] ml-auto">
|
||||
{topRightSlot}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
ref={messageContainerRef}
|
||||
// chat-uikit-message-box-container__message__message-box__content
|
||||
className={classnames(
|
||||
messageBubbleWrapperClassname,
|
||||
'select-text relative flex flex-row w-fit max-w-full',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
// className={classnames('chat-uikit-message-box-inner', {
|
||||
// 'chat-uikit-message-box-inner--primary':
|
||||
// theme === 'primary',
|
||||
// 'chat-uikit-message-box-inner--whiteness':
|
||||
// theme === 'whiteness',
|
||||
// 'chat-uikit-message-box-inner--colorful':
|
||||
// theme === 'colorful',
|
||||
// 'chat-uikit-message-box-inner--border': theme === 'border',
|
||||
// 'chat-uikit-message-box-inner--none': theme === 'none',
|
||||
// '!coz-bg-image-user !coz-stroke-image-user':
|
||||
// showBackground && theme === 'primary',
|
||||
// '!coz-stroke-image-user !coz-bg-image-bots':
|
||||
// showBackground && theme === 'border',
|
||||
// '!coz-bg-image-bots !coz-stroke-image-bots':
|
||||
// showBackground && theme === 'whiteness',
|
||||
// 'chat-uikit-message-box-inner--color-border':
|
||||
// theme === 'color-border',
|
||||
// 'chat-uikit-message-box-inner--color-border-card':
|
||||
// theme === 'color-border-card',
|
||||
// })}
|
||||
className={classnames(
|
||||
messageBubbleClassname,
|
||||
typeSafeMessageBoxInnerVariants({
|
||||
showBackground: Boolean(showBackground),
|
||||
...getMessageBoxInnerVariantsByTheme({ theme }),
|
||||
}),
|
||||
layout === Layout.MOBILE ? '!text-[16px]' : '',
|
||||
)}
|
||||
>
|
||||
<ErrorBoundary
|
||||
errorBoundaryName="chat-message-box-children"
|
||||
FallbackComponent={FallbackComponent}
|
||||
>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
<div
|
||||
// chat-uikit-message-box-container__message__message-box__content__right
|
||||
className={classnames(
|
||||
'absolute right-0 bottom-[1px]',
|
||||
messageErrorWrapperClassname,
|
||||
)}
|
||||
>
|
||||
{right}
|
||||
</div>
|
||||
</div>
|
||||
{/* 对这个 dom 的样式改动前请先阅读上方的 refreshContainerWidthConditionally 方法 */}
|
||||
<div
|
||||
ref={messageFooterRef}
|
||||
// chat-uikit-message-box-container__message__message-box__footer
|
||||
className="overflow-visible"
|
||||
>
|
||||
{renderFooter?.(refreshContainerWidthConditionally)}
|
||||
</div>
|
||||
{isHovering || hoverContentVisible ? (
|
||||
<div
|
||||
// chat-uikit-message-box-container__message__message-box__hover-container
|
||||
className="absolute right-[-12px] bottom-[-20px]"
|
||||
>
|
||||
{hoverContent}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UIKitMessageBoxProvider>
|
||||
);
|
||||
};
|
||||
|
||||
MessageBoxWrap.displayName = 'UIKitMessageBoxWrap';
|
||||
@@ -0,0 +1,3 @@
|
||||
.chat-uikit-message-box-bg-primary {
|
||||
background: var(--coz-mg-hglt-plus-dim);
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import {
|
||||
type IEventCallbacks,
|
||||
type Layout,
|
||||
type IContentConfigs,
|
||||
type IMessage,
|
||||
type GetBotInfo,
|
||||
} from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { type UserLabelInfo } from '../user-label';
|
||||
|
||||
/**
|
||||
* 样式主题
|
||||
* @deprecated 考虑替换实现方式,目前使用不够灵活
|
||||
*/
|
||||
export type MessageBoxTheme =
|
||||
/** 主题色 */
|
||||
| 'primary'
|
||||
/** 白色背景 */
|
||||
| 'whiteness'
|
||||
/**
|
||||
* 灰色背景
|
||||
*/
|
||||
| 'grey'
|
||||
/** home用的炫彩底色 */
|
||||
| 'colorful'
|
||||
/** 官方通知,有彩色边框 */
|
||||
| 'color-border'
|
||||
/** 官方通知,有彩色边框,但是不留 padding */
|
||||
| 'color-border-card'
|
||||
| 'border'
|
||||
| 'none';
|
||||
|
||||
interface MessageBoxBasicProps {
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
senderInfo: {
|
||||
userUniqueName?: string;
|
||||
nickname?: string;
|
||||
url?: string;
|
||||
id: string;
|
||||
userLabel?: UserLabelInfo | null;
|
||||
};
|
||||
/**
|
||||
* 消息 id
|
||||
*/
|
||||
messageId: string | null;
|
||||
|
||||
showUserInfo?: boolean;
|
||||
/**
|
||||
* 主题
|
||||
*/
|
||||
theme?: MessageBoxTheme;
|
||||
/**
|
||||
* 插入消息的footer
|
||||
*/
|
||||
renderFooter?: (refreshContainerWidth: () => void) => React.ReactNode;
|
||||
/** 鼠标悬浮时展示的组件 */
|
||||
hoverContent?: ReactNode;
|
||||
/**
|
||||
* 左侧插槽
|
||||
*/
|
||||
right?: React.ReactNode;
|
||||
/**
|
||||
* 右上角插槽
|
||||
*/
|
||||
topRightSlot?: React.ReactNode;
|
||||
getBotInfo: GetBotInfo;
|
||||
/**
|
||||
* 是否是移动端
|
||||
*/
|
||||
layout?: Layout;
|
||||
classname?: string;
|
||||
|
||||
messageBubbleWrapperClassname?: string;
|
||||
messageBoxWraperClassname?: string; // message box的直接父亲样式
|
||||
messageBubbleClassname?: string; // message消息气泡的样式
|
||||
messageErrorWrapperClassname?: string; // message错误的父亲样式
|
||||
isHoverShowUserInfo?: boolean; // hover的时候是否显示用户详细信息
|
||||
|
||||
showBackground?: boolean;
|
||||
/**
|
||||
* 容器动态宽度,用于动态计算图片尺寸
|
||||
*/
|
||||
imageAutoSizeContainerWidth?: number;
|
||||
/**
|
||||
* 是否启用图片自适应模式
|
||||
*/
|
||||
enableImageAutoSize?: boolean;
|
||||
/**
|
||||
* 事件回调
|
||||
*/
|
||||
eventCallbacks?: IEventCallbacks;
|
||||
/**
|
||||
* 针对 JS Error 的响应
|
||||
*/
|
||||
onError?: (error: unknown) => void;
|
||||
}
|
||||
|
||||
/** 只是套壳,内容由 children 呈现 */
|
||||
export interface MessageBoxShellProps extends MessageBoxBasicProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/** 含有完整内置实现的 MessageBox */
|
||||
export interface NormalMessageBoxProps extends MessageBoxBasicProps {
|
||||
/**
|
||||
* 消息体
|
||||
*/
|
||||
message: IMessage;
|
||||
/**
|
||||
* 文件需要用到的必备参数
|
||||
*/
|
||||
contentConfigs?: IContentConfigs;
|
||||
/** 样式主题 */
|
||||
theme?: MessageBoxTheme;
|
||||
footer?: ReactNode;
|
||||
readonly?: boolean;
|
||||
isContentLoading?: boolean;
|
||||
isCardDisabled?: boolean;
|
||||
}
|
||||
|
||||
export type MessageBoxProps = MessageBoxShellProps | NormalMessageBoxProps;
|
||||
|
||||
export interface MessageBoxWrapProps {
|
||||
nickname?: string;
|
||||
avatar?: string;
|
||||
theme: MessageBoxTheme;
|
||||
showUserInfo?: boolean;
|
||||
renderFooter?: (refreshContainerWidth: () => void) => React.ReactNode;
|
||||
|
||||
/** 鼠标悬浮时展示的组件 */
|
||||
hoverContent?: React.ReactNode;
|
||||
right?: React.ReactNode;
|
||||
/**
|
||||
* 右上角插槽
|
||||
*/
|
||||
topRightSlot?: React.ReactNode;
|
||||
messageId: string | null;
|
||||
senderId: string;
|
||||
layout: Layout;
|
||||
contentTime: number | undefined;
|
||||
classname?: string;
|
||||
|
||||
messageBoxWraperClassname?: string; // message box的直接父亲样式
|
||||
messageBubbleClassname?: string; // message消息气泡的样式
|
||||
messageBubbleWrapperClassname?: string; // message消息气泡的父亲样式
|
||||
messageErrorWrapperClassname?: string; // message错误的父亲样式
|
||||
isHoverShowUserInfo?: boolean; // hover的时候是否显示用户详细信息
|
||||
|
||||
showBackground?: boolean;
|
||||
extendedUserInfo?: {
|
||||
userLabel?: UserLabelInfo | null;
|
||||
userUniqueName?: string;
|
||||
};
|
||||
/**
|
||||
* 容器动态宽度,用于动态计算图片尺寸
|
||||
*/
|
||||
imageAutoSizeContainerWidth?: number;
|
||||
/**
|
||||
* 是否启用图片自适应模式
|
||||
*/
|
||||
enableImageAutoSize?: boolean;
|
||||
eventCallbacks?: IEventCallbacks;
|
||||
/**
|
||||
* 针对 JS Error 的响应
|
||||
*/
|
||||
onError?: (error: unknown) => void;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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 { exhaustiveCheckSimple } from '@coze-common/chat-area-utils';
|
||||
|
||||
import { type MessageBoxInnerVariantProps } from '../../../variants/message-box-inner-variants';
|
||||
import { type MessageBoxTheme } from './type';
|
||||
|
||||
export const getMessageBoxInnerVariantsByTheme: (props: {
|
||||
theme: MessageBoxTheme;
|
||||
}) => Pick<MessageBoxInnerVariantProps, 'color' | 'border' | 'tight'> = ({
|
||||
theme,
|
||||
}) => {
|
||||
if (theme === 'primary' || theme === 'whiteness' || theme === 'grey') {
|
||||
return { color: theme, border: null, tight: false };
|
||||
}
|
||||
if (theme === 'colorful') {
|
||||
return { color: 'primary', border: null, tight: false };
|
||||
}
|
||||
if (theme === 'border') {
|
||||
return { color: 'whiteness', border: 'primary', tight: true };
|
||||
}
|
||||
|
||||
if (theme === 'color-border') {
|
||||
return { color: 'whiteness', border: 'highlight', tight: false };
|
||||
}
|
||||
if (theme === 'color-border-card') {
|
||||
return { color: 'whiteness', border: 'highlight', tight: true };
|
||||
}
|
||||
if (theme === 'none') {
|
||||
return { tight: true, color: null, border: null };
|
||||
}
|
||||
exhaustiveCheckSimple(theme);
|
||||
return { tight: false, color: null, border: null };
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
export const messageBoxContainerVariants = cva(['flex', 'flex-row', 'my-0'], {
|
||||
variants: {
|
||||
isMobileLayout: {
|
||||
true: ['mx-[12px]'],
|
||||
false: ['mx-[24px]'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const botNicknameVariants = cva(
|
||||
[
|
||||
'text-base',
|
||||
'font-normal',
|
||||
'leading-[16px]',
|
||||
'break-words',
|
||||
'flex-shrink-0',
|
||||
'!max-w-[400px]',
|
||||
],
|
||||
{
|
||||
variants: {
|
||||
showBackground: {
|
||||
true: ['coz-fg-images-user-name'],
|
||||
false: ['coz-fg-secondary'],
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
export type BotNicknameVariantsProps = Required<
|
||||
VariantProps<typeof botNicknameVariants>
|
||||
>;
|
||||
export const typeSafeBotNicknameVariants: (
|
||||
props: BotNicknameVariantsProps,
|
||||
) => string = botNicknameVariants;
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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 classNames from 'classnames';
|
||||
|
||||
import { formatMessageBoxContentTime } from '../../../utils/date-time';
|
||||
|
||||
export const MessageContentTime = ({
|
||||
contentTime,
|
||||
className,
|
||||
showBackground,
|
||||
}: {
|
||||
contentTime?: number;
|
||||
className?: string;
|
||||
showBackground: boolean;
|
||||
}) => {
|
||||
if (!contentTime) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
'text-[12px] leading-[16px] ml-[8px] font-normal',
|
||||
'chat-uikit-message-box-container__message-content-time',
|
||||
{
|
||||
'coz-fg-images-secondary': showBackground,
|
||||
'coz-fg-secondary': !showBackground,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{formatMessageBoxContentTime(contentTime)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
MessageContentTime.displayName = 'MessageContentTime';
|
||||
@@ -0,0 +1,3 @@
|
||||
.to-newest-tip-ui-animation {
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 classNames from 'classnames';
|
||||
import { IconCozLongArrowUp } from '@coze-arch/coze-design/icons';
|
||||
|
||||
import { OutlinedIconButton } from '../button';
|
||||
import { type ToNewestTipProps } from './type';
|
||||
import './animation.less';
|
||||
|
||||
export const ToNewestTipUI = (props: ToNewestTipProps) => {
|
||||
const { onClick, style, className, show, showBackground } = props;
|
||||
return (
|
||||
<OutlinedIconButton
|
||||
className={classNames(
|
||||
[
|
||||
'shadow-normal',
|
||||
'coz-fg-hglt',
|
||||
'to-newest-tip-ui-animation',
|
||||
'!rounded-full',
|
||||
],
|
||||
!show && ['pointer-events-none', 'opacity-0'],
|
||||
className,
|
||||
)}
|
||||
size="large"
|
||||
onClick={onClick}
|
||||
style={style}
|
||||
icon={<IconCozLongArrowUp className="rotate-180" />}
|
||||
showBackground={showBackground}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
ToNewestTipUI.displayName = 'UIKitToNewestTip';
|
||||
@@ -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 { type CSSProperties } from 'react';
|
||||
|
||||
export interface ToNewestTipProps {
|
||||
onClick: () => void;
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
show?: boolean;
|
||||
showBackground: boolean;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import { Tooltip, type TooltipProps } from '@coze-arch/coze-design';
|
||||
// import { type TooltipProps } from '@douyinfe/semi-ui/lib/es/tooltip';
|
||||
|
||||
type IProps = {
|
||||
children: JSX.Element;
|
||||
hideToolTip?: boolean;
|
||||
} & TooltipProps;
|
||||
|
||||
export const UIKitTooltip: FC<IProps> = props => {
|
||||
const {
|
||||
content,
|
||||
children,
|
||||
hideToolTip,
|
||||
theme = 'dark',
|
||||
...restProps
|
||||
} = props;
|
||||
return content ? (
|
||||
<Tooltip
|
||||
trigger={hideToolTip ? 'custom' : 'hover'}
|
||||
visible={hideToolTip ? false : undefined}
|
||||
content={content}
|
||||
theme={theme}
|
||||
{...restProps}
|
||||
style={{ marginBottom: '8px' }}
|
||||
>
|
||||
{children}
|
||||
</Tooltip>
|
||||
) : (
|
||||
<>{children}</>
|
||||
);
|
||||
};
|
||||
|
||||
UIKitTooltip.displayName = 'UIKitTooltip';
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import cs from 'classnames';
|
||||
|
||||
import { UIKitTooltip } from '../tooltips';
|
||||
|
||||
export interface UserLabelInfo {
|
||||
label_name?: string;
|
||||
icon_url?: string;
|
||||
jump_link?: string;
|
||||
}
|
||||
|
||||
export const UserLabel: FC<{
|
||||
userLabel?: UserLabelInfo | null;
|
||||
}> = ({ userLabel }) => {
|
||||
if (!userLabel?.icon_url || !userLabel?.label_name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<UIKitTooltip content={userLabel.label_name} theme="light">
|
||||
<div
|
||||
className={cs(
|
||||
'flex-[0_0_auto] flex items-center h-[20px] ml-[4px]',
|
||||
userLabel?.jump_link && 'cursor-pointer',
|
||||
)}
|
||||
onClick={event => {
|
||||
if (userLabel?.jump_link) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
window.open(userLabel?.jump_link, '_blank');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<img src={userLabel.icon_url} width={14} height={14} />
|
||||
</div>
|
||||
</UIKitTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: 增加 show background 变体
|
||||
export const UserName: FC<{
|
||||
userUniqueName?: string;
|
||||
className?: string;
|
||||
showBackground: boolean | undefined;
|
||||
}> = ({ userUniqueName, className, showBackground }) => {
|
||||
if (!userUniqueName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cs(
|
||||
'coz-fg-secondary text-[12px] leading-[16px] font-normal ml-[4px]',
|
||||
showBackground && '!coz-fg-images-secondary',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@{userUniqueName}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 { FileTypeEnum } from '@coze-common/chat-core/shared/const';
|
||||
|
||||
import EXCELSuccess from '../../../../../assets/file/xlsx-success.svg';
|
||||
import EXCELFail from '../../../../../assets/file/xlsx-fail.svg';
|
||||
import VIDEOSuccess from '../../../../../assets/file/video-success.svg';
|
||||
import VIDEOFail from '../../../../../assets/file/video-fail.svg';
|
||||
import TXTSuccess from '../../../../../assets/file/txt-success.svg';
|
||||
import TXTFail from '../../../../../assets/file/txt-fail.svg';
|
||||
import PPTSuccess from '../../../../../assets/file/ppt-success.svg';
|
||||
import PPTFail from '../../../../../assets/file/ppt-fail.svg';
|
||||
import PDFSuccess from '../../../../../assets/file/pdf-success.svg';
|
||||
import PDFFail from '../../../../../assets/file/pdf-fail.svg';
|
||||
import DOCXSuccess from '../../../../../assets/file/docx-success.svg';
|
||||
import DOCXFail from '../../../../../assets/file/docx-fail.svg';
|
||||
import DefaultUnknownSuccess from '../../../../../assets/file/default-unknown-success.svg';
|
||||
import DefaultUnknownFail from '../../../../../assets/file/default-unknown-fail.svg';
|
||||
import CSVSuccess from '../../../../../assets/file/csv-success.svg';
|
||||
import CSVFail from '../../../../../assets/file/csv-fail.svg';
|
||||
import CODESuccess from '../../../../../assets/file/code-success.svg';
|
||||
import CODEFail from '../../../../../assets/file/code-fail.svg';
|
||||
import AUDIOSuccess from '../../../../../assets/file/audio-success.svg';
|
||||
import AUDIOFail from '../../../../../assets/file/audio-fail.svg';
|
||||
import ARCHIVESuccess from '../../../../../assets/file/archive-success.svg';
|
||||
import ARCHIVEFail from '../../../../../assets/file/archive-fail.svg';
|
||||
|
||||
export const SUCCESS_FILE_ICON_MAP = {
|
||||
[FileTypeEnum.CSV]: CSVSuccess,
|
||||
[FileTypeEnum.DOCX]: DOCXSuccess,
|
||||
[FileTypeEnum.EXCEL]: EXCELSuccess,
|
||||
[FileTypeEnum.PDF]: PDFSuccess,
|
||||
[FileTypeEnum.AUDIO]: AUDIOSuccess,
|
||||
[FileTypeEnum.VIDEO]: VIDEOSuccess,
|
||||
[FileTypeEnum.ARCHIVE]: ARCHIVESuccess,
|
||||
[FileTypeEnum.CODE]: CODESuccess,
|
||||
[FileTypeEnum.TXT]: TXTSuccess,
|
||||
[FileTypeEnum.PPT]: PPTSuccess,
|
||||
[FileTypeEnum.DEFAULT_UNKNOWN]: DefaultUnknownSuccess,
|
||||
};
|
||||
|
||||
export const FAIL_FILE_ICON_MAP = {
|
||||
[FileTypeEnum.CSV]: CSVFail,
|
||||
[FileTypeEnum.DOCX]: DOCXFail,
|
||||
[FileTypeEnum.EXCEL]: EXCELFail,
|
||||
[FileTypeEnum.PDF]: PDFFail,
|
||||
[FileTypeEnum.AUDIO]: AUDIOFail,
|
||||
[FileTypeEnum.VIDEO]: VIDEOFail,
|
||||
[FileTypeEnum.ARCHIVE]: ARCHIVEFail,
|
||||
[FileTypeEnum.CODE]: CODEFail,
|
||||
[FileTypeEnum.TXT]: TXTFail,
|
||||
[FileTypeEnum.PPT]: PPTFail,
|
||||
[FileTypeEnum.DEFAULT_UNKNOWN]: DefaultUnknownFail,
|
||||
};
|
||||
|
||||
export const FILE_CARD_WIDTH = 280;
|
||||
export const PERCENT_DENOMINATOR = 100;
|
||||
@@ -0,0 +1,3 @@
|
||||
.chat-uikit-file-card-progress-animation {
|
||||
transition: width 0.3s linear;
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
IconCozCopy,
|
||||
IconCozCross,
|
||||
IconCozRefresh,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import { IconButton, Typography } from '@coze-arch/coze-design';
|
||||
import { Layout } from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { UIKitTooltip } from '../../../../common/tooltips';
|
||||
import { getFileExtensionAndName } from '../../../../../utils/file-name';
|
||||
import { convertBytes } from '../../../../../utils/convert-bytes';
|
||||
import {
|
||||
typeSafeFileCardNameVariants,
|
||||
typeSafeFileCardVariants,
|
||||
} from './variants';
|
||||
import { type IFileCardProps } from './type';
|
||||
import {
|
||||
FAIL_FILE_ICON_MAP,
|
||||
FILE_CARD_WIDTH,
|
||||
PERCENT_DENOMINATOR,
|
||||
SUCCESS_FILE_ICON_MAP,
|
||||
} from './constants';
|
||||
|
||||
import './file-card.less';
|
||||
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function
|
||||
const FileCard: FC<IFileCardProps> = props => {
|
||||
const {
|
||||
file,
|
||||
attributeKeys,
|
||||
tooltipsCopywriting,
|
||||
readonly,
|
||||
onCancel,
|
||||
onCopy,
|
||||
onRetry,
|
||||
className,
|
||||
layout,
|
||||
showBackground,
|
||||
} = props;
|
||||
|
||||
const { statusKey, statusEnum, percentKey } = attributeKeys;
|
||||
|
||||
const percent = file[percentKey];
|
||||
|
||||
const fileIconMap = [statusEnum.cancelEnum, statusEnum.failEnum].includes(
|
||||
file[statusKey],
|
||||
)
|
||||
? FAIL_FILE_ICON_MAP
|
||||
: SUCCESS_FILE_ICON_MAP;
|
||||
|
||||
const buttonsVisible = !readonly;
|
||||
const { extension, nameWithoutExtension } = getFileExtensionAndName(
|
||||
file.file_name,
|
||||
);
|
||||
|
||||
const isCanceled = file[statusKey] === statusEnum.cancelEnum;
|
||||
|
||||
return (
|
||||
<div
|
||||
// className={classNames(className, 'chat-uikit-file-card', {
|
||||
// 'chat-uikit-file-card--error': file[statusKey] === statusEnum.failEnum,
|
||||
// 'chat-uikit-file-card-pc': layout === Layout.PC,
|
||||
// 'chat-uikit-file-card-mobile': layout === Layout.MOBILE,
|
||||
// '!coz-bg-image-bots !coz-stroke-image-bots':
|
||||
// showBackground && file[statusKey] !== statusEnum.failEnum,
|
||||
// })}
|
||||
className={classNames(
|
||||
typeSafeFileCardVariants({
|
||||
isError: file[statusKey] === statusEnum.failEnum,
|
||||
layout: layout === Layout.PC ? 'pc' : 'mobile',
|
||||
showBackground,
|
||||
}),
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={fileIconMap[file.file_type]}
|
||||
// chat-uikit-file-card__icon
|
||||
className="h-[32px] w-[32px]"
|
||||
></img>
|
||||
<div
|
||||
// chat-uikit-file-card__info
|
||||
className="flex flex-1 flex-col ml-8px overflow-hidden"
|
||||
>
|
||||
<Typography.Text
|
||||
ellipsis={{
|
||||
showTooltip:
|
||||
layout === Layout.MOBILE
|
||||
? false
|
||||
: {
|
||||
opts: {
|
||||
content: file.file_name,
|
||||
style: { wordWrap: 'break-word' },
|
||||
},
|
||||
},
|
||||
suffix: extension,
|
||||
}}
|
||||
// chat-uikit-file-card__info__name
|
||||
// chat-uikit-file-card__info__name_pc
|
||||
// chat-uikit-file-card__info__name_mobile
|
||||
className={typeSafeFileCardNameVariants({
|
||||
isCanceled,
|
||||
layout: layout === Layout.PC ? 'pc' : 'mobile',
|
||||
})}
|
||||
>
|
||||
{nameWithoutExtension}
|
||||
</Typography.Text>
|
||||
<span
|
||||
// chat-uikit-file-card__info__size
|
||||
className={classNames(
|
||||
'text-base font-normal leading-[16px]',
|
||||
isCanceled ? 'coz-fg-dim' : 'coz-fg-secondary',
|
||||
)}
|
||||
>
|
||||
{convertBytes(file.file_size)}
|
||||
</span>
|
||||
</div>
|
||||
{buttonsVisible ? (
|
||||
<>
|
||||
<div
|
||||
// chat-uikit-file-card__buttons
|
||||
className="ml-8px"
|
||||
>
|
||||
{file[statusKey] === statusEnum.uploadingEnum && (
|
||||
<UIKitTooltip
|
||||
theme="light"
|
||||
position="top"
|
||||
content={tooltipsCopywriting?.cancel}
|
||||
hideToolTip={layout === Layout.MOBILE}
|
||||
>
|
||||
<IconButton
|
||||
// chat-uikit-file-card__buttons__button
|
||||
icon={
|
||||
<IconCozCross // chat-uikit-file-card__buttons__icon
|
||||
/>
|
||||
}
|
||||
size="small"
|
||||
color="secondary"
|
||||
onClick={onCancel}
|
||||
/>
|
||||
</UIKitTooltip>
|
||||
)}
|
||||
{[statusEnum.cancelEnum, statusEnum.failEnum].includes(
|
||||
file[statusKey],
|
||||
) && (
|
||||
<UIKitTooltip
|
||||
theme="light"
|
||||
position="top"
|
||||
content={tooltipsCopywriting?.retry}
|
||||
hideToolTip={layout === Layout.MOBILE}
|
||||
>
|
||||
<IconButton
|
||||
// chat-uikit-file-card__buttons__button
|
||||
|
||||
icon={
|
||||
<IconCozRefresh // chat-uikit-file-card__buttons__icon
|
||||
/>
|
||||
}
|
||||
size="small"
|
||||
color="secondary"
|
||||
onClick={onRetry}
|
||||
/>
|
||||
</UIKitTooltip>
|
||||
)}
|
||||
{file[statusKey] === statusEnum.successEnum && (
|
||||
<UIKitTooltip
|
||||
theme="light"
|
||||
position="top"
|
||||
content={tooltipsCopywriting?.copy}
|
||||
hideToolTip={layout === Layout.MOBILE}
|
||||
>
|
||||
<IconButton
|
||||
// chat-uikit-file-card__buttons__button
|
||||
|
||||
icon={
|
||||
<IconCozCopy // chat-uikit-file-card__buttons__icon
|
||||
/>
|
||||
}
|
||||
size="small"
|
||||
color="secondary"
|
||||
onClick={onCopy}
|
||||
/>
|
||||
</UIKitTooltip>
|
||||
)}
|
||||
</div>
|
||||
{file[statusKey] === statusEnum.uploadingEnum && (
|
||||
<div
|
||||
// chat-uikit-file-card__progress-wrap
|
||||
className={classNames(
|
||||
// TODO: ui 补充进度条颜色
|
||||
'coz-fg-hglt-dim absolute top-0 left-0 w-[280px] h-[72px]',
|
||||
'chat-uikit-file-card-progress-animation',
|
||||
)}
|
||||
style={{
|
||||
width: `${FILE_CARD_WIDTH * (percent / PERCENT_DENOMINATOR)}px`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FileCard.displayName = 'FileCard';
|
||||
|
||||
export default FileCard;
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type IFileAttributeKeys,
|
||||
type IFileCardTooltipsCopyWritingConfig,
|
||||
type IFileInfo,
|
||||
type Layout,
|
||||
} from '@coze-common/chat-uikit-shared';
|
||||
|
||||
export interface IFileCardProps {
|
||||
file: IFileInfo;
|
||||
/**
|
||||
* 用于识别成功 / 失败状态的key
|
||||
*/
|
||||
attributeKeys: IFileAttributeKeys;
|
||||
/**
|
||||
* 文案配置
|
||||
*/
|
||||
tooltipsCopywriting?: IFileCardTooltipsCopyWritingConfig;
|
||||
/**
|
||||
* 是否只读
|
||||
*/
|
||||
readonly?: boolean;
|
||||
/**
|
||||
* 取消上传事件回调
|
||||
*/
|
||||
onCancel: () => void;
|
||||
/**
|
||||
* 重试上传事件回调
|
||||
*/
|
||||
onRetry: () => void;
|
||||
/**
|
||||
* 拷贝url事件回调
|
||||
*/
|
||||
onCopy: () => void;
|
||||
className?: string;
|
||||
layout: Layout;
|
||||
showBackground: boolean;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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 { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
const fileCardVariants = cva(
|
||||
[
|
||||
'select-none',
|
||||
'relative',
|
||||
'overflow-hidden',
|
||||
'flex',
|
||||
'flex-row',
|
||||
'items-center',
|
||||
'box-border',
|
||||
'p-12px',
|
||||
'border-[1px]',
|
||||
'border-solid',
|
||||
'rounded-normal',
|
||||
'coz-mg-card',
|
||||
'w-full',
|
||||
],
|
||||
{
|
||||
variants: {
|
||||
layout: {
|
||||
pc: ['min-w-[282px]', 'max-w-[320px]'],
|
||||
mobile: ['w-full'],
|
||||
},
|
||||
isError: {
|
||||
true: ['coz-stroke-hglt-red'],
|
||||
false: ['coz-stroke-primary'],
|
||||
},
|
||||
showBackground: {
|
||||
true: ['!coz-bg-image-bots', '!coz-stroke-image-bots'],
|
||||
false: [],
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
showBackground: true,
|
||||
isError: false,
|
||||
className: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const fileCardNameVariants = cva(['text-lg', 'font-normal', 'leading-[20px]'], {
|
||||
variants: {
|
||||
layout: {
|
||||
pc: ['w-[180px]'],
|
||||
mobile: ['w-full', 'max-w-[calc(100vw-170px)]'],
|
||||
},
|
||||
isCanceled: {
|
||||
true: ['coz-fg-dim'],
|
||||
false: ['coz-fg-primary'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const typeSafeFileCardVariants: (
|
||||
props: Required<VariantProps<typeof fileCardVariants>>,
|
||||
) => string = fileCardVariants;
|
||||
|
||||
export const typeSafeFileCardNameVariants: (
|
||||
props: Required<VariantProps<typeof fileCardNameVariants>>,
|
||||
) => string = fileCardNameVariants;
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
type IFileAttributeKeys,
|
||||
type IOnRetryUploadParams,
|
||||
type IOnCancelUploadParams,
|
||||
type IOnCopyUploadParams,
|
||||
type IFileCopywritingConfig,
|
||||
type IBaseContentProps,
|
||||
type Layout,
|
||||
} from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { safeJSONParse } from '../../../utils/safe-json-parse';
|
||||
import { isFile } from '../../../utils/is-file';
|
||||
import FileCard from './components/FileCard';
|
||||
|
||||
export type IProps = IBaseContentProps & {
|
||||
copywriting?: IFileCopywritingConfig;
|
||||
fileAttributeKeys?: IFileAttributeKeys;
|
||||
onCancel?: (params: IOnCancelUploadParams) => void;
|
||||
onRetry?: (params: IOnRetryUploadParams) => void;
|
||||
onCopy?: (params: IOnCopyUploadParams) => void;
|
||||
layout: Layout;
|
||||
showBackground: boolean;
|
||||
};
|
||||
|
||||
export const FileContent: FC<IProps> = props => {
|
||||
const {
|
||||
message,
|
||||
copywriting,
|
||||
fileAttributeKeys,
|
||||
readonly,
|
||||
onCancel,
|
||||
onCopy,
|
||||
onRetry,
|
||||
layout,
|
||||
showBackground,
|
||||
} = props;
|
||||
|
||||
const { content_obj = safeJSONParse(message.content) } = message;
|
||||
|
||||
/**
|
||||
* 判断是否为文件类型的卡片 或者 没有配置file属性config则拒绝使用该卡片
|
||||
*/
|
||||
if (
|
||||
!isFile(content_obj) ||
|
||||
!fileAttributeKeys ||
|
||||
content_obj.file_list.length <= 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理点击取消上传的事件
|
||||
*/
|
||||
const handleCancel = () => {
|
||||
onCancel?.({ message, extra: {} });
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理重试上传的事件
|
||||
*/
|
||||
const handleRetry = () => {
|
||||
onRetry?.({ message, extra: {} });
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理拷贝文件地址的事件
|
||||
*/
|
||||
const handleCopy = (fileIndex?: number) => {
|
||||
onCopy?.({ message, extra: { fileIndex } });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{content_obj.file_list.map((file, index) => (
|
||||
<FileCard
|
||||
file={file}
|
||||
attributeKeys={fileAttributeKeys}
|
||||
tooltipsCopywriting={copywriting?.tooltips}
|
||||
readonly={readonly}
|
||||
onCancel={handleCancel}
|
||||
onCopy={() => handleCopy(index)}
|
||||
onRetry={handleRetry}
|
||||
layout={layout}
|
||||
showBackground={showBackground}
|
||||
className={classNames({
|
||||
'mb-[8px]': index < content_obj.file_list.length - 1,
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
FileContent.displayName = 'FileContent';
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Row, Col } from '@coze-arch/coze-design';
|
||||
import { Image } from '@coze-arch/bot-md-box-adapter/slots';
|
||||
import {
|
||||
type OnImageClickCallback,
|
||||
type OnImageRenderCallback,
|
||||
} from '@coze-arch/bot-md-box-adapter';
|
||||
|
||||
import './index.less';
|
||||
|
||||
export enum CompressAlgorithm {
|
||||
None = 0,
|
||||
Snappy = 1,
|
||||
Zstd = 2,
|
||||
}
|
||||
export interface MsgContentData {
|
||||
card_data?: string;
|
||||
compress?: CompressAlgorithm;
|
||||
}
|
||||
|
||||
export interface ContentBoxEvents {
|
||||
onError?: (err: unknown) => void;
|
||||
onLoadStart?: () => void;
|
||||
onLoadEnd?: () => void;
|
||||
onLoad?: () => Promise<MsgContentData | undefined>;
|
||||
}
|
||||
|
||||
export interface BaseContentBoxProps {
|
||||
/** 是否在浏览器视窗内,true:在,false:不在,undefined:未检测 */
|
||||
inView?: boolean;
|
||||
contentBoxEvents?: ContentBoxEvents;
|
||||
}
|
||||
|
||||
export interface ImageMessageContent {
|
||||
key: string;
|
||||
image_thumb: {
|
||||
url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
image_ori: {
|
||||
url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
request_id?: string;
|
||||
}
|
||||
|
||||
export interface ImageContent {
|
||||
image_list: ImageMessageContent[];
|
||||
}
|
||||
|
||||
export interface ImageBoxProps extends BaseContentBoxProps {
|
||||
data: ImageContent;
|
||||
eventCallbacks?: {
|
||||
onImageClick?: OnImageClickCallback;
|
||||
onImageRender?: OnImageRenderCallback;
|
||||
};
|
||||
}
|
||||
const getImageBoxGutterAndSpan = (
|
||||
length: number,
|
||||
): {
|
||||
gutter: React.ComponentProps<typeof Row>['gutter'];
|
||||
span: React.ComponentProps<typeof Col>['span'];
|
||||
} => {
|
||||
if (length === 1) {
|
||||
return { gutter: [1, 1], span: 24 };
|
||||
}
|
||||
return { gutter: [2, 2], span: 12 };
|
||||
};
|
||||
|
||||
export const ImageBox: FC<ImageBoxProps> = ({ data, eventCallbacks }) => {
|
||||
const { onImageClick, onImageRender } = eventCallbacks || {};
|
||||
const { image_list = [] } = data || {};
|
||||
|
||||
const layout = getImageBoxGutterAndSpan(image_list?.length);
|
||||
|
||||
return (
|
||||
<div className={classNames('chat-uikit-image-box', 'rounded-normal')}>
|
||||
<Row gutter={layout.gutter}>
|
||||
{image_list.map(({ image_thumb }, index) => (
|
||||
<Col span={layout.span} key={index}>
|
||||
<Image
|
||||
onImageClick={onImageClick}
|
||||
onImageRender={onImageRender}
|
||||
src={image_thumb.url}
|
||||
imageOptions={{
|
||||
squareContainer: true,
|
||||
}}
|
||||
className="object-cover"
|
||||
/>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
.chat-uikit-image-content {
|
||||
user-select: none;
|
||||
width: 240px;
|
||||
|
||||
img {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-uikit-image-error-boundary {
|
||||
>.semi-image {
|
||||
>img {
|
||||
width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.chat-uikit-image-box {
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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 { ErrorBoundary } from 'react-error-boundary';
|
||||
import { type FC } from 'react';
|
||||
|
||||
import { Image } from '@coze-arch/coze-design';
|
||||
import {
|
||||
type IOnImageClickParams,
|
||||
type IBaseContentProps,
|
||||
} from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { safeJSONParse } from '../../../utils/safe-json-parse';
|
||||
import { isImage } from '../../../utils/is-image';
|
||||
import defaultImage from '../../../assets/image-empty.png';
|
||||
import { ImageBox } from './image-box';
|
||||
|
||||
import './index.less';
|
||||
|
||||
export type IImageMessageContentProps = IBaseContentProps & {
|
||||
onImageClick?: (params: IOnImageClickParams) => void;
|
||||
};
|
||||
|
||||
export const ImageContentImpl: FC<IImageMessageContentProps> = props => {
|
||||
const { message, onImageClick } = props;
|
||||
|
||||
const { content_obj = safeJSONParse(message.content) } = message;
|
||||
|
||||
if (!isImage(content_obj)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chat-uikit-image-content">
|
||||
<ImageBox
|
||||
data={{
|
||||
image_list: content_obj?.image_list ?? [],
|
||||
}}
|
||||
eventCallbacks={{
|
||||
onImageClick: (e, eventData) => {
|
||||
onImageClick?.({
|
||||
message,
|
||||
extra: { url: eventData.src as string },
|
||||
});
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ImageContentImpl.displayName = 'ImageContentImpl';
|
||||
|
||||
export const ImageContent: FC<IImageMessageContentProps> = props => (
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
<div className="chat-uikit-image-error-boundary">
|
||||
<Image src={defaultImage} preview={false} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ImageContentImpl {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
ImageContent.displayName = 'ImageContent';
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './file-content';
|
||||
export * from './image-content';
|
||||
export * from './plain-text-content';
|
||||
export * from './simple-function-content';
|
||||
export * from './suggestion-content';
|
||||
export * from './text-content';
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import { type FileMixItem } from '@coze-common/chat-core';
|
||||
import {
|
||||
type IFileAttributeKeys,
|
||||
type IOnCopyUploadParams,
|
||||
type IOnRetryUploadParams,
|
||||
type IOnCancelUploadParams,
|
||||
type IMessage,
|
||||
type IFileCopywritingConfig,
|
||||
type Layout,
|
||||
} from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import FileCard from '../file-content/components/FileCard';
|
||||
import { isFileMixItem } from '../../../utils/multimodal';
|
||||
|
||||
export interface FileItemListProps {
|
||||
message: IMessage;
|
||||
fileItemList: FileMixItem[];
|
||||
fileAttributeKeys?: IFileAttributeKeys;
|
||||
fileCopywriting?: IFileCopywritingConfig;
|
||||
readonly?: boolean;
|
||||
layout: Layout;
|
||||
showBackground: boolean;
|
||||
onCancel?: (params: IOnCancelUploadParams) => void;
|
||||
onCopy?: (params: IOnCopyUploadParams) => void;
|
||||
onRetry?: (params: IOnRetryUploadParams) => void;
|
||||
}
|
||||
|
||||
export const FileItemList: FC<FileItemListProps> = ({
|
||||
fileItemList,
|
||||
fileAttributeKeys,
|
||||
fileCopywriting,
|
||||
readonly,
|
||||
onRetry,
|
||||
onCancel,
|
||||
onCopy,
|
||||
message,
|
||||
layout,
|
||||
showBackground,
|
||||
}) => {
|
||||
/**
|
||||
* 处理点击取消上传的事件
|
||||
*/
|
||||
const handleCancel = () => {
|
||||
onCancel?.({ message, extra: {} });
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理重试上传的事件
|
||||
*/
|
||||
const handleRetry = () => {
|
||||
onRetry?.({ message, extra: {} });
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理拷贝文件地址的事件
|
||||
*/
|
||||
const handleCopy = () => {
|
||||
onCopy?.({ message, extra: {} });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{fileItemList.map(item => {
|
||||
if (isFileMixItem(item) && fileAttributeKeys) {
|
||||
return (
|
||||
<FileCard
|
||||
className="chat-uikit-multi-modal-file-image-content select-none"
|
||||
key={item.file.file_key}
|
||||
file={item.file}
|
||||
attributeKeys={fileAttributeKeys}
|
||||
tooltipsCopywriting={fileCopywriting?.tooltips}
|
||||
readonly={readonly}
|
||||
onCancel={handleCancel}
|
||||
onCopy={handleCopy}
|
||||
onRetry={handleRetry}
|
||||
layout={layout}
|
||||
showBackground={showBackground}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import { type ImageMixItem } from '@coze-common/chat-core';
|
||||
import {
|
||||
type IOnImageClickParams,
|
||||
type IMessage,
|
||||
} from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { SingleImageContentWithAutoSize } from '../single-image-content/auto-size';
|
||||
import { ImageBox } from '../image-content/image-box';
|
||||
import { typeSafeMessageBoxInnerVariants } from '../../../variants/message-box-inner-variants';
|
||||
import { makeFakeImageMessage } from '../../../utils/make-fake-image-message';
|
||||
|
||||
interface ImageItemListProps {
|
||||
imageItemList: ImageMixItem[];
|
||||
message: IMessage;
|
||||
onImageClick?: (params: IOnImageClickParams) => void;
|
||||
}
|
||||
|
||||
export const ImageItemList: FC<ImageItemListProps> = ({
|
||||
imageItemList,
|
||||
message,
|
||||
onImageClick,
|
||||
}) => {
|
||||
const handleImageClick = (originUrl: string) => {
|
||||
onImageClick?.({ message, extra: { url: originUrl } });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{Boolean(imageItemList.length) &&
|
||||
(imageItemList.length === 1 ? (
|
||||
<SingleImageContentWithAutoSize
|
||||
key={imageItemList[0].image.image_thumb.url}
|
||||
message={makeFakeImageMessage({
|
||||
originMessage: message,
|
||||
url: imageItemList[0].image.image_ori.url,
|
||||
key: imageItemList[0].image.image_ori.url,
|
||||
width: imageItemList[0].image.image_ori.width,
|
||||
height: imageItemList[0].image.image_ori.height,
|
||||
})}
|
||||
onImageClick={onImageClick}
|
||||
className="mb-[16px] rounded-[16px] overflow-hidden"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
// 这里借用了 messageBoxInner 的样式风格
|
||||
className={typeSafeMessageBoxInnerVariants({
|
||||
color: 'whiteness',
|
||||
border: null,
|
||||
tight: true,
|
||||
showBackground: false,
|
||||
})}
|
||||
style={{ width: '240px' }}
|
||||
key={imageItemList[0].image.image_thumb.url}
|
||||
>
|
||||
<ImageBox
|
||||
data={{ image_list: imageItemList.map(item => item.image) }}
|
||||
eventCallbacks={{
|
||||
onImageClick: (_, eventData) =>
|
||||
handleImageClick(eventData.src ?? ''),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
.chat-uikit-multi-modal-file-image-content {
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@@ -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 ReactNode } from 'react';
|
||||
|
||||
import {
|
||||
type FileMixItem,
|
||||
type TextMixItem,
|
||||
type ImageMixItem,
|
||||
} from '@coze-common/chat-core';
|
||||
import { type GetBotInfo } from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { type IImageMessageContentProps } from '../image-content';
|
||||
import { type IProps as IFileContentProps } from '../file-content';
|
||||
import {
|
||||
isFileMixItem,
|
||||
isImageMixItem,
|
||||
isMultimodalContentListLike,
|
||||
isTextMixItem,
|
||||
} from '../../../utils/multimodal';
|
||||
import { TextItemList } from './text-item-list';
|
||||
import { ImageItemList } from './image-item-list';
|
||||
import { FileItemList } from './file-item-list';
|
||||
|
||||
import './index.less';
|
||||
|
||||
export type MultimodalContentProps = IImageMessageContentProps &
|
||||
IFileContentProps & {
|
||||
getBotInfo: GetBotInfo;
|
||||
renderTextContentAddonTop?: ReactNode;
|
||||
isContentLoading: boolean | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* 这个组件并不单纯 实际上并不应该叫 Content
|
||||
*/
|
||||
|
||||
// TODO: @liushuoyan 提供开关啊~~
|
||||
export const MultimodalContent: React.FC<MultimodalContentProps> = ({
|
||||
renderTextContentAddonTop,
|
||||
message,
|
||||
getBotInfo,
|
||||
fileAttributeKeys,
|
||||
copywriting: fileCopywriting,
|
||||
onCancel,
|
||||
onCopy,
|
||||
onRetry,
|
||||
readonly,
|
||||
onImageClick,
|
||||
layout,
|
||||
showBackground,
|
||||
isContentLoading,
|
||||
}) => {
|
||||
const { content_obj } = message;
|
||||
if (!isMultimodalContentListLike(content_obj)) {
|
||||
// TODO: broke 的消息应该需要加一个统一的兜底和上报
|
||||
return null;
|
||||
}
|
||||
|
||||
const fileItemList = content_obj.item_list.filter(
|
||||
(item): item is FileMixItem => isFileMixItem(item),
|
||||
);
|
||||
|
||||
const textItemList = content_obj.item_list.filter(
|
||||
(item): item is TextMixItem => isTextMixItem(item),
|
||||
);
|
||||
|
||||
const imageItemList = content_obj.item_list.filter(
|
||||
(item): item is ImageMixItem => isImageMixItem(item),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FileItemList
|
||||
fileItemList={fileItemList}
|
||||
fileAttributeKeys={fileAttributeKeys}
|
||||
fileCopywriting={fileCopywriting}
|
||||
readonly={readonly}
|
||||
onRetry={onRetry}
|
||||
onCancel={onCancel}
|
||||
onCopy={onCopy}
|
||||
message={message}
|
||||
layout={layout}
|
||||
showBackground={showBackground}
|
||||
/>
|
||||
|
||||
<ImageItemList
|
||||
imageItemList={imageItemList}
|
||||
message={message}
|
||||
onImageClick={onImageClick}
|
||||
/>
|
||||
|
||||
<TextItemList
|
||||
textItemList={textItemList}
|
||||
renderTextContentAddonTop={renderTextContentAddonTop}
|
||||
message={message}
|
||||
showBackground={showBackground}
|
||||
getBotInfo={getBotInfo}
|
||||
isContentLoading={isContentLoading}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
MultimodalContent.displayName = 'MultimodalContent';
|
||||
@@ -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 { type ReactNode, type FC } from 'react';
|
||||
|
||||
import { type TextMixItem } from '@coze-common/chat-core';
|
||||
import { type GetBotInfo, type IMessage } from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { PlainTextContent } from '../plain-text-content';
|
||||
import { typeSafeMessageBoxInnerVariants } from '../../../variants/message-box-inner-variants';
|
||||
import { isTextMixItem } from '../../../utils/multimodal';
|
||||
|
||||
export interface TextItemListProps {
|
||||
textItemList: TextMixItem[];
|
||||
renderTextContentAddonTop: ReactNode;
|
||||
message: IMessage;
|
||||
showBackground: boolean;
|
||||
getBotInfo: GetBotInfo;
|
||||
isContentLoading: boolean | undefined;
|
||||
}
|
||||
|
||||
export const TextItemList: FC<TextItemListProps> = ({
|
||||
textItemList,
|
||||
renderTextContentAddonTop,
|
||||
message,
|
||||
showBackground,
|
||||
getBotInfo,
|
||||
isContentLoading,
|
||||
}) => (
|
||||
<>
|
||||
{textItemList.map(item => {
|
||||
if (isTextMixItem(item)) {
|
||||
const TextContentAddonTop = renderTextContentAddonTop;
|
||||
const isTextAndMentionedEmpty =
|
||||
!item.text && !message.mention_list.at(0);
|
||||
|
||||
if (isTextAndMentionedEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
/**
|
||||
* TODO: 由于目前设计不支持一条 message 渲染多个 content 这里需要借用一下发送消息的文字气泡背景色
|
||||
* 目前只有用户才能发送 multimodal 消息
|
||||
*/
|
||||
<div
|
||||
className={typeSafeMessageBoxInnerVariants({
|
||||
color: 'primary',
|
||||
border: null,
|
||||
tight: false,
|
||||
showBackground,
|
||||
})}
|
||||
style={{ width: 'fit-content' }}
|
||||
key={item.text}
|
||||
>
|
||||
{TextContentAddonTop}
|
||||
<PlainTextContent
|
||||
isContentLoading={isContentLoading}
|
||||
content={item.text}
|
||||
mentioned={message.mention_list.at(0)}
|
||||
getBotInfo={getBotInfo}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</>
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
# 介绍
|
||||
该user-text-content比较特殊,目前用户输入的文本应当不支持以markdown语法的富文本,因此需要特殊使用user-text-content这种content类型
|
||||
@@ -0,0 +1,4 @@
|
||||
.chat-uikit-plain-text-content {
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import { type MessageMentionListFields } from '@coze-common/chat-core/src/message/types';
|
||||
import {
|
||||
type IBaseContentProps,
|
||||
type GetBotInfo,
|
||||
} from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { ThinkingPlaceholder } from '../../chat';
|
||||
import { isText } from '../../../utils/is-text';
|
||||
|
||||
import './index.less';
|
||||
|
||||
export type IPlainTextMessageContentProps = Omit<
|
||||
IBaseContentProps,
|
||||
'message'
|
||||
> & {
|
||||
getBotInfo: GetBotInfo;
|
||||
content: string;
|
||||
mentioned: MessageMentionListFields['mention_list'][0] | undefined;
|
||||
isContentLoading: boolean | undefined;
|
||||
};
|
||||
|
||||
export const PlainTextContent: FC<IPlainTextMessageContentProps> = props => {
|
||||
const { content, isContentLoading } = props;
|
||||
|
||||
if (!isText(content)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chat-uikit-plain-text-content">
|
||||
{isContentLoading ? (
|
||||
<ThinkingPlaceholder className="!p-0 !h-20px" />
|
||||
) : (
|
||||
<span>{`${getMentionBotContent(props)}${content}`}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PlainTextContent.displayName = 'PlainTextContent';
|
||||
|
||||
const getMentionBotContent = ({
|
||||
mentioned,
|
||||
getBotInfo,
|
||||
}: IPlainTextMessageContentProps) => {
|
||||
// 接口真不一定返回了 mention_list
|
||||
if (!mentioned) {
|
||||
return '';
|
||||
}
|
||||
const name = getBotInfo(mentioned.id)?.nickname;
|
||||
if (!name) {
|
||||
return '';
|
||||
}
|
||||
return `@${name} `;
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import { typeSafeJsonParse } from '@coze-common/chat-area-utils';
|
||||
import { IconCozLoading } from '@coze-arch/coze-design/icons';
|
||||
import { Typography } from '@coze-arch/coze-design';
|
||||
|
||||
import { isFunctionCall } from '../../../utils/is-function-call';
|
||||
import { type ISimpleFunctionMessageContentProps } from './type';
|
||||
|
||||
export const SimpleFunctionContent: FC<
|
||||
ISimpleFunctionMessageContentProps
|
||||
> = props => {
|
||||
const { message, copywriting } = props;
|
||||
|
||||
const { content } = message;
|
||||
|
||||
const contentObj = typeSafeJsonParse(content, () => undefined);
|
||||
|
||||
if (!isFunctionCall(contentObj, message)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
// chat-uikit-simple-function-content
|
||||
className="coz-fg-hglt select-none flex items-center max-w-[230px] text-xxl leading-[26px]"
|
||||
>
|
||||
<IconCozLoading
|
||||
// chat-uikit-simple-function-content__prefix-icon
|
||||
className="animate-spin"
|
||||
/>
|
||||
<div
|
||||
// chat-uikit-simple-function-content__prefix-text
|
||||
className="mr-[4px] ml-[8px]"
|
||||
>
|
||||
{copywriting?.using ?? 'using'}
|
||||
</div>
|
||||
<Typography.Text
|
||||
// chat-uikit-simple-function-content__plugin-name
|
||||
className="coz-fg-hglt flex-1 text-xxl font-bold leading-[26px]"
|
||||
ellipsis={{
|
||||
showTooltip: {
|
||||
opts: {
|
||||
content: contentObj.name,
|
||||
style: { wordWrap: 'inherit' },
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{contentObj.name}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SimpleFunctionContent.displayName = 'SimpleFunctionContent';
|
||||
@@ -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 {
|
||||
type ISimpleFunctionContentCopywriting,
|
||||
type IBaseContentProps,
|
||||
} from '@coze-common/chat-uikit-shared';
|
||||
|
||||
export type ISimpleFunctionMessageContentProps = IBaseContentProps & {
|
||||
copywriting?: ISimpleFunctionContentCopywriting;
|
||||
};
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState, type FC } from 'react';
|
||||
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import classNames from 'classnames';
|
||||
import { type ContentType, type Message } from '@coze-common/chat-core';
|
||||
import {
|
||||
safeAsyncThrow,
|
||||
typeSafeJsonParseEnhanced,
|
||||
} from '@coze-common/chat-area-utils';
|
||||
import { Skeleton } from '@coze-arch/coze-design';
|
||||
import { type IImageContent } from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { type IImageMessageContentProps } from '../image-content';
|
||||
import { isImage } from '../../../utils/is-image';
|
||||
import { getImageDisplayAttribute } from '../../../utils/image/get-image-display-attribute';
|
||||
import { useUiKitMessageBoxContext } from '../../../context/message-box';
|
||||
import DefaultImage from '../../../assets/image-default.png';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface ImageInfo {
|
||||
url: string;
|
||||
displayWidth: number;
|
||||
displayHeight: number;
|
||||
}
|
||||
type IBlobImageMap = Record<string, ImageInfo>;
|
||||
|
||||
interface SingleImageContentWithAutoSizeProps
|
||||
extends IImageMessageContentProps {
|
||||
content_obj: IImageContent;
|
||||
}
|
||||
|
||||
export const SingleImageContentWithAutoSize: FC<
|
||||
IImageMessageContentProps
|
||||
> = props => {
|
||||
const { message } = props;
|
||||
|
||||
const {
|
||||
content_obj = typeSafeJsonParseEnhanced<Message<ContentType.Image>>({
|
||||
str: message.content,
|
||||
onParseError: e => {
|
||||
safeAsyncThrow(e.message);
|
||||
},
|
||||
onVerifyError: e => {
|
||||
safeAsyncThrow(e.message);
|
||||
},
|
||||
verifyStruct: (sth: unknown): sth is Message<ContentType.Image> =>
|
||||
Boolean(sth && 'image_list' in { ...sth }),
|
||||
}),
|
||||
} = message;
|
||||
// 类型守卫,一般情况也不影响hooks的顺序问题
|
||||
if (!isImage(content_obj)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<SingleImageContentWithAutoSizeImpl content_obj={content_obj} {...props} />
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 这里这么做是有原因的
|
||||
* 前端计算groupId是通过replyId分组(服务端未ack前是localMessageId)
|
||||
* 因此服务端ack后会导致循环的key发生变化,导致组件unmount -> mount(销毁重建)
|
||||
* 因此需要用比较trick的方式来实现图片展示优化的问题
|
||||
*/
|
||||
const blobImageMap: IBlobImageMap = {};
|
||||
const isBlob = (url: string) => url?.startsWith('blob:');
|
||||
|
||||
const SingleImageContentWithAutoSizeImpl: FC<
|
||||
SingleImageContentWithAutoSizeProps
|
||||
> = props => {
|
||||
const { message, onImageClick, className, content_obj } = props;
|
||||
const { imageAutoSizeContainerWidth = 0 } = useUiKitMessageBoxContext();
|
||||
const localMessageId = message.extra_info.local_message_id;
|
||||
|
||||
// 目前服务端下发的图片 ori = thumb 因此目前用一个就行
|
||||
const currentImageUrl = content_obj?.image_list?.at(0)?.image_ori?.url ?? '';
|
||||
|
||||
const { displayHeight, displayWidth, isCover } = getImageDisplayAttribute(
|
||||
content_obj.image_list.at(0)?.image_ori.width ?? 0,
|
||||
content_obj.image_list.at(0)?.image_ori.height ?? 0,
|
||||
imageAutoSizeContainerWidth,
|
||||
);
|
||||
|
||||
if (isBlob(currentImageUrl) && imageAutoSizeContainerWidth > 0) {
|
||||
blobImageMap[localMessageId] = {
|
||||
url: currentImageUrl,
|
||||
displayHeight,
|
||||
displayWidth,
|
||||
};
|
||||
}
|
||||
|
||||
const [imageInfo, setImageInfo] = useState<ImageInfo>(
|
||||
blobImageMap[localMessageId] ?? {
|
||||
url: currentImageUrl,
|
||||
displayWidth,
|
||||
displayHeight,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const preloadImage = new Image();
|
||||
|
||||
if (currentImageUrl.startsWith('http')) {
|
||||
preloadImage.src = currentImageUrl;
|
||||
preloadImage.onload = () => {
|
||||
setImageInfo({
|
||||
url: currentImageUrl,
|
||||
displayHeight,
|
||||
displayWidth,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
return () => {
|
||||
preloadImage.onload = null;
|
||||
};
|
||||
}, [currentImageUrl, imageAutoSizeContainerWidth]);
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
loading={isEmpty(imageInfo?.url)}
|
||||
style={{
|
||||
width: imageInfo?.displayWidth,
|
||||
height: imageInfo?.displayHeight,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={imageInfo?.url ?? DefaultImage}
|
||||
style={{
|
||||
width: imageInfo?.displayWidth,
|
||||
height: imageInfo?.displayHeight,
|
||||
maxWidth: '100%',
|
||||
objectFit: isCover ? 'cover' : undefined,
|
||||
objectPosition: 'left top',
|
||||
}}
|
||||
onClick={e =>
|
||||
onImageClick?.({
|
||||
message,
|
||||
extra: {
|
||||
url: imageInfo?.url,
|
||||
},
|
||||
})
|
||||
}
|
||||
className={classNames('block', className, {
|
||||
'cursor-zoom-in': Boolean(onImageClick),
|
||||
})}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
};
|
||||
|
||||
SingleImageContentWithAutoSize.displayName = 'SingleImageContentWithAutoSize';
|
||||
@@ -0,0 +1,19 @@
|
||||
.chat-uikit-single-image-content {
|
||||
user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&__image {
|
||||
>img {
|
||||
cursor: zoom-in;
|
||||
|
||||
min-width: 48px;
|
||||
max-width: 240px;
|
||||
min-height: 48px;
|
||||
max-height: 240px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 { useState, type FC, useEffect } from 'react';
|
||||
|
||||
import { type IImageMessageContentProps } from '../image-content';
|
||||
import { safeJSONParse } from '../../../utils/safe-json-parse';
|
||||
import { isImage } from '../../../utils/is-image';
|
||||
import { SingleImageContentUI } from './single-image-content-ui';
|
||||
|
||||
import './index.less';
|
||||
|
||||
type IBlobImageMap = Record<string, string>;
|
||||
|
||||
/**
|
||||
* 这里这么做是有原因的
|
||||
* 前端计算groupId是通过replyId分组(服务端未ack前是localMessageId)
|
||||
* 因此服务端ack后会导致循环的key发生变化,导致组件unmount -> mount(销毁重建)
|
||||
* 因此需要用比较trick的方式来实现图片展示优化的问题
|
||||
*/
|
||||
const blobImageMap: IBlobImageMap = {};
|
||||
const isBlob = (url: string) => url?.startsWith('blob:');
|
||||
|
||||
/**
|
||||
* @deprecated 废弃不再维护,请尽快迁移至 SingleImageContentWithAutoSize 组件
|
||||
*/
|
||||
export const SingleImageContent: FC<IImageMessageContentProps> = props => {
|
||||
const { message, onImageClick } = props;
|
||||
|
||||
// @liushuoyan 这里类型大溃败,引入了 any
|
||||
const { content_obj = safeJSONParse(message.content) } = message;
|
||||
|
||||
const localMessageId = message.extra_info.local_message_id;
|
||||
|
||||
// 目前服务端下发的图片 ori = thumb 因此目前用一个就行
|
||||
const currentImageUrl = content_obj?.image_list?.at(0)?.image_ori?.url ?? '';
|
||||
|
||||
if (isBlob(currentImageUrl)) {
|
||||
blobImageMap[localMessageId] = currentImageUrl;
|
||||
}
|
||||
|
||||
const [imageUrl, setImageUrl] = useState<string>(
|
||||
isBlob(currentImageUrl) ? currentImageUrl : blobImageMap[localMessageId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const preloadImage = new Image();
|
||||
if (currentImageUrl.startsWith('http')) {
|
||||
preloadImage.src = currentImageUrl;
|
||||
preloadImage.onload = () => {
|
||||
setImageUrl(currentImageUrl);
|
||||
};
|
||||
}
|
||||
|
||||
return () => {
|
||||
preloadImage.onload = null;
|
||||
};
|
||||
}, [currentImageUrl]);
|
||||
|
||||
if (!isImage(content_obj)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SingleImageContentUI
|
||||
onClick={originUrl => {
|
||||
onImageClick?.({
|
||||
message,
|
||||
extra: { url: originUrl },
|
||||
});
|
||||
}}
|
||||
thumbUrl={imageUrl}
|
||||
originalUrl={imageUrl}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
SingleImageContent.displayName = 'SingleImageContent';
|
||||
@@ -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 classNames from 'classnames';
|
||||
import { Image } from '@coze-arch/coze-design';
|
||||
|
||||
import EmptyImage from '../../../assets/image-empty.png';
|
||||
|
||||
import './index.less';
|
||||
|
||||
export interface SingleImageContentUIProps {
|
||||
thumbUrl: string;
|
||||
originalUrl: string;
|
||||
onClick?: (originUrl: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const SingleImageContentUI: React.FC<SingleImageContentUIProps> = ({
|
||||
thumbUrl,
|
||||
originalUrl,
|
||||
onClick,
|
||||
className,
|
||||
}) => (
|
||||
<div
|
||||
className={classNames(className, 'chat-uikit-single-image-content')}
|
||||
onClick={() => onClick?.(originalUrl)}
|
||||
>
|
||||
<Image
|
||||
src={thumbUrl || EmptyImage}
|
||||
className="chat-uikit-single-image-content__image"
|
||||
/**
|
||||
* 这里不采用 semi Image 组件自带的 preview 功能。传入的 onImageClick 回调中有副作用会拉起 preview 组件
|
||||
*/
|
||||
preview={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
SingleImageContentUI.displayName = 'SingleImageContentUI';
|
||||
@@ -0,0 +1,12 @@
|
||||
|
||||
|
||||
// 特别处理 有背景图的情况下 不区分主题深浅色
|
||||
.chat-uikit-suggestion-item-background-mg {
|
||||
&:hover {
|
||||
background-color: rgba(235, 235, 235, 75%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: rgba(235, 235, 235, 70%);;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { type IMessage } from '@coze-common/chat-uikit-shared';
|
||||
|
||||
import { isText } from '../../../../../utils/is-text';
|
||||
import { typeSafeSuggestionItemVariants } from './variants';
|
||||
import './index.less';
|
||||
|
||||
interface ISuggestionItemProps {
|
||||
message?: Pick<IMessage, 'content_obj' | 'sender_id'>;
|
||||
content?: string;
|
||||
readonly?: boolean;
|
||||
showBackground?: boolean;
|
||||
className?: string;
|
||||
color?: 'white' | 'grey';
|
||||
onSuggestionClick?: (sugParam: {
|
||||
text: string;
|
||||
mentionList: { id: string }[];
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export const SuggestionItem: FC<ISuggestionItemProps> = props => {
|
||||
const {
|
||||
content,
|
||||
message,
|
||||
readonly,
|
||||
onSuggestionClick,
|
||||
showBackground,
|
||||
className,
|
||||
color,
|
||||
} = props;
|
||||
const { content_obj = content } = message ?? {};
|
||||
|
||||
if (!isText(content_obj)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
'!bg-[235, 235, 235, 0.75]',
|
||||
typeSafeSuggestionItemVariants({
|
||||
showBackground: Boolean(showBackground),
|
||||
readonly: Boolean(readonly),
|
||||
color: color ?? 'white',
|
||||
}),
|
||||
)}
|
||||
onClick={() => {
|
||||
if (readonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
const senderId = message?.sender_id;
|
||||
onSuggestionClick?.({
|
||||
text: content_obj,
|
||||
mentionList: senderId ? [{ id: senderId }] : [],
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span className="w-full">{content_obj}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SuggestionItem.displayName = 'SuggestionItem';
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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 { cva, type VariantProps } from 'class-variance-authority';
|
||||
const suggestionItemVariants = cva(
|
||||
[
|
||||
'w-fit',
|
||||
'border-[1px]',
|
||||
'border-solid',
|
||||
'rounded-normal',
|
||||
'coz-fg-primary',
|
||||
'py-6px',
|
||||
'px-16px',
|
||||
'flex',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'mb-8px',
|
||||
'max-w-full',
|
||||
'text-[14px]',
|
||||
'font-normal',
|
||||
'leading-[20px]',
|
||||
'break-words',
|
||||
],
|
||||
{
|
||||
variants: {
|
||||
showBackground: {
|
||||
true: ['coz-bg-image-question', 'coz-stroke-image-bots'],
|
||||
false: ['coz-stroke-plus'],
|
||||
},
|
||||
color: {
|
||||
white: [],
|
||||
grey: [],
|
||||
},
|
||||
readonly: {
|
||||
true: ['cursor-default'],
|
||||
false: ['cursor-pointer'],
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
showBackground: false,
|
||||
color: 'white',
|
||||
className: [],
|
||||
},
|
||||
{
|
||||
showBackground: false,
|
||||
color: 'grey',
|
||||
className: ['bg-[var(--coz-mg-secondary)]'],
|
||||
},
|
||||
{
|
||||
readonly: false,
|
||||
showBackground: false,
|
||||
className: [
|
||||
'hover:bg-[var(--coz-mg-secondary-hovered)]',
|
||||
'active:bg-[var(--coz-mg-secondary-pressed)]',
|
||||
],
|
||||
},
|
||||
{
|
||||
readonly: false,
|
||||
showBackground: true,
|
||||
className: ['chat-uikit-suggestion-item-background-mg'],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
type SuggestionItemVariantsProps = VariantProps<typeof suggestionItemVariants>;
|
||||
|
||||
export const typeSafeSuggestionItemVariants: (
|
||||
props: Required<SuggestionItemVariantsProps>,
|
||||
) => string = suggestionItemVariants;
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import { SuggestionItem } from './components/suggestion-item';
|
||||
|
||||
export { SuggestionItem };
|
||||
@@ -0,0 +1,3 @@
|
||||
.chat-uikit-text-content {
|
||||
word-break: break-word;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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 MouseEvent, type FC, useRef } from 'react';
|
||||
|
||||
import {
|
||||
type IOnImageClickParams,
|
||||
type IOnLinkClickParams,
|
||||
type IBaseContentProps,
|
||||
type MdBoxProps,
|
||||
} from '@coze-common/chat-uikit-shared';
|
||||
import { Image } from '@coze-arch/bot-md-box-adapter/slots';
|
||||
import { type ImageOptions } from '@coze-arch/bot-md-box-adapter';
|
||||
|
||||
import { CozeLink } from '../../md-box-slots/link';
|
||||
import { CozeImage } from '../../md-box-slots/coze-image';
|
||||
import { LazyCozeMdBox } from '../../common/coze-md-box/lazy';
|
||||
import { isText } from '../../../utils/is-text';
|
||||
import './index.less';
|
||||
|
||||
export type IMessageContentProps = IBaseContentProps & {
|
||||
onImageClick?: (params: IOnImageClickParams) => void;
|
||||
mdBoxProps?: MdBoxProps;
|
||||
enableAutoSizeImage?: boolean;
|
||||
imageOptions?: ImageOptions;
|
||||
onLinkClick?: (
|
||||
params: IOnLinkClickParams,
|
||||
event: MouseEvent<Element, globalThis.MouseEvent>,
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const TextContent: FC<IMessageContentProps> = props => {
|
||||
const {
|
||||
message,
|
||||
readonly,
|
||||
onImageClick,
|
||||
onLinkClick,
|
||||
mdBoxProps,
|
||||
enableAutoSizeImage,
|
||||
imageOptions,
|
||||
} = props;
|
||||
const MdBoxLazy = LazyCozeMdBox;
|
||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { content } = message;
|
||||
|
||||
if (!isText(content)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isStreaming = !message.is_finish;
|
||||
const text = content.slice(0, message.broken_pos ?? Infinity);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="chat-uikit-text-content"
|
||||
data-testid="bot.ide.chat_area.message.text-answer-message-content"
|
||||
ref={contentRef}
|
||||
data-grab-mark={message.message_id}
|
||||
data-grab-source={message.source}
|
||||
>
|
||||
<MdBoxLazy
|
||||
markDown={text}
|
||||
autoFixSyntax={{ autoFixEnding: isStreaming }}
|
||||
showIndicator={isStreaming}
|
||||
smooth={isStreaming}
|
||||
imageOptions={{ forceHttps: !IS_OPEN_SOURCE, ...imageOptions }}
|
||||
eventCallbacks={{
|
||||
onImageClick: (e, eventData) => {
|
||||
eventData.src &&
|
||||
onImageClick?.({
|
||||
message,
|
||||
extra: { url: eventData.src },
|
||||
});
|
||||
},
|
||||
onLinkClick: (e, eventData) => {
|
||||
onLinkClick?.(
|
||||
{
|
||||
message,
|
||||
extra: { ...eventData },
|
||||
},
|
||||
e,
|
||||
);
|
||||
|
||||
if (readonly) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
},
|
||||
}}
|
||||
{...mdBoxProps}
|
||||
slots={{
|
||||
Image: enableAutoSizeImage ? CozeImage : Image,
|
||||
Link: CozeLink,
|
||||
...mdBoxProps?.slots,
|
||||
}}
|
||||
></MdBoxLazy>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TextContent.displayName = 'TextContent';
|
||||
@@ -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 * from './chat';
|
||||
export * from './common';
|
||||
export * from './contents';
|
||||
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
* 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 react-hooks/rules-of-hooks */
|
||||
import {
|
||||
type CSSProperties,
|
||||
useEffect,
|
||||
useState,
|
||||
type FC,
|
||||
useRef,
|
||||
memo,
|
||||
} from 'react';
|
||||
|
||||
import { isEqual } from 'lodash-es';
|
||||
import classNames from 'classnames';
|
||||
import { ImagePreview, Skeleton } from '@coze-arch/coze-design';
|
||||
import { ImageStatus } from '@coze-arch/bot-md-box-adapter/lazy';
|
||||
import { type MdBoxImageProps } from '@coze-arch/bot-md-box-adapter';
|
||||
|
||||
import { getImageDisplayAttribute } from '../../utils/image/get-image-display-attribute';
|
||||
import { useOnboardingContext } from '../../context/onboarding';
|
||||
import { useUiKitMessageBoxContext } from '../../context/message-box';
|
||||
import DefaultImage from '../../assets/image-default.png';
|
||||
|
||||
interface OriginImageInfo {
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export type CozeImageProps = MdBoxImageProps & {
|
||||
imageAutoSizeContainerWidth?: number;
|
||||
enablePreview?: boolean;
|
||||
onImageElementEnter?: (params: {
|
||||
element: HTMLElement;
|
||||
link: string;
|
||||
}) => void;
|
||||
onImageElementLeave?: (params: {
|
||||
element: HTMLElement;
|
||||
link: string;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
const TIME_OUT = 10000;
|
||||
|
||||
// TODO: @liushuoyan 看看这里能不能搞一个插槽之类的东西
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, @coze-arch/max-line-per-function
|
||||
export const _CozeImage: FC<CozeImageProps> = props => {
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
const {
|
||||
className,
|
||||
src,
|
||||
onImageClick,
|
||||
imageAutoSizeContainerWidth: imageAutoSizeContainerWidthFromProps,
|
||||
enablePreview,
|
||||
style,
|
||||
} = props;
|
||||
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const {
|
||||
imageAutoSizeContainerWidth: imageAutoSizeContainerWidthFromContext,
|
||||
eventCallbacks,
|
||||
onError,
|
||||
} = useUiKitMessageBoxContext();
|
||||
|
||||
const {
|
||||
onMdBoxImageElementEnter: onImageElementEnterFromEventCallback,
|
||||
onMdBoxImageElementLeave: onImageElementLeaveFromEventCallback,
|
||||
} = eventCallbacks ?? {};
|
||||
|
||||
const {
|
||||
imageAutoSizeContainerWidth:
|
||||
imageAutoSizeContainerWidthFromOnboardingContext,
|
||||
eventCallbacks: eventCallbacksFromOnboarding,
|
||||
} = useOnboardingContext();
|
||||
|
||||
const {
|
||||
onMdBoxImageElementEnter: onMdBoxImageElementEnterFromOnboarding,
|
||||
onMdBoxImageElementLeave: onMdBoxImageElementLeaveFromOnboarding,
|
||||
} = eventCallbacksFromOnboarding ?? {};
|
||||
|
||||
const onImageElementEnter =
|
||||
props.onImageElementEnter ??
|
||||
onImageElementEnterFromEventCallback ??
|
||||
onMdBoxImageElementEnterFromOnboarding;
|
||||
|
||||
const onImageElementLeave =
|
||||
props.onImageElementLeave ??
|
||||
onImageElementLeaveFromEventCallback ??
|
||||
onMdBoxImageElementLeaveFromOnboarding;
|
||||
|
||||
const imageAutoSizeContainerWidth =
|
||||
imageAutoSizeContainerWidthFromProps ??
|
||||
imageAutoSizeContainerWidthFromContext ??
|
||||
imageAutoSizeContainerWidthFromOnboardingContext;
|
||||
|
||||
const originImageInfoRef = useRef<OriginImageInfo>({});
|
||||
const [imageStyles, setImageStyles] = useState<CSSProperties>({});
|
||||
|
||||
// 加载状态
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [imageSrc, setImageSrc] = useState(DefaultImage);
|
||||
|
||||
const timeout = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const resetImageSize = (width: number, height: number) => {
|
||||
if (!imageAutoSizeContainerWidth) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { displayHeight, displayWidth, isCover } = getImageDisplayAttribute(
|
||||
width,
|
||||
height,
|
||||
imageAutoSizeContainerWidth,
|
||||
);
|
||||
|
||||
setImageStyles({
|
||||
display: 'block',
|
||||
width: displayWidth,
|
||||
height: displayHeight,
|
||||
objectFit: isCover ? 'cover' : undefined,
|
||||
objectPosition: 'left top',
|
||||
});
|
||||
};
|
||||
|
||||
const clearImageErrorTimeout = () => {
|
||||
if (!timeout.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(timeout.current);
|
||||
};
|
||||
|
||||
const builtinLoadImage = ({ loadImageSrc }: { loadImageSrc: string }) => {
|
||||
const image = new Image();
|
||||
image.src = loadImageSrc;
|
||||
|
||||
clearImageErrorTimeout();
|
||||
|
||||
image.onload = () => {
|
||||
clearImageErrorTimeout();
|
||||
originImageInfoRef.current = {
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
};
|
||||
resetImageSize(image.width, image.height);
|
||||
setImageSrc(loadImageSrc);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
image.onerror = () => {
|
||||
clearImageErrorTimeout();
|
||||
timeout.current = setTimeout(() => {
|
||||
setImageSrc(DefaultImage);
|
||||
setLoading(false);
|
||||
|
||||
onError?.(new Error('coze image load error: time out'));
|
||||
}, TIME_OUT);
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
|
||||
builtinLoadImage({
|
||||
loadImageSrc: src ?? '',
|
||||
});
|
||||
}, [src]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!imageAutoSizeContainerWidth || !originImageInfoRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!originImageInfoRef.current.width ||
|
||||
!originImageInfoRef.current.height
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
resetImageSize(
|
||||
originImageInfoRef.current.width,
|
||||
originImageInfoRef.current.height,
|
||||
);
|
||||
}, [imageAutoSizeContainerWidth, originImageInfoRef.current]);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (!containerRef.current) {
|
||||
return;
|
||||
}
|
||||
onImageElementEnter?.({
|
||||
element: containerRef.current,
|
||||
link: src ?? '',
|
||||
});
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (!containerRef.current) {
|
||||
return;
|
||||
}
|
||||
onImageElementLeave?.({
|
||||
element: containerRef.current,
|
||||
link: src ?? '',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={classNames('w-fit', className)}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
style={style}
|
||||
>
|
||||
{enablePreview && src ? (
|
||||
<ImagePreview
|
||||
src={src ?? ''}
|
||||
visible={showPreview}
|
||||
onVisibleChange={() => setShowPreview(false)}
|
||||
/>
|
||||
) : null}
|
||||
<Skeleton
|
||||
style={{ width: 240, height: 120 }}
|
||||
className="coz-mg-secondary"
|
||||
loading={loading}
|
||||
active
|
||||
>
|
||||
<img
|
||||
src={imageSrc ?? ''}
|
||||
style={imageStyles}
|
||||
className={classNames('rounded-[8px]', className, {
|
||||
'cursor-zoom-in': Boolean(onImageClick) || enablePreview,
|
||||
})}
|
||||
onClick={e => {
|
||||
onImageClick?.(e, {
|
||||
src: imageSrc ?? '',
|
||||
status: ImageStatus.Success,
|
||||
});
|
||||
|
||||
if (enablePreview) {
|
||||
setShowPreview(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Skeleton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CozeImage = memo(_CozeImage, (prevProps, nextProps) =>
|
||||
isEqual(prevProps, nextProps),
|
||||
);
|
||||
|
||||
export const CozeImageWithPreview: FC<CozeImageProps> = props => (
|
||||
<CozeImage {...props} enablePreview={true} />
|
||||
);
|
||||
|
||||
CozeImageWithPreview.displayName = 'CozeImageWithPreview';
|
||||
@@ -0,0 +1,229 @@
|
||||
/*
|
||||
* 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 FC, useEffect, useRef } from 'react';
|
||||
|
||||
import { isObject, isString, omit } from 'lodash-es';
|
||||
import cs from 'classnames';
|
||||
import { typeSafeJsonParse } from '@coze-common/chat-area-utils';
|
||||
import {
|
||||
LinkType,
|
||||
type MdBoxLinkProps,
|
||||
} from '@coze-arch/bot-md-box-adapter/lazy';
|
||||
|
||||
import { useOnboardingContext } from '../../../context/onboarding';
|
||||
import { useUiKitMessageBoxContext } from '../../../context/message-box';
|
||||
import { safeParseUrl } from './utils';
|
||||
|
||||
const isHttpLink = (link: string) => {
|
||||
const parsedLink = safeParseUrl(link);
|
||||
|
||||
if (!parsedLink) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parsedLink.protocol === 'http:' || parsedLink.protocol === 'https:';
|
||||
};
|
||||
|
||||
const isCocoLink = (link: string) => {
|
||||
const parsedLink = safeParseUrl(link);
|
||||
|
||||
if (!parsedLink) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parsedLink.protocol === 'coco:';
|
||||
};
|
||||
|
||||
/** 被链接元素替换成的组件 */
|
||||
export const CozeLink: FC<
|
||||
MdBoxLinkProps & {
|
||||
onLinkElementEnter?: (params: {
|
||||
element: HTMLElement;
|
||||
link: string;
|
||||
}) => void;
|
||||
onLinkElementLeave?: (params: {
|
||||
element: HTMLElement;
|
||||
link: string;
|
||||
}) => void;
|
||||
}
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function
|
||||
> = ({
|
||||
className,
|
||||
style,
|
||||
href,
|
||||
children,
|
||||
onSendMessage,
|
||||
onLinkClick,
|
||||
onLinkRender,
|
||||
onOpenLink,
|
||||
type: _type,
|
||||
onLinkElementEnter: onLinkElementEnterFromProps,
|
||||
onLinkElementLeave: onLinkElementLeaveFromProps,
|
||||
...restProps
|
||||
}) => {
|
||||
const handleOpenLink = (url?: string) => {
|
||||
if (onOpenLink) {
|
||||
onOpenLink?.(url);
|
||||
return;
|
||||
}
|
||||
|
||||
window.open(url);
|
||||
};
|
||||
const parsedUrl = href ? safeParseUrl(href) : null;
|
||||
|
||||
const containerRef = useRef<HTMLAnchorElement | null>(null);
|
||||
|
||||
const { eventCallbacks } = useUiKitMessageBoxContext();
|
||||
|
||||
const { eventCallbacks: eventCallbacksFromOnboarding } =
|
||||
useOnboardingContext();
|
||||
const {
|
||||
onMdBoxLinkElementEnter: onLinkElementEnterFromEventCallback,
|
||||
onMdBoxLinkElementLeave: onLinkElementLeaveFromEventCallback,
|
||||
} = eventCallbacks ?? {};
|
||||
|
||||
const {
|
||||
onMdBoxLinkElementEnter: onMdBoxLinkElementEnterFromOnboarding,
|
||||
onMdBoxLinkElementLeave: onMdBoxLinkElementLeaveFromOnboarding,
|
||||
} = eventCallbacksFromOnboarding ?? {};
|
||||
|
||||
const onLinkElementEnter =
|
||||
onLinkElementEnterFromProps ??
|
||||
onLinkElementEnterFromEventCallback ??
|
||||
onMdBoxLinkElementEnterFromOnboarding;
|
||||
|
||||
const onLinkElementLeave =
|
||||
onLinkElementLeaveFromProps ??
|
||||
onLinkElementLeaveFromEventCallback ??
|
||||
onMdBoxLinkElementLeaveFromOnboarding;
|
||||
|
||||
const onClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
/* istanbul ignore if */
|
||||
if (!href || !parsedUrl) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCocoLink(href)) {
|
||||
event.preventDefault();
|
||||
const message = parsedUrl.searchParams.get('msg');
|
||||
const ext = parsedUrl.searchParams.get('ext');
|
||||
const extObj = ext
|
||||
? typeSafeJsonParse(ext, error => {
|
||||
reportError(error);
|
||||
})
|
||||
: undefined;
|
||||
const wikiLink =
|
||||
isObject(extObj) &&
|
||||
's$wiki_link' in extObj &&
|
||||
isString(extObj?.s$wiki_link)
|
||||
? extObj?.s$wiki_link
|
||||
: '';
|
||||
|
||||
/* istanbul ignore if */
|
||||
if (wikiLink) {
|
||||
if (isHttpLink(wikiLink)) {
|
||||
onLinkClick?.(event, {
|
||||
url: href,
|
||||
parsedUrl,
|
||||
exts: { wiki_link: wikiLink, type: LinkType.wiki },
|
||||
openLink: handleOpenLink,
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (message) {
|
||||
onSendMessage?.(message);
|
||||
return;
|
||||
}
|
||||
|
||||
onLinkClick?.(event, {
|
||||
url: href,
|
||||
parsedUrl,
|
||||
exts: { type: LinkType.coco },
|
||||
openLink: handleOpenLink,
|
||||
});
|
||||
}
|
||||
|
||||
if (!isHttpLink(href)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (onLinkClick) {
|
||||
onLinkClick(event, {
|
||||
url: href,
|
||||
parsedUrl,
|
||||
exts: {
|
||||
type: LinkType.normal,
|
||||
},
|
||||
openLink: handleOpenLink,
|
||||
});
|
||||
} else {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
window.open(href);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (href && parsedUrl) {
|
||||
onLinkRender?.({
|
||||
url: href,
|
||||
parsedUrl,
|
||||
});
|
||||
}
|
||||
}, [href]);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (!containerRef.current) {
|
||||
return;
|
||||
}
|
||||
onLinkElementEnter?.({
|
||||
element: containerRef.current,
|
||||
link: href ?? '',
|
||||
});
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (!containerRef.current) {
|
||||
return;
|
||||
}
|
||||
onLinkElementLeave?.({
|
||||
element: containerRef.current,
|
||||
link: href ?? '',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<a
|
||||
{...omit(restProps, 'href')}
|
||||
className={cs(['!coz-fg-hglt'], className)}
|
||||
style={style}
|
||||
onClick={onClick}
|
||||
href={parsedUrl ? href : undefined}
|
||||
target="_blank"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
ref={containerRef}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
@@ -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 const safeParseUrl = (url: string) => {
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user