Files
coco-app/src/components/Assistant/AssistantList.tsx
BiggerRain ed8a1cb477 refactor: replace legacy components with shadcn/ui components (#1002)
* chore: shadcn config

* feat: add shadcn ui config

* style: adjust styles

* style: adjust styles

* refactor: update style

* style: adjust styles

* style: adjust styles

* style: adjust styles

* style: adjust styles

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* style: adjust styles

* style: adjust styles

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* style: web styles

* refactor: update

* style: web styles

* style: web styles

* refactor: update

* refactor: update

* refactor: update

* chhore: add

* chore: add

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* chore: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* chore: rename

* refactor: update

* refactor: update

* chore: add

* refactor: update

* chore: update

* chroe: up

* refactor: update

* refactor: update

* chore: up

* refactor: update

* chore: up

* feat: support for extracting css variables

* chore: update

* fix: fixed dark mode

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* docs: update release notes

* style: adjust styles

* style: adjust styles

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

---------

Co-authored-by: ayang <473033518@qq.com>
2025-12-18 10:26:13 +08:00

292 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useRef, useCallback, useEffect } from "react";
import { ChevronDownIcon, RefreshCw } from "lucide-react";
import { useTranslation } from "react-i18next";
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "@/components/ui/popover";
import { useDebounce, useKeyPress, usePagination } from "ahooks";
import clsx from "clsx";
import logoImg from "@/assets/icon.svg";
import VisibleKey from "@/components/Common/VisibleKey";
import { useConnectStore } from "@/stores/connectStore";
import FontIcon from "@/components/Common/Icons/FontIcon";
import { useShortcutsStore } from "@/stores/shortcutsStore";
import NoDataImage from "@/components/Common/NoDataImage";
import PopoverInput from "@/components/Common/PopoverInput";
import { AssistantFetcher } from "./AssistantFetcher";
import AssistantItem from "./AssistantItem";
import Pagination from "@/components/Common/Pagination";
import { useSearchStore } from "@/stores/searchStore";
import { Button } from "../ui/button";
interface AssistantListProps {
assistantIDs?: string[];
}
export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
const { t } = useTranslation();
const currentService = useConnectStore((state) => state.currentService);
const currentAssistant = useConnectStore((state) => state.currentAssistant);
const setCurrentAssistant = useConnectStore((state) => {
return state.setCurrentAssistant;
});
const assistantList = useConnectStore((state) => state.assistantList);
const aiAssistant = useShortcutsStore((state) => state.aiAssistant);
const [assistants, setAssistants] = useState<any[]>([]);
const [isRefreshing, setIsRefreshing] = useState(false);
const popoverRef = useRef<HTMLDivElement>(null);
const popoverButtonRef = useRef<HTMLButtonElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null);
const [keyword, setKeyword] = useState("");
const debounceKeyword = useDebounce(keyword, { wait: 500 });
const askAiAssistantId = useSearchStore((state) => state.askAiAssistantId);
const setAskAiAssistantId = useSearchStore((state) => {
return state.setAskAiAssistantId;
});
const targetAssistantId = useSearchStore((state) => state.targetAssistantId);
const setTargetAssistantId = useSearchStore((state) => {
return state.setTargetAssistantId;
});
const { fetchAssistant } = AssistantFetcher({
debounceKeyword,
assistantIDs,
});
const getAssistants = (params: { current: number; pageSize: number }) => {
return fetchAssistant(params);
};
const { pagination, runAsync } = usePagination(getAssistants, {
defaultPageSize: 5,
refreshDeps: [currentService?.id, debounceKeyword, currentService?.enabled],
onSuccess(data) {
setAssistants(data.list);
if (data.list.length === 0) {
setCurrentAssistant(void 0);
}
},
});
const handleRefresh = async () => {
setIsRefreshing(true);
await runAsync({ current: 1, pageSize: 5 });
setTimeout(() => setIsRefreshing(false), 1000);
};
const [highlightIndex, setHighlightIndex] = useState<number>(-1);
const [isKeyboardActive, setIsKeyboardActive] = useState(false);
const [open, setOpen] = useState(false);
useEffect(() => {
const targetId = askAiAssistantId ?? targetAssistantId;
if (!targetId || assistantList.length === 0) return;
const matched = assistantList.find((item) => item._id === targetId);
if (!matched) return;
if (currentAssistant?._id !== matched._id) {
setCurrentAssistant(matched);
}
if (askAiAssistantId) {
setAskAiAssistantId(void 0);
} else if (targetAssistantId) {
setTargetAssistantId(void 0);
}
}, [assistantList, askAiAssistantId, targetAssistantId]);
useKeyPress(
["uparrow", "downarrow", "enter"],
(event, key) => {
const isClose = !open;
if (isClose) return;
event.stopPropagation();
event.preventDefault();
setIsKeyboardActive(true);
const index = assistants.findIndex(
(item) => item._id === currentAssistant?._id
);
const length = assistants.length;
if (length <= 1) return;
let nextIndex = highlightIndex === -1 ? index : highlightIndex;
if (key === "uparrow") {
nextIndex = nextIndex > 0 ? nextIndex - 1 : length - 1;
} else if (key === "downarrow") {
nextIndex = nextIndex < length - 1 ? nextIndex + 1 : 0;
}
if (key === "enter") {
setCurrentAssistant(assistants[nextIndex]);
return popoverButtonRef.current?.click();
}
setHighlightIndex(nextIndex);
},
{
target: popoverRef,
}
);
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]);
const handleMouseMove = useCallback(() => {
setHighlightIndex(-1);
setIsKeyboardActive(false);
}, []);
return (
<div ref={popoverRef} className="relative">
<Popover
open={open}
onOpenChange={(v) => {
setOpen(v);
}}
>
<PopoverTrigger
ref={popoverButtonRef}
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"
>
{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")}
/>
)}
<div className="max-w-[100px] truncate">
{currentAssistant?._source?.name || "Coco AI"}
</div>
<VisibleKey
shortcut={aiAssistant}
onKeyPress={() => {
popoverButtonRef.current?.click();
}}
>
<ChevronDownIcon className="size-4 text-muted-foreground transition-transform" />
</VisibleKey>
</PopoverTrigger>
<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"
onMouseMove={handleMouseMove}
>
<div className="flex items-center justify-between text-sm font-bold">
<div>
{t("assistant.popover.title")}{pagination.total}
</div>
<Button
variant="outline"
size="icon"
onClick={handleRefresh}
className="size-6"
disabled={isRefreshing}
>
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
<RefreshCw
className={clsx(
"size-3 text-[#0287FF] transition-transform duration-1000",
{
"animate-spin": isRefreshing,
}
)}
/>
</VisibleKey>
</Button>
</div>
<VisibleKey
shortcut="F"
rootClassName="w-full my-3"
shortcutClassName="left-4"
onKeyPress={() => {
searchInputRef.current?.focus();
}}
>
<PopoverInput
ref={searchInputRef}
autoFocus
value={keyword}
placeholder={t("assistant.popover.search")}
className="w-full h-8"
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setKeyword(event.target.value);
}}
/>
</VisibleKey>
{assistants.length > 0 ? (
<>
{assistants.map((assistant, index) => {
return (
<AssistantItem
key={assistant._id}
{...assistant}
isActive={currentAssistant?._id === assistant._id}
isHighlight={highlightIndex === index}
isKeyboardActive={isKeyboardActive}
onClick={() => {
setCurrentAssistant(assistant);
popoverButtonRef.current?.click();
}}
/>
);
})}
<Pagination
current={pagination.current}
totalPage={pagination.totalPage}
onPrev={handlePrev}
onNext={handleNext}
className="-mx-3 -mb-3"
/>
</>
) : (
<div className="flex justify-center items-center py-2">
<NoDataImage />
</div>
)}
</PopoverContent>
</Popover>
</div>
);
}