feat: manually mirror opencoze's code from bytedance

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

View File

@@ -0,0 +1,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,
};
};