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 } -