新增提示词列表和提示词修改页面

This commit is contained in:
2025-03-28 20:56:13 +08:00
parent afadd79fe8
commit 65da73071d
20 changed files with 2217 additions and 479 deletions
+711
View File
@@ -0,0 +1,711 @@
import { useEffect, useState } from "react";
import { MetaFunction, LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/node";
import { Link, useLoaderData, useSubmit, useNavigation } from "@remix-run/react";
import { Button } from "~/components/ui/Button";
import { PromptTemplate } from "./prompts._index";
import newStyles from "~/styles/pages/prompts_new.css?url";
// 样式链接
export function links() {
return [{ rel: "stylesheet", href: newStyles }];
}
// 页面元数据
export const meta: MetaFunction = () => {
return [
{ title: "提示词模板编辑 - 中国烟草AI合同及卷宗审核系统" },
{ name: "description", content: "创建或编辑提示词模板" },
];
};
// 面包屑导航
export const handle = {
breadcrumb: "编辑提示词模板"
};
// 从模拟数据中获取模板
const getTemplateById = (id: string): PromptTemplate | undefined => {
// 与prompts._index.tsx中的模拟数据保持一致
const MOCK_TEMPLATES: PromptTemplate[] = [
{
id: "1",
template_name: "行政处罚-抽取通用模板",
template_type: "Extraction",
description: "本模板用于抽取行政处罚决定书编号等信息",
version: "v1.0",
status: "system",
created_by: "system",
template_content: `你是一个专业的文档信息抽取助手。请从以下{docType}文档中抽取关键信息:
1. 处罚决定书编号
2. 处罚对象名称
3. 处罚事由
4. 处罚依据
5. 处罚内容
6. 处罚金额
7. 发文日期
请将结果以JSON格式输出,包含以上字段。如果某个字段在文档中未找到,则该字段的值设为null。`,
variables: JSON.stringify({ "docType": "文档类型" })
},
{
id: "2",
template_name: "销售合同-甲方信息评估",
template_type: "Evaluation",
description: "评估销售合同中甲方信息是否完整",
version: "v1.2",
status: "active",
created_by: "admin",
template_content: `你是一个专业的合同审核助手。请评估以下{docType}中甲方信息的完整性:
请检查以下要素是否存在且完整:
1. 甲方全称
2. 注册地址
3. 统一社会信用代码
4. 法定代表人
5. 联系方式
请给出评估结果,并标明缺失或不完整的信息。`,
variables: JSON.stringify({ "docType": "文档类型" })
},
{
id: "3",
template_name: "专卖许可证-摘要模板",
template_type: "Summary",
description: "生成专卖许可证申请文件的内容摘要",
version: "v1.0",
status: "active",
created_by: "admin",
template_content: `你是一个专业的文档摘要助手。请为以下{docType}生成一份简洁的摘要:
摘要应包含以下要点:
1. 申请人基本信息
2. 许可证类型
3. 申请事项
4. 经营范围
5. 申请日期
请控制摘要在200字以内,保留关键信息。`,
variables: JSON.stringify({ "docType": "文档类型" })
},
{
id: "4",
template_name: "采购合同-乙方资质抽取",
template_type: "Extraction",
description: "抽取采购合同中乙方的资质信息",
version: "v1.1",
status: "inactive",
created_by: "zhangsan",
template_content: `你是一个专业的合同信息抽取助手。请从以下{docType}中抽取乙方的资质信息:
需要抽取的信息包括:
1. 乙方全称
2. 资质证书类型
3. 资质证书编号
4. 资质等级
5. 证书有效期
请将结果以JSON格式输出,包含以上字段。`,
variables: JSON.stringify({ "docType": "文档类型" })
},
{
id: "5",
template_name: "合同通用-关键条款评估",
template_type: "Evaluation",
description: "评估合同中关键条款是否明确、合规",
version: "v2.0",
status: "active",
created_by: "lisi",
template_content: `你是一个专业的{industry}行业合同审核助手。请评估以下合同中的关键条款是否明确、合规:
请重点关注以下条款:
1. 合同标的
2. 价格条款
3. 付款条件
4. 交付方式
5. 违约责任
6. 争议解决
请对每一项给出评估结果,并指出不明确或存在风险的条款。`,
variables: JSON.stringify({ "industry": "行业类型", "docType": "文档类型" })
}
];
return MOCK_TEMPLATES.find(t => t.id === id);
};
// 加载函数
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 response = await fetch(`${process.env.API_BASE_URL}/api/prompt-templates/${id}`);
// if (!response.ok) throw new Error(`获取提示词模板失败: ${response.status}`);
// template = await response.json();
// 使用模拟数据
template = getTemplateById(id);
if (!template) {
throw new Error(`未找到ID为${id}的模板`);
}
}
return Response.json({
template,
mode
});
} catch (error) {
console.error("加载提示词模板失败:", error);
return Response.json(
{
template: null,
mode: "create",
error: error instanceof Error ? error.message : "加载提示词模板失败"
},
{ status: 500 }
);
}
}
// Action函数 - 处理表单提交
export async function action({ request }: ActionFunctionArgs) {
try {
const formData = await request.formData();
const templateData = Object.fromEntries(formData);
// 表单验证
const errors: Record<string, string> = {};
if (!templateData.template_name) {
errors.template_name = "模板名称不能为空";
}
if (!templateData.template_type) {
errors.template_type = "请选择模板类型";
}
if (!templateData.template_content) {
errors.template_content = "模板内容不能为空";
}
if (Object.keys(errors).length > 0) {
return Response.json({ errors, success: false }, { status: 400 });
}
// 实际应用中,这里应该调用API保存数据
// const apiUrl = templateData.id
// ? `${process.env.API_BASE_URL}/api/prompt-templates/${templateData.id}`
// : `${process.env.API_BASE_URL}/api/prompt-templates`;
//
// const response = await fetch(apiUrl, {
// method: templateData.id ? "PUT" : "POST",
// headers: { "Content-Type": "application/json" },
// body: JSON.stringify(templateData)
// });
//
// if (!response.ok) throw new Error(`保存提示词模板失败: ${response.status}`);
// const result = await response.json();
// 模拟API响应
console.log("提交的模板数据:", templateData);
return Response.json({
success: true,
message: "提示词模板保存成功",
template: {
...templateData,
id: templateData.id || Math.random().toString(36).substring(2, 10),
created_by: "当前用户",
created_at: new Date().toISOString()
}
});
} catch (error) {
console.error("保存提示词模板失败:", error);
return Response.json(
{
success: false,
message: error instanceof Error ? error.message : "保存提示词模板失败"
},
{ status: 500 }
);
}
}
// 提取变量函数
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 } = useLoaderData<typeof loader>();
const submit = useSubmit();
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
// 表单状态
const [formData, setFormData] = useState<Partial<PromptTemplate>>({
id: "",
template_name: "",
template_type: "Common",
description: "",
version: "v1.0",
status: "active",
template_content: "",
variables: "{}"
});
// 模式状态
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 (template) {
const newFormData = {
...template,
// 如果是克隆模式,则清除ID并修改名称
id: mode === "clone" ? "" : template.id,
template_name: mode === "clone" ? `${template.template_name} (副本)` : template.template_name,
// 如果是克隆模式,重置版本
version: mode === "clone" ? "v1.0" : template.version
};
setFormData(newFormData);
try {
// 解析模板变量
const vars = JSON.parse(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]);
// 处理输入变化
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target;
// 如果是模板内容,检测变量
if (name === "template_content") {
const vars = extractVariables(value);
setDetectedVariables(vars);
// 更新变量JSON
const varsJson = JSON.stringify(
Object.keys(vars).reduce((acc, key) => {
acc[key] = exampleValues[key] || key;
return acc;
}, {} as Record<string, string>)
);
setFormData(prev => ({
...prev,
[name]: value,
variables: varsJson
}));
} else {
setFormData(prev => ({
...prev,
[name]: value
}));
}
};
// 处理状态切换
const handleStatusToggle = (e: React.ChangeEvent<HTMLInputElement>) => {
const status = e.target.checked ? "active" : "inactive";
setFormData(prev => ({
...prev,
status
}));
};
// 处理示例值变更
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] || `[${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] || key;
return acc;
}, {} as Record<string, string>)
);
setFormData(prev => ({
...prev,
variables: varsJson
}));
}, [detectedVariables, exampleValues]);
// 处理表单提交
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (isViewMode) {
return;
}
const formElement = e.target as HTMLFormElement;
const submittingFormData = new FormData(formElement);
// 确保变量JSON被包含在提交中
submittingFormData.set("variables", formData.variables || "{}");
submit(submittingFormData, { method: "post" });
};
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}
onClick={() => document.getElementById("template-form")?.dispatchEvent(new Event("submit", { cancelable: true, bubbles: true }))}
>
{isSubmitting ? "保存中..." : "保存"}
</Button>
) : (
<Link to="/prompts">
<Button type="default" icon="ri-arrow-left-line">
</Button>
</Link>
)}
</div>
</div>
{/* 查看模式提示 */}
{isViewMode && (
<div className="alert alert-info">
<i className="ri-information-line"></i>
<div>
<div>&quot;&quot;</div>
</div>
</div>
)}
{/* 模板表单 */}
<form id="template-form" method="post" onSubmit={handleSubmit}>
{/* 模板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' : ''}`}
id="template-name"
name="template_name"
placeholder="请输入模板名称"
value={formData.template_name || ''}
onChange={handleInputChange}
readOnly={isViewMode}
required
/>
<div className="help-text text-xs">使&quot;-&quot;</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' : ''}`}
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="Extraction">(Extraction) - </option>
<option value="Evaluation">(Evaluation) - </option>
<option value="Summary">(Summary) - </option>
</select>
</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"
name="status"
checked={formData.status === 'active'}
onChange={handleStatusToggle}
disabled={isViewMode}
/>
<span className="slider"></span>
</label>
<span id="status-text">{formData.status === 'active' ? '启用' : '停用'}</span>
</div>
</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>使 <code>{"{varName}"}</code>使</div>
<div className="mt-1"><code>{"{docType}"}...</code></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' : ''}`}
id="template-content"
name="template_content"
placeholder="在此输入提示词模板内容..."
value={formData.template_content || ''}
onChange={handleInputChange}
readOnly={isViewMode}
rows={15}
required
></textarea>
<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> <code>{"{varName}"}</code> 使</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
type="submit"
className="ant-btn ant-btn-primary"
disabled={isSubmitting}
id="save-btn-bottom"
>
<i className="ri-save-line"></i> {isSubmitting ? "保存中..." : "保存"}
</button>
)}
</div>
</div>
</form>
</div>
);
}