From 66d2f7cef4277b74060787fd5e1e142c82aade74 Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Tue, 30 Dec 2025 18:35:48 +0800 Subject: [PATCH] =?UTF-8?q?1.=E6=B7=BB=E5=8A=A0=E7=A7=BB=E5=8A=A8=E7=AB=AF?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=9A=84=E6=A3=80=E6=B5=8B=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E7=B1=BB=EF=BC=8C=E7=A7=BB=E5=8A=A8=E7=AB=AF=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=8F=AA=E8=83=BD=E8=AE=BF=E9=97=AE=E5=AF=B9=E8=AF=9D=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E3=80=82=202.=E8=AF=84=E6=9F=A5=E7=82=B9=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E6=B7=BB=E5=8A=A0=E6=96=87=E6=A1=A3=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=AD=97=E6=AE=B5=E3=80=82=203.=E4=BC=98?= =?UTF-8?q?=E5=8C=96dify=E7=9A=84=E5=AF=B9=E8=AF=9D=E4=BE=A7=E8=BE=B9?= =?UTF-8?q?=E6=A0=8F=E7=9A=84=E6=98=BE=E7=A4=BA=E6=95=88=E6=9E=9C=E3=80=82?= =?UTF-8?q?=204.=E8=AF=84=E6=9F=A5=E7=82=B9=E8=A7=84=E5=88=99=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E4=BD=BF=E7=94=A8=E6=96=87=E6=A1=A3=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E7=9A=84=E8=BE=93=E5=85=A5=E6=A1=86=E3=80=82?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=A4=9A=E5=AE=9E=E4=BD=93=E5=BC=80=E5=85=B3?= =?UTF-8?q?=E7=9A=84=E6=93=8D=E4=BD=9C=E5=BC=80=E5=85=B3=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/evaluation_points/rules.ts | 61 +++++++- app/components/dify-chat/index.tsx | 12 +- app/components/dify-chat/markdown.tsx | 5 +- app/components/layout/Layout.tsx | 9 +- app/components/rules/new/BasicInfo.tsx | 147 ++++++++++++++++-- .../rules/new/ExtractionSettings.tsx | 47 ++++++ app/components/rules/new/PageHeader.tsx | 13 +- app/models/evaluation_points.ts | 17 ++ app/models/rule.ts | 1 + app/root.tsx | 22 ++- app/routes/callback.tsx | 14 +- app/routes/files.upload.tsx | 2 +- app/routes/rules.list.tsx | 28 ++-- app/routes/rules.new.tsx | 14 +- .../components/chat-with-llm/markdown.css | 52 +++++++ .../components/chat-with-llm/sidebar.css | 37 ++++- app/styles/pages/rules_index.css | 21 ++- app/utils/mobile-detect.server.ts | 106 +++++++++++++ 18 files changed, 552 insertions(+), 56 deletions(-) create mode 100644 app/utils/mobile-detect.server.ts diff --git a/app/api/evaluation_points/rules.ts b/app/api/evaluation_points/rules.ts index aa1f9b0..b9c1042 100644 --- a/app/api/evaluation_points/rules.ts +++ b/app/api/evaluation_points/rules.ts @@ -311,7 +311,8 @@ export async function getRulesList(params: RulesQueryParams): Promise<{data: Rul isActive: point.is_enabled, createdAt: formatDate(point.created_at || ''), updatedAt: formatDate(point.updated_at || ''), - area: point.area || '' + area: point.area || '', + documentAttributeType: point.document_attribute_type || '' }; }); // console.log('✅ [getRulesList] 成功映射评查点列表数据', response.data.data[0]); @@ -1045,6 +1046,7 @@ export interface EvaluationPointData { ruleType?: string; // 评查点类型(一级分组名称) groupName?: string; // 所属规则组(二级分组名称) groupId?: string; // 规则组ID(二级分组ID的字符串形式) + document_attribute_type?: string; // 文档属性类型 references_laws: { name: string; content: string; @@ -1217,4 +1219,61 @@ export async function getEvaluationPoint( status: 500 }; } +} + +/** + * 适用属性类型选项 + */ +export interface AttributeTypeOption { + code: string; + label: string; +} + +/** + * 获取适用属性类型列表 + * 从后端获取当前评查点表中所有已使用的 document_attribute_type 去重列表 + * @param token JWT token (可选) + * @returns 适用属性类型列表 + */ +export async function getAttributeTypes( + token?: string +): Promise<{data: AttributeTypeOption[]; error?: never} | {data?: never; error: string; status?: number}> { + try { + // 调用后端 FastAPI 接口: GET /api/v3/evaluation-points/attribute-types + const response = await apiRequest<{ + types: AttributeTypeOption[]; + }>( + '/api/v3/evaluation-points/attribute-types', + { + method: 'GET', + headers: { + ...(token ? { 'Authorization': `Bearer ${token}` } : {}) + } + } + ); + + if (response.error) { + return { error: response.error, status: response.status }; + } + + if (!response.data || !Array.isArray(response.data.types)) { + // 返回默认的属性类型列表 + return { + data: [ + { code: 'ALL', label: '通用' } + ] + }; + } + + console.log('✅ getAttributeTypes 成功:', response.data.types); + return { data: response.data.types }; + } catch (error) { + console.error('❌ 获取适用属性类型列表出错:', error); + // 出错时返回默认列表,不阻塞用户操作 + return { + data: [ + { code: 'ALL', label: '通用' } + ] + }; + } } \ No newline at end of file diff --git a/app/components/dify-chat/index.tsx b/app/components/dify-chat/index.tsx index 032b2d7..ed9d8df 100644 --- a/app/components/dify-chat/index.tsx +++ b/app/components/dify-chat/index.tsx @@ -665,13 +665,11 @@ export default function Chat() { return ( - {/* 移动端遮罩层 */} - {!sidebarCollapsed && isMobile && ( -
- )} + {/* 移动端遮罩层 - 点击可收起侧边栏 */} +
{/* ChatSidebar 隐藏时显示的展开按钮 */} {sidebarCollapsed && ( diff --git a/app/components/dify-chat/markdown.tsx b/app/components/dify-chat/markdown.tsx index 0b01df8..c524d23 100644 --- a/app/components/dify-chat/markdown.tsx +++ b/app/components/dify-chat/markdown.tsx @@ -82,10 +82,11 @@ export const SourcesPanel = React.memo(({ resources }: { resources: RetrieverRes
} - placement="topLeft" - autoAdjustOverflow={false} + placement="top" + autoAdjustOverflow={true} color="rgba(0, 104, 74, 0.92)" classNames={{ root: 'source-tooltip-overlay' }} + overlayStyle={{ maxWidth: 'calc(100vw - 32px)' }} >
{resource.position} diff --git a/app/components/layout/Layout.tsx b/app/components/layout/Layout.tsx index ec4a9dd..250ee89 100644 --- a/app/components/layout/Layout.tsx +++ b/app/components/layout/Layout.tsx @@ -9,6 +9,7 @@ interface LayoutProps { children: React.ReactNode; userRole?: UserRole; frontendJWT?: string; + isMobile?: boolean; // 是否为移动端设备(服务端通过 User-Agent 检测) } // 添加一个接口表示路由handle可能包含的属性 @@ -23,7 +24,7 @@ interface Match { data: unknown; } -export function Layout({ children, userRole = 'developer' as UserRole, frontendJWT = '' }: LayoutProps) { +export function Layout({ children, userRole = 'developer' as UserRole, frontendJWT = '', isMobile = false }: LayoutProps) { const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [effectiveUserRole, setEffectiveUserRole] = useState(userRole); const [effectiveFrontendJWT, setEffectiveFrontendJWT] = useState(frontendJWT); @@ -32,10 +33,12 @@ export function Layout({ children, userRole = 'developer' as UserRole, frontendJ // 检查当前路径是否应该隐藏侧边栏 const noLayoutPaths = ['/login', '/']; - const shouldHideSidebar = noLayoutPaths.includes(location.pathname); + // 移动端设备强制隐藏侧边栏 + const shouldHideSidebar = isMobile || noLayoutPaths.includes(location.pathname); // 检查当前路由是否应该隐藏默认面包屑 - const shouldHideBreadcrumb = shouldHideSidebar || matches.some(match => + // 移动端设备强制隐藏面包屑(避免显示首页链接) + const shouldHideBreadcrumb = isMobile || shouldHideSidebar || matches.some(match => match.handle && match.handle.hideBreadcrumb === true ); diff --git a/app/components/rules/new/BasicInfo.tsx b/app/components/rules/new/BasicInfo.tsx index b1d1893..81e3b66 100644 --- a/app/components/rules/new/BasicInfo.tsx +++ b/app/components/rules/new/BasicInfo.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import type { EvaluationPoint } from '~/models/evaluation_points'; import type { EvaluationPointGroup } from '~/models/evaluation_point_groups'; -import { getRulesList, getRuleTypes, type RuleType } from '~/api/evaluation_points/rules'; +import { getRulesList, getRuleTypes, getAttributeTypes, type RuleType, type AttributeTypeOption } from '~/api/evaluation_points/rules'; interface BasicInfoProps { onChange?: (data: Record) => void; @@ -101,6 +101,11 @@ export function BasicInfo({ const [filteredRuleTypes, setFilteredRuleTypes] = useState([]); const [ruleTypesLoading, setRuleTypesLoading] = useState(false); + // 适用文档属性类型相关状态 + const [attributeTypeOptions, setAttributeTypeOptions] = useState([]); + const [attributeTypesLoading, setAttributeTypesLoading] = useState(false); + const [isCustomAttributeType, setIsCustomAttributeType] = useState(false); // 是否为自定义输入模式 + // 从 Session Storage 获取 documentTypeIds 并调用 API 获取评查点类型 useEffect(() => { const fetchRuleTypes = async () => { @@ -140,6 +145,43 @@ export function BasicInfo({ fetchRuleTypes(); }, [frontendJWT]); + // 获取适用文档属性类型选项 + useEffect(() => { + const fetchAttributeTypes = async () => { + try { + setAttributeTypesLoading(true); + const response = await getAttributeTypes(frontendJWT); + + if (response.data) { + setAttributeTypeOptions(response.data); + } else { + // 使用默认选项 + setAttributeTypeOptions([{ code: 'ALL', label: '通用' }]); + } + } catch (error) { + console.error('获取适用属性类型失败:', error); + setAttributeTypeOptions([{ code: 'ALL', label: '通用' }]); + } finally { + setAttributeTypesLoading(false); + } + }; + + fetchAttributeTypes(); + }, [frontendJWT]); + + // 检查初始数据中的 document_attribute_type 是否为自定义值 + useEffect(() => { + if (formData.document_attribute_type) { + const isInOptions = attributeTypeOptions.some( + opt => opt.code === formData.document_attribute_type + ); + // 如果当前值不在选项列表中,则切换到自定义模式 + if (!isInOptions && attributeTypeOptions.length > 0) { + setIsCustomAttributeType(true); + } + } + }, [formData.document_attribute_type, attributeTypeOptions]); + // 根据选择的评查点类型筛选可用的规则组 const filteredRuleGroups = evaluationPointGroups.filter(group => formData.evaluation_point_groups_pid && @@ -268,6 +310,14 @@ export function BasicInfo({ newData.evaluation_point_groups_id = null; // 清空规则组选择 } break; + case 'document-attribute-type': + // 处理适用文档属性类型选择(下拉框模式) + newData.document_attribute_type = value || 'ALL'; + break; + case 'document-attribute-type-custom': + // 处理适用文档属性类型输入(自定义模式) + newData.document_attribute_type = value; + break; } setFormData(newData); @@ -320,18 +370,19 @@ export function BasicInfo({ } }, [formData.references_laws?.articles]); - // 检查是否需要自动展开描述区域 + // 检查是否需要自动展开描述区域(仅在初始数据加载时执行一次) useEffect(() => { - // 如果描述或法律依据相关字段有值,则自动展开 + // 如果初始数据中描述或法律依据相关字段有值,则自动展开 if ( - formData.description || - formData.references_laws?.name || - (formData.references_laws?.articles && formData.references_laws.articles.length > 0) || - formData.references_laws?.content + initialData?.description || + initialData?.references_laws?.name || + (initialData?.references_laws?.articles && initialData.references_laws.articles.length > 0) || + initialData?.references_laws?.content ) { setIsDescExpanded(true); } - }, [formData]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // 注释掉自动选择规则组的逻辑,避免无限循环 // 原因:此 useEffect 依赖 onChange 和 filteredRuleGroups,每次渲染都可能触发 @@ -461,8 +512,8 @@ export function BasicInfo({
- + ) : ( + // 下拉选择模式 + + )} + +
+
+ {isCustomAttributeType + ? '输入自定义的文档属性类型,点击右侧按钮可切换回下拉选择' + : '选择评查点适用的文档属性类型,点击右侧按钮可自定义输入'} +
+
{ if (e.key === 'Enter' || e.key === ' ') { @@ -487,7 +610,7 @@ export function BasicInfo({ role="button" > - +
diff --git a/app/components/rules/new/ExtractionSettings.tsx b/app/components/rules/new/ExtractionSettings.tsx index cd115a2..cba58b7 100644 --- a/app/components/rules/new/ExtractionSettings.tsx +++ b/app/components/rules/new/ExtractionSettings.tsx @@ -48,10 +48,19 @@ export function ExtractionSettings({ vlmFieldTypeOptions = EVALUATION_OPTIONS.vlmFieldTypeOptions, }: ExtractionSettingsProps) { + // 多实体抽取开关状态 + const [multiEntityEnabled, setMultiEntityEnabled] = useState( + initialData?.extraction_config?.multi_entity?.enabled ?? false + ); + // 核心数据状态 const [formData, setFormData] = useState({ // 字段配置 extraction_config: { + multi_entity: initialData?.extraction_config?.multi_entity ?? { + enabled: false, + expand_mode: 'awareness', + }, llm: initialData?.extraction_config?.llm ?? { fields: [], prompt_setting: { @@ -488,6 +497,10 @@ export function ExtractionSettings({ const updatedFormData = { ...formData, extraction_config: { + multi_entity: { + enabled: multiEntityEnabled, + expand_mode: 'awareness' as const + }, llm: { fields: fields.llm, prompt_setting: { @@ -562,12 +575,46 @@ export function ExtractionSettings({ } }; + // 处理多实体抽取开关变化 + const handleMultiEntityToggle = () => { + const newValue = !multiEntityEnabled; + setMultiEntityEnabled(newValue); + setHasPendingChanges(true); + }; + return (

抽取设置

+ {/* 多实体抽取开关 */} +
+
+
+ +
+ 多实体抽取 + 启用后,系统将按实体展开字段进行抽取(AI感知模式) +
+
+ + + +
+
+
+ 返回 + )} {showSaveButton && (