完善提示词管理和配置管理的增删改查

This commit is contained in:
2025-04-09 21:48:28 +08:00
parent fda6515891
commit 4e43df00c0
14 changed files with 1100 additions and 484 deletions
+80 -86
View File
@@ -40,13 +40,13 @@ export const EXTENDED_ENVIRONMENT_LABELS: Record<string, string> = {
};
interface ConfigData {
id: string;
id: number;
name: string;
type: string;
environment: string;
is_active: boolean;
config: Record<string, unknown>;
remarks?: string;
remark?: string;
}
interface LoaderData {
@@ -76,11 +76,11 @@ export async function loader({ request }: LoaderFunctionArgs) {
config = detailResponse.data;
}
return json<LoaderData>({
return Response.json({
config,
isEdit: !!config,
types: optionsResponse.data.types,
environments: optionsResponse.data.environments
types: optionsResponse.data?.types || [],
environments: optionsResponse.data?.environments || []
});
}
@@ -103,7 +103,7 @@ export async function action({ request }: ActionFunctionArgs) {
const environment = formData.get("environment") as string;
const config = formData.get("config") as string;
const is_active = formData.get("is_active") === "true";
const remarks = formData.get("remarks") as string;
const remark = formData.get("remark") as string;
const errors: ActionData["errors"] = {};
@@ -131,7 +131,7 @@ export async function action({ request }: ActionFunctionArgs) {
}
if (Object.keys(errors).length > 0) {
return json<ActionData>({ errors });
return Response.json({ errors });
}
try {
@@ -141,7 +141,7 @@ export async function action({ request }: ActionFunctionArgs) {
environment,
config: JSON.parse(config),
is_active,
remarks
remark
};
if (id) {
@@ -161,7 +161,7 @@ export async function action({ request }: ActionFunctionArgs) {
return redirect("/config-lists");
} catch (error) {
console.error("保存配置失败:", error);
return json<ActionData>({
return Response.json({
success: false,
errors: {
general: "保存配置失败,请稍后重试"
@@ -170,18 +170,17 @@ export async function action({ request }: ActionFunctionArgs) {
}
}
// JSON模板数据
const JSON_TEMPLATES = {
// 配置模板常量
const CONFIG_TEMPLATES = {
database: {
database: {
host: "localhost",
port: 5432,
username: "db_user",
password: "******",
name: "app_database",
pool_size: 10,
timeout: 5000,
ssl: false
host: "localhost",
port: 5432,
database: "mydb",
username: "admin",
password: "******",
pool: {
min: 2,
max: 10
}
},
file: {
@@ -220,6 +219,9 @@ export default function ConfigNew() {
const [selectedModule, setSelectedModule] = useState<string>("");
const [selectedEnvironment, setSelectedEnvironment] = useState<string>("");
// 在 ConfigNew 组件中添加状态来跟踪当前选中的模板
const [selectedTemplate, setSelectedTemplate] = useState<keyof typeof CONFIG_TEMPLATES | null>(null);
useEffect(() => {
// 初始化配置数据
if (config) {
@@ -268,6 +270,7 @@ export default function ConfigNew() {
// 格式化JSON
const handleFormatJson = () => {
if (configDataValue.trim() === "") return;
try {
@@ -283,50 +286,6 @@ export default function ConfigNew() {
}
};
// 加载JSON模板
const handleLoadTemplate = (type: keyof typeof JSON_TEMPLATES) => {
const template = JSON_TEMPLATES[type];
setConfigDataValue(JSON.stringify(template, null, 2));
setJsonError(null);
};
// 显示JSON语法高亮
const renderJsonWithSyntaxHighlight = (json: string) => {
try {
// 如果是空字符串,直接返回
if (!json.trim()) return "";
// 解析并格式化JSON
const parsed = JSON.parse(json);
const formatted = JSON.stringify(parsed, null, 2);
// 添加语法高亮
return formatted
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, (match) => {
let cls = 'number';
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'key';
match = match.replace(':', '');
} else {
cls = 'string';
}
} else if (/true|false/.test(match)) {
cls = 'boolean';
} else if (/null/.test(match)) {
cls = 'null';
}
return `<span class="code-json ${cls}">${match}</span>`;
});
} catch (e) {
// 如果解析失败,返回原始JSON
return json;
}
};
return (
<div className="config-new-page">
<div className="flex justify-between items-center mb-4">
@@ -342,6 +301,12 @@ export default function ConfigNew() {
</Button>
</div>
</div>
{actionData?.errors?.general && (
<div className="mb-4 w-full">
<div className="error-message general-error">{actionData.errors.general}</div>
</div>
)}
<Card className="config-form-card">
<Form method="post" id="configForm" className="config-form">
@@ -412,7 +377,7 @@ export default function ConfigNew() {
<div className="error-message">{actionData.errors.type}</div>
)}
<div className="tag-buttons mt-2">
{types.map(type => (
{types.map((type: string) => (
<button
key={type}
type="button"
@@ -449,7 +414,7 @@ export default function ConfigNew() {
<div className="error-message">{actionData.errors.environment}</div>
)}
<div className="tag-buttons mt-2">
{environments.map(env => (
{environments.map((env: string) => (
<button
key={env}
type="button"
@@ -484,7 +449,10 @@ export default function ConfigNew() {
<Button
type="default"
size="small"
onClick={handleFormatJson}
onClick={(e) => {
e.preventDefault();
handleFormatJson();
}}
>
<i className="ri-braces-line mr-1"></i> JSON
</Button>
@@ -501,32 +469,62 @@ export default function ConfigNew() {
<div className="example-title"></div>
</div>
<div className="example-content">
<pre
className="example-pre"
dangerouslySetInnerHTML={{ __html: renderJsonWithSyntaxHighlight(exampleJsonValue) }}
/>
<div className="json-display">
{exampleJsonValue.split('\n').map((line, index) => (
<div key={index} className="json-line">
{line.split('').map((char, charIndex) => {
let className = 'json-char';
if (char === '{' || char === '}') className += ' json-brace';
if (char === '[' || char === ']') className += ' json-bracket';
if (char === '"') className += ' json-quote';
if (char === ':') className += ' json-colon';
if (char === ',') className += ' json-comma';
return (
<span key={charIndex} className={className}>
{char}
</span>
);
})}
</div>
))}
</div>
</div>
<div className="example-footer">
<div className="text-sm font-medium mb-2">:</div>
<div className="flex flex-wrap gap-2">
<Button
type="default"
type="default"
size="small"
onClick={() => handleLoadTemplate('database')}
className={`${selectedTemplate === 'database' ? 'border-[#00684a] text-[#00684a] focus:ring-0' : ''}`}
onClick={(e) => {
e.preventDefault();
setExampleJsonValue(JSON.stringify(CONFIG_TEMPLATES.database, null, 2));
setSelectedTemplate('database');
}}
>
</Button>
<Button
type="default"
type="default"
size="small"
onClick={() => handleLoadTemplate('file')}
className={`${selectedTemplate === 'file' ? 'border-[#00684a] text-[#00684a] focus:ring-0' : ''}`}
onClick={(e) => {
e.preventDefault();
setExampleJsonValue(JSON.stringify(CONFIG_TEMPLATES.file, null, 2));
setSelectedTemplate('file');
}}
>
</Button>
<Button
type="default"
type="default"
size="small"
onClick={() => handleLoadTemplate('ai')}
className={`${selectedTemplate === 'ai' ? 'border-[#00684a] text-[#00684a] focus:ring-0' : ''}`}
onClick={(e) => {
e.preventDefault();
setExampleJsonValue(JSON.stringify(CONFIG_TEMPLATES.ai, null, 2));
setSelectedTemplate('ai');
}}
>
AI服务配置
</Button>
@@ -542,12 +540,12 @@ export default function ConfigNew() {
{/* 备注 */}
<div className="form-group">
<label htmlFor="remarks" className="form-label"></label>
<label htmlFor="remark" className="form-label"></label>
<textarea
id="remarks"
name="remarks"
id="remark"
name="remark"
className="form-textarea"
defaultValue={config?.remarks || ''}
defaultValue={config?.remark || ''}
rows={2}
placeholder="请输入配置备注信息"
/>
@@ -556,11 +554,7 @@ export default function ConfigNew() {
</div>
</div>
{actionData?.errors?.general && (
<div className="form-row">
<div className="error-message general-error">{actionData.errors.general}</div>
</div>
)}
</Form>
</Card>
</div>
+22
View File
@@ -0,0 +1,22 @@
import { Outlet } from "react-router-dom";
import {type MetaFunction} from "@remix-run/node";
export const meta: MetaFunction = () => {
return [
{title: "文档类型列表 - 中国烟草AI合同及卷宗审核系统"},
{name: "document-types", content: "文档类型列表,新增,修改"}
]
}
export const handle = {
breadcrumb: "文档类型列表"
}
/**
* 文档类型列表路由布局
*/
export default function DocumentTypesLayout() {
return (
<Outlet />
)
}
+121 -140
View File
@@ -1,11 +1,13 @@
import { MetaFunction } from "@remix-run/node";
import { useSearchParams, useNavigate } from "@remix-run/react";
import { MetaFunction, json, type LoaderFunctionArgs } from "@remix-run/node";
import { useSearchParams, useNavigate, useLoaderData } from "@remix-run/react";
import { useState } from "react";
import indexStyles from "~/styles/pages/prompts_index.css?url";
import { Card } from "~/components/ui/Card";
import { Button } from "~/components/ui/Button";
import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel";
import { Table } from "~/components/ui/Table";
import { Pagination } from "~/components/ui/Pagination";
import { getPromptTemplates, deletePromptTemplate, type PromptTemplateUI } from "~/api/prompts/prompts";
// 定义提示词模板类型
export interface PromptTemplate {
@@ -33,123 +35,61 @@ export const meta: MetaFunction = () => {
];
};
// 模拟数据
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": "文档类型" })
}
];
// 定义加载器返回数据类型
interface LoaderData {
templates: PromptTemplateUI[];
total: number;
pageSize: number;
currentPage: number;
error?: string;
}
// 数据加载器
export async function loader() {
export async function loader({ request }: LoaderFunctionArgs) {
try {
// 实际应用中,这里应该调用API获取数据
// const response = await fetch(`${process.env.API_BASE_URL}/api/prompt-templates`);
// if (!response.ok) throw new Error(`获取提示词模板失败: ${response.status}`);
// const templates = await response.json();
const url = new URL(request.url);
const name = url.searchParams.get('name') || undefined;
const type = url.searchParams.get('type') || undefined;
const status = url.searchParams.get('status') || undefined;
const page = parseInt(url.searchParams.get('page') || '1', 10);
const pageSize = parseInt(url.searchParams.get('pageSize') || '10', 10);
// 使用模拟数据
const templates = MOCK_TEMPLATES;
console.log('加载提示词模板参数:', { name, type, status, page, pageSize });
return Response.json({
templates,
total: templates.length,
pageSize: 10,
currentPage: 1
// 从 API 获取数据
const result = await getPromptTemplates({
name,
type,
status,
page,
pageSize
});
if (result.error) {
console.error('获取提示词模板失败:', result.error);
return json<LoaderData>(
{
templates: [],
total: 0,
pageSize,
currentPage: page,
error: result.error
},
{ status: result.status || 500 }
);
}
console.log(`成功加载${result.data?.templates.length || 0}条提示词模板数据`);
return json<LoaderData>({
templates: result.data?.templates || [],
total: result.data?.total || 0,
pageSize,
currentPage: page
});
} catch (error) {
console.error("加载提示词模板失败:", error);
return Response.json(
return json<LoaderData>(
{
templates: [],
total: 0,
@@ -166,6 +106,8 @@ export async function loader() {
export default function PromptsIndex() {
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const { templates, total, currentPage, pageSize, error } = useLoaderData<typeof loader>();
const [isLoading, setIsLoading] = useState(false);
// 处理搜索名称
const handleNameSearch = (value: string) => {
@@ -210,6 +152,12 @@ export default function PromptsIndex() {
setSearchParams(new URLSearchParams());
};
// 处理搜索按钮点击
// const handleSearch = () => {
// // 搜索已经由 URL 参数变化触发,这里不需要额外操作
// console.log('搜索参数:', Object.fromEntries(searchParams.entries()));
// };
// 查看模板详情
const handleViewTemplate = (id: string) => {
navigate(`/prompts/new?id=${id}&mode=view`);
@@ -228,23 +176,48 @@ export default function PromptsIndex() {
};
// 删除模板
const handleDeleteTemplate = (id: string) => {
const handleDeleteTemplate = async (id: string) => {
if (confirm('确定要删除该模板吗?删除后无法恢复。')) {
// 实际应该调用API删除数据
console.log('删除模板ID:', id);
alert('删除成功!');
// 刷新页面
window.location.reload();
setIsLoading(true);
try {
const result = await deletePromptTemplate(id);
if (result.error) {
alert(`删除失败: ${result.error}`);
} else {
alert('删除成功!');
// 刷新页面
window.location.reload();
}
} catch (error) {
alert(`删除失败: ${error instanceof Error ? error.message : '未知错误'}`);
} finally {
setIsLoading(false);
}
}
};
// 处理分页
const handlePageChange = (page: number) => {
const newParams = new URLSearchParams(searchParams);
newParams.set('page', page.toString());
setSearchParams(newParams);
};
// 处理每页条数变更
const handlePageSizeChange = (size: number) => {
const newParams = new URLSearchParams(searchParams);
newParams.set('pageSize', size.toString());
newParams.set('page', '1'); // 更改每页条数时,重置到第一页
setSearchParams(newParams);
};
// 定义表格列配置
const columns = [
{
title: "模板名称",
key: "template_name",
width: "300px",
render: (_: unknown, record: PromptTemplate) => (
render: (_: unknown, record: PromptTemplateUI) => (
<div className="flex items-center">
<i className="ri-file-list-line text-primary mr-2"></i>
<span className="truncate">{record.template_name}</span>
@@ -255,7 +228,7 @@ export default function PromptsIndex() {
title: "类型",
key: "template_type",
width: "100px",
render: (_: unknown, record: PromptTemplate) => {
render: (_: unknown, record: PromptTemplateUI) => {
let typeText = '';
let typeClass = '';
@@ -284,7 +257,7 @@ export default function PromptsIndex() {
{
title: "描述",
key: "description",
render: (_: unknown, record: PromptTemplate) => (
render: (_: unknown, record: PromptTemplateUI) => (
<div className="text-secondary text-sm truncate max-w-xs" title={record.description}>
{record.description}
</div>
@@ -294,13 +267,13 @@ export default function PromptsIndex() {
title: "版本",
key: "version",
width: "80px",
render: (_: unknown, record: PromptTemplate) => record.version
render: (_: unknown, record: PromptTemplateUI) => record.version
},
{
title: "状态",
key: "status",
width: "80px",
render: (_: unknown, record: PromptTemplate) => {
render: (_: unknown, record: PromptTemplateUI) => {
let statusText = '';
let statusClass = '';
@@ -326,14 +299,15 @@ export default function PromptsIndex() {
title: "创建者",
key: "created_by",
width: "100px",
render: (_: unknown, record: PromptTemplate) => record.created_by
render: (_: unknown, record: PromptTemplateUI) => (
<span className="text-secondary"> {record.created_by}</span>
)
},
{
title: "操作",
key: "operation",
width: "150px",
// align: "center",
render: (_: unknown, record: PromptTemplate) => (
render: (_: unknown, record: PromptTemplateUI) => (
<div>
{record.status === 'system' ? (
<>
@@ -361,6 +335,7 @@ export default function PromptsIndex() {
<button
className="operation-btn text-error"
onClick={() => handleDeleteTemplate(record.id)}
disabled={isLoading}
>
<i className="ri-delete-bin-line"></i>
</button>
@@ -400,12 +375,13 @@ export default function PromptsIndex() {
>
</Button>
<Button
{/* <Button
type="primary"
icon="ri-search-line"
onClick={handleSearch}
>
搜索
</Button>
</Button> */}
</>
}
noActionDivider={true}
@@ -424,7 +400,6 @@ export default function PromptsIndex() {
name="type"
value={searchParams.get('type') || ''}
options={[
// { value: "", label: "全部" },
{ value: "Extraction", label: "抽取(Extraction)" },
{ value: "Evaluation", label: "评估(Evaluation)" },
{ value: "Summary", label: "摘要(Summary)" },
@@ -439,7 +414,6 @@ export default function PromptsIndex() {
name="status"
value={searchParams.get('status') || ''}
options={[
// { value: "", label: "全部" },
{ value: "active", label: "启用" },
{ value: "inactive", label: "停用" },
{ value: "system", label: "系统预设" }
@@ -449,27 +423,34 @@ export default function PromptsIndex() {
/>
</FilterPanel>
{/* 错误信息 */}
{error && (
<div className="error-alert mb-4 p-4 bg-red-50 text-red-700 rounded-md">
<i className="ri-error-warning-line mr-2"></i> {error}
</div>
)}
{/* 数据表格 */}
<Card bodyClassName="px-4 py-4">
<Table
columns={columns}
dataSource={MOCK_TEMPLATES}
dataSource={templates}
rowKey="id"
emptyText="暂无提示词模板数据"
loading={isLoading}
/>
{/* 分页 */}
<Pagination
currentPage={1}
total={MOCK_TEMPLATES.length}
pageSize={10}
onChange={(page) => {
const newParams = new URLSearchParams(searchParams);
newParams.set('page', page.toString());
setSearchParams(newParams);
}}
showTotal={true}
/>
currentPage={currentPage}
total={total}
pageSize={pageSize}
onChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
showTotal={true}
showPageSizeChanger={true}
pageSizeOptions={[10, 20, 30, 50]}
/>
</Card>
</div>
);
+214 -214
View File
@@ -1,9 +1,9 @@
import { useEffect, useState } from "react";
import { MetaFunction, LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/node";
import { Link, useLoaderData, useSubmit, useNavigation } from "@remix-run/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 { PromptTemplate } from "./prompts._index";
import newStyles from "~/styles/pages/prompts_new.css?url";
import { getPromptTemplate, createPromptTemplate, updatePromptTemplate, type PromptTemplateUI } from "~/api/prompts/prompts";
// 样式链接
export function links() {
@@ -32,111 +32,34 @@ export const handle = {
};
interface LoaderData {
template: PromptTemplate;
template: PromptTemplateUI | null;
mode: string;
error?: 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: "抽取采购合同中乙方的资质信息",
// 定义本地表单数据接口
interface FormDataState extends Omit<PromptTemplateUI, 'variables'> {
variables: string; // 在表单状态中我们保存变量为 JSON 字符串
}
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);
};
interface ActionData {
success?: boolean;
errors?: {
template_name?: string;
template_type?: string;
template_content?: string;
general?: string;
};
formData?: {
template_name: string;
template_type: "Extraction" | "Evaluation" | "Summary" | "Common";
description: string;
template_content: string;
variables: string;
status: "active" | "inactive" | "system";
version: string;
};
}
// 加载函数
export async function loader({ request }: LoaderFunctionArgs) {
@@ -149,25 +72,27 @@ export async function loader({ request }: LoaderFunctionArgs) {
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();
// API获取数据
const result = await getPromptTemplate(id);
// 使用模拟数据
template = getTemplateById(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 Response.json({
return json<LoaderData>({
template,
mode
});
} catch (error) {
console.error("加载提示词模板失败:", error);
return Response.json(
return json<LoaderData>(
{
template: null,
mode: "create",
@@ -180,69 +105,102 @@ export async function loader({ request }: LoaderFunctionArgs) {
// 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 "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 {
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()
// 准备变量数据
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 Response.json(
{
success: false,
message: error instanceof Error ? error.message : "保存提示词模板失败"
return json({
errors: {
general: error instanceof Error ? error.message : "保存提示词模板失败"
},
{ status: 500 }
);
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> = {};
@@ -260,13 +218,13 @@ const extractVariables = (content: string) => {
// 页面组件
export default function PromptsNew() {
const { template, mode } = useLoaderData<typeof loader>();
const submit = useSubmit();
const { template, mode, error } = useLoaderData<typeof loader>();
const actionData = useActionData<ActionData>();
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
// 表单状态
const [formData, setFormData] = useState<Partial<PromptTemplate>>({
const [formData, setFormData] = useState<FormDataState>({
id: "",
template_name: "",
template_type: "Common",
@@ -274,7 +232,10 @@ export default function PromptsNew() {
version: "v1.0",
status: "active",
template_content: "",
variables: "{}"
created_by: 1,
variables: "{}",
created_at: "",
updated_at: ""
});
// 模式状态
@@ -282,43 +243,57 @@ export default function PromptsNew() {
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) {
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并修改名称
id: mode === "clone" ? "" : template.id,
template_name: mode === "clone" ? `${template.template_name} (副本)` : template.template_name,
// 如果是克隆模式,重置版本
version: mode === "clone" ? "v1.0" : template.version
version: mode === "clone" ? "v1.0" : template.version,
variables: variablesJson
};
setFormData(newFormData);
try {
// 解析模板变量
const vars = JSON.parse(template.variables);
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") {
@@ -328,7 +303,7 @@ export default function PromptsNew() {
} else {
setPageTitle("新增提示词模板");
}
}, [template, mode]);
}, [template, mode, actionData?.formData]);
// 处理输入变化
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
@@ -337,16 +312,18 @@ export default function PromptsNew() {
// 如果是模板内容,检测变量
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] || key;
acc[key] = exampleValues[key] || "";
return acc;
}, {} as Record<string, string>)
);
// console.log("更新变量JSON",varsJson);
setFormData(prev => ({
...prev,
[name]: value,
@@ -362,11 +339,19 @@ export default function PromptsNew() {
// 处理状态切换
const handleStatusToggle = (e: React.ChangeEvent<HTMLInputElement>) => {
// console.log("状态切换前:", formData.status);
const status = e.target.checked ? "active" : "inactive";
setFormData(prev => ({
...prev,
status
}));
// console.log("新状态值:", status);
// 直接更新formData状态
setFormData(prev => {
const newState = {
...prev,
status: status as "active" | "inactive" | "system"
};
// console.log("状态更新后:", newState.status);
return newState;
});
};
// 处理示例值变更
@@ -383,7 +368,7 @@ export default function PromptsNew() {
// 替换变量
Object.entries(detectedVariables).forEach(([key]) => {
const exampleValue = exampleValues[key] || `[${key}]`;
const exampleValue = exampleValues[key] || ``;
const regex = new RegExp(`{${key}}`, 'g');
content = content.replace(regex, exampleValue);
});
@@ -395,7 +380,7 @@ export default function PromptsNew() {
useEffect(() => {
const varsJson = JSON.stringify(
Object.keys(detectedVariables).reduce((acc, key) => {
acc[key] = exampleValues[key] || key;
acc[key] = exampleValues[key] || "";
return acc;
}, {} as Record<string, string>)
);
@@ -405,23 +390,6 @@ export default function PromptsNew() {
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">
@@ -439,7 +407,7 @@ export default function PromptsNew() {
type="primary"
icon="ri-save-line"
disabled={isSubmitting}
onClick={() => document.getElementById("template-form")?.dispatchEvent(new Event("submit", { cancelable: true, bubbles: true }))}
form="template-form"
>
{isSubmitting ? "保存中..." : "保存"}
</Button>
@@ -447,6 +415,21 @@ export default function PromptsNew() {
</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">
@@ -458,7 +441,7 @@ export default function PromptsNew() {
)}
{/* 模板表单 */}
<form id="template-form" method="post" onSubmit={handleSubmit}>
<Form id="template-form" method="post">
{/* 模板ID - 隐藏字段 */}
<input type="hidden" name="id" value={formData.id || ''} />
@@ -474,7 +457,7 @@ export default function PromptsNew() {
</label>
<input
type="text"
className={`form-input py-1 ${isViewMode ? 'read-only-field' : ''}`}
className={`form-input py-1 ${isViewMode ? 'read-only-field' : ''} ${actionData?.errors?.template_name ? 'input-error' : ''}`}
id="template-name"
name="template_name"
placeholder="请输入模板名称"
@@ -483,6 +466,9 @@ export default function PromptsNew() {
readOnly={isViewMode}
required
/>
{actionData?.errors?.template_name && (
<div className="error-message">{actionData.errors.template_name}</div>
)}
<div className="help-text text-xs">使&quot;-&quot;</div>
</div>
@@ -492,7 +478,7 @@ export default function PromptsNew() {
<span className="text-error">*</span>
</label>
<select
className={`form-select py-1 ${isViewMode ? 'read-only-field' : ''}`}
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 || ''}
@@ -506,6 +492,9 @@ export default function PromptsNew() {
<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>
{/* 模板描述 */}
@@ -535,15 +524,22 @@ export default function PromptsNew() {
<input
type="checkbox"
id="status-toggle"
name="status"
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>
{/* 模板版本 */}
@@ -584,7 +580,7 @@ export default function PromptsNew() {
<span className="text-error">*</span>
</label>
<textarea
className={`form-code-editor w-full ${isViewMode ? 'read-only-field' : ''}`}
className={`form-code-editor w-full ${isViewMode ? 'read-only-field' : ''} ${actionData?.errors?.template_content ? 'input-error' : ''}`}
id="template-content"
name="template_content"
placeholder="在此输入提示词模板内容..."
@@ -594,6 +590,9 @@ export default function PromptsNew() {
rows={15}
required
></textarea>
{actionData?.errors?.template_content && (
<div className="error-message">{actionData.errors.template_content}</div>
)}
<div className="help-text">AI完成特定任务的指令</div>
</div>
@@ -642,6 +641,7 @@ export default function PromptsNew() {
className="form-input"
value={varName}
readOnly
/>
<input
type="text"
@@ -703,7 +703,7 @@ export default function PromptsNew() {
</Link>
{!isViewMode && (
<button
type="submit"
form="template-form"
className="ant-btn ant-btn-primary"
disabled={isSubmitting}
id="save-btn-bottom"
@@ -713,7 +713,7 @@ export default function PromptsNew() {
)}
</div>
</div>
</form>
</Form>
</div>
);
}