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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,145 @@
<svg width="356" height="178" viewBox="0 0 356 178" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.2">
<mask id="mask0_7107_191040" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="356" height="178">
<rect x="0.540771" width="354.918" height="177.459" fill="url(#paint0_linear_7107_191040)"/>
</mask>
<g mask="url(#mask0_7107_191040)">
<mask id="mask1_7107_191040" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="356" height="178">
<rect x="0.540771" width="354.918" height="177.459" fill="url(#paint1_linear_7107_191040)"/>
</mask>
<g mask="url(#mask1_7107_191040)">
<rect x="0.540771" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="30.1172" width="29.5765" height="29.5765" fill="url(#paint2_linear_7107_191040)" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="59.6938" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="89.2705" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="118.847" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="148.424" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="178" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="207.576" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="237.153" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="266.729" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="296.306" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="325.883" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="0.540771" y="29.5767" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="30.1172" y="29.5767" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="59.6938" y="29.5767" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="89.2705" y="29.5767" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="118.847" y="29.5767" width="29.5765" height="29.5765" fill="url(#paint3_linear_7107_191040)" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="148.424" y="29.5767" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="178" y="29.5767" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="207.576" y="29.5767" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="237.153" y="29.5767" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="266.729" y="29.5767" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="296.306" y="29.5767" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="325.883" y="29.5767" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="0.540771" y="59.1528" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="30.1172" y="59.1528" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="59.6938" y="59.1528" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="89.2705" y="59.1528" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="118.847" y="59.1528" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="148.424" y="59.1528" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="178" y="59.1528" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="207.576" y="59.1528" width="29.5765" height="29.5765" fill="url(#paint4_linear_7107_191040)" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="237.153" y="59.1528" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="266.729" y="59.1528" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="296.306" y="59.1528" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="325.883" y="59.1528" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="0.540771" y="88.7295" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="30.1172" y="88.7295" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="59.6938" y="88.7295" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="89.2705" y="88.7295" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="118.847" y="88.7295" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="148.424" y="88.7295" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="178" y="88.7295" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="207.576" y="88.7295" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="237.153" y="88.7295" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="266.729" y="88.7295" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="296.306" y="88.7295" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="325.883" y="88.7295" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="0.540771" y="118.306" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="30.1172" y="118.306" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="59.6938" y="118.306" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="89.2705" y="118.306" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="118.847" y="118.306" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="148.424" y="118.306" width="29.5765" height="29.5765" fill="url(#paint5_linear_7107_191040)" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="178" y="118.306" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="207.576" y="118.306" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="237.153" y="118.306" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="266.729" y="118.306" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="296.306" y="118.306" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="325.883" y="118.306" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="0.540771" y="147.883" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="30.1172" y="147.883" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="59.6938" y="147.883" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="89.2705" y="147.883" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="118.847" y="147.883" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="148.424" y="147.883" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="178" y="147.883" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="207.576" y="147.883" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="237.153" y="147.883" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="266.729" y="147.883" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="296.306" y="147.883" width="29.5765" height="29.5765" fill="url(#paint6_linear_7107_191040)" stroke="#3C445C" stroke-width="0.616178"/>
<rect x="325.883" y="147.883" width="29.5765" height="29.5765" stroke="#3C445C" stroke-width="0.616178"/>
</g>
</g>
</g>
<rect x="80.7778" y="82.3135" width="194.444" height="70.8333" rx="5.55556" fill="#F9F9F9"/>
<rect x="81.1251" y="82.6607" width="193.75" height="70.1389" rx="5.20833" stroke="url(#paint7_linear_7107_191040)" stroke-opacity="0.24" stroke-width="0.694444"/>
<rect opacity="0.12" x="91.8889" y="94.814" width="72.2222" height="9.72222" rx="4.86111" fill="#2B3245"/>
<rect opacity="0.12" x="91.8889" y="110.092" width="136.111" height="9.72222" rx="4.86111" fill="#2B3245"/>
<g filter="url(#filter0_d_7107_191040)">
<path d="M236.333 97.8693C236.333 96.3135 236.333 95.5357 236.636 94.9415C236.902 94.4188 237.327 93.9939 237.85 93.7276C238.444 93.4248 239.222 93.4248 240.778 93.4248H259.667C261.222 93.4248 262 93.4248 262.594 93.7276C263.117 93.9939 263.542 94.4188 263.808 94.9415C264.111 95.5357 264.111 96.3135 264.111 97.8692V116.758C264.111 118.314 264.111 119.092 263.808 119.686C263.542 120.209 263.117 120.634 262.594 120.9C262 121.203 261.222 121.203 259.667 121.203H240.778C239.222 121.203 238.444 121.203 237.85 120.9C237.327 120.634 236.902 120.209 236.636 119.686C236.333 119.092 236.333 118.314 236.333 116.758V97.8693Z" fill="url(#paint8_linear_7107_191040)"/>
<path d="M236.403 97.8693C236.403 97.0903 236.403 96.5102 236.44 96.0505C236.478 95.5916 236.552 95.2587 236.698 94.973C236.958 94.4634 237.372 94.0491 237.881 93.7894C238.167 93.6439 238.5 93.5694 238.959 93.5319C239.419 93.4943 239.999 93.4942 240.778 93.4942H259.667C260.446 93.4942 261.026 93.4943 261.485 93.5319C261.944 93.5694 262.277 93.6439 262.563 93.7894C263.072 94.0491 263.487 94.4634 263.746 94.973C263.892 95.2587 263.966 95.5916 264.004 96.0505C264.042 96.5102 264.042 97.0903 264.042 97.8692V116.758C264.042 117.537 264.042 118.117 264.004 118.577C263.966 119.036 263.892 119.369 263.746 119.654C263.487 120.164 263.072 120.578 262.563 120.838C262.277 120.984 261.944 121.058 261.485 121.096C261.026 121.133 260.446 121.133 259.667 121.133H240.778C239.999 121.133 239.419 121.133 238.959 121.096C238.5 121.058 238.167 120.984 237.881 120.838C237.372 120.578 236.958 120.164 236.698 119.654C236.552 119.369 236.478 119.036 236.44 118.577C236.403 118.117 236.403 117.537 236.403 116.758V97.8693Z" stroke="black" stroke-opacity="0.08" stroke-width="0.138889"/>
</g>
<rect opacity="0.12" x="91.8889" y="132.313" width="172.222" height="9.72222" rx="4.86111" fill="#2B3245"/>
<defs>
<filter id="filter0_d_7107_191040" x="230.381" y="89.4566" width="39.6826" height="39.6826" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1.98413"/>
<feGaussianBlur stdDeviation="2.97619"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_7107_191040"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_7107_191040" result="shape"/>
</filter>
<linearGradient id="paint0_linear_7107_191040" x1="355.459" y1="88.7296" x2="0.540769" y2="88.7296" gradientUnits="userSpaceOnUse">
<stop stop-color="#D9D9D9" stop-opacity="0"/>
<stop offset="0.500048" stop-color="#D9D9D9"/>
<stop offset="1" stop-color="#D9D9D9" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint1_linear_7107_191040" x1="178" y1="0" x2="178" y2="177.459" gradientUnits="userSpaceOnUse">
<stop stop-color="#D9D9D9" stop-opacity="0"/>
<stop offset="0.493585" stop-color="#D9D9D9"/>
<stop offset="1" stop-color="#D9D9D9" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint2_linear_7107_191040" x1="44.9055" y1="0" x2="44.9055" y2="29.5765" gradientUnits="userSpaceOnUse">
<stop stop-color="#3C445C" stop-opacity="0"/>
<stop offset="1" stop-color="#3C445C"/>
</linearGradient>
<linearGradient id="paint3_linear_7107_191040" x1="133.635" y1="29.5767" x2="133.635" y2="59.1532" gradientUnits="userSpaceOnUse">
<stop stop-color="#3C445C" stop-opacity="0"/>
<stop offset="1" stop-color="#3C445C"/>
</linearGradient>
<linearGradient id="paint4_linear_7107_191040" x1="222.365" y1="59.1528" x2="222.365" y2="88.7294" gradientUnits="userSpaceOnUse">
<stop stop-color="#3C445C" stop-opacity="0"/>
<stop offset="1" stop-color="#3C445C"/>
</linearGradient>
<linearGradient id="paint5_linear_7107_191040" x1="163.212" y1="118.306" x2="163.212" y2="147.883" gradientUnits="userSpaceOnUse">
<stop stop-color="#3C445C" stop-opacity="0"/>
<stop offset="1" stop-color="#3C445C"/>
</linearGradient>
<linearGradient id="paint6_linear_7107_191040" x1="311.094" y1="147.883" x2="311.094" y2="177.459" gradientUnits="userSpaceOnUse">
<stop stop-color="#3C445C" stop-opacity="0"/>
<stop offset="1" stop-color="#3C445C"/>
</linearGradient>
<linearGradient id="paint7_linear_7107_191040" x1="178" y1="82.3135" x2="178" y2="153.147" gradientUnits="userSpaceOnUse">
<stop stop-color="#38415A"/>
<stop offset="1" stop-color="#2B3245" stop-opacity="0.4"/>
</linearGradient>
<linearGradient id="paint8_linear_7107_191040" x1="250.222" y1="93.4248" x2="250.222" y2="121.203" gradientUnits="userSpaceOnUse">
<stop stop-color="#F45D68"/>
<stop offset="1" stop-color="#FFCA00"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -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 '@coze-arch/bot-typings';
import '@coze-arch/bot-env/typings';
export { usePromptLibraryModal } from './prompt-library';

View File

@@ -0,0 +1,39 @@
/*
* 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';
import EmptyPromptIcon from '../assets/empty-prompt-icon.svg';
export const UnselectedPrompt = (props: { className?: string }) => (
<div className={props.className}>
<EmptyState
title={I18n.t('prompt_library_unselected')}
icon={<img src={EmptyPromptIcon} alt="empty-prompt" />}
/>
</div>
);
export const EmptyPrompt = (props: { className?: string }) => (
<div className={props.className}>
<EmptyState
title={I18n.t('prompt_library_prompt_empty')}
icon={<IconCozEmpty />}
/>
</div>
);

View File

@@ -0,0 +1,41 @@
/*
* 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 copy from 'copy-to-clipboard';
import { type EditorAPI } from '@coze-editor/editor/preset-prompt';
import { I18n } from '@coze-arch/i18n';
import { Button, Toast } from '@coze-arch/coze-design';
export const CopyPrompt = (props: {
editor: EditorAPI;
onCopyPrompt: () => void;
}) => {
const { editor, onCopyPrompt } = props;
return (
<Button
color="primary"
onClick={() => {
const text = editor?.$view.state.doc.toString();
const result = copy(text, { format: 'text/plain' });
result &&
Toast.success(I18n.t('prompt_library_prompt_copied_successfully'));
onCopyPrompt?.();
}}
>
{I18n.t('prompt_detail_copy_prompt')}
</Button>
);
};

View File

@@ -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 { CopyPrompt } from './copy-prompt';
export { InsertToEditor } from './insert-to-editor';
export { PromptDiff } from './prompt-diff';

View File

@@ -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 { type EditorAPI } from '@coze-editor/editor/preset-prompt';
import { insertToNewline } from '@coze-common/prompt-kit-base/shared';
import { I18n } from '@coze-arch/i18n';
import { Button } from '@coze-arch/coze-design';
export const InsertToEditor = (props: {
outerEditor: EditorAPI;
prompt: string;
onInsertPrompt: (prompt: string) => void;
onCancel: (e: React.MouseEvent) => void;
}) => {
const { outerEditor, prompt, onInsertPrompt, onCancel } = props;
return (
<Button
disabled={!prompt}
onClick={async e => {
const insertPrompt = await insertToNewline({
editor: outerEditor,
prompt,
});
onInsertPrompt(insertPrompt);
onCancel?.(e);
}}
>
{I18n.t('prompt_resource_insert_prompt')}
</Button>
);
};

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { I18n } from '@coze-arch/i18n';
import { Button } from '@coze-arch/coze-design';
export const PromptDiff = (props: { onDiff: () => void }) => {
const { onDiff } = props;
return (
<Button
color="primary"
onClick={() => {
onDiff?.();
}}
>
{I18n.t('compare_prompt_compare_debug')}
</Button>
);
};

View File

@@ -0,0 +1,16 @@
.prompt-library-modal {
:global {
.semi-modal-body {
> div:first-child {
> div:first-child {
display: flex;
flex-direction: column;
}
}
}
.semi-modal-footer {
height: 32px;
}
}
}

View File

@@ -0,0 +1,424 @@
/*
* 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';
import classNames 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 { IconCozPlus } from '@coze-arch/coze-design/icons';
import {
Modal,
type ModalProps,
Search,
Button,
} from '@coze-arch/coze-design';
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
import { PlaygroundApi } from '@coze-arch/bot-api';
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 { usePromptConfiguratorModal } from '@coze-common/prompt-kit-adapter/create-prompt';
import { type LibraryInfo, getLibraryListByCategory } from './library-request';
import { LibraryList, type InfiniteListRef } from './library-list';
import { CopyPrompt, InsertToEditor, PromptDiff } from './footer-actions';
import { EmptyPrompt } from './empty';
import '@coze-common/prompt-kit-base/shared/css';
import styles from './index.module.less';
const getTabLabelMap = (isPersonal: boolean) => ({
Recommended: I18n.t('prompt_resource_recommended'),
Team: isPersonal
? I18n.t('prompt_resource_personal')
: I18n.t('prompt_resource_team'),
});
const LIMIT_LIBRARY_SIZE = 15;
interface PromptContextInfo {
botId?: string;
name?: string;
description?: string;
contextHistory?: string;
}
export interface ActionExtraInfo {
category: 'Recommended' | 'Team';
id: string;
}
interface PromptLibraryProps extends ModalProps {
spaceId: string;
isPersonal?: boolean;
editor: EditorAPI;
getConversationId?: () => string | undefined;
getPromptContextInfo?: () => PromptContextInfo;
enableDiff?: boolean;
importPromptWhenEmpty?: string;
defaultActiveTab?: 'Recommended' | 'Team';
tabs?: ('Recommended' | 'Team')[];
/** 用于埋点: 页面来源 */
source: string;
/** 用于埋点: bot_id */
botId?: string;
/** 用于埋点: project_id */
projectId?: string;
/** 用于埋点: workflow_id */
workflowId?: string;
onInsertPrompt?: (prompt: string, selectedLibrary: ActionExtraInfo) => void;
onUpdateSuccess?: (
mode: 'create' | 'edit' | 'info',
selectedLibrary: ActionExtraInfo,
) => void;
onCopyPrompt?: (selectedLibrary: ActionExtraInfo) => void;
onDeletePrompt?: (selectedLibrary: ActionExtraInfo) => void;
onDiff?: ({
prompt,
libraryId,
}: {
prompt: string;
libraryId: string;
}) => void;
onCancel?: () => void;
}
/* eslint-disable @coze-arch/max-line-per-function */
export const PromptLibrary = ({
spaceId,
onCancel,
defaultActiveTab = 'Recommended',
getConversationId,
getPromptContextInfo,
editor: outerEditor,
source,
tabs = ['Recommended', 'Team'],
botId,
projectId,
workflowId,
isPersonal = false,
importPromptWhenEmpty,
enableDiff = false,
onInsertPrompt,
onUpdateSuccess,
onCopyPrompt,
onDeletePrompt,
onDiff,
}: PromptLibraryProps) => {
const [activeTab, setActiveTab] = useState<'Recommended' | 'Team'>(
defaultActiveTab,
);
const [prompt, setPrompt] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [dataList, setDataList] = useState<LibraryInfo[]>([]);
const [selectedLibraryId, setSelectedLibraryId] = useState<string>('');
const [searchWord, setSearchWord] = useState<string | undefined>(undefined);
const targetRef = useRef<HTMLDivElement>(null);
const listRef = useRef<InfiniteListRef<LibraryInfo>>(null);
const editor = useEditor<EditorAPI>();
const isEmptyList = !isLoading && dataList.length === 0;
const { open: openPromptConfiguratorModal, node: PromptConfiguratorModal } =
usePromptConfiguratorModal({
spaceId,
isPersonal,
enableDiff,
getConversationId,
getPromptContextInfo,
importPromptWhenEmpty,
source,
botId,
projectId,
workflowId,
onDiff,
onUpdateSuccess: (mode, id) => {
if (tabs.includes(activeTab)) {
setActiveTab('Team');
}
onUpdateSuccess?.(mode, {
id: id || '',
category: 'Team',
});
listRef.current?.reload();
},
});
useEffect(() => {
if (!editor) {
return;
}
editor.$view.dispatch({
changes: {
from: 0,
to: editor.$view.state.doc.length,
insert: prompt,
},
});
}, [editor, prompt]);
useEffect(() => {
if (!selectedLibraryId || isLoading || !dataList.length) {
return;
}
const selectedLibrary = dataList.find(
item => item.id === selectedLibraryId,
);
if (!selectedLibrary) {
return;
}
const { promptText } = selectedLibrary;
if (promptText) {
setPrompt(promptText);
return;
}
PlaygroundApi.GetPromptResourceInfo({
prompt_resource_id: selectedLibraryId,
}).then(({ data: { prompt_text: newPrompt } = {} }) => {
setPrompt(newPrompt ?? '');
});
}, [selectedLibraryId, dataList, isLoading]);
// 切换tab、无选中提示词重置搜索词
useEffect(() => {
setSelectedLibraryId('');
setPrompt('');
}, [activeTab, isEmptyList]);
return (
<>
<Modal
title={I18n.t('prompt_library_prompt_library')}
visible
className={styles['prompt-library-modal']}
width="880px"
closeOnEsc={false}
maskClosable={false}
footer={
<div className="flex justify-end">
{enableDiff ? (
<PromptDiff
onDiff={() => {
onDiff?.({ prompt, libraryId: selectedLibraryId });
sendTeaEvent(EVENT_NAMES.prompt_library_front, {
source,
prompt_id: selectedLibraryId,
space_id: spaceId,
prompt_type: 'workspace',
action: 'compare',
bot_id: botId,
project_id: projectId,
workflow_id: workflowId,
});
sendTeaEvent(EVENT_NAMES.compare_mode_front, {
source,
action: 'start',
compare_type: 'prompts',
bot_id: botId,
});
}}
/>
) : null}
<CopyPrompt
editor={editor}
onCopyPrompt={() => {
onCopyPrompt?.({ id: selectedLibraryId, category: activeTab });
sendTeaEvent(EVENT_NAMES.prompt_library_front, {
source,
prompt_id: selectedLibraryId,
space_id: spaceId,
prompt_type: 'workspace',
action: 'copy',
bot_id: botId,
project_id: projectId,
workflow_id: workflowId,
});
}}
/>
<InsertToEditor
outerEditor={outerEditor}
prompt={prompt}
onInsertPrompt={insertPrompt => {
onInsertPrompt?.(insertPrompt, {
id: selectedLibraryId,
category: activeTab,
});
sendTeaEvent(EVENT_NAMES.prompt_library_front, {
source,
prompt_id: selectedLibraryId,
space_id: spaceId,
prompt_type: 'workspace',
action: 'insert',
bot_id: botId,
project_id: projectId,
workflow_id: workflowId,
});
}}
onCancel={() => {
onCancel?.();
}}
/>
</div>
}
onCancel={onCancel}
>
<div className="flex flex-col gap-5 overflow-hidden h-[620px]">
<div className="flex justify-between items-center">
<div className="flex gap-3 pl-3">
{tabs.map(category => (
<div
key={category}
className={classNames(
'coz-fg-secondary text-sm cursor-pointer font-medium',
{
'!coz-fg-hglt': activeTab === category,
},
)}
onClick={() => {
setActiveTab(category);
}}
>
{getTabLabelMap(isPersonal)[category]}
</div>
))}
</div>
<div className="flex gap-2">
<Search
className="w-[192px]"
placeholder={I18n.t('Search')}
onSearch={setSearchWord}
/>
{isEmptyList ? null : (
<Button
type="primary"
className="!coz-mg-hglt !coz-fg-hglt hover:!coz-mg-hglt-hovered active:!coz-mg-hglt-pressed"
icon={<IconCozPlus />}
onClick={() => {
openPromptConfiguratorModal({
mode: 'create',
});
}}
>
{I18n.t('prompt_library_new_prompt')}
</Button>
)}
</div>
</div>
<div className="flex gap-2 overflow-hidden flex-1">
<div
className="flex-1 basis-1/3 overflow-y-auto styled-scrollbar hover-show-scrollbar"
ref={targetRef}
>
<LibraryList
ref={listRef}
targetRef={targetRef}
spaceId={spaceId}
category={activeTab}
size={LIMIT_LIBRARY_SIZE}
searchWord={searchWord}
getData={getLibraryListByCategory}
onChangeState={(newIsLoading, newDataList) => {
setIsLoading(newIsLoading);
setDataList(newDataList);
}}
onActive={id => {
setSelectedLibraryId(id);
}}
onDeleteAction={id => {
PlaygroundApi.DeletePromptResource({
prompt_resource_id: id,
}).then(() => {
onDeletePrompt?.({ id, category: 'Team' });
sendTeaEvent(EVENT_NAMES.prompt_library_front, {
source,
prompt_id: id,
space_id: spaceId,
prompt_type: 'workspace',
action: 'delete',
bot_id: botId,
project_id: projectId,
workflow_id: workflowId,
});
listRef.current?.reload();
});
}}
onEditAction={id => {
openPromptConfiguratorModal({
mode: 'edit',
editId: id,
});
}}
onEmptyClick={() => {
openPromptConfiguratorModal({
mode: 'create',
});
}}
/>
</div>
<div className="flex-1 basis-2/3 coz-bg-max rounded-normal border-solid coz-stroke-primary border-[0.5px] p-2 empty:hidden overflow-y-auto styled-scrollbar hover-show-scrollbar">
{isEmptyList ? null : (
<>
{prompt ? (
<div className="relative ">
<PromptEditorRender defaultValue={prompt} readonly />
<InputSlotWidget mode="input" />
<LibraryBlockWidget
librarys={[]}
readonly
spaceId={spaceId}
/>
</div>
) : (
<EmptyPrompt className="flex justify-center items-center h-full w-full" />
)}
</>
)}
</div>
</div>
</div>
</Modal>
{PromptConfiguratorModal}
</>
);
};
export const usePromptLibraryModal = (
props: Omit<PromptLibraryProps, 'onClose'>,
) => {
const [visible, setVisible] = useState(false);
const [dynamicProps, setDynamicProps] = useState<Partial<PromptLibraryProps>>(
{},
);
const close = () => {
setVisible(false);
};
const open = (options?: Partial<PromptLibraryProps>) => {
setVisible(true);
setDynamicProps(options ?? {});
};
return {
node: visible ? (
<PromptEditorProvider>
<PromptLibrary {...props} {...dynamicProps} onCancel={close} />
</PromptEditorProvider>
) : null,
close,
open,
};
};

View File

@@ -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 React from 'react';
import { I18n } from '@coze-arch/i18n';
import { IconCozWarningCircle } from '@coze-arch/coze-design/icons';
import { EmptyState, Spin } from '@coze-arch/coze-design';
import { type EmptyProps } from './type';
import s from './index.module.less';
/* Plugin header */
function Index(props: EmptyProps) {
const {
isLoading,
loadRetry,
isError,
renderEmpty,
text,
btn,
icon,
className,
size,
} = props;
return (
<div className={s['height-whole-100']}>
{renderEmpty?.(props) ||
(!isError ? (
isLoading ? (
<Spin
tip={
<span className={s['loading-text']}>{I18n.t('Loading')}</span>
}
wrapperClassName={s.spin}
size="middle"
/>
) : (
<div className={className}>
<EmptyState
title={text?.emptyTitle || I18n.t('inifinit_list_empty_title')}
size={size}
description={text?.emptyDesc || ''}
buttonText={btn?.emptyText}
buttonProps={btn?.emptyButtonProps}
onButtonClick={btn?.emptyClick}
icon={icon}
/>
</div>
)
) : (
<div className={className}>
<EmptyState
className={s['load-fail']}
title={I18n.t('inifinit_list_load_fail')}
icon={<IconCozWarningCircle />}
buttonText={loadRetry && I18n.t('inifinit_list_retry')}
onButtonClick={() => {
loadRetry?.();
}}
/>
</div>
))}
</div>
);
}
export default Index;

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { Spin, Button } from '@coze-arch/coze-design';
import { type FooterProps } from './type';
import s from './index.module.less';
/* Plugin header */
function Index(props: FooterProps) {
const {
isLoading,
loadRetry,
isError,
renderFooter,
isNeedBtnLoadMore,
noMore,
} = props;
return (
<div className={classNames(s['footer-container'], 'empty:hidden')}>
{renderFooter?.(props) ||
(isLoading ? (
<>
<Spin />
<span className={s.loading}>{I18n.t('Loading')}</span>
</>
) : isError ? (
<>
<Spin />
<span className={s['error-retry']} onClick={loadRetry}>
{I18n.t('inifinit_list_retry')}
</span>
</>
) : isNeedBtnLoadMore && !noMore ? (
<Button
onClick={loadRetry}
className={s['load-more-btn']}
theme="borderless"
>
{I18n.t('mkpl_load_btn')}
</Button>
) : null)}
</div>
);
}
export default Index;

View File

@@ -0,0 +1,111 @@
.footer-container {
padding: 12px 0 28px;
text-align: center;
* {
vertical-align: middle;
}
.loading,
.error-retry {
margin-left: 10px;
line-height: 20px;
color: var(--semi-color-text-3, rgba(29, 28, 35, 35%));
}
.error-retry {
cursor: pointer;
color: var(--semi-color-focus-border, #4D53E8);
}
:global {
.semi-spin-middle>.semi-spin-wrapper {
height: 16px;
svg {
width: 16px;
height: 16px;
}
}
}
.load-more-btn {
font-weight: 600;
background: #FFF;
border-radius: 40px;
span {
color: #1D1C23;
}
&:hover {
background: #FFF;
border: none;
}
}
&.responseive-foot-container {
padding: 0 0 16px;
.load-more-btn {
height: 40px;
padding: 16px 24px;
}
}
}
.height-whole-100 {
overflow: visible;
height: 100%;
.spin {
display: block;
width: 100%;
height: 100%;
:global {
.semi-spin-wrapper {
position: absolute;
display: flex;
justify-content: center;
margin-top: 16px;
svg {
width: 24px;
height: 24px;
}
}
.semi-tabs-content {
padding: 0;
}
.semi-spin-children {
height: 100%;
}
}
.loading-text {
margin-left: 8px;
font-size: 16px;
font-weight: 400;
line-height: 22px;
color: var(--semi-color-text-3, rgba(29, 28, 35, 35%));
}
}
:global {
.semi-list-item {
padding: 0
}
.semi-list-footer {
padding: 0;
}
}
}

View File

@@ -0,0 +1,123 @@
/*
* 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,
type RefObject,
useEffect,
type ForwardedRef,
} from 'react';
import cls from 'classnames';
import { List } from '@coze-arch/coze-design';
import useScroll from './use-scroll';
import { type InfiniteListProps, type InfiniteListRef } from './type';
import Footer from './footer';
import Empty from './empty';
import s from './index.module.less';
export type { InfiniteListRef };
// modify from packages/community/components/src/infinite-list/index.tsx
function Index<T extends object>(
props: InfiniteListProps<T>,
ref: ForwardedRef<InfiniteListRef<T>>,
) {
const {
className,
grid,
renderItem,
itemClassName,
renderFooter,
scrollConf,
onChangeState,
isNeedBtnLoadMore = false,
retryFunc,
containerClassName,
emptyConf,
} = props;
const {
dataList,
isLoading,
loadMore,
noMore,
isLoadingError,
reload,
getDataList,
} = useScroll<T>({ ...scrollConf, isNeedBtnLoadMore });
useImperativeHandle(
ref,
() => ({
reload,
getDataList,
}),
[reload, getDataList],
);
useEffect(() => {
onChangeState?.(!!isLoading, dataList ?? []);
}, [dataList, isLoading]);
return (
<div className={cls(s['height-whole-100'], containerClassName)}>
{!dataList?.length ? (
/** 数据为空的时候,操作如何显示空页面 */
<Empty
isError={isLoadingError}
isLoading={isLoading}
loadRetry={retryFunc || loadMore}
{...emptyConf}
/>
) : (
<List
{...{ className, grid }}
emptyContent={<></>}
dataSource={dataList}
split={false}
renderItem={(item, number) => (
<List.Item
className={
typeof itemClassName === 'string'
? itemClassName
: itemClassName?.(item) // 支持动态行className
}
>
{renderItem?.(item, number)}
</List.Item>
)}
footer={
<Footer
isError={isLoadingError}
noMore={noMore}
isLoading={isLoading}
loadRetry={retryFunc || loadMore}
renderFooter={renderFooter}
isNeedBtnLoadMore={isNeedBtnLoadMore}
dataNum={dataList?.length}
/>
}
/>
)}
</div>
);
}
export const InfiniteList = forwardRef(Index) as <T>(
props: InfiniteListProps<T> & { ref?: RefObject<InfiniteListRef<T>> },
) => JSX.Element;

View File

@@ -0,0 +1,107 @@
/*
* 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 ReactElement, type RefObject } from 'react';
import {
type ListProps,
type EmptyStateProps,
type ButtonProps,
} from '@coze-arch/coze-design';
export interface EmptyProps {
className?: string;
isError?: boolean;
isLoading?: boolean;
loadRetry?: () => void; //重试加载
size?: EmptyStateProps['size'];
text?: {
emptyTitle?: string;
emptyDesc?: string;
searchEmptyTitle?: string;
};
btn?: {
emptyClick?: () => void; //
emptyText?: string;
emptyButtonProps?: ButtonProps;
};
icon?: ReactElement;
renderEmpty?: (
emptyProps: Omit<EmptyProps, 'renderEmpty'>,
) => React.ReactNode | null;
}
export interface FooterProps {
isError?: boolean; // 是否加载出错
isLoading?: boolean; // 是否加载中
noMore?: boolean; //没有更多数据
isNeedBtnLoadMore?: boolean;
dataNum?: number;
loadRetry?: () => void; //重试加载
renderFooter?: (
footerProps: Omit<FooterProps, 'renderFooter'>,
) => React.ReactNode | null;
}
export interface InfiniteListDataProps<T> {
list: T[];
hasMore?: boolean;
// nextPage: number;
cursor: string;
[key: string]: unknown;
}
export interface ScrollProps<T> {
threshold?: number; //距离下方多长距离,开始加载数据
targetRef?: RefObject<HTMLDivElement>; // 监听滚动的Dom 引用
loadData: (
current: InfiniteListDataProps<T>,
) => Promise<InfiniteListDataProps<T>>; // 加载更多数据
reloadDeps?: unknown[]; // 重新加载数据依赖
isNeedBtnLoadMore?: boolean;
isLoading?: boolean; // 是否加载中
resetDataIfReload?: boolean; // 当reload时是否先reset列表已存在数据默认为true
}
export interface InfiniteListProps<T>
extends Partial<
Pick<ListProps<T>, 'className' | 'emptyContent' | 'grid' | 'renderItem'>
> {
containerClassName?: string;
canShowData?: boolean; //是否能够显示数据了
isSearching?: boolean; // 是否搜索中,主要是用于错误显示的时候,选择文案使用
itemClassName?: string | ((item: T) => string);
isNeedBtnLoadMore?: boolean;
isResponsive?: boolean;
emptyConf?: {
className?: string;
renderEmpty?: EmptyProps['renderEmpty'];
text?: EmptyProps['text'];
btn?: EmptyProps['btn'];
icon?: EmptyProps['icon'];
size?: EmptyProps['size'];
};
renderFooter?: FooterProps['renderFooter'];
scrollConf: ScrollProps<T>;
rowKey?: string;
retryFunc?: () => void;
onChangeState?: (loading: boolean, data: T[]) => void;
}
export interface InfiniteListRef<T> {
reload: () => void;
getDataList: () => T[]; // 获取当前列表数据
}

View File

@@ -0,0 +1,223 @@
/*
* 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,
useRef,
useEffect,
type Dispatch,
type SetStateAction,
} from 'react';
import {
useInfiniteScroll,
useUpdateEffect,
useMemoizedFn,
useDebounceFn,
} from 'ahooks';
import { type ScrollProps, type InfiniteListDataProps } from './type';
/* 滚动Hooks */
function useForwardFunc<T>(
dataInfo: InfiniteListDataProps<T>,
mutate: Dispatch<SetStateAction<InfiniteListDataProps<T>>>,
) {
// 手动插入数据,不通过接口
const insertData = (item: T, index: number) => {
dataInfo.list.splice(index, 0, item);
mutate({
...dataInfo,
list: [...dataInfo.list],
});
};
// 手动删除数据,不通过接口
const removeData = (index: number) => {
dataInfo.list.splice(index, 1);
mutate({
...dataInfo,
list: [...dataInfo.list],
});
};
const getDataList = () => dataInfo?.list;
return { insertData, removeData, getDataList };
}
// eslint-disable-next-line max-lines-per-function, @coze-arch/max-line-per-function -- 看了下代码行数不太好优化
function useScroll<T>(props: ScrollProps<T>) {
const {
targetRef,
loadData,
threshold,
reloadDeps,
isNeedBtnLoadMore,
resetDataIfReload = true,
} = props;
const [isLoadingError, setIsLoadingError] = useState<boolean>(false);
const refFetchNo = useRef<number>(0);
const refResolve = useRef<(value: InfiniteListDataProps<T>) => void>();
const {
loading,
data: dataInfo,
loadingMore,
loadMore,
noMore,
cancel,
mutate,
reload,
} = useInfiniteScroll<InfiniteListDataProps<T>>(
async current => {
// 此处逻辑如此复杂是解决Scroll中的bug。
// useInfiniteScroll中的cancel只是取消了一次请求但是数据会根据current重新设置一遍。
const defaultData = {
cursor: '0',
list: [],
};
const fetchNo = refFetchNo.current;
if (refResolve.current) {
// 保证顺序执行,如果有当前方法,就取消上一次的请求,防止出现由于网络原因导致数据覆盖问题
// 同时发出A1,A2,三次请求但是A1先到达然后请求了B1, 但是A1过慢导致了A1覆盖了B1的请求。
refResolve.current({
...defaultData,
...(current || {}),
});
}
const result = await new Promise((resolve, reject) => {
refResolve.current = resolve;
loadData(current || defaultData)
.then(value => resolve(value))
.catch(err => reject(err));
});
// @ts-expect-error -- linter-disable-autofix
refResolve.current = null;
// 切换Tab的时候如果此时正在请求防止数据的残留界面显示
if (refFetchNo.current !== fetchNo) {
if (current) {
current.list = [];
}
return {
list: [],
cursor: '0',
};
}
return result as InfiniteListDataProps<T>;
},
{
target: isLoadingError || isNeedBtnLoadMore ? null : targetRef, //失败的时候通过去掉target的事件绑定禁止滚动加载。
threshold,
onBefore: () => {
//setIsLoadingError(false);
},
isNoMore: data => data?.hasMore !== undefined && !data?.hasMore,
onSuccess: () => {
if (isLoadingError) {
setIsLoadingError(false);
}
},
onError: e => {
// 如果在请求第一页数据时发生错误并且当前列表不为空则reset数据
// 这个case只有当resetDataIfReload设置为false时才会发生
// @ts-expect-error -- linter-disable-autofix
if (dataInfo.cursor === '0' && (dataInfo?.list?.length ?? 0) > 0) {
// @ts-expect-error -- linter-disable-autofix
mutate({
...dataInfo,
list: [],
});
}
setIsLoadingError(true);
},
},
);
const { insertData, removeData, getDataList } = useForwardFunc(
// @ts-expect-error -- linter-disable-autofix
dataInfo,
mutate,
);
useEffect(() => {
if (isNeedBtnLoadMore && !(loading || loadingMore)) {
reload();
}
}, []);
const reloadData = useMemoizedFn(() => {
mutate({
// @ts-expect-error -- linter-disable-autofix
list: resetDataIfReload ? [] : dataInfo.list,
hasMore: undefined,
cursor: '0',
});
cancel();
setIsLoadingError(false);
reload();
});
useUpdateEffect(() => {
refFetchNo.current++;
reloadData();
}, [...(reloadDeps || [])]);
const isLoading = loading || loadingMore || props.isLoading;
const { run: loadMoreDebounce } = useDebounceFn(
() => {
if (isLoading) {
return;
}
if (!isNeedBtnLoadMore) {
loadMore();
}
},
{ wait: 500 },
);
useEffect(() => {
const resize = () => {
loadMoreDebounce();
};
window.addEventListener('resize', resize);
return () => {
window.removeEventListener('resize', resize);
};
}, []);
const { list } = dataInfo || {};
return {
dataList: list,
isLoading,
loadMore: () => {
if (!isLoading) {
//如果已经有数据加载中了,需要禁止重复加载。
loadMore();
}
},
reload: reloadData,
noMore,
cancel,
isLoadingError,
mutate,
insertData,
removeData,
getDataList,
};
}
export default useScroll;

View File

@@ -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 { useState } from 'react';
import cls from 'classnames';
import { ActionKey } from '@coze-arch/idl/resource';
import { type ResourceAction } from '@coze-arch/idl/plugin_develop';
import { I18n } from '@coze-arch/i18n';
import { IconCozMore } from '@coze-arch/coze-design/icons';
import { Tooltip, Typography } from '@coze-arch/coze-design';
const { Text } = Typography;
export interface LibraryItemProps {
id: string;
title: string;
description: string;
isSelected?: boolean;
onActive?: (id: string) => void;
actions?: ResourceAction[];
onDeleteAction?: (id: string) => void;
onEditAction?: (id: string) => void;
}
const actionsMap: {
[key in ActionKey.Delete | ActionKey.Edit]: string;
} = {
[ActionKey.Delete]: I18n.t('Delete'),
[ActionKey.Edit]: I18n.t('Edit'),
};
export const LibraryItem = ({
id,
title,
description,
actions,
isSelected,
onActive,
onDeleteAction,
onEditAction,
}: LibraryItemProps) => {
const [isHover, setIsHover] = useState(false);
const handleActions = (action: ActionKey) => {
if (action === ActionKey.Delete) {
onDeleteAction?.(id);
return;
}
if (action === ActionKey.Edit) {
onEditAction?.(id);
}
};
return (
<>
<div
className={cls(
'w-full flex flex-row justify-between items-center overflow-hidden px-3 h-[64px]',
'relative',
'after:content-[""] after:absolute after:left-0 after:right-0',
'after:bottom-0 after:h-[1px] after:coz-mg-primary',
'hover:coz-mg-secondary-hovered hover:rounded hover:after:hidden',
'cursor-pointer',
{
'rounded coz-mg-primary after:hidden': isSelected,
},
)}
onClick={() => {
onActive?.(id);
}}
onMouseEnter={() => {
setIsHover(true);
}}
onMouseLeave={() => {
setIsHover(false);
}}
>
<div className="flex flex-1 min-w-[0px] w-0 flex-col gap-[2px]">
<Text className="text-lg flex-1 font-medium" ellipsis>
{title}
</Text>
<Text
className="text-base"
ellipsis={{
rows: 1,
showTooltip: {
opts: {
position: 'right',
},
},
}}
>
{description}
</Text>
</div>
<Tooltip
position="bottom"
className="!p-1"
content={
<div className="flex flex-col gap-[2px] w-[120px]">
{actions
?.filter(action => action.enable)
?.filter(action => action.key in actionsMap)
.map(action => (
<div
key={action.key}
onClick={() => handleActions(action.key as ActionKey)}
className="w-full text-sm h-[32px] p-2 flex items-center cursor-pointer hover:coz-mg-primary-hovered hover:rounded-mini"
>
{actionsMap[action.key as keyof typeof actionsMap]}
</div>
))}
</div>
}
>
<div
className={cls(
'w-6 h-6 rounded-little coz-mg-secondary-hovered flex items-center justify-center',
{
hidden: !actions?.length || !isHover,
},
)}
onClick={e => e.stopPropagation()}
>
<IconCozMore />
</div>
</Tooltip>
</div>
</>
);
};

View File

@@ -0,0 +1,160 @@
/*
* 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,
type RefObject,
useState,
useRef,
type ForwardedRef,
forwardRef,
useImperativeHandle,
} from 'react';
import { I18n } from '@coze-arch/i18n';
import { IconCozPlus } from '@coze-arch/coze-design/icons';
import EmptyLibraryIcon from '../assets/empty-library-icon.svg';
import {
type LibraryInfo,
type LibraryListRequest,
type LibraryListResponse,
} from './library-request';
import { LibraryItem } from './library-item';
import { InfiniteList, type InfiniteListRef } from './infinite-list';
export type { InfiniteListRef };
interface LibraryListProps {
searchWord?: string | undefined;
category: 'Recommended' | 'Team';
spaceId: string;
size: number;
targetRef: RefObject<HTMLDivElement>;
onActive: (id: string) => void;
onEditAction: (id: string) => void;
onDeleteAction: (id: string) => void;
getData: (req: LibraryListRequest) => Promise<LibraryListResponse>;
onChangeState?: (isLoading: boolean, dataList: LibraryInfo[]) => void;
onEmptyClick?: () => void;
}
export const Index = (
props: LibraryListProps,
ref: ForwardedRef<InfiniteListRef<LibraryInfo>>,
) => {
const {
getData,
onActive,
searchWord,
category,
targetRef,
size,
onChangeState,
onDeleteAction,
onEditAction,
spaceId,
onEmptyClick,
} = props;
const listRef = useRef<InfiniteListRef<LibraryInfo>>(null);
const [dataList, setDataList] = useState<LibraryInfo[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [selectedLibraryId, setSelectedLibraryId] = useState<string>('');
// 切换tab默认选中第一个
useEffect(() => {
if (!dataList.length || isLoading) {
return;
}
const firstLibraryId = dataList[0].id;
setSelectedLibraryId(firstLibraryId);
onActive?.(firstLibraryId);
}, [dataList, isLoading]);
useImperativeHandle(
ref,
() => ({
reload: listRef.current?.reload ?? (() => void 0),
getDataList: listRef.current?.getDataList ?? (() => []),
}),
[],
);
return (
<InfiniteList<LibraryInfo>
ref={listRef}
isNeedBtnLoadMore={false}
onChangeState={(newIsLoading, newDataList) => {
onChangeState?.(newIsLoading, newDataList);
setDataList(newDataList);
setIsLoading(newIsLoading);
}}
renderItem={(card: LibraryInfo) => (
<LibraryItem
key={card.id}
id={card.id}
title={card.name}
description={card.description}
actions={card.actions}
isSelected={selectedLibraryId === card.id}
onActive={id => {
onActive?.(id);
setSelectedLibraryId(id);
}}
onEditAction={id => {
onEditAction?.(id);
}}
onDeleteAction={id => {
onDeleteAction?.(id);
}}
/>
)}
scrollConf={{
reloadDeps: [searchWord, category],
targetRef,
loadData: current =>
getData({
cursor: current?.cursor ?? '0',
searchWord: searchWord ?? '',
category,
spaceId,
size,
}),
}}
emptyConf={{
className: 'flex flex-col items-center justify-center h-full',
icon: <img src={EmptyLibraryIcon} alt="empty-library" />,
size: 'full_screen',
text: {
emptyTitle: I18n.t('prompt_library_empty_title'),
emptyDesc: I18n.t('prompt_library_empty_describe'),
},
btn: {
emptyText: I18n.t('prompt_library_new_prompt'),
emptyButtonProps: {
type: 'primary',
className:
'!coz-mg-hglt !coz-fg-hglt hover:!coz-mg-hglt-hovered active:!coz-mg-hglt-pressed',
icon: <IconCozPlus />,
onClick: () => {
onEmptyClick?.();
},
},
},
}}
/>
);
};
export const LibraryList = forwardRef(Index) as <T>(
props: LibraryListProps & { ref?: RefObject<InfiniteListRef<T>> },
) => JSX.Element;

View File

@@ -0,0 +1,89 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ResourceAction, ResType } from '@coze-arch/idl/plugin_develop';
import { PlaygroundApi, PluginDevelopApi } from '@coze-arch/bot-api';
export interface LibraryInfo {
id: string;
name: string;
description: string;
actions?: ResourceAction[];
promptText?: string;
}
export interface LibraryListRequest {
searchWord: string;
cursor: string;
category: 'Recommended' | 'Team';
spaceId: string;
size: number;
}
export interface LibraryListResponse {
list: LibraryInfo[];
hasMore: boolean;
cursor: string;
code: number;
[key: string]: unknown;
}
export const getTeamLibraryRequest = async (req: LibraryListRequest) => {
const res = await PluginDevelopApi.LibraryResourceList({
space_id: req.spaceId,
size: req.size,
cursor: req.cursor,
name: req.searchWord,
search_keys: ['full_text'],
res_type_filter: [ResType.Prompt],
});
return {
list:
res.resource_list?.map(item => ({
id: item.res_id ?? '',
name: item.name ?? '',
description: item.desc ?? '',
actions: item?.actions ?? [],
})) ?? [],
hasMore: res.has_more ?? false,
cursor: res.cursor ?? '',
code: Number(res.code) ?? 0,
};
};
export const getRecommendLibraryRequest = async (req: LibraryListRequest) => {
const res = await PlaygroundApi.GetOfficialPromptResourceList({
keyword: req.searchWord,
});
return {
list:
res.data?.map(item => ({
id: item.id ?? '',
name: item.name ?? '',
description: item.description ?? '',
promptText: item.prompt_text ?? '',
})) ?? [],
hasMore: false,
cursor: '0',
code: Number(res.code) ?? 0,
};
};
export const getLibraryListByCategory = (
req: LibraryListRequest,
): Promise<LibraryListResponse> => {
if (req.category === 'Team') {
return getTeamLibraryRequest(req);
}
return getRecommendLibraryRequest(req);
};

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>
);