2025-02-18 09:40:00 +08:00
|
|
|
import {
|
|
|
|
|
forwardRef,
|
|
|
|
|
memo,
|
|
|
|
|
useCallback,
|
|
|
|
|
useEffect,
|
|
|
|
|
useImperativeHandle,
|
|
|
|
|
useRef,
|
|
|
|
|
useState,
|
2025-02-20 15:38:55 +08:00
|
|
|
useMemo,
|
2025-02-18 09:40:00 +08:00
|
|
|
} from "react";
|
|
|
|
|
import { isTauri } from "@tauri-apps/api/core";
|
|
|
|
|
import { useTranslation } from "react-i18next";
|
2025-02-19 17:23:49 +08:00
|
|
|
import { debounce } from "lodash-es";
|
2025-02-21 18:57:32 +08:00
|
|
|
import { listen } from "@tauri-apps/api/event";
|
|
|
|
|
import { invoke } from "@tauri-apps/api/core";
|
2025-02-18 09:40:00 +08:00
|
|
|
|
|
|
|
|
import { ChatMessage } from "./ChatMessage";
|
2025-02-20 15:38:55 +08:00
|
|
|
import type { Chat } from "./types";
|
2025-02-18 09:40:00 +08:00
|
|
|
import { tauriFetch } from "@/api/tauriFetchClient";
|
|
|
|
|
import { useChatStore } from "@/stores/chatStore";
|
|
|
|
|
import { useWindows } from "@/hooks/useWindows";
|
2025-02-20 16:24:53 +08:00
|
|
|
import { ChatHeader } from "./ChatHeader";
|
2025-02-21 18:57:32 +08:00
|
|
|
import { useAppStore } from "@/stores/appStore";
|
|
|
|
|
|
2024-11-20 10:08:08 +08:00
|
|
|
interface ChatAIProps {
|
2025-02-18 09:40:00 +08:00
|
|
|
isTransitioned: boolean;
|
|
|
|
|
isSearchActive?: boolean;
|
|
|
|
|
isDeepThinkActive?: boolean;
|
2025-02-20 15:38:55 +08:00
|
|
|
isChatPage?: boolean;
|
|
|
|
|
activeChatProp?: Chat;
|
|
|
|
|
changeInput?: (val: string) => void;
|
2024-11-20 10:08:08 +08:00
|
|
|
}
|
|
|
|
|
|
2024-11-24 19:25:47 +08:00
|
|
|
export interface ChatAIRef {
|
2025-02-18 09:40:00 +08:00
|
|
|
init: (value: string) => void;
|
|
|
|
|
cancelChat: () => void;
|
|
|
|
|
connected: boolean;
|
|
|
|
|
reconnect: () => void;
|
2025-02-20 15:38:55 +08:00
|
|
|
handleSendMessage: (value: string) => void;
|
2024-11-24 19:25:47 +08:00
|
|
|
}
|
2024-11-20 10:08:08 +08:00
|
|
|
|
2025-02-18 09:40:00 +08:00
|
|
|
const ChatAI = memo(
|
|
|
|
|
forwardRef<ChatAIRef, ChatAIProps>(
|
|
|
|
|
(
|
2025-02-20 15:38:55 +08:00
|
|
|
{
|
|
|
|
|
isTransitioned,
|
|
|
|
|
changeInput,
|
|
|
|
|
isSearchActive,
|
|
|
|
|
isDeepThinkActive,
|
|
|
|
|
isChatPage = false,
|
|
|
|
|
activeChatProp,
|
|
|
|
|
},
|
2025-02-18 09:40:00 +08:00
|
|
|
ref
|
|
|
|
|
) => {
|
2025-02-21 18:57:32 +08:00
|
|
|
if (!isTransitioned) return null;
|
|
|
|
|
|
2025-02-18 09:40:00 +08:00
|
|
|
const { t } = useTranslation();
|
2025-02-21 18:57:32 +08:00
|
|
|
|
2025-02-18 09:40:00 +08:00
|
|
|
useImperativeHandle(ref, () => ({
|
|
|
|
|
init: init,
|
|
|
|
|
cancelChat: cancelChat,
|
|
|
|
|
connected: connected,
|
|
|
|
|
reconnect: reconnect,
|
2025-02-20 15:38:55 +08:00
|
|
|
handleSendMessage: handleSendMessage,
|
2025-02-18 09:40:00 +08:00
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const { createWin } = useWindows();
|
|
|
|
|
|
2025-02-21 18:57:32 +08:00
|
|
|
const { curChatEnd, setCurChatEnd, connected, setConnected, messages, setMessages } =
|
|
|
|
|
useChatStore();
|
|
|
|
|
const activeServer = useAppStore((state) => state.activeServer);
|
2025-02-18 09:40:00 +08:00
|
|
|
|
|
|
|
|
const [activeChat, setActiveChat] = useState<Chat>();
|
|
|
|
|
const [isTyping, setIsTyping] = useState(false);
|
2025-02-19 17:23:49 +08:00
|
|
|
const [timedoutShow, setTimedoutShow] = useState(false);
|
2025-02-18 09:40:00 +08:00
|
|
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
|
|
const [curMessage, setCurMessage] = useState("");
|
|
|
|
|
|
2025-02-19 17:23:49 +08:00
|
|
|
const websocketIdRef = useRef("");
|
|
|
|
|
|
2025-02-18 09:40:00 +08:00
|
|
|
const curChatEndRef = useRef(curChatEnd);
|
|
|
|
|
curChatEndRef.current = curChatEnd;
|
|
|
|
|
|
2025-02-20 15:38:55 +08:00
|
|
|
const curIdRef = useRef("");
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
activeChatProp && setActiveChat(activeChatProp);
|
|
|
|
|
}, [activeChatProp]);
|
2025-02-18 09:40:00 +08:00
|
|
|
|
|
|
|
|
const handleMessageChunk = useCallback((chunk: string) => {
|
|
|
|
|
setCurMessage((prev) => prev + chunk);
|
|
|
|
|
}, []);
|
|
|
|
|
|
2025-02-21 18:57:32 +08:00
|
|
|
const reconnect = async () => {
|
|
|
|
|
if (!activeServer?.id) return;
|
|
|
|
|
try {
|
|
|
|
|
await invoke("connect_to_server", { id: activeServer?.id });
|
|
|
|
|
setConnected(true);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to connect:", error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-02-19 17:23:49 +08:00
|
|
|
const messageTimeoutRef = useRef<NodeJS.Timeout>();
|
2025-02-18 09:40:00 +08:00
|
|
|
|
2025-02-21 18:57:32 +08:00
|
|
|
const dealMsg = useCallback(
|
|
|
|
|
(msg: string) => {
|
|
|
|
|
// console.log("msg:", msg);
|
|
|
|
|
if (messageTimeoutRef.current) {
|
|
|
|
|
clearTimeout(messageTimeoutRef.current);
|
|
|
|
|
}
|
2025-02-18 09:40:00 +08:00
|
|
|
|
2025-02-21 18:57:32 +08:00
|
|
|
if (msg.includes("websocket-session-id")) {
|
|
|
|
|
const array = msg.split(" ");
|
|
|
|
|
websocketIdRef.current = array[2];
|
|
|
|
|
return "";
|
|
|
|
|
} else if (msg.includes("PRIVATE")) {
|
|
|
|
|
messageTimeoutRef.current = setTimeout(() => {
|
|
|
|
|
if (!curChatEnd && isTyping) {
|
|
|
|
|
console.log("AI response timeout");
|
|
|
|
|
setTimedoutShow(true);
|
|
|
|
|
cancelChat();
|
|
|
|
|
}
|
|
|
|
|
}, 30000);
|
2025-02-19 17:23:49 +08:00
|
|
|
|
2025-02-21 18:57:32 +08:00
|
|
|
if (msg.includes("assistant finished output")) {
|
|
|
|
|
if (messageTimeoutRef.current) {
|
|
|
|
|
clearTimeout(messageTimeoutRef.current);
|
|
|
|
|
}
|
|
|
|
|
console.log("AI finished output");
|
|
|
|
|
setCurChatEnd(true);
|
|
|
|
|
} else {
|
|
|
|
|
const cleanedData = msg.replace(/^PRIVATE /, "");
|
|
|
|
|
try {
|
|
|
|
|
// console.log("cleanedData", cleanedData);
|
|
|
|
|
const chunkData = JSON.parse(cleanedData);
|
|
|
|
|
if (chunkData.reply_to_message === curIdRef.current) {
|
|
|
|
|
handleMessageChunk(chunkData.message_chunk);
|
|
|
|
|
return chunkData.message_chunk;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("parse error:", error);
|
2025-02-18 09:40:00 +08:00
|
|
|
}
|
2025-02-17 16:37:33 +08:00
|
|
|
}
|
2025-02-18 09:40:00 +08:00
|
|
|
}
|
2025-02-21 18:57:32 +08:00
|
|
|
},
|
|
|
|
|
[curChatEnd, isTyping]
|
2025-02-19 17:23:49 +08:00
|
|
|
);
|
2024-11-24 19:25:47 +08:00
|
|
|
|
2025-02-21 18:57:32 +08:00
|
|
|
useEffect(() => {
|
|
|
|
|
const unlisten = listen("ws-message", (event) => {
|
|
|
|
|
const data = dealMsg(String(event.payload));
|
|
|
|
|
if (data) {
|
|
|
|
|
setMessages((prev) => prev + data);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
unlisten.then((fn) => fn());
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
2025-02-20 15:38:55 +08:00
|
|
|
const assistantMessage = useMemo(() => {
|
|
|
|
|
if (!activeChat?._id || (!curMessage && !messages)) return null;
|
|
|
|
|
return {
|
2025-02-18 09:40:00 +08:00
|
|
|
_id: activeChat._id,
|
|
|
|
|
_source: {
|
|
|
|
|
type: "assistant",
|
2025-02-19 17:23:49 +08:00
|
|
|
message: curMessage || messages,
|
2025-02-18 09:40:00 +08:00
|
|
|
},
|
2025-02-17 16:37:33 +08:00
|
|
|
};
|
2025-02-20 15:38:55 +08:00
|
|
|
}, [activeChat?._id, curMessage, messages]);
|
2024-11-27 19:41:54 +08:00
|
|
|
|
2025-02-20 15:38:55 +08:00
|
|
|
const updatedChat = useMemo(() => {
|
|
|
|
|
if (!activeChat?._id || !assistantMessage) return null;
|
|
|
|
|
return {
|
2025-02-18 09:40:00 +08:00
|
|
|
...activeChat,
|
|
|
|
|
messages: [...(activeChat.messages || []), assistantMessage],
|
2025-02-17 16:37:33 +08:00
|
|
|
};
|
2025-02-20 15:38:55 +08:00
|
|
|
}, [activeChat, assistantMessage]);
|
2025-02-21 18:57:32 +08:00
|
|
|
|
2025-02-20 15:38:55 +08:00
|
|
|
const simulateAssistantResponse = useCallback(() => {
|
|
|
|
|
if (!updatedChat) return;
|
2025-02-19 17:23:49 +08:00
|
|
|
|
2025-02-21 18:57:32 +08:00
|
|
|
console.log("updatedChat:", updatedChat);
|
2025-02-19 17:23:49 +08:00
|
|
|
setActiveChat(updatedChat);
|
2025-02-18 09:40:00 +08:00
|
|
|
setMessages("");
|
|
|
|
|
setCurMessage("");
|
2025-02-19 17:23:49 +08:00
|
|
|
setIsTyping(false);
|
2025-02-20 15:38:55 +08:00
|
|
|
}, [updatedChat]);
|
2025-02-18 09:40:00 +08:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (curChatEnd) {
|
|
|
|
|
simulateAssistantResponse();
|
|
|
|
|
}
|
2025-02-19 17:23:49 +08:00
|
|
|
}, [curChatEnd]);
|
2025-02-18 09:40:00 +08:00
|
|
|
|
2025-02-19 17:23:49 +08:00
|
|
|
const scrollToBottom = useCallback(
|
|
|
|
|
debounce(() => {
|
|
|
|
|
messagesEndRef.current?.scrollIntoView({
|
|
|
|
|
behavior: "smooth",
|
|
|
|
|
block: "end",
|
|
|
|
|
});
|
|
|
|
|
}, 100),
|
|
|
|
|
[]
|
|
|
|
|
);
|
2025-02-18 09:40:00 +08:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
scrollToBottom();
|
|
|
|
|
}, [activeChat?.messages, isTyping, curMessage]);
|
|
|
|
|
|
|
|
|
|
const createNewChat = useCallback(async (value: string = "") => {
|
|
|
|
|
chatClose();
|
|
|
|
|
try {
|
|
|
|
|
const response = await tauriFetch({
|
|
|
|
|
url: "/chat/_new",
|
|
|
|
|
method: "POST",
|
|
|
|
|
});
|
|
|
|
|
console.log("_new", response);
|
|
|
|
|
const newChat: Chat = response.data;
|
2025-02-20 15:38:55 +08:00
|
|
|
|
2025-02-18 09:40:00 +08:00
|
|
|
setActiveChat(newChat);
|
|
|
|
|
handleSendMessage(value, newChat);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to fetch user data:", error);
|
2025-02-17 16:37:33 +08:00
|
|
|
}
|
2025-02-18 09:40:00 +08:00
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const init = (value: string) => {
|
|
|
|
|
if (!curChatEnd) return;
|
|
|
|
|
if (!activeChat?._id) {
|
|
|
|
|
createNewChat(value);
|
|
|
|
|
} else {
|
|
|
|
|
handleSendMessage(value);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-02-20 15:38:55 +08:00
|
|
|
const handleSendMessage = useCallback(
|
|
|
|
|
async (content: string, newChat?: Chat) => {
|
2025-02-21 18:57:32 +08:00
|
|
|
console.log("11111111", isSearchActive, isDeepThinkActive);
|
2025-02-20 15:38:55 +08:00
|
|
|
newChat = newChat || activeChat;
|
|
|
|
|
if (!newChat?._id || !content) return;
|
|
|
|
|
setTimedoutShow(false);
|
|
|
|
|
try {
|
|
|
|
|
const response = await tauriFetch({
|
|
|
|
|
url: `/chat/${newChat?._id}/_send?search=${isSearchActive}&deep_thinking=${isDeepThinkActive}`,
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
"WEBSOCKET-SESSION-ID": websocketIdRef.current,
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({ message: content }),
|
|
|
|
|
});
|
|
|
|
|
console.log("_send", response, websocketIdRef.current);
|
|
|
|
|
curIdRef.current = response.data[0]?._id;
|
|
|
|
|
|
|
|
|
|
const updatedChat: Chat = {
|
|
|
|
|
...newChat,
|
|
|
|
|
messages: [
|
|
|
|
|
...(newChat?.messages || []),
|
|
|
|
|
...(response.data || []),
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
changeInput && changeInput("");
|
2025-02-21 18:57:32 +08:00
|
|
|
console.log("updatedChat2", updatedChat);
|
2025-02-20 15:38:55 +08:00
|
|
|
setActiveChat(updatedChat);
|
|
|
|
|
setIsTyping(true);
|
|
|
|
|
setCurChatEnd(false);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to fetch user data:", error);
|
|
|
|
|
}
|
|
|
|
|
},
|
2025-02-21 18:57:32 +08:00
|
|
|
[activeChat, isSearchActive, isDeepThinkActive]
|
2025-02-20 15:38:55 +08:00
|
|
|
);
|
2025-02-18 09:40:00 +08:00
|
|
|
|
|
|
|
|
const chatClose = async () => {
|
|
|
|
|
if (!activeChat?._id) return;
|
|
|
|
|
try {
|
|
|
|
|
const response = await tauriFetch({
|
|
|
|
|
url: `/chat/${activeChat._id}/_close`,
|
|
|
|
|
method: "POST",
|
|
|
|
|
});
|
|
|
|
|
console.log("_close", response);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to fetch user data:", error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const cancelChat = async () => {
|
2025-02-19 17:23:49 +08:00
|
|
|
if (curMessage || messages) {
|
|
|
|
|
simulateAssistantResponse();
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-18 09:40:00 +08:00
|
|
|
setCurChatEnd(true);
|
|
|
|
|
setIsTyping(false);
|
|
|
|
|
if (!activeChat?._id) return;
|
|
|
|
|
try {
|
|
|
|
|
const response = await tauriFetch({
|
|
|
|
|
url: `/chat/${activeChat._id}/_cancel`,
|
|
|
|
|
method: "POST",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log("_cancel", response);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to fetch user data:", error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
async function openChatAI() {
|
|
|
|
|
if (isTauri()) {
|
|
|
|
|
createWin &&
|
|
|
|
|
createWin({
|
|
|
|
|
label: "chat",
|
|
|
|
|
title: "Coco Chat",
|
|
|
|
|
dragDropEnabled: true,
|
|
|
|
|
center: true,
|
|
|
|
|
width: 1000,
|
|
|
|
|
height: 800,
|
|
|
|
|
alwaysOnTop: false,
|
|
|
|
|
skipTaskbar: false,
|
|
|
|
|
decorations: true,
|
|
|
|
|
closable: true,
|
|
|
|
|
url: "/ui/chat",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-19 17:23:49 +08:00
|
|
|
useEffect(() => {
|
|
|
|
|
return () => {
|
|
|
|
|
if (messageTimeoutRef.current) {
|
|
|
|
|
clearTimeout(messageTimeoutRef.current);
|
|
|
|
|
}
|
|
|
|
|
chatClose();
|
|
|
|
|
setMessages("");
|
|
|
|
|
setCurMessage("");
|
|
|
|
|
setActiveChat(undefined);
|
|
|
|
|
setIsTyping(false);
|
|
|
|
|
setCurChatEnd(true);
|
|
|
|
|
scrollToBottom.cancel();
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
2025-02-18 09:40:00 +08:00
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
data-tauri-drag-region
|
2025-02-20 15:38:55 +08:00
|
|
|
className={`h-full flex flex-col rounded-xl overflow-hidden`}
|
2025-02-18 09:40:00 +08:00
|
|
|
>
|
2025-02-20 15:38:55 +08:00
|
|
|
{isChatPage ? null : (
|
2025-02-20 16:24:53 +08:00
|
|
|
<ChatHeader
|
|
|
|
|
onCreateNewChat={createNewChat}
|
|
|
|
|
onOpenChatAI={openChatAI}
|
|
|
|
|
/>
|
2025-02-20 15:38:55 +08:00
|
|
|
)}
|
2025-02-18 09:40:00 +08:00
|
|
|
|
|
|
|
|
{/* Chat messages */}
|
2025-02-19 17:23:49 +08:00
|
|
|
<div className="w-full overflow-x-hidden overflow-y-auto border-t border-[rgba(0,0,0,0.1)] dark:border-[rgba(255,255,255,0.15)] custom-scrollbar relative">
|
|
|
|
|
<ChatMessage
|
|
|
|
|
key={"greetings"}
|
|
|
|
|
message={{
|
2025-02-20 15:38:55 +08:00
|
|
|
_id: "greetings",
|
2025-02-19 17:23:49 +08:00
|
|
|
_source: {
|
2025-02-20 15:38:55 +08:00
|
|
|
type: "assistant",
|
|
|
|
|
message: t("assistant.chat.greetings"),
|
|
|
|
|
},
|
2025-02-19 17:23:49 +08:00
|
|
|
}}
|
|
|
|
|
isTyping={false}
|
|
|
|
|
/>
|
|
|
|
|
|
2025-02-18 09:40:00 +08:00
|
|
|
{activeChat?.messages?.map((message, index) => (
|
|
|
|
|
<ChatMessage
|
|
|
|
|
key={message._id + index}
|
|
|
|
|
message={message}
|
|
|
|
|
isTyping={
|
|
|
|
|
isTyping &&
|
|
|
|
|
index === (activeChat.messages?.length || 0) - 1 &&
|
|
|
|
|
message._source?.type === "assistant"
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
2025-02-19 17:23:49 +08:00
|
|
|
|
2025-02-18 09:40:00 +08:00
|
|
|
{!curChatEnd && activeChat?._id ? (
|
|
|
|
|
<ChatMessage
|
|
|
|
|
key={"last"}
|
|
|
|
|
message={{
|
|
|
|
|
_id: activeChat?._id,
|
|
|
|
|
_source: {
|
|
|
|
|
type: "assistant",
|
|
|
|
|
message: curMessage,
|
|
|
|
|
},
|
|
|
|
|
}}
|
|
|
|
|
isTyping={!curChatEnd}
|
|
|
|
|
/>
|
|
|
|
|
) : null}
|
2025-02-19 17:23:49 +08:00
|
|
|
|
2025-02-20 15:38:55 +08:00
|
|
|
{timedoutShow ? (
|
|
|
|
|
<ChatMessage
|
|
|
|
|
key={"timedout"}
|
|
|
|
|
message={{
|
|
|
|
|
_id: "timedout",
|
|
|
|
|
_source: {
|
|
|
|
|
type: "assistant",
|
|
|
|
|
message: t("assistant.chat.timedout"),
|
|
|
|
|
},
|
|
|
|
|
}}
|
|
|
|
|
isTyping={false}
|
|
|
|
|
/>
|
|
|
|
|
) : null}
|
2025-02-19 17:23:49 +08:00
|
|
|
|
2025-02-18 09:40:00 +08:00
|
|
|
<div ref={messagesEndRef} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2025-02-17 16:37:33 +08:00
|
|
|
}
|
2025-02-18 09:40:00 +08:00
|
|
|
)
|
|
|
|
|
);
|
2024-11-20 10:08:08 +08:00
|
|
|
|
2024-11-24 19:25:47 +08:00
|
|
|
export default ChatAI;
|