feat:替换 Dify 为自建 RAG去实现
1、修复了若干无权限时的失败提示语 2、新增了一个生成后续建议问题的功能 3、重构了知识问答部分的权限管理模块 4、修复了若干渲染不恰当的样式渲染
This commit is contained in:
@@ -13,6 +13,7 @@ interface ChatMessageProps {
|
||||
onFeedback?: (messageId: string, feedback: Feedbacktype) => void;
|
||||
isResponding?: boolean;
|
||||
onRegenerate?: (messageId: string) => void;
|
||||
onSuggestedQuestionClick?: (question: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -22,7 +23,8 @@ export default function ChatMessage({
|
||||
message,
|
||||
onFeedback,
|
||||
isResponding = false,
|
||||
onRegenerate
|
||||
onRegenerate,
|
||||
onSuggestedQuestionClick,
|
||||
}: ChatMessageProps) {
|
||||
const [feedback, setFeedback] = useState<'like' | 'dislike' | null>(
|
||||
message.feedback?.rating || null
|
||||
@@ -124,28 +126,51 @@ export default function ChatMessage({
|
||||
};
|
||||
|
||||
/**
|
||||
* 渲染建议问题
|
||||
* 渲染建议问题(继续探索)
|
||||
*/
|
||||
const renderSuggestedQuestions = () => {
|
||||
if (!suggestedQuestions || suggestedQuestions.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="suggested-questions">
|
||||
<div className="text-sm text-gray-500 mb-2">建议问题:</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="suggested-questions" style={{ marginTop: 12, paddingTop: 12, borderTop: '1px solid #f0f0f0' }}>
|
||||
<div style={{ fontSize: 11, color: '#8c8c8c', marginBottom: 8, display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<i className="ri-compass-3-line" />
|
||||
<span>继续探索</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{suggestedQuestions.map((question, index) => (
|
||||
<Button
|
||||
<button
|
||||
key={index}
|
||||
size="small"
|
||||
type="dashed"
|
||||
className="question-button text-left"
|
||||
onClick={() => {
|
||||
// 这里可以添加点击建议问题的处理逻辑
|
||||
// console.log('Suggested question clicked:', question);
|
||||
onClick={() => onSuggestedQuestionClick?.(question)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
padding: '6px 10px',
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: 6,
|
||||
background: '#fafafa',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'left',
|
||||
fontSize: 13,
|
||||
color: '#595959',
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = '#00684a';
|
||||
e.currentTarget.style.color = '#00684a';
|
||||
e.currentTarget.style.background = 'rgba(0,104,74,0.04)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = '#e8e8e8';
|
||||
e.currentTarget.style.color = '#595959';
|
||||
e.currentTarget.style.background = '#fafafa';
|
||||
}}
|
||||
>
|
||||
{question}
|
||||
</Button>
|
||||
<i className="ri-search-line" style={{ color: '#8c8c8c', flexShrink: 0, fontSize: 12 }} />
|
||||
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{question}</span>
|
||||
<i className="ri-arrow-right-line" style={{ color: '#bfbfbf', flexShrink: 0, fontSize: 12 }} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -198,10 +223,14 @@ export default function ChatMessage({
|
||||
<div className="flex items-start gap-2">
|
||||
{/* 消息内容 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{isAnswer ? renderAnswerContent() : (
|
||||
{isAnswer ? (
|
||||
<>
|
||||
{renderAnswerContent()}
|
||||
{!isResponding && renderSuggestedQuestions()}
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
<Markdown content={content} />
|
||||
{/* {renderImages(message_files)} */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useBoolean, useGetState } from 'ahooks';
|
||||
import { Layout, theme } from 'antd';
|
||||
import { Layout } from 'antd';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import ChatInput from './chat-input';
|
||||
import ChatMessage from './chat-message';
|
||||
@@ -8,6 +8,7 @@ import ChatSidebar, { type ChatSidebarRef } from './sidebar';
|
||||
import type { ChatItem, ConversationItem } from '~/api/dify-chat';
|
||||
import { fetchAppParams, fetchChatList, fetchConversations } from '~/api/dify-chat';
|
||||
import { CHAT_CONFIG } from '../../config/chat';
|
||||
import { usePermission } from '~/hooks/usePermission';
|
||||
import useChatMessage from '../../hooks/use-chat-message';
|
||||
import useConversation from '../../hooks/use-conversation';
|
||||
import { useChatApps } from '../../hooks/dify-chat-apps/useChatApps';
|
||||
@@ -28,29 +29,34 @@ interface ChatTheme {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取主题token - 避免在SSR环境中调用
|
||||
* 主题配置常量
|
||||
* 避免 SSR 环境下调用 theme.useToken() 导致 CSS-in-JS 注入报错
|
||||
*/
|
||||
function useChatTheme(): ChatTheme {
|
||||
// Ant Design的theme.useToken()必须在组件顶层调用,不能放在useEffect中
|
||||
const antdToken = typeof window !== 'undefined' ? theme.useToken().token : null;
|
||||
|
||||
return {
|
||||
colorBgContainer: antdToken?.colorBgContainer || '#ffffff',
|
||||
borderRadiusLG: antdToken?.borderRadiusLG || 8,
|
||||
};
|
||||
}
|
||||
const CHAT_THEME: ChatTheme = {
|
||||
colorBgContainer: '#ffffff',
|
||||
borderRadiusLG: 8,
|
||||
};
|
||||
|
||||
/**
|
||||
* 主聊天组件
|
||||
* 实现单页面应用模式,参考webapp-conversation的初始化逻辑
|
||||
*/
|
||||
export default function Chat() {
|
||||
// SSR 兼容:Ant Design 的 CSS-in-JS 在 Remix SSR 环境下
|
||||
// hydration 时会因找不到样式容器而报错,需要客户端渲染
|
||||
const [mounted, setMounted] = useState(false);
|
||||
useEffect(() => { setMounted(true); }, []);
|
||||
|
||||
// 权限检查
|
||||
const { hasPermission: checkPerm } = usePermission();
|
||||
const canChat = checkPerm('dify:chat:use');
|
||||
|
||||
// 侧边栏状态
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
// 获取主题配置,避免SSR错误
|
||||
const { colorBgContainer, borderRadiusLG } = useChatTheme();
|
||||
// 主题配置
|
||||
const { colorBgContainer, borderRadiusLG } = CHAT_THEME;
|
||||
|
||||
// 对话应用管理
|
||||
const {
|
||||
@@ -162,8 +168,11 @@ export default function Chat() {
|
||||
// 应用状态
|
||||
const [appUnavailable, setAppUnavailable] = useState<boolean>(false);
|
||||
const [isUnknownReason, setIsUnknownReason] = useState<boolean>(false);
|
||||
const [conversationPermissionDenied, setConversationPermissionDenied] = useState<boolean>(false);
|
||||
const [inited, setInited] = useState<boolean>(false);
|
||||
const [promptConfig, setPromptConfig] = useState<any>(null);
|
||||
// 防止重复初始化
|
||||
const [initializing, setInitializing] = useState<boolean>(false);
|
||||
|
||||
// 会话状态管理
|
||||
const [conversationIdChangeBecauseOfNew, setConversationIdChangeBecauseOfNew, getConversationIdChangeBecauseOfNew] = useGetState(false);
|
||||
@@ -507,7 +516,8 @@ export default function Chat() {
|
||||
};
|
||||
|
||||
/**
|
||||
* 组件初始化 - 参考webapp-conversation的逻辑
|
||||
* 组件初始化 - 等待 currentChatApp 就绪后再获取会话列表
|
||||
* 确保按当前应用过滤会话,避免不同应用的会话混在一起
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!hasSetAppConfig) {
|
||||
@@ -516,14 +526,28 @@ export default function Chat() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 必须等 currentChatApp 就绪,否则不知道该获取哪个应用的会话
|
||||
if (!currentChatApp || initializing || inited) {
|
||||
return;
|
||||
}
|
||||
|
||||
setInitializing(true);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
// console.log('🚀 开始初始化聊天应用...');
|
||||
console.log('🚀 [Chat] 开始初始化,当前应用:', currentChatApp.app_name, currentChatApp.app_id);
|
||||
|
||||
// 并行获取会话列表和应用参数
|
||||
// 用当前应用的 appId 获取会话列表(失败时降级为空列表,不阻塞初始化)
|
||||
const [conversationData, appParams] = await Promise.all([
|
||||
fetchConversations(),
|
||||
fetchAppParams()
|
||||
fetchConversations(currentChatApp.app_id).catch(err => {
|
||||
console.warn('⚠️ [Chat] 获取会话列表失败(权限不足或网络问题),降级为空列表:', err.message);
|
||||
setConversationPermissionDenied(true);
|
||||
return { data: [] };
|
||||
}),
|
||||
fetchAppParams().catch(err => {
|
||||
console.warn('⚠️ [Chat] 获取应用参数失败,使用默认值:', err.message);
|
||||
return { data: { user_input_form: [], opening_statement: '' } };
|
||||
}),
|
||||
]);
|
||||
|
||||
console.log('📋 [Chat] 获取到的数据:', { conversationData, appParams });
|
||||
@@ -532,13 +556,8 @@ export default function Chat() {
|
||||
const conversations = (conversationData as any).data || [];
|
||||
console.log('📋 [Chat] 会话列表:', conversations);
|
||||
|
||||
if ((conversationData as any).error) {
|
||||
console.error('❌ [Chat] 获取会话列表失败:', (conversationData as any).error);
|
||||
throw new Error((conversationData as any).error);
|
||||
}
|
||||
|
||||
// 处理当前会话ID
|
||||
const _conversationId = getConversationIdFromStorage(CHAT_CONFIG.APP_ID);
|
||||
const _conversationId = getConversationIdFromStorage(currentChatApp.app_id);
|
||||
const isNotNewConversation = conversations.some((item: ConversationItem) => item.id === _conversationId);
|
||||
|
||||
console.log('💾 [Chat] 初始化 - 本地存储的会话ID:', {
|
||||
@@ -568,15 +587,15 @@ export default function Chat() {
|
||||
// 如果存在有效的会话ID,则设置为当前会话
|
||||
if (isNotNewConversation) {
|
||||
console.log('🎯 [Chat] 初始化 - 设置当前会话ID:', _conversationId);
|
||||
setCurrConversationId(_conversationId, CHAT_CONFIG.APP_ID, false);
|
||||
setCurrConversationId(_conversationId, currentChatApp.app_id, false);
|
||||
} else {
|
||||
// 如果localStorage为空或会话不存在,自动创建新会话
|
||||
console.log('🆕 [Chat] 初始化 - localStorage为空或会话不存在,创建新会话');
|
||||
setCurrConversationId('-1', CHAT_CONFIG.APP_ID, false);
|
||||
setCurrConversationId('-1', currentChatApp.app_id, false);
|
||||
}
|
||||
|
||||
setInited(true);
|
||||
// console.log('✅ 聊天应用初始化完成');
|
||||
console.log('✅ [Chat] 聊天应用初始化完成');
|
||||
} catch (e: any) {
|
||||
console.error('❌ 初始化失败:', e);
|
||||
if (e.status === 404) {
|
||||
@@ -587,7 +606,7 @@ export default function Chat() {
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
}, [currentChatApp]);
|
||||
|
||||
// 监听会话切换
|
||||
useEffect(() => {
|
||||
@@ -663,6 +682,18 @@ export default function Chat() {
|
||||
const conversationName = currConversationInfo?.name || '新对话';
|
||||
const conversationIntroduction = currConversationInfo?.introduction || '';
|
||||
|
||||
// SSR 兼容:等客户端挂载后再渲染 Ant Design 组件
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div>
|
||||
<p className="text-gray-600">正在加载...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout style={{ height: '100%', display: 'flex', flexDirection: 'row', position: 'relative' }}>
|
||||
{/* 移动端遮罩层 - 点击可收起侧边栏 */}
|
||||
@@ -704,6 +735,7 @@ export default function Chat() {
|
||||
loadingChatApps={loadingChatApps}
|
||||
currentChatApp={currentChatApp}
|
||||
onChatAppChange={handleChatAppChange}
|
||||
conversationReadOnly={conversationPermissionDenied}
|
||||
/>
|
||||
|
||||
{/* 主内容区域 */}
|
||||
@@ -738,6 +770,7 @@ export default function Chat() {
|
||||
message={item}
|
||||
isResponding={isResponding && item.id === chatList[chatList.length - 1]?.id}
|
||||
onFeedback={handleFeedback}
|
||||
onSuggestedQuestionClick={(question) => handleSendMessage(question)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -747,8 +780,8 @@ export default function Chat() {
|
||||
<div className="flex-shrink-0 bg-white">
|
||||
<ChatInput
|
||||
onSendMessage={handleSendMessage}
|
||||
disabled={isResponding}
|
||||
placeholder="有什么我能帮您的吗?"
|
||||
disabled={isResponding || !canChat}
|
||||
placeholder={canChat ? "有什么我能帮您的吗?" : "您没有发送消息的权限"}
|
||||
onStop={stopResponding}
|
||||
isResponding={isResponding}
|
||||
/>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Button, Dropdown, Input, Layout, Menu, Modal, Tooltip, message, theme,
|
||||
import { forwardRef, useImperativeHandle, useMemo, useState } from 'react';
|
||||
import type { ChatApp } from '~/api/dify-chat-apps/types';
|
||||
import type { ConversationItem } from '~/api/dify-chat';
|
||||
import { usePermission } from '~/hooks/usePermission';
|
||||
import { deleteConversation, renameConversation } from '~/api/dify-chat';
|
||||
import '../../styles/components/chat-with-llm/sidebar.css';
|
||||
|
||||
@@ -32,6 +33,8 @@ interface ChatSidebarProps {
|
||||
currentChatApp: ChatApp | null;
|
||||
onChatAppChange: (appId: string) => void;
|
||||
onConversationRenamed?: (conversationId: string, newName: string) => void;
|
||||
/** 会话列表权限被拒绝时,隐藏重命名/删除按钮 */
|
||||
conversationReadOnly?: boolean;
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法接口
|
||||
@@ -55,7 +58,10 @@ const ChatSidebar = forwardRef<ChatSidebarRef, ChatSidebarProps>(({
|
||||
onNewConversation,
|
||||
onConversationDeleted,
|
||||
onConversationRenamed,
|
||||
conversationReadOnly = false,
|
||||
}, ref) => {
|
||||
const { hasPermission } = usePermission();
|
||||
const canDeleteConversation = hasPermission('dify:conversation:delete');
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [renameModalVisible, setRenameModalVisible] = useState(false);
|
||||
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
||||
@@ -208,24 +214,24 @@ const ChatSidebar = forwardRef<ChatSidebarRef, ChatSidebarProps>(({
|
||||
<span className="truncate flex-1" title={conv.name}>
|
||||
{conv.name}
|
||||
</span>
|
||||
{!collapsed && (
|
||||
{!collapsed && (!conversationReadOnly || canDeleteConversation) && (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
...(!conversationReadOnly ? [{
|
||||
key: 'rename',
|
||||
icon: <EditOutlined />,
|
||||
label: '重命名',
|
||||
onClick: () => handleRename(conv),
|
||||
},
|
||||
{
|
||||
}] : []),
|
||||
...(canDeleteConversation ? [{
|
||||
key: 'delete',
|
||||
icon: <DeleteOutlined />,
|
||||
label: '删除',
|
||||
danger: true,
|
||||
onClick: () => handleDeleteClick(conv),
|
||||
},
|
||||
],
|
||||
}] : []),
|
||||
].filter(Boolean),
|
||||
}}
|
||||
trigger={['click']}
|
||||
placement="bottomRight"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
Flex,
|
||||
Typography,
|
||||
Popconfirm,
|
||||
Spin,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import {
|
||||
@@ -35,8 +34,7 @@ import {
|
||||
CheckCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useAreaDatasetConfig } from '~/hooks/use-area-dataset-config';
|
||||
import { fetchDatasets } from '~/api/dify-dataset/api/datasetApi';
|
||||
import type { Dataset as DifyDataset } from '~/api/dify-dataset/type';
|
||||
import { usePermission } from '~/hooks/usePermission';
|
||||
import type { AreaDataset } from '~/api/v3/dify/area-datasets';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
@@ -90,13 +88,11 @@ export default function AreaDatasetConfig() {
|
||||
canManageDataset,
|
||||
} = useAreaDatasetConfig();
|
||||
|
||||
const { userRole: rawUserRole, userArea: rawUserArea } = usePermission();
|
||||
const isProvincialAdmin = rawUserRole === 'provincial_admin';
|
||||
|
||||
// 内部状态
|
||||
const [form] = Form.useForm();
|
||||
const [difyDatasets, setDifyDatasets] = useState<DifyDataset[]>([]);
|
||||
const [difyDatasetsLoading, setDifyDatasetsLoading] = useState<boolean>(false);
|
||||
const [difyDatasetsTotal, setDifyDatasetsTotal] = useState<number>(0);
|
||||
const [difyDatasetsPage, setDifyDatasetsPage] = useState<number>(1);
|
||||
const [isLoadingDifyDatasets, setIsLoadingDifyDatasets] = useState<boolean>(false);
|
||||
|
||||
// ==================== Effects ====================
|
||||
|
||||
@@ -107,7 +103,6 @@ export default function AreaDatasetConfig() {
|
||||
if (record) {
|
||||
form.setFieldsValue({
|
||||
area: record.area,
|
||||
dataset_id: record.dataset_id,
|
||||
dataset_name: record.dataset_name,
|
||||
dataset_description: record.dataset_description,
|
||||
is_public: record.is_public,
|
||||
@@ -116,51 +111,14 @@ export default function AreaDatasetConfig() {
|
||||
});
|
||||
}
|
||||
} else if (!editingId && modalVisible) {
|
||||
// 新增时重置表单
|
||||
form.resetFields();
|
||||
loadDifyDatasets(); // 加载Dify知识库列表
|
||||
// 非省级管理员自动填充地区
|
||||
if (!isProvincialAdmin && rawUserArea) {
|
||||
form.setFieldValue('area', rawUserArea);
|
||||
}
|
||||
}
|
||||
}, [editingId, modalVisible, datasets, form]);
|
||||
|
||||
// ==================== Dify Datasets Loading ====================
|
||||
|
||||
/**
|
||||
* 从Dify API加载知识库列表
|
||||
*/
|
||||
const loadDifyDatasets = async (pageNum: number = 1) => {
|
||||
if (isLoadingDifyDatasets) return;
|
||||
|
||||
setIsLoadingDifyDatasets(true);
|
||||
try {
|
||||
const response = await fetchDatasets(pageNum, 20);
|
||||
setDifyDatasets(response.data);
|
||||
setDifyDatasetsTotal(response.total);
|
||||
setDifyDatasetsPage(pageNum);
|
||||
setDifyDatasetsLoading(false);
|
||||
} catch (error: any) {
|
||||
console.error('加载Dify知识库列表失败:', error);
|
||||
message.error('加载Dify知识库列表失败');
|
||||
setDifyDatasetsLoading(false);
|
||||
} finally {
|
||||
setIsLoadingDifyDatasets(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Dify数据集选择器滚动加载
|
||||
*/
|
||||
const handleDatasetSelectScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||
const { target } = e;
|
||||
const { scrollTop, scrollHeight, clientHeight } = target as HTMLDivElement;
|
||||
|
||||
// 滚动到底部且还有更多数据时加载下一页
|
||||
if (scrollHeight - scrollTop === clientHeight &&
|
||||
difyDatasets.length < difyDatasetsTotal &&
|
||||
!difyDatasetsLoading) {
|
||||
loadDifyDatasets(difyDatasetsPage + 1);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== Event Handlers ====================
|
||||
|
||||
/**
|
||||
@@ -168,13 +126,12 @@ export default function AreaDatasetConfig() {
|
||||
*/
|
||||
const handleCreateClick = () => {
|
||||
if (!canManageDataset) {
|
||||
message.error('您没有创建知识库绑定的权限');
|
||||
message.error('您没有创建知识库的权限');
|
||||
return;
|
||||
}
|
||||
setEditingId(null);
|
||||
setModalVisible(true);
|
||||
form.resetFields();
|
||||
loadDifyDatasets();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -208,6 +165,24 @@ export default function AreaDatasetConfig() {
|
||||
* 处理表单提交
|
||||
*/
|
||||
const handleFormSubmit = async (values: any) => {
|
||||
// 编辑时检查 is_default 是否从 false 变为 true
|
||||
if (editingId && values.is_default) {
|
||||
const record = datasets.find((item) => item.id === editingId);
|
||||
if (record && !record.is_default) {
|
||||
Modal.confirm({
|
||||
title: '切换默认知识库',
|
||||
content: '确认将此知识库设为默认?该地区的对话助手将自动绑定此知识库进行问答。',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: () => doSubmit(values),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
await doSubmit(values);
|
||||
};
|
||||
|
||||
const doSubmit = async (values: any) => {
|
||||
let success = false;
|
||||
|
||||
if (editingId) {
|
||||
@@ -269,6 +244,7 @@ export default function AreaDatasetConfig() {
|
||||
key: 'dataset_name',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
align: 'center',
|
||||
render: (text: string) => (
|
||||
<Tooltip title={text}>
|
||||
<Text style={{ color: colors.text }} strong>
|
||||
@@ -277,25 +253,26 @@ export default function AreaDatasetConfig() {
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '知识库ID',
|
||||
dataIndex: 'dataset_id',
|
||||
key: 'dataset_id',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
render: (text: string) => (
|
||||
<Tooltip title={text}>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
{text.substring(0, 8)}...{text.substring(text.length - 4)}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
// {
|
||||
// title: '知识库ID',
|
||||
// dataIndex: 'dataset_id',
|
||||
// key: 'dataset_id',
|
||||
// width: 200,
|
||||
// ellipsis: true,
|
||||
// render: (text: string) => (
|
||||
// <Tooltip title={text}>
|
||||
// <Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
// {text.substring(0, 8)}...{text.substring(text.length - 4)}
|
||||
// </Text>
|
||||
// </Tooltip>
|
||||
// ),
|
||||
// },
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'dataset_description',
|
||||
key: 'dataset_description',
|
||||
ellipsis: true,
|
||||
align: 'center',
|
||||
render: (text: string) =>
|
||||
text ? (
|
||||
<Tooltip title={text}>
|
||||
@@ -319,7 +296,7 @@ export default function AreaDatasetConfig() {
|
||||
</Tag>
|
||||
)}
|
||||
{record.is_default && (
|
||||
<Tag color={colors.primary} style={{ color: '#fff' }}>
|
||||
<Tag color={colors.primary} style={{ color: '#00684a' }}>
|
||||
默认
|
||||
</Tag>
|
||||
)}
|
||||
@@ -330,7 +307,7 @@ export default function AreaDatasetConfig() {
|
||||
title: '排序',
|
||||
dataIndex: 'sort_order',
|
||||
key: 'sort_order',
|
||||
width: 70,
|
||||
width: 170,
|
||||
align: 'center' as const,
|
||||
sorter: (a: AreaDataset, b: AreaDataset) => a.sort_order - b.sort_order,
|
||||
},
|
||||
@@ -364,29 +341,37 @@ export default function AreaDatasetConfig() {
|
||||
key: 'actions',
|
||||
width: 120,
|
||||
fixed: 'right' as const,
|
||||
render: (_: any, record: AreaDataset) => (
|
||||
<Space size="small">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEditClick(record)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定删除?"
|
||||
description="删除后该地区的用户将无法访问此知识库"
|
||||
onConfirm={() => handleDeleteClick(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button type="link" danger size="small" icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
render: (_: any, record: AreaDataset) => {
|
||||
// 市级管理员只能编辑自己地区的知识库
|
||||
const canEdit = isProvincialAdmin || record.area === rawUserArea;
|
||||
return (
|
||||
<Space size="small">
|
||||
{canEdit && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEditClick(record)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
)}
|
||||
{isProvincialAdmin && (
|
||||
<Popconfirm
|
||||
title="确定删除?"
|
||||
description="删除后该地区的用户将无法访问此知识库"
|
||||
onConfirm={() => handleDeleteClick(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button type="link" danger size="small" icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@@ -432,7 +417,7 @@ export default function AreaDatasetConfig() {
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleCreateClick}
|
||||
>
|
||||
新增绑定
|
||||
新增知识库
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
@@ -503,7 +488,7 @@ export default function AreaDatasetConfig() {
|
||||
|
||||
{/* 新增/编辑对话框 */}
|
||||
<Modal
|
||||
title={editingId ? '编辑知识库绑定' : '新增知识库绑定'}
|
||||
title={editingId ? '编辑知识库' : '新增知识库'}
|
||||
open={modalVisible}
|
||||
onOk={() => form.submit()}
|
||||
onCancel={handleFormCancel}
|
||||
@@ -529,55 +514,12 @@ export default function AreaDatasetConfig() {
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择地区"
|
||||
disabled={!!editingId} // 编辑时禁用
|
||||
options={Array.isArray(areas) ? areas.map((area) => ({
|
||||
label: area,
|
||||
value: area,
|
||||
})) : []}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* Dify知识库选择(仅新增时可选) */}
|
||||
<Form.Item
|
||||
name="dataset_id"
|
||||
label="Dify知识库"
|
||||
rules={[{ required: true, message: '请选择Dify知识库' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择或输入知识库ID"
|
||||
disabled={!!editingId}
|
||||
loading={difyDatasetsLoading}
|
||||
onPopupScroll={handleDatasetSelectScroll}
|
||||
dropdownRender={(menu) => (
|
||||
<div>
|
||||
{menu}
|
||||
{difyDatasets.length < difyDatasetsTotal && (
|
||||
<div style={{ textAlign: 'center', padding: '8px' }}>
|
||||
<Spin size="small" />
|
||||
<Text style={{ marginLeft: '8px' }} type="secondary">
|
||||
加载中...
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
onDropdownVisibleChange={(open) => {
|
||||
if (open && !editingId) {
|
||||
loadDifyDatasets();
|
||||
}
|
||||
}}
|
||||
options={difyDatasets.map((ds) => ({
|
||||
label: (
|
||||
<Flex vertical>
|
||||
<Text strong>{ds.name}</Text>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
ID: {ds.id}
|
||||
</Text>
|
||||
</Flex>
|
||||
),
|
||||
value: ds.id,
|
||||
}))}
|
||||
styles={{ popup: { root: { maxHeight: '300px' } } }}
|
||||
options={
|
||||
isProvincialAdmin
|
||||
? (Array.isArray(areas) ? areas.map((area) => ({ label: area, value: area })) : [])
|
||||
: (rawUserArea ? [{ label: rawUserArea, value: rawUserArea }] : [])
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -594,7 +536,7 @@ export default function AreaDatasetConfig() {
|
||||
</Form.Item>
|
||||
|
||||
{/* 知识库描述 */}
|
||||
{/* <Form.Item
|
||||
<Form.Item
|
||||
name="dataset_description"
|
||||
label="知识库描述"
|
||||
>
|
||||
@@ -603,36 +545,33 @@ export default function AreaDatasetConfig() {
|
||||
rows={3}
|
||||
maxLength={500}
|
||||
/>
|
||||
</Form.Item> */}
|
||||
</Form.Item>
|
||||
|
||||
{/* 高级设置折叠面板 */}
|
||||
{/* 高级设置 */}
|
||||
<div style={{ marginTop: '24px' }}>
|
||||
<Text strong style={{ color: colors.text, display: 'block', marginBottom: '16px' }}>
|
||||
高级设置
|
||||
</Text>
|
||||
|
||||
<Flex gap="24px">
|
||||
{/* 是否公开 */}
|
||||
<Form.Item
|
||||
name="is_public"
|
||||
label="公开知识库"
|
||||
valuePropName="checked"
|
||||
tooltip="公开后所有地区的用户都可以访问此知识库"
|
||||
tooltip="公开后,其他地区的用户可以在对话中选择此知识库的问答助手"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
{/* 是否默认 */}
|
||||
<Form.Item
|
||||
name="is_default"
|
||||
label="默认知识库"
|
||||
valuePropName="checked"
|
||||
tooltip="设置为该地区的默认知识库,用户优先使用"
|
||||
tooltip="设为默认后,该地区的对话助手将自动切换为使用此知识库"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
{/* 排序顺序 */}
|
||||
<Form.Item
|
||||
name="sort_order"
|
||||
label="排序顺序"
|
||||
|
||||
@@ -2,6 +2,7 @@ import { CheckCircleFilled, QuestionCircleOutlined, SaveOutlined } from '@ant-de
|
||||
import { Button, Card, Checkbox, Descriptions, Divider, InputNumber, Select, Slider, Spin, Tag, Tooltip } from 'antd';
|
||||
import { useDatasetSettings, type SearchMethod } from '~/hooks/dify-dataset-manager/dataset-settings';
|
||||
import type { DatasetSettingsProps } from '~/types/dify-dataset-manager/dataset-settings';
|
||||
import { usePermission } from '~/hooks/usePermission';
|
||||
|
||||
// 检索方式选项
|
||||
const SEARCH_METHOD_OPTIONS: { label: string; value: SearchMethod; description: string }[] = [
|
||||
@@ -19,6 +20,7 @@ const SEARCH_METHOD_OPTIONS: { label: string; value: SearchMethod; description:
|
||||
export default function DatasetSettings({
|
||||
dataset,
|
||||
onDatasetUpdated,
|
||||
canEditDataset = true,
|
||||
}: DatasetSettingsProps) {
|
||||
const {
|
||||
saving,
|
||||
@@ -29,6 +31,9 @@ export default function DatasetSettings({
|
||||
updateRetrievalSettings,
|
||||
} = useDatasetSettings(dataset, onDatasetUpdated);
|
||||
|
||||
const { hasPermission } = usePermission();
|
||||
const canWrite = hasPermission('dify:settings:write') && canEditDataset;
|
||||
|
||||
// 是否需要显示 Reranking 提示(语义检索和混合检索需要,且强制开启)
|
||||
const showRerankingInfo = retrievalSettings.searchMethod === 'semantic_search' || retrievalSettings.searchMethod === 'hybrid_search';
|
||||
// 权重设置:由于 Reranking 强制开启,混合检索时由 Reranking 模型决定排序,不需要手动设置权重
|
||||
@@ -245,7 +250,7 @@ export default function DatasetSettings({
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="form-actions" style={{ marginTop: 24, display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
||||
<Button onClick={handleReset} disabled={!hasChanges}>
|
||||
<Button onClick={handleReset} disabled={!hasChanges || !canWrite}>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
@@ -253,7 +258,7 @@ export default function DatasetSettings({
|
||||
icon={<SaveOutlined />}
|
||||
onClick={handleSave}
|
||||
loading={saving}
|
||||
disabled={!hasChanges}
|
||||
disabled={!hasChanges || !canWrite}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
EyeOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useDocumentDetail } from '~/hooks/dify-dataset-manager/document-detail';
|
||||
import { usePermission } from '~/hooks/usePermission';
|
||||
import type { DocumentDetailProps } from '~/types/dify-dataset-manager/document-detail';
|
||||
import { INDEXING_STATUS_CONFIG } from '~/types/dify-dataset-manager/document-detail';
|
||||
|
||||
@@ -26,6 +27,7 @@ import { INDEXING_STATUS_CONFIG } from '~/types/dify-dataset-manager/document-de
|
||||
export default function DocumentDetail({
|
||||
datasetId,
|
||||
document,
|
||||
canEditDataset = true,
|
||||
}: DocumentDetailProps) {
|
||||
const {
|
||||
settings,
|
||||
@@ -41,6 +43,10 @@ export default function DocumentDetail({
|
||||
handleSaveAndProcess,
|
||||
} = useDocumentDetail(datasetId, document);
|
||||
|
||||
const { hasPermission } = usePermission();
|
||||
const canManageDoc = hasPermission('dify:document:manage') && canEditDataset;
|
||||
const readOnly = !canManageDoc || isProcessing;
|
||||
|
||||
if (!document) {
|
||||
return (
|
||||
<div className="document-detail-empty">
|
||||
@@ -83,7 +89,7 @@ export default function DocumentDetail({
|
||||
value={settings.separator}
|
||||
onChange={(e) => updateSettings('separator', e.target.value)}
|
||||
placeholder="\n\n"
|
||||
disabled={isProcessing}
|
||||
disabled={readOnly}
|
||||
className="setting-input"
|
||||
/>
|
||||
</div>
|
||||
@@ -102,7 +108,7 @@ export default function DocumentDetail({
|
||||
onChange={(value) => updateSettings('maxTokens', value || 500)}
|
||||
min={100}
|
||||
max={4000}
|
||||
disabled={isProcessing}
|
||||
disabled={readOnly}
|
||||
className="setting-input-number"
|
||||
/>
|
||||
<span className="input-suffix">characters</span>
|
||||
@@ -123,7 +129,7 @@ export default function DocumentDetail({
|
||||
onChange={(value) => updateSettings('chunkOverlap', value || 50)}
|
||||
min={0}
|
||||
max={500}
|
||||
disabled={isProcessing}
|
||||
disabled={readOnly}
|
||||
className="setting-input-number"
|
||||
/>
|
||||
<span className="input-suffix">characters</span>
|
||||
@@ -141,7 +147,7 @@ export default function DocumentDetail({
|
||||
<Checkbox
|
||||
checked={settings.removeExtraSpaces}
|
||||
onChange={(e) => updateSettings('removeExtraSpaces', e.target.checked)}
|
||||
disabled={isProcessing}
|
||||
disabled={readOnly}
|
||||
>
|
||||
替换掉连续的空格、换行符和制表符
|
||||
</Checkbox>
|
||||
@@ -149,7 +155,7 @@ export default function DocumentDetail({
|
||||
<Checkbox
|
||||
checked={settings.removeUrlsEmails}
|
||||
onChange={(e) => updateSettings('removeUrlsEmails', e.target.checked)}
|
||||
disabled={isProcessing}
|
||||
disabled={readOnly}
|
||||
>
|
||||
删除所有 URL 和电子邮件地址
|
||||
</Checkbox>
|
||||
@@ -163,16 +169,16 @@ export default function DocumentDetail({
|
||||
<h3 className="section-title">索引方式</h3>
|
||||
<div className="index-options">
|
||||
<div
|
||||
className={`index-option ${settings.indexingTechnique === 'high_quality' ? 'active' : ''} ${isProcessing ? 'disabled' : ''}`}
|
||||
onClick={() => !isProcessing && updateSettings('indexingTechnique', 'high_quality')}
|
||||
className={`index-option ${settings.indexingTechnique === 'high_quality' ? 'active' : ''} ${readOnly ? 'disabled' : ''}`}
|
||||
onClick={() => !readOnly && updateSettings('indexingTechnique', 'high_quality')}
|
||||
>
|
||||
<span className="option-radio"></span>
|
||||
<span className="option-label">高质量</span>
|
||||
<span className="option-badge recommended">推荐</span>
|
||||
</div>
|
||||
<div
|
||||
className={`index-option ${settings.indexingTechnique === 'economy' ? 'active' : ''} ${isProcessing ? 'disabled' : ''}`}
|
||||
onClick={() => !isProcessing && updateSettings('indexingTechnique', 'economy')}
|
||||
className={`index-option ${settings.indexingTechnique === 'economy' ? 'active' : ''} ${readOnly ? 'disabled' : ''}`}
|
||||
onClick={() => !readOnly && updateSettings('indexingTechnique', 'economy')}
|
||||
>
|
||||
<span className="option-radio"></span>
|
||||
<span className="option-label">经济</span>
|
||||
@@ -190,29 +196,33 @@ export default function DocumentDetail({
|
||||
>
|
||||
预览块
|
||||
</Button>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleReset}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
{canManageDoc && (
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleReset}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 保存并处理按钮 */}
|
||||
<div className="save-actions">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleSaveAndProcess}
|
||||
loading={saving}
|
||||
disabled={isProcessing}
|
||||
block
|
||||
>
|
||||
{isProcessing ? '处理中...' : '保存并处理'}
|
||||
</Button>
|
||||
</div>
|
||||
{canManageDoc && (
|
||||
<div className="save-actions">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleSaveAndProcess}
|
||||
loading={saving}
|
||||
disabled={isProcessing}
|
||||
block
|
||||
>
|
||||
{isProcessing ? '处理中...' : '保存并处理'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 右侧预览区域 */}
|
||||
|
||||
@@ -22,6 +22,7 @@ import type { ColumnsType } from 'antd/es/table';
|
||||
import type { Document, IndexingStatus } from '~/api/dify-dataset/type/documentTypes';
|
||||
import { useDocumentList } from '~/hooks/dify-dataset-manager/document-list';
|
||||
import type { DocumentListProps } from '~/types/dify-dataset-manager/document-list';
|
||||
import { usePermission } from '~/hooks/usePermission';
|
||||
import '../../styles/components/dify-dataset-manager/index.css';
|
||||
import DocumentUpload from './document-upload';
|
||||
|
||||
@@ -40,6 +41,7 @@ export default function DocumentList({
|
||||
onDocumentStatusChanged,
|
||||
onRefresh,
|
||||
onViewDocument,
|
||||
canEditDataset = true,
|
||||
}: DocumentListProps) {
|
||||
const {
|
||||
searchValue,
|
||||
@@ -57,6 +59,9 @@ export default function DocumentList({
|
||||
filterDocuments,
|
||||
} = useDocumentList(datasetId, onDocumentDeleted, onDocumentStatusChanged, onRefresh);
|
||||
|
||||
const { hasPermission } = usePermission();
|
||||
const canWrite = hasPermission('dify:document:manage') && canEditDataset;
|
||||
|
||||
// 过滤文档
|
||||
const filteredDocuments = filterDocuments(documents);
|
||||
|
||||
@@ -111,6 +116,7 @@ export default function DocumentList({
|
||||
<Switch
|
||||
size="small"
|
||||
checked={enabled}
|
||||
disabled={!canWrite}
|
||||
onChange={(checked) => handleToggleStatus(record.id, checked)}
|
||||
/>
|
||||
),
|
||||
@@ -136,24 +142,26 @@ export default function DocumentList({
|
||||
onClick={() => onViewDocument?.(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Popconfirm
|
||||
title="确定要删除这个文档吗?"
|
||||
description="删除后无法恢复"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<Tooltip title="删除">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
loading={deletingId === record.id}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
{canWrite && (
|
||||
<Popconfirm
|
||||
title="确定要删除这个文档吗?"
|
||||
description="删除后无法恢复"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<Tooltip title="删除">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
loading={deletingId === record.id}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
@@ -183,14 +191,16 @@ export default function DocumentList({
|
||||
loading={loading}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<CloudUploadOutlined />}
|
||||
onClick={handleUploadClick}
|
||||
disabled={!datasetId}
|
||||
>
|
||||
添加文件
|
||||
</Button>
|
||||
{canWrite && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<CloudUploadOutlined />}
|
||||
onClick={handleUploadClick}
|
||||
disabled={!datasetId}
|
||||
>
|
||||
添加文件
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import RetrieveTest from './retrieve-test';
|
||||
import DatasetSettings from './dataset-settings';
|
||||
import AreaDatasetConfig from './area-dataset-config';
|
||||
import { useDatasetManager } from '~/hooks/dify-dataset-manager';
|
||||
import { usePermission } from '~/hooks/usePermission';
|
||||
import '../../styles/components/dify-dataset-manager/index.css';
|
||||
|
||||
/**
|
||||
@@ -43,6 +44,10 @@ export default function DatasetManager() {
|
||||
handleDatasetChange,
|
||||
} = useDatasetManager();
|
||||
|
||||
// 判断当前用户是否能编辑当前知识库(省级管理员可编辑全部,市级管理员只能编辑本地区)
|
||||
const { userRole, userArea } = usePermission();
|
||||
const canEditDataset = userRole === 'provincial_admin' || ((dataset as any)?.area === userArea);
|
||||
|
||||
// 加载中状态
|
||||
if (!inited || loadingDataset) {
|
||||
return (
|
||||
@@ -80,6 +85,7 @@ export default function DatasetManager() {
|
||||
<DocumentDetail
|
||||
datasetId={dataset?.id || ''}
|
||||
document={selectedDocument}
|
||||
canEditDataset={canEditDataset}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -98,6 +104,7 @@ export default function DatasetManager() {
|
||||
onDocumentStatusChanged={handleDocumentStatusChanged}
|
||||
onRefresh={handleRefresh}
|
||||
onViewDocument={handleViewDocument}
|
||||
canEditDataset={canEditDataset}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -118,6 +125,7 @@ export default function DatasetManager() {
|
||||
<DatasetSettings
|
||||
dataset={dataset}
|
||||
onDatasetUpdated={handleDatasetUpdated}
|
||||
canEditDataset={canEditDataset}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
SwapOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { DatasetLayoutProps, MenuTab, MenuItem } from '~/types/dify-dataset-manager/layout';
|
||||
import { usePermission } from '~/hooks/usePermission';
|
||||
|
||||
/**
|
||||
* 知识库布局组件
|
||||
@@ -25,11 +26,13 @@ export default function DatasetLayout({
|
||||
loadingAvailableDatasets = false,
|
||||
onDatasetChange,
|
||||
}: DatasetLayoutProps) {
|
||||
const { hasPermission } = usePermission();
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
{ key: 'documents', icon: <FileTextOutlined />, label: '文档' },
|
||||
{ key: 'retrieve', icon: <SearchOutlined />, label: '召回测试' },
|
||||
{ key: 'area-config', icon: <AppstoreOutlined />, label: '配置管理' },
|
||||
{ key: 'settings', icon: <SettingOutlined />, label: '设置' },
|
||||
...(hasPermission('dify:dataset:read') ? [{ key: 'documents' as MenuTab, icon: <FileTextOutlined />, label: '文档' }] : []),
|
||||
...(hasPermission('dify:retrieve:test') ? [{ key: 'retrieve' as MenuTab, icon: <SearchOutlined />, label: '召回测试' }] : []),
|
||||
...(hasPermission('dify:config:manage') ? [{ key: 'area-config' as MenuTab, icon: <AppstoreOutlined />, label: '配置管理' }] : []),
|
||||
...(hasPermission('dify:settings:write') ? [{ key: 'settings' as MenuTab, icon: <SettingOutlined />, label: '设置' }] : []),
|
||||
];
|
||||
|
||||
// 是否显示知识库选择器(有多个知识库时显示)
|
||||
|
||||
Reference in New Issue
Block a user