refactor: show placeholder image when history is empty (#398)

This commit is contained in:
ayangweb
2025-04-21 14:39:06 +08:00
committed by GitHub
parent f6e5e826fd
commit 77e6b58381
4 changed files with 201 additions and 174 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -19,6 +19,7 @@ import { useTranslation } from "react-i18next";
import VisibleKey from "../VisibleKey"; import VisibleKey from "../VisibleKey";
import { Chat } from "@/components/Assistant/types"; import { Chat } from "@/components/Assistant/types";
import NoDataImage from "../NoDataImage";
dayjs.extend(isSameOrAfter); dayjs.extend(isSameOrAfter);
@@ -158,7 +159,7 @@ const HistoryList: FC<HistoryListProps> = (props) => {
ref={listRef} ref={listRef}
id={id} id={id}
className={clsx( className={clsx(
"h-full overflow-auto px-3 py-2 text-sm bg-[#F3F4F6] dark:bg-[#1F2937]" "flex flex-col h-full overflow-auto px-3 py-2 text-sm bg-[#F3F4F6] dark:bg-[#1F2937]"
)} )}
> >
<div className="flex gap-1 children:h-8"> <div className="flex gap-1 children:h-8">
@@ -197,194 +198,202 @@ const HistoryList: FC<HistoryListProps> = (props) => {
</div> </div>
</div> </div>
<div className="mt-6"> {list.length > 0 ? (
{Object.entries(sortedList).map(([label, list]) => { <>
return ( <div className="mt-6">
<div key={label}> {Object.entries(sortedList).map(([label, list]) => {
<span className="text-xs text-[#999] px-3">{t(label)}</span> return (
<div key={label}>
<span className="text-xs text-[#999] px-3">{t(label)}</span>
<ul> <ul>
{list.map((item) => { {list.map((item) => {
const { _id, _source } = item; const { _id, _source } = item;
const isActive = _id === active?._id; const isActive = _id === active?._id;
const title = _source?.title ?? _id; const title = _source?.title ?? _id;
return ( return (
<li <li
key={_id} key={_id}
id={_id} id={_id}
className={clsx( className={clsx(
"flex items-center mt-1 h-10 rounded-lg cursor-pointer hover:bg-[#EDEDED] dark:hover:bg-[#353F4D] transition", "flex items-center mt-1 h-10 rounded-lg cursor-pointer hover:bg-[#EDEDED] dark:hover:bg-[#353F4D] transition",
{ {
"!bg-[#E5E7EB] dark:!bg-[#2B3444]": isActive, "!bg-[#E5E7EB] dark:!bg-[#2B3444]": isActive,
} }
)}
onClick={() => {
if (!isActive) {
setIsEdit(false);
}
onSelect(item);
}}
>
<div
className={clsx("w-1 h-6 rounded-sm bg-[#0072FF]", {
"opacity-0": _id !== active?._id,
})}
/>
<div className="flex-1 flex items-center justify-between gap-2 px-2 overflow-hidden">
{isEdit && isActive ? (
<Input
autoFocus
defaultValue={title}
className="flex-1 -mx-px outline-none bg-transparent border border-[#0061FF] rounded-[4px]"
onKeyDown={(event) => {
if (event.key !== "Enter") return;
onRename(item._id, event.currentTarget.value);
setIsEdit(false);
}}
onBlur={(event) => {
onRename(item._id, event.target.value);
setIsEdit(false);
}}
/>
) : (
<span className="truncate">{title}</span>
)}
<div className="flex items-center gap-2">
{isActive && !isEdit && (
<VisibleKey
shortcut="↑↓"
rootClassName="w-6"
shortcutClassName="w-6"
/>
)} )}
onClick={() => {
if (!isActive) {
setIsEdit(false);
}
<Popover> onSelect(item);
{isActive && !isEdit && ( }}
<PopoverButton >
ref={moreButtonRef} <div
className="flex gap-2" className={clsx("w-1 h-6 rounded-sm bg-[#0072FF]", {
> "opacity-0": _id !== active?._id,
<VisibleKey })}
shortcut="O" />
onKeyPress={() => {
moreButtonRef.current?.click(); <div className="flex-1 flex items-center justify-between gap-2 px-2 overflow-hidden">
}} {isEdit && isActive ? (
> <Input
<Ellipsis className="size-4 text-[#979797]" /> autoFocus
</VisibleKey> defaultValue={title}
</PopoverButton> className="flex-1 -mx-px outline-none bg-transparent border border-[#0061FF] rounded-[4px]"
onKeyDown={(event) => {
if (event.key !== "Enter") return;
onRename(item._id, event.currentTarget.value);
setIsEdit(false);
}}
onBlur={(event) => {
onRename(item._id, event.target.value);
setIsEdit(false);
}}
/>
) : (
<span className="truncate">{title}</span>
)} )}
<PopoverPanel <div className="flex items-center gap-2">
anchor="bottom" {isActive && !isEdit && (
className="flex flex-col rounded-lg shadow-md z-100 bg-white dark:bg-[#202126] p-1 border border-black/2 dark:border-white/10" <VisibleKey
onClick={(event) => { shortcut="↑↓"
event.stopPropagation(); rootClassName="w-6"
}} shortcutClassName="w-6"
> />
{menuItems.map((menuItem) => { )}
const {
label,
icon: Icon,
shortcut,
iconColor,
onClick,
} = menuItem;
return ( <Popover>
<button {isActive && !isEdit && (
key={label} <PopoverButton
className="flex items-center gap-2 px-3 py-2 text-sm rounded-md hover:bg-[#EDEDED] dark:hover:bg-[#2B2C31] transition" ref={moreButtonRef}
onClick={onClick} className="flex gap-2"
> >
<VisibleKey <VisibleKey
shortcut={shortcut} shortcut="O"
onKeyPress={onClick} onKeyPress={() => {
moreButtonRef.current?.click();
}}
> >
<Icon <Ellipsis className="size-4 text-[#979797]" />
className="size-4"
style={{
color: iconColor,
}}
/>
</VisibleKey> </VisibleKey>
</PopoverButton>
)}
<span>{t(label)}</span> <PopoverPanel
</button> anchor="bottom"
); className="flex flex-col rounded-lg shadow-md z-100 bg-white dark:bg-[#202126] p-1 border border-black/2 dark:border-white/10"
})} onClick={(event) => {
</PopoverPanel> event.stopPropagation();
</Popover> }}
</div> >
</div> {menuItems.map((menuItem) => {
</li> const {
); label,
})} icon: Icon,
</ul> shortcut,
iconColor,
onClick,
} = menuItem;
return (
<button
key={label}
className="flex items-center gap-2 px-3 py-2 text-sm rounded-md hover:bg-[#EDEDED] dark:hover:bg-[#2B2C31] transition"
onClick={onClick}
>
<VisibleKey
shortcut={shortcut}
onKeyPress={onClick}
>
<Icon
className="size-4"
style={{
color: iconColor,
}}
/>
</VisibleKey>
<span>{t(label)}</span>
</button>
);
})}
</PopoverPanel>
</Popover>
</div>
</div>
</li>
);
})}
</ul>
</div>
);
})}
</div>
<Dialog
open={isOpen}
onClose={() => setIsOpen(false)}
className="relative z-1000"
>
<div
id="headlessui-popover-panel:delete-history"
className="fixed inset-0 flex items-center justify-center w-screen"
>
<DialogPanel className="flex flex-col justify-between w-[360px] h-[160px] p-3 text-[#333] dark:text-white/90 border border-[#e6e6e6] bg-white dark:bg-[#202126] dark:border-white/10 shadow-xl rounded-lg">
<div className="flex flex-col gap-3">
<DialogTitle className="text-base font-bold">
{t("history_list.delete_modal.title")}
</DialogTitle>
<Description className="text-sm">
{t("history_list.delete_modal.description", {
replace: [active?._source?.title || active?._id],
})}
</Description>
</div>
<div className="flex gap-4 self-end">
<VisibleKey
shortcut="N"
shortcutClassName="left-[unset] right-0"
onKeyPress={() => setIsOpen(false)}
>
<button
className="h-8 px-4 text-sm text-[#666666] bg-[#F8F9FA] dark:text-white dark:bg-[#202126] border border-[#E6E6E6] dark:border-white/10 rounded-lg"
onClick={() => setIsOpen(false)}
>
{t("history_list.delete_modal.button.cancel")}
</button>
</VisibleKey>
<VisibleKey
shortcut="Y"
shortcutClassName="left-[unset] right-0"
onKeyPress={handleRemove}
>
<button
className="h-8 px-4 text-sm text-white bg-[#EF4444] rounded-lg"
onClick={handleRemove}
>
{t("history_list.delete_modal.button.delete")}
</button>
</VisibleKey>
</div>
</DialogPanel>
</div> </div>
); </Dialog>
})} </>
</div> ) : (
<div className="flex items-center justify-center flex-1">
<Dialog <NoDataImage />
open={isOpen}
onClose={() => setIsOpen(false)}
className="relative z-1000"
>
<div
id="headlessui-popover-panel:delete-history"
className="fixed inset-0 flex items-center justify-center w-screen"
>
<DialogPanel className="flex flex-col justify-between w-[360px] h-[160px] p-3 text-[#333] dark:text-white/90 border border-[#e6e6e6] bg-white dark:bg-[#202126] dark:border-white/10 shadow-xl rounded-lg">
<div className="flex flex-col gap-3">
<DialogTitle className="text-base font-bold">
{t("history_list.delete_modal.title")}
</DialogTitle>
<Description className="text-sm">
{t("history_list.delete_modal.description", {
replace: [active?._source?.title || active?._id],
})}
</Description>
</div>
<div className="flex gap-4 self-end">
<VisibleKey
shortcut="N"
shortcutClassName="left-[unset] right-0"
onKeyPress={() => setIsOpen(false)}
>
<button
className="h-8 px-4 text-sm text-[#666666] bg-[#F8F9FA] dark:text-white dark:bg-[#202126] border border-[#E6E6E6] dark:border-white/10 rounded-lg"
onClick={() => setIsOpen(false)}
>
{t("history_list.delete_modal.button.cancel")}
</button>
</VisibleKey>
<VisibleKey
shortcut="Y"
shortcutClassName="left-[unset] right-0"
onKeyPress={handleRemove}
>
<button
className="h-8 px-4 text-sm text-white bg-[#EF4444] rounded-lg"
onClick={handleRemove}
>
{t("history_list.delete_modal.button.delete")}
</button>
</VisibleKey>
</div>
</DialogPanel>
</div> </div>
</Dialog> )}
</div> </div>
); );
}; };

View File

@@ -0,0 +1,18 @@
import { useThemeStore } from "@/stores/themeStore";
import clsx from "clsx";
import { FC, HTMLAttributes } from "react";
const NoDataImage: FC<HTMLAttributes<HTMLImageElement>> = (props) => {
const { className } = props;
const isDark = useThemeStore((state) => state.isDark);
return (
<img
{...props}
src={isDark ? "/assets/no_data_dark.png" : "/assets/no_data_light.png"}
className={clsx("size-16", className)}
/>
);
};
export default NoDataImage;