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-12-18 10:26:13 +08:00
|
|
|
|
import {
|
|
|
|
|
|
Popover,
|
|
|
|
|
|
PopoverTrigger,
|
|
|
|
|
|
PopoverContent,
|
|
|
|
|
|
} from "@/components/ui/popover";
|
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-12-18 10:26:13 +08:00
|
|
|
|
import { Button } from "../ui/button";
|
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-10-10 14:26:59 +08:00
|
|
|
|
const assistantList = useConnectStore((state) => state.assistantList);
|
|
|
|
|
|
|
2025-04-23 00:12:22 +08:00
|
|
|
|
const aiAssistant = useShortcutsStore((state) => state.aiAssistant);
|
2025-10-10 14:26:59 +08:00
|
|
|
|
|
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("");
|
2025-10-10 14:26:59 +08:00
|
|
|
|
|
2025-04-24 16:03:34 +08:00
|
|
|
|
const debounceKeyword = useDebounce(keyword, { wait: 500 });
|
2025-10-10 14:26:59 +08:00
|
|
|
|
|
2025-05-30 17:18:52 +08:00
|
|
|
|
const askAiAssistantId = useSearchStore((state) => state.askAiAssistantId);
|
|
|
|
|
|
const setAskAiAssistantId = useSearchStore((state) => {
|
|
|
|
|
|
return state.setAskAiAssistantId;
|
|
|
|
|
|
});
|
2025-11-27 10:12:49 +08:00
|
|
|
|
const targetAssistantId = useSearchStore((state) => state.targetAssistantId);
|
|
|
|
|
|
const setTargetAssistantId = useSearchStore((state) => {
|
|
|
|
|
|
return state.setTargetAssistantId;
|
|
|
|
|
|
});
|
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 }) => {
|
|
|
|
|
|
return fetchAssistant(params);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const { pagination, runAsync } = usePagination(getAssistants, {
|
2025-04-24 16:03:34 +08:00
|
|
|
|
defaultPageSize: 5,
|
2025-07-07 19:41:29 +08:00
|
|
|
|
refreshDeps: [currentService?.id, debounceKeyword, currentService?.enabled],
|
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-12-18 10:26:13 +08:00
|
|
|
|
const [open, setOpen] = useState(false);
|
2025-05-26 15:44:05 +08:00
|
|
|
|
|
2025-05-30 17:18:52 +08:00
|
|
|
|
useEffect(() => {
|
2025-11-27 10:12:49 +08:00
|
|
|
|
const targetId = askAiAssistantId ?? targetAssistantId;
|
|
|
|
|
|
if (!targetId || assistantList.length === 0) return;
|
2025-05-30 17:18:52 +08:00
|
|
|
|
|
2025-11-27 10:12:49 +08:00
|
|
|
|
const matched = assistantList.find((item) => item._id === targetId);
|
2025-05-30 17:18:52 +08:00
|
|
|
|
if (!matched) return;
|
|
|
|
|
|
|
2025-11-27 10:12:49 +08:00
|
|
|
|
if (currentAssistant?._id !== matched._id) {
|
|
|
|
|
|
setCurrentAssistant(matched);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (askAiAssistantId) {
|
|
|
|
|
|
setAskAiAssistantId(void 0);
|
|
|
|
|
|
} else if (targetAssistantId) {
|
|
|
|
|
|
setTargetAssistantId(void 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [assistantList, askAiAssistantId, targetAssistantId]);
|
2025-05-30 17:18:52 +08:00
|
|
|
|
|
2025-04-29 15:16:46 +08:00
|
|
|
|
useKeyPress(
|
|
|
|
|
|
["uparrow", "downarrow", "enter"],
|
|
|
|
|
|
(event, key) => {
|
2025-12-18 10:26:13 +08:00
|
|
|
|
const isClose = !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-12-18 10:26:13 +08:00
|
|
|
|
<div ref={popoverRef} className="relative">
|
|
|
|
|
|
<Popover
|
|
|
|
|
|
open={open}
|
|
|
|
|
|
onOpenChange={(v) => {
|
|
|
|
|
|
setOpen(v);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<PopoverTrigger
|
2025-04-24 16:03:34 +08:00
|
|
|
|
ref={popoverButtonRef}
|
2025-12-18 10:26:13 +08:00
|
|
|
|
className="h-6 p-1 px-1.5 flex items-center gap-1 rounded-full border border-input bg-background text-sm/6 font-semibold text-foreground hover:bg-accent hover:text-accent-foreground focus:outline-none"
|
2025-04-22 18:36:59 +08:00
|
|
|
|
>
|
2025-12-18 10:26:13 +08:00
|
|
|
|
{currentAssistant?._source?.icon?.startsWith("font_") ? (
|
|
|
|
|
|
<FontIcon
|
|
|
|
|
|
name={currentAssistant._source.icon}
|
|
|
|
|
|
className="w-4 h-4"
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<img
|
|
|
|
|
|
src={logoImg}
|
|
|
|
|
|
className="w-4 h-4"
|
|
|
|
|
|
alt={t("assistant.message.logo")}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2025-04-24 16:03:34 +08:00
|
|
|
|
<div className="max-w-[100px] truncate">
|
|
|
|
|
|
{currentAssistant?._source?.name || "Coco AI"}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<VisibleKey
|
|
|
|
|
|
shortcut={aiAssistant}
|
|
|
|
|
|
onKeyPress={() => {
|
|
|
|
|
|
popoverButtonRef.current?.click();
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2025-12-18 10:26:13 +08:00
|
|
|
|
<ChevronDownIcon className="size-4 text-muted-foreground transition-transform" />
|
2025-04-24 16:03:34 +08:00
|
|
|
|
</VisibleKey>
|
2025-12-18 10:26:13 +08:00
|
|
|
|
</PopoverTrigger>
|
2025-04-24 16:03:34 +08:00
|
|
|
|
|
2025-12-18 10:26:13 +08:00
|
|
|
|
<PopoverContent
|
|
|
|
|
|
side="bottom"
|
|
|
|
|
|
align="start"
|
|
|
|
|
|
className="z-50 w-60 rounded-xl p-3 shadow-lg focus:outline-none max-h-[calc(100vh-150px)] overflow-y-auto"
|
2025-05-26 15:44:05 +08:00
|
|
|
|
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-12-18 10:26:13 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
size="icon"
|
2025-04-20 21:27:25 +08:00
|
|
|
|
onClick={handleRefresh}
|
2025-12-18 10:26:13 +08:00
|
|
|
|
className="size-6"
|
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>
|
2025-12-18 10:26:13 +08:00
|
|
|
|
</Button>
|
2025-04-20 21:27:25 +08:00
|
|
|
|
</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")}
|
2025-12-18 10:26:13 +08:00
|
|
|
|
className="w-full h-8"
|
|
|
|
|
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
2025-06-27 10:16:02 +08:00
|
|
|
|
setKeyword(event.target.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>
|
|
|
|
|
|
)}
|
2025-12-18 10:26:13 +08:00
|
|
|
|
</PopoverContent>
|
2025-04-24 16:03:34 +08:00
|
|
|
|
</Popover>
|
2025-04-20 21:27:25 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|