From b95b21992597498ef2e59a4f0384db5f818215e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B7=B1=E7=A9=BA?= <2793500992@qq.com> Date: Thu, 17 Apr 2025 03:08:17 -0400 Subject: [PATCH 1/2] Function: Add File Upload Support --- package-lock.json | 25 ++- package.json | 1 + src/components/chat/ChatMessageArea.tsx | 154 +++++++++++------- src/components/chat/FileAttachmentDisplay.tsx | 148 +++++++++++++++++ src/components/chat/FileUploadButton.tsx | 90 ++++++++++ src/components/chat/MarkdownContent.tsx | 77 ++++++--- src/components/pages/ChatPage.tsx | 24 +++ src/services/chat-service.ts | 130 +++++++++++++++ src/services/file-upload-service.ts | 138 ++++++++++++++++ src/services/message-helper.ts | 119 +++++++++++++- src/services/providers/openai-service.ts | 61 ++++++- src/types/chat.ts | 7 + 12 files changed, 887 insertions(+), 87 deletions(-) create mode 100644 src/components/chat/FileAttachmentDisplay.tsx create mode 100644 src/components/chat/FileUploadButton.tsx create mode 100644 src/services/file-upload-service.ts diff --git a/package-lock.json b/package-lock.json index c6462a4..c5a68eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "i18next-browser-languagedetector": "^8.0.4", "katex": "^0.16.21", "lucide-react": "^0.344.0", + "mime": "^4.0.7", "prism-themes": "^1.9.0", "prismjs": "^1.30.0", "react": "^18.3.1", @@ -4546,6 +4547,17 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/electron-publish/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/electron-publish/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -7765,14 +7777,17 @@ } }, "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.7.tgz", + "integrity": "sha512-2OfDPL+e03E0LrXaGYOtTFIYhiuzep94NSsuhrNULq+stylcJedcHdzHtz0atMUuGwJfFYs0YL5xeC/Ca2x0eQ==", + "funding": [ + "https://github.com/sponsors/broofa" + ], "bin": { - "mime": "cli.js" + "mime": "bin/cli.js" }, "engines": { - "node": ">=4.0.0" + "node": ">=16" } }, "node_modules/mime-db": { diff --git a/package.json b/package.json index 93ef7b7..a1f6cd7 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "i18next-browser-languagedetector": "^8.0.4", "katex": "^0.16.21", "lucide-react": "^0.344.0", + "mime": "^4.0.7", "prism-themes": "^1.9.0", "prismjs": "^1.30.0", "react": "^18.3.1", diff --git a/src/components/chat/ChatMessageArea.tsx b/src/components/chat/ChatMessageArea.tsx index eed379f..f090cb0 100644 --- a/src/components/chat/ChatMessageArea.tsx +++ b/src/components/chat/ChatMessageArea.tsx @@ -10,12 +10,15 @@ import { ChatService } from '../../services/chat-service'; import { AIServiceCapability } from '../../types/capabilities'; import ProviderIcon from '../ui/ProviderIcon'; import { useTranslation } from '../../hooks/useTranslation'; +import FileUploadButton from './FileUploadButton'; +import FileAttachmentDisplay from './FileAttachmentDisplay'; interface ChatMessageAreaProps { activeConversation: Conversation | null; isLoading: boolean; error: string | null; onSendMessage: (content: string) => void; + onSendMessageWithFiles?: (content: string, files: File[]) => void; onStopStreaming?: () => void; onRegenerateResponse?: (messageId: string) => void; onEditMessage?: (messageId: string, newContent: string) => void; @@ -29,6 +32,7 @@ export const ChatMessageArea: React.FC = ({ isLoading, error, onSendMessage, + onSendMessageWithFiles, onStopStreaming, onRegenerateResponse, onEditMessage, @@ -48,6 +52,7 @@ export const ChatMessageArea: React.FC = ({ const [ableToWebSearch, setAbleToWebSearch] = useState(false); const [webSearchActive, setWebSearchActive] = useState(false); const [isWebSearchPreviewEnabled, setIsWebSearchPreviewEnabled] = useState(false); + const [selectedFiles, setSelectedFiles] = useState([]); // Scroll to bottom when messages change useEffect(() => { @@ -93,28 +98,31 @@ export const ChatMessageArea: React.FC = ({ } }, [isCurrentlyStreaming]); - const handleSubmit = (e: FormEvent) => { - e.preventDefault(); - - if (!inputValue.trim() || isLoading || isCurrentlyStreaming) return; - - onSendMessage(inputValue); - - setInput(''); - - const textarea = inputRef.current; - if(!textarea) return; - // Calculate new height based on scrollHeight, with min and max constraints - const minHeight = 36; // Approx height for 1 row + // Handle file selection + const handleFilesSelected = (files: File[]) => { + setSelectedFiles([...selectedFiles, ...files]); + }; - textarea.style.height = `${minHeight}px`; + // Remove a selected file + const handleRemoveFile = (index: number) => { + const newFiles = [...selectedFiles]; + newFiles.splice(index, 1); + setSelectedFiles(newFiles); }; - const handleStopStreaming = () => { - if (onStopStreaming) { - onStopStreaming(); - isCurrentlyStreaming = false; + // Handle form submission with files + const handleFormSubmit = (e: FormEvent) => { + e.preventDefault(); + if (isLoading || isCurrentlyStreaming || !inputValue.trim()) return; + + if (selectedFiles.length > 0 && onSendMessageWithFiles) { + onSendMessageWithFiles(inputValue, selectedFiles); + setSelectedFiles([]); + } else { + onSendMessage(inputValue); } + + setInput(''); }; // Handle regenerate response @@ -161,6 +169,12 @@ export const ChatMessageArea: React.FC = ({ }); }; + const handleStopStreaming = () => { + if (onStopStreaming) { + onStopStreaming(); + } + }; + // Placeholder error handler for other actions // const handleActionError = (action: string) => { // console.error(`Function not implemented yet: ${action}`); @@ -275,6 +289,35 @@ export const ChatMessageArea: React.FC = ({ // Check if there's a streaming message const hasStreamingMessage = Array.from(activeConversation.messages.values()).some(m => m.messageId.startsWith('streaming-')); + const webSearchElement = isWebSearchPreviewEnabled ? ( + ableToWebSearch ? ( + + ) + : + ( + + ) + ) + :<>; + return (
{/* Messages area */} @@ -325,7 +368,7 @@ export const ChatMessageArea: React.FC = ({ return (
setHoveredMessageId(message.messageId)} onMouseLeave={() => setHoveredMessageId(null)} > @@ -388,12 +431,12 @@ export const ChatMessageArea: React.FC = ({ }`} > {isUserMessage ? ( - + ) : ( (message.content.length === 0 || MessageHelper.MessageContentToText(message.content).length === 0) ? (
) : ( - + ) )}
@@ -438,15 +481,30 @@ export const ChatMessageArea: React.FC = ({
{/* Input form */} -
{ inputRef.current?.focus(); }} onFocus={() => { inputRef.current?.focus(); }} - className={`relative flex ${isWebSearchPreviewEnabled ? 'flex-col' : 'flex-row justify-stretch items-center'} gap-2 px-4 pt-3 pb-2 m-2 mb-4 transition-all duration-200 rounded-lg form-textarea-border cursor-text`} + className={`relative flex flex-col gap-2 h-fit px-4 pt-3 pb-2 m-2 mb-4 transition-all duration-200 rounded-lg form-textarea-border cursor-text`} > + {/* Selected Files Display */} + {selectedFiles.length > 0 && ( +
+ {selectedFiles.map((file, index) => ( + handleRemoveFile(index)} + /> + ))} +
+ )} + -
+
+
+ {/* File upload button */} + {onSendMessageWithFiles && ( + + )} +
+ + {/* Web search element */} { - isWebSearchPreviewEnabled ? ( - ableToWebSearch ? ( - - ) - : - ( - - ) - ) - :<> + webSearchElement } -