feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect, useRef, useState } from 'react';
interface ScrollControlProps {
activeTab: string;
tabs: string[];
loading?: boolean;
data?: Record<string, unknown>;
}
export const useScrollControl = ({
activeTab,
tabs,
loading,
data,
}: ScrollControlProps) => {
const scrollRefs = useRef<(HTMLDivElement | null)[]>([]);
const [canScrollLeft, setCanScrollLeft] = useState(false);
const [canScrollRight, setCanScrollRight] = useState(false);
const getTabIndex = (tab: string) => tabs.indexOf(tab);
const checkScrollable = (index: number) => {
const scrollRef = scrollRefs.current[index];
if (scrollRef) {
const { scrollLeft, scrollWidth, clientWidth } = scrollRef;
setCanScrollLeft(scrollLeft > 0);
setCanScrollRight(scrollLeft < scrollWidth - clientWidth - 10);
}
};
const handleScroll = (direction: 'left' | 'right') => {
const index = getTabIndex(activeTab);
if (scrollRefs.current[index]) {
const scrollAmount = 300;
const newScrollLeft =
scrollRefs.current[index].scrollLeft +
(direction === 'left' ? -scrollAmount : scrollAmount);
scrollRefs.current[index].scrollTo({
left: newScrollLeft,
behavior: 'smooth',
});
}
};
useEffect(() => {
const handleResize = () => checkScrollable(getTabIndex(activeTab));
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [activeTab]);
useEffect(() => {
if (!loading && data?.[activeTab]) {
setTimeout(() => checkScrollable(getTabIndex(activeTab)), 0);
}
}, [loading, data, activeTab]);
useEffect(() => {
const scrollElement = scrollRefs.current[getTabIndex(activeTab)];
if (!scrollElement) {
return;
}
const onScroll = () => checkScrollable(getTabIndex(activeTab));
scrollElement.addEventListener('scroll', onScroll);
checkScrollable(getTabIndex(activeTab));
return () => scrollElement.removeEventListener('scroll', onScroll);
}, [data, activeTab]);
return {
scrollRefs,
canScrollLeft,
canScrollRight,
handleScroll,
};
};

View File

@@ -0,0 +1,139 @@
/*
* 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, useState } from 'react';
import { useRequest } from 'ahooks';
import {
type LibraryResourceListResponse,
type LibraryResourceListRequest,
ResType,
} from '@coze-arch/idl/plugin_develop';
import { type GetOfficialPromptResourceListResponse } from '@coze-arch/idl/playground_api';
import { PlaygroundApi, PluginDevelopApi } from '@coze-arch/bot-api';
interface LibraryInfo {
id: string;
name: string;
description: string;
promptText?: string;
}
export const useGetLibrarys = () => {
const {
runAsync: runRecommendLibrary,
loading: loadingRecommendLibrary,
data: dataRecommendLibrary,
} = useGetRecommendLibrarys();
const {
runAsync: runTeamLibrary,
loading: loadingTeamLibrary,
data: dataTeamLibrary,
} = useGetTeamLibrarys();
return {
loading: loadingRecommendLibrary || loadingTeamLibrary,
data: {
Recommended: dataRecommendLibrary ?? [],
Team: dataTeamLibrary ?? [],
},
runAsync: (
type: 'Recommended' | 'Team',
options: LibraryResourceListRequest,
) => {
if (type === 'Recommended') {
return runRecommendLibrary({ size: options.size });
}
if (type === 'Team') {
return runTeamLibrary(options);
}
},
};
};
export const useGetRecommendLibrarys = (): {
data: LibraryInfo[] | undefined;
runAsync: (options: {
size?: number;
}) => Promise<GetOfficialPromptResourceListResponse>;
loading: boolean;
} => {
const size = useRef<number | undefined>();
const [slicedData, setSlicedData] =
useState<GetOfficialPromptResourceListResponse['data']>();
const { runAsync, loading } = useRequest(
() => PlaygroundApi.GetOfficialPromptResourceList(),
{
manual: true,
onSuccess: res => {
const processedData = size.current
? res.data?.slice(0, size.current)
: res.data;
setSlicedData(processedData);
return res;
},
},
);
const runAsyncHandler = async (options: { size?: number }) => {
size.current = options.size;
return runAsync();
};
const commonData = slicedData?.map(
({
id = '',
name = '',
description = '',
prompt_text: promptText = '',
}) => ({
id,
name,
description,
promptText,
}),
);
return {
data: commonData,
runAsync: runAsyncHandler,
loading,
};
};
export const useGetTeamLibrarys = (): {
data: LibraryInfo[] | undefined;
runAsync: (
options: LibraryResourceListRequest,
) => Promise<LibraryResourceListResponse>;
loading: boolean;
} => {
const { data, runAsync, loading } = useRequest(
(options: LibraryResourceListRequest) =>
PluginDevelopApi.LibraryResourceList({
...options,
res_type_filter: [ResType.Prompt],
}),
{
manual: true,
},
);
const commonData = data?.resource_list?.map(
({ res_id: id = '', name = '', desc = '' }) => ({
id,
name,
description: desc,
}),
);
return { data: commonData, runAsync, loading };
};

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { RecommendCard } from './recommend-card/index';
export { RecommendPannel } from './recommend-pannel/index';

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Skeleton } from '@coze-arch/coze-design';
export const RecommendCardLoading = () => (
<div className="flex flex-col flex-shrink-0 flex-nowrap px-3 py-2 aspect-[180/120] rounded-lg border coz-stroke-primary coz-bg-max">
<Skeleton
placeholder={<Skeleton.Title />}
className="mb-3 w-2/3"
></Skeleton>
<Skeleton
placeholder={<Skeleton.Paragraph rows={3} />}
className="w-full"
></Skeleton>
</div>
);

View File

@@ -0,0 +1,197 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect, useState } from 'react';
import cls from 'classnames';
import { useEditor } from '@coze-editor/editor/react';
import { type EditorAPI } from '@coze-editor/editor/preset-prompt';
import { Popover, Button, Typography } from '@coze-arch/coze-design';
import { PlaygroundApi } from '@coze-arch/bot-api';
import { ThemeExtension } from '@coze-common/editor-plugins/theme';
import { LibraryBlockWidget } from '@coze-common/editor-plugins/library-insert';
import { InputSlotWidget } from '@coze-common/editor-plugins/input-slot';
import {
PromptEditorRender,
PromptEditorProvider,
} from '@coze-common/prompt-kit-base/editor';
import { I18n } from '@coze-arch/i18n';
import { EditorView } from '@codemirror/view';
import '@coze-common/prompt-kit-base/shared/css';
interface RecommendCardProps {
id: string;
title: string;
description: string;
position?: 'topLeft' | 'top';
prompt?: string;
spaceId: string;
onInsertPrompt?: (prompt: string) => void;
className?: string;
}
export const RecommendCard = (props: RecommendCardProps) => {
const {
id,
title,
description,
prompt,
onInsertPrompt,
spaceId,
className,
position,
} = props;
const [promptText, setPromptText] = useState(prompt ?? '');
const [isPopoverVisible, setIsPopoverVisible] = useState(false);
useEffect(() => {
if (prompt) {
return;
}
PlaygroundApi.GetPromptResourceInfo({
prompt_resource_id: id,
}).then(({ data: { prompt_text } = {} }) => {
setPromptText(prompt_text ?? '');
});
}, [prompt, id]);
return (
<PromptEditorProvider>
<Popover
position={position}
visible={isPopoverVisible}
onVisibleChange={setIsPopoverVisible}
trigger="hover"
key={id}
className="rounded"
showArrow
// mouseLeaveDelay={150}
// mouseEnterDelay={150}
autoAdjustOverflow
content={
isPopoverVisible ? (
<UsePromptPopoverContent
prompt={promptText}
title={title}
spaceId={spaceId}
onInsertPrompt={value => {
onInsertPrompt?.(value);
setIsPopoverVisible(false);
}}
/>
) : null
}
>
<div
className={cls(
'flex flex-col flex-shrink-0 flex-nowrap gap-1 px-3 py-2 relative',
'aspect-[180/120] overflow-hidden',
'rounded-lg border coz-stroke-primary coz-bg-max cursor-pointer',
'coz-stroke-primary border-[0.5px] border-solid',
'hover:coz-mg-secondary-hovered',
className,
)}
>
<Typography.Text
className="font-medium text-lg"
ellipsis={{ rows: 1 }}
>
{title}
</Typography.Text>
<Typography.Text className="text-base" ellipsis={{ rows: 3 }}>
{description ?? prompt?.slice(0, 50)}
</Typography.Text>
</div>
</Popover>
</PromptEditorProvider>
);
};
const UsePromptPopoverContent: React.FC<{
prompt?: string;
title: string;
spaceId: string;
onInsertPrompt?: (prompt: string) => void;
}> = ({ prompt = '', title, spaceId, onInsertPrompt }) => {
const editor = useEditor<EditorAPI>();
useEffect(() => {
editor?.$view.dispatch({
changes: {
from: 0,
to: editor?.$view.state.doc.length,
insert: prompt,
},
});
}, [editor, prompt]);
return (
<div className="flex flex-col justify-between w-[300px] h-[300px] gap-3">
<div className="flex flex-col gap-1 overflow-y-auto styled-scrollbar hover-show-scrollbar">
<div className="text-sm font-medium coz-fg-primary">{title}</div>
<PromptEditorRender defaultValue={prompt} readonly />
<InputSlotWidget mode="input" />
<LibraryBlockWidget librarys={[]} readonly spaceId={spaceId} />
<ThemeExtension
themes={[
EditorView.theme({
'.cm-line': {
paddingLeft: '0 !important',
},
}),
]}
/>
</div>
<div className="coz-mg-hglt hover:!coz-mg-hglt-hovered rounded">
<Button
color="primary"
className="w-full font-sm font-medium !bg-transparent !coz-fg-hglt "
onClick={() => {
onInsertPrompt?.(prompt);
}}
>
{I18n.t('prompt_resource_insert_prompt')}
</Button>
</div>
</div>
);
};
export const ViewAll = ({
onClick,
className,
}: {
onClick: () => void;
className?: string;
}) => (
<div
onClick={onClick}
className={cls(
'flex flex-col flex-shrink-0 flex-nowrap gap-1 px-3 py-2 items-center justify-center',
'aspect-[180/120]',
'rounded-lg border coz-stroke-primary coz-bg-max cursor-pointer text-sm',
'coz-stroke-primary border-[0.5px] border-solid',
'hover:coz-mg-secondary-hovered',
className,
)}
>
<div className="coz-fg-primary font-medium">
{I18n.t('prompt_resource_view_all')}
</div>
</div>
);

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { I18n } from '@coze-arch/i18n';
import { IconCozEmpty } from '@coze-arch/coze-design/icons';
import { EmptyState } from '@coze-arch/coze-design';
export const EmptyRecommend = () => (
<div className="flex h-full items-center justify-center">
<EmptyState
title={I18n.t('prompt_library_empty_title')}
icon={<IconCozEmpty />}
style={{ maxWidth: '300px' }}
/>
</div>
);

View File

@@ -0,0 +1,59 @@
/* stylelint-disable declaration-no-important */
.recommend-pannel {
.semi-tabs-tab-button.semi-tabs-tab {
cursor: pointer;
font-size: 14px;
font-weight: 500;
line-height: 20px;
color: rgba(6, 7, 9, 80%);
}
:global {
.semi-tabs-tab-button {
display: flex;
gap: 4px;
align-items: center;
justify-content: center;
height: 32px;
padding: 6px 8px;
font-weight: 500;
color: rgba(6, 7, 9, 80%);
}
.semi-tabs-tab-button.semi-tabs-tab-active {
color: #4E40E5 !important;
background: rgba(186, 192, 255, 20%);
border-radius: 8px;
}
.semi-tabs-tab-button.semi-tabs-tab:hover:not(.semi-tabs-tab-active) {
background: rgba(6, 7, 9, 8%);
border-radius: 8px;
}
.semi-tabs-content {
padding: 0;
}
.semi-tabs-bar {
margin-bottom: 8px;
}
.semi-tabs-pane-motion-overlay {
box-sizing: content-box;
height: 100%;
}
.semi-tabs-bar-extra {
position: absolute;
right: 0;
}
}
}

View File

@@ -0,0 +1,277 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
type ForwardedRef,
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
} from 'react';
import cls from 'classnames';
import { useEditor } from '@coze-editor/editor/react';
import { type EditorAPI } from '@coze-editor/editor/preset-prompt';
import { I18n } from '@coze-arch/i18n';
import { IconCozArrowRightFill } from '@coze-arch/coze-design/icons';
import { Tabs, TabPane, Button } from '@coze-arch/coze-design';
import {
insertToNewline,
type PromptContextInfo,
} from '@coze-common/prompt-kit-base/shared';
import { RecommendCardLoading } from '../recommend-card/card-loading';
import { ViewAll, RecommendCard } from '../recommend-card';
import { useGetLibrarys } from '../hooks/use-get-librarys';
import { useScrollControl } from '../hooks/use-case/use-scroll-control';
import { usePromptLibraryModal } from '../../prompt-library';
import { EmptyRecommend } from './empty';
import styles from './index.module.less';
import '@coze-common/prompt-kit-base/shared/css';
import { LeftScrollButton, RightScrollButton } from './scroll-button';
const LIMIT_LIBRARY_SIZE = 6;
type TabType = 'Recommended' | 'Team';
const getTabLabelMap = (isPersonal: boolean) => ({
Recommended: I18n.t('prompt_resource_recommended'),
Team: isPersonal
? I18n.t('prompt_resource_personal')
: I18n.t('prompt_resource_team'),
});
interface ActionExtraInfo {
id: string;
category: string;
}
interface RecommendPannelProps {
className?: string;
cardClassName?: string;
listContainerClassName?: string;
tabs: TabType[];
/** 用于埋点: 页面来源 */
source: string;
importPromptWhenEmpty?: string;
spaceId: string;
/** 用于埋点: bot_id */
botId?: string;
/** 用于埋点: project_id */
projectId?: string;
/** 用于埋点: workflow_id */
workflowId?: string;
isPersonal?: boolean;
enableLibrary?: boolean;
getConversationId?: () => string | undefined;
getPromptContextInfo?: () => PromptContextInfo;
onInsertPrompt?: (prompt: string, info?: ActionExtraInfo) => void;
onUpdateSuccess?: (
mode: 'create' | 'edit' | 'info',
info: ActionExtraInfo,
) => void;
onCopyPrompt?: (info: ActionExtraInfo) => void;
onDeletePrompt?: (info: ActionExtraInfo) => void;
ref: ForwardedRef<RecommendPannelRef>;
}
/* eslint-disable @coze-arch/max-line-per-function */
export const Index = (props: RecommendPannelProps) => {
const domRef = useRef<HTMLDivElement | null>(null);
const {
className,
cardClassName,
listContainerClassName,
onInsertPrompt,
tabs,
spaceId,
enableLibrary = false,
getConversationId,
getPromptContextInfo,
importPromptWhenEmpty,
source,
botId,
projectId,
workflowId,
ref,
isPersonal = false,
onCopyPrompt,
onDeletePrompt,
onUpdateSuccess,
} = props;
const [activeTab, setActiveTab] = useState<(typeof tabs)[number]>(tabs[0]);
const editor = useEditor<EditorAPI>();
const handleInsertPrompt = async (prompt: string, id: string) => {
const insertPrompt = await insertToNewline({ editor, prompt });
onInsertPrompt?.(insertPrompt, { id, category: activeTab });
};
const { loading, data, runAsync } = useGetLibrarys();
const isEmpty = !loading && data?.[activeTab]?.length === 0;
const { open, node: PromptLibrary } = usePromptLibraryModal({
spaceId,
getConversationId,
editor,
isPersonal,
source,
botId,
projectId,
workflowId,
getPromptContextInfo,
importPromptWhenEmpty,
onInsertPrompt,
onUpdateSuccess: (mode, selectedLibrary) => {
runAsync(activeTab, {
space_id: spaceId,
size: LIMIT_LIBRARY_SIZE,
});
onUpdateSuccess?.(mode, selectedLibrary);
},
onCopyPrompt,
onDeletePrompt,
});
useEffect(() => {
if (!spaceId) {
return;
}
runAsync(activeTab, {
space_id: spaceId,
size: LIMIT_LIBRARY_SIZE,
});
}, [spaceId, activeTab]);
const { scrollRefs, canScrollLeft, canScrollRight, handleScroll } =
useScrollControl({
activeTab,
tabs,
loading,
data,
});
useImperativeHandle(ref, () => ({
refresh: (tab: 'Recommended' | 'Team') => {
runAsync(tab, {
space_id: spaceId,
size: LIMIT_LIBRARY_SIZE,
});
},
}));
return (
<div
ref={el => {
if (typeof ref === 'function') {
ref(null);
}
domRef.current = el;
}}
className={cls(
styles['recommend-pannel'],
'flex flex-col justify-between w-full',
'absolute bottom-0 left-0 right-0',
'py-3 px-5',
className,
)}
>
<Tabs
type="button"
activeKey={activeTab}
onChange={key => setActiveTab(key as (typeof tabs)[number])}
tabBarExtraContent={
enableLibrary ? (
<div
className="coz-fg-primary text-sm flex items-center cursor-pointer font-medium"
onClick={() => open({ defaultActiveTab: activeTab })}
>
<Button
icon={<IconCozArrowRightFill className="!coz-fg-primary" />}
color="secondary"
iconPosition="right"
>
<span className="coz-fg-primary">
{I18n.t('workflow_prompt_editor_view_library')}
</span>
</Button>
</div>
) : null
}
>
{tabs.map((item, index) => (
<TabPane
itemKey={item}
tab={getTabLabelMap(isPersonal)[item]}
className="relative"
>
{canScrollLeft ? (
<LeftScrollButton handleScroll={() => handleScroll('left')} />
) : null}
<div className="relative">
<div
ref={el => (scrollRefs.current[index] = el)}
className={cls(
'relative overflow-x-auto styled-scrollbar h-[120px] box-content hover-show-scrollbar',
'flex-1',
listContainerClassName,
)}
>
{isEmpty ? (
<EmptyRecommend />
) : (
<div className="flex gap-3 flex-row flex-nowrap overflow-visible h-full min-w-min">
{loading
? Array.from({ length: LIMIT_LIBRARY_SIZE }).map(
(_, _index) => <RecommendCardLoading key={_index} />,
)
: null}
{data?.[item]?.map((card, _index) => (
<RecommendCard
className={cls(cardClassName)}
key={card.id}
id={card.id}
position={_index === 0 ? 'topLeft' : 'top'}
spaceId={spaceId}
title={card.name}
description={card.description}
prompt={card.promptText}
onInsertPrompt={prompt =>
handleInsertPrompt(prompt, card.id)
}
/>
))}
<ViewAll onClick={() => open({ defaultActiveTab: item })} />
</div>
)}
</div>
{canScrollRight ? (
<RightScrollButton handleScroll={() => handleScroll('right')} />
) : null}
</div>
</TabPane>
))}
</Tabs>
{PromptLibrary}
</div>
);
};
interface RecommendPannelRef {
refresh: (tab: 'Recommended' | 'Team') => void;
}
export const RecommendPannel = forwardRef<
RecommendPannelRef,
RecommendPannelProps
>((props, ref) => <Index {...props} ref={ref} />);

View File

@@ -0,0 +1,62 @@
/*
* 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 {
IconCozArrowLeftFill,
IconCozArrowRightFill,
} from '@coze-arch/coze-design/icons';
export const LeftScrollButton = ({
handleScroll,
}: {
handleScroll: () => void;
}) => (
<div
className="absolute bottom-0 left-0 top-0 w-8 z-10"
style={{
background:
'linear-gradient(90deg, #F9F9F9 0%, rgba(249, 249, 249, 0.00) 100%)',
}}
>
<div
onClick={handleScroll}
className="w-6 h-6 coz-bg-max flex justify-center items-center absolute left-0 top-1/2 -translate-y-1/2 z-20 cursor-pointer rounded-lg coz-stroke-primary coz-shadow-small"
>
<IconCozArrowLeftFill className="w-4 h-4" />
</div>
</div>
);
export const RightScrollButton = ({
handleScroll,
}: {
handleScroll: () => void;
}) => (
<div
className="absolute bottom-0 right-0 top-0 w-8"
style={{
background:
'linear-gradient(270deg, #F9F9F9 0%, rgba(249, 249, 249, 0.00) 100%)',
}}
>
<div
onClick={handleScroll}
className="w-6 h-6 coz-bg-max flex justify-center items-center absolute right-0 top-1/2 -translate-y-1/2 z-20 cursor-pointer rounded-lg coz-stroke-primary coz-shadow-small"
>
<IconCozArrowRightFill className="w-4 h-4" />
</div>
</div>
);