2025-03-24 12:00:11 +08:00
|
|
|
import { useState, useRef, useEffect, useCallback } from "react";
|
2025-03-25 20:57:46 +08:00
|
|
|
import { convertFileSrc } from "@tauri-apps/api/core";
|
2025-03-17 16:24:18 +08:00
|
|
|
import { listen } from "@tauri-apps/api/event";
|
|
|
|
|
import {
|
|
|
|
|
checkScreenRecordingPermission,
|
|
|
|
|
requestScreenRecordingPermission,
|
|
|
|
|
} from "tauri-plugin-macos-permissions-api";
|
|
|
|
|
import {
|
|
|
|
|
getScreenshotableMonitors,
|
|
|
|
|
getScreenshotableWindows,
|
|
|
|
|
getMonitorScreenshot,
|
|
|
|
|
getWindowScreenshot,
|
|
|
|
|
} from "tauri-plugin-screenshots-api";
|
|
|
|
|
import { open } from "@tauri-apps/plugin-dialog";
|
|
|
|
|
import { metadata, icon } from "tauri-plugin-fs-pro-api";
|
2024-11-03 17:02:13 +08:00
|
|
|
|
2025-03-24 12:00:11 +08:00
|
|
|
import ChatAI, { ChatAIRef } from "@/components/Assistant/Chat";
|
2025-04-02 14:03:40 +08:00
|
|
|
import type { Chat as typeChat } from "@/components/Assistant/types";
|
2025-02-26 10:20:53 +08:00
|
|
|
import { useConnectStore } from "@/stores/connectStore";
|
2025-03-04 15:49:04 +08:00
|
|
|
import InputBox from "@/components/Search/InputBox";
|
2025-03-25 20:57:46 +08:00
|
|
|
import {
|
|
|
|
|
chat_history,
|
|
|
|
|
session_chat_history,
|
|
|
|
|
close_session_chat,
|
|
|
|
|
open_session_chat,
|
2025-04-02 14:03:40 +08:00
|
|
|
delete_session_chat,
|
|
|
|
|
update_session_chat,
|
2025-03-25 20:57:46 +08:00
|
|
|
} from "@/commands";
|
2025-04-02 14:03:40 +08:00
|
|
|
import { DataSource } from "@/types/commands";
|
|
|
|
|
import HistoryList from "@/components/Common/HistoryList";
|
2025-04-15 20:13:42 +08:00
|
|
|
import { useSyncStore } from "@/hooks/useSyncStore";
|
2025-04-24 19:00:16 +08:00
|
|
|
import platformAdapter from "@/utils/platformAdapter";
|
2025-03-17 16:24:18 +08:00
|
|
|
|
2025-02-20 15:38:55 +08:00
|
|
|
interface ChatProps {}
|
2024-11-03 17:02:13 +08:00
|
|
|
|
2025-02-20 15:38:55 +08:00
|
|
|
export default function Chat({}: ChatProps) {
|
2025-02-26 10:20:53 +08:00
|
|
|
const currentService = useConnectStore((state) => state.currentService);
|
2025-04-24 19:00:16 +08:00
|
|
|
const currentAssistant = useConnectStore((state) => state.currentAssistant);
|
2025-01-05 09:38:12 +08:00
|
|
|
|
2025-02-20 15:38:55 +08:00
|
|
|
const chatAIRef = useRef<ChatAIRef>(null);
|
2025-01-09 15:46:34 +08:00
|
|
|
|
2025-04-02 14:03:40 +08:00
|
|
|
const [chats, setChats] = useState<typeChat[]>([]);
|
|
|
|
|
const [activeChat, setActiveChat] = useState<typeChat>();
|
2024-11-20 10:08:08 +08:00
|
|
|
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
2025-03-04 15:49:04 +08:00
|
|
|
const isTyping = false;
|
2024-11-03 17:02:13 +08:00
|
|
|
|
2025-03-04 15:49:04 +08:00
|
|
|
const [input, setInput] = useState("");
|
2024-11-27 19:41:54 +08:00
|
|
|
|
2025-02-13 23:01:21 +08:00
|
|
|
const [isSearchActive, setIsSearchActive] = useState(false);
|
2025-02-17 16:37:33 +08:00
|
|
|
const [isDeepThinkActive, setIsDeepThinkActive] = useState(false);
|
2025-04-24 19:00:16 +08:00
|
|
|
const [isMCPActive, setIsMCPActive] = useState(false);
|
2025-04-02 14:03:40 +08:00
|
|
|
const [keyword, setKeyword] = useState("");
|
2025-02-13 23:01:21 +08:00
|
|
|
|
2025-03-25 20:57:46 +08:00
|
|
|
const isChatPage = true;
|
2025-03-04 15:49:04 +08:00
|
|
|
|
2025-04-15 20:13:42 +08:00
|
|
|
useSyncStore();
|
|
|
|
|
|
2024-11-09 11:30:36 +08:00
|
|
|
useEffect(() => {
|
|
|
|
|
getChatHistory();
|
2025-04-02 14:03:40 +08:00
|
|
|
}, [keyword]);
|
2024-11-09 11:30:36 +08:00
|
|
|
|
|
|
|
|
const getChatHistory = async () => {
|
|
|
|
|
try {
|
2025-03-25 20:57:46 +08:00
|
|
|
let response: any = await chat_history({
|
2025-02-26 10:20:53 +08:00
|
|
|
serverId: currentService?.id,
|
2025-02-26 09:27:51 +08:00
|
|
|
from: 0,
|
|
|
|
|
size: 20,
|
2025-04-02 14:03:40 +08:00
|
|
|
query: keyword,
|
2024-11-09 11:30:36 +08:00
|
|
|
});
|
2025-04-18 10:36:00 +08:00
|
|
|
response = response ? JSON.parse(response) : null;
|
2024-11-09 11:30:36 +08:00
|
|
|
console.log("_history", response);
|
2025-02-25 17:34:10 +08:00
|
|
|
const hits = response?.hits?.hits || [];
|
2024-11-09 11:30:36 +08:00
|
|
|
setChats(hits);
|
|
|
|
|
if (hits[0]) {
|
|
|
|
|
onSelectChat(hits[0]);
|
|
|
|
|
} else {
|
2025-02-20 15:38:55 +08:00
|
|
|
chatAIRef.current?.init("");
|
2024-11-09 11:30:36 +08:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2025-03-06 21:47:09 +08:00
|
|
|
console.error("chat_history:", error);
|
2024-11-09 11:30:36 +08:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-04-11 17:33:58 +08:00
|
|
|
const deleteChat = (chatId: string) => {
|
|
|
|
|
handleDelete(chatId);
|
|
|
|
|
|
|
|
|
|
setChats((prev) => prev.filter((chat) => chat._id !== chatId));
|
|
|
|
|
if (activeChat?._id === chatId) {
|
|
|
|
|
const remainingChats = chats.filter((chat) => chat._id !== chatId);
|
|
|
|
|
if (remainingChats.length > 0) {
|
|
|
|
|
setActiveChat(remainingChats[0]);
|
|
|
|
|
} else {
|
|
|
|
|
chatAIRef.current?.init("");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-11-03 17:02:13 +08:00
|
|
|
|
2024-11-09 11:30:36 +08:00
|
|
|
const handleSendMessage = async (content: string) => {
|
2025-03-04 15:49:04 +08:00
|
|
|
setInput(content);
|
2025-03-03 20:51:45 +08:00
|
|
|
chatAIRef.current?.init(content);
|
2024-11-09 11:30:36 +08:00
|
|
|
};
|
2024-11-03 17:02:13 +08:00
|
|
|
|
2025-04-02 14:03:40 +08:00
|
|
|
const chatHistory = async (chat: typeChat) => {
|
2024-11-09 11:30:36 +08:00
|
|
|
try {
|
2025-03-25 20:57:46 +08:00
|
|
|
let response: any = await session_chat_history({
|
2025-02-26 10:20:53 +08:00
|
|
|
serverId: currentService?.id,
|
2025-02-25 17:34:10 +08:00
|
|
|
sessionId: chat?._id,
|
|
|
|
|
from: 0,
|
|
|
|
|
size: 20,
|
2024-11-09 11:30:36 +08:00
|
|
|
});
|
2025-04-18 10:36:00 +08:00
|
|
|
response = response ? JSON.parse(response) : null;
|
2024-11-09 11:30:36 +08:00
|
|
|
console.log("id_history", response);
|
2025-02-25 17:34:10 +08:00
|
|
|
const hits = response?.hits?.hits || [];
|
2025-04-02 14:03:40 +08:00
|
|
|
const updatedChat: typeChat = {
|
2024-11-09 11:30:36 +08:00
|
|
|
...chat,
|
|
|
|
|
messages: hits,
|
2024-11-03 17:02:13 +08:00
|
|
|
};
|
2024-11-09 11:30:36 +08:00
|
|
|
setActiveChat(updatedChat);
|
|
|
|
|
} catch (error) {
|
2025-03-06 21:47:09 +08:00
|
|
|
console.error("session_chat_history:", error);
|
2024-11-09 11:30:36 +08:00
|
|
|
}
|
|
|
|
|
};
|
2024-11-03 17:02:13 +08:00
|
|
|
|
2024-11-09 11:30:36 +08:00
|
|
|
const chatClose = async () => {
|
|
|
|
|
if (!activeChat?._id) return;
|
|
|
|
|
try {
|
2025-03-25 20:57:46 +08:00
|
|
|
let response: any = await close_session_chat({
|
2025-02-26 10:20:53 +08:00
|
|
|
serverId: currentService?.id,
|
2025-02-25 17:34:10 +08:00
|
|
|
sessionId: activeChat?._id,
|
2024-11-09 11:30:36 +08:00
|
|
|
});
|
2025-04-18 10:36:00 +08:00
|
|
|
response = response ? JSON.parse(response) : null;
|
2024-11-09 11:30:36 +08:00
|
|
|
console.log("_close", response);
|
|
|
|
|
} catch (error) {
|
2025-03-06 21:47:09 +08:00
|
|
|
console.error("close_session_chat:", error);
|
2024-11-09 11:30:36 +08:00
|
|
|
}
|
|
|
|
|
};
|
2024-11-03 17:02:13 +08:00
|
|
|
|
2024-11-09 11:30:36 +08:00
|
|
|
const onSelectChat = async (chat: any) => {
|
|
|
|
|
chatClose();
|
|
|
|
|
try {
|
2025-03-25 20:57:46 +08:00
|
|
|
let response: any = await open_session_chat({
|
2025-02-26 10:20:53 +08:00
|
|
|
serverId: currentService?.id,
|
2025-02-25 17:34:10 +08:00
|
|
|
sessionId: chat?._id,
|
2024-11-09 11:30:36 +08:00
|
|
|
});
|
2025-04-18 10:36:00 +08:00
|
|
|
response = response ? JSON.parse(response) : null;
|
2024-11-09 11:30:36 +08:00
|
|
|
console.log("_open", response);
|
2025-02-25 17:34:10 +08:00
|
|
|
chatHistory(response);
|
2024-11-20 10:08:08 +08:00
|
|
|
} catch (error) {
|
2025-03-06 21:47:09 +08:00
|
|
|
console.error("open_session_chat:", error);
|
2024-11-20 10:08:08 +08:00
|
|
|
}
|
|
|
|
|
};
|
2024-11-24 19:25:47 +08:00
|
|
|
|
2024-11-20 10:08:08 +08:00
|
|
|
const cancelChat = async () => {
|
2025-03-04 15:49:04 +08:00
|
|
|
chatAIRef.current?.cancelChat();
|
2024-11-03 17:02:13 +08:00
|
|
|
};
|
|
|
|
|
|
2025-02-26 09:27:51 +08:00
|
|
|
const clearChat = () => {
|
|
|
|
|
chatClose();
|
|
|
|
|
setActiveChat(undefined);
|
2025-02-26 10:20:53 +08:00
|
|
|
};
|
2025-02-26 09:27:51 +08:00
|
|
|
|
2025-03-04 15:49:04 +08:00
|
|
|
const reconnect = () => {
|
|
|
|
|
chatAIRef.current?.reconnect();
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-17 16:24:18 +08:00
|
|
|
const getFileUrl = useCallback((path: string) => {
|
|
|
|
|
return convertFileSrc(path);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const setupWindowFocusListener = useCallback(async (callback: () => void) => {
|
|
|
|
|
return listen("tauri://focus", callback);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const checkScreenPermission = useCallback(async () => {
|
|
|
|
|
return checkScreenRecordingPermission();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const requestScreenPermission = useCallback(() => {
|
|
|
|
|
return requestScreenRecordingPermission();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const getScreenMonitors = useCallback(async () => {
|
|
|
|
|
return getScreenshotableMonitors();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const getScreenWindows = useCallback(async () => {
|
|
|
|
|
return getScreenshotableWindows();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const captureMonitorScreenshot = useCallback(async (id: number) => {
|
|
|
|
|
return getMonitorScreenshot(id);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const captureWindowScreenshot = useCallback(async (id: number) => {
|
|
|
|
|
return getWindowScreenshot(id);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const openFileDialog = useCallback(async (options: { multiple: boolean }) => {
|
|
|
|
|
return open(options);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const getFileMetadata = useCallback(async (path: string) => {
|
|
|
|
|
return metadata(path);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const getFileIcon = useCallback(async (path: string, size: number) => {
|
2025-04-17 16:15:05 +08:00
|
|
|
return icon(path, { size });
|
2025-03-17 16:24:18 +08:00
|
|
|
}, []);
|
|
|
|
|
|
2025-04-02 14:03:40 +08:00
|
|
|
const handleSearch = (keyword: string) => {
|
|
|
|
|
setKeyword(keyword);
|
|
|
|
|
};
|
|
|
|
|
|
2025-04-14 22:09:13 +08:00
|
|
|
const handleRename = (chatId: string, title: string) => {
|
2025-04-02 14:03:40 +08:00
|
|
|
if (!currentService?.id) return;
|
|
|
|
|
|
2025-04-14 22:09:13 +08:00
|
|
|
setChats((prev) => {
|
|
|
|
|
const updatedChats = prev.map((item) => {
|
|
|
|
|
if (item._id !== chatId) return item;
|
|
|
|
|
|
|
|
|
|
return { ...item, _source: { ...item._source, title } };
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const modifiedChat = updatedChats.find((item) => {
|
|
|
|
|
return item._id === chatId;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!modifiedChat) {
|
|
|
|
|
return updatedChats;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
modifiedChat,
|
|
|
|
|
...updatedChats.filter((item) => item._id !== chatId),
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (activeChat?._id === chatId) {
|
|
|
|
|
setActiveChat((prev) => {
|
|
|
|
|
if (!prev) return prev;
|
|
|
|
|
|
|
|
|
|
return { ...prev, _source: { ...prev._source, title } };
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
update_session_chat({
|
2025-04-02 14:03:40 +08:00
|
|
|
serverId: currentService.id,
|
2025-04-14 22:09:13 +08:00
|
|
|
sessionId: chatId,
|
2025-04-02 14:03:40 +08:00
|
|
|
title,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDelete = async (id: string) => {
|
|
|
|
|
if (!currentService?.id) return;
|
|
|
|
|
|
|
|
|
|
await delete_session_chat(currentService.id, id);
|
|
|
|
|
};
|
|
|
|
|
|
2025-04-28 09:52:39 +08:00
|
|
|
const getDataSourcesByServer = useCallback(
|
2025-04-24 19:00:16 +08:00
|
|
|
async (
|
|
|
|
|
serverId: string,
|
|
|
|
|
options?: {
|
|
|
|
|
from?: number;
|
|
|
|
|
size?: number;
|
|
|
|
|
query?: string;
|
|
|
|
|
}
|
|
|
|
|
): Promise<DataSource[]> => {
|
|
|
|
|
let response: any;
|
2025-04-28 09:52:39 +08:00
|
|
|
response = await platformAdapter.invokeBackend("datasource_search", {
|
2025-04-24 19:00:16 +08:00
|
|
|
id: serverId,
|
|
|
|
|
options,
|
|
|
|
|
});
|
|
|
|
|
let ids = currentAssistant?._source?.datasource?.ids;
|
|
|
|
|
if (Array.isArray(ids) && ids.length > 0 && !ids.includes("*")) {
|
|
|
|
|
response = response?.filter((item: any) => ids.includes(item.id));
|
|
|
|
|
}
|
2025-04-28 09:52:39 +08:00
|
|
|
return response || [];
|
|
|
|
|
},
|
|
|
|
|
[JSON.stringify(currentAssistant)]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const getMCPByServer = useCallback(
|
|
|
|
|
async (
|
|
|
|
|
serverId: string,
|
|
|
|
|
options?: {
|
|
|
|
|
from?: number;
|
|
|
|
|
size?: number;
|
|
|
|
|
query?: string;
|
|
|
|
|
}
|
|
|
|
|
): Promise<DataSource[]> => {
|
|
|
|
|
let response: any;
|
|
|
|
|
response = await platformAdapter.invokeBackend("mcp_server_search", {
|
|
|
|
|
id: serverId,
|
|
|
|
|
options,
|
|
|
|
|
});
|
|
|
|
|
let ids = currentAssistant?._source?.mcp_servers?.ids;
|
|
|
|
|
if (Array.isArray(ids) && ids.length > 0 && !ids.includes("*")) {
|
|
|
|
|
response = response?.filter((item: any) => ids.includes(item.id));
|
|
|
|
|
}
|
2025-04-24 19:00:16 +08:00
|
|
|
return response || [];
|
|
|
|
|
},
|
|
|
|
|
[JSON.stringify(currentAssistant)]
|
|
|
|
|
);
|
|
|
|
|
|
2024-11-03 17:02:13 +08:00
|
|
|
return (
|
2024-11-27 19:41:54 +08:00
|
|
|
<div className="h-screen">
|
2025-04-11 10:33:40 +08:00
|
|
|
<div className="h-full flex">
|
2024-11-03 17:02:13 +08:00
|
|
|
{/* Sidebar */}
|
2024-11-12 09:44:49 +08:00
|
|
|
{isSidebarOpen ? (
|
|
|
|
|
<div
|
|
|
|
|
className={`fixed inset-y-0 left-0 z-50 w-64 transform ${
|
|
|
|
|
isSidebarOpen ? "translate-x-0" : "-translate-x-full"
|
2024-11-27 19:41:54 +08:00
|
|
|
} transition-transform duration-300 ease-in-out md:translate-x-0 md:static md:block bg-gray-100 dark:bg-gray-800`}
|
2024-11-12 09:44:49 +08:00
|
|
|
>
|
2025-04-02 14:03:40 +08:00
|
|
|
<HistoryList
|
|
|
|
|
list={chats}
|
|
|
|
|
active={activeChat}
|
|
|
|
|
onSearch={handleSearch}
|
|
|
|
|
onRefresh={getChatHistory}
|
|
|
|
|
onSelect={onSelectChat}
|
|
|
|
|
onRename={handleRename}
|
2025-04-11 17:33:58 +08:00
|
|
|
onRemove={deleteChat}
|
2025-02-26 09:27:51 +08:00
|
|
|
/>
|
2024-11-12 09:44:49 +08:00
|
|
|
</div>
|
|
|
|
|
) : null}
|
2024-11-03 17:02:13 +08:00
|
|
|
|
|
|
|
|
{/* Main content */}
|
2024-11-27 19:41:54 +08:00
|
|
|
<div className={`flex-1 flex flex-col bg-white dark:bg-gray-900`}>
|
2024-11-03 17:02:13 +08:00
|
|
|
{/* Chat messages */}
|
2025-04-11 10:33:40 +08:00
|
|
|
<div className="flex-1 overflow-auto">
|
|
|
|
|
<ChatAI
|
|
|
|
|
ref={chatAIRef}
|
|
|
|
|
key="ChatAI"
|
|
|
|
|
activeChatProp={activeChat}
|
|
|
|
|
isSearchActive={isSearchActive}
|
|
|
|
|
isDeepThinkActive={isDeepThinkActive}
|
|
|
|
|
setIsSidebarOpen={setIsSidebarOpen}
|
|
|
|
|
isSidebarOpen={isSidebarOpen}
|
|
|
|
|
clearChatPage={clearChat}
|
|
|
|
|
isChatPage={isChatPage}
|
|
|
|
|
getFileUrl={getFileUrl}
|
|
|
|
|
changeInput={setInput}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2024-11-03 17:02:13 +08:00
|
|
|
|
|
|
|
|
{/* Input area */}
|
2025-03-25 20:57:46 +08:00
|
|
|
<div
|
|
|
|
|
className={`border-t p-4 pb-0 border-gray-200 dark:border-gray-800`}
|
|
|
|
|
>
|
2025-03-04 15:49:04 +08:00
|
|
|
<InputBox
|
|
|
|
|
isChatMode={true}
|
|
|
|
|
inputValue={input}
|
2024-11-09 11:30:36 +08:00
|
|
|
onSend={handleSendMessage}
|
2025-03-04 15:49:04 +08:00
|
|
|
changeInput={setInput}
|
2024-11-09 11:30:36 +08:00
|
|
|
disabled={isTyping}
|
2025-03-04 15:49:04 +08:00
|
|
|
disabledChange={cancelChat}
|
|
|
|
|
reconnect={reconnect}
|
2025-02-13 23:01:21 +08:00
|
|
|
isSearchActive={isSearchActive}
|
|
|
|
|
setIsSearchActive={() => setIsSearchActive((prev) => !prev)}
|
2025-02-17 16:37:33 +08:00
|
|
|
isDeepThinkActive={isDeepThinkActive}
|
|
|
|
|
setIsDeepThinkActive={() => setIsDeepThinkActive((prev) => !prev)}
|
2025-04-24 19:00:16 +08:00
|
|
|
isMCPActive={isMCPActive}
|
|
|
|
|
setIsMCPActive={() => setIsMCPActive((prev) => !prev)}
|
2025-03-04 15:49:04 +08:00
|
|
|
isChatPage={isChatPage}
|
2025-03-17 16:24:18 +08:00
|
|
|
getDataSourcesByServer={getDataSourcesByServer}
|
|
|
|
|
setupWindowFocusListener={setupWindowFocusListener}
|
|
|
|
|
checkScreenPermission={checkScreenPermission}
|
|
|
|
|
requestScreenPermission={requestScreenPermission}
|
|
|
|
|
getScreenMonitors={getScreenMonitors}
|
|
|
|
|
getScreenWindows={getScreenWindows}
|
|
|
|
|
captureMonitorScreenshot={captureMonitorScreenshot}
|
|
|
|
|
captureWindowScreenshot={captureWindowScreenshot}
|
|
|
|
|
openFileDialog={openFileDialog}
|
|
|
|
|
getFileMetadata={getFileMetadata}
|
|
|
|
|
getFileIcon={getFileIcon}
|
2025-04-24 19:00:16 +08:00
|
|
|
getMCPByServer={getMCPByServer}
|
2024-11-09 11:30:36 +08:00
|
|
|
/>
|
2024-11-24 19:25:47 +08:00
|
|
|
</div>
|
2024-11-03 17:02:13 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2024-11-24 19:25:47 +08:00
|
|
|
</div>
|
2024-11-03 17:02:13 +08:00
|
|
|
);
|
|
|
|
|
}
|