temp:临时备份,完成一半知识库管理模块

This commit is contained in:
PingChuan
2025-12-01 12:33:53 +08:00
parent 754ec2c7b5
commit 0c1b81cfb2
25 changed files with 3564 additions and 560 deletions
+78
View File
@@ -0,0 +1,78 @@
/**
* Dify Dataset 知识库 API 模块
*
* 提供浏览器端调用 Dify 知识库管理 API 的函数
*
* @module api/dify-dataset/api/datasetApi
*/
import axios from 'axios';
import type { Dataset, DatasetsResponse } from '../type';
/**
* API 基础 URL
*/
const API_URL = '/api/dataset';
/**
* 获取知识库列表
*
* @param page - 页码,默认 1
* @param limit - 每页数量,默认 20
* @returns 知识库列表响应
*/
export async function fetchDatasets(
page: number = 1,
limit: number = 20
): Promise<DatasetsResponse> {
const params = new URLSearchParams({
page: page.toString(),
limit: limit.toString(),
});
const response = await axios.get<DatasetsResponse>(
`${API_URL}/datasets?${params}`,
{ withCredentials: true }
);
return response.data;
}
/**
* 获取单个知识库详情
*
* @param datasetId - 知识库 ID
* @returns 知识库详情
*/
export async function fetchDataset(datasetId: string): Promise<Dataset> {
const response = await axios.get<Dataset>(
`${API_URL}/datasets/${datasetId}`,
{ withCredentials: true }
);
return response.data;
}
/**
* 更新知识库名称
*
* 注意:仅允许修改知识库名称,其他字段不开放修改
*
* @param datasetId - 知识库 ID
* @param name - 新的知识库名称
* @returns 更新后的知识库详情
*/
export async function updateDatasetName(
datasetId: string,
name: string
): Promise<Dataset> {
console.log('[Dataset Client] 更新知识库名称:', { datasetId, name });
const response = await axios.patch<Dataset>(
`${API_URL}/datasets/${datasetId}`,
{ name },
{
headers: { 'Content-Type': 'application/json' },
withCredentials: true,
}
);
return response.data;
}
@@ -1,104 +1,25 @@
/**
* Dify Dataset API
* Dify Dataset API
*
* Dify API
* Remix API Routes
* Dify API
*
* @module api/dify-dataset/client
* @module api/dify-dataset/api/documentApi
*/
import axios from 'axios';
import type {
Dataset,
DatasetsResponse,
DocumentsResponse,
SegmentsResponse,
Document,
OperationResult,
DocumentsResponse,
IndexingStatusResponse,
UploadFileInfo,
UpdateDatasetRequest,
} from './types';
// ============================================================================
// 基础配置
// ============================================================================
OperationResult,
} from '../type';
/**
* API URL
* Remix API Routes/api/dataset/*
*/
const API_URL = '/api/dataset';
// ============================================================================
// 知识库 API
// ============================================================================
/**
*
*
* @param page - 1
* @param limit - 20
* @returns
*/
export async function fetchDatasets(
page: number = 1,
limit: number = 20
): Promise<DatasetsResponse> {
const params = new URLSearchParams({
page: page.toString(),
limit: limit.toString(),
});
const response = await axios.get<DatasetsResponse>(
`${API_URL}/datasets?${params}`,
{ withCredentials: true }
);
return response.data;
}
/**
*
*
* @param datasetId - ID
* @returns
*/
export async function fetchDataset(datasetId: string): Promise<Dataset> {
const response = await axios.get<Dataset>(
`${API_URL}/datasets/${datasetId}`,
{ withCredentials: true }
);
return response.data;
}
/**
*
*
* @param datasetId - ID
* @param data -
* @returns
*/
export async function updateDataset(
datasetId: string,
data: UpdateDatasetRequest
): Promise<Dataset> {
console.log('[Dataset Client] 更新知识库:', { datasetId, data });
const response = await axios.patch<Dataset>(
`${API_URL}/datasets/${datasetId}`,
data,
{
headers: { 'Content-Type': 'application/json' },
withCredentials: true,
}
);
return response.data;
}
// ============================================================================
// 文档 API
// ============================================================================
/**
*
*
@@ -199,101 +120,6 @@ export async function toggleDocumentStatus(
return response.data;
}
// ============================================================================
// 文档分段 API
// ============================================================================
/**
*
*
* @param datasetId - ID
* @param documentId - ID
* @param page - 1
* @param limit - 20
* @param keyword -
* @returns
*/
export async function fetchSegments(
datasetId: string,
documentId: string,
page: number = 1,
limit: number = 20,
keyword?: string
): Promise<SegmentsResponse> {
const params = new URLSearchParams({
page: page.toString(),
limit: limit.toString(),
});
if (keyword) {
params.append('keyword', keyword);
}
console.log('[Dataset Client] 获取分段列表:', { datasetId, documentId, page, limit });
const response = await axios.get<SegmentsResponse>(
`${API_URL}/datasets/${datasetId}/documents/${documentId}/segments?${params}`,
{ withCredentials: true }
);
return response.data;
}
/**
*
*
* @param datasetId - ID
* @param documentId - ID
* @param segmentId - ID
* @returns
*/
export async function deleteSegment(
datasetId: string,
documentId: string,
segmentId: string
): Promise<OperationResult> {
console.log('[Dataset Client] 删除分段:', { datasetId, documentId, segmentId });
const response = await axios.delete<OperationResult>(
`${API_URL}/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}`,
{ withCredentials: true }
);
return response.data;
}
/**
* /
* Dify API: POST /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}
*
*
* @param datasetId - ID
* @param documentId - ID
* @param segmentId - ID
* @param enabled -
* @returns
*/
export async function toggleSegmentStatus(
datasetId: string,
documentId: string,
segmentId: string,
enabled: boolean
): Promise<OperationResult> {
console.log('[Dataset Client] 切换分段状态:', { datasetId, documentId, segmentId, enabled });
const response = await axios.post<OperationResult>(
`${API_URL}/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}`,
{ segment: { enabled } },
{
headers: { 'Content-Type': 'application/json' },
withCredentials: true,
}
);
return response.data;
}
// ============================================================================
// 文件上传 API
// ============================================================================
/**
*
*
@@ -373,3 +199,56 @@ export async function fetchUploadFileInfo(
);
return response.data;
}
/**
*
*/
export interface ProcessRule {
mode: 'automatic' | 'custom';
rules?: {
pre_processing_rules?: Array<{
id: 'remove_extra_spaces' | 'remove_urls_emails';
enabled: boolean;
}>;
segmentation?: {
separator: string;
max_tokens: number;
};
};
}
/**
*
*/
export interface UpdateDocumentSettings {
indexing_technique?: 'high_quality' | 'economy';
process_rule?: ProcessRule;
}
/**
*
* Dify API
*
*
* @param datasetId - ID
* @param documentId - ID
* @param settings -
* @returns
*/
export async function updateDocumentWithSettings(
datasetId: string,
documentId: string,
settings: UpdateDocumentSettings
): Promise<OperationResult> {
console.log('[Dataset Client] 更新文档设置:', { datasetId, documentId, settings });
const response = await axios.post<OperationResult>(
`${API_URL}/datasets/${datasetId}/documents/${documentId}/update-settings`,
settings,
{
headers: { 'Content-Type': 'application/json' },
withCredentials: true,
}
);
return response.data;
}
+38
View File
@@ -0,0 +1,38 @@
/**
* Dify Dataset API 客户端统一导出
*
* @module api/dify-dataset/api
*/
// 知识库 API
export {
fetchDatasets,
fetchDataset,
updateDatasetName,
} from './datasetApi';
// 文档 API
export {
fetchDocuments,
fetchDocument,
deleteDocument,
toggleDocumentStatus,
uploadDocument,
fetchIndexingStatus,
fetchUploadFileInfo,
} from './documentApi';
// 分段、子分段、检索 API
export {
fetchSegments,
fetchSegment,
createSegments,
updateSegment,
deleteSegment,
toggleSegmentStatus,
fetchChildChunks,
createChildChunk,
updateChildChunk,
deleteChildChunk,
retrieveDataset,
} from './segmentApi';
+359
View File
@@ -0,0 +1,359 @@
/**
* Dify Dataset 分段、子分段、检索 API 模块
*
* 提供浏览器端调用 Dify 分段管理和检索 API 的函数
*
* @module api/dify-dataset/api/segmentApi
*/
import axios from 'axios';
import type {
Segment,
SegmentsResponse,
CreateSegmentRequest,
UpdateSegmentRequest,
CreateSegmentsResponse,
ChildChunk,
ChildChunksResponse,
CreateChildChunkResponse,
RetrieveRequest,
RetrieveResponse,
OperationResult,
} from '../type';
/**
* API 基础 URL
*/
const API_URL = '/api/dataset';
// ============================================================================
// 分段 API
// ============================================================================
/**
* 获取文档分段列表
*
* @param datasetId - 知识库 ID
* @param documentId - 文档 ID
* @param page - 页码,默认 1
* @param limit - 每页数量,默认 20
* @param keyword - 搜索关键词
* @returns 分段列表响应
*/
export async function fetchSegments(
datasetId: string,
documentId: string,
page: number = 1,
limit: number = 20,
keyword?: string
): Promise<SegmentsResponse> {
const params = new URLSearchParams({
page: page.toString(),
limit: limit.toString(),
});
if (keyword) {
params.append('keyword', keyword);
}
console.log('[Dataset Client] 获取分段列表:', { datasetId, documentId, page, limit });
const response = await axios.get<SegmentsResponse>(
`${API_URL}/datasets/${datasetId}/documents/${documentId}/segments?${params}`,
{ withCredentials: true }
);
return response.data;
}
/**
* 获取单个分段详情
*
* @param datasetId - 知识库 ID
* @param documentId - 文档 ID
* @param segmentId - 分段 ID
* @returns 分段详情
*/
export async function fetchSegment(
datasetId: string,
documentId: string,
segmentId: string
): Promise<Segment> {
console.log('[Dataset Client] 获取分段详情:', { datasetId, documentId, segmentId });
const response = await axios.get<{ data: Segment }>(
`${API_URL}/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}`,
{ withCredentials: true }
);
return response.data.data;
}
/**
* 新增分段(批量)
* Dify API: POST /datasets/{dataset_id}/documents/{document_id}/segments
*
* @param datasetId - 知识库 ID
* @param documentId - 文档 ID
* @param segments - 分段列表
* @returns 创建的分段列表
*/
export async function createSegments(
datasetId: string,
documentId: string,
segments: CreateSegmentRequest[]
): Promise<CreateSegmentsResponse> {
console.log('[Dataset Client] 新增分段:', { datasetId, documentId, count: segments.length });
const response = await axios.post<CreateSegmentsResponse>(
`${API_URL}/datasets/${datasetId}/documents/${documentId}/segments`,
{ segments },
{
headers: { 'Content-Type': 'application/json' },
withCredentials: true,
}
);
return response.data;
}
/**
* 更新分段内容
* Dify API: POST /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}
*
* @param datasetId - 知识库 ID
* @param documentId - 文档 ID
* @param segmentId - 分段 ID
* @param segment - 更新内容
* @returns 更新后的分段
*/
export async function updateSegment(
datasetId: string,
documentId: string,
segmentId: string,
segment: UpdateSegmentRequest
): Promise<{ data: Segment; doc_form: string }> {
console.log('[Dataset Client] 更新分段:', { datasetId, documentId, segmentId, segment });
const response = await axios.post<{ data: Segment; doc_form: string }>(
`${API_URL}/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}`,
{ segment },
{
headers: { 'Content-Type': 'application/json' },
withCredentials: true,
}
);
return response.data;
}
/**
* 删除分段
*
* @param datasetId - 知识库 ID
* @param documentId - 文档 ID
* @param segmentId - 分段 ID
* @returns 操作结果
*/
export async function deleteSegment(
datasetId: string,
documentId: string,
segmentId: string
): Promise<OperationResult> {
console.log('[Dataset Client] 删除分段:', { datasetId, documentId, segmentId });
const response = await axios.delete<OperationResult>(
`${API_URL}/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}`,
{ withCredentials: true }
);
return response.data;
}
/**
* 启用/禁用分段
* Dify API: POST /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}
* 通过更新分段的方式来切换状态
*
* @param datasetId - 知识库 ID
* @param documentId - 文档 ID
* @param segmentId - 分段 ID
* @param enabled - 是否启用
* @returns 操作结果
*/
export async function toggleSegmentStatus(
datasetId: string,
documentId: string,
segmentId: string,
enabled: boolean
): Promise<OperationResult> {
console.log('[Dataset Client] 切换分段状态:', { datasetId, documentId, segmentId, enabled });
const response = await axios.post<OperationResult>(
`${API_URL}/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}`,
{ segment: { enabled } },
{
headers: { 'Content-Type': 'application/json' },
withCredentials: true,
}
);
return response.data;
}
// ============================================================================
// 子分段 API(父子模式)
// ============================================================================
/**
* 获取子分段列表
* Dify API: GET /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks
*
* @param datasetId - 知识库 ID
* @param documentId - 文档 ID
* @param segmentId - 分段 ID
* @param page - 页码
* @param limit - 每页数量
* @param keyword - 搜索关键词
* @returns 子分段列表响应
*/
export async function fetchChildChunks(
datasetId: string,
documentId: string,
segmentId: string,
page: number = 1,
limit: number = 20,
keyword?: string
): Promise<ChildChunksResponse> {
const params = new URLSearchParams({
page: page.toString(),
limit: limit.toString(),
});
if (keyword) {
params.append('keyword', keyword);
}
console.log('[Dataset Client] 获取子分段列表:', { datasetId, documentId, segmentId, page, limit });
const response = await axios.get<ChildChunksResponse>(
`${API_URL}/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks?${params}`,
{ withCredentials: true }
);
return response.data;
}
/**
* 新增子分段
* Dify API: POST /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks
*
* @param datasetId - 知识库 ID
* @param documentId - 文档 ID
* @param segmentId - 分段 ID
* @param content - 子分段内容
* @returns 创建的子分段
*/
export async function createChildChunk(
datasetId: string,
documentId: string,
segmentId: string,
content: string
): Promise<CreateChildChunkResponse> {
console.log('[Dataset Client] 新增子分段:', { datasetId, documentId, segmentId });
const response = await axios.post<CreateChildChunkResponse>(
`${API_URL}/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks`,
{ content },
{
headers: { 'Content-Type': 'application/json' },
withCredentials: true,
}
);
return response.data;
}
/**
* 更新子分段
* Dify API: PATCH /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id}
*
* @param datasetId - 知识库 ID
* @param documentId - 文档 ID
* @param segmentId - 分段 ID
* @param childChunkId - 子分段 ID
* @param content - 更新内容
* @returns 更新后的子分段
*/
export async function updateChildChunk(
datasetId: string,
documentId: string,
segmentId: string,
childChunkId: string,
content: string
): Promise<ChildChunk> {
console.log('[Dataset Client] 更新子分段:', { datasetId, documentId, segmentId, childChunkId });
const response = await axios.patch<{ data: ChildChunk }>(
`${API_URL}/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks/${childChunkId}`,
{ content },
{
headers: { 'Content-Type': 'application/json' },
withCredentials: true,
}
);
return response.data.data;
}
/**
* 删除子分段
* Dify API: DELETE /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id}
*
* @param datasetId - 知识库 ID
* @param documentId - 文档 ID
* @param segmentId - 分段 ID
* @param childChunkId - 子分段 ID
* @returns 操作结果
*/
export async function deleteChildChunk(
datasetId: string,
documentId: string,
segmentId: string,
childChunkId: string
): Promise<OperationResult> {
console.log('[Dataset Client] 删除子分段:', { datasetId, documentId, segmentId, childChunkId });
const response = await axios.delete<OperationResult>(
`${API_URL}/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks/${childChunkId}`,
{ withCredentials: true }
);
return response.data;
}
// ============================================================================
// 检索 API
// ============================================================================
/**
* 检索知识库
* Dify API: POST /datasets/{dataset_id}/retrieve
*
* @param datasetId - 知识库 ID
* @param query - 检索关键词
* @param retrievalModel - 检索模型配置
* @returns 检索结果
*/
export async function retrieveDataset(
datasetId: string,
query: string,
retrievalModel?: RetrieveRequest['retrieval_model']
): Promise<RetrieveResponse> {
console.log('[Dataset Client] 检索知识库:', { datasetId, query });
const requestBody: RetrieveRequest = {
query,
retrieval_model: retrievalModel,
};
const response = await axios.post<RetrieveResponse>(
`${API_URL}/datasets/${datasetId}/retrieve`,
requestBody,
{
headers: { 'Content-Type': 'application/json' },
withCredentials: true,
}
);
return response.data;
}
+10 -41
View File
@@ -1,47 +1,16 @@
/**
* Dify Dataset API 模块统一导出
* Dify Dataset API 模块
*
* 推荐直接从子包导入:
* - 类型:import type { ... } from '~/api/dify-dataset/type'
* - APIimport { ... } from '~/api/dify-dataset/api'
* - 服务端:import { ... } from '~/api/dify-dataset/client.server'
*
* @module api/dify-dataset
*/
// 类型导出
export type {
Dataset,
DatasetsResponse,
Document,
DocumentsResponse,
Segment,
SegmentsResponse,
IndexingStatus,
OperationResult,
CreateDocumentResponse,
UploadProgress,
DocumentIndexingStatus,
IndexingStatusResponse,
UploadFileInfo,
RetrievalModel,
UpdateDatasetRequest,
} from './types';
// 类型子包重新导出
export * from './type';
// 客户端 API 导出(浏览器端使用 axios)
export {
// 知识库
fetchDatasets,
fetchDataset,
updateDataset,
// 文档
fetchDocuments,
fetchDocument,
deleteDocument,
toggleDocumentStatus,
uploadDocument,
fetchIndexingStatus,
fetchUploadFileInfo,
// 分段
fetchSegments,
deleteSegment,
toggleSegmentStatus,
} from './client';
// 服务端 API 请直接从 client.server.ts 导入
// import { difyDatasetFetch } from '~/api/dify-dataset/client.server';
// API 子包重新导出
export * from './api';
+13
View File
@@ -0,0 +1,13 @@
/**
* Dify Dataset API 通用类型定义
*
* @module api/dify-dataset/type/commonTypes
*/
/**
* 通用操作结果
*/
export interface OperationResult {
result: 'success' | 'error';
message?: string;
}
+66
View File
@@ -0,0 +1,66 @@
/**
* Dify Dataset API 知识库类型定义
*
* @module api/dify-dataset/type/datasetTypes
*/
/**
* 知识库信息
*/
export interface Dataset {
id: string;
name: string;
description: string;
permission: 'only_me' | 'all_team_members';
data_source_type: 'upload_file' | 'notion_import' | 'website_crawl';
indexing_technique: 'high_quality' | 'economy';
app_count: number;
document_count: number;
word_count: number;
created_by: string;
created_at: number;
updated_by: string;
updated_at: number;
}
/**
* 知识库列表响应
*/
export interface DatasetsResponse {
data: Dataset[];
has_more: boolean;
limit: number;
total: number;
page: number;
}
/**
* 检索模型配置
*/
export interface RetrievalModel {
search_method: 'keyword_search' | 'semantic_search' | 'full_text_search' | 'hybrid_search';
reranking_enable?: boolean;
reranking_mode?: string | null;
reranking_model?: {
reranking_provider_name: string;
reranking_model_name: string;
};
weights?: number | null;
top_k?: number;
score_threshold_enabled?: boolean;
score_threshold?: number | null;
}
/**
* 更新知识库请求参数
*/
export interface UpdateDatasetRequest {
name?: string;
description?: string;
indexing_technique?: 'high_quality' | 'economy';
permission?: 'only_me' | 'all_team_members' | 'partial_members';
embedding_model_provider?: string;
embedding_model?: string;
retrieval_model?: RetrievalModel;
partial_member_list?: string[];
}
+116
View File
@@ -0,0 +1,116 @@
/**
* Dify Dataset API 文档类型定义
*
* @module api/dify-dataset/type/documentTypes
*/
/**
* 文档索引状态
*/
export type IndexingStatus =
| 'waiting'
| 'parsing'
| 'cleaning'
| 'splitting'
| 'indexing'
| 'paused'
| 'error'
| 'completed';
/**
* 文档信息
*/
export interface Document {
id: string;
position: number;
data_source_type: 'upload_file' | 'notion_import' | 'website_crawl';
data_source_info: {
upload_file_id?: string;
notion_page_id?: string;
website_url?: string;
};
dataset_process_rule_id: string;
name: string;
created_from: string;
created_by: string;
created_at: number;
tokens: number;
indexing_status: IndexingStatus;
error?: string;
enabled: boolean;
disabled_at?: number;
disabled_by?: string;
archived: boolean;
display_status: string;
word_count: number;
hit_count: number;
doc_form: string;
}
/**
* 文档列表响应
*/
export interface DocumentsResponse {
data: Document[];
has_more: boolean;
limit: number;
total: number;
page: number;
}
/**
* 创建文档响应
*/
export interface CreateDocumentResponse {
document: Document;
batch: string;
}
/**
* 文档上传进度
*/
export interface UploadProgress {
loaded: number;
total: number;
percent: number;
}
/**
* 单个文档的索引状态
*/
export interface DocumentIndexingStatus {
id: string;
indexing_status: IndexingStatus;
processing_started_at: number | null;
parsing_completed_at: number | null;
cleaning_completed_at: number | null;
splitting_completed_at: number | null;
completed_at: number | null;
paused_at: number | null;
error: string | null;
stopped_at: number | null;
completed_segments: number;
total_segments: number;
}
/**
* 批量文档索引状态响应
*/
export interface IndexingStatusResponse {
data: DocumentIndexingStatus[];
}
/**
* 上传文件信息
*/
export interface UploadFileInfo {
id: string;
name: string;
size: number;
extension: string;
url: string;
download_url: string;
mime_type: string;
created_by: string;
created_at: number;
}
+47
View File
@@ -0,0 +1,47 @@
/**
* Dify Dataset API 类型定义统一导出
*
* @module api/dify-dataset/type
*/
// 通用类型
export type { OperationResult } from './commonTypes';
// 知识库类型
export type {
Dataset,
DatasetsResponse,
RetrievalModel,
UpdateDatasetRequest,
} from './datasetTypes';
// 文档类型
export type {
IndexingStatus,
Document,
DocumentsResponse,
CreateDocumentResponse,
UploadProgress,
DocumentIndexingStatus,
IndexingStatusResponse,
UploadFileInfo,
} from './documentTypes';
// 分段、子分段、检索类型
export type {
Segment,
SegmentsResponse,
CreateSegmentRequest,
UpdateSegmentRequest,
CreateSegmentsResponse,
ChildChunk,
ChildChunksResponse,
CreateChildChunkRequest,
UpdateChildChunkRequest,
CreateChildChunkResponse,
MetadataFilterCondition,
MetadataFilteringConditions,
RetrieveRequest,
RetrieveRecord,
RetrieveResponse,
} from './segmentTypes';
+191
View File
@@ -0,0 +1,191 @@
/**
* Dify Dataset API 分段、子分段、检索类型定义
*
* @module api/dify-dataset/type/segmentTypes
*/
// ============================================================================
// 分段类型
// ============================================================================
/**
* 文档分段
*/
export interface Segment {
id: string;
position: number;
document_id: string;
content: string;
answer?: string;
word_count: number;
tokens: number;
keywords: string[];
index_node_id: string;
index_node_hash: string;
hit_count: number;
enabled: boolean;
disabled_at?: number;
disabled_by?: string;
status: 'waiting' | 'completed' | 'error' | 'indexing';
created_by: string;
created_at: number;
indexing_at?: number;
completed_at?: number;
error?: string;
stopped_at?: number;
}
/**
* 分段列表响应
*/
export interface SegmentsResponse {
data: Segment[];
has_more: boolean;
limit: number;
total: number;
}
/**
* 创建分段请求参数
*/
export interface CreateSegmentRequest {
content: string;
answer?: string;
keywords?: string[];
}
/**
* 更新分段请求参数
*/
export interface UpdateSegmentRequest {
content?: string;
answer?: string;
keywords?: string[];
enabled?: boolean;
}
/**
* 创建分段响应
*/
export interface CreateSegmentsResponse {
data: Segment[];
doc_form: string;
}
// ============================================================================
// 子分段类型(父子模式)
// ============================================================================
/**
* 子分段信息
*/
export interface ChildChunk {
id: string;
segment_id: string;
content: string;
word_count: number;
tokens: number;
index_node_id: string;
index_node_hash: string;
hit_count: number;
created_by: string;
created_at: number;
updated_by?: string;
updated_at?: number;
status: 'waiting' | 'completed' | 'error' | 'indexing';
error?: string;
}
/**
* 子分段列表响应
*/
export interface ChildChunksResponse {
data: ChildChunk[];
has_more: boolean;
limit: number;
total: number;
page: number;
}
/**
* 创建子分段请求参数
*/
export interface CreateChildChunkRequest {
content: string;
}
/**
* 更新子分段请求参数
*/
export interface UpdateChildChunkRequest {
content: string;
}
/**
* 创建子分段响应
*/
export interface CreateChildChunkResponse {
data: ChildChunk;
}
// ============================================================================
// 检索功能类型
// ============================================================================
/**
* 元数据过滤条件
*/
export interface MetadataFilterCondition {
name: string;
comparison_operator: 'contains' | 'not contains' | 'start with' | 'end with' | 'is' | 'is not' | 'empty' | 'not empty' | 'before' | 'after' | '=' | '!=' | '>' | '<' | '>=' | '<=';
value?: string | number;
}
/**
* 元数据过滤条件组
*/
export interface MetadataFilteringConditions {
logical_operator: 'and' | 'or';
conditions: MetadataFilterCondition[];
}
/**
* 检索请求参数
*/
export interface RetrieveRequest {
query: string;
retrieval_model?: {
search_method: 'keyword_search' | 'semantic_search' | 'full_text_search' | 'hybrid_search';
reranking_enable?: boolean;
reranking_model?: {
reranking_provider_name: string;
reranking_model_name: string;
};
top_k?: number;
score_threshold_enabled?: boolean;
score_threshold?: number;
};
metadata_filtering_conditions?: MetadataFilteringConditions;
}
/**
* 检索结果记录
*/
export interface RetrieveRecord {
segment: Segment;
score: number;
tsne_position?: {
x: number;
y: number;
};
}
/**
* 检索响应
*/
export interface RetrieveResponse {
query: {
content: string;
};
records: RetrieveRecord[];
}
-250
View File
@@ -1,250 +0,0 @@
/**
* Dify Dataset API 类型定义
*
* @module api/dify-dataset/types
*/
// ============================================================================
// 知识库类型
// ============================================================================
/**
* 知识库信息
*/
export interface Dataset {
id: string;
name: string;
description: string;
permission: 'only_me' | 'all_team_members';
data_source_type: 'upload_file' | 'notion_import' | 'website_crawl';
indexing_technique: 'high_quality' | 'economy';
app_count: number;
document_count: number;
word_count: number;
created_by: string;
created_at: number;
updated_by: string;
updated_at: number;
}
/**
* 知识库列表响应
*/
export interface DatasetsResponse {
data: Dataset[];
has_more: boolean;
limit: number;
total: number;
page: number;
}
// ============================================================================
// 文档类型
// ============================================================================
/**
* 文档索引状态
*/
export type IndexingStatus =
| 'waiting'
| 'parsing'
| 'cleaning'
| 'splitting'
| 'indexing'
| 'paused'
| 'error'
| 'completed';
/**
* 文档信息
*/
export interface Document {
id: string;
position: number;
data_source_type: 'upload_file' | 'notion_import' | 'website_crawl';
data_source_info: {
upload_file_id?: string;
notion_page_id?: string;
website_url?: string;
};
dataset_process_rule_id: string;
name: string;
created_from: string;
created_by: string;
created_at: number;
tokens: number;
indexing_status: IndexingStatus;
error?: string;
enabled: boolean;
disabled_at?: number;
disabled_by?: string;
archived: boolean;
display_status: string;
word_count: number;
hit_count: number;
doc_form: string;
}
/**
* 文档列表响应
*/
export interface DocumentsResponse {
data: Document[];
has_more: boolean;
limit: number;
total: number;
page: number;
}
// ============================================================================
// 文档分段类型
// ============================================================================
/**
* 文档分段
*/
export interface Segment {
id: string;
position: number;
document_id: string;
content: string;
answer?: string;
word_count: number;
tokens: number;
keywords: string[];
index_node_id: string;
index_node_hash: string;
hit_count: number;
enabled: boolean;
disabled_at?: number;
disabled_by?: string;
status: 'waiting' | 'completed' | 'error' | 'indexing';
created_by: string;
created_at: number;
indexing_at?: number;
completed_at?: number;
error?: string;
stopped_at?: number;
}
/**
* 分段列表响应
*/
export interface SegmentsResponse {
data: Segment[];
has_more: boolean;
limit: number;
total: number;
}
// ============================================================================
// 操作响应类型
// ============================================================================
/**
* 通用操作结果
*/
export interface OperationResult {
result: 'success' | 'error';
message?: string;
}
/**
* 创建文档响应
*/
export interface CreateDocumentResponse {
document: Document;
batch: string;
}
/**
* 文档上传进度
*/
export interface UploadProgress {
loaded: number;
total: number;
percent: number;
}
// ============================================================================
// 索引状态类型
// ============================================================================
/**
* 单个文档的索引状态
*/
export interface DocumentIndexingStatus {
id: string;
indexing_status: IndexingStatus;
processing_started_at: number | null;
parsing_completed_at: number | null;
cleaning_completed_at: number | null;
splitting_completed_at: number | null;
completed_at: number | null;
paused_at: number | null;
error: string | null;
stopped_at: number | null;
completed_segments: number;
total_segments: number;
}
/**
* 批量文档索引状态响应
*/
export interface IndexingStatusResponse {
data: DocumentIndexingStatus[];
}
// ============================================================================
// 上传文件信息类型
// ============================================================================
/**
* 上传文件信息
*/
export interface UploadFileInfo {
id: string;
name: string;
size: number;
extension: string;
url: string;
download_url: string;
mime_type: string;
created_by: string;
created_at: number;
}
// ============================================================================
// 知识库更新类型
// ============================================================================
/**
* 检索模型配置
*/
export interface RetrievalModel {
search_method: 'keyword_search' | 'semantic_search' | 'full_text_search' | 'hybrid_search';
reranking_enable?: boolean;
reranking_mode?: string | null;
reranking_model?: {
reranking_provider_name: string;
reranking_model_name: string;
};
weights?: number | null;
top_k?: number;
score_threshold_enabled?: boolean;
score_threshold?: number | null;
}
/**
* 更新知识库请求参数
*/
export interface UpdateDatasetRequest {
name?: string;
description?: string;
indexing_technique?: 'high_quality' | 'economy';
permission?: 'only_me' | 'all_team_members' | 'partial_members';
embedding_model_provider?: string;
embedding_model?: string;
retrieval_model?: RetrievalModel;
partial_member_list?: string[];
}
@@ -0,0 +1,187 @@
import { useState, useEffect } from 'react';
import { Form, Input, Button, Card, message, Spin } from 'antd';
import { SaveOutlined } from '@ant-design/icons';
import type { Dataset } from '~/api/dify-dataset/type/datasetTypes';
import { updateDatasetName } from '~/api/dify-dataset/api/datasetApi';
const { TextArea } = Input;
interface DatasetSettingsProps {
dataset: Dataset | null;
onDatasetUpdated: (dataset: Dataset) => void;
}
/**
* 知识库设置组件
* 用于修改知识库名称和描述
*/
export default function DatasetSettings({
dataset,
onDatasetUpdated,
}: DatasetSettingsProps) {
const [form] = Form.useForm();
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 = () => {
const values = form.getFieldsValue();
const changed =
values.name !== dataset?.name ||
values.description !== (dataset?.description || '');
setHasChanges(changed);
};
/**
* 保存设置
*/
const handleSave = 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);
}
};
/**
* 重置表单
*/
const handleReset = () => {
if (dataset) {
form.setFieldsValue({
name: dataset.name,
description: dataset.description || '',
});
setHasChanges(false);
}
};
if (!dataset) {
return (
<div className="settings-loading">
<Spin size="large" />
</div>
);
}
return (
<div className="dataset-settings-page">
{/* 页面标题 */}
<div className="page-header">
<h1></h1>
<p className="page-description">
</p>
</div>
{/* 设置表单 */}
<Card className="settings-card">
<Form
form={form}
layout="vertical"
onValuesChange={handleValuesChange}
>
<Form.Item
name="name"
label="知识库名称"
rules={[
{ required: true, message: '请输入知识库名称' },
{ max: 100, message: '名称不能超过100个字符' },
]}
>
<Input placeholder="请输入知识库名称" maxLength={100} />
</Form.Item>
<Form.Item
name="description"
label="知识库描述"
extra="描述知识库的用途和内容(仅展示,暂不支持修改)"
>
<TextArea
placeholder="请输入知识库描述"
rows={4}
maxLength={500}
showCount
disabled
/>
</Form.Item>
{/* 只读信息 */}
<div className="readonly-info">
<div className="info-item">
<span className="info-label"></span>
<span className="info-value">
{dataset.indexing_technique === 'high_quality' ? '高质量' : '经济'}
</span>
</div>
{/* <div className="info-item">
<span className="info-label">Embedding 模型:</span>
<span className="info-value">
{dataset.embedding_model || '默认模型'}
</span>
</div> */}
<div className="info-item">
<span className="info-label"></span>
<span className="info-value">{dataset.document_count}</span>
</div>
<div className="info-item">
<span className="info-label"></span>
<span className="info-value">{dataset.word_count?.toLocaleString()}</span>
</div>
<div className="info-item">
<span className="info-label"></span>
<span className="info-value">
{new Date(dataset.created_at * 1000).toLocaleString('zh-CN')}
</span>
</div>
</div>
{/* 操作按钮 */}
<div className="form-actions">
<Button onClick={handleReset} disabled={!hasChanges}>
</Button>
<Button
type="primary"
icon={<SaveOutlined />}
onClick={handleSave}
loading={saving}
disabled={!hasChanges}
>
</Button>
</div>
</Form>
</Card>
</div>
);
}
@@ -0,0 +1,367 @@
import { useState, useEffect } from 'react';
import {
Input,
Button,
InputNumber,
Checkbox,
Select,
Card,
Empty,
Spin,
message,
Divider,
Tooltip,
} from 'antd';
import {
QuestionCircleOutlined,
ReloadOutlined,
EyeOutlined,
} from '@ant-design/icons';
import type { Document } from '~/api/dify-dataset/type/documentTypes';
import type { Segment } from '~/api/dify-dataset/type';
import { fetchSegments } from '~/api/dify-dataset/api/segmentApi';
import { updateDocumentWithSettings } from '~/api/dify-dataset/api/documentApi';
interface DocumentDetailProps {
datasetId: string;
document: Document | null;
}
/**
* 分段设置配置
* 注意:Dify API 支持的参数有限
* - separator: ✅ 支持
* - maxTokens: ✅ 支持
* - removeExtraSpaces: ✅ 支持
* - removeUrlsEmails: ✅ 支持
* - useQASegment: ⚠️ 需要 doc_form: "qa_model"
*/
interface SegmentationSettings {
separator: string;
maxTokens: number;
removeExtraSpaces: boolean;
removeUrlsEmails: boolean;
useQASegment: boolean;
qaLanguage: string;
}
/**
* 默认分段设置
*/
const DEFAULT_SETTINGS: SegmentationSettings = {
separator: '\\n\\n',
maxTokens: 500,
removeExtraSpaces: true,
removeUrlsEmails: false,
useQASegment: false,
qaLanguage: 'Chinese',
};
/**
* 文档详情组件
* 显示文档的分段设置,支持修改并重新处理
*/
export default function DocumentDetail({
datasetId,
document,
}: DocumentDetailProps) {
// 分段设置状态
const [settings, setSettings] = useState<SegmentationSettings>(DEFAULT_SETTINGS);
// 预览状态
const [previewSegments, setPreviewSegments] = useState<Segment[]>([]);
const [previewLoading, setPreviewLoading] = useState(false);
const [showPreview, setShowPreview] = useState(false);
// 保存状态
const [saving, setSaving] = useState(false);
// 当文档变化时重置设置
useEffect(() => {
if (document) {
// 可以从文档中读取已有的设置,这里使用默认值
setSettings(DEFAULT_SETTINGS);
setPreviewSegments([]);
setShowPreview(false);
}
}, [document?.id]);
/**
* 更新设置
*/
const updateSettings = (key: keyof SegmentationSettings, value: any) => {
setSettings(prev => ({ ...prev, [key]: value }));
};
/**
* 重置设置
*/
const handleReset = () => {
setSettings(DEFAULT_SETTINGS);
setPreviewSegments([]);
setShowPreview(false);
};
/**
* 预览分段
*/
const handlePreview = 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);
}
};
/**
* 保存并处理
*/
const handleSaveAndProcess = 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);
}
};
if (!document) {
return (
<div className="document-detail-empty">
<Empty description="请选择一个文档" />
</div>
);
}
return (
<div className="document-detail-page">
<div className="document-detail-content">
{/* 左侧设置区域 */}
<div className="settings-panel">
{/* 分段设置 */}
<div className="settings-section">
<h3 className="section-title"></h3>
{/* 分块模式 */}
<div className="setting-item mode-selector">
<div className="mode-option active">
<div className="mode-icon">
<i className="ri-text-spacing"></i>
</div>
<div className="mode-info">
<span className="mode-name"></span>
<span className="mode-desc"></span>
</div>
</div>
</div>
{/* 分段标识符 */}
<div className="setting-item">
<label className="setting-label">
<Tooltip title="系统会在遇到指定分隔符时自动分段,默认值为 \n\n(按段落分段)">
<QuestionCircleOutlined className="help-icon" />
</Tooltip>
</label>
<Input
value={settings.separator}
onChange={(e) => updateSettings('separator', e.target.value)}
placeholder="\n\n"
className="setting-input"
/>
</div>
{/* 分段最大长度 */}
<div className="setting-item">
<label className="setting-label">
<Tooltip title="指定每个分段允许的最大字符数,超过此限制系统会强制分段">
<QuestionCircleOutlined className="help-icon" />
</Tooltip>
</label>
<div className="setting-input-with-suffix">
<InputNumber
value={settings.maxTokens}
onChange={(value) => updateSettings('maxTokens', value || 500)}
min={100}
max={4000}
className="setting-input-number"
/>
<span className="input-suffix">characters</span>
</div>
</div>
</div>
<Divider />
{/* 文本预处理规则 */}
<div className="settings-section">
<h3 className="section-title"></h3>
<div className="checkbox-group">
<Checkbox
checked={settings.removeExtraSpaces}
onChange={(e) => updateSettings('removeExtraSpaces', e.target.checked)}
>
</Checkbox>
<Checkbox
checked={settings.removeUrlsEmails}
onChange={(e) => updateSettings('removeUrlsEmails', e.target.checked)}
>
URL
</Checkbox>
</div>
</div>
<Divider />
{/* Q&A 分段 */}
<div className="settings-section">
<div className="qa-segment-row">
<Checkbox
checked={settings.useQASegment}
onChange={(e) => updateSettings('useQASegment', e.target.checked)}
>
使 Q&A
</Checkbox>
<Select
value={settings.qaLanguage}
onChange={(value) => updateSettings('qaLanguage', value)}
disabled={!settings.useQASegment}
style={{ width: 120 }}
options={[
{ value: 'Chinese', label: 'Chinese' },
{ value: 'English', label: 'English' },
{ value: 'Japanese', label: 'Japanese' },
{ value: 'Korean', label: 'Korean' },
]}
/>
</div>
</div>
{/* 操作按钮 */}
<div className="settings-actions">
<Button
icon={<EyeOutlined />}
onClick={handlePreview}
loading={previewLoading}
>
</Button>
<Button
icon={<ReloadOutlined />}
onClick={handleReset}
>
</Button>
</div>
<Divider />
{/* 保存并处理按钮 */}
<div className="save-actions">
<Button
type="primary"
onClick={handleSaveAndProcess}
loading={saving}
block
>
</Button>
<Button block style={{ marginTop: 8 }}>
</Button>
</div>
</div>
{/* 右侧预览区域 */}
<div className="preview-panel">
<Card
title={
<div className="preview-header">
<span></span>
<Select
value={document.name}
style={{ width: 200 }}
disabled
options={[{ value: document.name, label: document.name }]}
/>
<span className="segment-count">
{showPreview ? `${previewSegments.length} 段块` : '0 段块'}
</span>
</div>
}
className="preview-card"
>
{previewLoading ? (
<div className="preview-loading">
<Spin size="large" />
<div className="loading-text">...</div>
</div>
) : !showPreview ? (
<div className="preview-empty">
<div className="empty-icon">
<EyeOutlined />
</div>
<p>"预览块"</p>
</div>
) : previewSegments.length === 0 ? (
<Empty description="暂无分段数据" />
) : (
<div className="preview-segments">
{previewSegments.map((segment, index) => (
<div key={segment.id} className="segment-item">
<div className="segment-header">
<span className="segment-index">#{index + 1}</span>
<span className="segment-chars">
{segment.word_count}
</span>
</div>
<div className="segment-content">
{segment.content}
</div>
</div>
))}
</div>
)}
</Card>
</div>
</div>
</div>
);
}
@@ -27,8 +27,8 @@ import {
PauseCircleOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import type { Document, IndexingStatus } from '~/api/dify-dataset';
import { deleteDocument, toggleDocumentStatus, uploadDocument } from '~/api/dify-dataset';
import type { Document, IndexingStatus } from '~/api/dify-dataset/type/documentTypes';
import { deleteDocument, toggleDocumentStatus, uploadDocument } from '~/api/dify-dataset/api/documentApi';
import '../../styles/components/dify-dataset-manager/index.css';
interface DocumentListProps {
@@ -43,6 +43,7 @@ interface DocumentListProps {
onDocumentDeleted: (documentId: string) => void;
onDocumentStatusChanged: (documentId: string, enabled: boolean) => void;
onRefresh: () => void;
onViewDocument?: (document: Document) => void;
}
/**
@@ -50,7 +51,6 @@ interface DocumentListProps {
*/
export default function DocumentList({
datasetId,
datasetName,
documents,
loading,
total,
@@ -60,6 +60,7 @@ export default function DocumentList({
onDocumentDeleted,
onDocumentStatusChanged,
onRefresh,
onViewDocument,
}: DocumentListProps) {
const [searchValue, setSearchValue] = useState('');
const [uploading, setUploading] = useState(false);
@@ -238,15 +239,12 @@ export default function DocumentList({
width: 120,
render: (_, record) => (
<Space size="small">
<Tooltip title="查看详情">
<Tooltip title="查看分段">
<Button
type="text"
size="small"
icon={<EyeOutlined />}
onClick={() => {
// TODO: 查看文档详情/分段
message.info('功能开发中');
}}
onClick={() => onViewDocument?.(record)}
/>
</Tooltip>
<Popconfirm
@@ -273,11 +271,16 @@ export default function DocumentList({
];
return (
<div className="dataset-content">
{/* 头部区域 */}
<div className="dataset-header">
<h1>{datasetName || '知识库文档'}</h1>
<div className="dataset-header-actions">
<div className="document-list-page">
{/* 页面头部 */}
<div className="page-header">
<div className="header-left">
<h1></h1>
{/* <p className="page-description">
知识库的所有文件都在这里显示,整个知识库都可以被接到 Dify 引用或通过 Chat 插件进行索引。
</p> */}
</div>
<div className="header-actions">
<Tooltip title="刷新">
<Button
icon={<ReloadOutlined />}
@@ -297,36 +300,34 @@ export default function DocumentList({
loading={uploading}
disabled={!datasetId}
>
</Button>
</Upload>
</div>
</div>
{/* 工具栏 */}
<div className="document-list-toolbar">
{/* 搜索栏 */}
<div className="document-search-bar">
<Input
className="document-list-search"
placeholder="搜索文档..."
prefix={<SearchOutlined />}
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
allowClear
style={{ width: 280 }}
/>
</div>
{/* 文档表格 - 固定表头和分页 */}
<div className="document-table-container">
{/* 文档表格 */}
<div className="document-table-wrapper">
{loading && documents.length === 0 ? (
<div className="dataset-loading">
<div className="loading-state">
<Spin size="large" />
<span className="text-gray-500">...</span>
<div className="loading-text">...</div>
</div>
) : filteredDocuments.length === 0 ? (
<div className="dataset-empty">
<Empty
description={searchValue ? '未找到匹配的文档' : '暂无文档'}
>
<div className="empty-state">
<Empty description={searchValue ? '未找到匹配的文档' : '暂无文档'}>
{!searchValue && (
<Upload
beforeUpload={handleUpload}
@@ -354,7 +355,7 @@ export default function DocumentList({
)}
</div>
{/* 固定底部分页器 */}
{/* 底部分页器 */}
{filteredDocuments.length > 0 && (
<div className="document-pagination">
<span className="pagination-total"> {total} </span>
+105 -18
View File
@@ -1,13 +1,19 @@
import { useEffect, useState } from 'react';
import { message, Spin } from 'antd';
import DatasetLayout, { type MenuTab } from './layout';
import DocumentList from './document-list';
import type { Dataset, Document } from '~/api/dify-dataset';
import { fetchDatasets, fetchDocuments } from '~/api/dify-dataset';
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 '../../styles/components/dify-dataset-manager/index.css';
/**
* 知识库管理主组件
* 简化版 - 假设只有一个知识库,直接显示文档列表
* 带左侧菜单栏的完整布局
*/
export default function DatasetManager() {
// 知识库状态
@@ -25,6 +31,12 @@ export default function DatasetManager() {
const [inited, setInited] = useState(false);
const [error, setError] = useState<string | null>(null);
// 菜单状态
const [activeTab, setActiveTab] = useState<MenuTab>('documents');
// 选中的文档(用于查看文档详情)
const [selectedDocument, setSelectedDocument] = useState<Document | null>(null);
/**
* 加载知识库(获取第一个知识库)
*/
@@ -123,6 +135,39 @@ export default function DatasetManager() {
}
};
/**
* 查看文档详情(分段管理)
*/
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();
@@ -132,11 +177,9 @@ export default function DatasetManager() {
if (!inited || loadingDataset) {
return (
<div className="dataset-manager-wrapper">
<div className="dataset-manager-card">
<div className="dataset-loading-state">
<Spin size="large" />
<span className="loading-text">...</span>
</div>
<div className="dataset-loading-state">
<Spin size="large" />
<span className="loading-text">...</span>
</div>
</div>
);
@@ -146,20 +189,32 @@ export default function DatasetManager() {
if (error) {
return (
<div className="dataset-manager-wrapper">
<div className="dataset-manager-card">
<div className="dataset-error-state">
<i className="ri-error-warning-line error-icon"></i>
<h3></h3>
<p>{error}</p>
</div>
<div className="dataset-error-state">
<i className="ri-error-warning-line error-icon"></i>
<h3></h3>
<p>{error}</p>
</div>
</div>
);
}
return (
<div className="dataset-manager-wrapper">
<div className="dataset-manager-card">
/**
* 渲染右侧内容区
*/
const renderContent = () => {
// 文档菜单
if (activeTab === 'documents') {
// 如果选中了文档,显示文档详情
if (selectedDocument) {
return (
<DocumentDetail
datasetId={dataset?.id || ''}
document={selectedDocument}
/>
);
}
// 否则显示文档列表
return (
<DocumentList
datasetId={dataset?.id || ''}
datasetName={dataset?.name || ''}
@@ -172,8 +227,40 @@ export default function DatasetManager() {
onDocumentDeleted={handleDocumentDeleted}
onDocumentStatusChanged={handleDocumentStatusChanged}
onRefresh={handleRefresh}
onViewDocument={handleViewDocument}
/>
</div>
);
}
// 召回测试菜单
if (activeTab === 'retrieve') {
return <RetrieveTest datasetId={dataset?.id || ''} />;
}
// 设置菜单
if (activeTab === 'settings') {
return (
<DatasetSettings
dataset={dataset}
onDatasetUpdated={handleDatasetUpdated}
/>
);
}
return null;
};
return (
<div className="dataset-manager-wrapper">
<DatasetLayout
dataset={dataset}
activeTab={activeTab}
onTabChange={handleTabChange}
showBackButton={activeTab === 'documents' && !!selectedDocument}
onBack={handleBackToDocuments}
>
{renderContent()}
</DatasetLayout>
</div>
);
}
@@ -0,0 +1,109 @@
import { ReactNode } from 'react';
import { Button, Tooltip } from 'antd';
import {
FileTextOutlined,
SearchOutlined,
SettingOutlined,
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;
}
/**
* 知识库布局组件
* 包含左侧菜单栏和右侧内容区
*/
export default function DatasetLayout({
dataset,
activeTab,
onTabChange,
showBackButton = false,
onBack,
children,
}: DatasetLayoutProps) {
const menuItems: { key: MenuTab; icon: ReactNode; label: string }[] = [
{ key: 'documents', icon: <FileTextOutlined />, label: '文档' },
{ key: 'retrieve', icon: <SearchOutlined />, label: '召回测试' },
{ key: 'settings', icon: <SettingOutlined />, label: '设置' },
];
return (
<div className="dataset-layout">
{/* 左侧侧边栏 */}
<aside className="dataset-sidebar">
{/* 返回按钮 */}
{showBackButton && onBack && (
<div className="sidebar-back">
<Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={onBack}
className="back-btn"
>
</Button>
</div>
)}
{/* 知识库信息 */}
<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>
</div>
{/* 统计信息 */}
<div className="sidebar-stats">
<span className="stat-item">
{dataset?.document_count || 0}
</span>
</div>
{/* 导航菜单 */}
<nav className="sidebar-menu">
{menuItems.map((item) => (
<button
key={item.key}
className={`menu-item ${activeTab === item.key ? 'active' : ''}`}
onClick={() => onTabChange(item.key)}
>
<span className="menu-icon">{item.icon}</span>
<span className="menu-label">{item.label}</span>
</button>
))}
</nav>
</aside>
{/* 右侧内容区 */}
<main className="dataset-main">
{children}
</main>
</div>
);
}
@@ -0,0 +1,202 @@
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 type { RetrieveRecord } from '~/api/dify-dataset/type';
import { retrieveDataset } from '~/api/dify-dataset/api/segmentApi';
interface RetrieveTestProps {
datasetId: string;
}
/**
* 召回测试组件
* 用于测试知识库的检索效果
*/
export default function RetrieveTest({ datasetId }: RetrieveTestProps) {
const [searchQuery, setSearchQuery] = useState('');
const [retrieveResults, setRetrieveResults] = useState<RetrieveRecord[]>([]);
const [retrieving, setRetrieving] = useState(false);
const [searchMethod, setSearchMethod] = useState<string>('hybrid_search');
const [topK, setTopK] = useState<number>(5);
/**
* 执行检索
*/
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<RetrieveRecord> = [
{
title: '相关度',
dataIndex: 'score',
key: 'score',
width: 100,
render: (score: number) => (
<Tag color={score > 0.8 ? 'green' : score > 0.5 ? 'orange' : 'default'}>
{(score * 100).toFixed(1)}%
</Tag>
),
},
{
title: '内容',
key: 'content',
render: (_, record) => (
<div className="retrieve-result-content">
<div className="content-text">
{record.segment.content.length > 300
? record.segment.content.substring(0, 300) + '...'
: record.segment.content}
</div>
{record.segment.answer && (
<div className="answer-text">
<strong></strong>
{record.segment.answer.length > 150
? record.segment.answer.substring(0, 150) + '...'
: record.segment.answer}
</div>
)}
</div>
),
},
{
title: '字数',
key: 'word_count',
width: 80,
render: (_, record) => record.segment.word_count,
},
{
title: '命中次数',
key: 'hit_count',
width: 100,
render: (_, record) => record.segment.hit_count,
},
];
return (
<div className="retrieve-test-page">
{/* 页面标题 */}
<div className="page-header">
<h1></h1>
<p className="page-description">
</p>
</div>
{/* 检索设置 */}
<Card className="retrieve-settings" size="small">
<div className="search-row">
<Input
placeholder="输入检索关键词..."
prefix={<FileSearchOutlined />}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onPressEnter={handleRetrieve}
className="search-input"
/>
<Button
type="primary"
onClick={handleRetrieve}
loading={retrieving}
>
</Button>
</div>
<div className="options-row">
<div className="option-item">
<span className="option-label"></span>
<Select
value={searchMethod}
onChange={setSearchMethod}
style={{ width: 140 }}
options={[
{ value: 'keyword_search', label: '关键词搜索' },
{ value: 'semantic_search', label: '语义搜索' },
{ value: 'full_text_search', label: '全文搜索' },
{ value: 'hybrid_search', label: '混合搜索' },
]}
/>
</div>
<div className="option-item">
<span className="option-label"> (Top K)</span>
<Slider
value={topK}
onChange={setTopK}
min={1}
max={20}
style={{ width: 120 }}
/>
<span className="option-value">{topK}</span>
</div>
</div>
</Card>
{/* 检索结果 */}
<div className="retrieve-results">
{retrieving ? (
<div className="loading-state">
<Spin size="large" />
<div className="loading-text">...</div>
</div>
) : retrieveResults.length === 0 ? (
<Empty
description="请输入关键词进行检索"
className="empty-state"
/>
) : (
<>
<div className="results-header">
<span> {retrieveResults.length} </span>
</div>
<Table
columns={columns}
dataSource={retrieveResults}
rowKey={(record) => record.segment.id}
pagination={false}
size="small"
/>
</>
)}
</div>
</div>
);
}
@@ -0,0 +1,93 @@
import { type ActionFunctionArgs } from '@remix-run/node';
import { API_BASE_URL } from '~/config/api-config';
/**
* PATCH /api/dataset/datasets/:datasetId/documents/:documentId/segments/:segmentId/child_chunks/:childChunkId - 更新子分段
* DELETE /api/dataset/datasets/:datasetId/documents/:documentId/segments/:segmentId/child_chunks/:childChunkId - 删除子分段
*
* Dify API:
* - PATCH /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id}
* - DELETE /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id}
*/
export async function action({ request, params }: ActionFunctionArgs) {
try {
const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT } = await getUserSession(request);
if (!frontendJWT) {
return new Response(
JSON.stringify({ error: 'JWT认证失败,请重新登录' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
}
const { datasetId, documentId, segmentId, childChunkId } = params;
if (!datasetId || !documentId || !segmentId || !childChunkId) {
return new Response(
JSON.stringify({ error: '缺少必要参数' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
const method = request.method;
const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks/${childChunkId}`;
if (method === 'DELETE') {
console.log('[API] Delete Child Chunk:', { datasetId, documentId, segmentId, childChunkId });
const response = await fetch(apiUrl, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${frontendJWT}`,
},
});
// Dify 删除子分段返回 204 No Content
if (response.status === 204) {
return new Response(
JSON.stringify({ result: 'success' }),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
}
const data = await response.json();
return new Response(JSON.stringify(data), {
status: response.status,
headers: { 'Content-Type': 'application/json' },
});
}
if (method === 'PATCH') {
const body = await request.json();
console.log('[API] Update Child Chunk:', { datasetId, documentId, segmentId, childChunkId, body });
const response = await fetch(apiUrl, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${frontendJWT}`,
},
body: JSON.stringify(body),
});
const data = await response.json();
return new Response(JSON.stringify(data), {
status: response.status,
headers: { 'Content-Type': 'application/json' },
});
}
return new Response(
JSON.stringify({ error: 'Method not allowed' }),
{ status: 405, headers: { 'Content-Type': 'application/json' } }
);
} catch (error: any) {
console.error('[API] Child Chunk Action - Error:', error.message);
return new Response(
JSON.stringify({ error: error.message || 'Failed to process child chunk request' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
}
@@ -0,0 +1,126 @@
import { type LoaderFunctionArgs, type ActionFunctionArgs } from '@remix-run/node';
import { API_BASE_URL } from '~/config/api-config';
/**
* GET /api/dataset/datasets/:datasetId/documents/:documentId/segments/:segmentId/child_chunks - 获取子分段列表
* Dify API: GET /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks
*/
export async function loader({ request, params }: LoaderFunctionArgs) {
try {
const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT } = await getUserSession(request);
if (!frontendJWT) {
return new Response(
JSON.stringify({ error: 'JWT认证失败,请重新登录' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
}
const { datasetId, documentId, segmentId } = params;
if (!datasetId || !documentId || !segmentId) {
return new Response(
JSON.stringify({ error: '缺少必要参数' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
// 获取查询参数
const url = new URL(request.url);
const page = url.searchParams.get('page') || '1';
const limit = url.searchParams.get('limit') || '20';
const keyword = url.searchParams.get('keyword') || '';
console.log('[API] Child Chunks List:', { datasetId, documentId, segmentId, page, limit, keyword });
// 构建查询参数
const queryParams = new URLSearchParams({ page, limit });
if (keyword) queryParams.append('keyword', keyword);
// 转发请求到 FastAPI -> Dify API
const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks?${queryParams}`;
const response = await fetch(apiUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${frontendJWT}`,
},
});
const data = await response.json();
return new Response(JSON.stringify(data), {
status: response.status,
headers: { 'Content-Type': 'application/json' },
});
} catch (error: any) {
console.error('[API] Child Chunks List - Error:', error.message);
return new Response(
JSON.stringify({ error: error.message || 'Failed to get child chunks' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
}
/**
* POST /api/dataset/datasets/:datasetId/documents/:documentId/segments/:segmentId/child_chunks - 新增子分段
* Dify API: POST /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks
* 请求体: { content: string }
*/
export async function action({ request, params }: ActionFunctionArgs) {
try {
const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT } = await getUserSession(request);
if (!frontendJWT) {
return new Response(
JSON.stringify({ error: 'JWT认证失败,请重新登录' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
}
const { datasetId, documentId, segmentId } = params;
if (!datasetId || !documentId || !segmentId) {
return new Response(
JSON.stringify({ error: '缺少必要参数' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
if (request.method !== 'POST') {
return new Response(
JSON.stringify({ error: 'Method not allowed' }),
{ status: 405, headers: { 'Content-Type': 'application/json' } }
);
}
const body = await request.json();
console.log('[API] Create Child Chunk:', { datasetId, documentId, segmentId });
// 转发请求到 FastAPI -> Dify API
const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks`;
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${frontendJWT}`,
},
body: JSON.stringify(body),
});
const data = await response.json();
return new Response(JSON.stringify(data), {
status: response.status,
headers: { 'Content-Type': 'application/json' },
});
} catch (error: any) {
console.error('[API] Create Child Chunk - Error:', error.message);
return new Response(
JSON.stringify({ error: error.message || 'Failed to create child chunk' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
}
@@ -1,4 +1,4 @@
import { type LoaderFunctionArgs } from '@remix-run/node';
import { type LoaderFunctionArgs, type ActionFunctionArgs } from '@remix-run/node';
import { API_BASE_URL } from '~/config/api-config';
/**
@@ -64,3 +64,65 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
);
}
}
/**
* POST /api/dataset/datasets/:datasetId/documents/:documentId/segments - 新增分段(批量)
* Dify API: POST /datasets/{dataset_id}/documents/{document_id}/segments
* 请求体: { segments: [{ content, answer?, keywords? }] }
*/
export async function action({ request, params }: ActionFunctionArgs) {
try {
const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT } = await getUserSession(request);
if (!frontendJWT) {
return new Response(
JSON.stringify({ error: 'JWT认证失败,请重新登录' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
}
const { datasetId, documentId } = params;
if (!datasetId || !documentId) {
return new Response(
JSON.stringify({ error: '缺少必要参数' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
if (request.method !== 'POST') {
return new Response(
JSON.stringify({ error: 'Method not allowed' }),
{ status: 405, headers: { 'Content-Type': 'application/json' } }
);
}
const body = await request.json();
console.log('[API] Create Segments:', { datasetId, documentId, segmentsCount: body.segments?.length });
// 转发请求到 FastAPI -> Dify API
const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}/documents/${documentId}/segments`;
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${frontendJWT}`,
},
body: JSON.stringify(body),
});
const data = await response.json();
return new Response(JSON.stringify(data), {
status: response.status,
headers: { 'Content-Type': 'application/json' },
});
} catch (error: any) {
console.error('[API] Create Segments - Error:', error.message);
return new Response(
JSON.stringify({ error: error.message || 'Failed to create segments' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
}
@@ -0,0 +1,87 @@
import { type ActionFunctionArgs } from '@remix-run/node';
import { API_BASE_URL } from '~/config/api-config';
/**
* POST /api/dataset/datasets/:datasetId/retrieve - 检索知识库
* Dify API: POST /datasets/{dataset_id}/retrieve
*
* 请求体:
* {
* query: string, // 检索关键词
* retrieval_model?: {
* search_method: 'keyword_search' | 'semantic_search' | 'full_text_search' | 'hybrid_search',
* reranking_enable?: boolean,
* reranking_model?: {
* reranking_provider_name: string,
* reranking_model_name: string
* },
* top_k?: number,
* score_threshold_enabled?: boolean,
* score_threshold?: number
* },
* metadata_filtering_conditions?: {
* logical_operator: 'and' | 'or',
* conditions: Array<{
* name: string,
* comparison_operator: string,
* value?: string | number
* }>
* }
* }
*/
export async function action({ request, params }: ActionFunctionArgs) {
try {
const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT } = await getUserSession(request);
if (!frontendJWT) {
return new Response(
JSON.stringify({ error: 'JWT认证失败,请重新登录' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
}
const { datasetId } = params;
if (!datasetId) {
return new Response(
JSON.stringify({ error: '缺少知识库ID' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
if (request.method !== 'POST') {
return new Response(
JSON.stringify({ error: 'Method not allowed' }),
{ status: 405, headers: { 'Content-Type': 'application/json' } }
);
}
const body = await request.json();
console.log('[API] Retrieve Dataset:', { datasetId, query: body.query });
// 转发请求到 FastAPI -> Dify API
const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}/retrieve`;
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${frontendJWT}`,
},
body: JSON.stringify(body),
});
const data = await response.json();
return new Response(JSON.stringify(data), {
status: response.status,
headers: { 'Content-Type': 'application/json' },
});
} catch (error: any) {
console.error('[API] Retrieve Dataset - Error:', error.message);
return new Response(
JSON.stringify({ error: error.message || 'Failed to retrieve dataset' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
}
+26 -20
View File
@@ -54,27 +54,15 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
}
/**
* PATCH /api/dataset/datasets/:datasetId - 修改知识库详情
* PATCH /api/dataset/datasets/:datasetId - 修改知识库名称
*
* Dify API: PATCH /datasets/{dataset_id}
*
* 请求体示例:
* {
* "name": "知识库名称",
* "description": "描述",
* "indexing_technique": "high_quality",
* "permission": "only_me",
* "embedding_model_provider": "zhipuai",
* "embedding_model": "embedding-3",
* "retrieval_model": {
* "search_method": "semantic_search",
* "reranking_enable": false,
* "top_k": 2,
* "score_threshold_enabled": false
* }
* }
* 请求体: { "name": "新的知识库名称" }
*
* 注意:删除知识库功能不对外开放
* 注意:
* - 仅允许修改知识库名称,其他字段不开放修改
* - 删除知识库功能不对外开放
*/
export async function action({ request, params }: ActionFunctionArgs) {
try {
@@ -99,9 +87,27 @@ export async function action({ request, params }: ActionFunctionArgs) {
const method = request.method;
if (method === 'PATCH') {
// 修改知识库详情
const body = await request.json();
console.log('[API] Update Dataset:', { datasetId, body });
// 只允许修改 name 字段
if (!body.name || typeof body.name !== 'string') {
return new Response(
JSON.stringify({ error: '请提供有效的知识库名称 (name)' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
// 只传递 name 字段,忽略其他字段
const allowedBody = { name: body.name.trim() };
if (allowedBody.name.length === 0) {
return new Response(
JSON.stringify({ error: '知识库名称不能为空' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
console.log('[API] Update Dataset Name:', { datasetId, name: allowedBody.name });
const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}`;
const response = await fetch(apiUrl, {
@@ -110,7 +116,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
'Content-Type': 'application/json',
'Authorization': `Bearer ${frontendJWT}`,
},
body: JSON.stringify(body),
body: JSON.stringify(allowedBody),
});
const data = await response.json();
+1 -1
View File
@@ -43,7 +43,7 @@ export const meta: MetaFunction = () => {
export default function ChatWithLLMIndex() {
return (
<div className="chat-container" style={{
height: '93vh', // 使用视口高度的90%
height: '90vh', // 使用视口高度的90%
borderRadius: '0.5rem',
marginBottom: '-20px',
marginTop: '2vh',
+5 -5
View File
@@ -1,7 +1,7 @@
import DatasetManager from "~/components/dify-dataset-manager";
import datasetManagerStyles from "~/styles/components/dify-dataset-manager/index.css?url";
import sidebarStyles from "~/styles/components/dify-dataset-manager/sidebar.css?url";
import documentListStyles from "~/styles/components/dify-dataset-manager/document-list.css?url";
// import sidebarStyles from "~/styles/components/dify-dataset-manager/sidebar.css?url";
// import documentListStyles from "~/styles/components/dify-dataset-manager/document-list.css?url";
/**
* 注册样式
@@ -9,8 +9,8 @@ import documentListStyles from "~/styles/components/dify-dataset-manager/documen
export function links() {
return [
{ rel: "stylesheet", href: datasetManagerStyles },
{ rel: "stylesheet", href: sidebarStyles },
{ rel: "stylesheet", href: documentListStyles },
// { rel: "stylesheet", href: sidebarStyles },
// { rel: "stylesheet", href: documentListStyles },
];
}
@@ -19,7 +19,7 @@ export function links() {
*/
export default function DatasetManagerIndex() {
return (
<div className="dataset-manager-page" style={{ height: '93vh', padding: '16px' }}>
<div className="dataset-manager-page" style={{ height: '90vh', padding: '16px' }}>
<DatasetManager />
</div>
);
File diff suppressed because it is too large Load Diff