2025-05-30 17:18:52 +08:00
|
|
|
|
import { useState, useRef, useCallback, useEffect } from "react";
|
2025-05-26 15:44:05 +08:00
|
|
|
|
import { ChevronDownIcon, RefreshCw } from "lucide-react";
|
2025-04-20 21:27:25 +08:00
|
|
|
|
import { useTranslation } from "react-i18next";
|
2025-05-21 09:04:57 +08:00
|
|
|
|
import { isNil } from "lodash-es";
|
|
|
|
|
|
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
2025-05-26 15:44:05 +08:00
|
|
|
|
import { useDebounce, useKeyPress, usePagination } from "ahooks";
|
2025-05-21 09:04:57 +08:00
|
|
|
|
import clsx from "clsx";
|
2025-04-20 21:27:25 +08:00
|
|
|
|
|
|
|
|
|
|
import logoImg from "@/assets/icon.svg";
|
|
|
|
|
|
import VisibleKey from "@/components/Common/VisibleKey";
|
|
|
|
|
|
import { useConnectStore } from "@/stores/connectStore";
|
|
|
|
|
|
import FontIcon from "@/components/Common/Icons/FontIcon";
|
2025-04-22 18:36:59 +08:00
|
|
|
|
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
2025-05-21 11:34:03 +08:00
|
|
|
|
import NoDataImage from "@/components/Common/NoDataImage";
|
|
|
|
|
|
import PopoverInput from "@/components/Common/PopoverInput";
|
|
|
|
|
|
import { AssistantFetcher } from "./AssistantFetcher";
|
2025-05-26 15:44:05 +08:00
|
|
|
|
import AssistantItem from "./AssistantItem";
|
|
|
|
|
|
import Pagination from "@/components/Common/Pagination";
|
2025-05-30 17:18:52 +08:00
|
|
|
|
import { useSearchStore } from "@/stores/searchStore";
|
2025-06-11 16:28:53 +08:00
|
|
|
|
import { useChatStore } from "@/stores/chatStore";
|
2025-04-20 21:27:25 +08:00
|
|
|
|
|
|
|
|
|
|
interface AssistantListProps {
|
2025-04-23 18:23:40 +08:00
|
|
|
|
assistantIDs?: string[];
|
2025-04-20 21:27:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-23 18:23:40 +08:00
|
|
|
|
export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
2025-04-20 21:27:25 +08:00
|
|
|
|
const { t } = useTranslation();
|
2025-05-21 09:04:57 +08:00
|
|
|
|
|
2025-04-20 21:27:25 +08:00
|
|
|
|
const currentService = useConnectStore((state) => state.currentService);
|
|
|
|
|
|
const currentAssistant = useConnectStore((state) => state.currentAssistant);
|
2025-04-24 16:03:34 +08:00
|
|
|
|
const setCurrentAssistant = useConnectStore((state) => {
|
|
|
|
|
|
return state.setCurrentAssistant;
|
|
|
|
|
|
});
|
2025-04-23 00:12:22 +08:00
|
|
|
|
const aiAssistant = useShortcutsStore((state) => state.aiAssistant);
|
2025-04-24 16:03:34 +08:00
|
|
|
|
const [assistants, setAssistants] = useState<any[]>([]);
|
2025-04-20 21:27:25 +08:00
|
|
|
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
2025-04-29 15:16:46 +08:00
|
|
|
|
const popoverRef = useRef<HTMLDivElement>(null);
|
2025-04-24 16:03:34 +08:00
|
|
|
|
const popoverButtonRef = useRef<HTMLButtonElement>(null);
|
|
|
|
|
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
|
const [keyword, setKeyword] = useState("");
|
|
|
|
|
|
const debounceKeyword = useDebounce(keyword, { wait: 500 });
|
2025-05-30 17:18:52 +08:00
|
|
|
|
const askAiAssistantId = useSearchStore((state) => state.askAiAssistantId);
|
|
|
|
|
|
const setAskAiAssistantId = useSearchStore((state) => {
|
|
|
|
|
|
return state.setAskAiAssistantId;
|
|
|
|
|
|
});
|
|
|
|
|
|
const assistantList = useConnectStore((state) => state.assistantList);
|
2025-06-11 16:28:53 +08:00
|
|
|
|
const connected = useChatStore((state) => {
|
|
|
|
|
|
return state.connected;
|
|
|
|
|
|
});
|
2025-04-24 16:03:34 +08:00
|
|
|
|
|
2025-05-21 11:34:03 +08:00
|
|
|
|
const { fetchAssistant } = AssistantFetcher({
|
|
|
|
|
|
debounceKeyword,
|
|
|
|
|
|
assistantIDs,
|
|
|
|
|
|
});
|
2025-04-24 16:03:34 +08:00
|
|
|
|
|
2025-06-11 16:28:53 +08:00
|
|
|
|
const getAssistants = (params: { current: number; pageSize: number }) => {
|
|
|
|
|
|
if (!connected) {
|
|
|
|
|
|
return Promise.resolve({
|
|
|
|
|
|
total: 0,
|
|
|
|
|
|
list: [],
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return fetchAssistant(params);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const { pagination, runAsync } = usePagination(getAssistants, {
|
2025-04-24 16:03:34 +08:00
|
|
|
|
defaultPageSize: 5,
|
2025-06-11 16:28:53 +08:00
|
|
|
|
refreshDeps: [
|
|
|
|
|
|
currentService?.id,
|
|
|
|
|
|
debounceKeyword,
|
|
|
|
|
|
currentService?.enabled,
|
|
|
|
|
|
connected,
|
|
|
|
|
|
],
|
2025-04-24 16:03:34 +08:00
|
|
|
|
onSuccess(data) {
|
|
|
|
|
|
setAssistants(data.list);
|
2025-06-09 11:54:44 +08:00
|
|
|
|
|
|
|
|
|
|
if (data.list.length === 0) {
|
|
|
|
|
|
setCurrentAssistant(void 0);
|
|
|
|
|
|
}
|
2025-04-24 16:03:34 +08:00
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const handleRefresh = async () => {
|
2025-04-20 21:27:25 +08:00
|
|
|
|
setIsRefreshing(true);
|
2025-04-24 16:03:34 +08:00
|
|
|
|
|
|
|
|
|
|
await runAsync({ current: 1, pageSize: 5 });
|
|
|
|
|
|
|
2025-04-20 21:27:25 +08:00
|
|
|
|
setTimeout(() => setIsRefreshing(false), 1000);
|
2025-04-24 16:03:34 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-05-26 15:44:05 +08:00
|
|
|
|
const [highlightIndex, setHighlightIndex] = useState<number>(-1);
|
|
|
|
|
|
const [isKeyboardActive, setIsKeyboardActive] = useState(false);
|
|
|
|
|
|
|
2025-05-30 17:18:52 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!askAiAssistantId || assistantList.length === 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
const matched = assistantList.find((item) => {
|
|
|
|
|
|
return item._id === askAiAssistantId;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!matched) return;
|
|
|
|
|
|
|
|
|
|
|
|
setCurrentAssistant(matched);
|
|
|
|
|
|
setAskAiAssistantId(void 0);
|
|
|
|
|
|
}, [assistantList, askAiAssistantId]);
|
|
|
|
|
|
|
2025-04-29 15:16:46 +08:00
|
|
|
|
useKeyPress(
|
|
|
|
|
|
["uparrow", "downarrow", "enter"],
|
|
|
|
|
|
(event, key) => {
|
|
|
|
|
|
const isClose = isNil(popoverButtonRef.current?.dataset["open"]);
|
2025-04-24 16:03:34 +08:00
|
|
|
|
|
2025-04-29 15:16:46 +08:00
|
|
|
|
if (isClose) return;
|
2025-04-25 14:19:15 +08:00
|
|
|
|
|
2025-04-29 15:16:46 +08:00
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
event.preventDefault();
|
2025-04-24 16:03:34 +08:00
|
|
|
|
|
2025-05-26 15:44:05 +08:00
|
|
|
|
setIsKeyboardActive(true);
|
2025-04-24 16:03:34 +08:00
|
|
|
|
|
2025-04-29 15:16:46 +08:00
|
|
|
|
const index = assistants.findIndex(
|
|
|
|
|
|
(item) => item._id === currentAssistant?._id
|
|
|
|
|
|
);
|
|
|
|
|
|
const length = assistants.length;
|
|
|
|
|
|
|
|
|
|
|
|
if (length <= 1) return;
|
|
|
|
|
|
|
2025-05-26 15:44:05 +08:00
|
|
|
|
let nextIndex = highlightIndex === -1 ? index : highlightIndex;
|
2025-04-29 15:16:46 +08:00
|
|
|
|
|
|
|
|
|
|
if (key === "uparrow") {
|
2025-05-26 15:44:05 +08:00
|
|
|
|
nextIndex = nextIndex > 0 ? nextIndex - 1 : length - 1;
|
|
|
|
|
|
} else if (key === "downarrow") {
|
|
|
|
|
|
nextIndex = nextIndex < length - 1 ? nextIndex + 1 : 0;
|
2025-04-29 15:16:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-26 15:44:05 +08:00
|
|
|
|
if (key === "enter") {
|
|
|
|
|
|
setCurrentAssistant(assistants[nextIndex]);
|
|
|
|
|
|
return popoverButtonRef.current?.click();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setHighlightIndex(nextIndex);
|
2025-04-29 15:16:46 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
target: popoverRef,
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
2025-04-24 16:03:34 +08:00
|
|
|
|
|
|
|
|
|
|
const handlePrev = useCallback(() => {
|
|
|
|
|
|
if (pagination.current <= 1) return;
|
|
|
|
|
|
|
|
|
|
|
|
pagination.changeCurrent(pagination.current - 1);
|
|
|
|
|
|
}, [pagination]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleNext = useCallback(() => {
|
|
|
|
|
|
if (pagination.current >= pagination.totalPage) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pagination.changeCurrent(pagination.current + 1);
|
|
|
|
|
|
}, [pagination]);
|
2025-04-20 21:27:25 +08:00
|
|
|
|
|
2025-05-26 15:44:05 +08:00
|
|
|
|
const handleMouseMove = useCallback(() => {
|
|
|
|
|
|
setHighlightIndex(-1);
|
|
|
|
|
|
setIsKeyboardActive(false);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2025-04-20 21:27:25 +08:00
|
|
|
|
return (
|
2025-04-24 16:03:34 +08:00
|
|
|
|
<div className="relative">
|
2025-04-29 15:16:46 +08:00
|
|
|
|
<Popover ref={popoverRef}>
|
2025-04-24 16:03:34 +08:00
|
|
|
|
<PopoverButton
|
|
|
|
|
|
ref={popoverButtonRef}
|
|
|
|
|
|
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-22 18:36:59 +08:00
|
|
|
|
>
|
2025-04-25 14:54:32 +08:00
|
|
|
|
<div className="w-4 h-4 flex justify-center items-center rounded-full bg-white border border-[#E6E6E6]">
|
2025-04-24 16:03:34 +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>
|
|
|
|
|
|
<VisibleKey
|
|
|
|
|
|
shortcut={aiAssistant}
|
|
|
|
|
|
onKeyPress={() => {
|
|
|
|
|
|
popoverButtonRef.current?.click();
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<ChevronDownIcon className="size-4 text-gray-500 dark:text-gray-400 transition-transform" />
|
|
|
|
|
|
</VisibleKey>
|
|
|
|
|
|
</PopoverButton>
|
|
|
|
|
|
|
2025-05-26 15:44:05 +08:00
|
|
|
|
<PopoverPanel
|
|
|
|
|
|
className="absolute z-50 top-full mt-1 left-0 w-60 rounded-xl bg-white dark:bg-[#202126] p-3 text-sm/6 text-[#333] dark:text-[#D8D8D8] shadow-lg border dark:border-white/10 focus:outline-none max-h-[calc(100vh-80px)] overflow-y-auto"
|
|
|
|
|
|
onMouseMove={handleMouseMove}
|
|
|
|
|
|
>
|
2025-04-24 16:03:34 +08:00
|
|
|
|
<div className="flex items-center justify-between text-sm font-bold">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
{t("assistant.popover.title")}({pagination.total})
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-04-20 21:27:25 +08:00
|
|
|
|
<button
|
|
|
|
|
|
onClick={handleRefresh}
|
2025-04-24 16:03:34 +08:00
|
|
|
|
className="flex items-center justify-center size-6 bg-white dark:bg-[#202126] rounded-lg border dark:border-white/10"
|
2025-04-20 21:27:25 +08:00
|
|
|
|
disabled={isRefreshing}
|
|
|
|
|
|
>
|
|
|
|
|
|
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
|
|
|
|
|
<RefreshCw
|
2025-04-24 16:03:34 +08:00
|
|
|
|
className={clsx(
|
|
|
|
|
|
"size-3 text-[#0287FF] transition-transform duration-1000",
|
|
|
|
|
|
{
|
|
|
|
|
|
"animate-spin": isRefreshing,
|
|
|
|
|
|
}
|
|
|
|
|
|
)}
|
2025-04-20 21:27:25 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</VisibleKey>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2025-04-24 16:03:34 +08:00
|
|
|
|
|
|
|
|
|
|
<VisibleKey
|
|
|
|
|
|
shortcut="F"
|
|
|
|
|
|
rootClassName="w-full my-3"
|
|
|
|
|
|
shortcutClassName="left-4"
|
|
|
|
|
|
onKeyPress={() => {
|
|
|
|
|
|
searchInputRef.current?.focus();
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2025-04-25 13:25:19 +08:00
|
|
|
|
<PopoverInput
|
2025-04-24 16:03:34 +08:00
|
|
|
|
ref={searchInputRef}
|
|
|
|
|
|
autoFocus
|
|
|
|
|
|
value={keyword}
|
|
|
|
|
|
placeholder={t("assistant.popover.search")}
|
|
|
|
|
|
className="w-full h-8 px-2 bg-transparent border rounded-md dark:border-white/10"
|
|
|
|
|
|
onChange={(event) => {
|
2025-06-27 10:08:33 +08:00
|
|
|
|
const value = event.target.value.trim();
|
|
|
|
|
|
|
2025-06-12 10:31:15 +08:00
|
|
|
|
setKeyword(value);
|
2025-04-20 21:27:25 +08:00
|
|
|
|
}}
|
2025-04-24 16:03:34 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</VisibleKey>
|
|
|
|
|
|
|
|
|
|
|
|
{assistants.length > 0 ? (
|
|
|
|
|
|
<>
|
2025-05-26 15:44:05 +08:00
|
|
|
|
{assistants.map((assistant, index) => {
|
2025-04-24 16:03:34 +08:00
|
|
|
|
return (
|
2025-05-26 15:44:05 +08:00
|
|
|
|
<AssistantItem
|
|
|
|
|
|
key={assistant._id}
|
|
|
|
|
|
{...assistant}
|
|
|
|
|
|
isActive={currentAssistant?._id === assistant._id}
|
|
|
|
|
|
isHighlight={highlightIndex === index}
|
|
|
|
|
|
isKeyboardActive={isKeyboardActive}
|
2025-04-24 16:03:34 +08:00
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setCurrentAssistant(assistant);
|
2025-04-29 15:16:46 +08:00
|
|
|
|
popoverButtonRef.current?.click();
|
2025-04-24 16:03:34 +08:00
|
|
|
|
}}
|
2025-05-26 15:44:05 +08:00
|
|
|
|
/>
|
2025-04-24 16:03:34 +08:00
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
|
2025-05-26 15:44:05 +08:00
|
|
|
|
<Pagination
|
|
|
|
|
|
current={pagination.current}
|
|
|
|
|
|
totalPage={pagination.totalPage}
|
|
|
|
|
|
onPrev={handlePrev}
|
|
|
|
|
|
onNext={handleNext}
|
|
|
|
|
|
className="-mx-3 -mb-3"
|
|
|
|
|
|
/>
|
2025-04-24 16:03:34 +08:00
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="flex justify-center items-center py-2">
|
|
|
|
|
|
<NoDataImage />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</PopoverPanel>
|
|
|
|
|
|
</Popover>
|
2025-04-20 21:27:25 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|