概述
本文深入分析Coze Studio中用户创建提示词功能的前端实现。该功能允许用户在资源库中创建、编辑和管理提示词资源,为开发者提供了强大的提示词管理能力。通过对源码的详细解析,我们将了解从资源库入口到提示词配置弹窗的完整架构设计、组件实现、状态管理和用户体验优化等核心技术要点。
功能特性
核心功能
- 提示词创建:支持自定义提示词名称、描述和内容
- 提示词管理:提供提示词列表展示、编辑和删除功能
- 富文本编辑:基于CodeMirror的专业提示词编辑器
- 实时预览:支持Markdown和Jinja语法高亮
- 模板插槽:支持输入槽位和库引用功能
用户体验特性
- 即时反馈:操作结果实时展示和验证
- 智能提示:编辑器提供语法提示和自动补全
- 便捷操作:支持复制、比较和快速编辑
- 国际化支持:多语言界面适配
技术架构
整体架构设计
┌─────────────────────────────────────────────────────────────┐
│ 提示词管理模块 │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ LibraryPage │ │LibraryHeader│ │ PromptConfigurator │ │
│ │ (资源库页面) │ │ (添加按钮) │ │ Modal │ │
│ └─────────────┘ └─────────────┘ │ (创建/编辑弹窗) │ │
│ ┌─────────────┐ ┌─────────────┐ └─────────────────────┘ │
│ │BaseLibrary │ │ Table │ ┌─────────────────────┐ │
│ │ Page │ │ (资源列表) │ │ PromptEditorRender │ │
│ └─────────────┘ └─────────────┘ │ (编辑器组件) │ │
│ └─────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 状态管理层 │
│ ┌─────────────────┐ ┌─────────────────────────────────┐ │
│ │usePromptConfig │ │ API Hooks │ │
│ │ (配置逻辑) │ │ UpsertPromptResource │ │
│ └─────────────────┘ └─────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ API服务层 │
│ ┌─────────────────────────────────────────────────────────┐
│ │ Playground API │
│ │ UpsertPromptResource │
│ └─────────────────────────────────────────────────────────┘
└────────────────────────────────────────────────────────────┘
核心模块结构
frontend/
├── apps/coze-studio/src/
│ └── pages/
│ └── library.tsx # 资源库入口页面
├── packages/studio/workspace/
│ ├── entry-adapter/src/pages/library/
│ │ └── index.tsx # LibraryPage适配器组件
│ └── entry-base/src/pages/library/
│ ├── index.tsx # BaseLibraryPage核心组件
│ ├── components/
│ │ └── library-header.tsx # LibraryHeader头部组件
│ └── hooks/use-entity-configs/
│ └── use-prompt-config.tsx # 提示词配置Hook
├── packages/common/prompt-kit/
│ ├── base/src/
│ │ ├── create-prompt/
│ │ │ ├── prompt-configurator-modal.tsx # 提示词配置弹窗
│ │ │ ├── context/
│ │ │ │ └── index.tsx # 提示词配置上下文
│ │ │ ├── types.ts # 类型定义
│ │ │ ├── use-modal.tsx # 弹窗Hook
│ │ │ └── components/
│ │ │ ├── prompt-info-input.tsx # 名称描述输入组件
│ │ │ ├── header.tsx # 弹窗头部组件
│ │ │ └── footer-actions/ # 底部操作按钮
│ │ │ ├── close-modal.tsx
│ │ │ ├── save-prompt.tsx
│ │ │ └── prompt-diff.tsx
│ │ └── editor/
│ │ ├── index.tsx # 编辑器导出
│ │ ├── render.tsx # PromptEditorRender组件
│ │ └── context/
│ │ └── index.tsx # 编辑器上下文
│ └── adapter/src/
│ └── create-prompt/
│ ├── index.tsx # 适配器导出
│ ├── prompt-configurator-modal.tsx # 适配器弹窗
│ └── use-modal.tsx # 适配器Hook
└── packages/arch/bot-api/src/
└── playground/
└── index.ts # PlaygroundApi定义
用户创建提示词流程概述
用户登录Coze Studio
↓
点击"资源库"菜单
↓
LibraryPage 组件加载
↓
点击右上角"+"按钮
↓
LibraryHeader 显示创建菜单
↓
点击"提示词"选项
↓
openCreatePrompt() 触发
↓
PromptConfiguratorModal 弹窗显示
↓
用户输入提示词名称(name字段)
↓
用户输入提示词描述(description字段)
↓
用户在PromptEditorRender中编写提示词内容
↓
表单验证(名称必填)
↓
用户点击"保存"按钮
↓
handleSubmit() 触发
↓
PlaygroundApi.UpsertPromptResource() 调用
↓
后端创建新提示词资源
↓
onUpdateSuccess() 处理成功响应
↓
Toast.success() 显示成功提示
↓
刷新资源库列表
该流程包含多层验证和处理:
- 前端表单验证:通过Form组件进行名称必填验证
- 编辑器集成:使用CodeMirror编辑器提供专业的提示词编写体验
- API调用:使用UpsertPromptResource API统一处理创建和更新
- 成功处理:通过Toast提示用户操作成功,并自动刷新列表
- 状态管理:通过usePromptConfig Hook管理弹窗状态和数据流
整个流程确保了提示词创建的便捷性和用户体验的流畅性。
核心组件实现
组件层次结构
提示词创建功能涉及多个层次的组件:
- LibraryPage组件:资源库主页面
- BaseLibraryPage组件:资源库核心逻辑
- LibraryHeader组件:包含创建按钮的头部
- PromptConfiguratorModal组件:提示词配置弹窗
- PromptEditorRender组件:提示词编辑器
1. 资源库入口组件(LibraryPage)
文件位置:frontend/packages/studio/workspace/entry-adapter/src/pages/library/index.tsx
作为资源库的适配器组件,整合各种资源配置:
typescript
import { type FC, useRef } from 'react';
import {
BaseLibraryPage,
useDatabaseConfig,
usePluginConfig,
useWorkflowConfig,
usePromptConfig,
useKnowledgeConfig,
} from '@coze-studio/workspace-base/library';
export const LibraryPage: FC<{ spaceId: string }> = ({ spaceId }) => {
const basePageRef = useRef<{ reloadList: () => void }>(null);
const configCommonParams = {
spaceId,
reloadList: () => {
basePageRef.current?.reloadList();
},
};
const { config: pluginConfig, modals: pluginModals } =
usePluginConfig(configCommonParams);
const { config: workflowConfig, modals: workflowModals } =
useWorkflowConfig(configCommonParams);
const { config: knowledgeConfig, modals: knowledgeModals } =
useKnowledgeConfig(configCommonParams);
const { config: promptConfig, modals: promptModals } =
usePromptConfig(configCommonParams);
const { config: databaseConfig, modals: databaseModals } =
useDatabaseConfig(configCommonParams);
return (
<>
<BaseLibraryPage
spaceId={spaceId}
ref={basePageRef}
entityConfigs={[
pluginConfig,
workflowConfig,
knowledgeConfig,
promptConfig,
databaseConfig,
]}
/>
{pluginModals}
{workflowModals}
{promptModals}
{databaseModals}
{knowledgeModals}
</>
);
};
<ResultModal
visible={!!successData}
data={successData}
onOk={refresh}
/>
</>
);
};
设计亮点:
- 状态集中管理 :通过
usePatOperation
Hook统一管理组件状态 - 组件解耦:各子组件职责明确,通过props进行通信
- 数据流清晰:单向数据流,状态变更可追踪
2. 资源库核心组件(BaseLibraryPage)
文件位置:frontend/packages/studio/workspace/entry-base/src/pages/library/index.tsx
负责资源库的核心展示逻辑:
typescript
import { forwardRef, useImperativeHandle } from 'react';
import classNames from 'classnames';
import { useInfiniteScroll } from 'ahooks';
import { I18n } from '@coze-arch/i18n';
import {
Table,
Select,
Search,
Layout,
Cascader,
Space,
} from '@coze-arch/coze-design';
import { renderHtmlTitle } from '@coze-arch/bot-utils';
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
import {
type ResType,
type LibraryResourceListRequest,
type ResourceInfo,
} from '@coze-arch/bot-api/plugin_develop';
import { PluginDevelopApi } from '@coze-arch/bot-api';
import { type ListData, type BaseLibraryPageProps } from './types';
import { LibraryHeader } from './components/library-header';
export const BaseLibraryPage = forwardRef<
{ reloadList: () => void },
BaseLibraryPageProps
>(
({ spaceId, isPersonalSpace = true, entityConfigs }, ref) => {
const { params, setParams, resetParams, hasFilter, ready } =
useCachedQueryParams({
spaceId,
});
const listResp = useInfiniteScroll<ListData>(
async prev => {
if (!ready) {
return {
list: [],
nextCursorId: undefined,
hasMore: false,
};
}
const resp = await PluginDevelopApi.LibraryResourceList(
entityConfigs.reduce<LibraryResourceListRequest>(
(res, config) => config.parseParams?.(res) ?? res,
{
...params,
cursor: prev?.nextCursorId,
space_id: spaceId,
size: LIBRARY_PAGE_SIZE,
},
),
);
return {
list: resp?.resource_list || [],
nextCursorId: resp?.cursor,
hasMore: !!resp?.has_more,
};
},
{
reloadDeps: [params, spaceId],
},
);
useImperativeHandle(ref, () => ({
reloadList: listResp.reload,
}));
return (
<Layout
className={s['layout-content']}
title={renderHtmlTitle(I18n.t('navigation_workspace_library'))}
>
<Layout.Header className={classNames(s['layout-header'], 'pb-0')}>
<div className="w-full">
<LibraryHeader entityConfigs={entityConfigs} />
{/* 过滤器组件 */}
</div>
</Layout.Header>
<Layout.Content>
{/* 表格和列表内容 */}
</Layout.Content>
</Layout>
);
}
);
3. 资源库头部组件(LibraryHeader)
文件位置:frontend/packages/studio/workspace/entry-base/src/pages/library/components/library-header.tsx
包含创建资源的入口按钮:
typescript
import React from 'react';
import { I18n } from '@coze-arch/i18n';
import { IconCozPlus } from '@coze-arch/coze-design/icons';
import { Button, Menu } from '@coze-arch/coze-design';
import { type LibraryEntityConfig } from '../types';
export const LibraryHeader: React.FC<{
entityConfigs: LibraryEntityConfig[];
}> = ({ entityConfigs }) => (
<div className="flex items-center justify-between mb-[16px]">
<div className="font-[500] text-[20px]">
{I18n.t('navigation_workspace_library')}
</div>
<Menu
position="bottomRight"
className="w-120px mt-4px mb-4px"
render={
<Menu.SubMenu mode="menu">
{entityConfigs.map(config => config.renderCreateMenu?.() ?? null)}
</Menu.SubMenu>
}
>
<Button
theme="solid"
type="primary"
icon={<IconCozPlus />}
data-testid="workspace.library.header.create"
>
{I18n.t('library_resource')}
</Button>
</Menu>
</div>
);
4. 提示词配置Hook(usePromptConfig)
文件位置:frontend/packages/studio/workspace/entry-base/src/pages/library/hooks/use-entity-configs/use-prompt-config.tsx
管理提示词创建和编辑的状态:
typescript
import { useNavigate } from 'react-router-dom';
import { useRef } from 'react';
import { useRequest } from 'ahooks';
import {
ActionKey,
ResType,
type ResourceInfo,
} from '@coze-arch/idl/plugin_develop';
import { I18n } from '@coze-arch/i18n';
import { IconCozLightbulb } from '@coze-arch/coze-design/icons';
import { Table, Menu, Toast } from '@coze-arch/coze-design';
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
import { PlaygroundApi } from '@coze-arch/bot-api';
import { usePromptConfiguratorModal } from '@coze-common/prompt-kit-adapter/create-prompt';
import { type UseEntityConfigHook } from './types';
const { TableAction } = Table;
export const usePromptConfig: UseEntityConfigHook = ({
spaceId,
isPersonalSpace = true,
reloadList,
getCommonActions,
}) => {
const navigate = useNavigate();
const recordRef = useRef<ResourceInfo | null>(null);
const { open: openCreatePrompt, node: promptConfiguratorModal } =
usePromptConfiguratorModal({
spaceId,
source: 'resource_library',
onUpdateSuccess: reloadList,
});
// delete
const { run: delPrompt } = useRequest(
(promptId: string) =>
PlaygroundApi.DeletePromptResource({
prompt_resource_id: promptId,
}),
{
manual: true,
onSuccess: () => {
reloadList();
Toast.success(I18n.t('Delete_success'));
},
},
);
return {
modals: (
<>
{promptConfiguratorModal}
</>
),
config: {
typeFilter: {
label: I18n.t('library_resource_type_prompt'),
value: ResType.Prompt,
},
renderCreateMenu: () => (
<Menu.Item
data-testid="workspace.library.header.create.prompt"
icon={<IconCozLightbulb />}
onClick={() => {
sendTeaEvent(EVENT_NAMES.widget_create_click, {
source: 'menu_bar',
workspace_type: isPersonalSpace
? 'personal_workspace'
: 'team_workspace',
});
openCreatePrompt({
mode: 'create',
});
}}
>
{I18n.t('creat_new_prompt_prompt')}
</Menu.Item>
),
target: [ResType.Prompt],
onItemClick: (record: ResourceInfo) => {
recordRef.current = record;
const canEdit = record.actions?.find(
action => action.key === ActionKey.Edit,
)?.enable;
openCreatePrompt({
mode: 'info',
canEdit,
editId: record.res_id || '',
});
},
renderActions: (libraryResource: ResourceInfo) => (
<TableAction
deleteProps={{
disabled: !libraryResource.actions?.find(
action => action.key === ActionKey.Delete,
)?.enable,
deleteDesc: I18n.t('prompt_resource_delete_describ'),
handler: () => {
delPrompt(libraryResource.res_id || '');
},
}}
editProps={{
disabled: !libraryResource.actions?.find(
action => action.key === ActionKey.Edit,
)?.enable,
handler: () => {
openCreatePrompt({
mode: 'edit',
editId: libraryResource.res_id || '',
});
},
}}
actionList={getCommonActions?.(libraryResource)}
/>
),
},
};
};
5. 提示词配置弹窗(PromptConfiguratorModal)
文件位置:frontend/packages/common/prompt-kit/base/src/create-prompt/prompt-configurator-modal.tsx
提示词创建和编辑的主要界面:
typescript
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 { 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';
interface PromptValues {
id?: string;
name: string;
description: string;
prompt_text?: string;
}
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 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);
}
};
return (
<PromptConfiguratorProvider props={props}>
<Modal {...props}>
<Form ref={formApiRef} layout="vertical">
<PromptInfoInput />
<PromptEditorRender
placeholder={editorPlaceholder}
getEditor={(api) => {
// 编辑器初始化逻辑
}}
/>
</Form>
</Modal>
</PromptConfiguratorProvider>
);
};
6. 提示词信息输入组件(PromptInfoInput)
文件位置:frontend/packages/common/prompt-kit/base/src/create-prompt/components/prompt-info-input.tsx
处理提示词名称和描述的输入:
typescript
export const PromptInfoInput: React.FC = () => {
return (
<>
<Form.Item
name="name"
label={I18n.t('prompt_name')}
rules={[
{ required: true, message: I18n.t('prompt_name_required') },
{ max: 50, message: I18n.t('prompt_name_too_long') },
]}
>
<Input placeholder={I18n.t('enter_prompt_name')} />
</Form.Item>
<Form.Item
name="description"
label={I18n.t('prompt_description')}
>
<Input.TextArea
placeholder={I18n.t('enter_prompt_description')}
rows={3}
maxLength={200}
/>
</Form.Item>
</>
);
};
7. 提示词编辑器组件(PromptEditorRender)
文件位置:frontend/packages/common/prompt-kit/base/src/editor/render.tsx
基于CodeMirror的专业提示词编辑器:
typescript
import { useCallback, useRef, useEffect, type ReactNode, useMemo } from 'react';
import { merge } from 'lodash-es';
import { Renderer, Placeholder, useEditor } from '@coze-editor/editor/react';
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;
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]);
// value controlled
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 />
</>
);
};
设计亮点:
- 状态集中管理 :通过
usePatOperation
Hook统一管理组件状态 - 组件解耦:各子组件职责明确,通过props进行通信
- 数据流清晰:单向数据流,状态变更可追踪
创建提示词逻辑
1. 表单验证系统
文件位置:frontend/packages/common/prompt-kit/base/src/utils/validation.ts
表单验证规则:
typescript
export interface ValidationRule {
required?: boolean;
maxLength?: number;
minLength?: number;
pattern?: RegExp;
validator?: (value: any) => Promise<void> | void;
message?: string;
}
export const promptValidationRules = {
name: [
{
required: true,
message: '请输入提示词名称',
},
{
maxLength: 50,
message: '提示词名称不能超过50个字符',
},
{
pattern: /^[\u4e00-\u9fa5a-zA-Z0-9_\-\s]+$/,
message: '提示词名称只能包含中文、英文、数字、下划线和连字符',
},
],
description: [
{
maxLength: 200,
message: '描述不能超过200个字符',
},
],
promptText: [
{
required: true,
message: '请输入提示词内容',
},
{
minLength: 10,
message: '提示词内容至少需要10个字符',
},
{
maxLength: 10000,
message: '提示词内容不能超过10000个字符',
},
],
};
export const validatePromptForm = async (values: any) => {
const errors: Record<string, string> = {};
// 验证名称
for (const rule of promptValidationRules.name) {
try {
await validateField(values.name, rule);
} catch (error) {
errors.name = error.message;
break;
}
}
// 验证描述
for (const rule of promptValidationRules.description) {
try {
await validateField(values.description, rule);
} catch (error) {
errors.description = error.message;
break;
}
}
// 验证提示词内容
for (const rule of promptValidationRules.promptText) {
try {
await validateField(values.promptText, rule);
} catch (error) {
errors.promptText = error.message;
break;
}
}
if (Object.keys(errors).length > 0) {
throw new Error(JSON.stringify(errors));
}
};
const validateField = async (value: any, rule: ValidationRule) => {
if (rule.required && (!value || value.trim() === '')) {
throw new Error(rule.message || '此字段为必填项');
}
if (rule.maxLength && value && value.length > rule.maxLength) {
throw new Error(rule.message || `长度不能超过${rule.maxLength}个字符`);
}
if (rule.minLength && value && value.length < rule.minLength) {
throw new Error(rule.message || `长度不能少于${rule.minLength}个字符`);
}
if (rule.pattern && value && !rule.pattern.test(value)) {
throw new Error(rule.message || '格式不正确');
}
if (rule.validator) {
await rule.validator(value);
}
};
设计亮点:
- 规则化验证:使用配置化的验证规则
- 异步支持:支持异步验证器
- 错误收集:统一收集和处理验证错误
- 可扩展性:易于添加新的验证规则
2.核心逻辑
文件位置:frontend/packages/common/prompt-kit/base/src/components/prompt-configurator-modal.tsx
核心代码:
typescript
export const PromptConfiguratorModal: FC<PromptConfiguratorModalProps> = ({
visible,
mode,
editId,
enableDiff,
onClose,
onSuccess,
}) => {
const [form] = Form.useForm();
const { editor, setEditor } = useEditor();
const [loading, setLoading] = useState(false);
// 获取提示词详情
const { data: promptInfo, loading: fetchLoading } = useRequest(
() => {
if (editId && mode !== 'create') {
return PlaygroundApi.GetPromptResourceInfo({ prompt_resource_id: editId });
}
return Promise.resolve(null);
},
{
refreshDeps: [editId, mode],
onSuccess: (data) => {
if (data?.prompt_resource) {
const { name, description, prompt_text } = data.prompt_resource;
form.setFieldsValue({ name, description });
editor?.commands.setContent(prompt_text || '');
}
},
}
);
// 保存提示词
const handleSave = async () => {
try {
setLoading(true);
const values = await form.validateFields();
const promptText = editor?.getText() || '';
const promptData = {
id: editId,
name: values.name,
description: values.description,
prompt_text: promptText,
};
await PlaygroundApi.UpsertPromptResource({ prompt: promptData });
onSuccess?.();
onClose();
} catch (error) {
console.error('Save prompt failed:', error);
} finally {
setLoading(false);
}
};
return (
<Modal
title={mode === 'create' ? '创建提示词' : mode === 'edit' ? '编辑提示词' : '查看提示词'}
visible={visible}
onCancel={onClose}
footer={mode === 'info' ? null : [
<Button key="cancel" onClick={onClose}>取消</Button>,
<Button key="save" type="primary" loading={loading} onClick={handleSave}>
保存
</Button>,
]}
>
<Form form={form} layout="vertical">
<PromptInfoInput
field="name"
label="提示词名称"
required
readonly={mode === 'info'}
maxLength={50}
/>
<PromptInfoInput
field="description"
label="描述"
rows={3}
readonly={mode === 'info'}
maxLength={200}
/>
</Form>
<PromptEditorRender
readonly={mode === 'info'}
placeholder="请输入提示词内容..."
onChange={(content) => {
// 实时更新编辑器内容
}}
/>
</Modal>
);
};
bot-api/package.json
文件位置:frontend/packages/arch/bot-api/package.json
核心代码:
json
{
"name": "@coze-arch/bot-api",
"version": "0.0.1",
"description": "RPC wrapper for bot studio application",
"author": "fanwenjie.fe@bytedance.com",
"exports": {
".": "./src/index.ts",
},
}
代码作用:
- 1.包定义 :定义了一个名为 @coze-arch/bot-api 的 npm 包,版本为 0.0.1,这是一个用于 bot studio 应用的 RPC 包装器。
- 2.通过主入口文件 :
在frontend\packages\arch\bot-api\src\index.ts
中, PlaygroundApi 被导出:
typescript
export { PlaygroundApi } from './playground-api';
这允许通过 @coze-arch/bot-api 直接导入 PlaygroundApi 。
3.PlaygroundApi 实现 :在 src/playground-api.ts 中, PlaygroundApi 是一个配置好的服务实例,它使用了 PlaygroundService 和 axios 请求配置。
src/playground-api.ts
文件位置:frontend\packages\arch\bot-api\src\playground-api.ts
核心代码:
typescript
import PlaygroundApiService from './idl/playground_api';
import { axiosInstance, type BotAPIRequestConfig } from './axios';
// eslint-disable-next-line @typescript-eslint/naming-convention
export const PlaygroundApi = new PlaygroundApiService<BotAPIRequestConfig>({
request: (params, config = {}) => {
config.headers = Object.assign(config.headers || {}, {
'Agw-Js-Conv': 'str',
});
return axiosInstance.request({ ...params, ...config });
},
});
axiosInstance说明
1.axiosInstance 在整个项目中是全局共享的
2.bot-api 包中的导入 ( frontend/packages/arch/bot-api/src/axios.ts )
是直接从 @coze-arch/bot-http 包导入了 axiosInstance 。
typescript
import {
axiosInstance,
isApiError,
type AxiosRequestConfig,
} from '@coze-arch/bot-http';
3.bot-http 包中的定义 ( frontend/packages/arch/bot-http/src/axios.ts ):
typescript
export const axiosInstance = axios.create();
这里创建了一个全局的 axios 实例,与用户名修改保存请求的 axios 实例是同一个。
PlaygroundApiService说明
1.bot-api包中的导入路径:
import PlaygroundApiService from './idl/playground_api';
实际指向
frontend/packages/arch/bot-api/src/idl/playground_api.ts
文件内容重新导出了 @coze-arch/idl/playground_api 包的所有内容,包括默认导出
typescript
export * from '@coze-arch/idl/playground_api';
export { default as default } from '@coze-arch/idl/playground_api';
2.idl包的模块映射
文件位置:frontend/packages/arch/idl/package.json
核心代码:
json
"name": "@coze-arch/idl",
"version": "0.0.1",
"description": "IDL files for bot studio application",
"author": "fanwenjie.fe@bytedance.com",
"exports": {
"./playground_api": "./src/auto-generated/playground_api/index.ts",
代码作用:将 @coze-arch/idl/playground_api 映射到实际文件路径frontend/packages/arch/idl/src/auto-generated/playground_api/index.ts
这个文件说明后续见 添加提示词-API接口实现 这个章节。
API层设计与实现
IDL基础类型定义(base.thrift)
文件位置:idl/base.thrift
核心代码:
thrift
namespace py base
namespace go base
namespace java com.bytedance.thrift.base
struct TrafficEnv {
1: bool Open = false,
2: string Env = "" ,
}
struct Base {
1: string LogID = "",
2: string Caller = "",
3: string Addr = "",
4: string Client = "",
5: optional TrafficEnv TrafficEnv ,
6: optional map<string,string> Extra ,
}
struct BaseResp {
1: string StatusMessage = "",
2: i32 StatusCode = 0 ,
3: optional map<string,string> Extra ,
}
struct EmptyReq {
}
struct EmptyData {}
struct EmptyResp {
1: i64 code,
2: string msg ,
3: EmptyData data,
}
struct EmptyRpcReq {
255: optional Base Base,
}
struct EmptyRpcResp {
255: optional BaseResp BaseResp,
}
文件作用:
定义了项目中所有接口的基础数据结构,作为其他IDL文件的依赖基础。
创建提示词-IDL结构体定义(prompt_resource.thrift)
文件路径:idl\playground\prompt_resource.thrift
核心代码:
thrift
namespace go playground
include "../base.thrift"
struct PromptResource {
1: optional i64 ID (agw.js_conv="str",api.js_conv="true",api.body="id")
2: optional i64 SpaceID (agw.js_conv="str",api.js_conv="true",api.body="space_id")
3: optional string Name (api.body="name")
4: optional string Description (api.body="description")
5: optional string PromptText (api.body="prompt_text")
}
struct UpsertPromptResourceRequest {
1: required PromptResource Prompt (api.body="prompt")
255: base.Base Base (api.none="true")
}
struct UpsertPromptResourceResponse {
1: optional ShowPromptResource data
253: required i64 code
254: required string msg
255: required base.BaseResp BaseResp
}
struct ShowPromptResource {
1: i64 ID (agw.js_conv="str",api.js_conv="true",api.body="id")
}
源码作用:定义PAT权限添加令牌相关的数据结构
创建提示词-IDL接口定义(playground_service.thrift)
文件路径:idl\playground\playground_service.thrift
核心代码:
thrift
include "../base.thrift"
include "prompt_resource.thrift"
namespace go playground
service PlaygroundService {
prompt_resource.UpsertPromptResourceResponse UpsertPromptResource(1:prompt_resource.UpsertPromptResourceRequest request)(api.post='/api/playground_api/upsert_prompt_resource', api.category="prompt_resource",agw.preserve_base="true")
}
源码作用:定义提示词资源创建、更新、删除和获取相关的接口
创建提示词-API接口实现(playground_api/index.ts)
文件位置:frontend/packages/arch/idl/src/auto-generated/playground_api/index.ts
核心代码:
typescript
export default class PlaygroundApiService<T> {
private request: any = () => {
throw new Error('PlaygroundApiService.request is undefined');
};
private baseURL: string | ((path: string) => string) = '';
constructor(options?: {
baseURL?: string | ((path: string) => string);
request?<R>(
params: {
url: string;
method: 'GET' | 'DELETE' | 'POST' | 'PUT' | 'PATCH';
data?: any;
params?: any;
headers?: any;
},
options?: T,
): Promise<R>;
}) {
this.request = options?.request || this.request;
this.baseURL = options?.baseURL || '';
}
/** POST /api/playground_api/upsert_prompt_resource */
UpsertPromptResource(
req: prompt_resource.UpsertPromptResourceRequest,
options?: T,
): Promise<prompt_resource.UpsertPromptResourceResponse> {
const _req = req;
const url = this.genBaseURL('/api/playground_api/upsert_prompt_resource');
const method = 'POST';
const data = { prompt: _req['prompt'] };
return this.request({ url, method, data }, options);
}
// ... 其他API方法
}
代码作用:PlaygroundApiService
类有成员函数 UpsertPromptResource 。
这个方法用于创建提示词。
此文件是基于playground_service.thrift自动生成的,开发者无需手动修改。
创建提示词--结构体实现(prompt_resource.thrift)
文件路径:frontend\packages\arch\idl\src\auto-generated\playground_api\namespaces\prompt_resource.ts
thrift
export interface PromptResource {
id?: string;
space_id?: string;
name?: string;
description?: string;
prompt_text?: string;
}
export interface ShowPromptResource {
id?: string;
}
export interface UpsertPromptResourceRequest {
prompt: PromptResource;
}
export interface UpsertPromptResourceResponse {
data?: ShowPromptResource;
code: Int64;
msg: string;
}
idl2ts-cli 工具
工具名称
@coze-arch/idl2ts-cli
详细地址
项目路径 :frontend/infra/idl/idl2ts-cli/
工具详细信息
版本:0.1.7
描述:IDL(Interface Definition Language)到TypeScript的转换工具
主要功能:
- gen命令:从Thrift或Protocol Buffer文件生成API代码
- filter命令:生成过滤后的API类型定义
可执行文件 :idl2ts
(位于 ./src/cli.js
)
最终调用的是frontend/infra/idl/idl2ts-cli/src/cli.ts
这个文件
核心依赖:
@coze-arch/idl2ts-generator
:代码生成器@coze-arch/idl2ts-helper
:辅助工具@coze-arch/idl2ts-plugin
:插件系统commander
:命令行界面prettier
:代码格式化
使用方式:
bash
# 生成API代码
idl2ts gen <projectRoot> [-f --format-config <formatConfig>]
# 生成过滤类型
idl2ts filter <projectRoot> [-f --format-config <formatConfig>]
许可证:Apache-2.0
作者:fanwenjie.fe@bytedance.com
这个工具是Coze Studio项目中统一处理所有IDL文件(包括playground_service.thrift
和passport.thrift
)的核心工具,确保了整个项目中API代码生成的一致性。
结语
Coze Studio的创建提示词功能是现代前端开发的优秀实践案例,它不仅展现了技术实现的专业性,更体现了对用户体验的深度思考。通过对其源码的深入分析,我们可以学习到:
- 架构设计的重要性:良好的架构是项目成功的基础
- 用户体验的价值:技术服务于用户,体验决定产品成败
- 工程化的必要性:规范的流程和工具是质量的保障
- 持续优化的意识:性能和安全需要持续关注和改进
- 团队协作的力量:标准化和文档化是团队效率的关键
这个功能的实现为我们提供了宝贵的学习资源,无论是技术架构、代码实现还是工程实践,都值得深入研究和借鉴。在未来的项目开发中,我们可以参考这些最佳实践,构建更加优秀的前端应用。与优化建议
架构优势
-
模块化设计:
- 组件职责清晰,易于维护和扩展
- 状态管理集中化,避免状态混乱
- API层抽象良好,便于测试和替换
-
类型安全:
- 全面的TypeScript类型定义
- 接口规范统一,减少运行时错误
- 编译时类型检查,提高代码质量
-
用户体验:
- 实时预览功能,所见即所得
- 语法高亮支持,提升编辑体验
- 表单验证完善,减少用户错误
-
性能优化:
- 防抖节流机制,优化用户交互
- 智能缓存策略,减少网络请求
- 懒加载支持,提升页面加载速度
可优化方向
-
代码分割:
typescript// 建议使用动态导入优化首屏加载 const PromptEditor = lazy(() => import('./PromptEditor'));
-
错误边界:
typescript// 添加错误边界组件 class PromptErrorBoundary extends Component { componentDidCatch(error: Error, errorInfo: ErrorInfo) { // 错误上报和处理 } }
-
国际化支持:
typescript// 支持多语言 const { t } = useTranslation('prompt');
-
可访问性增强:
typescript// 添加ARIA标签和键盘导航 <button aria-label={t('create-prompt')} />
技术栈总结
- 前端框架:React + TypeScript
- 状态管理:Zustand + Custom Hooks
- UI组件:自定义组件库
- 编辑器:CodeMirror扩展
- API通信:Axios + RESTful API
- 构建工具:Vite + ESBuild
- 代码质量:ESLint + Prettier + Husky
最佳实践与开发规范
组件开发规范
-
组件命名:
- 使用PascalCase命名组件文件
- 组件名称应该清晰表达其功能
- 避免使用缩写,保持名称的可读性
-
文件组织:
src/ ├── components/ │ ├── PromptEditor/ │ │ ├── index.tsx │ │ ├── PromptEditor.tsx │ │ ├── PromptEditor.module.css │ │ └── types.ts │ └── PromptLibrary/ ├── hooks/ │ ├── usePromptConfig.ts │ └── usePromptValidation.ts ├── utils/ │ ├── validation.ts │ └── performance.ts └── types/ └── prompt.ts
-
代码风格:
- 使用TypeScript严格模式
- 遵循ESLint和Prettier配置
- 保持函数单一职责原则
- 使用有意义的变量和函数名
状态管理最佳实践
-
Hook设计原则:
typescript// ✅ 好的Hook设计 const usePromptEditor = () => { const [content, setContent] = useState(''); const [isValid, setIsValid] = useState(false); const validateContent = useCallback((value: string) => { // 验证逻辑 }, []); return { content, setContent, isValid, validateContent, }; };
-
状态更新模式:
typescript// ✅ 使用函数式更新 setPrompts(prev => [...prev, newPrompt]); // ❌ 避免直接修改状态 prompts.push(newPrompt);
性能优化指南
-
组件优化:
typescript// 使用React.memo优化组件 const PromptItem = React.memo(({ prompt, onEdit }) => { return ( <div onClick={() => onEdit(prompt.id)}> {prompt.name} </div> ); });
-
事件处理优化:
typescript// 使用useCallback缓存事件处理函数 const handleEdit = useCallback((id: string) => { // 编辑逻辑 }, []);
错误处理策略
-
边界错误处理:
typescriptclass PromptErrorBoundary extends Component { state = { hasError: false }; static getDerivedStateFromError(error: Error) { return { hasError: true }; } componentDidCatch(error: Error, errorInfo: ErrorInfo) { console.error('Prompt component error:', error, errorInfo); } }
-
API错误处理:
typescriptconst handleApiError = (error: any) => { if (error.response?.status === 401) { // 处理认证错误 } else if (error.response?.status >= 500) { // 处理服务器错误 } else { // 处理其他错误 } };
工具函数
提示词处理工具
frontend/packages/common/prompt-kit/base/src/utils/prompt.ts
提供了完整的提示词处理功能:
typescript:d:\coze_829\coze-studio-main\frontend\packages\common\prompt-kit\base\src\utils\prompt.ts
// 提示词类型枚举
export enum PromptType {
SYSTEM = 'system',
USER = 'user',
ASSISTANT = 'assistant',
FUNCTION = 'function',
}
// 提示词变量解析
export const parsePromptVariables = (content: string): string[] => {
const variableRegex = /\{\{([^}]+)\}\}/g;
const variables: string[] = [];
let match;
while ((match = variableRegex.exec(content)) !== null) {
const variable = match[1].trim();
if (!variables.includes(variable)) {
variables.push(variable);
}
}
return variables;
};
// 提示词内容验证
export const validatePromptContent = (content: string): {
isValid: boolean;
errors: string[];
} => {
const errors: string[] = [];
if (!content.trim()) {
errors.push('提示词内容不能为空');
}
if (content.length > 10000) {
errors.push('提示词内容不能超过10000个字符');
}
// 检查变量格式
const invalidVariables = content.match(/\{[^}]*\}/g)?.filter(v => !v.match(/^\{\{[^}]+\}\}$/));
if (invalidVariables?.length) {
errors.push('变量格式错误,应使用 {{变量名}} 格式');
}
return {
isValid: errors.length === 0,
errors,
};
};
// 提示词预览渲染
export const renderPromptPreview = (content: string, variables: Record<string, string>): string => {
let rendered = content;
Object.entries(variables).forEach(([key, value]) => {
const regex = new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, 'g');
rendered = rendered.replace(regex, value || `{{${key}}}`);
});
return rendered;
};
设计亮点:
- 变量解析精确:准确识别和提取提示词中的变量
- 内容验证完善:多维度验证提示词内容的有效性
- 预览功能强大:支持实时预览变量替换效果
- 类型安全:完整的TypeScript类型定义
性能优化
1. 组件渲染优化
条件渲染:
typescript
// 根据状态条件渲染组件
{showPromptModal && (
<PromptConfiguratorModal
visible={showPromptModal}
mode={modalMode}
// ... props
/>
)}
{!!previewData && (
<PromptPreviewModal
visible={!!previewData}
data={previewData}
// ... props
/>
)}
状态最小化:
typescript
// 只在必要时更新状态
const openCreateModal = useMemoizedFn(() => {
setModalMode('create');
setEditData(undefined); // 清理编辑数据
setShowPromptModal(true);
});
2. 事件处理优化
函数缓存:
typescript
// 使用useMemoizedFn缓存事件处理函数
const handlePromptSave = useMemoizedFn(async (promptData: PromptData) => {
try {
await PlaygroundApi.UpsertPromptResource(promptData);
refreshPromptList();
} catch (error) {
console.error('保存提示词失败:', error);
}
});
防抖处理:
typescript
// 对提示词内容变化进行防抖处理
const debouncedValidation = useDebounce((content: string) => {
const { isValid, errors } = validatePromptContent(content);
setValidationResult({ isValid, errors });
}, 300);
3. 数据管理优化
useRequest状态管理:
typescript
const { loading, run: fetchPrompts } = useRequest(fetchPromptList, {
manual: true,
onSuccess: responseData => {
setPromptList(responseData?.data?.prompts || []);
},
});
状态同步:
typescript
// 操作成功后同步更新列表
const handleDelete = async (id: string) => {
await PlaygroundApi.DeletePromptResource({ id });
fetchPrompts(); // 重新获取提示词列表
};
用户体验设计
1. 即时反馈
操作状态反馈:
typescript
// 保存成功提示
Toast.success({
content: I18n.t('prompt_saved_successfully'),
showClose: false,
});
// 加载状态显示
<PromptTable loading={loading} dataSource={promptList || []} />
实时预览:
typescript
// 根据提示词内容实时更新预览
const previewContent = useMemo(() => {
return renderPromptPreview(content, variables);
}, [content, variables]);
<PromptPreview
content={previewContent}
className={classNames(styles['preview-panel'], {
[styles['preview-error']]: !isValid,
})}
/>
2. 交互优化
安全确认:
typescript
// 删除提示词需要确认
<Popconfirm
onConfirm={() => onDelete(`${record?.id}`)}
content={I18n.t('delete_prompt_confirm')}
title={I18n.t('delete_prompt_warning')}
>
<UIButton icon={<IconCozMinusCircle />} />
</Popconfirm>
智能提示:
typescript
// 变量输入提示
<Tooltip content={I18n.t('variable_format_hint')}>
<PromptEditor
value={content}
onChange={handleContentChange}
placeholder="请输入提示词内容,使用 {{变量名}} 定义变量"
/>
</Tooltip>
3. 空状态处理
引导式空状态:
typescript
<UIEmpty
title={I18n.t('no_prompts')}
description={I18n.t('no_prompts_description')}
action={
<Button onClick={() => openCreateModal()} theme="solid" type="primary">
{I18n.t('create_first_prompt')}
</Button>
}
/>
安全性设计
1. 内容安全
敏感信息过滤:
typescript
// 检测和过滤敏感信息
const filterSensitiveContent = (content: string): string => {
// 过滤可能的API密钥、密码等敏感信息
const sensitivePatterns = [
/api[_-]?key[\s]*[:=][\s]*['"]?[\w\-]{20,}['"]?/gi,
/password[\s]*[:=][\s]*['"]?[\w\-]{8,}['"]?/gi,
/token[\s]*[:=][\s]*['"]?[\w\-]{20,}['"]?/gi,
];
let filtered = content;
sensitivePatterns.forEach(pattern => {
filtered = filtered.replace(pattern, '[敏感信息已隐藏]');
});
return filtered;
};
内容审核:
typescript
// 提示词内容审核
const auditPromptContent = async (content: string): Promise<{
isApproved: boolean;
warnings: string[];
}> => {
const warnings: string[] = [];
// 检查是否包含不当内容
if (content.includes('恶意') || content.includes('攻击')) {
warnings.push('内容可能包含不当信息');
}
return {
isApproved: warnings.length === 0,
warnings,
};
};
2. 操作权限
权限验证:
typescript
// 基于用户权限控制操作
const canEditPrompt = (prompt: PromptResource, user: User): boolean => {
return prompt.creator_id === user.id || user.role === 'admin';
};
<UIButton
disabled={!canEditPrompt(prompt, currentUser)}
onClick={() => onEdit(prompt)}
/>
资源访问控制:
typescript
// 检查用户是否有权限访问特定提示词
const checkPromptAccess = (promptId: string, userId: string): boolean => {
// 实现权限检查逻辑
return hasPermission(userId, 'prompt:read', promptId);
};
3. 输入验证
表单验证:
typescript
<Form.Input
field="name"
maxLength={100}
rules={[
{ required: true, message: '提示词名称不能为空' },
{ pattern: /^[\u4e00-\u9fa5a-zA-Z0-9_\-\s]+$/, message: '名称包含非法字符' }
]}
/>
内容长度限制:
typescript
// 限制提示词内容长度
const validateContentLength = (content: string): boolean => {
const maxLength = 10000; // 最大10000字符
return content.length <= maxLength;
};
国际化支持
1. 文本国际化
统一文本管理:
typescript
// 所有用户可见文本都通过I18n管理
<h3>{I18n.t('prompt_library')}</h3>
<Button>{I18n.t('create_prompt')}</Button>
<p>{I18n.t('prompt_creation_guide')}</p>
动态文本生成:
typescript
// 支持参数化的国际化文本
label: I18n.t('prompt_variables_count', {
count: variables.length,
variables: variables.join(', '),
})
2. 内容格式化
本地化内容显示:
typescript
// 根据用户语言环境格式化提示词内容
export const formatPromptContent = (content: string, locale: string) => {
// 根据语言环境调整内容显示
if (locale === 'zh-CN') {
return content.replace(/\n/g, '\n\n'); // 中文增加段落间距
}
return content;
};
// 格式化创建时间
export const formatCreatedTime = (timestamp: number, locale: string) => {
return dayjs.unix(timestamp).locale(locale).format('YYYY-MM-DD HH:mm:ss');
};
架构设计最佳实践
1. 模块化设计
组件职责分离:
- LibraryHeader:负责顶部操作区域和创建按钮
- PromptTable:负责提示词列表展示
- PromptConfiguratorModal:负责提示词创建和编辑
- PromptPreviewModal:负责提示词预览展示
Hook职责分离:
- usePromptConfig:提示词操作状态管理
- usePromptList/useCreatePrompt等:API调用
- 提示词工具函数:业务逻辑处理
2. 状态管理策略
集中式状态管理:
typescript
// 通过usePromptConfig集中管理操作状态
const {
showPromptModal,
modalMode,
editData,
openCreateModal,
openEditModal,
onCancel,
onSaveSuccess,
refresh,
} = usePromptConfig();
数据状态分离:
typescript
// API数据通过SWR独立管理
const { data: promptList, loading, mutate } = usePromptList();
3. 类型安全
完整的TypeScript支持:
typescript
// 接口类型定义
interface PromptTableProps {
loading: boolean;
dataSource: PromptResource[];
onEdit?: (data?: PromptResource) => void;
onDelete: (id: string) => void;
onPreview: (data: PromptResource) => void;
}
// API响应类型
type UpsertPromptResourceResponseData = {
prompt_resource: PromptResource;
id: string;
};