2025-04-20 21:27:25 +08:00
|
|
|
import { useEffect, useState, useRef, useCallback } from "react";
|
|
|
|
|
import { ChevronDownIcon, RefreshCw, Check } from "lucide-react";
|
|
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
|
|
|
|
|
|
import { useAppStore } from "@/stores/appStore";
|
|
|
|
|
import logoImg from "@/assets/icon.svg";
|
|
|
|
|
import platformAdapter from "@/utils/platformAdapter";
|
|
|
|
|
import { useClickAway } from "@/hooks/useClickAway";
|
|
|
|
|
import VisibleKey from "@/components/Common/VisibleKey";
|
|
|
|
|
import { useConnectStore } from "@/stores/connectStore";
|
|
|
|
|
import FontIcon from "@/components/Common/Icons/FontIcon";
|
2025-04-23 00:12:22 +08:00
|
|
|
import { useChatStore } from "@/stores/chatStore";
|
2025-04-22 18:36:59 +08:00
|
|
|
import { AI_ASSISTANT_PANEL_ID } from "@/constants";
|
|
|
|
|
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
2025-04-20 21:27:25 +08:00
|
|
|
|
|
|
|
|
interface AssistantListProps {
|
|
|
|
|
showChatHistory?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function AssistantList({ showChatHistory = true }: AssistantListProps) {
|
|
|
|
|
const { t } = useTranslation();
|
2025-04-23 00:12:22 +08:00
|
|
|
const { connected } = useChatStore();
|
2025-04-20 21:27:25 +08:00
|
|
|
const isTauri = useAppStore((state) => state.isTauri);
|
|
|
|
|
const currentService = useConnectStore((state) => state.currentService);
|
|
|
|
|
const currentAssistant = useConnectStore((state) => state.currentAssistant);
|
2025-04-22 18:36:59 +08:00
|
|
|
const setCurrentAssistant = useConnectStore(
|
|
|
|
|
(state) => state.setCurrentAssistant
|
|
|
|
|
);
|
2025-04-23 00:12:22 +08:00
|
|
|
const aiAssistant = useShortcutsStore((state) => state.aiAssistant);
|
2025-04-20 21:27:25 +08:00
|
|
|
|
|
|
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
|
|
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
|
|
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
|
|
useClickAway(menuRef, () => setIsOpen(false));
|
|
|
|
|
const [assistants, setAssistants] = useState<any[]>([]);
|
|
|
|
|
|
2025-04-23 00:12:22 +08:00
|
|
|
const fetchAssistant = useCallback(async (serverId: string) => {
|
2025-04-20 21:27:25 +08:00
|
|
|
if (!isTauri) return;
|
2025-04-23 00:12:22 +08:00
|
|
|
if (!serverId) return;
|
2025-04-20 21:27:25 +08:00
|
|
|
platformAdapter
|
|
|
|
|
.commands("assistant_search", {
|
2025-04-23 00:12:22 +08:00
|
|
|
serverId,
|
2025-04-20 21:27:25 +08:00
|
|
|
})
|
|
|
|
|
.then((res: any) => {
|
|
|
|
|
res = res ? JSON.parse(res) : null;
|
|
|
|
|
console.log("assistant_search", res);
|
|
|
|
|
const assistantList = res?.hits?.hits || [];
|
|
|
|
|
setAssistants(assistantList);
|
2025-04-23 00:12:22 +08:00
|
|
|
if (assistantList.length > 0) {
|
|
|
|
|
const assistant = assistantList.find(
|
|
|
|
|
(item: any) => item._id === currentAssistant?._id
|
|
|
|
|
);
|
|
|
|
|
if (assistant) {
|
|
|
|
|
setCurrentAssistant(assistant);
|
|
|
|
|
} else {
|
|
|
|
|
setCurrentAssistant(assistantList[0]);
|
|
|
|
|
}
|
2025-04-20 21:27:25 +08:00
|
|
|
}
|
2025-04-23 00:12:22 +08:00
|
|
|
})
|
|
|
|
|
.catch((err: any) => {
|
|
|
|
|
setAssistants([]);
|
|
|
|
|
setCurrentAssistant(null);
|
|
|
|
|
console.log("assistant_search", err);
|
2025-04-20 21:27:25 +08:00
|
|
|
});
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2025-04-23 00:12:22 +08:00
|
|
|
connected && fetchAssistant(currentService?.id);
|
|
|
|
|
}, [connected, currentService?.id]);
|
2025-04-20 21:27:25 +08:00
|
|
|
|
2025-04-23 00:12:22 +08:00
|
|
|
const handleRefresh = useCallback(async () => {
|
2025-04-20 21:27:25 +08:00
|
|
|
setIsRefreshing(true);
|
2025-04-23 00:12:22 +08:00
|
|
|
await fetchAssistant(currentService?.id);
|
2025-04-20 21:27:25 +08:00
|
|
|
setTimeout(() => setIsRefreshing(false), 1000);
|
2025-04-23 00:12:22 +08:00
|
|
|
}, [currentService?.id]);
|
2025-04-20 21:27:25 +08:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="relative" ref={menuRef}>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
|
|
|
className="h-6 p-1 px-1.5 flex items-center gap-1 rounded-full bg-white dark:bg-[#202126] text-sm/6 font-semibold text-gray-800 dark:text-[#d8d8d8] border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none"
|
|
|
|
|
>
|
2025-04-23 00:12:22 +08:00
|
|
|
<div className="w-4 h-4 flex justify-center items-center rounded-full bg-white">
|
2025-04-20 21:27:25 +08:00
|
|
|
{currentAssistant?._source?.icon?.startsWith("font_") ? (
|
|
|
|
|
<FontIcon
|
|
|
|
|
name={currentAssistant._source.icon}
|
|
|
|
|
className="w-3 h-3"
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<img
|
|
|
|
|
src={logoImg}
|
|
|
|
|
className="w-3 h-3"
|
|
|
|
|
alt={t("assistant.message.logo")}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="max-w-[100px] truncate">
|
|
|
|
|
{currentAssistant?._source?.name || "Coco AI"}
|
|
|
|
|
</div>
|
|
|
|
|
{showChatHistory && isTauri && (
|
2025-04-22 18:36:59 +08:00
|
|
|
<VisibleKey
|
|
|
|
|
aria-controls={isOpen ? AI_ASSISTANT_PANEL_ID : ""}
|
|
|
|
|
shortcut={aiAssistant}
|
|
|
|
|
onKeyPress={() => {
|
|
|
|
|
setIsOpen(!isOpen);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<ChevronDownIcon
|
|
|
|
|
className={`size-4 text-gray-500 dark:text-gray-400 transition-transform ${
|
|
|
|
|
isOpen ? "rotate-180" : ""
|
|
|
|
|
}`}
|
|
|
|
|
/>
|
|
|
|
|
</VisibleKey>
|
2025-04-20 21:27:25 +08:00
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{showChatHistory && isTauri && isOpen && (
|
2025-04-22 18:36:59 +08:00
|
|
|
<div
|
|
|
|
|
id={isOpen ? AI_ASSISTANT_PANEL_ID : ""}
|
|
|
|
|
className="absolute z-50 top-full mt-1 left-0 w-64 rounded-xl bg-white dark:bg-[#202126] p-2 text-sm/6 text-gray-800 dark:text-white shadow-lg border border-gray-200 dark:border-gray-700 focus:outline-none max-h-[calc(100vh-80px)] overflow-y-auto"
|
|
|
|
|
>
|
2025-04-20 21:27:25 +08:00
|
|
|
<div className="sticky top-0 mb-2 px-2 py-1 text-sm font-medium text-gray-900 dark:text-white bg-white dark:bg-[#202126] flex justify-between">
|
|
|
|
|
<div>AI Assistant</div>
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleRefresh}
|
|
|
|
|
className="p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 dark:text-gray-400"
|
|
|
|
|
disabled={isRefreshing}
|
|
|
|
|
>
|
|
|
|
|
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
|
|
|
|
<RefreshCw
|
|
|
|
|
className={`h-4 w-4 text-[#0287FF] transition-transform duration-1000 ${
|
|
|
|
|
isRefreshing ? "animate-spin" : ""
|
|
|
|
|
}`}
|
|
|
|
|
/>
|
|
|
|
|
</VisibleKey>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
{assistants.map((assistant) => (
|
|
|
|
|
<button
|
|
|
|
|
key={assistant._id}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setCurrentAssistant(assistant);
|
|
|
|
|
setIsOpen(false);
|
|
|
|
|
}}
|
|
|
|
|
className={`w-full flex items-center gap-2 rounded-lg p-1 py-1.5 mb-1 ${
|
|
|
|
|
currentAssistant?._id === assistant._id
|
|
|
|
|
? "bg-[#F3F4F6] dark:bg-[#1F2937]"
|
|
|
|
|
: "hover:bg-[#F3F4F6] dark:hover:bg-[#1F2937]"
|
|
|
|
|
}
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{assistant._source?.icon?.startsWith("font_") ? (
|
2025-04-23 00:12:22 +08:00
|
|
|
<div className="w-7 h-7 flex items-center justify-center rounded-full bg-white">
|
|
|
|
|
<FontIcon
|
|
|
|
|
name={assistant._source?.icon}
|
|
|
|
|
className="w-5 h-5"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2025-04-20 21:27:25 +08:00
|
|
|
) : (
|
|
|
|
|
<img
|
|
|
|
|
src={logoImg}
|
2025-04-23 00:12:22 +08:00
|
|
|
className="w-5 h-5 rounded-full"
|
2025-04-20 21:27:25 +08:00
|
|
|
alt={assistant.name}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
<div className="text-left flex-1 min-w-0">
|
|
|
|
|
<div className="font-medium text-gray-900 dark:text-white truncate">
|
|
|
|
|
{assistant._source?.name || "-"}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">
|
|
|
|
|
{assistant._source?.description || ""}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{currentAssistant?._id === assistant._id && (
|
|
|
|
|
<div className="flex items-center">
|
|
|
|
|
<VisibleKey
|
|
|
|
|
shortcut="↓↑"
|
|
|
|
|
shortcutClassName="w-6 -translate-x-4"
|
|
|
|
|
>
|
|
|
|
|
<Check className="w-4 h-4 text-gray-500 dark:text-gray-400" />
|
|
|
|
|
</VisibleKey>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|