mirror of
https://github.com/infinilabs/coco-app.git
synced 2026-08-29 10:09:30 +02:00
refactor: show placeholder image when history is empty (#398)
This commit is contained in:
BIN
public/assets/no_data_dark.png
Normal file
BIN
public/assets/no_data_dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
BIN
public/assets/no_data_light.png
Normal file
BIN
public/assets/no_data_light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
18
src/components/Common/NoDataImage.tsx
Normal file
18
src/components/Common/NoDataImage.tsx
Normal 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;
|
||||||
Reference in New Issue
Block a user