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,28 @@
/*
* 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';
interface CloseModalProps {
onCancel?: (e: React.MouseEvent) => void;
}
export const CloseModal = ({ onCancel }: CloseModalProps) => (
<Button color="primary" onClick={onCancel}>
{I18n.t('Cancel')}
</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 { CloseModal } from './close-modal';
export { SavePrompt } from './save-prompt';
export { PromptDiff } from './prompt-diff';

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 EditorAPI } from '@coze-editor/editor/preset-prompt';
import { I18n } from '@coze-arch/i18n';
import { Button } from '@coze-arch/coze-design';
import { sendTeaEvent, EVENT_NAMES } from '@coze-arch/bot-tea';
interface PromptDiffProps {
mode: 'info' | 'edit' | 'create';
editor?: EditorAPI;
spaceId: string;
botId?: string;
projectId?: string;
workflowId?: string;
source: string;
submitFun?: (
e: React.MouseEvent<Element, MouseEvent>,
) => Promise<{ mode: string; id: string } | undefined>;
editId?: string;
onDiff?: ({
prompt,
libraryId,
}: {
prompt: string;
libraryId: string;
}) => void;
onCancel?: (e: React.MouseEvent<Element, MouseEvent>) => void;
}
export const PromptDiff = ({
mode,
editor,
spaceId,
botId,
projectId,
workflowId,
source,
submitFun,
editId,
onDiff,
onCancel,
}: PromptDiffProps) => {
if (mode === 'info') {
return (
<Button
color="primary"
onClick={e => {
if (!editId) {
return;
}
onDiff?.({ prompt: editor?.getValue() ?? '', libraryId: editId });
sendTeaEvent(EVENT_NAMES.compare_mode_front, {
source,
space_id: spaceId,
action: 'start',
compare_type: 'prompts',
bot_id: botId,
from: 'prompt_resource',
project_id: projectId,
workflow_id: workflowId,
});
onCancel?.(e);
}}
>
{I18n.t('compare_prompt_compare_debug')}
</Button>
);
}
return (
<Button
color="primary"
onClick={async e => {
const res = await submitFun?.(e);
if (res?.id) {
onDiff?.({ prompt: editor?.getValue() ?? '', libraryId: res.id });
sendTeaEvent(EVENT_NAMES.compare_mode_front, {
source,
space_id: spaceId,
action: 'start',
compare_type: 'prompts',
bot_id: botId,
from: 'prompt_resource',
project_id: projectId,
workflow_id: workflowId,
});
}
onCancel?.(e);
}}
>
{I18n.t('creat_prompt_button_comfirm_and_compare')}
</Button>
);
};

View File

@@ -0,0 +1,33 @@
/*
* 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';
interface SavePromptProps {
mode: 'info' | 'edit' | 'create';
isSubmitting?: boolean;
onSubmit?: (e: React.MouseEvent) => void;
}
export const SavePrompt = ({
mode,
isSubmitting,
onSubmit,
}: SavePromptProps) => (
<Button loading={isSubmitting} onClick={onSubmit}>
{mode === 'info' ? I18n.t('prompt_detail_copy_prompt') : I18n.t('Confirm')}
</Button>
);

View File

@@ -0,0 +1,57 @@
/*
* 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 { IconCozEdit } from '@coze-arch/coze-design/icons';
import { Tooltip, Divider, IconButton } from '@coze-arch/coze-design';
interface PromptHeaderProps {
canEdit: boolean;
onEditIconClick?: () => void;
mode: 'info' | 'edit' | 'create';
}
export const PromptHeader = ({
canEdit,
onEditIconClick,
mode,
}: PromptHeaderProps) => {
if (mode === 'info' && canEdit) {
return (
<div className="flex items-center justify-between w-full">
<span>{I18n.t('prompt_detail_prompt_detail')}</span>
<div className="flex items-center ">
<Tooltip content={I18n.t('prompt_library_edit')}>
<IconButton
color="secondary"
icon={<IconCozEdit className="semi-icon-default" />}
onClick={onEditIconClick}
size="small"
/>
</Tooltip>
<Divider layout="vertical" className="mx-[10px] coz-stroke-primary" />
</div>
</div>
);
}
if (mode === 'create') {
return <>{I18n.t('creat_new_prompt')}</>;
}
if (mode === 'edit') {
return <>{I18n.t('edit_prompt')}</>;
}
return <>{I18n.t('prompt_detail_prompt_detail')}</>;
};

View File

@@ -0,0 +1,151 @@
/*
* 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 { useLayoutEffect, type PropsWithChildren } from 'react';
import { useLocalStorageState } from 'ahooks';
import { useEditor, useInjector } from '@coze-editor/editor/react';
import { type EditorAPI } from '@coze-editor/editor/preset-prompt';
import { insertInputSlot } from '@coze-common/editor-plugins/actions';
import { I18n } from '@coze-arch/i18n';
import { IconCozInputSlot } from '@coze-arch/coze-design/icons';
import { Button, Tooltip } from '@coze-arch/coze-design';
import { type ButtonProps } from '@coze-arch/coze-design';
import { keymap } from '@codemirror/view';
import { useReadonly } from '../../shared/hooks/use-editor-readonly';
import InsertBlankSlotGuideEn from '../../assets/insert-blank-slot-guide-en.png';
import InsertBlankSlotGuideCn from '../../assets/insert-blank-slot-guide-cn.png';
import BlankSlotShortCutIcon from '../../assets/blank-slot-shortcut-icon.png';
type NlPromptActionProps = Pick<ButtonProps, 'className'> & {
disabled?: boolean;
};
export const InsertInputSlotButton: React.FC<NlPromptActionProps> = props => {
const { className, disabled } = props;
const editor = useEditor<EditorAPI | undefined>();
const readonly = useReadonly();
const injector = useInjector();
const [showActionGuide, setShowActionGuide] = useLocalStorageState(
insertInputSlotTooltipGuideKey,
{
defaultValue: true,
},
);
useLayoutEffect(
() =>
injector.inject([
keymap.of([
{
key: 'Cmd-k',
run() {
if (!editor || readonly || disabled) {
return false;
}
insertInputSlot(editor);
return false;
},
},
]),
]),
[injector, editor, readonly, disabled],
);
return (
<div className="hover:coz-mg-secondary-hovered coz-icon-button coz-icon-button-default rounded-little">
<GuideTooltip showActionGuide={!!showActionGuide}>
<Button
color="primary"
size="small"
disabled={readonly || disabled}
icon={<IconCozInputSlot />}
className={className}
onMouseDown={e => {
e.preventDefault();
e.stopPropagation();
}}
onClick={e => {
e.preventDefault();
e.stopPropagation();
if (!editor || readonly) {
return;
}
setShowActionGuide(false);
insertInputSlot(editor);
}}
>
{I18n.t('creat_new_prompt_edit_block')}
</Button>
</GuideTooltip>
</div>
);
};
const insertInputSlotTooltipGuideKey = 'insert_input_slot_tooltip_guide';
const GuideTooltip: React.FC<
PropsWithChildren<{
showActionGuide: boolean;
}>
> = ({ showActionGuide, children }) => {
if (showActionGuide) {
return (
<Tooltip
content={
<div className="flex flex-col">
<img
className="w-full h-auto"
src={
IS_CN_REGION ? InsertBlankSlotGuideCn : InsertBlankSlotGuideEn
}
/>
<div className="flex flex-col mt-2 p-2 gap-1">
<div className="flex items-center justify-between ">
<span className="text-xxl font-medium">
{I18n.t('edit_block_guild_title')}
</span>
<img src={BlankSlotShortCutIcon} className="w-[33px] h-5" />
</div>
<div className="text-sm coz-fg-primary">
{I18n.t('edit_block_guild_describe')}
</div>
</div>
</div>
}
className="!w-[301px] !max-w-[301px]"
>
{children}
</Tooltip>
);
}
return (
<Tooltip
content={
<div
className="coz-fg-primary text-sm"
style={{
fontFamily: '-apple-system, SF Pro',
}}
>
K
</div>
}
className="coz-fg-primary text-sm"
>
{children}
</Tooltip>
);
};

View File

@@ -0,0 +1,45 @@
/*
* 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 { useEditor } from '@coze-editor/editor/react';
import { type EditorAPI } from '@coze-editor/editor/preset-prompt';
import { I18n } from '@coze-arch/i18n';
import { useCreatePromptContext } from '@/create-prompt/context';
export const ImportPromptWhenEmptyPlaceholder = () => {
const editor = useEditor<EditorAPI>();
const { props, formApiRef } = useCreatePromptContext() || {};
const { importPromptWhenEmpty } = props || {};
return importPromptWhenEmpty ? (
<div
className="coz-fg-hglt text-sm cursor-pointer mt-1"
onClick={() => {
editor?.$view.dispatch({
changes: {
from: 0,
to: editor.$view.state.doc.length,
insert: importPromptWhenEmpty,
},
});
formApiRef?.current?.setValue('prompt_text', importPromptWhenEmpty);
}}
>
{I18n.t('creat_new_prompt_import_link')}
</div>
) : null;
};

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, useState, type ComponentProps } from 'react';
import { withField, FormInput, FormTextArea } from '@coze-arch/coze-design';
interface PromptInfoInputProps {
readonly?: boolean;
initCount?: number;
value?: string;
disabled?: boolean;
rows?: number;
field: string;
label?: string;
placeholder?: string;
maxLength?: number;
maxCount?: number;
rules?: ComponentProps<typeof FormInput>['rules'];
}
export const PromptInfoInput = (props: PromptInfoInputProps) => {
const { initCount, disabled, rows } = props;
const [count, setCount] = useState(initCount || 0);
const handleChange = (v: string) => {
setCount(v.length);
};
useEffect(() => {
setCount(initCount || 0);
}, [initCount]);
const countSuffix = (
<div className="overflow-hidden coz-fg-secondary text-sm pr-[9px]">{`${count}/${props.maxCount}`}</div>
);
if (disabled) {
return <ReadonlyInput {...props} />;
}
if (rows && rows > 1) {
return (
<FormTextArea
{...props}
autosize
autoComplete="off"
onChange={(value: string) => handleChange(value)}
/>
);
}
return (
<FormInput
{...props}
autoComplete="off"
suffix={countSuffix}
onChange={value => handleChange(value)}
/>
);
};
const ReadonlyInputCom = (props: PromptInfoInputProps) => {
const { value } = props;
return (
<div className="w-full">
<div className="coz-fg-secondary text-base break-all whitespace-pre-line">
{value}
</div>
</div>
);
};
const ReadonlyInput = withField(ReadonlyInputCom, {
valueKey: 'value',
onKeyChangeFnName: 'onChange',
});

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createContext, useContext } from 'react';
import { type FormApi } from '@coze-arch/coze-design';
import { type PromptConfiguratorModalProps } from '../types';
export interface PromptConfiguratorContextType {
props: PromptConfiguratorModalProps;
formApiRef: React.RefObject<FormApi>;
isReadOnly: boolean;
}
export const PromptConfiguratorContext =
createContext<PromptConfiguratorContextType | null>(null);
export const PromptConfiguratorProvider = PromptConfiguratorContext.Provider;
export const useCreatePromptContext = () =>
useContext(PromptConfiguratorContext);

View File

@@ -0,0 +1,7 @@
.prompt-configurator-modal {
:global {
.semi-modal-header {
align-items: center;
}
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { usePromptConfiguratorModal } from './use-modal';
export type { PromptConfiguratorModalProps } from './types';
export { ImportPromptWhenEmptyPlaceholder } from './components/placeholder/import-prompt-when-empty';
export { useCreatePromptContext } from './context';
export { InsertInputSlotButton } from './components/insert-input-slot';
export { PromptConfiguratorModal } from './prompt-configurator-modal';
export type { UsePromptConfiguratorModalProps } from './use-modal';

View File

@@ -0,0 +1,379 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/no-magic-numbers */
/* eslint-disable max-lines-per-function */
import { useEffect, useRef, Suspense, lazy, useState } from 'react';
import classNames from 'classnames';
import {
useEditor,
ActiveLinePlaceholder,
Placeholder,
} from '@coze-editor/editor/react';
import { type EditorAPI } from '@coze-editor/editor/preset-prompt';
import { Modal, Form, Toast, type FormApi } from '@coze-arch/coze-design';
import { sendTeaEvent, EVENT_NAMES } from '@coze-arch/bot-tea';
import { PlaygroundApi } from '@coze-arch/bot-api';
import {
LibraryBlockWidget,
type ILibraryList,
} from '@coze-common/editor-plugins/library-insert';
import { InputSlotWidget } from '@coze-common/editor-plugins/input-slot';
import { ActionBar } from '@coze-common/editor-plugins/action-bar';
import { I18n } from '@coze-arch/i18n';
import { PromptEditorRender } from '@/editor';
import { type PromptConfiguratorModalProps } from './types';
import { PromptConfiguratorProvider } from './context';
import { PromptInfoInput } from './components/prompt-info-input';
import { PromptHeader } from './components/header';
import {
CloseModal,
PromptDiff,
SavePrompt,
} from './components/footer-actions';
import styles from './index.module.less';
const MAX_NAME_LENGTH = IS_OVERSEA ? 40 : 20;
const MAX_DESCRIPTION_LENGTH = IS_OVERSEA ? 100 : 50;
const NAME_ROW_LENGTH = 1;
const DESCRIPTION_ROW_LENGTH = IS_OVERSEA ? 2 : 1;
interface PromptValues {
id?: string;
name: string;
description: string;
prompt_text?: string;
}
const EMPTY_LIBRARY: ILibraryList = [];
const ReactMarkdown = lazy(() => import('react-markdown'));
/* eslint-disable @coze-arch/max-line-per-function */
export const PromptConfiguratorModal = (
props: PromptConfiguratorModalProps,
) => {
const {
mode,
editId,
spaceId,
botId,
projectId,
workflowId,
canEdit,
onUpdateSuccess,
promptSectionConfig,
enableDiff,
onDiff,
defaultPrompt,
source,
containerAppendSlot,
} = props;
const formApiRef = useRef<FormApi | null>(null);
const editor = useEditor<EditorAPI>();
const [modalMode, setModalMode] = useState<'info' | 'edit' | 'create'>(mode);
const [errMsg, setErrMsg] = useState('');
const isSubmiting = useRef(false);
const [actionBarVisible, setActionBarVisible] = useState(false);
const selectionInInputSlotRef = useRef(false);
const isReadOnly = modalMode === 'info';
const {
editorPlaceholder,
editorActions,
headerActions,
editorActiveLinePlaceholder,
editorExtensions,
} = promptSectionConfig ?? {};
const [formValues, setFormValues] = useState<PromptValues>({
name: '',
description: '',
prompt_text: '',
});
const handleSubmit = async (e: React.MouseEvent<Element, MouseEvent>) => {
if (isSubmiting.current) {
return;
}
const submitValues = await formApiRef.current?.validate();
if (!submitValues) {
return;
}
isSubmiting.current = true;
if (modalMode === 'info') {
handleInfoModeAction();
return;
}
if (modalMode === 'create' || modalMode === 'edit') {
const result = await handleUpdateModeAction(e);
isSubmiting.current = false;
sendTeaEvent(EVENT_NAMES.prompt_library_front, {
bot_id: botId,
project_id: projectId,
workflow_id: workflowId,
space_id: spaceId,
prompt_id: result?.id ?? '',
prompt_type: 'workspace',
action: mode,
source,
});
return result;
}
isSubmiting.current = false;
};
const handleInfoModeAction = () => {
const promptText = editor?.getValue();
navigator.clipboard.writeText(promptText ?? '');
Toast.success(I18n.t('prompt_library_prompt_copied_successfully'));
};
const handleUpdateModeAction = async (
e: React.MouseEvent<Element, MouseEvent>,
) => {
try {
const submitValues = await formApiRef.current?.validate();
if (!submitValues) {
return;
}
const res = await PlaygroundApi.UpsertPromptResource(
{
prompt: {
...submitValues,
space_id: spaceId,
...(modalMode === 'edit' && { id: editId }),
},
},
{
__disableErrorToast: true,
},
);
props.onCancel?.(e);
const id = modalMode === 'edit' ? editId : res?.data?.id;
if (mode === 'create') {
Toast.success(I18n.t('prompt_library_prompt_creat_successfully'));
}
onUpdateSuccess?.(mode, id);
if (!id) {
return;
}
return {
mode,
id,
};
} catch (error) {
setErrMsg((error as Error).message);
}
};
useEffect(() => {
if (!defaultPrompt || !editor) {
return;
}
editor?.$view.dispatch({
changes: {
from: 0,
to: editor.$view.state.doc.length,
insert: defaultPrompt,
},
});
}, [defaultPrompt, editor]);
useEffect(() => {
if (!editId || !editor) {
return;
}
PlaygroundApi.GetPromptResourceInfo({
prompt_resource_id: editId,
}).then(
({ data: { name = '', description = '', prompt_text = '' } = {} }) => {
formApiRef.current?.setValues({
prompt_text,
name,
description,
});
editor?.$view.dispatch({
changes: {
from: 0,
to: editor.$view.state.doc.length,
insert: prompt_text,
},
});
setFormValues({
name,
description,
prompt_text,
});
},
);
}, [editId, modalMode, editor]);
return (
<PromptConfiguratorProvider
value={{
props,
formApiRef,
isReadOnly,
}}
>
<Modal
title={
<PromptHeader
canEdit={!!canEdit}
mode={modalMode}
onEditIconClick={() => {
setModalMode('edit');
}}
/>
}
closeOnEsc={false}
maskClosable={false}
visible
width="640px"
footer={
<div className="flex items-center justify-end">
{enableDiff ? (
<PromptDiff
spaceId={spaceId}
botId={botId}
projectId={projectId}
workflowId={workflowId}
source={source}
mode={modalMode}
editor={editor}
submitFun={handleSubmit}
editId={editId}
onDiff={({ prompt, libraryId }) => {
onDiff?.({ prompt, libraryId });
}}
onCancel={e => {
props.onCancel?.(e);
}}
/>
) : (
<CloseModal onCancel={props.onCancel} />
)}
<SavePrompt
mode={modalMode}
isSubmitting={isSubmiting.current}
onSubmit={handleSubmit}
/>
</div>
}
onCancel={props.onCancel}
className={styles['prompt-configurator-modal']}
>
<div className="flex flex-col gap-4">
<div>
<Form<PromptValues>
getFormApi={formApi => {
formApiRef.current = formApi;
}}
>
<PromptInfoInput
disabled={modalMode === 'info'}
label={I18n.t('creat_new_prompt_prompt_name')}
placeholder={I18n.t('creat_new_prompt_name_placeholder')}
maxLength={MAX_NAME_LENGTH}
maxCount={MAX_NAME_LENGTH}
initCount={formValues.name.length}
rows={NAME_ROW_LENGTH}
rules={[
{
required: !isReadOnly,
message: I18n.t('creat_new_prompt_name_placeholder'),
},
]}
field="name"
/>
<PromptInfoInput
disabled={modalMode === 'info'}
label={I18n.t('creat_new_prompt_prompt_description')}
placeholder={I18n.t('creat_new_prompt_des_placeholder')}
maxLength={MAX_DESCRIPTION_LENGTH}
maxCount={MAX_DESCRIPTION_LENGTH}
initCount={formValues.description.length}
rows={DESCRIPTION_ROW_LENGTH}
field="description"
/>
<div className="flex flex-col gap-1">
<div className="flex justify-between items-center">
<Form.Label
text={I18n.t('creat_new_prompt_prompt')}
className="mb-0"
/>
{headerActions}
</div>
<div
className={classNames(
'rounded-lg border border-solid coz-stroke-plus h-[400px] overflow-y-auto styled-scrollbar hover-show-scrollbar',
)}
>
<PromptEditorRender
readonly={modalMode === 'info'}
options={{
minHeight: 300,
}}
onChange={value => {
formApiRef.current?.setValue('prompt_text', value);
}}
/>
<InputSlotWidget
mode="configurable"
onSelectionInInputSlot={selection => {
selectionInInputSlotRef.current = !!selection;
}}
/>
<LibraryBlockWidget
librarys={EMPTY_LIBRARY}
readonly
spaceId={spaceId}
/>
<ActionBar
trigger="custom"
visible={actionBarVisible}
onVisibleChange={visible => {
if (selectionInInputSlotRef.current) {
return;
}
setActionBarVisible(visible);
}}
>
{editorActions}
</ActionBar>
<Placeholder>{editorPlaceholder}</Placeholder>
<ActiveLinePlaceholder>
{editorActiveLinePlaceholder}
</ActiveLinePlaceholder>
{editorExtensions}
</div>
</div>
</Form>
{errMsg ? (
<div className="text-red">
<Suspense fallback={null}>
<ReactMarkdown skipHtml={true} linkTarget="_blank">
{errMsg}
</ReactMarkdown>
</Suspense>
</div>
) : null}
</div>
</div>
</Modal>
{containerAppendSlot}
</PromptConfiguratorProvider>
);
};

View File

@@ -0,0 +1,64 @@
/*
* 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 ModalProps } from '@coze-arch/coze-design';
export interface PromptContextInfo {
botId?: string;
name?: string;
description?: string;
contextHistory?: string;
}
export interface PromptConfiguratorModalProps extends ModalProps {
mode: 'create' | 'edit' | 'info';
editId?: string;
isPersonal?: boolean;
spaceId: string;
botId?: string;
projectId?: string;
workflowId?: string;
defaultPrompt?: string;
canEdit?: boolean;
/** 用于埋点: 页面来源 */
source: string;
enableDiff?: boolean;
promptSectionConfig?: {
/** 提示词输入框的 placeholder */
editorPlaceholder?: React.ReactNode;
/** 提示词划词actions */
editorActions?: React.ReactNode;
/** 头部 actions */
headerActions?: React.ReactNode;
/** 提示词输入框的 active line placeholder */
editorActiveLinePlaceholder?: React.ReactNode;
/** 提示词输入框的 extensions */
editorExtensions?: React.ReactNode;
};
/** 最外层容器插槽 */
containerAppendSlot?: React.ReactNode;
importPromptWhenEmpty?: string;
getConversationId?: () => string | undefined;
getPromptContextInfo?: () => PromptContextInfo;
onUpdateSuccess?: (mode: 'create' | 'edit' | 'info', id?: string) => void;
onDiff?: ({
prompt,
libraryId,
}: {
prompt: string;
libraryId: string;
}) => void;
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/naming-convention */
import { useState } from 'react';
import { PromptEditorProvider } from '@/editor';
import { type PromptConfiguratorModalProps } from './types';
import { PromptConfiguratorModal } from './prompt-configurator-modal';
type DynamicProps = Pick<
PromptConfiguratorModalProps,
'mode' | 'editId' | 'canEdit' | 'defaultPrompt'
>;
export type UsePromptConfiguratorModalProps = Pick<
PromptConfiguratorModalProps,
| 'spaceId'
| 'getConversationId'
| 'getPromptContextInfo'
| 'onUpdateSuccess'
| 'importPromptWhenEmpty'
| 'onDiff'
| 'enableDiff'
| 'isPersonal'
| 'source'
| 'botId'
| 'projectId'
| 'workflowId'
> &
Partial<DynamicProps> & {
CustomPromptConfiguratorModal?: (
props: PromptConfiguratorModalProps,
) => React.JSX.Element;
};
export const usePromptConfiguratorModal = (
props: UsePromptConfiguratorModalProps,
) => {
const { CustomPromptConfiguratorModal = PromptConfiguratorModal } = props;
const [visible, setVisible] = useState(false);
const [dynamicProps, setDynamicProps] = useState<DynamicProps>({
mode: 'create',
editId: '',
canEdit: true,
defaultPrompt: '',
});
const close = () => {
setVisible(false);
};
const open = (
options: Pick<
PromptConfiguratorModalProps,
'mode' | 'editId' | 'canEdit' | 'defaultPrompt'
>,
) => {
setVisible(true);
setDynamicProps(options);
};
return {
node: visible ? (
<PromptEditorProvider>
<CustomPromptConfiguratorModal
{...props}
{...dynamicProps}
onCancel={close}
/>
</PromptEditorProvider>
) : null,
close,
open,
};
};

View File

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

View File

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

View File

@@ -0,0 +1,158 @@
/*
* 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 { useCallback, useRef, useEffect, type ReactNode, useMemo } from 'react';
import { merge } from 'lodash-es';
import { Renderer, Placeholder, useEditor } from '@coze-editor/editor/react';
// promptPreset是针对Prompt提供了一系列内置扩展的集合
import promptPreset, {
type EditorAPI,
} from '@coze-editor/editor/preset-prompt';
import { ThemeExtension } from '@coze-common/editor-plugins/theme';
import { SyntaxHighlight } from '@coze-common/editor-plugins/syntax-highlight';
import { LanguageSupport } from '@coze-common/editor-plugins/language-support';
import { defaultTheme } from '@/theme/default';
export interface PromptEditorRenderProps {
readonly?: boolean; // 所有插入编辑器相关操作禁用action-bar
placeholder?: ReactNode;
className?: string;
dataTestID?: string;
defaultValue?: string;
fontSize?: number;
value?: string;
onChange?: (value: string) => void;
onFocus?: () => void;
/**
* 光标焦点丢失
*/
onBlur?: () => void;
options?: Record<string, string | number>;
isControled?: boolean; // 是否受控
getEditor?: (editor: EditorAPI) => void;
}
export const PromptEditorRender: React.FC<PromptEditorRenderProps> = props => {
const {
readonly,
placeholder,
defaultValue,
className,
dataTestID,
value,
onChange,
onFocus,
onBlur,
options,
isControled,
getEditor,
} = props;
const apiRef = useRef<EditorAPI | null>(null);
const editor = useEditor<EditorAPI>();
useEffect(() => {
if (!editor || !onBlur) {
return;
}
editor.$on('blur', onBlur);
return () => {
editor.$off('blur', onBlur);
};
}, [editor, onBlur]);
useEffect(() => {
if (!editor || !onFocus) {
return;
}
editor.$on('focus', onFocus);
return () => {
editor.$off('focus', onFocus);
};
}, [editor, onFocus]);
// 值受控
useEffect(() => {
const curEditor = apiRef.current;
if (!curEditor || !isControled || !editor) {
return;
}
const preVal = curEditor.getValue();
if (typeof value === 'string' && value !== preVal) {
editor.$view.dispatch({
changes: {
from: 0,
to: editor.$view.state.doc.length,
insert: value ?? '',
},
});
}
}, [isControled, value, editor]);
const handleChange = useCallback(
(e: { value: string }) => {
if (typeof onChange === 'function') {
onChange(e.value);
}
},
[onChange],
);
const contentAttributes = useMemo(
() => ({
class: className ?? '',
'data-testid': dataTestID ?? '',
}),
[className, dataTestID],
);
return (
<>
<Renderer
plugins={promptPreset}
defaultValue={defaultValue}
options={merge(
{
fontSize: 14,
contentAttributes,
editable: !readonly,
readOnly: readonly,
},
options,
)}
onChange={handleChange}
didMount={api => {
apiRef.current = api;
if (getEditor) {
getEditor(api);
}
}}
/>
<Placeholder>{placeholder}</Placeholder>
<ThemeExtension themes={[defaultTheme]} />
<LanguageSupport />
<SyntaxHighlight.Markdown />
<SyntaxHighlight.Jinja />
</>
);
};

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 { PromptEditorRender } from './editor/render';
export { PromptEditorProvider } from './editor/context';
export type { PromptEditorRenderProps } from './editor/render';

View File

@@ -0,0 +1,11 @@
.hover-show-scrollbar {
&::-webkit-scrollbar-thumb {
background-color: transparent;
}
&:hover {
&::-webkit-scrollbar-thumb {
background-color: rgba(6, 7, 9, 20%);
}
}
}

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 { useEffect, useState } from 'react';
import { useEditor } from '@coze-editor/editor/react';
import { type EditorAPI } from '@coze-editor/editor/preset-prompt';
import { type ViewUpdate } from '@codemirror/view';
export const useReadonly = () => {
const editor = useEditor<EditorAPI>();
const [isReadOnly, setIsReadOnly] = useState(false);
useEffect(() => {
if (!editor) {
return;
}
setIsReadOnly(editor.$view.state.readOnly);
const handleViewUpdate = (update: ViewUpdate) => {
if (update.startState.readOnly !== update.state.readOnly) {
setIsReadOnly(update.state.readOnly);
}
};
editor.$on('viewUpdate', handleViewUpdate);
return () => {
editor.$off('viewUpdate', handleViewUpdate);
};
}, [editor]);
return isReadOnly;
};

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState } from 'react';
import { localStorageService } from '@coze-foundation/local-storage';
const SESSION_HIDDEN_KEY = 'coze-promptkit-recommend-pannel-hidden-key';
export const useSetSessionVisiblePersist = (key: string) => {
const [isSessionVisible, setIsSessionVisible] = useState(isKeyExist(key));
return {
isSessionVisible,
toggleSessionVisible: (visible: boolean) => {
const oldValue = localStorageService.getValue(SESSION_HIDDEN_KEY) || '';
if (isKeyExist(key) && visible) {
return;
}
if (visible) {
localStorageService.setValue(
SESSION_HIDDEN_KEY,
oldValue ? `${oldValue},${key}` : key,
);
setIsSessionVisible(true);
return;
}
localStorageService.setValue(
SESSION_HIDDEN_KEY,
oldValue.replace(key, ''),
);
setIsSessionVisible(false);
},
};
};
const isKeyExist = (key: string) => {
const oldValue = localStorageService.getValue(SESSION_HIDDEN_KEY);
return oldValue?.includes(key);
};

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {
createFreeGrabModalHierarchyStore,
type FreeGrabModalHierarchyStore,
} from './service/free-grab-modal-hierarchy-service/store';
export { FreeGrabModalHierarchyService } from './service/free-grab-modal-hierarchy-service';
export { getSelectionBoundary } from './utils/rect';
export { useReadonly } from './hooks/use-editor-readonly';
export { insertToNewline } from './utils/insert-to-newline';
export { type PromptContextInfo } from './types';

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ModalHierarchyServiceConstructor } from './type';
import { type FreeGrabModalHierarchyAction } from './store';
export class FreeGrabModalHierarchyService {
/** Tip: semi modal zIndex 为 1000 */
private baseZIndex = 1000;
public registerModal: FreeGrabModalHierarchyAction['registerModal'];
public removeModal: FreeGrabModalHierarchyAction['removeModal'];
public onFocus: FreeGrabModalHierarchyAction['setModalToTopLayer'];
private getModalIndex: FreeGrabModalHierarchyAction['getModalIndex'];
constructor({
registerModal,
removeModal,
getModalIndex,
setModalToTopLayer,
}: ModalHierarchyServiceConstructor) {
this.registerModal = registerModal;
this.removeModal = removeModal;
this.getModalIndex = getModalIndex;
this.onFocus = setModalToTopLayer;
}
public getModalZIndex = (keyOrIndex: string | number) => {
if (typeof keyOrIndex === 'string') {
return this.getModalIndex(keyOrIndex) + this.baseZIndex;
}
return keyOrIndex + this.baseZIndex;
};
}

View File

@@ -0,0 +1,96 @@
/*
* 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 { devtools } from 'zustand/middleware';
import { create } from 'zustand';
import { produce } from 'immer';
export interface FreeGrabModalHierarchyState {
// modal 的 key list
modalHierarchyList: string[];
}
export interface FreeGrabModalHierarchyAction {
registerModal: (key: string) => void;
removeModal: (key: string) => void;
getModalIndex: (key: string) => number;
setModalToTopLayer: (key: string) => void;
}
/**
* 可自由拖拽的弹窗之间的层级关系
*/
export const createFreeGrabModalHierarchyStore = () =>
create<FreeGrabModalHierarchyState & FreeGrabModalHierarchyAction>()(
devtools(
(set, get) => ({
modalHierarchyList: [],
getModalIndex: key =>
get().modalHierarchyList.findIndex(modalKey => modalKey === key),
registerModal: key => {
set(
{
modalHierarchyList: produce(get().modalHierarchyList, draft => {
draft.unshift(key);
}),
},
false,
'registerModal',
);
},
removeModal: key => {
set(
{
modalHierarchyList: produce(get().modalHierarchyList, draft => {
const index = get().getModalIndex(key);
if (index < 0) {
return;
}
draft.splice(index, 1);
}),
},
false,
'removeModal',
);
},
setModalToTopLayer: key => {
set(
{
modalHierarchyList: produce(get().modalHierarchyList, draft => {
const index = get().getModalIndex(key);
if (index < 0) {
return;
}
get().removeModal(key);
get().registerModal(key);
}),
},
false,
'setModalToTopLayer',
);
},
}),
{
enabled: IS_DEV_MODE,
name: 'botStudio.botEditor.ModalHierarchy',
},
),
);
export type FreeGrabModalHierarchyStore = ReturnType<
typeof createFreeGrabModalHierarchyStore
>;

View File

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

View File

@@ -0,0 +1,17 @@
/*
* 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 { type PromptContextInfo } from './prompt';

View File

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

View File

@@ -0,0 +1,58 @@
/*
* 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';
export const insertToNewline = async ({
editor,
prompt,
}: {
editor?: EditorAPI;
prompt: string;
}): Promise<string> => {
if (!editor) {
return '';
}
const { state } = editor.$view;
const isDocEmpty = state.doc.length === 0;
const insertPrompt = isDocEmpty ? prompt : `\n${prompt}`;
const selection = isDocEmpty
? undefined
: {
anchor: state.doc.length,
head: state.doc.length + insertPrompt.length,
};
editor.$view.dispatch({
changes: {
from: state.doc.length,
to: state.doc.length,
insert: insertPrompt,
},
selection,
scrollIntoView: true,
});
// 等待下一个微任务周期,确保状态已更新
await Promise.resolve();
// 使用更新后的state获取最新文档内容
const newDoc = editor.$view.state.doc.toString();
// 插入到新一行
// 注意:该操作提前会触发 chrome bug 导致崩溃问题
editor.focus();
return newDoc;
};

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type EditorAPI } from '@coze-editor/editor/preset-prompt';
export const getSelectionBoundary = (editor: EditorAPI) => {
const rects = editor.getMainSelectionRects();
if (rects.length === 0) {
return { left: 0, top: 0, width: 0, height: 0 };
}
// 初始化最大矩形
let maxLeft = Infinity;
let maxTop = Infinity;
let maxRight = -Infinity;
let maxBottom = -Infinity;
// 遍历所有矩形,计算包围盒的边界
rects.forEach(rect => {
maxLeft = Math.min(maxLeft, rect.left);
maxTop = Math.min(maxTop, rect.top);
maxRight = Math.max(maxRight, rect.left + (rect.width ?? 0));
maxBottom = Math.max(maxBottom, rect.top + (rect.height ?? 0));
});
// 计算最终的宽度和高度
const width = maxRight - maxLeft;
const height = maxBottom - maxTop;
// 获取编辑器的滚动位置
const { scrollLeft } = editor.$view.scrollDOM;
const { scrollTop } = editor.$view.scrollDOM;
// 获取编辑器容器的位置
const editorRect = editor.$view.dom.getBoundingClientRect();
// 计算相对于视口的绝对位置
const absoluteLeft = editorRect.left + maxLeft - scrollLeft;
const absoluteTop = editorRect.top + maxTop - scrollTop;
const absoluteBottom = editorRect.top + maxBottom - scrollTop;
return {
left: absoluteLeft,
top: absoluteTop,
bottom: absoluteBottom,
width,
height,
};
};

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 { type Extension } from '@coze-common/editor-plugins/types';
import { EditorView } from '@codemirror/view';
export const defaultTheme: Extension = EditorView.theme({
'.cm-content': {
color: 'rgba(6, 7, 9, 0.80)',
},
'.cm-line': {
lineHeight: '24px',
paddingLeft: '12px',
},
'.cm-cursor': {
height: '20px !important',
},
});

View File

@@ -0,0 +1,23 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// <reference types='@coze-arch/bot-typings' />
declare const IS_OVERSEA: boolean;
declare const IS_CN_REGION: boolean;
declare const IS_DEV_MODE: boolean;
declare const IS_PROD_MODE: boolean;
declare const IS_RELEASE_VERSION: boolean;