Files
leaudit-platform-frontend/app/routes/prompts.new.tsx
T

719 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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: (data:LoaderData) => {
if (data.mode === "edit") {
return "编辑提示词模板";
} else if (data.mode === "view") {
return "查看提示词模板";
} else {
return "新增提示词模板";
}
}
};
interface LoaderData {
template: PromptTemplate;
mode: string;
}
// 从模拟数据中获取模板
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>
)}
</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>使 &#123;varName&#125;使</div>
<div className="mt-1">&#123;docType&#125;...</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> &#123;varName&#125; 使</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">
使 &#123;&#125;
</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>
);
}