mirror of
https://github.com/infinilabs/coco-app.git
synced 2026-09-01 19:51:52 +02:00
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
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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<String>,
|
||||
attachments: Option<Vec<String>>,
|
||||
query_params: Option<HashMap<String, Value>>,
|
||||
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<String>,
|
||||
attachments: Option<Vec<String>>,
|
||||
query_params: Option<HashMap<String, Value>>, //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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub attachments: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -72,11 +72,19 @@ pub async fn upload_attachment(
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn get_attachment(server_id: String, session_id: String) -> Result<Value, String> {
|
||||
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<String>,
|
||||
) -> Result<Value, String> {
|
||||
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))?;
|
||||
|
||||
|
||||
@@ -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<string, any>;
|
||||
clientId: string;
|
||||
}): Promise<GetResponse> {
|
||||
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<string, any>;
|
||||
clientId: string;
|
||||
}): Promise<string> {
|
||||
@@ -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<GetAttachmentResponse>("get_attachment", {
|
||||
...payload,
|
||||
});
|
||||
export const get_attachment_by_ids = (payload: GetAttachmentByIdsPayload) => {
|
||||
return invokeWithErrorHandler<GetAttachmentResponse>(
|
||||
"get_attachment_by_ids",
|
||||
{
|
||||
...payload,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const delete_attachment = (payload: DeleteAttachmentPayload) => {
|
||||
|
||||
182
src/components/Assistant/AttachmentList.tsx
Normal file
182
src/components/Assistant/AttachmentList.tsx
Normal file
@@ -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 (
|
||||
<div className="flex flex-wrap gap-y-2 -mx-1 text-sm">
|
||||
{uploadAttachments.map((file) => {
|
||||
return (
|
||||
<AttachmentItem
|
||||
key={file.id}
|
||||
{...file}
|
||||
deletable
|
||||
onDelete={deleteFile}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface AttachmentItemProps extends UploadAttachments {
|
||||
deletable?: boolean;
|
||||
onDelete?: (id: string) => void;
|
||||
}
|
||||
|
||||
export const AttachmentItem: FC<AttachmentItemProps> = (props) => {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
path,
|
||||
extname,
|
||||
size,
|
||||
uploaded,
|
||||
attachmentId,
|
||||
uploadFailed,
|
||||
failedMessage,
|
||||
deletable,
|
||||
onDelete,
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div key={id} className="w-1/3 px-1">
|
||||
<div className="relative group flex items-center gap-1 p-1 rounded-[4px] bg-[#dedede] dark:bg-[#202126]">
|
||||
{(uploadFailed || attachmentId) && deletable && (
|
||||
<div
|
||||
className="absolute flex justify-center items-center size-[14px] bg-red-600 top-0 right-0 rounded-full cursor-pointer translate-x-[5px] -translate-y-[5px] transition opacity-0 group-hover:opacity-100 "
|
||||
onClick={() => {
|
||||
onDelete?.(id);
|
||||
}}
|
||||
>
|
||||
<X className="size-[10px] text-white" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FileIcon path={path} />
|
||||
|
||||
<div className="flex flex-col justify-between overflow-hidden">
|
||||
<div className="truncate text-sm text-[#333333] dark:text-[#D8D8D8]">
|
||||
{name}
|
||||
</div>
|
||||
|
||||
<div className="text-xs">
|
||||
{uploadFailed && failedMessage ? (
|
||||
<Tooltip2 content={failedMessage}>
|
||||
<span className="text-red-500">Upload Failed</span>
|
||||
</Tooltip2>
|
||||
) : (
|
||||
<div className="text-[#999]">
|
||||
{uploaded ? (
|
||||
<div className="flex gap-2">
|
||||
{extname && <span>{extname}</span>}
|
||||
<span>{filesize(size)}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span>{t("assistant.fileList.uploading")}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AttachmentList;
|
||||
@@ -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 && (
|
||||
<PrevSuggestion sendMessage={init} />
|
||||
<PrevSuggestion
|
||||
sendMessage={(message) => {
|
||||
init({ message });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -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<HTMLDivElement>(null);
|
||||
|
||||
const { scrollToBottom } = useChatScroll(messagesEndRef);
|
||||
@@ -168,13 +167,13 @@ export const ChatContent = ({
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* {uploadFiles.length > 0 && (
|
||||
<div key={sessionId} className="max-h-[120px] overflow-auto p-2">
|
||||
<FileList />
|
||||
{uploadAttachments.length > 0 && (
|
||||
<div key={currentSessionId} className="max-h-[120px] overflow-auto p-2">
|
||||
<AttachmentList />
|
||||
</div>
|
||||
)} */}
|
||||
)}
|
||||
|
||||
{/* {sessionId && <SessionFile sessionId={sessionId} />} */}
|
||||
{/* {currentSessionId && <SessionFile sessionId={currentSessionId} />} */}
|
||||
|
||||
<ScrollToBottom scrollRef={scrollRef} isAtBottom={isAtBottom} />
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex flex-wrap gap-y-2 -mx-1 text-sm">
|
||||
{uploadFiles.map((file) => {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
path,
|
||||
extname,
|
||||
size,
|
||||
uploaded,
|
||||
attachmentId,
|
||||
uploadFailed,
|
||||
failedMessage,
|
||||
} = file;
|
||||
|
||||
return (
|
||||
<div key={id} className="w-1/3 px-1">
|
||||
<div className="relative group flex items-center gap-1 p-1 rounded-[4px] bg-[#dedede] dark:bg-[#202126]">
|
||||
{(uploadFailed || attachmentId) && (
|
||||
<div
|
||||
className="absolute flex justify-center items-center size-[14px] bg-red-600 top-0 right-0 rounded-full cursor-pointer translate-x-[5px] -translate-y-[5px] transition opacity-0 group-hover:opacity-100 "
|
||||
onClick={() => {
|
||||
deleteFile(file);
|
||||
}}
|
||||
>
|
||||
<X className="size-[10px] text-white" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FileIcon path={path} />
|
||||
|
||||
<div className="flex flex-col justify-between overflow-hidden">
|
||||
<div className="truncate text-[#333333] dark:text-[#D8D8D8]">
|
||||
{name}
|
||||
</div>
|
||||
|
||||
<div className="text-xs">
|
||||
{uploadFailed && failedMessage ? (
|
||||
<Tooltip2 content={failedMessage}>
|
||||
<span className="text-red-500">Upload Failed</span>
|
||||
</Tooltip2>
|
||||
) : (
|
||||
<div className="text-[#999]">
|
||||
{uploaded ? (
|
||||
<div className="flex gap-2">
|
||||
{extname && <span>{extname}</span>}
|
||||
<span>
|
||||
{filesize(size, { standard: "jedec", spacer: "" })}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span>{t("assistant.fileList.uploading")}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileList;
|
||||
@@ -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) => {
|
||||
</div>
|
||||
<div className="text-xs text-[#999]">
|
||||
{icon && <span className="pr-2">{icon}</span>}
|
||||
<span>
|
||||
{filesize(size, { standard: "jedec", spacer: "" })}
|
||||
</span>
|
||||
<span>{filesize(size)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<UserMessageProps> = (props) => {
|
||||
const { message, attachments } = props;
|
||||
|
||||
const [showCopyButton, setShowCopyButton] = useState(false);
|
||||
const { currentService } = useConnectStore();
|
||||
const [attachmentData, setAttachmentData] = useState<any[]>([]);
|
||||
const { addError } = useAppStore();
|
||||
|
||||
const handleDoubleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
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 (
|
||||
<div
|
||||
className="max-w-full flex gap-1 items-center justify-end"
|
||||
onMouseEnter={() => setShowCopyButton(true)}
|
||||
onMouseLeave={() => setShowCopyButton(false)}
|
||||
>
|
||||
<div
|
||||
className={clsx("size-6 transition", {
|
||||
"opacity-0": !showCopyButton,
|
||||
})}
|
||||
>
|
||||
<CopyButton textToCopy={messageContent} />
|
||||
</div>
|
||||
<div
|
||||
className="max-w-[85%] overflow-auto text-left px-3 py-2 bg-white dark:bg-[#202126] rounded-xl border border-black/12 dark:border-black/15 font-normal text-sm text-[#333333] dark:text-[#D8D8D8] cursor-pointer user-select-text whitespace-pre-wrap"
|
||||
onDoubleClick={handleDoubleClick}
|
||||
>
|
||||
{messageContent}
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
{message && (
|
||||
<div
|
||||
className="flex gap-1 items-center justify-end"
|
||||
onMouseEnter={() => setShowCopyButton(true)}
|
||||
onMouseLeave={() => setShowCopyButton(false)}
|
||||
>
|
||||
<div
|
||||
className={clsx("size-6 transition", {
|
||||
"opacity-0": !showCopyButton,
|
||||
})}
|
||||
>
|
||||
<CopyButton textToCopy={message} />
|
||||
</div>
|
||||
<div
|
||||
className="max-w-[85%] overflow-auto text-left px-3 py-2 bg-white dark:bg-[#202126] rounded-xl border border-black/12 dark:border-black/15 font-normal text-sm text-[#333333] dark:text-[#D8D8D8] cursor-pointer user-select-text whitespace-pre-wrap"
|
||||
onDoubleClick={handleDoubleClick}
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{attachmentData && (
|
||||
<div
|
||||
className={clsx("flex justify-end flex-wrap gap-y-2 w-full", {
|
||||
"mt-3": message,
|
||||
})}
|
||||
>
|
||||
{attachmentData.map((item) => {
|
||||
const { id, name, size, icon } = item._source;
|
||||
|
||||
return (
|
||||
<AttachmentItem
|
||||
{...item._source}
|
||||
key={id}
|
||||
uploading={false}
|
||||
uploaded
|
||||
id={id}
|
||||
extname={icon}
|
||||
attachmentId={id}
|
||||
name={name}
|
||||
path={name}
|
||||
size={size}
|
||||
deletable={false}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 <UserMessage messageContent={messageContent} />;
|
||||
return <UserMessage message={messageContent} attachments={attachments} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -18,7 +18,9 @@ const FileIcon: FC<FileIconProps> = (props) => {
|
||||
.then(setIconName);
|
||||
});
|
||||
|
||||
return <FontIcon name={iconName} className={twMerge("size-8", className)} />;
|
||||
return (
|
||||
<FontIcon name={iconName} className={twMerge("min-w-8 h-8", className)} />
|
||||
);
|
||||
};
|
||||
|
||||
export default FileIcon;
|
||||
|
||||
@@ -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<ChatIconsProps> = ({
|
||||
if (curChatEnd) {
|
||||
return (
|
||||
<button
|
||||
className={`ml-1 p-1 ${
|
||||
inputValue ? "bg-[#0072FF]" : "bg-[#E4E5F0] dark:bg-[rgb(84,84,84)]"
|
||||
} rounded-full transition-colors h-6`}
|
||||
className={clsx(
|
||||
"ml-1 p-1 rounded-full transition-colors h-6 bg-[#E4E5F0] dark:bg-[rgb(84,84,84)]",
|
||||
{
|
||||
"!bg-[#0072FF]": inputValue || isAttachmentsUploaded(),
|
||||
}
|
||||
)}
|
||||
type="submit"
|
||||
onClick={() => onSend(inputValue.trim())}
|
||||
onClick={() => {
|
||||
onSend({
|
||||
message: inputValue.trim(),
|
||||
attachments: getUploadedAttachmentsId(),
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Send className="w-4 h-4 text-white" />
|
||||
</button>
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 ? (
|
||||
<div className="flex gap-2 text-[12px] leading-3 text-[#333] dark:text-[#d8d8d8]">
|
||||
{/* <InputUpload
|
||||
checkScreenPermission={checkScreenPermission}
|
||||
requestScreenPermission={requestScreenPermission}
|
||||
getScreenMonitors={getScreenMonitors}
|
||||
getScreenWindows={getScreenWindows}
|
||||
captureMonitorScreenshot={captureMonitorScreenshot}
|
||||
captureWindowScreenshot={captureWindowScreenshot}
|
||||
openFileDialog={openFileDialog}
|
||||
getFileMetadata={getFileMetadata}
|
||||
getFileIcon={getFileIcon}
|
||||
/> */}
|
||||
{source?.upload?.enabled && (
|
||||
<InputUpload
|
||||
checkScreenPermission={checkScreenPermission}
|
||||
requestScreenPermission={requestScreenPermission}
|
||||
getScreenMonitors={getScreenMonitors}
|
||||
getScreenWindows={getScreenWindows}
|
||||
captureMonitorScreenshot={captureMonitorScreenshot}
|
||||
captureWindowScreenshot={captureWindowScreenshot}
|
||||
openFileDialog={openFileDialog}
|
||||
getFileMetadata={getFileMetadata}
|
||||
getFileIcon={getFileIcon}
|
||||
/>
|
||||
)}
|
||||
|
||||
{source?.type === "deep_think" && source?.config?.visible && (
|
||||
<button
|
||||
className={clsx(
|
||||
"flex items-center gap-1 p-1 rounded-md transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]",
|
||||
"flex items-center justify-center gap-1 h-[20px] px-1 rounded-md transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]",
|
||||
{
|
||||
"!bg-[rgba(0,114,255,0.3)]": isDeepThinkActive,
|
||||
}
|
||||
@@ -231,7 +232,8 @@ InputControlsProps) => {
|
||||
getMCPByServer={getMCPByServer}
|
||||
/>
|
||||
|
||||
{!(source?.datasource?.enabled && source?.datasource?.visible) &&
|
||||
{!source?.upload?.enabled &&
|
||||
!(source?.datasource?.enabled && source?.datasource?.visible) &&
|
||||
(source?.type !== "deep_think" || !source?.config?.visible) &&
|
||||
!(source?.mcp_servers?.enabled && source?.mcp_servers?.visible) && (
|
||||
<div className="px-[9px]">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, Fragment, MouseEvent } from "react";
|
||||
import { FC, Fragment, MouseEvent, useEffect, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ChevronRight, Plus } from "lucide-react";
|
||||
import {
|
||||
@@ -19,6 +19,8 @@ import { useAppStore } from "@/stores/appStore";
|
||||
import Tooltip from "@/components/Common/Tooltip";
|
||||
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||
import clsx from "clsx";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import { filesize } from "@/utils";
|
||||
|
||||
interface State {
|
||||
screenRecordingPermission?: boolean;
|
||||
@@ -61,9 +63,26 @@ const InputUpload: FC<InputUploadProps> = (props) => {
|
||||
getFileMetadata,
|
||||
} = props;
|
||||
const { t, i18n } = useTranslation();
|
||||
const { uploadFiles, setUploadFiles } = useChatStore();
|
||||
const { uploadAttachments, setUploadAttachments } = useChatStore();
|
||||
const { withVisibility, addError } = useAppStore();
|
||||
const { modifierKey, addFile, modifierKeyPressed } = useShortcutsStore();
|
||||
const { currentAssistant } = useConnectStore();
|
||||
const uploadMaxSizeRef = useRef(1024 * 1024);
|
||||
const uploadMaxCountRef = useRef(6);
|
||||
const setVisibleStartPage = useConnectStore((state) => {
|
||||
return state.setVisibleStartPage;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentAssistant?._source?.upload) return;
|
||||
|
||||
const { max_file_size_in_bytes, max_file_count } =
|
||||
currentAssistant._source.upload;
|
||||
|
||||
uploadMaxSizeRef.current = max_file_size_in_bytes;
|
||||
|
||||
uploadMaxCountRef.current = max_file_count;
|
||||
}, [currentAssistant]);
|
||||
|
||||
const state = useReactive<State>({
|
||||
screenshotableMonitors: [],
|
||||
@@ -83,19 +102,25 @@ const InputUpload: FC<InputUploadProps> = (props) => {
|
||||
|
||||
if (isNil(selectedFiles)) return;
|
||||
|
||||
setVisibleStartPage(false);
|
||||
|
||||
handleUploadFiles(selectedFiles);
|
||||
};
|
||||
|
||||
const handleUploadFiles = async (paths: string | string[]) => {
|
||||
const files: typeof uploadFiles = [];
|
||||
const files: typeof uploadAttachments = [];
|
||||
|
||||
for await (const path of castArray(paths)) {
|
||||
if (find(uploadFiles, { path })) continue;
|
||||
if (find(uploadAttachments, { path })) continue;
|
||||
|
||||
const stat = await getFileMetadata(path);
|
||||
|
||||
if (stat.size / 1024 / 1024 > 100) {
|
||||
addError(t("search.input.uploadFileHints.maxSize"));
|
||||
if (stat.size > uploadMaxSizeRef.current) {
|
||||
addError(
|
||||
t("search.input.uploadFileHints.maxSize", {
|
||||
replace: [filesize(uploadMaxSizeRef.current)],
|
||||
})
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
@@ -107,7 +132,7 @@ const InputUpload: FC<InputUploadProps> = (props) => {
|
||||
});
|
||||
}
|
||||
|
||||
setUploadFiles([...uploadFiles, ...files]);
|
||||
setUploadAttachments([...uploadAttachments, ...files]);
|
||||
};
|
||||
|
||||
const menuItems = useCreation<MenuItem[]>(() => {
|
||||
@@ -176,8 +201,15 @@ const InputUpload: FC<InputUploadProps> = (props) => {
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuButton className="flex p-1 rounded-md transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]">
|
||||
<Tooltip content={t("search.input.uploadFileHints.tooltip")}>
|
||||
<MenuButton className="flex items-center justify-center h-[20px] px-1 rounded-md transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]">
|
||||
<Tooltip
|
||||
content={t("search.input.uploadFileHints.tooltip", {
|
||||
replace: [
|
||||
uploadMaxCountRef.current,
|
||||
filesize(uploadMaxSizeRef.current),
|
||||
],
|
||||
})}
|
||||
>
|
||||
<Plus
|
||||
className={clsx("size-3 scale-[1.3]", {
|
||||
hidden: modifierKeyPressed,
|
||||
|
||||
@@ -166,7 +166,7 @@ export default function MCPPopover({
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"flex items-center gap-1 p-1 rounded-md transition hover:bg-[#EDEDED] dark:hover:bg-[#202126] cursor-pointer",
|
||||
"flex justify-center items-center gap-1 h-[20px] px-1 rounded-md transition hover:bg-[#EDEDED] dark:hover:bg-[#202126] cursor-pointer",
|
||||
{
|
||||
"!bg-[rgba(0,114,255,0.3)]": isMCPActive,
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ export default function SearchPopover({
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"flex items-center gap-1 p-1 rounded-md transition hover:bg-[#EDEDED] dark:hover:bg-[#202126] cursor-pointer",
|
||||
"flex justify-center items-center gap-1 h-[20px] px-1 rounded-md transition hover:bg-[#EDEDED] dark:hover:bg-[#202126] cursor-pointer",
|
||||
{
|
||||
"!bg-[rgba(0,114,255,0.3)]": isSearchActive,
|
||||
}
|
||||
|
||||
@@ -13,7 +13,10 @@ import { useMount } from "ahooks";
|
||||
|
||||
import Search from "@/components/Search/Search";
|
||||
import InputBox from "@/components/Search/InputBox";
|
||||
import ChatAI, { ChatAIRef } from "@/components/Assistant/Chat";
|
||||
import ChatAI, {
|
||||
ChatAIRef,
|
||||
SendMessageParams,
|
||||
} from "@/components/Assistant/Chat";
|
||||
import { isLinux, isWin } from "@/utils/platform";
|
||||
import { appReducer, initialAppState } from "@/reducers/appReducer";
|
||||
import { useWindowEvents } from "@/hooks/useWindowEvents";
|
||||
@@ -25,6 +28,7 @@ import { useThemeStore } from "@/stores/themeStore";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import { useAppearanceStore } from "@/stores/appearanceStore";
|
||||
import type { StartPage } from "@/types/chat";
|
||||
import { isAttachmentsUploaded } from "@/utils";
|
||||
|
||||
interface SearchChatProps {
|
||||
isTauri?: boolean;
|
||||
@@ -148,10 +152,12 @@ function SearchChat({
|
||||
}, []);
|
||||
|
||||
const handleSendMessage = useCallback(
|
||||
async (value: string) => {
|
||||
dispatch({ type: "SET_INPUT", payload: value });
|
||||
async (params: SendMessageParams) => {
|
||||
if (!isAttachmentsUploaded()) return;
|
||||
|
||||
dispatch({ type: "SET_INPUT", payload: params?.message ?? "" });
|
||||
if (isChatMode) {
|
||||
chatAIRef.current?.init(value);
|
||||
chatAIRef.current?.init(params);
|
||||
}
|
||||
},
|
||||
[isChatMode]
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
INITIAL_MODE_SWITCH,
|
||||
INITIAL_RETURN_TO_INPUT,
|
||||
// INITIAL_VOICE_INPUT,
|
||||
// INITIAL_ADD_FILE,
|
||||
INITIAL_ADD_FILE,
|
||||
INITIAL_DEEP_THINKING,
|
||||
INITIAL_INTERNET_SEARCH,
|
||||
INITIAL_INTERNET_SEARCH_SCOPE,
|
||||
@@ -46,8 +46,8 @@ const Shortcuts = () => {
|
||||
setReturnToInput,
|
||||
// voiceInput,
|
||||
// setVoiceInput,
|
||||
// addFile,
|
||||
// setAddFile,
|
||||
addFile,
|
||||
setAddFile,
|
||||
deepThinking,
|
||||
setDeepThinking,
|
||||
internetSearch,
|
||||
@@ -106,15 +106,13 @@ const Shortcuts = () => {
|
||||
// value: voiceInput,
|
||||
// setValue: setVoiceInput,
|
||||
// },
|
||||
// {
|
||||
// title: "settings.advanced.shortcuts.addFile.title",
|
||||
// description: "settings.advanced.shortcuts.addFile.description",
|
||||
// value: addFile,
|
||||
// setValue: setAddFile,
|
||||
// reset: () => {
|
||||
// handleChange(INITIAL_ADD_FILE, setAddFile);
|
||||
// },
|
||||
// },
|
||||
{
|
||||
title: "settings.advanced.shortcuts.addFile.title",
|
||||
description: "settings.advanced.shortcuts.addFile.description",
|
||||
initialValue: INITIAL_ADD_FILE,
|
||||
value: addFile,
|
||||
setValue: setAddFile,
|
||||
},
|
||||
{
|
||||
title: "settings.advanced.shortcuts.deepThinking.title",
|
||||
description: "settings.advanced.shortcuts.deepThinking.description",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useContext, useMemo, useState } from "react";
|
||||
import { filesize } from "filesize";
|
||||
import dayjs from "dayjs";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAsyncEffect } from "ahooks";
|
||||
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { ExtensionsContext } from "../../../index";
|
||||
import { filesize } from "@/utils";
|
||||
|
||||
interface Metadata {
|
||||
name: string;
|
||||
@@ -58,7 +58,7 @@ const App = () => {
|
||||
},
|
||||
{
|
||||
label: t("settings.extensions.application.details.size"),
|
||||
value: filesize(size, { standard: "jedec", spacer: "" }),
|
||||
value: filesize(size),
|
||||
},
|
||||
{
|
||||
label: t("settings.extensions.application.details.created"),
|
||||
|
||||
@@ -9,6 +9,9 @@ import { useSearchStore } from "@/stores/searchStore";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
import { unrequitable } from "@/utils";
|
||||
import { streamPost } from "@/api/streamFetch";
|
||||
import { SendMessageParams } from "@/components/Assistant/Chat";
|
||||
import { isEmpty } from "lodash-es";
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
|
||||
export function useChatActions(
|
||||
setActiveChat: (chat: Chat | undefined) => void,
|
||||
@@ -40,6 +43,9 @@ export function useChatActions(
|
||||
} = useConnectStore();
|
||||
const sourceDataIds = useSearchStore((state) => state.sourceDataIds);
|
||||
const MCPIds = useSearchStore((state) => state.MCPIds);
|
||||
const setUploadAttachments = useChatStore((state) => {
|
||||
return state.setUploadAttachments;
|
||||
});
|
||||
|
||||
const [keyword, setKeyword] = useState("");
|
||||
|
||||
@@ -289,10 +295,12 @@ export function useChatActions(
|
||||
);
|
||||
|
||||
const prepareChatSession = useCallback(
|
||||
async (value: string, timestamp: number) => {
|
||||
async (timestamp: number, value: string) => {
|
||||
// 1. Cleaning and preparation
|
||||
await clearAllChunkData();
|
||||
|
||||
setUploadAttachments([]);
|
||||
|
||||
// 2. Update the status again
|
||||
await new Promise<void>((resolve) => {
|
||||
changeInput && changeInput("");
|
||||
@@ -310,12 +318,17 @@ export function useChatActions(
|
||||
);
|
||||
|
||||
const createNewChat = useCallback(
|
||||
async (value: string = "") => {
|
||||
if (!value) return;
|
||||
async (params?: SendMessageParams) => {
|
||||
const { message, attachments } = params || {};
|
||||
|
||||
console.log("message", message);
|
||||
console.log("attachments", attachments);
|
||||
|
||||
if (!message && isEmpty(attachments)) return;
|
||||
|
||||
const timestamp = Date.now();
|
||||
|
||||
await prepareChatSession(value, timestamp);
|
||||
await prepareChatSession(timestamp, message ?? "");
|
||||
|
||||
const queryParams = {
|
||||
search: isSearchActive,
|
||||
@@ -328,19 +341,22 @@ export function useChatActions(
|
||||
|
||||
if (isTauri) {
|
||||
if (!currentService?.id) return;
|
||||
|
||||
console.log("chat_create", clientId, timestamp);
|
||||
|
||||
await platformAdapter.commands("chat_create", {
|
||||
serverId: currentService?.id,
|
||||
message: value,
|
||||
message,
|
||||
attachments,
|
||||
queryParams,
|
||||
clientId: `chat-stream-${clientId}-${timestamp}`,
|
||||
});
|
||||
console.log("_create end", value);
|
||||
console.log("_create end", message);
|
||||
resetChatState();
|
||||
} else {
|
||||
await streamPost({
|
||||
url: "/chat/_create",
|
||||
body: { message: value },
|
||||
body: { message },
|
||||
queryParams,
|
||||
onMessage: (line) => {
|
||||
console.log("⏳", line);
|
||||
@@ -365,12 +381,16 @@ export function useChatActions(
|
||||
);
|
||||
|
||||
const sendMessage = useCallback(
|
||||
async (content: string, newChat: Chat) => {
|
||||
if (!newChat?._id || !content) return;
|
||||
async (newChat: Chat, params?: SendMessageParams) => {
|
||||
if (!newChat?._id || !params) return;
|
||||
|
||||
const { message, attachments } = params;
|
||||
|
||||
if (!message && isEmpty(attachments)) return;
|
||||
|
||||
const timestamp = Date.now();
|
||||
|
||||
await prepareChatSession(content, timestamp);
|
||||
await prepareChatSession(timestamp, message ?? "");
|
||||
|
||||
const queryParams = {
|
||||
search: isSearchActive,
|
||||
@@ -388,15 +408,16 @@ export function useChatActions(
|
||||
serverId: currentService?.id,
|
||||
sessionId: newChat?._id,
|
||||
queryParams,
|
||||
message: content,
|
||||
message,
|
||||
attachments,
|
||||
clientId: `chat-stream-${clientId}-${timestamp}`,
|
||||
});
|
||||
console.log("chat_chat end", content, clientId);
|
||||
console.log("chat_chat end", message, clientId);
|
||||
resetChatState();
|
||||
} else {
|
||||
await streamPost({
|
||||
url: `/chat/${newChat?._id}/_chat`,
|
||||
body: { message: content },
|
||||
body: { message },
|
||||
queryParams,
|
||||
onMessage: (line) => {
|
||||
console.log("line", line);
|
||||
@@ -421,10 +442,14 @@ export function useChatActions(
|
||||
);
|
||||
|
||||
const handleSendMessage = useCallback(
|
||||
async (content: string, activeChat?: Chat) => {
|
||||
if (!activeChat?._id || !content) return;
|
||||
async (activeChat?: Chat, params?: SendMessageParams) => {
|
||||
if (!activeChat?._id) return;
|
||||
|
||||
await chatHistory(activeChat, (chat) => sendMessage(content, chat));
|
||||
const { message, attachments } = params ?? {};
|
||||
|
||||
if (!message && isEmpty(attachments)) return;
|
||||
|
||||
await chatHistory(activeChat, (chat) => sendMessage(chat, params));
|
||||
},
|
||||
[chatHistory, sendMessage]
|
||||
);
|
||||
|
||||
@@ -351,8 +351,8 @@
|
||||
"allScope": "All Scope"
|
||||
},
|
||||
"uploadFileHints": {
|
||||
"tooltip": "Support screenshots, upload files, up to 50, single file up to 100 MB.",
|
||||
"maxSize": "The file size cannot exceed 100 MB."
|
||||
"tooltip": "Support screenshots, upload files, up to {{0}}, single file up to {{1}}.",
|
||||
"maxSize": "The file size cannot exceed {{0}}."
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
|
||||
@@ -351,8 +351,8 @@
|
||||
"allScope": "所有范围"
|
||||
},
|
||||
"uploadFileHints": {
|
||||
"tooltip": "支持截图、上传文件,最多 50个,单个文件最大 100 MB。",
|
||||
"maxSize": "文件大小不能超过 100 MB。"
|
||||
"tooltip": "支持截图、上传文件,最多 {{0}} 个,单个文件最大 {{1}}。",
|
||||
"maxSize": "文件大小不能超过 {{0}}。"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
|
||||
@@ -14,7 +14,10 @@ import {
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { metadata, icon } from "tauri-plugin-fs-pro-api";
|
||||
|
||||
import ChatAI, { ChatAIRef } from "@/components/Assistant/Chat";
|
||||
import ChatAI, {
|
||||
ChatAIRef,
|
||||
SendMessageParams,
|
||||
} from "@/components/Assistant/Chat";
|
||||
import type { Chat as typeChat } from "@/types/chat";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import InputBox from "@/components/Search/InputBox";
|
||||
@@ -99,14 +102,14 @@ export default function StandaloneChat({}: StandaloneChatProps) {
|
||||
if (remainingChats.length > 0) {
|
||||
setActiveChat(remainingChats[0]);
|
||||
} else {
|
||||
chatAIRef.current?.init("");
|
||||
chatAIRef.current?.init({ message: "" });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendMessage = async (content: string) => {
|
||||
setInput(content);
|
||||
chatAIRef.current?.init(content);
|
||||
const handleSendMessage = async (params: SendMessageParams) => {
|
||||
setInput(params?.message ?? "");
|
||||
chatAIRef.current?.init(params);
|
||||
};
|
||||
|
||||
const chatHistory = async (chat: typeChat) => {
|
||||
|
||||
@@ -5,9 +5,10 @@ import {
|
||||
} from "zustand/middleware";
|
||||
import { Metadata } from "tauri-plugin-fs-pro-api";
|
||||
|
||||
export interface UploadFile extends Metadata {
|
||||
export interface UploadAttachments extends Metadata {
|
||||
id: string;
|
||||
path: string;
|
||||
uploading: boolean;
|
||||
uploaded?: boolean;
|
||||
attachmentId?: string;
|
||||
uploadFailed?: boolean;
|
||||
@@ -28,8 +29,8 @@ export type IChatStore = {
|
||||
setConnected: (value: boolean) => void;
|
||||
messages: string;
|
||||
setMessages: (value: string | ((prev: string) => string)) => void;
|
||||
uploadFiles: UploadFile[];
|
||||
setUploadFiles: (value: UploadFile[]) => void;
|
||||
uploadAttachments: UploadAttachments[];
|
||||
setUploadAttachments: (value: UploadAttachments[]) => void;
|
||||
synthesizeItem?: SynthesizeItem;
|
||||
setSynthesizeItem: (synthesizeItem?: SynthesizeItem) => void;
|
||||
};
|
||||
@@ -48,9 +49,9 @@ export const useChatStore = create<IChatStore>()(
|
||||
set((state) => ({
|
||||
messages: typeof value === "function" ? value(state.messages) : value,
|
||||
})),
|
||||
uploadFiles: [],
|
||||
setUploadFiles: (uploadFiles: UploadFile[]) => {
|
||||
return set(() => ({ uploadFiles }));
|
||||
uploadAttachments: [],
|
||||
setUploadAttachments: (uploadAttachments: UploadAttachments[]) => {
|
||||
return set(() => ({ uploadAttachments }));
|
||||
},
|
||||
setSynthesizeItem(synthesizeItem?: SynthesizeItem) {
|
||||
return set(() => ({ synthesizeItem }));
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface ISource {
|
||||
session_id?: string;
|
||||
type?: string;
|
||||
message?: any;
|
||||
attachments?: string[];
|
||||
title?: string;
|
||||
question?: string;
|
||||
details?: any[] | null;
|
||||
@@ -74,4 +75,4 @@ export interface Assistant {
|
||||
ids?: string[];
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,10 @@ export interface UploadAttachmentResponse {
|
||||
attachments: string[];
|
||||
}
|
||||
|
||||
export type GetAttachmentPayload = Omit<UploadAttachmentPayload, "filePaths">;
|
||||
export interface GetAttachmentByIdsPayload {
|
||||
serverId: string;
|
||||
attachments: string[];
|
||||
}
|
||||
|
||||
export interface AttachmentHit {
|
||||
_index: string;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { isArray, isNil, isObject, isString } from "lodash-es";
|
||||
import { filesize as filesizeLib } from "filesize";
|
||||
|
||||
import platformAdapter from "./platformAdapter";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { DEFAULT_COCO_SERVER_ID, HISTORY_PANEL_ID } from "@/constants";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
|
||||
// 1
|
||||
export async function copyToClipboard(text: string) {
|
||||
@@ -192,3 +194,23 @@ export const isDefaultServer = (checkAvailability = true) => {
|
||||
|
||||
return isTauri && isDefaultServer;
|
||||
};
|
||||
|
||||
export const filesize = (value: number, spacer?: string) => {
|
||||
return filesizeLib(value, { standard: "jedec", spacer });
|
||||
};
|
||||
|
||||
export const isAttachmentsUploaded = () => {
|
||||
const { uploadAttachments } = useChatStore.getState();
|
||||
|
||||
if (uploadAttachments.length === 0) return false;
|
||||
|
||||
return uploadAttachments.every((item) => !item.uploading);
|
||||
};
|
||||
|
||||
export const getUploadedAttachmentsId = () => {
|
||||
const { uploadAttachments } = useChatStore.getState();
|
||||
|
||||
return uploadAttachments
|
||||
.map((item) => item.attachmentId)
|
||||
.filter((id) => !isNil(id));
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user