From af33de09db56a7c86f9cf6893b53ac9dd13bb484 Mon Sep 17 00:00:00 2001 From: pingchuan <1259732256@qq.com> Date: Wed, 4 Jun 2025 11:18:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9F=BA=E4=BA=8E=20shiy-temp=E5=88=86?= =?UTF-8?q?=E6=94=AF=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.exaple | 6 + app/components/chat/chat-input.tsx | 256 ++++ app/components/chat/chat-message.tsx | 208 ++++ app/components/chat/index.tsx | 571 +++++++++ app/components/chat/markdown.tsx | 242 ++++ app/components/chat/sidebar.tsx | 397 ++++++ app/components/chat/thought-process.tsx | 220 ++++ app/components/layout/Sidebar.tsx | 82 +- app/config/chat.ts | 98 ++ app/hooks/use-chat-message.ts | 574 +++++++++ app/hooks/use-conversation.ts | 246 ++++ app/root.tsx | 79 +- app/routes/_index.tsx | 52 +- app/routes/api.chat-messages.tsx | 68 + app/routes/api.conversations.$id.name.tsx | 43 + app/routes/api.conversations.$id.tsx | 46 + app/routes/api.conversations.tsx | 35 + app/routes/api.file-upload.tsx | 50 + app/routes/api.messages.tsx | 41 + app/routes/api.parameters.tsx | 32 + app/routes/chat-with-llm._index.tsx | 42 + app/routes/chat-with-llm.tsx | 23 + app/services/api.client.ts | 580 +++++++++ app/services/dify-client.server.ts | 185 +++ .../components/chat-with-llm/chat-input.css | 27 + .../components/chat-with-llm/chat-message.css | 132 ++ app/styles/components/chat-with-llm/index.css | 246 ++++ .../components/chat-with-llm/markdown.css | 136 ++ .../components/chat-with-llm/sidebar.css | 74 ++ .../chat-with-llm/thought-process.css | 57 + app/types/dify_chat.ts | 264 ++++ app/utils/chat-utils.ts | 123 ++ app/utils/session.server.ts | 53 + package-lock.json | 1102 ++++++++++++++++- package.json | 6 +- vite.config.ts | 2 +- 36 files changed, 6293 insertions(+), 105 deletions(-) create mode 100644 .env.exaple create mode 100644 app/components/chat/chat-input.tsx create mode 100644 app/components/chat/chat-message.tsx create mode 100644 app/components/chat/index.tsx create mode 100644 app/components/chat/markdown.tsx create mode 100644 app/components/chat/sidebar.tsx create mode 100644 app/components/chat/thought-process.tsx create mode 100644 app/config/chat.ts create mode 100644 app/hooks/use-chat-message.ts create mode 100644 app/hooks/use-conversation.ts create mode 100644 app/routes/api.chat-messages.tsx create mode 100644 app/routes/api.conversations.$id.name.tsx create mode 100644 app/routes/api.conversations.$id.tsx create mode 100644 app/routes/api.conversations.tsx create mode 100644 app/routes/api.file-upload.tsx create mode 100644 app/routes/api.messages.tsx create mode 100644 app/routes/api.parameters.tsx create mode 100644 app/routes/chat-with-llm._index.tsx create mode 100644 app/routes/chat-with-llm.tsx create mode 100644 app/services/api.client.ts create mode 100644 app/services/dify-client.server.ts create mode 100644 app/styles/components/chat-with-llm/chat-input.css create mode 100644 app/styles/components/chat-with-llm/chat-message.css create mode 100644 app/styles/components/chat-with-llm/index.css create mode 100644 app/styles/components/chat-with-llm/markdown.css create mode 100644 app/styles/components/chat-with-llm/sidebar.css create mode 100644 app/styles/components/chat-with-llm/thought-process.css create mode 100644 app/types/dify_chat.ts create mode 100644 app/utils/chat-utils.ts create mode 100644 app/utils/session.server.ts diff --git a/.env.exaple b/.env.exaple new file mode 100644 index 0000000..5876800 --- /dev/null +++ b/.env.exaple @@ -0,0 +1,6 @@ +# APP ID +NEXT_PUBLIC_APP_ID= +# APP API key +NEXT_PUBLIC_APP_KEY= +# API url prefix +NEXT_PUBLIC_API_URL= \ No newline at end of file diff --git a/app/components/chat/chat-input.tsx b/app/components/chat/chat-input.tsx new file mode 100644 index 0000000..7f7d276 --- /dev/null +++ b/app/components/chat/chat-input.tsx @@ -0,0 +1,256 @@ +import React, { useState, useRef, useCallback, useEffect } from 'react'; +import { Input, Button, Upload, Tooltip, message as antdMessage, Space } from 'antd'; +import { SendOutlined, StopOutlined, PaperClipOutlined, PictureOutlined } from '@ant-design/icons'; +import type { VisionFile } from '../../types/dify_chat'; +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 ( + + +