Files
coco-app/src/components/ChatAI/Chat.tsx
BiggerRain fbfb980fec feat: add connect services (#111)
* feat: add connect services

* chore: adjust auth and userInfo

* chore: add responsiveness to auth and userInfo

* chore: add dark css

* chore: handle /, to join the baseURL and url.

* chore: use http:// or https:// rather http

* chore: handle /, to join the baseURL and url.

* chore: active current service

* chore: connect

* chore: data source & connect data

* feat: add handleKeyDown open settings

* chore: settings name
2025-01-25 16:20:49 +08:00

305 lines
8.4 KiB
TypeScript

import {
useState,
useRef,
useEffect,
forwardRef,
useImperativeHandle,
} from "react";
import { MessageSquarePlus, PanelLeft } from "lucide-react";
import { isTauri } from "@tauri-apps/api/core";
import { ChatMessage } from "./ChatMessage";
import type { Chat, Message } from "./types";
import { tauriFetch } from "../../api/tauriFetchClient";
import { useWebSocket } from "../../hooks/useWebSocket";
import { useChatStore } from "../../stores/chatStore";
import { useWindows } from "../../hooks/useWindows";
import { clientEnv } from "@/utils/env";
import { useAppStore } from '@/stores/appStore';
interface ChatAIProps {
inputValue: string;
isTransitioned: boolean;
changeInput: (val: string) => void;
}
export interface ChatAIRef {
init: () => void;
cancelChat: () => void;
connected: boolean;
reconnect: () => void;
}
const ChatAI = forwardRef<ChatAIRef, ChatAIProps>(
({ inputValue, isTransitioned, changeInput }, ref) => {
useImperativeHandle(ref, () => ({
init: init,
cancelChat: cancelChat,
connected: connected,
reconnect: reconnect
}));
const appStore = useAppStore();
const { createWin } = useWindows();
const { curChatEnd, setCurChatEnd, setConnected } = useChatStore();
const [activeChat, setActiveChat] = useState<Chat>();
const [isTyping, setIsTyping] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const [websocketId, setWebsocketId] = useState("");
const [curMessage, setCurMessage] = useState("");
const [curId, setCurId] = useState("");
const curChatEndRef = useRef(curChatEnd);
curChatEndRef.current = curChatEnd;
const curIdRef = useRef(curId);
curIdRef.current = curId;
console.log("chat useWebSocket", clientEnv.COCO_WEBSOCKET_URL)
const { messages, setMessages, connected, reconnect } = useWebSocket(
`${appStore.endpoint_websocket || clientEnv.COCO_WEBSOCKET_URL}`,
(msg) => {
console.log("msg", msg);
if (msg.includes("websocket-session-id")) {
const array = msg.split(" ");
setWebsocketId(array[2]);
}
if (msg.includes("PRIVATE")) {
if (
msg.includes("assistant finished output") ||
curChatEndRef.current
) {
setCurChatEnd(true);
} else {
const cleanedData = msg.replace(/^PRIVATE /, "");
try {
const chunkData = JSON.parse(cleanedData);
if (chunkData.reply_to_message === curIdRef.current) {
setCurMessage((prev) => prev + chunkData.message_chunk);
return chunkData.message_chunk;
}
} catch (error) {
console.error("JSON Parse error:", error);
}
}
}
}
);
useEffect(()=>{
setConnected(connected)
}, [connected])
// websocket
useEffect(() => {
if (messages.length === 0 || !activeChat?._id) return;
const simulateAssistantResponse = () => {
console.log("messages", messages);
const assistantMessage: Message = {
_id: activeChat._id,
_source: {
type: "assistant",
message: messages,
},
};
const updatedChat = {
...activeChat,
messages: [...(activeChat.messages || []), assistantMessage],
};
setMessages("");
setCurMessage("");
setActiveChat(updatedChat);
setTimeout(() => setIsTyping(false), 1000);
};
if (curChatEnd) {
simulateAssistantResponse();
}
}, [messages, curChatEnd]);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({
behavior: "smooth",
block: "end",
});
};
useEffect(() => {
scrollToBottom();
}, [activeChat?.messages, isTyping, curMessage]);
const createNewChat = async () => {
chatClose();
try {
const response = await tauriFetch({
url: "/chat/_new",
method: "POST",
baseURL: appStore.endpoint_http,
});
console.log("_new", response);
const newChat: Chat = response.data;
setActiveChat(newChat);
handleSendMessage(inputValue, newChat);
} catch (error) {
console.error("Failed to fetch user data:", error);
}
};
const init = () => {
if (!curChatEnd) return;
if (!activeChat?._id) {
createNewChat();
} else {
handleSendMessage(inputValue);
}
};
const handleSendMessage = async (content: string, newChat?: Chat) => {
newChat = newChat || activeChat;
if (!newChat?._id || !content) return;
try {
const response = await tauriFetch({
url: `/chat/${newChat?._id}/_send`,
method: "POST",
headers: {
"WEBSOCKET-SESSION-ID": websocketId,
},
body: JSON.stringify({ message: content }),
baseURL: appStore.endpoint_http,
});
console.log("_send", response, websocketId);
setCurId(response.data[0]?._id);
const updatedChat: Chat = {
...newChat,
messages: [...(newChat?.messages || []), ...(response.data || [])],
};
changeInput("");
setActiveChat(updatedChat);
setIsTyping(true);
setCurChatEnd(false);
} catch (error) {
console.error("Failed to fetch user data:", error);
}
};
const chatClose = async () => {
if (!activeChat?._id) return;
try {
const response = await tauriFetch({
url: `/chat/${activeChat._id}/_close`,
method: "POST",
baseURL: appStore.endpoint_http,
});
console.log("_close", response);
} catch (error) {
console.error("Failed to fetch user data:", error);
}
};
const cancelChat = async () => {
setCurChatEnd(true);
setIsTyping(false);
if (!activeChat?._id) return;
try {
const response = await tauriFetch({
url: `/chat/${activeChat._id}/_cancel`,
method: "POST",
baseURL: appStore.endpoint_http,
});
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: 900,
height: 800,
alwaysOnTop: true,
skipTaskbar: true,
decorations: true,
closable: true,
url: "/ui/chat",
});
}
}
if (!isTransitioned) return null;
return (
<div
data-tauri-drag-region
className={`h-[500px] flex flex-col rounded-xl overflow-hidden`}
>
<header
data-tauri-drag-region
className={`flex items-center justify-between py-2 px-1`}
>
<button
onClick={() => openChatAI()}
className={`p-2 rounded-lg transition-colors text-[#333] dark:text-[#d8d8d8]`}
>
<PanelLeft className="h-4 w-4" />
</button>
<button
onClick={() => {
createNewChat();
}}
className={`p-2 rounded-lg transition-colors text-[#333] dark:text-[#d8d8d8]`}
>
<MessageSquarePlus className="h-4 w-4" />
</button>
</header>
{/* Chat messages */}
<div className="overflow-y-auto border-t border-[rgba(0,0,0,0.1)] dark:border-[rgba(255,255,255,0.15)] custom-scrollbar">
{activeChat?.messages?.map((message, index) => (
<ChatMessage
key={message._id + index}
message={message}
isTyping={
isTyping &&
index === (activeChat.messages?.length || 0) - 1 &&
message._source?.type === "assistant"
}
/>
))}
{!curChatEnd && activeChat?._id ? (
<ChatMessage
key={"last"}
message={{
_id: activeChat?._id,
_source: {
type: "assistant",
message: curMessage,
},
}}
isTyping={!curChatEnd}
/>
) : null}
<div ref={messagesEndRef} />
</div>
</div>
);
}
);
export default ChatAI;