- {fileList.map((file) => (
+ {fileList.map((file: UploadFile) => (
@@ -768,7 +330,7 @@ export default function DocumentUpload({
value={currentDoc?.documentId || currentDoc?.file.name}
style={{ width: 500 }}
onChange={handleDocumentChange}
- options={uploadedDocuments.map((doc, idx) => ({
+ options={uploadedDocuments.map((doc: UploadedDocument) => ({
value: doc.documentId || doc.file.name,
label: (
@@ -800,7 +362,7 @@ export default function DocumentUpload({
{getStatusText()}
-
- {/* 索引阶段详情 */}
- {currentDoc?.stage === 'indexing' && (
-
-
-
- 等待处理
-
-
-
- 解析文档
-
-
-
- 清洗文本
-
-
-
- 分段处理
-
-
-
- 建立索引
-
-
-
- 完成
-
-
- )}
) : currentDoc?.stage === 'error' ? (
@@ -855,7 +387,7 @@ export default function DocumentUpload({
- {currentDoc?.segments.map((segment, index) => (
+ {currentDoc?.segments.map((segment: Segment, index: number) => (
#{index + 1}
@@ -881,7 +413,7 @@ export default function DocumentUpload({
{stats.completed}/{stats.total} 个文档处理完成
)}
diff --git a/app/components/dify-dataset-manager/index.tsx b/app/components/dify-dataset-manager/index.tsx
index 2bdb8eb..42b62fe 100644
--- a/app/components/dify-dataset-manager/index.tsx
+++ b/app/components/dify-dataset-manager/index.tsx
@@ -1,14 +1,10 @@
-import { useEffect, useState } from 'react';
-import { message, Spin } from 'antd';
-import DatasetLayout, { type MenuTab } from './layout';
+import { Spin } from 'antd';
+import DatasetLayout from './layout';
import DocumentList from './document-list';
import DocumentDetail from './document-detail';
import RetrieveTest from './retrieve-test';
import DatasetSettings from './dataset-settings';
-import type { Dataset } from '~/api/dify-dataset/type/datasetTypes';
-import type { Document } from '~/api/dify-dataset/type/documentTypes';
-import { fetchDatasets } from '~/api/dify-dataset/api/datasetApi';
-import { fetchDocuments } from '~/api/dify-dataset/api/documentApi';
+import { useDatasetManager } from '~/hooks/dify-dataset-manager';
import '../../styles/components/dify-dataset-manager/index.css';
/**
@@ -16,162 +12,30 @@ import '../../styles/components/dify-dataset-manager/index.css';
* 带左侧菜单栏的完整布局
*/
export default function DatasetManager() {
- // 知识库状态
- const [dataset, setDataset] = useState
(null);
- const [loadingDataset, setLoadingDataset] = useState(true);
-
- // 文档状态
- const [documents, setDocuments] = useState([]);
- const [loadingDocuments, setLoadingDocuments] = useState(false);
- const [documentTotal, setDocumentTotal] = useState(0);
- const [documentPage, setDocumentPage] = useState(1);
- const [documentPageSize] = useState(20);
-
- // 初始化状态
- const [inited, setInited] = useState(false);
- const [error, setError] = useState(null);
-
- // 菜单状态
- const [activeTab, setActiveTab] = useState('documents');
-
- // 选中的文档(用于查看文档详情)
- const [selectedDocument, setSelectedDocument] = useState(null);
-
- /**
- * 加载知识库(获取第一个知识库)
- */
- const loadDataset = async () => {
- setLoadingDataset(true);
- try {
- console.log('[DatasetManager] 加载知识库...');
- const response = await fetchDatasets(1, 1);
- console.log('[DatasetManager] 知识库响应:', response);
-
- if (response && response.data && response.data.length > 0) {
- const firstDataset = response.data[0];
- setDataset(firstDataset);
- // 立即加载文档
- await loadDocuments(firstDataset.id, 1);
- } else {
- setError('未找到知识库,请先在Dify中创建知识库');
- }
- } catch (err: any) {
- console.error('[DatasetManager] 加载知识库失败:', err);
- setError(err.message || '加载知识库失败');
- message.error('加载知识库失败');
- } finally {
- setLoadingDataset(false);
- setInited(true);
- }
- };
-
- /**
- * 加载文档列表
- */
- const loadDocuments = async (datasetId: string, page: number = 1) => {
- if (!datasetId) return;
-
- setLoadingDocuments(true);
- try {
- console.log('[DatasetManager] 加载文档列表:', { datasetId, page });
- const response = await fetchDocuments(datasetId, page, documentPageSize);
- console.log('[DatasetManager] 文档列表响应:', response);
-
- if (response && response.data) {
- setDocuments(response.data);
- setDocumentTotal(response.total);
- setDocumentPage(page);
- }
- } catch (err: any) {
- console.error('[DatasetManager] 加载文档列表失败:', err);
- message.error('加载文档列表失败');
- } finally {
- setLoadingDocuments(false);
- }
- };
-
- /**
- * 处理文档页码变化
- */
- const handlePageChange = (page: number) => {
- if (dataset) {
- loadDocuments(dataset.id, page);
- }
- };
-
- /**
- * 处理文档删除
- */
- const handleDocumentDeleted = (documentId: string) => {
- setDocuments((prev) => prev.filter((doc) => doc.id !== documentId));
- setDocumentTotal((prev) => prev - 1);
-
- // 更新知识库的文档数量
- if (dataset) {
- setDataset({
- ...dataset,
- document_count: dataset.document_count - 1
- });
- }
- };
-
- /**
- * 处理文档状态变化
- */
- const handleDocumentStatusChanged = (documentId: string, enabled: boolean) => {
- setDocuments((prev) =>
- prev.map((doc) =>
- doc.id === documentId ? { ...doc, enabled } : doc
- )
- );
- };
-
- /**
- * 刷新文档列表
- */
- const handleRefresh = () => {
- if (dataset) {
- loadDocuments(dataset.id, documentPage);
- }
- };
-
- /**
- * 查看文档详情(分段管理)
- */
- const handleViewDocument = (doc: Document) => {
- console.log('[DatasetManager] 查看文档详情:', doc);
- setSelectedDocument(doc);
- };
-
- /**
- * 返回文档列表
- */
- const handleBackToDocuments = () => {
- setSelectedDocument(null);
- };
-
- /**
- * 处理菜单切换
- */
- const handleTabChange = (tab: MenuTab) => {
- setActiveTab(tab);
- // 切换菜单时清除选中的文档
- if (tab !== 'documents') {
- setSelectedDocument(null);
- }
- };
-
- /**
- * 处理知识库更新
- */
- const handleDatasetUpdated = (updatedDataset: Dataset) => {
- setDataset(updatedDataset);
- };
-
- // 初始化
- useEffect(() => {
- loadDataset();
- }, []);
+ const {
+ // 状态
+ dataset,
+ loadingDataset,
+ documents,
+ loadingDocuments,
+ documentTotal,
+ documentPage,
+ documentPageSize,
+ inited,
+ error,
+ activeTab,
+ selectedDocument,
+
+ // 方法
+ handlePageChange,
+ handleDocumentDeleted,
+ handleDocumentStatusChanged,
+ handleRefresh,
+ handleViewDocument,
+ handleBackToDocuments,
+ handleTabChange,
+ handleDatasetUpdated,
+ } = useDatasetManager();
// 加载中状态
if (!inited || loadingDataset) {
diff --git a/app/components/dify-dataset-manager/layout.tsx b/app/components/dify-dataset-manager/layout.tsx
index 8ca457b..88e3fe4 100644
--- a/app/components/dify-dataset-manager/layout.tsx
+++ b/app/components/dify-dataset-manager/layout.tsx
@@ -1,4 +1,3 @@
-import { ReactNode } from 'react';
import { Button, Tooltip } from 'antd';
import {
FileTextOutlined,
@@ -7,27 +6,7 @@ import {
ArrowLeftOutlined,
DatabaseOutlined,
} from '@ant-design/icons';
-import type { Dataset } from '~/api/dify-dataset/type/datasetTypes';
-
-/**
- * 菜单项类型
- */
-export type MenuTab = 'documents' | 'retrieve' | 'settings';
-
-interface DatasetLayoutProps {
- /** 知识库信息 */
- dataset: Dataset | null;
- /** 当前激活的菜单 */
- activeTab: MenuTab;
- /** 菜单切换回调 */
- onTabChange: (tab: MenuTab) => void;
- /** 是否显示返回按钮(在文档详情页时显示) */
- showBackButton?: boolean;
- /** 返回按钮点击回调 */
- onBack?: () => void;
- /** 子组件 */
- children: ReactNode;
-}
+import type { DatasetLayoutProps, MenuTab, MenuItem } from '~/types/dify-dataset-manager/layout';
/**
* 知识库布局组件
@@ -41,7 +20,7 @@ export default function DatasetLayout({
onBack,
children,
}: DatasetLayoutProps) {
- const menuItems: { key: MenuTab; icon: ReactNode; label: string }[] = [
+ const menuItems: MenuItem[] = [
{ key: 'documents', icon: , label: '文档' },
{ key: 'retrieve', icon: , label: '召回测试' },
{ key: 'settings', icon: , label: '设置' },
@@ -106,3 +85,6 @@ export default function DatasetLayout({
);
}
+
+// 重新导出类型,保持向后兼容
+export type { MenuTab } from '~/types/dify-dataset-manager/layout';
diff --git a/app/components/dify-dataset-manager/retrieve-test.tsx b/app/components/dify-dataset-manager/retrieve-test.tsx
index 7ce129c..840dc6b 100644
--- a/app/components/dify-dataset-manager/retrieve-test.tsx
+++ b/app/components/dify-dataset-manager/retrieve-test.tsx
@@ -1,202 +1,306 @@
-import { useState } from 'react';
-import {
- Input,
- Button,
- Card,
- Select,
- Slider,
- Table,
- Tag,
- Empty,
- Spin,
- message,
-} from 'antd';
-import { FileSearchOutlined } from '@ant-design/icons';
-import type { ColumnsType } from 'antd/es/table';
+import { SearchOutlined, FileSearchOutlined } from '@ant-design/icons';
+import { Button, Tag, Input, Slider, Spin, Select, Flex } from 'antd';
import type { RetrieveRecord } from '~/api/dify-dataset/type';
-import { retrieveDataset } from '~/api/dify-dataset/api/segmentApi';
+import { useRetrieveTest } from '~/hooks/dify-dataset-manager/retrieve-test';
+import type { RetrieveTestProps } from '~/types/dify-dataset-manager/retrieve-test';
-interface RetrieveTestProps {
- datasetId: string;
+// 颜色常量
+const colors = {
+ bgContainer: '#fff',
+ bgLayout: '#f5f5f5',
+ bgElevated: '#fafafa',
+ border: '#e8e8e8',
+ text: '#262626',
+ textSecondary: '#8c8c8c',
+ textTertiary: '#bfbfbf',
+ textQuaternary: '#d9d9d9',
+ fillTertiary: '#f0f0f0',
+};
+
+/**
+ * 检索结果项组件
+ */
+function ResultItem({ record, index }: { record: RetrieveRecord; index: number }) {
+ const scorePercent = (record.score * 100).toFixed(1);
+ const scoreColor = record.score > 0.8 ? '#52c41a' : record.score > 0.5 ? '#faad14' : '#666';
+
+ return (
+
+
+
+
+ {scorePercent}%
+
+
+ #{index + 1} · {record.segment.word_count} 字 · 命中 {record.segment.hit_count} 次
+
+
+ {record.segment.document && (
+
+ 来源: {record.segment.document.name}
+
+ )}
+
+
+ {record.segment.content.length > 500
+ ? record.segment.content.substring(0, 500) + '...'
+ : record.segment.content}
+
+ {record.segment.answer && (
+
+
+ 答案:
+
+
+ {record.segment.answer.length > 200
+ ? record.segment.answer.substring(0, 200) + '...'
+ : record.segment.answer}
+
+
+ )}
+
+ );
}
/**
* 召回测试组件
- * 用于测试知识库的检索效果
*/
export default function RetrieveTest({ datasetId }: RetrieveTestProps) {
- const [searchQuery, setSearchQuery] = useState('');
- const [retrieveResults, setRetrieveResults] = useState
([]);
- const [retrieving, setRetrieving] = useState(false);
- const [searchMethod, setSearchMethod] = useState('hybrid_search');
- const [topK, setTopK] = useState(5);
+ const {
+ searchQuery,
+ setSearchQuery,
+ retrieveResults,
+ retrieving,
+ searchMethod,
+ setSearchMethod,
+ topK,
+ setTopK,
+ handleRetrieve,
+ } = useRetrieveTest(datasetId);
- /**
- * 执行检索
- */
- const handleRetrieve = async () => {
- if (!searchQuery.trim()) {
- message.warning('请输入检索关键词');
- return;
- }
-
- if (!datasetId) {
- message.warning('知识库ID不存在');
- return;
- }
-
- setRetrieving(true);
- try {
- const response = await retrieveDataset(datasetId, searchQuery, {
- search_method: searchMethod as any,
- top_k: topK,
- });
- setRetrieveResults(response.records || []);
- if (response.records?.length === 0) {
- message.info('未找到匹配的结果');
- }
- } catch (err: any) {
- console.error('检索失败:', err);
- message.error(err.message || '检索失败');
- } finally {
- setRetrieving(false);
- }
- };
-
- // 检索结果列定义
- const columns: ColumnsType = [
- {
- title: '相关度',
- dataIndex: 'score',
- key: 'score',
- width: 100,
- render: (score: number) => (
- 0.8 ? 'green' : score > 0.5 ? 'orange' : 'default'}>
- {(score * 100).toFixed(1)}%
-
- ),
- },
- {
- title: '内容',
- key: 'content',
- render: (_, record) => (
-
-
- {record.segment.content.length > 300
- ? record.segment.content.substring(0, 300) + '...'
- : record.segment.content}
-
- {record.segment.answer && (
-
- 答案:
- {record.segment.answer.length > 150
- ? record.segment.answer.substring(0, 150) + '...'
- : record.segment.answer}
-
- )}
-
- ),
- },
- {
- title: '字数',
- key: 'word_count',
- width: 80,
- render: (_, record) => record.segment.word_count,
- },
- {
- title: '命中次数',
- key: 'hit_count',
- width: 100,
- render: (_, record) => record.segment.hit_count,
- },
+ // 检索方式选项(只有3种)
+ const searchMethodOptions = [
+ { label: '向量检索', value: 'semantic_search' },
+ { label: '全文检索', value: 'full_text_search' },
+ { label: '混合检索', value: 'hybrid_search' },
];
return (
-
- {/* 页面标题 */}
-
-
召回测试
-
- 输入查询内容,测试知识库的检索效果
-
-
+
+ {/* 左侧面板 - 输入区域 */}
+
+ {/* 标题 */}
+
+
+ 召回测试
+
+
+ 根据给定的查询文本测试知识库召回效果
+
+
- {/* 检索设置 */}
-
-
- }
- value={searchQuery}
- onChange={(e) => setSearchQuery(e.target.value)}
- onPressEnter={handleRetrieve}
- className="search-input"
- />
-
-
-
-
-
- 检索方式:
+ {/* 查询输入区 */}
+
+
+
+ 源文本
+
-
- 返回数量 (Top K):
+
+ setSearchQuery(e.target.value)}
+ onPressEnter={(e) => {
+ if (!e.shiftKey) {
+ e.preventDefault();
+ handleRetrieve();
+ }
+ }}
+ autoSize={{ minRows: 6, maxRows: 12 }}
+ style={{
+ background: colors.bgContainer,
+ resize: 'none',
+ }}
+ />
+
+
+ {searchQuery.length} / 200
+
+ }
+ onClick={handleRetrieve}
+ loading={retrieving}
+ >
+ 测试
+
+
+
+
+ {/* 检索设置 */}
+
+
+ 检索设置
+
+
+
+ 返回数量 (Top K):
+
- {topK}
-
-
-
+
+ {topK}
+
+
+
+
- {/* 检索结果 */}
-
+ {/* 右侧面板 - 结果展示 */}
+
{retrieving ? (
-
+
+ 检索中...
+
+
) : retrieveResults.length === 0 ? (
-
+
+
+
+ 召回测试结果将展示在这里
+
+
) : (
<>
-
- 找到 {retrieveResults.length} 条结果
-
-
record.segment.id}
- pagination={false}
- size="small"
- />
+
+
+ 检索结果
+
+
+ 共找到 {retrieveResults.length} 条结果
+
+
+
+ {retrieveResults.map((record, index) => (
+
+ ))}
+
>
)}
-
-
+
+
);
}
diff --git a/app/config/api-config.ts b/app/config/api-config.ts
index fddae8f..5fba726 100644
--- a/app/config/api-config.ts
+++ b/app/config/api-config.ts
@@ -28,6 +28,13 @@ interface ApiConfig {
// 应用ID(用于登出)
appId?: string;
};
+ // Dify 知识库检索配置
+ dify: {
+ // Reranking 模型提供商
+ rerankingProviderName: string;
+ // Reranking 模型名称
+ rerankingModelName: string;
+ };
}
// 端口特定配置映射
@@ -37,17 +44,12 @@ const portConfigs: Record> = {
// 主要
// 梅州
'51703': {
- // baseUrl: 'http://172.16.0.55:8073',
- // documentUrl: 'http://172.16.0.55:8073/docauditai/',
- // uploadUrl: 'http://172.16.0.55:8073/admin/documents',
- // collaboraUrl: 'http://172.16.0.81:9980',
- // appUrl: 'http://172.16.0.34:51703',
+ baseUrl: 'http://172.16.0.78:8073',
+ documentUrl: 'http://172.16.0.78:8073/docauditai/',
+ uploadUrl: 'http://172.16.0.78:8073/admin/documents',
- baseUrl: 'http://10.79.97.17:8000',
- documentUrl: 'http://10.79.97.17:8000/docauditai/',
- uploadUrl: 'http://10.79.97.17:8000/admin/documents',
- collaboraUrl: 'http://10.79.97.17:9980',
- appUrl: 'http://10.79.97.17:51703',
+ collaboraUrl: 'http://172.16.0.81:9980',
+ appUrl: 'http://172.16.0.34:51703',
oauth: {
redirectUri: 'http://10.79.97.17:51703/callback'
@@ -119,20 +121,23 @@ const portConfigs: Record> = {
const configs: Record = {
// 开发环境
development: {
- baseUrl: 'http://172.16.0.55:8073', // FastAPI后端(包含/dify代理)
- documentUrl: 'http://172.16.0.55:8073/docauditai/',
- uploadUrl: 'http://172.16.0.55:8073/admin/documents',
+ baseUrl: 'http://172.16.0.78:8073', // FastAPI后端(包含/dify代理)
+ documentUrl: 'http://172.16.0.78:8073/docauditai/',
+ uploadUrl: 'http://172.16.0.78:8073/admin/documents',
collaboraUrl: 'http://172.16.0.81:9980',
- // appUrl: 'http://172.16.0.34:51709',
- appUrl: 'http://172.16.0.34:5173',
-
+ appUrl: 'http://172.16.0.78:51703',
+
oauth: {
serverUrl: 'http://10.79.112.85', // IDaaS服务器地址
clientId: 'none',
clientSecret: 'none', // 需要替换为实际的Client Secret
redirectUri: 'http://10.79.97.17/', // 回调地址
appId: 'idaasoauth2' // 应用ID,用于登出
+ },
+ dify: {
+ rerankingProviderName: 'langgenius/tongyi/tongyi',
+ rerankingModelName: 'gte-rerank'
}
},
@@ -149,6 +154,10 @@ const configs: Record = {
clientSecret: 'placeholder', // 需要替换为实际的Client Secret
redirectUri: 'http://10.79.97.17/', // 回调地址
appId: 'idaasoauth2' // 应用ID,用于登出
+ },
+ dify: {
+ rerankingProviderName: 'langgenius/tongyi/tongyi',
+ rerankingModelName: 'gte-rerank'
}
},
@@ -170,6 +179,10 @@ const configs: Record = {
clientSecret: 'placeholder', // 占位符,实际值从环境变量获取
redirectUri: 'http://10.79.97.17/', // 回调地址
appId: 'idaasoauth2' // 应用ID,用于登出
+ },
+ dify: {
+ rerankingProviderName: 'langgenius/tongyi/tongyi',
+ rerankingModelName: 'gte-rerank'
}
},
@@ -186,6 +199,10 @@ const configs: Record = {
clientSecret: 'your_client_secret', // 需要替换为实际的Client Secret
redirectUri: 'http://172.16.0.119:3000/callback', // 回调地址
appId: 'idaasoauth2' // 应用ID,用于登出
+ },
+ dify: {
+ rerankingProviderName: 'langgenius/tongyi/tongyi',
+ rerankingModelName: 'gte-rerank'
}
}
};
@@ -235,7 +252,8 @@ const getConfigFromEnv = (defaultConfig: ApiConfig): ApiConfig => {
clientSecret: process.env.OAUTH_CLIENT_SECRET || defaultConfig.oauth.clientSecret,
redirectUri: process.env.NEXT_PUBLIC_OAUTH_REDIRECT_URI || defaultConfig.oauth.redirectUri,
appId: process.env.NEXT_PUBLIC_OAUTH_APP_ID || defaultConfig.oauth.appId
- }
+ },
+ dify: defaultConfig.dify
};
};
@@ -363,7 +381,8 @@ export const {
uploadUrl: UPLOAD_URL,
collaboraUrl: COLLABORA_URL,
appUrl: APP_URL,
- oauth: OAUTH_CONFIG
+ oauth: OAUTH_CONFIG,
+ dify: DIFY_CONFIG
} = apiConfig;
/**
diff --git a/app/hooks/dify-dataset-manager/dataset-settings.ts b/app/hooks/dify-dataset-manager/dataset-settings.ts
new file mode 100644
index 0000000..dcaba5a
--- /dev/null
+++ b/app/hooks/dify-dataset-manager/dataset-settings.ts
@@ -0,0 +1,92 @@
+import { useState, useEffect, useCallback } from 'react';
+import { message } from 'antd';
+import type { FormInstance } from 'antd';
+import type { Dataset } from '~/api/dify-dataset/type/datasetTypes';
+import { updateDatasetName } from '~/api/dify-dataset/api/datasetApi';
+
+/**
+ * 知识库设置状态管理 Hook
+ */
+export function useDatasetSettings(
+ dataset: Dataset | null,
+ form: FormInstance,
+ onDatasetUpdated: (dataset: Dataset) => void
+) {
+ const [saving, setSaving] = useState(false);
+ const [hasChanges, setHasChanges] = useState(false);
+
+ // 初始化表单数据
+ useEffect(() => {
+ if (dataset) {
+ form.setFieldsValue({
+ name: dataset.name,
+ description: dataset.description || '',
+ });
+ setHasChanges(false);
+ }
+ }, [dataset, form]);
+
+ /**
+ * 处理表单值变化
+ */
+ const handleValuesChange = useCallback(() => {
+ const values = form.getFieldsValue();
+ const changed =
+ values.name !== dataset?.name ||
+ values.description !== (dataset?.description || '');
+ setHasChanges(changed);
+ }, [form, dataset]);
+
+ /**
+ * 保存设置
+ */
+ const handleSave = useCallback(async () => {
+ if (!dataset) {
+ message.error('知识库不存在');
+ return;
+ }
+
+ try {
+ const values = await form.validateFields();
+ setSaving(true);
+
+ // 目前只支持修改名称
+ const updatedDataset = await updateDatasetName(dataset.id, values.name);
+
+ message.success('保存成功');
+ onDatasetUpdated(updatedDataset);
+ setHasChanges(false);
+ } catch (err: any) {
+ console.error('保存设置失败:', err);
+ message.error(err.message || '保存失败');
+ } finally {
+ setSaving(false);
+ }
+ }, [dataset, form, onDatasetUpdated]);
+
+ /**
+ * 重置表单
+ */
+ const handleReset = useCallback(() => {
+ if (dataset) {
+ form.setFieldsValue({
+ name: dataset.name,
+ description: dataset.description || '',
+ });
+ setHasChanges(false);
+ }
+ }, [dataset, form]);
+
+ return {
+ // 状态
+ saving,
+ hasChanges,
+
+ // 方法
+ handleValuesChange,
+ handleSave,
+ handleReset,
+ };
+}
+
+export type UseDatasetSettingsReturn = ReturnType;
diff --git a/app/hooks/dify-dataset-manager/document-detail.ts b/app/hooks/dify-dataset-manager/document-detail.ts
new file mode 100644
index 0000000..36fb602
--- /dev/null
+++ b/app/hooks/dify-dataset-manager/document-detail.ts
@@ -0,0 +1,123 @@
+import { message } from 'antd';
+import { useCallback, useEffect, useState } from 'react';
+import { updateDocumentWithSettings } from '~/api/dify-dataset/api/documentApi';
+import { fetchSegments } from '~/api/dify-dataset/api/segmentApi';
+import type { Segment } from '~/api/dify-dataset/type';
+import type { Document } from '~/api/dify-dataset/type/documentTypes';
+import type { DocumentDetailSegmentationSettings } from '~/types/dify-dataset-manager/document-detail';
+import { DEFAULT_DOCUMENT_DETAIL_SETTINGS } from '~/types/dify-dataset-manager/document-detail';
+
+/**
+ * 文档详情状态管理 Hook
+ */
+export function useDocumentDetail(datasetId: string, document: Document | null) {
+ // 分段设置状态
+ const [settings, setSettings] = useState(DEFAULT_DOCUMENT_DETAIL_SETTINGS);
+
+ // 预览状态
+ const [previewSegments, setPreviewSegments] = useState([]);
+ const [previewLoading, setPreviewLoading] = useState(false);
+ const [showPreview, setShowPreview] = useState(false);
+
+ // 保存状态
+ const [saving, setSaving] = useState(false);
+
+ // 当文档变化时重置设置
+ useEffect(() => {
+ if (document) {
+ // 可以从文档中读取已有的设置,这里使用默认值
+ setSettings(DEFAULT_DOCUMENT_DETAIL_SETTINGS);
+ setPreviewSegments([]);
+ setShowPreview(false);
+ }
+ }, [document?.id]);
+
+ /**
+ * 更新设置
+ */
+ const updateSettings = useCallback((key: keyof DocumentDetailSegmentationSettings, value: any) => {
+ setSettings(prev => ({ ...prev, [key]: value }));
+ }, []);
+
+ /**
+ * 重置设置
+ */
+ const handleReset = useCallback(() => {
+ setSettings(DEFAULT_DOCUMENT_DETAIL_SETTINGS);
+ setPreviewSegments([]);
+ setShowPreview(false);
+ }, []);
+
+ /**
+ * 预览分段
+ */
+ const handlePreview = useCallback(async () => {
+ if (!document) return;
+
+ setPreviewLoading(true);
+ setShowPreview(true);
+ try {
+ // 获取当前文档的分段作为预览
+ const response = await fetchSegments(datasetId, document.id, 1, 50);
+ setPreviewSegments(response.data || []);
+ if (response.data?.length === 0) {
+ message.info('该文档暂无分段数据');
+ }
+ } catch (err: any) {
+ console.error('预览分段失败:', err);
+ message.error(err.message || '预览失败');
+ } finally {
+ setPreviewLoading(false);
+ }
+ }, [datasetId, document]);
+
+ /**
+ * 保存并处理
+ */
+ const handleSaveAndProcess = useCallback(async () => {
+ if (!document) return;
+
+ setSaving(true);
+ try {
+ await updateDocumentWithSettings(datasetId, document.id, {
+ indexing_technique: 'high_quality',
+ process_rule: {
+ mode: 'custom',
+ rules: {
+ pre_processing_rules: [
+ { id: 'remove_extra_spaces', enabled: settings.removeExtraSpaces },
+ { id: 'remove_urls_emails', enabled: settings.removeUrlsEmails },
+ ],
+ segmentation: {
+ separator: settings.separator.replace(/\\n/g, '\n'),
+ max_tokens: settings.maxTokens,
+ },
+ },
+ },
+ });
+ message.success('设置已保存,文档正在重新处理...');
+ } catch (err: any) {
+ console.error('保存设置失败:', err);
+ message.error(err.message || '保存失败');
+ } finally {
+ setSaving(false);
+ }
+ }, [datasetId, document, settings]);
+
+ return {
+ // 状态
+ settings,
+ previewSegments,
+ previewLoading,
+ showPreview,
+ saving,
+
+ // 方法
+ updateSettings,
+ handleReset,
+ handlePreview,
+ handleSaveAndProcess,
+ };
+}
+
+export type UseDocumentDetailReturn = ReturnType;
diff --git a/app/hooks/dify-dataset-manager/document-list.tsx b/app/hooks/dify-dataset-manager/document-list.tsx
new file mode 100644
index 0000000..6a487a8
--- /dev/null
+++ b/app/hooks/dify-dataset-manager/document-list.tsx
@@ -0,0 +1,156 @@
+import { useState, useCallback } from 'react';
+import { message } from 'antd';
+import type { Document, IndexingStatus } from '~/api/dify-dataset/type/documentTypes';
+import { deleteDocument, toggleDocumentStatus } from '~/api/dify-dataset/api/documentApi';
+import {
+ CheckCircleOutlined,
+ SyncOutlined,
+ ClockCircleOutlined,
+ PauseCircleOutlined,
+ ExclamationCircleOutlined,
+} from '@ant-design/icons';
+import type { StatusConfig } from '~/types/dify-dataset-manager/document-list';
+
+/**
+ * 文档列表状态管理 Hook
+ */
+export function useDocumentList(
+ datasetId: string,
+ onDocumentDeleted: (documentId: string) => void,
+ onDocumentStatusChanged: (documentId: string, enabled: boolean) => void,
+ onRefresh: () => void
+) {
+ const [searchValue, setSearchValue] = useState('');
+ const [deletingId, setDeletingId] = useState(null);
+ const [showUploadPage, setShowUploadPage] = useState(false);
+
+ /**
+ * 获取状态标签配置
+ */
+ const getStatusConfig = useCallback((status: IndexingStatus): StatusConfig => {
+ const configs: Record = {
+ completed: { color: 'success', icon: , text: '已完成' },
+ indexing: { color: 'processing', icon: , text: '索引中' },
+ waiting: { color: 'warning', icon: , text: '等待中' },
+ parsing: { color: 'processing', icon: , text: '解析中' },
+ cleaning: { color: 'processing', icon: , text: '清洗中' },
+ splitting: { color: 'processing', icon: , text: '分段中' },
+ paused: { color: 'default', icon: , text: '已暂停' },
+ error: { color: 'error', icon: , text: '错误' },
+ };
+ return configs[status] || { color: 'default', icon: null, text: status };
+ }, []);
+
+ /**
+ * 格式化日期
+ */
+ const formatDate = useCallback((timestamp: number) => {
+ return new Date(timestamp * 1000).toLocaleString('zh-CN', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+ }, []);
+
+ /**
+ * 格式化数字
+ */
+ const formatNumber = useCallback((num: number) => {
+ if (num >= 10000) {
+ return (num / 10000).toFixed(1) + 'w';
+ }
+ if (num >= 1000) {
+ return (num / 1000).toFixed(1) + 'k';
+ }
+ return num.toString();
+ }, []);
+
+ /**
+ * 处理删除文档
+ */
+ const handleDelete = useCallback(async (documentId: string) => {
+ setDeletingId(documentId);
+ try {
+ await deleteDocument(datasetId, documentId);
+ message.success('删除成功');
+ onDocumentDeleted(documentId);
+ } catch (err: any) {
+ console.error('删除文档失败:', err);
+ message.error(err.message || '删除失败');
+ } finally {
+ setDeletingId(null);
+ }
+ }, [datasetId, onDocumentDeleted]);
+
+ /**
+ * 处理启用/禁用文档
+ */
+ const handleToggleStatus = useCallback(async (documentId: string, enabled: boolean) => {
+ try {
+ await toggleDocumentStatus(datasetId, documentId, enabled);
+ message.success(enabled ? '已启用' : '已禁用');
+ onDocumentStatusChanged(documentId, enabled);
+ } catch (err: any) {
+ console.error('切换文档状态失败:', err);
+ message.error(err.message || '操作失败');
+ }
+ }, [datasetId, onDocumentStatusChanged]);
+
+ /**
+ * 点击上传按钮,显示上传页面
+ */
+ const handleUploadClick = useCallback(() => {
+ if (!datasetId) {
+ message.error('请先选择知识库');
+ return;
+ }
+ setShowUploadPage(true);
+ }, [datasetId]);
+
+ /**
+ * 关闭上传页面
+ */
+ const handleUploadClose = useCallback(() => {
+ setShowUploadPage(false);
+ }, []);
+
+ /**
+ * 上传成功回调
+ */
+ const handleUploadSuccess = useCallback(() => {
+ setShowUploadPage(false);
+ onRefresh();
+ }, [onRefresh]);
+
+ /**
+ * 过滤文档
+ */
+ const filterDocuments = useCallback((documents: Document[]) => {
+ return documents.filter((doc) =>
+ doc.name.toLowerCase().includes(searchValue.toLowerCase())
+ );
+ }, [searchValue]);
+
+ return {
+ // 状态
+ searchValue,
+ setSearchValue,
+ deletingId,
+ showUploadPage,
+
+ // 方法
+ getStatusConfig,
+ formatDate,
+ formatNumber,
+ handleDelete,
+ handleToggleStatus,
+ handleUploadClick,
+ handleUploadClose,
+ handleUploadSuccess,
+ filterDocuments,
+ };
+}
+
+export type UseDocumentListReturn = ReturnType;
diff --git a/app/hooks/dify-dataset-manager/document-upload.ts b/app/hooks/dify-dataset-manager/document-upload.ts
new file mode 100644
index 0000000..1cb4b94
--- /dev/null
+++ b/app/hooks/dify-dataset-manager/document-upload.ts
@@ -0,0 +1,533 @@
+import type { UploadFile, UploadProps } from 'antd';
+import { message } from 'antd';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import {
+ fetchIndexingStatus,
+ updateDocumentByFile,
+ uploadDocumentWithConfig,
+} from '~/api/dify-dataset/api/documentApi';
+import { fetchSegments } from '~/api/dify-dataset/api/segmentApi';
+import type { IndexingStatus } from '~/api/dify-dataset/type/documentTypes';
+import type {
+ DocumentStage,
+ SegmentationSettings,
+ UploadedDocument,
+} from '~/types/dify-dataset-manager/document-upload';
+import {
+ DEFAULT_SEGMENTATION_SETTINGS,
+ INDEXING_STATUS_CONFIG,
+} from '~/types/dify-dataset-manager/document-upload';
+
+/**
+ * 文档上传状态管理 Hook
+ */
+export function useDocumentUpload(datasetId: string, onClose: () => void, onSuccess: () => void) {
+ // 步骤控制
+ const [step, setStep] = useState<1 | 2>(1);
+
+ // 文件相关
+ const [selectedFiles, setSelectedFiles] = useState([]);
+ const [fileList, setFileList] = useState([]);
+
+ // 多文档状态管理
+ const [uploadedDocuments, setUploadedDocuments] = useState([]);
+ // 当前选中查看的文档索引
+ const [currentDocIndex, setCurrentDocIndex] = useState(0);
+
+ // 当前显示的分段设置(来自当前选中的文档)
+ const [currentSettings, setCurrentSettings] = useState(DEFAULT_SEGMENTATION_SETTINGS);
+
+ // 预览相关
+ const [previewLoading, setPreviewLoading] = useState(false);
+
+ // 轮询定时器(支持多个文档)
+ const pollingTimersRef = useRef