feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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} />);
|
||||
@@ -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>
|
||||
);
|
||||
Reference in New Issue
Block a user