Files
coze-studio/frontend/packages/common/prompt-kit/main/src/prompt-recommend/recommend-pannel/index.tsx
2025-07-31 23:15:48 +08:00

278 lines
8.3 KiB
TypeScript

/*
* 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[];
/** For event tracking: page source */
source: string;
importPromptWhenEmpty?: string;
spaceId: string;
/** For event tracking: bot_id */
botId?: string;
/** For event tracking: project_id */
projectId?: string;
/** For event tracking: 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} />);