From c94cc00138be0ad412a38cdbf12ac8964f3f5023 Mon Sep 17 00:00:00 2001 From: PingChuan <1259732256@qq.com> Date: Sun, 30 Nov 2025 19:27:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=89=8D=E7=AB=AF=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=88=9D=E7=89=88=E7=9F=A5=E8=AF=86=E5=BA=93=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client.server.ts => dify-chat/chat.ts} | 128 +-- app/api/dify-chat/client.server.ts | 117 +++ app/api/{dify => dify-chat}/client.ts | 0 app/api/{dify => dify-chat}/index.ts | 0 app/api/{dify => dify-chat}/sse-handler.ts | 0 app/api/{dify => dify-chat}/types.ts | 0 app/api/dify-dataset/client.ts | 300 +++++++ app/api/dify-dataset/index.ts | 36 + app/api/dify-dataset/types.ts | 167 ++++ .../{chat => dify-chat}/chat-input.tsx | 508 +++++------ .../{chat => dify-chat}/chat-message.tsx | 14 +- app/components/{chat => dify-chat}/index.tsx | 13 +- .../{chat => dify-chat}/markdown.tsx | 0 .../{chat => dify-chat}/sidebar.tsx | 794 +++++++++--------- .../{chat => dify-chat}/thinking-block.tsx | 0 .../{chat => dify-chat}/thought-process.tsx | 438 +++++----- .../dify-dataset-manager/document-list.tsx | 373 ++++++++ app/components/dify-dataset-manager/index.tsx | 241 ++++++ .../dify-dataset-manager/sidebar.tsx | 160 ++++ app/components/layout/Sidebar.tsx | 5 +- app/config/chat.ts | 1 - app/hooks/use-chat-message.ts | 4 +- app/hooks/use-conversation.ts | 8 +- app/routes/api.chat-messages.tsx | 3 +- app/routes/api.conversations.$id.name.tsx | 4 +- app/routes/api.conversations.$id.tsx | 4 +- app/routes/api.conversations.tsx | 4 +- ...datasetId.documents.$documentId.status.tsx | 56 ++ ...asets.$datasetId.documents.$documentId.tsx | 118 +++ ....dataset.datasets.$datasetId.documents.tsx | 119 +++ app/routes/api.dataset.datasets.tsx | 60 ++ .../api.messages.$messageId.feedbacks.tsx | 2 +- app/routes/api.parameters.tsx | 4 +- app/routes/chat-with-llm._index.tsx | 2 +- app/routes/dataset-manager._index.tsx | 26 + app/routes/dataset-manager.tsx | 20 + .../dify-dataset-manager/document-list.css | 91 ++ .../components/dify-dataset-manager/index.css | 101 +++ .../dify-dataset-manager/sidebar.css | 135 +++ app/utils/chat-utils.ts | 2 +- 40 files changed, 3034 insertions(+), 1024 deletions(-) rename app/api/{dify/client.server.ts => dify-chat/chat.ts} (54%) create mode 100644 app/api/dify-chat/client.server.ts rename app/api/{dify => dify-chat}/client.ts (100%) rename app/api/{dify => dify-chat}/index.ts (100%) rename app/api/{dify => dify-chat}/sse-handler.ts (100%) rename app/api/{dify => dify-chat}/types.ts (100%) create mode 100644 app/api/dify-dataset/client.ts create mode 100644 app/api/dify-dataset/index.ts create mode 100644 app/api/dify-dataset/types.ts rename app/components/{chat => dify-chat}/chat-input.tsx (94%) rename app/components/{chat => dify-chat}/chat-message.tsx (97%) rename app/components/{chat => dify-chat}/index.tsx (99%) rename app/components/{chat => dify-chat}/markdown.tsx (100%) rename app/components/{chat => dify-chat}/sidebar.tsx (96%) rename app/components/{chat => dify-chat}/thinking-block.tsx (100%) rename app/components/{chat => dify-chat}/thought-process.tsx (93%) create mode 100644 app/components/dify-dataset-manager/document-list.tsx create mode 100644 app/components/dify-dataset-manager/index.tsx create mode 100644 app/components/dify-dataset-manager/sidebar.tsx create mode 100644 app/routes/api.dataset.datasets.$datasetId.documents.$documentId.status.tsx create mode 100644 app/routes/api.dataset.datasets.$datasetId.documents.$documentId.tsx create mode 100644 app/routes/api.dataset.datasets.$datasetId.documents.tsx create mode 100644 app/routes/api.dataset.datasets.tsx create mode 100644 app/routes/dataset-manager._index.tsx create mode 100644 app/routes/dataset-manager.tsx create mode 100644 app/styles/components/dify-dataset-manager/document-list.css create mode 100644 app/styles/components/dify-dataset-manager/index.css create mode 100644 app/styles/components/dify-dataset-manager/sidebar.css diff --git a/app/api/dify/client.server.ts b/app/api/dify-chat/chat.ts similarity index 54% rename from app/api/dify/client.server.ts rename to app/api/dify-chat/chat.ts index 7871bd8..8a8d531 100644 --- a/app/api/dify/client.server.ts +++ b/app/api/dify-chat/chat.ts @@ -1,112 +1,22 @@ /** - * Dify 服务端 API 模块 + * Dify Chat API 模块 * - * 提供 Node.js 服务端调用 FastAPI 后端的函数 + * 提供客户端调用 Dify API 的函数 * 用于 Remix loader/action 中调用 Dify API * - * 调用链路: - * Remix Server → FastAPI /dify/* → Dify - * - * @module api/dify/client.server + * @module api/dify/chat */ -import { API_BASE_URL } from '~/config/api-config'; +import { difyFetch } from './client.server'; // ============================================================================ -// 配置 +// Dify Chat API 客户端 // ============================================================================ /** - * 获取环境变量的服务端函数 - */ -const getServerEnvVar = (name: string, defaultValue: string = ''): string => { - return process.env[name] || defaultValue; -}; - -/** - * Dify API 客户端配置 - * 通过 FastAPI 后端的 /dify 路由代理访问 Dify - */ -const DIFY_CONFIG = { - API_URL: `${API_BASE_URL}/dify`, - API_KEY: getServerEnvVar('NEXT_PUBLIC_APP_KEY', ''), - APP_ID: (() => { - const rawAppId = getServerEnvVar('NEXT_PUBLIC_APP_ID', ''); - const match = rawAppId.match(/\/app\/([a-f0-9-]{36})/); - return match ? match[1] : rawAppId; - })(), -}; - -console.log('🔧 [Dify Server] 配置:', { - apiUrl: DIFY_CONFIG.API_URL, - apiBaseUrl: API_BASE_URL, - appId: DIFY_CONFIG.APP_ID, - configComplete: !!(DIFY_CONFIG.API_URL && DIFY_CONFIG.APP_ID) -}); - -// ============================================================================ -// 基础请求函数 -// ============================================================================ - -/** - * Dify API 基础请求函数 + * Dify Chat API 客户端 * - * 使用 JWT 认证通过 FastAPI 代理访问 Dify - * - * @param endpoint - API 端点路径 - * @param options - fetch 请求选项 * @param jwt - JWT 认证令牌 - * @returns Response 对象 - */ -async function difyFetch( - endpoint: string, - options: RequestInit = {}, - jwt?: string -): Promise { - const url = `${DIFY_CONFIG.API_URL}/${endpoint.replace(/^\//, '')}`; - - const headers: HeadersInit = { - 'Content-Type': 'application/json', - ...options.headers, - }; - - if (jwt) { - (headers as Record)['Authorization'] = `Bearer ${jwt}`; - } else { - console.warn('⚠️ [Dify Server] 没有提供 JWT,请求可能失败'); - } - - const response = await fetch(url, { - ...options, - headers, - }); - - if (!response.ok) { - const errorText = await response.text(); - console.error('❌ [Dify Server] Dify API 错误:', { - status: response.status, - statusText: response.statusText, - error: errorText - }); - - if (response.status === 401) { - throw new Error('JWT认证失败,请重新登录'); - } - - throw new Error(`Dify API Error: ${response.status} ${response.statusText}`); - } - - return response; -} - -// ============================================================================ -// Dify API 客户端 -// ============================================================================ - -/** - * Dify 服务端 API 客户端 - * - * 所有方法都需要传入 JWT 进行认证 * user 参数由后端自动从 JWT 中提取 */ export const difyClient = { @@ -187,7 +97,7 @@ export const difyClient = { return response; } - console.log('[Dify Server] 解析 JSON 响应'); + console.log('[Dify Chat] 解析 JSON 响应'); return response.json(); }, @@ -216,7 +126,7 @@ export const difyClient = { * 删除会话 */ async deleteConversation(conversationId: string, jwt?: string): Promise { - console.log('remix后端接收到删除请求,调用fastapi:', conversationId); + console.log('[Dify Chat] 删除会话:', conversationId); try { const response = await difyFetch(`conversations/${conversationId}`, { @@ -224,10 +134,9 @@ export const difyClient = { body: JSON.stringify({}), }, jwt); - // 对于 204 No Content 响应,直接返回成功 if (response.status === 204) { - console.log('删除会话' + conversationId + '成功'); + console.log('[Dify Chat] 删除会话成功:', conversationId); return { result: 'success' }; } @@ -235,16 +144,15 @@ export const difyClient = { if (contentType && contentType.includes('application/json')) { const data = await response.json(); - console.log('🗑️ [Dify Server] 删除会话 JSON 响应:', data); return data; } const text = await response.text(); - console.log('🗑️ [Dify Server] 删除会话文本响应:', text); + console.log('[Dify Chat] 删除会话文本响应:', text); return { result: 'success' }; } catch (error: any) { - console.warn('⚠️ [Dify Server] 删除会话请求失败,但可能已成功删除:', error.message); + console.warn('[Dify Chat] 删除会话请求失败,但可能已成功删除:', error.message); return { result: 'success' }; } }, @@ -268,17 +176,3 @@ export const difyClient = { return response.json(); }, }; - -// ============================================================================ -// 工具函数 -// ============================================================================ - -/** - * Dify 工具函数 - */ -export const difyUtils = { - /** - * 获取 Dify 配置 - */ - getConfig: () => DIFY_CONFIG, -}; diff --git a/app/api/dify-chat/client.server.ts b/app/api/dify-chat/client.server.ts new file mode 100644 index 0000000..210d6f3 --- /dev/null +++ b/app/api/dify-chat/client.server.ts @@ -0,0 +1,117 @@ +/** + * Dify 服务端基础 API 模块 + * + * 提供 Node.js 服务端调用 FastAPI 后端的基础功能 + * 包括配置管理和基础请求函数 + * + * 调用链路: + * Remix Server → FastAPI /dify/* → Dify + * + * @module api/dify/client.server + */ + +import { API_BASE_URL } from '~/config/api-config'; + +// ============================================================================ +// 配置 +// ============================================================================ + +/** + * 获取环境变量的服务端函数 + */ +const getServerEnvVar = (name: string, defaultValue: string = ''): string => { + return process.env[name] || defaultValue; +}; + +/** + * Dify API 客户端配置 + * 通过 FastAPI 后端的 /dify 路由代理访问 Dify + */ +const DIFY_CONFIG = { + API_URL: `${API_BASE_URL}/dify_chat`, + API_KEY: getServerEnvVar('NEXT_PUBLIC_APP_KEY', ''), + APP_ID: (() => { + const rawAppId = getServerEnvVar('NEXT_PUBLIC_APP_ID', ''); + const match = rawAppId.match(/\/app\/([a-f0-9-]{36})/); + return match ? match[1] : rawAppId; + })(), +}; + +console.log('[Dify Server] 配置:', { + apiUrl: DIFY_CONFIG.API_URL, + apiBaseUrl: API_BASE_URL, + appId: DIFY_CONFIG.APP_ID, + configComplete: !!(DIFY_CONFIG.API_URL && DIFY_CONFIG.APP_ID) +}); + +// ============================================================================ +// 基础请求函数 +// ============================================================================ + +/** + * Dify API 基础请求函数 + * + * 使用 JWT 认证通过 FastAPI 代理访问 Dify + * + * @param endpoint - API 端点路径 + * @param options - fetch 请求选项 + * @param jwt - JWT 认证令牌 + * @returns Response 对象 + */ +export async function difyFetch( + endpoint: string, + options: RequestInit = {}, + jwt?: string +): Promise { + const url = `${DIFY_CONFIG.API_URL}/${endpoint.replace(/^\//, '')}`; + + const headers: HeadersInit = { + 'Content-Type': 'application/json', + ...options.headers, + }; + + if (jwt) { + (headers as Record)['Authorization'] = `Bearer ${jwt}`; + } else { + console.warn('[Dify Server] 没有提供 JWT,转发fastapi请求可能失败'); + } + + const response = await fetch(url, { + ...options, + headers, + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('[Dify Server] API 转发错误:', { + status: response.status, + statusText: response.statusText, + error: errorText + }); + + if (response.status === 401) { + throw new Error('JWT认证失败,请重新登录'); + } + + throw new Error(`Dify API Error: ${response.status} ${response.statusText}`); + } + + return response; +} + +// ============================================================================ +// 工具函数 +// ============================================================================ + +/** + * Dify 工具函数 + */ +export const difyUtils = { + /** + * 获取 Dify 配置 + */ + getConfig: () => DIFY_CONFIG, +}; + +// 重新导出 chat 模块的 difyClient +export { difyClient } from './chat'; diff --git a/app/api/dify/client.ts b/app/api/dify-chat/client.ts similarity index 100% rename from app/api/dify/client.ts rename to app/api/dify-chat/client.ts diff --git a/app/api/dify/index.ts b/app/api/dify-chat/index.ts similarity index 100% rename from app/api/dify/index.ts rename to app/api/dify-chat/index.ts diff --git a/app/api/dify/sse-handler.ts b/app/api/dify-chat/sse-handler.ts similarity index 100% rename from app/api/dify/sse-handler.ts rename to app/api/dify-chat/sse-handler.ts diff --git a/app/api/dify/types.ts b/app/api/dify-chat/types.ts similarity index 100% rename from app/api/dify/types.ts rename to app/api/dify-chat/types.ts diff --git a/app/api/dify-dataset/client.ts b/app/api/dify-dataset/client.ts new file mode 100644 index 0000000..bc3eda4 --- /dev/null +++ b/app/api/dify-dataset/client.ts @@ -0,0 +1,300 @@ +/** + * Dify Dataset 客户端 API 模块 + * + * 提供浏览器端调用 Dify 知识库 API 的函数 + * 通过 Remix API Routes 代理请求 + * + * @module api/dify-dataset/client + */ + +import axios from 'axios'; +import type { + DatasetsResponse, + DocumentsResponse, + SegmentsResponse, + Document, + OperationResult, +} from './types'; + +// ============================================================================ +// 基础配置 +// ============================================================================ + +/** + * 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 { + const params = new URLSearchParams({ + page: page.toString(), + limit: limit.toString(), + }); + + const response = await axios.get( + `${API_URL}/datasets?${params}`, + { withCredentials: true } + ); + return response.data; +} + +/** + * 获取单个知识库详情 + * + * @param datasetId - 知识库 ID + * @returns 知识库详情 + */ +export async function fetchDataset(datasetId: string): Promise { + const response = await axios.get( + `${API_URL}/datasets/${datasetId}`, + { withCredentials: true } + ); + return response.data; +} + +// ============================================================================ +// 文档 API +// ============================================================================ + +/** + * 获取知识库文档列表 + * + * @param datasetId - 知识库 ID + * @param page - 页码,默认 1 + * @param limit - 每页数量,默认 20 + * @param keyword - 搜索关键词 + * @returns 文档列表响应 + */ +export async function fetchDocuments( + datasetId: string, + page: number = 1, + limit: number = 20, + keyword?: string +): Promise { + const params = new URLSearchParams({ + page: page.toString(), + limit: limit.toString(), + }); + + if (keyword) { + params.append('keyword', keyword); + } + + console.log('[Dataset Client] 获取文档列表:', { datasetId, page, limit, keyword }); + + const response = await axios.get( + `${API_URL}/datasets/${datasetId}/documents?${params}`, + { withCredentials: true } + ); + return response.data; +} + +/** + * 获取单个文档详情 + * + * @param datasetId - 知识库 ID + * @param documentId - 文档 ID + * @returns 文档详情 + */ +export async function fetchDocument( + datasetId: string, + documentId: string +): Promise { + const response = await axios.get( + `${API_URL}/datasets/${datasetId}/documents/${documentId}`, + { withCredentials: true } + ); + return response.data; +} + +/** + * 删除文档 + * + * @param datasetId - 知识库 ID + * @param documentId - 文档 ID + * @returns 操作结果 + */ +export async function deleteDocument( + datasetId: string, + documentId: string +): Promise { + console.log('[Dataset Client] 删除文档:', { datasetId, documentId }); + + const response = await axios.delete( + `${API_URL}/datasets/${datasetId}/documents/${documentId}`, + { withCredentials: true } + ); + return response.data; +} + +/** + * 启用/禁用文档 + * + * @param datasetId - 知识库 ID + * @param documentId - 文档 ID + * @param enabled - 是否启用 + * @returns 操作结果 + */ +export async function toggleDocumentStatus( + datasetId: string, + documentId: string, + enabled: boolean +): Promise { + console.log('[Dataset Client] 切换文档状态:', { datasetId, documentId, enabled }); + + const response = await axios.patch( + `${API_URL}/datasets/${datasetId}/documents/${documentId}/status`, + { enabled }, + { + headers: { 'Content-Type': 'application/json' }, + withCredentials: true, + } + ); + 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 { + 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( + `${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 { + console.log('[Dataset Client] 删除分段:', { datasetId, documentId, segmentId }); + + const response = await axios.delete( + `${API_URL}/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}`, + { withCredentials: true } + ); + return response.data; +} + +/** + * 启用/禁用分段 + * + * @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 { + const response = await axios.patch( + `${API_URL}/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/status`, + { enabled }, + { + headers: { 'Content-Type': 'application/json' }, + withCredentials: true, + } + ); + return response.data; +} + +// ============================================================================ +// 文件上传 API +// ============================================================================ + +/** + * 上传文件到知识库 + * + * @param datasetId - 知识库 ID + * @param file - 文件对象 + * @param onProgress - 上传进度回调 + * @returns 创建的文档信息 + */ +export async function uploadDocument( + datasetId: string, + file: File, + onProgress?: (percent: number) => void +): Promise { + const formData = new FormData(); + formData.append('file', file); + formData.append('data', JSON.stringify({ + indexing_technique: 'high_quality', + process_rule: { + mode: 'automatic', + }, + })); + + console.log('[Dataset Client] 上传文档:', { datasetId, fileName: file.name }); + + const response = await axios.post( + `${API_URL}/datasets/${datasetId}/documents/create-by-file`, + formData, + { + withCredentials: true, + onUploadProgress: (progressEvent) => { + if (progressEvent.total && onProgress) { + const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total); + onProgress(percent); + } + }, + } + ); + return response.data; +} diff --git a/app/api/dify-dataset/index.ts b/app/api/dify-dataset/index.ts new file mode 100644 index 0000000..ade8698 --- /dev/null +++ b/app/api/dify-dataset/index.ts @@ -0,0 +1,36 @@ +/** + * Dify Dataset API 模块统一导出 + * + * @module api/dify-dataset + */ + +// 类型导出 +export type { + Dataset, + DatasetsResponse, + Document, + DocumentsResponse, + Segment, + SegmentsResponse, + IndexingStatus, + OperationResult, + CreateDocumentResponse, + UploadProgress, +} from './types'; + +// 客户端 API 导出 +export { + // 知识库 + fetchDatasets, + fetchDataset, + // 文档 + fetchDocuments, + fetchDocument, + deleteDocument, + toggleDocumentStatus, + uploadDocument, + // 分段 + fetchSegments, + deleteSegment, + toggleSegmentStatus, +} from './client'; diff --git a/app/api/dify-dataset/types.ts b/app/api/dify-dataset/types.ts new file mode 100644 index 0000000..037c275 --- /dev/null +++ b/app/api/dify-dataset/types.ts @@ -0,0 +1,167 @@ +/** + * 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; +} diff --git a/app/components/chat/chat-input.tsx b/app/components/dify-chat/chat-input.tsx similarity index 94% rename from app/components/chat/chat-input.tsx rename to app/components/dify-chat/chat-input.tsx index 2e3c024..f7d3964 100644 --- a/app/components/chat/chat-input.tsx +++ b/app/components/dify-chat/chat-input.tsx @@ -1,255 +1,255 @@ -import React, { useState, useRef, useCallback, useEffect } from 'react'; -import { Input, Button, Upload, Tooltip, message as antdMessage, Space } from 'antd'; -import { StopOutlined, PictureOutlined, CommentOutlined } from '@ant-design/icons'; -import type { VisionFile } from '~/api/dify'; -import '../../styles/components/chat-with-llm/chat-input.css'; - -const { TextArea } = Input; - -interface ChatInputProps { - onSendMessage: (message: string, files?: VisionFile[]) => void; - disabled?: boolean; - placeholder?: string; - onStop?: () => void; - isResponding?: boolean; - visionConfig?: { - enabled: boolean; - number_limits?: number; - image_file_size_limit?: number; - transfer_methods?: string[]; - }; -} - -/** - * 聊天输入组件 - */ -export default function ChatInput({ - onSendMessage, - disabled = false, - placeholder = '输入消息...', - onStop, - isResponding = false, - visionConfig, -}: ChatInputProps) { - const [message, setMessage] = useState(''); - const [files, setFiles] = useState([]); - const textareaRef = useRef(null); - const isComposing = useRef(false); - - /** - * 提交消息 - */ - const handleSubmit = () => { - if (!message.trim() || disabled) return; - - onSendMessage(message, files.length > 0 ? files : undefined); - setMessage(''); - setFiles([]); - - // 聚焦回输入框 - setTimeout(() => { - textareaRef.current?.focus(); - }, 10); - }; - - /** - * 处理键盘事件 - */ - const handleKeyDown = (e: React.KeyboardEvent) => { - // 处理输入法状态 - if (e.nativeEvent.isComposing) { - isComposing.current = true; - return; - } - - // Enter发送,Shift+Enter换行 - if (e.key === 'Enter' && !e.shiftKey && !isComposing.current) { - e.preventDefault(); - handleSubmit(); - } - }; - - /** - * 处理输入法结束 - */ - const handleCompositionEnd = () => { - isComposing.current = false; - }; - - /** - * 停止响应 - */ - const handleStop = () => { - onStop?.(); - }; - - /** - * 处理文件上传 - */ - const handleFileUpload = (file: File) => { - // 检查文件数量限制 - if (visionConfig?.number_limits && files.length >= visionConfig.number_limits) { - antdMessage.error(`最多只能上传 ${visionConfig.number_limits} 个文件`); - return false; - } - - // 检查文件大小限制 - if (visionConfig?.image_file_size_limit) { - const limitMB = visionConfig.image_file_size_limit; - const fileSizeMB = file.size / (1024 * 1024); - if (fileSizeMB > limitMB) { - antdMessage.error(`文件大小不能超过 ${limitMB}MB`); - return false; - } - } - - // 检查文件类型 - if (!file.type.startsWith('image/')) { - antdMessage.error('只支持图片文件'); - return false; - } - - // 创建文件对象 - const reader = new FileReader(); - reader.onload = (e) => { - const newFile: VisionFile = { - id: `file-${Date.now()}-${Math.random()}`, - type: 'image', - transfer_method: 'local_file' as any, - url: e.target?.result as string, - upload_file_id: '', - }; - - setFiles(prev => [...prev, newFile]); - }; - reader.readAsDataURL(file); - - return false; // 阻止默认上传行为 - }; - - /** - * 移除文件 - */ - const handleRemoveFile = (fileId: string) => { - setFiles(prev => prev.filter(file => file.id !== fileId)); - }; - - /** - * 渲染文件预览 - */ - const renderFilePreview = () => { - if (files.length === 0) return null; - - return ( -
- {files.map((file) => ( -
- 预览 - -
- ))} -
- ); - }; - - /** - * 渲染上传按钮 - */ - const renderUploadButton = () => { - if (!visionConfig?.enabled) return null; - - const isDisabled = disabled || (visionConfig.number_limits ? files.length >= visionConfig.number_limits : false); - - return ( - - -