Files
coco-app/src/components/Assistant/AssistantList.tsx

292 lines
9.0 KiB
TypeScript
Raw Normal View History

import { useState, useRef, useCallback, useEffect } from "react";
import { ChevronDownIcon, RefreshCw } from "lucide-react";
import { useTranslation } from "react-i18next";
2025-12-18 10:26:13 +08:00
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";
2025-12-18 10:26:13 +08:00
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;
});
feat: add selection toolbar window for mac (#980) * feat: add selection window page * fix: chat input * feat: add selection page * chore: add * chore: test * feat: add * feat: add store * feat: add selection settings * chore: remove unused code * docs: add release note * docs: add release note * chore: format code * chore: format code * fix: copy error * disable hashbrown default feature * Enable unstable feature allocator_api To make coco-app compile in CI: ``` --> /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.15.5/src/raw/mod.rs:3856:12 | 3856 | impl<T, A: Allocator> RawIntoIter<T, A> { | ^^^^^^^^^ | = note: see issue #32838 <https://github.com/rust-lang/rust/issues/32838> for more information = help: add `#![feature(allocator_api)]` to the crate attributes to enable = note: this compiler was built on 2025-06-25; consider upgrading it if it is out of date ``` I don't know why it does not compile, feature `allocator-api2` is enabled for `hashbrown 0.15.5`, so technically [1] it should not use the allocator APIs from the std. According to [2], enabling the `nightly` feature of `allocator-api2` may cause this issue as well, but it is not enabled in our case either. Anyway, enabling `#![feature(allocator_api)]` should make it work. [1]: https://github.com/rust-lang/hashbrown/blob/b751eef8e99ccf3652046ef4a9e1ec47c1bfb78d/src/raw/alloc.rs#L26-L47 [2]: https://github.com/rust-lang/hashbrown/issues/564 * put it in main.rs * format main.rs * Enable default-features for hashbrown 0.15.5 * format main.rs * enable feature allocator-api2 * feat: add selection set config * fix: selection setting * fix: ci error * fix: ci error * fix: ci error * fix: ci error * merge: merge main * fix: rust code warn * fix: rust code error * fix: rust code error * fix: selection settings * style: selection styles * style: selection styles --------- Co-authored-by: Steve Lau <stevelauc@outlook.com>
2025-11-27 10:12:49 +08:00
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);
2025-12-18 10:26:13 +08:00
const [open, setOpen] = useState(false);
useEffect(() => {
feat: add selection toolbar window for mac (#980) * feat: add selection window page * fix: chat input * feat: add selection page * chore: add * chore: test * feat: add * feat: add store * feat: add selection settings * chore: remove unused code * docs: add release note * docs: add release note * chore: format code * chore: format code * fix: copy error * disable hashbrown default feature * Enable unstable feature allocator_api To make coco-app compile in CI: ``` --> /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.15.5/src/raw/mod.rs:3856:12 | 3856 | impl<T, A: Allocator> RawIntoIter<T, A> { | ^^^^^^^^^ | = note: see issue #32838 <https://github.com/rust-lang/rust/issues/32838> for more information = help: add `#![feature(allocator_api)]` to the crate attributes to enable = note: this compiler was built on 2025-06-25; consider upgrading it if it is out of date ``` I don't know why it does not compile, feature `allocator-api2` is enabled for `hashbrown 0.15.5`, so technically [1] it should not use the allocator APIs from the std. According to [2], enabling the `nightly` feature of `allocator-api2` may cause this issue as well, but it is not enabled in our case either. Anyway, enabling `#![feature(allocator_api)]` should make it work. [1]: https://github.com/rust-lang/hashbrown/blob/b751eef8e99ccf3652046ef4a9e1ec47c1bfb78d/src/raw/alloc.rs#L26-L47 [2]: https://github.com/rust-lang/hashbrown/issues/564 * put it in main.rs * format main.rs * Enable default-features for hashbrown 0.15.5 * format main.rs * enable feature allocator-api2 * feat: add selection set config * fix: selection setting * fix: ci error * fix: ci error * fix: ci error * fix: ci error * merge: merge main * fix: rust code warn * fix: rust code error * fix: rust code error * fix: selection settings * style: selection styles * style: selection styles --------- Co-authored-by: Steve Lau <stevelauc@outlook.com>
2025-11-27 10:12:49 +08:00
const targetId = askAiAssistantId ?? targetAssistantId;
if (!targetId || assistantList.length === 0) return;
feat: add selection toolbar window for mac (#980) * feat: add selection window page * fix: chat input * feat: add selection page * chore: add * chore: test * feat: add * feat: add store * feat: add selection settings * chore: remove unused code * docs: add release note * docs: add release note * chore: format code * chore: format code * fix: copy error * disable hashbrown default feature * Enable unstable feature allocator_api To make coco-app compile in CI: ``` --> /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.15.5/src/raw/mod.rs:3856:12 | 3856 | impl<T, A: Allocator> RawIntoIter<T, A> { | ^^^^^^^^^ | = note: see issue #32838 <https://github.com/rust-lang/rust/issues/32838> for more information = help: add `#![feature(allocator_api)]` to the crate attributes to enable = note: this compiler was built on 2025-06-25; consider upgrading it if it is out of date ``` I don't know why it does not compile, feature `allocator-api2` is enabled for `hashbrown 0.15.5`, so technically [1] it should not use the allocator APIs from the std. According to [2], enabling the `nightly` feature of `allocator-api2` may cause this issue as well, but it is not enabled in our case either. Anyway, enabling `#![feature(allocator_api)]` should make it work. [1]: https://github.com/rust-lang/hashbrown/blob/b751eef8e99ccf3652046ef4a9e1ec47c1bfb78d/src/raw/alloc.rs#L26-L47 [2]: https://github.com/rust-lang/hashbrown/issues/564 * put it in main.rs * format main.rs * Enable default-features for hashbrown 0.15.5 * format main.rs * enable feature allocator-api2 * feat: add selection set config * fix: selection setting * fix: ci error * fix: ci error * fix: ci error * fix: ci error * merge: merge main * fix: rust code warn * fix: rust code error * fix: rust code error * fix: selection settings * style: selection styles * style: selection styles --------- Co-authored-by: Steve Lau <stevelauc@outlook.com>
2025-11-27 10:12:49 +08:00
const matched = assistantList.find((item) => item._id === targetId);
if (!matched) return;
feat: add selection toolbar window for mac (#980) * feat: add selection window page * fix: chat input * feat: add selection page * chore: add * chore: test * feat: add * feat: add store * feat: add selection settings * chore: remove unused code * docs: add release note * docs: add release note * chore: format code * chore: format code * fix: copy error * disable hashbrown default feature * Enable unstable feature allocator_api To make coco-app compile in CI: ``` --> /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.15.5/src/raw/mod.rs:3856:12 | 3856 | impl<T, A: Allocator> RawIntoIter<T, A> { | ^^^^^^^^^ | = note: see issue #32838 <https://github.com/rust-lang/rust/issues/32838> for more information = help: add `#![feature(allocator_api)]` to the crate attributes to enable = note: this compiler was built on 2025-06-25; consider upgrading it if it is out of date ``` I don't know why it does not compile, feature `allocator-api2` is enabled for `hashbrown 0.15.5`, so technically [1] it should not use the allocator APIs from the std. According to [2], enabling the `nightly` feature of `allocator-api2` may cause this issue as well, but it is not enabled in our case either. Anyway, enabling `#![feature(allocator_api)]` should make it work. [1]: https://github.com/rust-lang/hashbrown/blob/b751eef8e99ccf3652046ef4a9e1ec47c1bfb78d/src/raw/alloc.rs#L26-L47 [2]: https://github.com/rust-lang/hashbrown/issues/564 * put it in main.rs * format main.rs * Enable default-features for hashbrown 0.15.5 * format main.rs * enable feature allocator-api2 * feat: add selection set config * fix: selection setting * fix: ci error * fix: ci error * fix: ci error * fix: ci error * merge: merge main * fix: rust code warn * fix: rust code error * fix: rust code error * fix: selection settings * style: selection styles * style: selection styles --------- Co-authored-by: Steve Lau <stevelauc@outlook.com>
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]);
useKeyPress(
["uparrow", "downarrow", "enter"],
(event, key) => {
2025-12-18 10:26:13 +08:00
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 (
2025-12-18 10:26:13 +08:00
<div ref={popoverRef} className="relative">
<Popover
open={open}
onOpenChange={(v) => {
setOpen(v);
}}
>
<PopoverTrigger
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-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")}
/>
)}
<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" />
</VisibleKey>
2025-12-18 10:26:13 +08:00
</PopoverTrigger>
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"
onMouseMove={handleMouseMove}
>
<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"
onClick={handleRefresh}
2025-12-18 10:26:13 +08:00
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>
2025-12-18 10:26:13 +08:00
</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")}
2025-12-18 10:26:13 +08:00
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>
)}
2025-12-18 10:26:13 +08:00
</PopoverContent>
</Popover>
</div>
);
}