From 855fb2a168c86a411ac61522937fbc6792a82415 Mon Sep 17 00:00:00 2001 From: ayangweb <75017711+ayangweb@users.noreply.github.com> Date: Thu, 31 Jul 2025 15:36:03 +0800 Subject: [PATCH] feat: support sending files in chat messages (#764) * feat: support sending files in chat messages * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * docs: update changelog --- docs/content.en/docs/release-notes/_index.md | 1 + src-tauri/src/assistant/mod.rs | 59 ++++-- src-tauri/src/common/assistant.rs | 3 + src-tauri/src/lib.rs | 2 +- src-tauri/src/server/attachment.rs | 16 +- src/commands/servers.ts | 19 +- src/components/Assistant/AttachmentList.tsx | 182 ++++++++++++++++++ src/components/Assistant/Chat.tsx | 30 ++- src/components/Assistant/ChatContent.tsx | 23 ++- src/components/Assistant/FileList.tsx | 142 -------------- src/components/Assistant/SessionFile.tsx | 17 +- src/components/ChatMessage/UserMessage.tsx | 109 ++++++++--- src/components/ChatMessage/index.tsx | 3 +- src/components/Common/Icons/FileIcon.tsx | 4 +- src/components/Search/ChatIcons.tsx | 21 +- src/components/Search/InputBox.tsx | 35 ++-- src/components/Search/InputControls.tsx | 52 ++--- src/components/Search/InputUpload.tsx | 50 ++++- src/components/Search/MCPPopover.tsx | 2 +- src/components/Search/SearchPopover.tsx | 2 +- src/components/SearchChat/index.tsx | 14 +- .../Advanced/components/Shortcuts/index.tsx | 22 +-- .../components/Details/Application/index.tsx | 4 +- src/hooks/useChatActions.ts | 57 ++++-- src/locales/en/translation.json | 4 +- src/locales/zh/translation.json | 4 +- src/pages/chat/index.tsx | 13 +- src/stores/chatStore.ts | 13 +- src/types/chat.ts | 3 +- src/types/commands.ts | 5 +- src/utils/index.ts | 22 +++ 31 files changed, 605 insertions(+), 328 deletions(-) create mode 100644 src/components/Assistant/AttachmentList.tsx delete mode 100644 src/components/Assistant/FileList.tsx diff --git a/docs/content.en/docs/release-notes/_index.md b/docs/content.en/docs/release-notes/_index.md index ee0030bb..d66af0bc 100644 --- a/docs/content.en/docs/release-notes/_index.md +++ b/docs/content.en/docs/release-notes/_index.md @@ -15,6 +15,7 @@ Information about release notes of Coco App is provided here. - feat: enhance ui for skipped version #834 - feat: support installing local extensions #749 +- feat: support sending files in chat messages #764 ### 🐛 Bug fix diff --git a/src-tauri/src/assistant/mod.rs b/src-tauri/src/assistant/mod.rs index 9b1e168f..b8067df5 100644 --- a/src-tauri/src/assistant/mod.rs +++ b/src-tauri/src/assistant/mod.rs @@ -115,21 +115,34 @@ pub async fn cancel_session_chat( pub async fn chat_create( app_handle: AppHandle, server_id: String, - message: String, + message: Option, + attachments: Option>, query_params: Option>, client_id: String, ) -> Result<(), String> { - let body = if !message.is_empty() { - let message = ChatRequestMessage { - message: Some(message), + println!("chat_create message: {:?}", message); + println!("chat_create attachments: {:?}", attachments); + + let message_empty = message.as_ref().map_or(true, |m| m.is_empty()); + let attachments_empty = attachments.as_ref().map_or(true, |a| a.is_empty()); + + if message_empty && attachments_empty { + return Err("Message and attachments are empty".to_string()); + } + + let body = { + let request_message: ChatRequestMessage = ChatRequestMessage { + message, + attachments, }; + + println!("chat_create body: {:?}", request_message); + Some( - serde_json::to_string(&message) + serde_json::to_string(&request_message) .map_err(|e| format!("Failed to serialize message: {}", e))? .into(), ) - } else { - None }; let response = HttpClient::advanced_post( @@ -165,8 +178,6 @@ pub async fn chat_create( if let Err(err) = app_handle.emit(&client_id, line) { log::error!("Emit failed: {:?}", err); - print!("Error sending message: {:?}", err); - let _ = app_handle.emit("chat-create-error", format!("Emit failed: {:?}", err)); } } @@ -179,21 +190,34 @@ pub async fn chat_chat( app_handle: AppHandle, server_id: String, session_id: String, - message: String, + message: Option, + attachments: Option>, query_params: Option>, //search,deep_thinking client_id: String, ) -> Result<(), String> { - let body = if !message.is_empty() { - let message = ChatRequestMessage { - message: Some(message), + println!("chat_chat message: {:?}", message); + println!("chat_chat attachments: {:?}", attachments); + + let message_empty = message.as_ref().map_or(true, |m| m.is_empty()); + let attachments_empty = attachments.as_ref().map_or(true, |a| a.is_empty()); + + if message_empty && attachments_empty { + return Err("Message and attachments are empty".to_string()); + } + + let body = { + let request_message = ChatRequestMessage { + message, + attachments, }; + + println!("chat_chat body: {:?}", request_message); + Some( - serde_json::to_string(&message) + serde_json::to_string(&request_message) .map_err(|e| format!("Failed to serialize message: {}", e))? .into(), ) - } else { - None }; let path = format!("/chat/{}/_chat", session_id); @@ -235,6 +259,9 @@ pub async fn chat_chat( if let Err(err) = app_handle.emit(&client_id, line) { log::error!("Emit failed: {:?}", err); + + print!("Error sending message: {:?}", err); + let _ = app_handle.emit("chat-create-error", format!("Emit failed: {:?}", err)); } } diff --git a/src-tauri/src/common/assistant.rs b/src-tauri/src/common/assistant.rs index 26ab0620..5284106d 100644 --- a/src-tauri/src/common/assistant.rs +++ b/src-tauri/src/common/assistant.rs @@ -3,7 +3,10 @@ use serde_json::Value; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChatRequestMessage { + #[serde(skip_serializing_if = "Option::is_none")] pub message: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub attachments: Option>, } #[allow(dead_code)] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index fc6c171d..9f471371 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -145,7 +145,7 @@ pub fn run() { // server::get_coco_server_connectors, get_app_search_source, server::attachment::upload_attachment, - server::attachment::get_attachment, + server::attachment::get_attachment_by_ids, server::attachment::delete_attachment, server::transcription::transcription, server::system_settings::get_system_settings, diff --git a/src-tauri/src/server/attachment.rs b/src-tauri/src/server/attachment.rs index 3d345c69..966d6a8d 100644 --- a/src-tauri/src/server/attachment.rs +++ b/src-tauri/src/server/attachment.rs @@ -72,11 +72,19 @@ pub async fn upload_attachment( } #[command] -pub async fn get_attachment(server_id: String, session_id: String) -> Result { - let mut query_params = Vec::new(); - query_params.push(format!("session={}", session_id)); +pub async fn get_attachment_by_ids( + server_id: String, + attachments: Vec, +) -> Result { + println!("get_attachment_by_ids server_id: {}", server_id); + println!("get_attachment_by_ids attachments: {:?}", attachments); - let response = HttpClient::get(&server_id, "/attachment/_search", Some(query_params)) + let request_body = serde_json::json!({ + "attachments": attachments + }); + let body = reqwest::Body::from(serde_json::to_string(&request_body).unwrap()); + + let response = HttpClient::post(&server_id, "/attachment/_search", None, Some(body)) .await .map_err(|e| format!("Request error: {}", e))?; diff --git a/src/commands/servers.ts b/src/commands/servers.ts index c82fd851..e00b8aba 100644 --- a/src/commands/servers.ts +++ b/src/commands/servers.ts @@ -8,7 +8,7 @@ import { GetResponse, UploadAttachmentPayload, UploadAttachmentResponse, - GetAttachmentPayload, + GetAttachmentByIdsPayload, GetAttachmentResponse, DeleteAttachmentPayload, TranscriptionPayload, @@ -255,17 +255,20 @@ export function cancel_session_chat({ export function chat_create({ serverId, message, + attachments, queryParams, clientId, }: { serverId: string; message: string; + attachments: string[]; queryParams?: Record; clientId: string; }): Promise { return invokeWithErrorHandler(`chat_create`, { serverId, message, + attachments, queryParams, clientId, }); @@ -275,12 +278,14 @@ export function chat_chat({ serverId, sessionId, message, + attachments, queryParams, clientId, }: { serverId: string; sessionId: string; message: string; + attachments: string[]; queryParams?: Record; clientId: string; }): Promise { @@ -288,6 +293,7 @@ export function chat_chat({ serverId, sessionId, message, + attachments, queryParams, clientId, }); @@ -342,10 +348,13 @@ export const upload_attachment = async (payload: UploadAttachmentPayload) => { } }; -export const get_attachment = (payload: GetAttachmentPayload) => { - return invokeWithErrorHandler("get_attachment", { - ...payload, - }); +export const get_attachment_by_ids = (payload: GetAttachmentByIdsPayload) => { + return invokeWithErrorHandler( + "get_attachment_by_ids", + { + ...payload, + } + ); }; export const delete_attachment = (payload: DeleteAttachmentPayload) => { diff --git a/src/components/Assistant/AttachmentList.tsx b/src/components/Assistant/AttachmentList.tsx new file mode 100644 index 00000000..ac382ee9 --- /dev/null +++ b/src/components/Assistant/AttachmentList.tsx @@ -0,0 +1,182 @@ +import { FC, useEffect, useMemo } from "react"; +import { X } from "lucide-react"; +import { useAsyncEffect } from "ahooks"; +import { useTranslation } from "react-i18next"; + +import { useChatStore, UploadAttachments } from "@/stores/chatStore"; +import { useConnectStore } from "@/stores/connectStore"; +import platformAdapter from "@/utils/platformAdapter"; +import Tooltip2 from "../Common/Tooltip2"; +import FileIcon from "../Common/Icons/FileIcon"; +import { filesize } from "@/utils"; + +const AttachmentList = () => { + const { uploadAttachments, setUploadAttachments } = useChatStore(); + const { currentService } = useConnectStore(); + + const serverId = useMemo(() => { + return currentService.id; + }, [currentService]); + + useEffect(() => { + return () => { + setUploadAttachments([]); + }; + }, []); + + const uploadAttachment = async (data: UploadAttachments) => { + const { uploading, uploaded, uploadFailed, path } = data; + + if (uploading || uploaded || uploadFailed) return; + + const { uploadAttachments } = useChatStore.getState(); + + const matched = uploadAttachments.find((item) => item.id === data.id); + + if (matched) { + matched.uploading = true; + + setUploadAttachments(uploadAttachments); + } + + try { + const attachmentIds: any = await platformAdapter.commands( + "upload_attachment", + { + serverId, + filePaths: [path], + } + ); + + if (!attachmentIds) { + throw new Error("Failed to get attachment id"); + } else { + Object.assign(data, { + uploaded: true, + attachmentId: attachmentIds[0], + }); + } + } catch (error) { + Object.assign(data, { + uploadFailed: true, + failedMessage: String(error), + }); + } finally { + Object.assign(data, { + uploading: false, + }); + + setUploadAttachments(uploadAttachments); + } + }; + + useAsyncEffect(async () => { + if (uploadAttachments.length === 0) return; + + for (const item of uploadAttachments) { + uploadAttachment(item); + } + }, [uploadAttachments]); + + const deleteFile = async (id: string) => { + const { uploadAttachments } = useChatStore.getState(); + + const matched = uploadAttachments.find((item) => item.id === id); + + if (!matched) return; + + const { uploadFailed, attachmentId } = matched; + + setUploadAttachments(uploadAttachments.filter((file) => file.id !== id)); + + if (uploadFailed) return; + + platformAdapter.commands("delete_attachment", { + serverId, + id: attachmentId, + }); + }; + + return ( +
+ {uploadAttachments.map((file) => { + return ( + + ); + })} +
+ ); +}; + +interface AttachmentItemProps extends UploadAttachments { + deletable?: boolean; + onDelete?: (id: string) => void; +} + +export const AttachmentItem: FC = (props) => { + const { + id, + name, + path, + extname, + size, + uploaded, + attachmentId, + uploadFailed, + failedMessage, + deletable, + onDelete, + } = props; + const { t } = useTranslation(); + + return ( +
+
+ {(uploadFailed || attachmentId) && deletable && ( +
{ + onDelete?.(id); + }} + > + +
+ )} + + + +
+
+ {name} +
+ +
+ {uploadFailed && failedMessage ? ( + + Upload Failed + + ) : ( +
+ {uploaded ? ( +
+ {extname && {extname}} + {filesize(size)} +
+ ) : ( + {t("assistant.fileList.uploading")} + )} +
+ )} +
+
+
+
+ ); +}; + +export default AttachmentList; diff --git a/src/components/Assistant/Chat.tsx b/src/components/Assistant/Chat.tsx index 57512120..a0c55425 100644 --- a/src/components/Assistant/Chat.tsx +++ b/src/components/Assistant/Chat.tsx @@ -43,8 +43,13 @@ interface ChatAIProps { instanceId?: string; } +export interface SendMessageParams { + message?: string; + attachments?: string[]; +} + export interface ChatAIRef { - init: (value: string) => void; + init: (params: SendMessageParams) => void; cancelChat: () => void; clearChat: () => void; } @@ -188,7 +193,7 @@ const ChatAI = memo( isDeepThinkActive, isMCPActive, changeInput, - showChatHistory, + showChatHistory ); const { dealMsg } = useMessageHandler( @@ -225,7 +230,7 @@ const ChatAI = memo( }, [activeChat, chatClose]); const init = useCallback( - async (value: string) => { + async (params: SendMessageParams) => { try { //console.log("init", curChatEnd, activeChat?._id); if (!isCurrentLogin) { @@ -237,9 +242,9 @@ const ChatAI = memo( return; } if (!activeChat?._id) { - await createNewChat(value); + await createNewChat(params); } else { - await handleSendMessage(value, activeChat); + await handleSendMessage(activeChat, params); } } catch (error) { console.error("Failed to initialize chat:", error); @@ -285,7 +290,10 @@ const ChatAI = memo( if (updatedChats.length > 0) { setActiveChat(updatedChats[0]); } else { - init(""); + init({ + message: "", + attachments: [], + }); } } @@ -396,8 +404,8 @@ const ChatAI = memo( loadingStep={loadingStep} timedoutShow={timedoutShow} Question={Question} - handleSendMessage={(value) => - handleSendMessage(value, activeChat) + handleSendMessage={(message) => + handleSendMessage(activeChat, { message }) } getFileUrl={getFileUrl} formatUrl={formatUrl} @@ -410,7 +418,11 @@ const ChatAI = memo( )} {!activeChat?._id && !visibleStartPage && ( - + { + init({ message }); + }} + /> )} diff --git a/src/components/Assistant/ChatContent.tsx b/src/components/Assistant/ChatContent.tsx index 295388be..9b9999a0 100644 --- a/src/components/Assistant/ChatContent.tsx +++ b/src/components/Assistant/ChatContent.tsx @@ -3,13 +3,14 @@ import { useTranslation } from "react-i18next"; import { ChatMessage } from "@/components/ChatMessage"; import { Greetings } from "./Greetings"; -// import FileList from "@/components/Assistant/FileList"; +import AttachmentList from "@/components/Assistant/AttachmentList"; import { useChatScroll } from "@/hooks/useChatScroll"; -import { useChatStore } from "@/stores/chatStore"; + import type { Chat, IChunkData } from "@/types/chat"; import { useConnectStore } from "@/stores/connectStore"; // import SessionFile from "./SessionFile"; import ScrollToBottom from "@/components/Common/ScrollToBottom"; +import { useChatStore } from "@/stores/chatStore"; interface ChatContentProps { activeChat?: Chat; @@ -44,14 +45,12 @@ export const ChatContent = ({ handleSendMessage, formatUrl, }: ChatContentProps) => { - // const sessionId = useConnectStore((state) => state.currentSessionId); - const setCurrentSessionId = useConnectStore((state) => { - return state.setCurrentSessionId; - }); + const { currentSessionId, setCurrentSessionId } = useConnectStore(); const { t } = useTranslation(); - // const uploadFiles = useChatStore((state) => state.uploadFiles); + const { uploadAttachments } = useChatStore(); + const messagesEndRef = useRef(null); const { scrollToBottom } = useChatScroll(messagesEndRef); @@ -168,13 +167,13 @@ export const ChatContent = ({
- {/* {uploadFiles.length > 0 && ( -
- + {uploadAttachments.length > 0 && ( +
+
- )} */} + )} - {/* {sessionId && } */} + {/* {currentSessionId && } */}
diff --git a/src/components/Assistant/FileList.tsx b/src/components/Assistant/FileList.tsx deleted file mode 100644 index 416c178a..00000000 --- a/src/components/Assistant/FileList.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { useEffect, useMemo } from "react"; -import { filesize } from "filesize"; -import { X } from "lucide-react"; -import { useAsyncEffect } from "ahooks"; -import { useTranslation } from "react-i18next"; - -import { useChatStore, UploadFile } from "@/stores/chatStore"; -import { useConnectStore } from "@/stores/connectStore"; -import platformAdapter from "@/utils/platformAdapter"; -import Tooltip2 from "../Common/Tooltip2"; -import FileIcon from "../Common/Icons/FileIcon"; - -const FileList = () => { - const { t } = useTranslation(); - const { uploadFiles, setUploadFiles } = useChatStore(); - const { currentService } = useConnectStore(); - - const serverId = useMemo(() => { - return currentService.id; - }, [currentService]); - - useEffect(() => { - return () => { - setUploadFiles([]); - }; - }, []); - - useAsyncEffect(async () => { - if (uploadFiles.length === 0) return; - - for await (const item of uploadFiles) { - const { uploaded, path } = item; - - if (uploaded) continue; - - try { - const attachmentIds: any = await platformAdapter.commands( - "upload_attachment", - { - serverId, - filePaths: [path], - } - ); - - if (!attachmentIds) { - throw new Error("Failed to get attachment id"); - } else { - Object.assign(item, { - uploaded: true, - attachmentId: attachmentIds[0], - }); - } - - setUploadFiles(uploadFiles); - } catch (error) { - Object.assign(item, { - uploadFailed: true, - failedMessage: String(error), - }); - } - } - }, [uploadFiles]); - - const deleteFile = async (file: UploadFile) => { - const { id, uploadFailed, attachmentId } = file; - - setUploadFiles(uploadFiles.filter((file) => file.id !== id)); - - if (uploadFailed) return; - - platformAdapter.commands("delete_attachment", { - serverId, - id: attachmentId, - }); - }; - - return ( -
- {uploadFiles.map((file) => { - const { - id, - name, - path, - extname, - size, - uploaded, - attachmentId, - uploadFailed, - failedMessage, - } = file; - - return ( -
-
- {(uploadFailed || attachmentId) && ( -
{ - deleteFile(file); - }} - > - -
- )} - - - -
-
- {name} -
- -
- {uploadFailed && failedMessage ? ( - - Upload Failed - - ) : ( -
- {uploaded ? ( -
- {extname && {extname}} - - {filesize(size, { standard: "jedec", spacer: "" })} - -
- ) : ( - {t("assistant.fileList.uploading")} - )} -
- )} -
-
-
-
- ); - })} -
- ); -}; - -export default FileList; diff --git a/src/components/Assistant/SessionFile.tsx b/src/components/Assistant/SessionFile.tsx index 1e324622..1b58280d 100644 --- a/src/components/Assistant/SessionFile.tsx +++ b/src/components/Assistant/SessionFile.tsx @@ -1,5 +1,4 @@ import clsx from "clsx"; -import { filesize } from "filesize"; import { Files, Trash2, X } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -10,6 +9,7 @@ import { AttachmentHit } from "@/types/commands"; import { useAppStore } from "@/stores/appStore"; import platformAdapter from "@/utils/platformAdapter"; import FileIcon from "../Common/Icons/FileIcon"; +import { filesize } from "@/utils"; interface SessionFileProps { sessionId: string; @@ -39,10 +39,13 @@ const SessionFile = (props: SessionFileProps) => { if (isTauri) { console.log("sessionId", sessionId); - const response: any = await platformAdapter.commands("get_attachment", { - serverId, - sessionId, - }); + const response: any = await platformAdapter.commands( + "get_attachment_by_ids", + { + serverId, + sessionId, + } + ); setUploadedFiles(response?.hits?.hits ?? []); } else { @@ -145,9 +148,7 @@ const SessionFile = (props: SessionFileProps) => {
{icon && {icon}} - - {filesize(size, { standard: "jedec", spacer: "" })} - + {filesize(size)}
diff --git a/src/components/ChatMessage/UserMessage.tsx b/src/components/ChatMessage/UserMessage.tsx index e9da9936..97c72333 100644 --- a/src/components/ChatMessage/UserMessage.tsx +++ b/src/components/ChatMessage/UserMessage.tsx @@ -1,17 +1,28 @@ -import { useState } from "react"; +import { FC, useState } from "react"; import clsx from "clsx"; import { CopyButton } from "@/components/Common/CopyButton"; +import { useAsyncEffect } from "ahooks"; +import platformAdapter from "@/utils/platformAdapter"; +import { useConnectStore } from "@/stores/connectStore"; +import { AttachmentItem } from "../Assistant/AttachmentList"; +import { useAppStore } from "@/stores/appStore"; interface UserMessageProps { - messageContent: string; + message: string; + attachments: string[]; } -export const UserMessage = ({ messageContent }: UserMessageProps) => { +export const UserMessage: FC = (props) => { + const { message, attachments } = props; + const [showCopyButton, setShowCopyButton] = useState(false); + const { currentService } = useConnectStore(); + const [attachmentData, setAttachmentData] = useState([]); + const { addError } = useAppStore(); const handleDoubleClick = (e: React.MouseEvent) => { - if (typeof window !== 'undefined' && typeof document !== 'undefined') { + if (typeof window !== "undefined" && typeof document !== "undefined") { const selection = window.getSelection(); const range = document.createRange(); @@ -21,31 +32,81 @@ export const UserMessage = ({ messageContent }: UserMessageProps) => { selection.removeAllRanges(); selection.addRange(range); } catch (error) { - console.error('Selection failed:', error); + console.error("Selection failed:", error); } } } }; + useAsyncEffect(async () => { + try { + if (attachments.length === 0) return; + + const result: any = await platformAdapter.commands( + "get_attachment_by_ids", + { + serverId: currentService.id, + attachments, + } + ); + + setAttachmentData(result?.hits?.hits); + } catch (error) { + addError(String(error)); + } + }, [attachments]); + return ( -
setShowCopyButton(true)} - onMouseLeave={() => setShowCopyButton(false)} - > -
- -
-
- {messageContent} -
-
+ <> + {message && ( +
setShowCopyButton(true)} + onMouseLeave={() => setShowCopyButton(false)} + > +
+ +
+
+ {message} +
+
+ )} + + {attachmentData && ( +
+ {attachmentData.map((item) => { + const { id, name, size, icon } = item._source; + + return ( + + ); + })} +
+ )} + ); }; diff --git a/src/components/ChatMessage/index.tsx b/src/components/ChatMessage/index.tsx index 701e3916..823f4fb3 100644 --- a/src/components/ChatMessage/index.tsx +++ b/src/components/ChatMessage/index.tsx @@ -89,6 +89,7 @@ export const ChatMessage = memo(function ChatMessage({ ]); const messageContent = message?._source?.message || ""; + const attachments = message?._source?.attachments ?? []; const details = message?._source?.details || []; const question = message?._source?.question || ""; @@ -103,7 +104,7 @@ export const ChatMessage = memo(function ChatMessage({ const renderContent = () => { if (!isAssistant) { - return ; + return ; } return ( diff --git a/src/components/Common/Icons/FileIcon.tsx b/src/components/Common/Icons/FileIcon.tsx index d30baf06..6ddeb2d0 100644 --- a/src/components/Common/Icons/FileIcon.tsx +++ b/src/components/Common/Icons/FileIcon.tsx @@ -18,7 +18,9 @@ const FileIcon: FC = (props) => { .then(setIconName); }); - return ; + return ( + + ); }; export default FileIcon; diff --git a/src/components/Search/ChatIcons.tsx b/src/components/Search/ChatIcons.tsx index 91b1a97a..faff3d19 100644 --- a/src/components/Search/ChatIcons.tsx +++ b/src/components/Search/ChatIcons.tsx @@ -2,13 +2,16 @@ import React from "react"; import { Send } from "lucide-react"; import StopIcon from "@/icons/Stop"; +import clsx from "clsx"; +import { SendMessageParams } from "../Assistant/Chat"; +import { getUploadedAttachmentsId, isAttachmentsUploaded } from "@/utils"; interface ChatIconsProps { lineCount: number; isChatMode: boolean; curChatEnd: boolean; inputValue: string; - onSend: (value: string) => void; + onSend: (params: SendMessageParams) => void; disabledChange: () => void; } @@ -26,11 +29,19 @@ const ChatIcons: React.FC = ({ if (curChatEnd) { return ( diff --git a/src/components/Search/InputBox.tsx b/src/components/Search/InputBox.tsx index 8e76ecfa..868f50ce 100644 --- a/src/components/Search/InputBox.tsx +++ b/src/components/Search/InputBox.tsx @@ -18,11 +18,17 @@ import { useAssistantManager } from "./AssistantManager"; import InputControls from "./InputControls"; import { useExtensionsStore } from "@/stores/extensionsStore"; import AudioRecording from "../AudioRecording"; -import { isDefaultServer } from "@/utils"; +import { + getUploadedAttachmentsId, + isAttachmentsUploaded, + isDefaultServer, +} from "@/utils"; import { useTauriFocus } from "@/hooks/useTauriFocus"; +import { SendMessageParams } from "../Assistant/Chat"; +import { isEmpty } from "lodash-es"; interface ChatInputProps { - onSend: (message: string) => void; + onSend: (params: SendMessageParams) => void; disabled: boolean; disabledChange: () => void; changeMode?: (isChatMode: boolean) => void; @@ -84,18 +90,13 @@ export default function ChatInput({ }: ChatInputProps) { const { t } = useTranslation(); - const currentAssistant = useConnectStore((state) => state.currentAssistant); - - const setBlurred = useAppStore((state) => state.setBlurred); - const isTauri = useAppStore((state) => state.isTauri); + const { currentAssistant } = useConnectStore(); const { sourceData, goAskAi } = useSearchStore(); const { modifierKey, returnToInput, setModifierKeyPressed } = useShortcutsStore(); - const language = useAppStore((state) => { - return state.language; - }); + const { isTauri, language, setBlurred } = useAppStore(); useEffect(() => { return () => { @@ -108,6 +109,7 @@ export default function ChatInput({ const { curChatEnd } = useChatStore(); const { setSearchValue, visibleExtensionStore, selectedExtension } = useSearchStore(); + const { uploadAttachments } = useChatStore(); useTauriFocus({ onFocus() { @@ -122,12 +124,19 @@ export default function ChatInput({ const handleSubmit = useCallback(() => { const trimmedValue = inputValue.trim(); + + if (!isAttachmentsUploaded()) return; + console.log("handleSubmit", trimmedValue, disabled); - if (trimmedValue && !disabled) { + + if ((trimmedValue || !isEmpty(uploadAttachments)) && !disabled) { changeInput(""); - onSend(trimmedValue); + onSend({ + message: trimmedValue, + attachments: getUploadedAttachmentsId(), + }); } - }, [inputValue, disabled, onSend]); + }, [inputValue, disabled, onSend, uploadAttachments]); useKeyboardHandlers(); @@ -138,7 +147,7 @@ export default function ChatInput({ changeInput(value); setSearchValue(value); if (!isChatMode) { - onSend(value); + onSend({ message: value }); } }, [changeInput, isChatMode, onSend] diff --git a/src/components/Search/InputControls.tsx b/src/components/Search/InputControls.tsx index ed2f5a60..71ea6430 100644 --- a/src/components/Search/InputControls.tsx +++ b/src/components/Search/InputControls.tsx @@ -16,8 +16,7 @@ import { useAppStore } from "@/stores/appStore"; import { useSearchStore } from "@/stores/searchStore"; import { useExtensionsStore } from "@/stores/extensionsStore"; import { parseSearchQuery, SearchQuery } from "@/utils"; -// import InputUpload from "./InputUpload"; -// import AiSummaryIcon from "@/components/Common/Icons/AiSummaryIcon"; +import InputUpload from "./InputUpload"; interface InputControlsProps { isChatMode: boolean; @@ -56,16 +55,16 @@ const InputControls = ({ isChatPage, hasModules, changeMode, -}: // checkScreenPermission, -// requestScreenPermission, -// getScreenMonitors, -// getScreenWindows, -// captureWindowScreenshot, -// captureMonitorScreenshot, -// openFileDialog, -// getFileMetadata, -// getFileIcon, -InputControlsProps) => { + checkScreenPermission, + requestScreenPermission, + getScreenMonitors, + getScreenWindows, + captureWindowScreenshot, + captureMonitorScreenshot, + openFileDialog, + getFileMetadata, + getFileIcon, +}: InputControlsProps) => { const { t } = useTranslation(); const isTauri = useAppStore((state) => state.isTauri); @@ -171,22 +170,24 @@ InputControlsProps) => { > {isChatMode ? (
- {/* */} + {source?.upload?.enabled && ( + + )} {source?.type === "deep_think" && source?.config?.visible && (