feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -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>
|
||||
);
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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')}</>;
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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);
|
||||
@@ -0,0 +1,7 @@
|
||||
.prompt-configurator-modal {
|
||||
:global {
|
||||
.semi-modal-header {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user