feat: 添加对话应用选择和知识库切换功能
- 新增对话应用管理模块(dify-chat-apps),支持获取和切换对话应用 - 优化对话应用切换后自动刷新会话列表功能 - 知识库管理页面新增下拉选择器,支持切换不同知识库 - API 层支持 app_id 参数传递,实现多应用会话隔离 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import { fetchAppParams, fetchChatList, fetchConversations } from '~/api/dify-ch
|
||||
import { CHAT_CONFIG } from '../../config/chat';
|
||||
import useChatMessage from '../../hooks/use-chat-message';
|
||||
import useConversation from '../../hooks/use-conversation';
|
||||
import { useChatApps } from '../../hooks/dify-chat-apps/useChatApps';
|
||||
import '../../styles/components/chat-with-llm/index.css';
|
||||
|
||||
const { Content } = Layout;
|
||||
@@ -51,6 +52,14 @@ export default function Chat() {
|
||||
// 获取主题配置,避免SSR错误
|
||||
const { colorBgContainer, borderRadiusLG } = useChatTheme();
|
||||
|
||||
// 对话应用管理
|
||||
const {
|
||||
chatApps,
|
||||
loadingChatApps,
|
||||
currentChatApp,
|
||||
handleChatAppChange: originalHandleChatAppChange,
|
||||
} = useChatApps();
|
||||
|
||||
// 会话管理
|
||||
const {
|
||||
conversationList,
|
||||
@@ -335,7 +344,8 @@ export default function Chat() {
|
||||
message: message.substring(0, 50) + (message.length > 50 ? '...' : ''),
|
||||
currConversationId,
|
||||
isNewConversation,
|
||||
willSendConversationId: isNewConversation ? null : currConversationId
|
||||
willSendConversationId: isNewConversation ? null : currConversationId,
|
||||
appId: currentChatApp?.app_id
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -347,12 +357,13 @@ export default function Chat() {
|
||||
});
|
||||
}
|
||||
|
||||
// 使用 useChatMessage 钩子的 handleSend 方法
|
||||
// 使用 useChatMessage 钩子的 handleSend 方法,传递当前选中的应用 ID
|
||||
await handleSend(
|
||||
message,
|
||||
isNewConversation ? null : currConversationId,
|
||||
files,
|
||||
toServerInputs
|
||||
toServerInputs,
|
||||
currentChatApp?.app_id // 传递对话应用 ID
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
@@ -389,6 +400,50 @@ export default function Chat() {
|
||||
createNewChat();
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理对话应用切换
|
||||
* 切换应用后刷新加载对应的会话列表
|
||||
*/
|
||||
const handleChatAppChange = async (appId: string) => {
|
||||
console.log('🔄 [Chat] 切换对话应用:', appId);
|
||||
|
||||
// 调用原始的切换方法
|
||||
originalHandleChatAppChange(appId, async (app) => {
|
||||
console.log('🔄 [Chat] 应用已切换到:', app.app_name, '开始刷新会话列表...');
|
||||
|
||||
try {
|
||||
// 重新获取会话列表,传入新的应用ID获取该应用的会话
|
||||
const conversationData = await fetchConversations(app.app_id);
|
||||
const conversations = (conversationData as any).data || [];
|
||||
|
||||
console.log('📋 [Chat] 切换应用后获取到会话列表:', conversations.length, '条');
|
||||
|
||||
// 更新会话列表
|
||||
setConversationList(conversations);
|
||||
|
||||
// 清空当前聊天,创建新会话
|
||||
setChatList([]);
|
||||
setChatNotStarted();
|
||||
|
||||
// 如果有会话,选择第一个;否则创建新会话
|
||||
if (conversations.length > 0) {
|
||||
const firstConversation = conversations[0];
|
||||
setCurrConversationId(firstConversation.id, app.app_id, false);
|
||||
console.log('🎯 [Chat] 自动选择第一个会话:', firstConversation.id);
|
||||
} else {
|
||||
setCurrConversationId('-1', app.app_id, false);
|
||||
console.log('🆕 [Chat] 无会话,创建新会话');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ [Chat] 切换应用后刷新会话列表失败:', error);
|
||||
// 即使刷新失败,也清空当前状态
|
||||
setConversationList([]);
|
||||
setChatList([]);
|
||||
setCurrConversationId('-1', appId, false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理会话删除后的状态更新
|
||||
*/
|
||||
@@ -614,6 +669,10 @@ export default function Chat() {
|
||||
onNewConversation={handleNewConversation}
|
||||
onConversationDeleted={handleConversationDeleted}
|
||||
onConversationRenamed={handleConversationRenamed}
|
||||
chatApps={chatApps}
|
||||
loadingChatApps={loadingChatApps}
|
||||
currentChatApp={currentChatApp}
|
||||
onChatAppChange={handleChatAppChange}
|
||||
/>
|
||||
|
||||
{/* 主内容区域 */}
|
||||
|
||||
@@ -65,9 +65,9 @@ export default function AreaDatasetConfig() {
|
||||
areas,
|
||||
// areasLoading, // 地区列表已加载hook中
|
||||
|
||||
// 筛选
|
||||
filterArea,
|
||||
setFilterArea,
|
||||
// 筛选 - 多选
|
||||
filterAreas,
|
||||
setFilterAreas,
|
||||
page,
|
||||
setPage,
|
||||
pageSize,
|
||||
@@ -233,24 +233,17 @@ export default function AreaDatasetConfig() {
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理地区筛选变化
|
||||
* 处理地区筛选变化 - 支持多选
|
||||
*/
|
||||
const handleAreaFilterChange = (value: string) => {
|
||||
setFilterArea(value);
|
||||
const handleAreaFilterChange = (values: string[]) => {
|
||||
setFilterAreas(values);
|
||||
setPage(1); // 重置到第一页
|
||||
};
|
||||
|
||||
// ==================== Render ====================
|
||||
|
||||
// 计算用户角色标签
|
||||
const userRoleLabel = (() => {
|
||||
const labels: Record<string, string> = {
|
||||
common: '普通用户',
|
||||
admin: '市级管理员',
|
||||
provincial_admin: '省级管理员',
|
||||
};
|
||||
return labels[userRole] || '未知角色';
|
||||
})();
|
||||
// 用户角色已经在 hook 中处理好了,直接使用 userRole
|
||||
const userRoleLabel = userRole || '未知角色';
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
@@ -456,16 +449,15 @@ export default function AreaDatasetConfig() {
|
||||
<Flex gap="16px" align="center">
|
||||
<Text style={{ color: colors.text }}>地区筛选:</Text>
|
||||
<Select
|
||||
style={{ width: '150px' }}
|
||||
placeholder="全部地区"
|
||||
mode="multiple"
|
||||
style={{ minWidth: '200px', maxWidth: '400px' }}
|
||||
placeholder="请选择地区(可多选)"
|
||||
allowClear
|
||||
value={filterArea || undefined}
|
||||
value={filterAreas}
|
||||
onChange={handleAreaFilterChange}
|
||||
options={[
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '省级', value: '省级' },
|
||||
...areas.map((area) => ({ label: area, value: area })),
|
||||
]}
|
||||
maxTagCount={3}
|
||||
maxTagPlaceholder={(omittedValues) => `+${omittedValues.length}个`}
|
||||
options={Array.isArray(areas) ? areas.map((area) => ({ label: area, value: area })) : []}
|
||||
/>
|
||||
</Flex>
|
||||
</Card>
|
||||
@@ -538,10 +530,10 @@ export default function AreaDatasetConfig() {
|
||||
<Select
|
||||
placeholder="请选择地区"
|
||||
disabled={!!editingId} // 编辑时禁用
|
||||
options={areas.map((area) => ({
|
||||
options={Array.isArray(areas) ? areas.map((area) => ({
|
||||
label: area,
|
||||
value: area,
|
||||
}))}
|
||||
})) : []}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import DocumentList from './document-list';
|
||||
import DocumentDetail from './document-detail';
|
||||
import RetrieveTest from './retrieve-test';
|
||||
import DatasetSettings from './dataset-settings';
|
||||
import AreaDatasetConfig from './area-dataset-config';
|
||||
import { useDatasetManager } from '~/hooks/dify-dataset-manager';
|
||||
import '../../styles/components/dify-dataset-manager/index.css';
|
||||
|
||||
@@ -25,7 +26,11 @@ export default function DatasetManager() {
|
||||
error,
|
||||
activeTab,
|
||||
selectedDocument,
|
||||
|
||||
|
||||
// 知识库列表(基于权限)
|
||||
availableDatasets,
|
||||
loadingAvailableDatasets,
|
||||
|
||||
// 方法
|
||||
handlePageChange,
|
||||
handleDocumentDeleted,
|
||||
@@ -35,6 +40,7 @@ export default function DatasetManager() {
|
||||
handleBackToDocuments,
|
||||
handleTabChange,
|
||||
handleDatasetUpdated,
|
||||
handleDatasetChange,
|
||||
} = useDatasetManager();
|
||||
|
||||
// 加载中状态
|
||||
@@ -101,6 +107,11 @@ export default function DatasetManager() {
|
||||
return <RetrieveTest datasetId={dataset?.id || ''} />;
|
||||
}
|
||||
|
||||
// 配置管理菜单
|
||||
if (activeTab === 'area-config') {
|
||||
return <AreaDatasetConfig />;
|
||||
}
|
||||
|
||||
// 设置菜单
|
||||
if (activeTab === 'settings') {
|
||||
return (
|
||||
@@ -122,6 +133,9 @@ export default function DatasetManager() {
|
||||
onTabChange={handleTabChange}
|
||||
showBackButton={activeTab === 'documents' && !!selectedDocument}
|
||||
onBack={handleBackToDocuments}
|
||||
availableDatasets={availableDatasets}
|
||||
loadingAvailableDatasets={loadingAvailableDatasets}
|
||||
onDatasetChange={handleDatasetChange}
|
||||
>
|
||||
{renderContent()}
|
||||
</DatasetLayout>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { Button, Tooltip, Select, Spin } from 'antd';
|
||||
import {
|
||||
FileTextOutlined,
|
||||
SearchOutlined,
|
||||
SettingOutlined,
|
||||
ArrowLeftOutlined,
|
||||
DatabaseOutlined,
|
||||
AppstoreOutlined,
|
||||
SwapOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { DatasetLayoutProps, MenuTab, MenuItem } from '~/types/dify-dataset-manager/layout';
|
||||
|
||||
@@ -19,28 +21,63 @@ export default function DatasetLayout({
|
||||
showBackButton = false,
|
||||
onBack,
|
||||
children,
|
||||
availableDatasets = [],
|
||||
loadingAvailableDatasets = false,
|
||||
onDatasetChange,
|
||||
}: DatasetLayoutProps) {
|
||||
const menuItems: MenuItem[] = [
|
||||
{ key: 'documents', icon: <FileTextOutlined />, label: '文档' },
|
||||
{ key: 'retrieve', icon: <SearchOutlined />, label: '召回测试' },
|
||||
{ key: 'area-config', icon: <AppstoreOutlined />, label: '配置管理' },
|
||||
{ key: 'settings', icon: <SettingOutlined />, label: '设置' },
|
||||
];
|
||||
|
||||
// 是否显示知识库选择器(有多个知识库时显示)
|
||||
const showDatasetSelector = availableDatasets.length > 1;
|
||||
|
||||
return (
|
||||
<div className="dataset-layout">
|
||||
{/* 左侧侧边栏 */}
|
||||
<aside className="dataset-sidebar">
|
||||
{/* 知识库信息 */}
|
||||
{/* 知识库信息 / 选择器 */}
|
||||
<div className="sidebar-header">
|
||||
<div className="dataset-icon">
|
||||
<DatabaseOutlined />
|
||||
</div>
|
||||
<div className="dataset-info">
|
||||
<Tooltip title={dataset?.name} placement="right">
|
||||
<h2 className="dataset-name">{dataset?.name || '知识库'}</h2>
|
||||
</Tooltip>
|
||||
<span className="dataset-type">本地文档</span>
|
||||
</div>
|
||||
{showDatasetSelector ? (
|
||||
/* 多个知识库时显示下拉选择器 */
|
||||
<div className="dataset-selector">
|
||||
<Select
|
||||
value={dataset?.id}
|
||||
onChange={onDatasetChange}
|
||||
loading={loadingAvailableDatasets}
|
||||
className="dataset-select"
|
||||
placeholder="选择知识库"
|
||||
suffixIcon={<SwapOutlined />}
|
||||
popupMatchSelectWidth={false}
|
||||
dropdownStyle={{ minWidth: 200 }}
|
||||
>
|
||||
{availableDatasets.map(ds => (
|
||||
<Select.Option key={ds.dataset_id} value={ds.dataset_id}>
|
||||
<div className="dataset-option">
|
||||
<span className="dataset-option-name">{ds.dataset_name}</span>
|
||||
{ds.is_default && <span className="dataset-option-tag">默认</span>}
|
||||
{ds.is_public && <span className="dataset-option-tag public">公共</span>}
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<span className="dataset-type">本地文档</span>
|
||||
</div>
|
||||
) : (
|
||||
/* 单个或无知识库时显示名称 */
|
||||
<div className="dataset-info">
|
||||
<Tooltip title={dataset?.name} placement="right">
|
||||
<h2 className="dataset-name">{dataset?.name || '知识库'}</h2>
|
||||
</Tooltip>
|
||||
<span className="dataset-type">本地文档</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 统计信息 */}
|
||||
|
||||
Reference in New Issue
Block a user