fix: 完善提示词管理页面的优化,数据库中添加相关字段来区分vlm和llm提示词。评查点设置中抽取设置的多模态抽取的类型通过查询数据库来返回数据。

This commit is contained in:
2025-11-11 01:16:27 +08:00
parent ddad57529d
commit 95381ddcc2
7 changed files with 261 additions and 76 deletions
+80 -8
View File
@@ -14,6 +14,8 @@ export interface PromptTemplate {
created_by: number;
created_at: string;
updated_at: string;
template_code?: string; // 模板代码(VLM_Extraction 类型时使用)
template_abbreviation?: string; // 模板简称(VLM_Extraction 类型时使用)
}
// 提示词模板前端接口
@@ -30,6 +32,8 @@ export interface PromptTemplateUI {
created_by_username?: string; // 创建者用户名
created_at: string;
updated_at: string;
template_code?: string; // 模板代码(VLM_Extraction 类型时使用)
template_abbreviation?: string; // 模板简称(VLM_Extraction 类型时使用)
}
// 搜索参数接口
@@ -108,7 +112,9 @@ export function convertToUITemplate(template: PromptTemplate & { sso_users?: { u
created_by: template.created_by,
created_by_username: template.sso_users?.username, // 从关联的用户信息中提取用户名
created_at: formatDate(template.created_at),
updated_at: formatDate(template.updated_at)
updated_at: formatDate(template.updated_at),
template_code: template.template_code,
template_abbreviation: template.template_abbreviation
};
}
@@ -131,7 +137,7 @@ export async function getPromptTemplates(searchParams: PromptSearchParams = {},
// 构建查询参数,包含对 sso_users 表的左连接
const params: PostgrestParams = {
select: `id,template_name,template_type,description,template_content,variables,status,version,created_by,created_at,updated_at,sso_users!created_by(username)`,
select: `id,template_name,template_type,description,template_content,variables,status,version,created_by,created_at,updated_at,template_code,template_abbreviation,sso_users!created_by(username)`,
order: 'updated_at.desc',
headers: {
'Prefer': 'count=exact'
@@ -232,7 +238,7 @@ export async function getPromptTemplate(id: string, frontendJWT?: string): Promi
}
const params: PostgrestParams = {
select: `id,template_name,template_type,description,template_content,variables,status,version,created_by,created_at,updated_at,sso_users!created_by(username)`,
select: `id,template_name,template_type,description,template_content,variables,status,version,created_by,created_at,updated_at,template_code,template_abbreviation,sso_users!created_by(username)`,
filter: {
'id': `eq.${id}`
},
@@ -305,7 +311,9 @@ export async function createPromptTemplate(template: Partial<PromptTemplateUI>,
variables: variablesData,
status: mapStatusToAPI(template.status || 'active'),
version: template.version || 'v1.0',
created_by: userId // 使用当前登录用户ID
created_by: userId, // 使用当前登录用户ID
template_code: template.template_code,
template_abbreviation: template.template_abbreviation
};
if(apiTemplate){
@@ -399,11 +407,19 @@ export async function updatePromptTemplate(id: string, template: Partial<PromptT
apiTemplate.version = template.version;
}
if (template.template_code !== undefined) {
apiTemplate.template_code = template.template_code;
}
if (template.template_abbreviation !== undefined) {
apiTemplate.template_abbreviation = template.template_abbreviation;
}
// if(apiTemplate){
// console.log('apiTemplate', apiTemplate);
// throw new Error('测试错误');
// }
const response = await postgrestPut<PromptTemplate, Partial<PromptTemplate>>(
'prompt_templates',
apiTemplate,
@@ -446,7 +462,7 @@ export async function deletePromptTemplate(id: string, frontendJWT?: string): Pr
if (!id) {
return { error: '模板ID不能为空', status: 400 };
}
// 使用真实删除替代状态更新
const response = await postgrestDelete<PromptTemplate>(
'prompt_templates',
@@ -457,11 +473,11 @@ export async function deletePromptTemplate(id: string, frontendJWT?: string): Pr
token: frontendJWT
}
);
if (response.error) {
return { error: response.error, status: response.status };
}
return { success: true };
} catch (error) {
console.error('删除提示词模板失败:', error);
@@ -470,4 +486,60 @@ export async function deletePromptTemplate(id: string, frontendJWT?: string): Pr
status: 500
};
}
}
/**
* 获取指定类型的提示词模板选项
* @param templateType 模板类型(如 'VLM_Extraction', 'LLM_Extraction' 等)
* @param frontendJWT JWT token (可选)
* @returns 模板选项列表 { value: template_code, label: template_abbreviation }
*/
export async function getPromptTemplateOptions(templateType: string, frontendJWT?: string): Promise<{
data?: Array<{ value: string; label: string }>;
error?: string;
status?: number;
}> {
try {
if (!templateType) {
return { error: '模板类型不能为空', status: 400 };
}
const params: PostgrestParams = {
select: 'template_code,template_abbreviation',
filter: {
'template_type': `eq.${templateType}`,
'status': 'gte.0' // 只查询有效状态的模板
},
order: 'template_abbreviation.asc', // 按标签排序
token: frontendJWT
};
const response = await postgrestGet<Array<{ template_code: string; template_abbreviation: string }>>('prompt_templates', params);
if (response.error) {
console.error('获取提示词模板选项失败:', response.error);
return { error: response.error, status: response.status };
}
const extractedData = extractApiData<Array<{ template_code: string; template_abbreviation: string }>>(response.data);
if (!extractedData) {
console.error('提取提示词模板选项数据失败');
return { error: '获取提示词模板选项失败', status: 500 };
}
// 转换为选项格式
const options = extractedData.map(item => ({
value: item.template_code,
label: item.template_abbreviation
}));
return { data: options };
} catch (error) {
console.error('获取提示词模板选项出错:', error);
return {
error: error instanceof Error ? error.message : '获取提示词模板选项失败',
status: 500
};
}
}
+35 -43
View File
@@ -45,6 +45,7 @@ interface ExtractionSettingsProps {
export function ExtractionSettings({
onChange,
initialData,
vlmFieldTypeOptions = EVALUATION_OPTIONS.vlmFieldTypeOptions,
}: ExtractionSettingsProps) {
// 核心数据状态
@@ -84,7 +85,10 @@ export function ExtractionSettings({
vlm: initialData?.extraction_config?.vlm?.fields || []
});
// VLM字段类型
const [selectedVlmFieldType, setSelectedVlmFieldType] = useState('vlm_default_prompt');
const [selectedVlmFieldType, setSelectedVlmFieldType] = useState(() => {
// 使用传入的选项中的第一个作为默认值,如果没有则使用 vlm_default_prompt
return vlmFieldTypeOptions.length > 0 ? vlmFieldTypeOptions[0].value : 'vlm_default_prompt';
});
// 自定义字段的提示词模板
const [customVlmPrompt, setCustomVlmPrompt] = useState('请识别文档中的印章信息,提取以下字段');
// 提示词类型
@@ -117,6 +121,14 @@ export function ExtractionSettings({
setCurrentTab(tab);
};
// 当 vlmFieldTypeOptions 加载完成时,更新默认选中的类型
useEffect(() => {
if (vlmFieldTypeOptions.length > 0 && !vlmFieldTypeOptions.find(opt => opt.value === selectedVlmFieldType)) {
// 如果当前选中的类型不在新的选项列表中,选择第一个选项
setSelectedVlmFieldType(vlmFieldTypeOptions[0].value);
}
}, [vlmFieldTypeOptions, selectedVlmFieldType]);
// 初始化自定义字段的提示词
useEffect(() => {
// 在编辑模式下,如果有自定义类型的字段,加载其 template
@@ -249,52 +261,32 @@ export function ExtractionSettings({
if (typeof field === 'string') {
const parts = field.split('_');
fieldName = parts[0];
fieldType = parts.length > 1 ? parts[1] : 'default';
fieldType = parts.length > 1 ? parts[1] : 'vlm_default_prompt';
} else {
fieldName = field.name;
fieldType = field.type;
}
switch (fieldType) {
case 'vlm_default_prompt':
typeName = '默认';
badgeClass = 'bg-gray-100 text-gray-800';
break;
case 'vlm_currency_prompt':
typeName = '货币';
badgeClass = 'bg-green-100 text-green-800';
break;
case 'vlm_print_prompt':
typeName = '打印';
badgeClass = 'bg-blue-100 text-blue-800';
break;
case 'vlm_seal_prompt':
typeName = '印章';
badgeClass = 'bg-red-100 text-red-800';
break;
case 'vlm_acrossPageSeal_prompt':
typeName = '骑缝章';
badgeClass = 'bg-orange-100 text-orange-800';
break;
case 'vlm_english_prompt':
typeName = '英文';
badgeClass = 'bg-purple-100 text-purple-800';
break;
case 'vlm_number_prompt':
typeName = '数字';
badgeClass = 'bg-yellow-100 text-yellow-800';
break;
case 'vlm_handwriting_prompt':
typeName = '手写';
badgeClass = 'bg-pink-100 text-pink-800';
break;
case 'custom':
typeName = '自定义';
badgeClass = 'bg-indigo-100 text-indigo-800';
break;
default:
typeName = '默认';
badgeClass = 'bg-gray-100 text-gray-800';
// 首先尝试从 vlmFieldTypeOptions 中查找对应的标签
const optionItem = vlmFieldTypeOptions.find(opt => opt.value === fieldType);
if (optionItem) {
typeName = optionItem.label;
// 根据不同类型设置不同的颜色
switch (fieldType) {
case 'vlm_default_prompt':
badgeClass = 'bg-gray-100 text-gray-800';
break;
case 'custom':
badgeClass = 'bg-indigo-100 text-indigo-800';
break;
default:
// 对于从数据库获取的类型,使用统一的蓝色系
badgeClass = 'bg-blue-100 text-blue-800';
}
} else {
// 如果找不到,使用默认值
typeName = '未知类型';
badgeClass = 'bg-gray-100 text-gray-800';
}
return { fieldName, fieldType, typeName, badgeClass };
@@ -408,7 +400,7 @@ export function ExtractionSettings({
value={selectedVlmFieldType}
onChange={(e) => setSelectedVlmFieldType(e.target.value)}
>
{EVALUATION_OPTIONS.vlmFieldTypeOptions.map((option) => (
{vlmFieldTypeOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
+8 -7
View File
@@ -8,6 +8,7 @@ import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterP
import { Table } from "~/components/ui/Table";
import { Pagination } from "~/components/ui/Pagination";
import { getPromptTemplates, deletePromptTemplate, type PromptTemplateUI } from "~/api/prompts/prompts";
import { toastService } from "~/components/ui";
// 样式链接
export function links() {
@@ -65,7 +66,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
if (result.error) {
console.error('获取提示词模板失败:', result.error);
return Response.json(
{
{
templates: [],
total: 0,
pageSize,
@@ -75,10 +76,10 @@ export async function loader({ request }: LoaderFunctionArgs) {
{ status: result.status || 500 }
);
}
// console.log(`成功加载${result.data?.templates.length || 0}条提示词模板数据`);
return Response.json({
return Response.json({
templates: result.data?.templates || [],
total: result.data?.total || 0,
pageSize,
@@ -218,10 +219,10 @@ export default function PromptsIndex() {
useEffect(() => {
if (fetcher.state === 'idle' && fetcher.data) {
if (fetcher.data.success) {
alert('删除成功!');
window.location.reload();
toastService.success('删除成功!');
// Remix 会自动重新验证 loader 数据,无需手动刷新页面
} else if (fetcher.data.error) {
alert(`删除失败: ${fetcher.data.error}`);
toastService.error(`删除失败: ${fetcher.data.error}`);
}
}
}, [fetcher.state, fetcher.data]);
+85 -11
View File
@@ -4,6 +4,7 @@ import { Link, useLoaderData, useNavigation, useActionData, Form } from "@remix-
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";
// import { toastService } from "~/components/ui";
// 样式链接
export function links() {
@@ -40,6 +41,8 @@ interface LoaderData {
// 定义本地表单数据接口
interface FormDataState extends Omit<PromptTemplateUI, 'variables'> {
variables: string; // 在表单状态中我们保存变量为 JSON 字符串
template_code?: string; // 模板代码
template_abbreviation?: string; // 模板简称
}
interface ActionData {
@@ -48,6 +51,8 @@ interface ActionData {
template_name?: string;
template_type?: string;
template_content?: string;
template_code?: string;
template_abbreviation?: string;
general?: string;
};
formData?: {
@@ -58,6 +63,8 @@ interface ActionData {
variables: string;
status: "active" | "inactive" | "system";
version: string;
template_code?: string;
template_abbreviation?: string;
};
}
@@ -110,8 +117,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
export async function action({ request }: ActionFunctionArgs) {
const { getUserSession } = await import("~/api/login/auth.server");
const { userInfo, frontendJWT } = await getUserSession(request);
const userId = userInfo.get('user_id')
const userId = userInfo.user_id;
const formData = await request.formData();
const id = formData.get("id") as string;
const template_name = formData.get("template_name") as string;
@@ -121,6 +128,8 @@ export async function action({ request }: ActionFunctionArgs) {
const variables = formData.get("variables") as string;
const status = formData.get("status") as string;
const version = formData.get("version") as string;
const template_code = formData.get("template_code") as string;
const template_abbreviation = formData.get("template_abbreviation") as string;
const errors: ActionData["errors"] = {};
@@ -137,6 +146,19 @@ export async function action({ request }: ActionFunctionArgs) {
errors.template_content = "模板内容不能为空";
}
// VLM_Extraction 类型的额外验证
if (template_type === 'VLM_Extraction') {
if (!template_code || template_code.trim() === "") {
errors.template_code = "VLM抽取类型必须填写模板code";
// toastService.error('VLM抽取类型必须填写模板code')
}
if (!template_abbreviation || template_abbreviation.trim() === "") {
errors.template_abbreviation = "VLM抽取类型必须填写模板简称";
// toastService.error('VLM抽取类型必须填写模板简称')
}
}
if (Object.keys(errors).length > 0) {
return Response.json({ errors });
}
@@ -160,7 +182,9 @@ export async function action({ request }: ActionFunctionArgs) {
template_content,
variables: variablesData,
status: status === "active" ? "active" : "inactive",
version: version || "v1.0"
version: version || "v1.0",
template_code: template_code || undefined,
template_abbreviation: template_abbreviation || undefined
};
let result;
@@ -173,7 +197,7 @@ export async function action({ request }: ActionFunctionArgs) {
}
if (result.error) {
return Response.json({
return Response.json({
errors: { general: result.error },
formData: {
template_name,
@@ -182,7 +206,9 @@ export async function action({ request }: ActionFunctionArgs) {
template_content,
variables,
status,
version
version,
template_code,
template_abbreviation
}
});
}
@@ -190,9 +216,9 @@ export async function action({ request }: ActionFunctionArgs) {
return redirect("/prompts");
} catch (error) {
console.error("保存提示词模板失败:", error);
return Response.json({
errors: {
general: error instanceof Error ? error.message : "保存提示词模板失败"
return Response.json({
errors: {
general: error instanceof Error ? error.message : "保存提示词模板失败"
},
formData: {
template_name,
@@ -201,7 +227,9 @@ export async function action({ request }: ActionFunctionArgs) {
template_content,
variables,
status,
version
version,
template_code,
template_abbreviation
}
});
}
@@ -252,7 +280,9 @@ export default function PromptsNew() {
created_by: 1,
variables: "{}",
created_at: "",
updated_at: ""
updated_at: "",
template_code: "",
template_abbreviation: ""
});
// 模式状态
@@ -497,7 +527,7 @@ export default function PromptsNew() {
<label htmlFor="template-type" className="form-label mb-1">
<span className="text-error">*</span>
</label>
<select
<select
className={`form-select py-1 ${isViewMode ? 'read-only-field' : ''} ${actionData?.errors?.template_type ? 'input-error' : ''}`}
id="template-type"
name="template_type"
@@ -517,6 +547,50 @@ export default function PromptsNew() {
<div className="error-message">{actionData.errors.template_type}</div>
)}
</div>
{/* 模板codeVLM_Extraction类型时必填) */}
<div className="form-group mb-3">
<label htmlFor="template-code" className="form-label mb-1">
code {formData.template_type === 'VLM_Extraction' && <span className="text-error">*</span>}
</label>
<input
type="text"
className={`form-input py-1 ${isViewMode ? 'read-only-field' : ''} ${actionData?.errors?.template_code ? 'input-error' : ''}`}
id="template-code"
name="template_code"
placeholder="请输入模板code,如:vlm_seal_prompt"
value={formData.template_code || ''}
onChange={handleInputChange}
readOnly={isViewMode}
required={formData.template_type === 'VLM_Extraction'}
/>
{actionData?.errors?.template_code && (
<div className="error-message">{actionData.errors.template_code}</div>
)}
<div className="help-text text-xs">VLM抽取类型必须填写</div>
</div>
{/* 模板简称(VLM_Extraction类型时必填) */}
<div className="form-group mb-3">
<label htmlFor="template-abbreviation" className="form-label mb-1">
{formData.template_type === 'VLM_Extraction' && <span className="text-error">*</span>}
</label>
<input
type="text"
className={`form-input py-1 ${isViewMode ? 'read-only-field' : ''} ${actionData?.errors?.template_abbreviation ? 'input-error' : ''}`}
id="template-abbreviation"
name="template_abbreviation"
placeholder="请输入模板简称,如:印章"
value={formData.template_abbreviation || ''}
onChange={handleInputChange}
readOnly={isViewMode}
required={formData.template_type === 'VLM_Extraction'}
/>
{actionData?.errors?.template_abbreviation && (
<div className="error-message">{actionData.errors.template_abbreviation}</div>
)}
<div className="help-text text-xs">VLM抽取类型必须填写</div>
</div>
{/* 模板描述 */}
<div className="form-group mb-3">
+6 -3
View File
@@ -9,6 +9,7 @@ import { Table } from "~/components/ui/Table";
import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel";
// import { Pagination } from "~/components/ui/Pagination";
import { getRuleGroups, getChildGroups, type RuleGroup, deleteRuleGroup } from "~/api/evaluation_points/rule-groups";
import { toastService } from "~/components/ui";
export function links() {
return [{ rel: "stylesheet", href: indexStyles }];
@@ -214,13 +215,15 @@ export default function RuleGroupsIndex() {
setExpandedGroups(prev => prev.filter(id => id !== groupId));
// 显示成功消息
alert('删除成功');
// alert('删除成功');
toastService.success('删除成功')
} else {
alert(`删除失败: ${result.error}`);
toastService.error(`删除失败: ${result.error}`);
console.error(`删除失败: ${result.error}`);
}
} catch (error) {
console.error('删除分组失败:', error);
alert('删除分组失败,请稍后重试');
toastService.error('删除分组失败,请稍后重试');
}
}
};
+38 -4
View File
@@ -51,6 +51,7 @@ import { RuleContext } from "~/contexts/RuleContext";
import { postgrestGet, postgrestPost, postgrestPut } from "~/api/postgrest-client";
import { toastService } from '~/components/ui/Toast';
import type { UserRole } from '~/root';
import { getPromptTemplateOptions } from '~/api/prompts/prompts';
export const meta: MetaFunction = () => {
return [
@@ -160,13 +161,16 @@ export default function RuleNew() {
const [formData, setFormData] = useState<EvaluationPoint>({});
const [evaluationPointGroups, setEvaluationPointGroups] = useState<EvaluationPointGroup[]>([]);
// 判断表单是否为只读模式
const isReadOnly = userRole === 'common';
// 添加用于共享的字段数据状态
const [extractionFields, setExtractionFields] = useState<string[]>([]);
// VLM字段类型选项
const [vlmFieldTypeOptions, setVlmFieldTypeOptions] = useState<Array<{ value: string; label: string }>>([]);
/**
* 从表单数据中提取所有字段
* 用于编辑模式下初始化字段数据
@@ -355,6 +359,34 @@ export default function RuleNew() {
}
}, [frontendJWT]);
/**
* 获取VLM字段类型选项
* 从API获取多模态抽取的字段类型选项
*/
const fetchVlmFieldTypeOptions = useCallback(async () => {
try {
// console.log("获取VLM字段类型选项");
const response = await getPromptTemplateOptions('VLM_Extraction', frontendJWT);
if (response.data && Array.isArray(response.data)) {
// 添加自定义选项
const optionsWithCustom = [
...response.data,
{ value: 'custom', label: '自定义' }
];
setVlmFieldTypeOptions(optionsWithCustom);
} else if (response.error) {
console.error('获取VLM字段类型选项失败:', response.error);
// 使用默认选项作为备选
setVlmFieldTypeOptions(EVALUATION_OPTIONS.vlmFieldTypeOptions);
}
} catch (error) {
console.error('获取VLM字段类型选项失败:', error);
// 使用默认选项作为备选
setVlmFieldTypeOptions(EVALUATION_OPTIONS.vlmFieldTypeOptions);
}
}, [frontendJWT]);
const handleSave = async () => {
// console.log("保存评查点", formData);
@@ -917,7 +949,9 @@ export default function RuleNew() {
// 获取评查点组数据
fetchEvaluationPointGroups();
}, [location.search, fetchEvaluationPoint, fetchEvaluationPointGroups, resetFormData]);
// 获取VLM字段类型选项
fetchVlmFieldTypeOptions();
}, [location.search, fetchEvaluationPoint, fetchEvaluationPointGroups, fetchVlmFieldTypeOptions, resetFormData]);
// 渲染页面内容
return (
@@ -958,7 +992,7 @@ export default function RuleNew() {
onChange={handleExtractionSettingsChange}
initialData={formData}
promptTypeOptions={EVALUATION_OPTIONS.llmPromptTypeOptions}
vlmFieldTypeOptions={EVALUATION_OPTIONS.vlmFieldTypeOptions}
vlmFieldTypeOptions={vlmFieldTypeOptions.length > 0 ? vlmFieldTypeOptions : EVALUATION_OPTIONS.vlmFieldTypeOptions}
/>
</div>
+9
View File
@@ -94,6 +94,15 @@
@apply mr-1;
}
/* 删除按钮 - 红色 */
.prompt-page .operation-btn.text-error {
@apply !text-red-600;
}
.prompt-page .operation-btn.text-error:hover {
@apply !text-red-700 bg-red-50;
}
/* 分页 */
.prompt-page .pagination-info {
@apply text-sm text-gray-500;