720 lines
25 KiB
TypeScript
720 lines
25 KiB
TypeScript
import { useEffect, useState } from "react";
|
||
import { MetaFunction, LoaderFunctionArgs, ActionFunctionArgs, json, redirect } from "@remix-run/node";
|
||
import { Link, useLoaderData, useNavigation, useActionData, Form } from "@remix-run/react";
|
||
import { Button } from "~/components/ui/Button";
|
||
import newStyles from "~/styles/pages/prompts_new.css?url";
|
||
import { getPromptTemplate, createPromptTemplate, updatePromptTemplate, type PromptTemplateUI } from "~/api/prompts/prompts";
|
||
|
||
// 样式链接
|
||
export function links() {
|
||
return [{ rel: "stylesheet", href: newStyles }];
|
||
}
|
||
|
||
// 页面元数据
|
||
export const meta: MetaFunction = () => {
|
||
return [
|
||
{ title: "提示词模板编辑 - 中国烟草AI合同及卷宗审核系统" },
|
||
{ name: "description", content: "创建或编辑提示词模板" },
|
||
];
|
||
};
|
||
|
||
// 面包屑导航
|
||
export const handle = {
|
||
breadcrumb: (data:LoaderData) => {
|
||
if (data.mode === "edit") {
|
||
return "编辑提示词模板";
|
||
} else if (data.mode === "view") {
|
||
return "查看提示词模板";
|
||
} else {
|
||
return "新增提示词模板";
|
||
}
|
||
}
|
||
};
|
||
|
||
interface LoaderData {
|
||
template: PromptTemplateUI | null;
|
||
mode: string;
|
||
error?: string;
|
||
}
|
||
|
||
// 定义本地表单数据接口
|
||
interface FormDataState extends Omit<PromptTemplateUI, 'variables'> {
|
||
variables: string; // 在表单状态中我们保存变量为 JSON 字符串
|
||
}
|
||
|
||
interface ActionData {
|
||
success?: boolean;
|
||
errors?: {
|
||
template_name?: string;
|
||
template_type?: string;
|
||
template_content?: string;
|
||
general?: string;
|
||
};
|
||
formData?: {
|
||
template_name: string;
|
||
template_type: 'LLM_Extraction' | 'VLM_Extraction' | 'Evaluation' | 'Summary' | 'Common';
|
||
description: string;
|
||
template_content: string;
|
||
variables: string;
|
||
status: "active" | "inactive" | "system";
|
||
version: string;
|
||
};
|
||
}
|
||
|
||
// 加载函数
|
||
export async function loader({ request }: LoaderFunctionArgs) {
|
||
try {
|
||
const url = new URL(request.url);
|
||
const id = url.searchParams.get("id");
|
||
const mode = url.searchParams.get("mode") || "create";
|
||
|
||
// 模板数据,如果是新建则为空
|
||
let template = null;
|
||
|
||
if (id) {
|
||
// 从API获取数据
|
||
const result = await getPromptTemplate(id);
|
||
|
||
if (result.error) {
|
||
console.error('获取提示词模板失败:', result.error);
|
||
throw new Error(result.error);
|
||
}
|
||
|
||
template = result.data || null;
|
||
if (!template) {
|
||
throw new Error(`未找到ID为${id}的模板`);
|
||
}
|
||
}
|
||
|
||
return json<LoaderData>({
|
||
template,
|
||
mode
|
||
});
|
||
} catch (error) {
|
||
console.error("加载提示词模板失败:", error);
|
||
return json<LoaderData>(
|
||
{
|
||
template: null,
|
||
mode: "create",
|
||
error: error instanceof Error ? error.message : "加载提示词模板失败"
|
||
},
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
}
|
||
|
||
// Action函数 - 处理表单提交
|
||
export async function action({ request }: ActionFunctionArgs) {
|
||
const formData = await request.formData();
|
||
const id = formData.get("id") as string;
|
||
const template_name = formData.get("template_name") as string;
|
||
const template_type = formData.get("template_type") as 'LLM_Extraction' | 'VLM_Extraction' | 'Evaluation' | 'Summary' | 'Common';
|
||
const description = formData.get("description") as string;
|
||
const template_content = formData.get("template_content") as string;
|
||
const variables = formData.get("variables") as string;
|
||
const status = formData.get("status") as string;
|
||
const version = formData.get("version") as string;
|
||
|
||
const errors: ActionData["errors"] = {};
|
||
|
||
// 表单验证
|
||
if (!template_name || template_name.trim() === "") {
|
||
errors.template_name = "模板名称不能为空";
|
||
}
|
||
|
||
if (!template_type) {
|
||
errors.template_type = "请选择模板类型";
|
||
}
|
||
|
||
if (!template_content || template_content.trim() === "") {
|
||
errors.template_content = "模板内容不能为空";
|
||
}
|
||
|
||
if (Object.keys(errors).length > 0) {
|
||
return json({ errors });
|
||
}
|
||
|
||
try {
|
||
// 准备变量数据
|
||
let variablesData: Record<string, string> = {};
|
||
try {
|
||
if (variables) {
|
||
variablesData = JSON.parse(variables);
|
||
}
|
||
} catch (e) {
|
||
console.error('解析变量JSON失败:', e);
|
||
}
|
||
|
||
// 准备API数据
|
||
const apiTemplate: Partial<PromptTemplateUI> = {
|
||
template_name,
|
||
template_type,
|
||
description,
|
||
template_content,
|
||
variables: variablesData,
|
||
status: status === "active" ? "active" : "inactive",
|
||
version: version || "v1.0"
|
||
};
|
||
|
||
let result;
|
||
if (id) {
|
||
// 更新模板
|
||
result = await updatePromptTemplate(id, apiTemplate);
|
||
} else {
|
||
// 创建模板
|
||
result = await createPromptTemplate(apiTemplate);
|
||
}
|
||
|
||
if (result.error) {
|
||
return json({
|
||
errors: { general: result.error },
|
||
formData: {
|
||
template_name,
|
||
template_type,
|
||
description,
|
||
template_content,
|
||
variables,
|
||
status,
|
||
version
|
||
}
|
||
});
|
||
}
|
||
|
||
return redirect("/prompts");
|
||
} catch (error) {
|
||
console.error("保存提示词模板失败:", error);
|
||
return json({
|
||
errors: {
|
||
general: error instanceof Error ? error.message : "保存提示词模板失败"
|
||
},
|
||
formData: {
|
||
template_name,
|
||
template_type,
|
||
description,
|
||
template_content,
|
||
variables,
|
||
status,
|
||
version
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// 提取变量函数 例如:{var1} {var2} {var3}
|
||
const extractVariables = (content: string) => {
|
||
const regex = /{([^{}]+)}/g;
|
||
const variables: Record<string, string> = {};
|
||
let match;
|
||
|
||
while ((match = regex.exec(content)) !== null) {
|
||
const varName = match[1].trim();
|
||
if (varName && !variables[varName]) {
|
||
variables[varName] = varName;
|
||
}
|
||
}
|
||
|
||
return variables;
|
||
};
|
||
|
||
// 页面组件
|
||
export default function PromptsNew() {
|
||
const { template, mode, error } = useLoaderData<typeof loader>();
|
||
const actionData = useActionData<ActionData>();
|
||
const navigation = useNavigation();
|
||
const isSubmitting = navigation.state === "submitting";
|
||
|
||
// 表单状态
|
||
const [formData, setFormData] = useState<FormDataState>({
|
||
id: "",
|
||
template_name: "",
|
||
template_type: "Common",
|
||
description: "",
|
||
version: "v1.0",
|
||
status: "active",
|
||
template_content: "",
|
||
created_by: 1,
|
||
variables: "{}",
|
||
created_at: "",
|
||
updated_at: ""
|
||
});
|
||
|
||
// 模式状态
|
||
const [isViewMode, setIsViewMode] = useState(false);
|
||
const [pageTitle, setPageTitle] = useState("新增提示词模板");
|
||
|
||
// 变量相关状态
|
||
// 检测到的变量
|
||
const [detectedVariables, setDetectedVariables] = useState<Record<string, string>>({});
|
||
// 示例值
|
||
const [exampleValues, setExampleValues] = useState<Record<string, string>>({});
|
||
// 预览内容
|
||
const [previewContent, setPreviewContent] = useState("");
|
||
|
||
// 初始化表单数据
|
||
useEffect(() => {
|
||
if (actionData?.formData) {
|
||
// 如果有保存失败的表单数据,使用它
|
||
setFormData(prev => ({
|
||
...prev,
|
||
...actionData.formData,
|
||
variables: actionData.formData?.variables || "{}",
|
||
status: actionData.formData?.status || "active"
|
||
}));
|
||
} else if (template) {
|
||
// 否则使用模板数据
|
||
const variablesJson = typeof template.variables === 'string'
|
||
? template.variables
|
||
: JSON.stringify(template.variables);
|
||
|
||
const newFormData = {
|
||
...template,
|
||
id: mode === "clone" ? "" : template.id,
|
||
template_name: mode === "clone" ? `${template.template_name} (副本)` : template.template_name,
|
||
version: mode === "clone" ? "v1.0" : template.version,
|
||
variables: variablesJson
|
||
};
|
||
|
||
setFormData(newFormData);
|
||
|
||
try {
|
||
const vars = typeof template.variables === 'string'
|
||
? JSON.parse(template.variables)
|
||
: template.variables;
|
||
|
||
setExampleValues(vars);
|
||
} catch (e) {
|
||
console.error("解析变量失败:", e);
|
||
}
|
||
|
||
if (template.template_content) {
|
||
const vars = extractVariables(template.template_content);
|
||
setDetectedVariables(vars);
|
||
}
|
||
}
|
||
|
||
setIsViewMode(mode === "view");
|
||
|
||
if (mode === "view") {
|
||
setPageTitle("查看提示词模板");
|
||
} else if (mode === "edit") {
|
||
setPageTitle("编辑提示词模板");
|
||
} else if (mode === "clone") {
|
||
setPageTitle("复制创建提示词模板");
|
||
} else {
|
||
setPageTitle("新增提示词模板");
|
||
}
|
||
}, [template, mode, actionData?.formData]);
|
||
|
||
// 处理输入变化
|
||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||
const { name, value } = e.target;
|
||
|
||
// 如果是模板内容,检测变量
|
||
if (name === "template_content") {
|
||
const vars = extractVariables(value);
|
||
// console.log("检测到的变量:",vars);
|
||
setDetectedVariables(vars);
|
||
|
||
// 更新变量JSON
|
||
const varsJson = JSON.stringify(
|
||
Object.keys(vars).reduce((acc, key) => {
|
||
acc[key] = exampleValues[key] || "";
|
||
return acc;
|
||
}, {} as Record<string, string>)
|
||
);
|
||
// console.log("更新变量JSON:",varsJson);
|
||
|
||
setFormData(prev => ({
|
||
...prev,
|
||
[name]: value,
|
||
variables: varsJson
|
||
}));
|
||
} else {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
[name]: value
|
||
}));
|
||
}
|
||
};
|
||
|
||
// 处理状态切换
|
||
const handleStatusToggle = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
// console.log("状态切换前:", formData.status);
|
||
const status = e.target.checked ? "active" : "inactive";
|
||
// console.log("新状态值:", status);
|
||
|
||
// 直接更新formData状态
|
||
setFormData(prev => {
|
||
const newState = {
|
||
...prev,
|
||
status: status as "active" | "inactive" | "system"
|
||
};
|
||
// console.log("状态更新后:", newState.status);
|
||
return newState;
|
||
});
|
||
};
|
||
|
||
// 处理示例值变更
|
||
const handleExampleValueChange = (varName: string, value: string) => {
|
||
setExampleValues(prev => ({
|
||
...prev,
|
||
[varName]: value
|
||
}));
|
||
};
|
||
|
||
// 更新预览
|
||
const updatePreview = () => {
|
||
let content = formData.template_content || "";
|
||
|
||
// 替换变量
|
||
Object.entries(detectedVariables).forEach(([key]) => {
|
||
const exampleValue = exampleValues[key] || ``;
|
||
const regex = new RegExp(`{${key}}`, 'g');
|
||
content = content.replace(regex, exampleValue);
|
||
});
|
||
|
||
setPreviewContent(content);
|
||
};
|
||
|
||
// 当检测到的变量变化时,更新变量JSON
|
||
useEffect(() => {
|
||
const varsJson = JSON.stringify(
|
||
Object.keys(detectedVariables).reduce((acc, key) => {
|
||
acc[key] = exampleValues[key] || "";
|
||
return acc;
|
||
}, {} as Record<string, string>)
|
||
);
|
||
|
||
setFormData(prev => ({
|
||
...prev,
|
||
variables: varsJson
|
||
}));
|
||
}, [detectedVariables, exampleValues]);
|
||
|
||
return (
|
||
<div className="prompt-new-page">
|
||
{/* 页面头部 */}
|
||
<div className="flex justify-between items-center mb-4">
|
||
<h2 className="text-xl font-medium">{pageTitle}</h2>
|
||
<div>
|
||
<Link to="/prompts" className="mr-2">
|
||
<Button type="default" icon="ri-arrow-left-line">
|
||
返回列表
|
||
</Button>
|
||
</Link>
|
||
{!isViewMode && (
|
||
<Button
|
||
type="primary"
|
||
icon="ri-save-line"
|
||
disabled={isSubmitting}
|
||
form="template-form"
|
||
>
|
||
{isSubmitting ? "保存中..." : "保存"}
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 错误信息 */}
|
||
{error && (
|
||
<div className="alert alert-error mb-4">
|
||
<i className="ri-error-warning-line"></i>
|
||
<div>{error}</div>
|
||
</div>
|
||
)}
|
||
|
||
{actionData?.errors?.general && (
|
||
<div className="alert alert-error mb-4">
|
||
<i className="ri-error-warning-line"></i>
|
||
<div>{actionData.errors.general}</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 查看模式提示 */}
|
||
{isViewMode && (
|
||
<div className="alert alert-info">
|
||
<i className="ri-information-line"></i>
|
||
<div>
|
||
<div>您正在查看系统预设模板,此模板不可修改。如需基于此模板创建新模板,请点击"复制创建"按钮。</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 模板表单 */}
|
||
<Form id="template-form" method="post">
|
||
{/* 模板ID - 隐藏字段 */}
|
||
<input type="hidden" name="id" value={formData.id || ''} />
|
||
|
||
{/* 模板信息卡片 */}
|
||
<div className="ant-card">
|
||
<div className="ant-card-header">提示词模板信息</div>
|
||
<div className="ant-card-body p-4">
|
||
<div className="grid grid-cols-2 gap-4">
|
||
{/* 模板名称 */}
|
||
<div className="form-group mb-3">
|
||
<label htmlFor="template-name" className="form-label mb-1">
|
||
模板名称 <span className="text-error">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
className={`form-input py-1 ${isViewMode ? 'read-only-field' : ''} ${actionData?.errors?.template_name ? 'input-error' : ''}`}
|
||
id="template-name"
|
||
name="template_name"
|
||
placeholder="请输入模板名称"
|
||
value={formData.template_name || ''}
|
||
onChange={handleInputChange}
|
||
readOnly={isViewMode}
|
||
required
|
||
/>
|
||
{actionData?.errors?.template_name && (
|
||
<div className="error-message">{actionData.errors.template_name}</div>
|
||
)}
|
||
<div className="help-text text-xs">建议使用"文档类型-功能"的命名方式</div>
|
||
</div>
|
||
|
||
{/* 模板类型 */}
|
||
<div className="form-group mb-3">
|
||
<label htmlFor="template-type" className="form-label mb-1">
|
||
模板类型 <span className="text-error">*</span>
|
||
</label>
|
||
<select
|
||
className={`form-select py-1 ${isViewMode ? 'read-only-field' : ''} ${actionData?.errors?.template_type ? 'input-error' : ''}`}
|
||
id="template-type"
|
||
name="template_type"
|
||
value={formData.template_type || ''}
|
||
onChange={handleInputChange}
|
||
disabled={isViewMode}
|
||
required
|
||
>
|
||
<option value="">请选择模板类型</option>
|
||
<option value="Common">通用(Common) - 适用于多种场景的通用提示词</option>
|
||
<option value="LLM_Extraction">LLM抽取(LLM_Extraction) - 使用LLM从文档中抽取结构化信息</option>
|
||
<option value="VLM_Extraction">VLM抽取(VLM_Extraction) - 使用VLM从文档中抽取结构化信息</option>
|
||
<option value="Evaluation">评估(Evaluation) - 对文档内容进行评估</option>
|
||
<option value="Summary">摘要(Summary) - 生成文档内容摘要</option>
|
||
</select>
|
||
{actionData?.errors?.template_type && (
|
||
<div className="error-message">{actionData.errors.template_type}</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 模板描述 */}
|
||
<div className="form-group mb-3">
|
||
<label htmlFor="template-description" className="form-label mb-1">
|
||
模板描述
|
||
</label>
|
||
<textarea
|
||
className={`form-textarea py-1 ${isViewMode ? 'read-only-field' : ''}`}
|
||
id="template-description"
|
||
name="description"
|
||
placeholder="请简要描述此模板的功能和用途"
|
||
value={formData.description || ''}
|
||
onChange={handleInputChange}
|
||
readOnly={isViewMode}
|
||
rows={2}
|
||
></textarea>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
{/* 模板状态 */}
|
||
<div className="form-group mb-3">
|
||
<label htmlFor="status-toggle" className="form-label mb-1">模板状态</label>
|
||
<div className="flex items-center mt-1">
|
||
<label className="switch mr-2" htmlFor="status-toggle">
|
||
<span className="sr-only">切换模板状态</span>
|
||
<input
|
||
type="checkbox"
|
||
id="status-toggle"
|
||
checked={formData.status === 'active'}
|
||
onChange={handleStatusToggle}
|
||
disabled={isViewMode}
|
||
/>
|
||
{/* 移除name属性,只使用隐藏字段传递状态值 */}
|
||
<span className="slider"></span>
|
||
</label>
|
||
<span id="status-text">{formData.status === 'active' ? '启用' : '停用'}</span>
|
||
|
||
</div>
|
||
{/* 使用隐藏字段传递状态值 */}
|
||
<input
|
||
type="hidden"
|
||
name="status"
|
||
value={formData.status}
|
||
/>
|
||
</div>
|
||
|
||
{/* 模板版本 */}
|
||
<div className="form-group mb-3">
|
||
<label htmlFor="template-version" className="form-label mb-1">模板版本</label>
|
||
<input
|
||
type="text"
|
||
className={`form-input py-1 ${isViewMode ? 'read-only-field' : ''}`}
|
||
id="template-version"
|
||
name="version"
|
||
placeholder="例如:v1.0"
|
||
value={formData.version || 'v1.0'}
|
||
onChange={handleInputChange}
|
||
readOnly={isViewMode}
|
||
required
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 模板内容卡片 */}
|
||
<div className="ant-card">
|
||
<div className="ant-card-header">提示词模板内容</div>
|
||
<div className="ant-card-body">
|
||
<div className="alert alert-warning mb-4">
|
||
<i className="ri-information-line"></i>
|
||
<div>
|
||
<div>模板内容支持使用变量,变量格式为 {varName},在使用时会自动替换。系统将自动识别模板中的变量。</div>
|
||
<div className="mt-1">例如:请从以下{docType}文档中抽取关键信息...</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 模板内容 */}
|
||
<div className="form-group">
|
||
<label htmlFor="template-content" className="form-label">
|
||
模板内容 <span className="text-error">*</span>
|
||
</label>
|
||
<textarea
|
||
className={`form-code-editor w-full ${isViewMode ? 'read-only-field' : ''} ${actionData?.errors?.template_content ? 'input-error' : ''}`}
|
||
id="template-content"
|
||
name="template_content"
|
||
placeholder="在此输入提示词模板内容..."
|
||
value={formData.template_content || ''}
|
||
onChange={handleInputChange}
|
||
readOnly={isViewMode}
|
||
rows={15}
|
||
required
|
||
></textarea>
|
||
{actionData?.errors?.template_content && (
|
||
<div className="error-message">{actionData.errors.template_content}</div>
|
||
)}
|
||
<div className="help-text">提示词模板是AI完成特定任务的指令,请清晰描述任务需求和输出格式</div>
|
||
</div>
|
||
|
||
{/* 变量识别区域 */}
|
||
<div className="form-group mt-6">
|
||
<label htmlFor="var-container" className="form-label">已识别的变量</label>
|
||
<div className="alert alert-info mb-3">
|
||
<i className="ri-lightbulb-line"></i>
|
||
<div>
|
||
<div>系统已自动识别出模板中的变量。变量以 {varName} 形式在模板中使用,无需手动定义。</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="var-container" className="var-container" aria-labelledby="var-container-label">
|
||
{Object.keys(detectedVariables).length > 0 ? (
|
||
Object.keys(detectedVariables).map(varName => (
|
||
<div
|
||
className="var-badge"
|
||
key={varName}
|
||
data-var-name={varName}
|
||
>
|
||
{varName}
|
||
</div>
|
||
))
|
||
) : (
|
||
<div className="text-secondary text-sm italic" id="no-vars-message">
|
||
暂未识别到任何变量,请在模板内容中使用 {变量名} 格式添加变量
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<input type="hidden" id="variables-json" name="variables" value={formData.variables || '{}'} />
|
||
</div>
|
||
|
||
{/* 模板效果预览 */}
|
||
<div className="form-group mt-6">
|
||
<label htmlFor="preview-content" className="form-label">模板效果预览</label>
|
||
<div className="example-section">
|
||
<div className="example-header">变量赋值示例:</div>
|
||
<div id="example-vars" className="mb-3">
|
||
{Object.keys(detectedVariables).length > 0 ? (
|
||
Object.keys(detectedVariables).map(varName => (
|
||
<div className="var-input-group" key={varName}>
|
||
<input
|
||
type="text"
|
||
className="form-input"
|
||
value={varName}
|
||
readOnly
|
||
|
||
/>
|
||
<input
|
||
type="text"
|
||
className={`form-input ${isViewMode ? 'read-only-field' : ''}`}
|
||
id={`example-${varName}`}
|
||
placeholder={`示例值,如 ${varName}`}
|
||
value={exampleValues[varName] || ''}
|
||
onChange={(e) => handleExampleValueChange(varName, e.target.value)}
|
||
readOnly={isViewMode}
|
||
/>
|
||
</div>
|
||
))
|
||
) : (
|
||
<div className="text-secondary text-sm italic">
|
||
暂无变量,无需赋值
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="example-header">预览效果:</div>
|
||
<div id="preview-content" className="bg-gray-50 p-4 rounded border border-gray-200">
|
||
{previewContent ? (
|
||
<pre style={{ whiteSpace: 'pre-wrap' }}>{previewContent}</pre>
|
||
) : (
|
||
<div className="text-gray-400 italic">根据变量值生成的模板预览将显示在这里...</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="flex justify-end mt-3">
|
||
<button
|
||
type="button"
|
||
className="ant-btn ant-btn-default"
|
||
onClick={updatePreview}
|
||
>
|
||
<i className="ri-eye-line"></i> 更新预览
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 底部按钮区域 */}
|
||
<div className="flex justify-between mt-6">
|
||
<div>
|
||
{isViewMode && (
|
||
<Link to={`/prompts/new?id=${formData.id}&mode=clone`}>
|
||
<button type="button" className="ant-btn ant-btn-default">
|
||
<i className="ri-file-copy-line"></i> 复制创建
|
||
</button>
|
||
</Link>
|
||
)}
|
||
</div>
|
||
<div>
|
||
<Link to="/prompts" className="mr-2">
|
||
<button type="button" className="ant-btn ant-btn-default">
|
||
<i className="ri-close-line"></i> 取消
|
||
</button>
|
||
</Link>
|
||
{!isViewMode && (
|
||
<button
|
||
form="template-form"
|
||
className="ant-btn ant-btn-primary"
|
||
disabled={isSubmitting}
|
||
id="save-btn-bottom"
|
||
>
|
||
<i className="ri-save-line"></i> {isSubmitting ? "保存中..." : "保存"}
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</Form>
|
||
</div>
|
||
);
|
||
}
|