mirror of
https://github.com/QuivrHQ/quivr.git
synced 2024-12-15 01:21:48 +03:00
fix(frontend): history to threads (#2201)
# Description Please include a summary of the changes and the related issue. Please also include relevant motivation and context. ## Checklist before requesting a review Please delete options that are not relevant. - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my code - [ ] I have commented hard-to-understand areas - [ ] I have ideally added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged ## Screenshots (if appropriate):
This commit is contained in:
parent
ef6ee14440
commit
6ae529f614
@ -13,8 +13,11 @@ RUN apt-get clean && apt-get update && apt-get install -y \
|
|||||||
pandoc \
|
pandoc \
|
||||||
curl \
|
curl \
|
||||||
git \
|
git \
|
||||||
|
poppler-utils \
|
||||||
|
tesseract-ocr \
|
||||||
autoconf \
|
autoconf \
|
||||||
automake \
|
automake \
|
||||||
|
build-essential \
|
||||||
libtool \
|
libtool \
|
||||||
python-dev \
|
python-dev \
|
||||||
build-essential \
|
build-essential \
|
||||||
@ -23,6 +26,8 @@ RUN apt-get clean && apt-get update && apt-get install -y \
|
|||||||
poppler-utils \
|
poppler-utils \
|
||||||
tesseract-ocr \
|
tesseract-ocr \
|
||||||
libreoffice \
|
libreoffice \
|
||||||
|
libpq-dev \
|
||||||
|
gcc \
|
||||||
pandoc && \
|
pandoc && \
|
||||||
rm -rf /var/lib/apt/lists/* && apt-get clean
|
rm -rf /var/lib/apt/lists/* && apt-get clean
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
@use "@/styles/ScreenSizes.module.scss";
|
@use "@/styles/ScreenSizes.module.scss";
|
||||||
@use "@/styles/Spacings.module.scss";
|
@use "@/styles/Spacings.module.scss";
|
||||||
@use "@/styles/Typography.module.scss";
|
@use "@/styles/Typography.module.scss";
|
||||||
|
@use "@/styles/ZIndexes.module.scss";
|
||||||
|
|
||||||
.brain_item_wrapper {
|
.brain_item_wrapper {
|
||||||
padding-inline: Spacings.$spacing05;
|
padding-inline: Spacings.$spacing05;
|
||||||
@ -16,6 +17,8 @@
|
|||||||
border: 1px solid Colors.$lightest-black;
|
border: 1px solid Colors.$lightest-black;
|
||||||
border-radius: Radius.$normal;
|
border-radius: Radius.$normal;
|
||||||
padding-block: Spacings.$spacing03;
|
padding-block: Spacings.$spacing03;
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: Colors.$primary;
|
border-color: Colors.$primary;
|
||||||
@ -53,31 +56,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.options_menu {
|
.options_modal {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: Colors.$highlight;
|
right: Spacings.$spacing02;
|
||||||
border-radius: Radius.$normal;
|
top: Spacings.$spacing08;
|
||||||
right: Spacings.$spacing07;
|
z-index: ZIndexes.$modal;
|
||||||
border-radius: Radius.$normal;
|
padding-bottom: Spacings.$spacing01;
|
||||||
box-shadow: 0 1px 2px rgb(0, 0, 0, 0.25);
|
|
||||||
|
|
||||||
.option {
|
|
||||||
padding: Spacings.$spacing03;
|
|
||||||
padding-inline: Spacings.$spacing05;
|
|
||||||
display: flex;
|
|
||||||
gap: Spacings.$spacing05;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
justify-content: space-between;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&:not(:first-child) {
|
|
||||||
border-top: 1px solid Colors.$light-grey;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: Colors.$primary-lightest;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,10 @@ import { DeleteOrUnsubscribeConfirmationModal } from "@/app/studio/[brainId]/com
|
|||||||
import { useBrainManagementTabs } from "@/app/studio/[brainId]/components/BrainManagementTabs/hooks/useBrainManagementTabs";
|
import { useBrainManagementTabs } from "@/app/studio/[brainId]/components/BrainManagementTabs/hooks/useBrainManagementTabs";
|
||||||
import { getBrainPermissions } from "@/app/studio/[brainId]/components/BrainManagementTabs/utils/getBrainPermissions";
|
import { getBrainPermissions } from "@/app/studio/[brainId]/components/BrainManagementTabs/utils/getBrainPermissions";
|
||||||
import Icon from "@/lib/components/ui/Icon/Icon";
|
import Icon from "@/lib/components/ui/Icon/Icon";
|
||||||
|
import { OptionsModal } from "@/lib/components/ui/OptionsModal/OptionsModal";
|
||||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||||
import { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";
|
import { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";
|
||||||
|
import { Option } from "@/lib/types/Options";
|
||||||
|
|
||||||
import styles from "./BrainItem.module.scss";
|
import styles from "./BrainItem.module.scss";
|
||||||
|
|
||||||
@ -17,34 +19,49 @@ type BrainItemProps = {
|
|||||||
|
|
||||||
export const BrainItem = ({ brain, even }: BrainItemProps): JSX.Element => {
|
export const BrainItem = ({ brain, even }: BrainItemProps): JSX.Element => {
|
||||||
const [optionsOpened, setOptionsOpened] = useState<boolean>(false);
|
const [optionsOpened, setOptionsOpened] = useState<boolean>(false);
|
||||||
const [deleteHovered, setDeleteHovered] = useState<boolean>(false);
|
|
||||||
const [editHovered, setEditHovered] = useState<boolean>(false);
|
|
||||||
const {
|
const {
|
||||||
handleUnsubscribeOrDeleteBrain,
|
handleUnsubscribeOrDeleteBrain,
|
||||||
isDeleteOrUnsubscribeModalOpened,
|
isDeleteOrUnsubscribeModalOpened,
|
||||||
setIsDeleteOrUnsubscribeModalOpened,
|
setIsDeleteOrUnsubscribeModalOpened,
|
||||||
isDeleteOrUnsubscribeRequestPending,
|
isDeleteOrUnsubscribeRequestPending,
|
||||||
} = useBrainManagementTabs(brain.id);
|
} = useBrainManagementTabs(brain.id);
|
||||||
|
|
||||||
const { allBrains } = useBrainContext();
|
const { allBrains } = useBrainContext();
|
||||||
|
|
||||||
const { isOwnedByCurrentUser } = getBrainPermissions({
|
const { isOwnedByCurrentUser } = getBrainPermissions({
|
||||||
brainId: brain.id,
|
brainId: brain.id,
|
||||||
userAccessibleBrains: allBrains,
|
userAccessibleBrains: allBrains,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const iconRef = useRef<HTMLDivElement | null>(null);
|
||||||
const optionsRef = useRef<HTMLDivElement | null>(null);
|
const optionsRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const options: Option[] = [
|
||||||
|
{
|
||||||
|
label: "Edit",
|
||||||
|
onClick: () => (window.location.href = `/studio/${brain.id}`),
|
||||||
|
iconName: "edit",
|
||||||
|
iconColor: "primary",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Delete",
|
||||||
|
onClick: () => void setIsDeleteOrUnsubscribeModalOpened(true),
|
||||||
|
iconName: "delete",
|
||||||
|
iconColor: "dangerous",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
if (
|
if (
|
||||||
|
iconRef.current &&
|
||||||
|
!iconRef.current.contains(event.target as Node) &&
|
||||||
optionsRef.current &&
|
optionsRef.current &&
|
||||||
!optionsRef.current.contains(event.target as Node)
|
!optionsRef.current.contains(event.target as Node)
|
||||||
) {
|
) {
|
||||||
setOptionsOpened(false);
|
setOptionsOpened(false);
|
||||||
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@ -53,68 +70,50 @@ export const BrainItem = ({ brain, even }: BrainItemProps): JSX.Element => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
className={`
|
<div
|
||||||
|
className={`
|
||||||
${even ? styles.even : styles.odd}
|
${even ? styles.even : styles.odd}
|
||||||
${styles.brain_item_wrapper}
|
${styles.brain_item_wrapper}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<Link className={styles.brain_info_wrapper} href={`/studio/${brain.id}`}>
|
<Link
|
||||||
<span className={styles.name}>{brain.name}</span>
|
className={styles.brain_info_wrapper}
|
||||||
<span className={styles.description}>{brain.description}</span>
|
href={`/studio/${brain.id}`}
|
||||||
</Link>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
onClick={(event: React.MouseEvent<HTMLElement>) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
setOptionsOpened(!optionsOpened);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Icon name="options" size="normal" color="black" handleHover={true} />
|
<span className={styles.name}>{brain.name}</span>
|
||||||
</div>
|
<span className={styles.description}>{brain.description}</span>
|
||||||
{optionsOpened && (
|
</Link>
|
||||||
<div className={styles.options_menu} ref={optionsRef}>
|
|
||||||
<div
|
<div>
|
||||||
className={styles.option}
|
<div
|
||||||
onClick={() => setIsDeleteOrUnsubscribeModalOpened(true)}
|
ref={iconRef}
|
||||||
onMouseEnter={() => setDeleteHovered(true)}
|
onClick={(event: React.MouseEvent<HTMLElement>) => {
|
||||||
onMouseLeave={() => setDeleteHovered(false)}
|
event.nativeEvent.stopImmediatePropagation();
|
||||||
>
|
setOptionsOpened(!optionsOpened);
|
||||||
<span>Delete</span>
|
}}
|
||||||
<Icon
|
>
|
||||||
name="delete"
|
<Icon
|
||||||
size="normal"
|
name="options"
|
||||||
color="dangerous"
|
size="normal"
|
||||||
hovered={deleteHovered}
|
color="black"
|
||||||
/>
|
handleHover={true}
|
||||||
</div>
|
/>
|
||||||
<div
|
|
||||||
className={styles.option}
|
|
||||||
onClick={() => (window.location.href = `/studio/${brain.id}`)}
|
|
||||||
onMouseEnter={() => setEditHovered(true)}
|
|
||||||
onMouseLeave={() => setEditHovered(false)}
|
|
||||||
>
|
|
||||||
<span>Edit</span>
|
|
||||||
<Icon
|
|
||||||
name="edit"
|
|
||||||
size="normal"
|
|
||||||
color="black"
|
|
||||||
hovered={editHovered}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<DeleteOrUnsubscribeConfirmationModal
|
||||||
|
isOpen={isDeleteOrUnsubscribeModalOpened}
|
||||||
|
setOpen={setIsDeleteOrUnsubscribeModalOpened}
|
||||||
|
onConfirm={() => void handleUnsubscribeOrDeleteBrain()}
|
||||||
|
isOwnedByCurrentUser={isOwnedByCurrentUser}
|
||||||
|
isDeleteOrUnsubscribeRequestPending={
|
||||||
|
isDeleteOrUnsubscribeRequestPending
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div ref={optionsRef} className={styles.options_modal}>
|
||||||
|
{optionsOpened && <OptionsModal options={options} />}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DeleteOrUnsubscribeConfirmationModal
|
</>
|
||||||
isOpen={isDeleteOrUnsubscribeModalOpened}
|
|
||||||
setOpen={setIsDeleteOrUnsubscribeModalOpened}
|
|
||||||
onConfirm={() => void handleUnsubscribeOrDeleteBrain()}
|
|
||||||
isOwnedByCurrentUser={isOwnedByCurrentUser}
|
|
||||||
isDeleteOrUnsubscribeRequestPending={
|
|
||||||
isDeleteOrUnsubscribeRequestPending
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -11,10 +11,10 @@ import { useMenuContext } from "@/lib/context/MenuProvider/hooks/useMenuContext"
|
|||||||
import styles from "./Menu.module.scss";
|
import styles from "./Menu.module.scss";
|
||||||
import { AnimatedDiv } from "./components/AnimationDiv";
|
import { AnimatedDiv } from "./components/AnimationDiv";
|
||||||
import { DiscussionButton } from "./components/DiscussionButton/DiscussionButton";
|
import { DiscussionButton } from "./components/DiscussionButton/DiscussionButton";
|
||||||
import { HistoryButton } from "./components/HistoryButton/HistoryButton";
|
|
||||||
import { HomeButton } from "./components/HomeButton/HomeButton";
|
import { HomeButton } from "./components/HomeButton/HomeButton";
|
||||||
import { ProfileButton } from "./components/ProfileButton/ProfileButton";
|
import { ProfileButton } from "./components/ProfileButton/ProfileButton";
|
||||||
import { StudioButton } from "./components/StudioButton/StudioButton";
|
import { StudioButton } from "./components/StudioButton/StudioButton";
|
||||||
|
import { ThreadsButton } from "./components/ThreadsButton/ThreadsButton";
|
||||||
import { UpgradeToPlusButton } from "./components/UpgradeToPlusButton/UpgradeToPlusButton";
|
import { UpgradeToPlusButton } from "./components/UpgradeToPlusButton/UpgradeToPlusButton";
|
||||||
|
|
||||||
export const Menu = (): JSX.Element => {
|
export const Menu = (): JSX.Element => {
|
||||||
@ -61,7 +61,7 @@ export const Menu = (): JSX.Element => {
|
|||||||
<DiscussionButton />
|
<DiscussionButton />
|
||||||
<HomeButton />
|
<HomeButton />
|
||||||
<StudioButton />
|
<StudioButton />
|
||||||
<HistoryButton />
|
<ThreadsButton />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.block}>
|
<div className={styles.block}>
|
||||||
<UpgradeToPlusButton />
|
<UpgradeToPlusButton />
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
import { ChatEntity } from "@/app/chat/[chatId]/types";
|
|
||||||
import Icon from "@/lib/components/ui/Icon/Icon";
|
|
||||||
|
|
||||||
import styles from "./ChatHistoryItem.module.scss";
|
|
||||||
|
|
||||||
import { useChatsListItem } from "../../hooks/useChatsListItem";
|
|
||||||
|
|
||||||
type ChatHistoryItemProps = {
|
|
||||||
chatHistoryItem: ChatEntity;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ChatHistoryItem = ({
|
|
||||||
chatHistoryItem,
|
|
||||||
}: ChatHistoryItemProps): JSX.Element => {
|
|
||||||
const {
|
|
||||||
chatName,
|
|
||||||
deleteChat,
|
|
||||||
editingName,
|
|
||||||
handleEditNameClick,
|
|
||||||
setChatName,
|
|
||||||
} = useChatsListItem(chatHistoryItem);
|
|
||||||
|
|
||||||
const onNameEdited = () => {
|
|
||||||
handleEditNameClick();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.chat_item_wrapper}>
|
|
||||||
{editingName ? (
|
|
||||||
<input
|
|
||||||
className={styles.edit_chat_name}
|
|
||||||
onChange={(event) => setChatName(event.target.value)}
|
|
||||||
value={chatName}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === "Enter") {
|
|
||||||
onNameEdited();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Link
|
|
||||||
className={styles.chat_item_name}
|
|
||||||
href={`/chat/${chatHistoryItem.chat_id}`}
|
|
||||||
>
|
|
||||||
{chatName.trim()}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
<div className={styles.icons_wrapper}>
|
|
||||||
<Icon
|
|
||||||
name={editingName ? "check" : "edit"}
|
|
||||||
size="normal"
|
|
||||||
color="black"
|
|
||||||
handleHover={true}
|
|
||||||
onClick={() => onNameEdited()}
|
|
||||||
/>
|
|
||||||
<Icon
|
|
||||||
name="delete"
|
|
||||||
size="normal"
|
|
||||||
color="dangerous"
|
|
||||||
handleHover={true}
|
|
||||||
onClick={() => void deleteChat()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -9,19 +9,8 @@
|
|||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
color: Colors.$dark-grey;
|
color: Colors.$dark-grey;
|
||||||
font-size: Typography.$small;
|
font-size: Typography.$small;
|
||||||
max-height: 200px;
|
max-height: 300px;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
&.fade_out {
|
|
||||||
mask-image: -webkit-gradient(
|
|
||||||
linear,
|
|
||||||
left top,
|
|
||||||
left bottom,
|
|
||||||
from(black),
|
|
||||||
to(transparent)
|
|
||||||
);
|
|
||||||
mask-image: linear-gradient(to bottom, black 50%, transparent 100%);
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -5,11 +5,11 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { FoldableSection } from "@/lib/components/ui/FoldableSection/FoldableSection";
|
import { FoldableSection } from "@/lib/components/ui/FoldableSection/FoldableSection";
|
||||||
import { useChatsContext } from "@/lib/context/ChatsProvider/hooks/useChatsContext";
|
import { useChatsContext } from "@/lib/context/ChatsProvider/hooks/useChatsContext";
|
||||||
|
|
||||||
import { ChatsSection } from "./ChatsSection/ChatsSection";
|
import styles from "./ThreadsButton.module.scss";
|
||||||
import styles from "./HistoryButton.module.scss";
|
import { ThreadsSection } from "./ThreadsSection/ThreadsSection";
|
||||||
import { isWithinLast30Days, isWithinLast7Days, isYesterday } from "./utils";
|
import { isWithinLast30Days, isWithinLast7Days, isYesterday } from "./utils";
|
||||||
|
|
||||||
export const HistoryButton = (): JSX.Element => {
|
export const ThreadsButton = (): JSX.Element => {
|
||||||
const [canScrollDown, setCanScrollDown] = useState<boolean>(false);
|
const [canScrollDown, setCanScrollDown] = useState<boolean>(false);
|
||||||
const { allChats } = useChatsContext();
|
const { allChats } = useChatsContext();
|
||||||
const { t } = useTranslation("chat");
|
const { t } = useTranslation("chat");
|
||||||
@ -49,10 +49,10 @@ export const HistoryButton = (): JSX.Element => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FoldableSection
|
<FoldableSection
|
||||||
label={t("history")}
|
label={t("threads")}
|
||||||
icon="history"
|
icon="history"
|
||||||
foldedByDefault={true}
|
foldedByDefault={true}
|
||||||
hideBorderIfUnfolded={true}
|
hideBorder={true}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
@ -60,10 +60,10 @@ export const HistoryButton = (): JSX.Element => {
|
|||||||
${canScrollDown ? styles.fade_out : ""}
|
${canScrollDown ? styles.fade_out : ""}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<ChatsSection chats={todayChats} title={t("today")} />
|
<ThreadsSection chats={todayChats} title={t("today")} />
|
||||||
<ChatsSection chats={yesterdayChats} title={t("yesterday")} />
|
<ThreadsSection chats={yesterdayChats} title={t("yesterday")} />
|
||||||
<ChatsSection chats={last7DaysChats} title={t("last7Days")} />
|
<ThreadsSection chats={last7DaysChats} title={t("last7Days")} />
|
||||||
<ChatsSection chats={last30DaysChats} title={t("last30Days")} />
|
<ThreadsSection chats={last30DaysChats} title={t("last30Days")} />
|
||||||
</div>
|
</div>
|
||||||
</FoldableSection>
|
</FoldableSection>
|
||||||
);
|
);
|
@ -2,8 +2,9 @@
|
|||||||
@use "@/styles/Radius.module.scss";
|
@use "@/styles/Radius.module.scss";
|
||||||
@use "@/styles/Spacings.module.scss";
|
@use "@/styles/Spacings.module.scss";
|
||||||
@use "@/styles/Typography.module.scss";
|
@use "@/styles/Typography.module.scss";
|
||||||
|
@use "@/styles/ZIndexes.module.scss";
|
||||||
|
|
||||||
.chat_item_wrapper {
|
.thread_item_wrapper {
|
||||||
color: Colors.$black;
|
color: Colors.$black;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -11,7 +12,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.edit_chat_name {
|
.edit_thread_name {
|
||||||
@include Typography.EllipsisOverflow;
|
@include Typography.EllipsisOverflow;
|
||||||
|
|
||||||
color: Colors.$black;
|
color: Colors.$black;
|
||||||
@ -28,7 +29,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat_item_name {
|
.thread_item_name {
|
||||||
@include Typography.EllipsisOverflow;
|
@include Typography.EllipsisOverflow;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@ -36,10 +37,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icons_wrapper {
|
.options_modal {
|
||||||
visibility: hidden;
|
position: absolute;
|
||||||
display: flex;
|
right: Spacings.$spacing02;
|
||||||
gap: Spacings.$spacing02;
|
z-index: ZIndexes.$modal;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
@ -0,0 +1,118 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import { ChatEntity } from "@/app/chat/[chatId]/types";
|
||||||
|
import Icon from "@/lib/components/ui/Icon/Icon";
|
||||||
|
import { OptionsModal } from "@/lib/components/ui/OptionsModal/OptionsModal";
|
||||||
|
import { Option } from "@/lib/types/Options";
|
||||||
|
|
||||||
|
import styles from "./ThreadItem.module.scss";
|
||||||
|
|
||||||
|
import { useChatsListItem } from "../../hooks/useChatsListItem";
|
||||||
|
|
||||||
|
type ChatHistoryItemProps = {
|
||||||
|
chatHistoryItem: ChatEntity;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ThreadItem = ({
|
||||||
|
chatHistoryItem,
|
||||||
|
}: ChatHistoryItemProps): JSX.Element => {
|
||||||
|
const [optionsOpened, setOptionsOpened] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
chatName,
|
||||||
|
deleteChat,
|
||||||
|
editingName,
|
||||||
|
handleEditNameClick,
|
||||||
|
setChatName,
|
||||||
|
} = useChatsListItem(chatHistoryItem);
|
||||||
|
|
||||||
|
const onNameEdited = () => {
|
||||||
|
handleEditNameClick();
|
||||||
|
};
|
||||||
|
|
||||||
|
const optionsRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const iconRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const options: Option[] = [
|
||||||
|
{
|
||||||
|
label: "Edit",
|
||||||
|
onClick: () => onNameEdited(),
|
||||||
|
iconName: "edit",
|
||||||
|
iconColor: "primary",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Delete",
|
||||||
|
onClick: () => void deleteChat(),
|
||||||
|
iconName: "delete",
|
||||||
|
iconColor: "dangerous",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (
|
||||||
|
iconRef.current &&
|
||||||
|
!iconRef.current.contains(event.target as Node) &&
|
||||||
|
optionsRef.current &&
|
||||||
|
!optionsRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
setOptionsOpened(false);
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.thread_item_wrapper}>
|
||||||
|
{editingName ? (
|
||||||
|
<input
|
||||||
|
className={styles.edit_thread_name}
|
||||||
|
onChange={(event) => setChatName(event.target.value)}
|
||||||
|
value={chatName}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
onNameEdited();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
className={styles.thread_item_name}
|
||||||
|
href={`/chat/${chatHistoryItem.chat_id}`}
|
||||||
|
>
|
||||||
|
{chatName.trim()}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
ref={iconRef}
|
||||||
|
onClick={(event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
event.nativeEvent.stopImmediatePropagation();
|
||||||
|
if (editingName) {
|
||||||
|
onNameEdited();
|
||||||
|
} else {
|
||||||
|
setOptionsOpened(!optionsOpened);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={editingName ? "check" : "options"}
|
||||||
|
size="small"
|
||||||
|
color="black"
|
||||||
|
handleHover={true}
|
||||||
|
/>
|
||||||
|
<div ref={optionsRef} className={styles.options_modal}>
|
||||||
|
{optionsOpened && <OptionsModal options={options} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -8,4 +8,5 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: Spacings.$spacing02;
|
gap: Spacings.$spacing02;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
@ -1,14 +1,14 @@
|
|||||||
import { ChatEntity } from "@/app/chat/[chatId]/types";
|
import { ChatEntity } from "@/app/chat/[chatId]/types";
|
||||||
|
|
||||||
import { ChatHistoryItem } from "./ChatHistoryItem/ChatHistoryItem";
|
import { ThreadItem } from "./ThreadItem/ThreadItem";
|
||||||
import styles from "./ChatsSection.module.scss";
|
import styles from "./ThreadsSection.module.scss";
|
||||||
|
|
||||||
type ChatSectionProps = {
|
type ChatSectionProps = {
|
||||||
chats: ChatEntity[];
|
chats: ChatEntity[];
|
||||||
title: string;
|
title: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ChatsSection = (props: ChatSectionProps): JSX.Element => {
|
export const ThreadsSection = (props: ChatSectionProps): JSX.Element => {
|
||||||
if (props.chats.length === 0) {
|
if (props.chats.length === 0) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
@ -18,7 +18,7 @@ export const ChatsSection = (props: ChatSectionProps): JSX.Element => {
|
|||||||
<div>{props.title}</div>
|
<div>{props.title}</div>
|
||||||
<div className={styles.chats_wrapper}>
|
<div className={styles.chats_wrapper}>
|
||||||
{props.chats.map((chat) => (
|
{props.chats.map((chat) => (
|
||||||
<ChatHistoryItem key={chat.chat_id} chatHistoryItem={chat} />
|
<ThreadItem key={chat.chat_id} chatHistoryItem={chat} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -11,7 +11,7 @@ interface FoldableSectionProps {
|
|||||||
icon: keyof typeof iconList;
|
icon: keyof typeof iconList;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
foldedByDefault?: boolean;
|
foldedByDefault?: boolean;
|
||||||
hideBorderIfUnfolded?: boolean;
|
hideBorder?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FoldableSection = (props: FoldableSectionProps): JSX.Element => {
|
export const FoldableSection = (props: FoldableSectionProps): JSX.Element => {
|
||||||
@ -26,7 +26,7 @@ export const FoldableSection = (props: FoldableSectionProps): JSX.Element => {
|
|||||||
className={`
|
className={`
|
||||||
${styles.foldable_section_wrapper}
|
${styles.foldable_section_wrapper}
|
||||||
${!folded ? styles.unfolded : ""}
|
${!folded ? styles.unfolded : ""}
|
||||||
${props.hideBorderIfUnfolded && folded ? styles.hide_border : ""}
|
${props.hideBorder ? styles.hide_border : ""}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<div className={styles.header_wrapper} onClick={() => setFolded(!folded)}>
|
<div className={styles.header_wrapper} onClick={() => setFolded(!folded)}>
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
@use "@/styles/Colors.module.scss";
|
||||||
|
@use "@/styles/Radius.module.scss";
|
||||||
|
@use "@/styles/Spacings.module.scss";
|
||||||
|
@use "@/styles/ZIndexes.module.scss";
|
||||||
|
|
||||||
|
.options_modal_wrapper {
|
||||||
|
background-color: Colors.$highlight;
|
||||||
|
border-radius: Radius.$normal;
|
||||||
|
border-radius: Radius.$normal;
|
||||||
|
box-shadow: 0 1px 2px rgb(0, 0, 0, 0.25);
|
||||||
|
width: fit-content;
|
||||||
|
|
||||||
|
.option {
|
||||||
|
padding: Spacings.$spacing03;
|
||||||
|
padding-inline: Spacings.$spacing05;
|
||||||
|
display: flex;
|
||||||
|
gap: Spacings.$spacing05;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
justify-content: space-between;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
border-top: 1px solid Colors.$light-grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: Colors.$primary-lightest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
frontend/lib/components/ui/OptionsModal/OptionsModal.tsx
Normal file
56
frontend/lib/components/ui/OptionsModal/OptionsModal.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import { Option } from "@/lib/types/Options";
|
||||||
|
|
||||||
|
import styles from "./OptionsModal.module.scss";
|
||||||
|
|
||||||
|
import { Icon } from "../Icon/Icon";
|
||||||
|
|
||||||
|
type OptionsModalProps = {
|
||||||
|
options: Option[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OptionsModal = ({ options }: OptionsModalProps): JSX.Element => {
|
||||||
|
const [hovered, setHovered] = useState<boolean[]>(
|
||||||
|
new Array(options.length).fill(false)
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMouseEnter = (index: number) => {
|
||||||
|
setHovered((prevHovered) =>
|
||||||
|
prevHovered.map((h, i) => (i === index ? true : h))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = (index: number) => {
|
||||||
|
setHovered((prevHovered) =>
|
||||||
|
prevHovered.map((h, i) => (i === index ? false : h))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
modalRef.current?.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.options_modal_wrapper} ref={modalRef} tabIndex={-1}>
|
||||||
|
{options.map((option, index) => (
|
||||||
|
<div
|
||||||
|
className={styles.option}
|
||||||
|
key={index}
|
||||||
|
onClick={option.onClick}
|
||||||
|
onMouseEnter={() => handleMouseEnter(index)}
|
||||||
|
onMouseLeave={() => handleMouseLeave(index)}
|
||||||
|
>
|
||||||
|
<span>{option.label}</span>
|
||||||
|
<Icon
|
||||||
|
name={option.iconName}
|
||||||
|
color={hovered[index] ? option.iconColor : "black"}
|
||||||
|
size="normal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
10
frontend/lib/types/Options.ts
Normal file
10
frontend/lib/types/Options.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { iconList } from "@/lib/helpers/iconList";
|
||||||
|
|
||||||
|
import { Color } from "./Colors";
|
||||||
|
|
||||||
|
export type Option = {
|
||||||
|
label: string;
|
||||||
|
iconName: keyof typeof iconList;
|
||||||
|
onClick: () => void;
|
||||||
|
iconColor: Color;
|
||||||
|
};
|
@ -58,6 +58,7 @@
|
|||||||
"subtitle": "Talk to a language model about your uploaded data",
|
"subtitle": "Talk to a language model about your uploaded data",
|
||||||
"successfully_deleted": "Successfully deleted",
|
"successfully_deleted": "Successfully deleted",
|
||||||
"thinking": "Thinking...",
|
"thinking": "Thinking...",
|
||||||
|
"threads": "Threads",
|
||||||
"title": "Chat with {{brain}}",
|
"title": "Chat with {{brain}}",
|
||||||
"today": "Today",
|
"today": "Today",
|
||||||
"tooManyRequests": "You have exceeded the number of requests per day. To continue chatting, please upgrade your account or come back tomorrow.",
|
"tooManyRequests": "You have exceeded the number of requests per day. To continue chatting, please upgrade your account or come back tomorrow.",
|
||||||
|
@ -58,6 +58,7 @@
|
|||||||
"subtitle": "Habla con un modelo de lenguaje acerca de tus datos subidos",
|
"subtitle": "Habla con un modelo de lenguaje acerca de tus datos subidos",
|
||||||
"successfully_deleted": "Chat borrado correctamente",
|
"successfully_deleted": "Chat borrado correctamente",
|
||||||
"thinking": "Pensando...",
|
"thinking": "Pensando...",
|
||||||
|
"threads": "Hilos",
|
||||||
"title": "Conversa con {{brain}}",
|
"title": "Conversa con {{brain}}",
|
||||||
"today": "Hoy",
|
"today": "Hoy",
|
||||||
"tooManyRequests": "Has excedido el número de solicitudes por día. Para continuar chateando, por favor ingresa una clave de API de OpenAI en tu perfil o en el cerebro utilizado.",
|
"tooManyRequests": "Has excedido el número de solicitudes por día. Para continuar chateando, por favor ingresa una clave de API de OpenAI en tu perfil o en el cerebro utilizado.",
|
||||||
|
@ -58,6 +58,7 @@
|
|||||||
"subtitle": "Parlez à un modèle linguistique de vos données téléchargées",
|
"subtitle": "Parlez à un modèle linguistique de vos données téléchargées",
|
||||||
"successfully_deleted": "Chat supprimé avec succès",
|
"successfully_deleted": "Chat supprimé avec succès",
|
||||||
"thinking": "Réflexion...",
|
"thinking": "Réflexion...",
|
||||||
|
"threads": "Fils",
|
||||||
"title": "Discuter avec {{brain}}",
|
"title": "Discuter avec {{brain}}",
|
||||||
"today": "Aujourd'hui",
|
"today": "Aujourd'hui",
|
||||||
"tooManyRequests": "Vous avez dépassé le nombre de requêtes par jour. Pour continuer à discuter, veuillez entrer une clé d'API OpenAI dans votre profil ou dans le cerveau utilisé.",
|
"tooManyRequests": "Vous avez dépassé le nombre de requêtes par jour. Pour continuer à discuter, veuillez entrer une clé d'API OpenAI dans votre profil ou dans le cerveau utilisé.",
|
||||||
|
@ -58,6 +58,7 @@
|
|||||||
"subtitle": "Converse com um modelo de linguagem sobre seus dados enviados",
|
"subtitle": "Converse com um modelo de linguagem sobre seus dados enviados",
|
||||||
"successfully_deleted": "Conversa excluída com sucesso",
|
"successfully_deleted": "Conversa excluída com sucesso",
|
||||||
"thinking": "Pensando...",
|
"thinking": "Pensando...",
|
||||||
|
"threads": "Tópicos",
|
||||||
"title": "Converse com {{brain}}",
|
"title": "Converse com {{brain}}",
|
||||||
"today": "Hoje",
|
"today": "Hoje",
|
||||||
"tooManyRequests": "Você excedeu o número de solicitações por dia. Para continuar conversando, insira uma chave de API da OpenAI em seu perfil ou no cérebro utilizado.",
|
"tooManyRequests": "Você excedeu o número de solicitações por dia. Para continuar conversando, insira uma chave de API da OpenAI em seu perfil ou no cérebro utilizado.",
|
||||||
|
@ -58,6 +58,7 @@
|
|||||||
"subtitle": "Общайтесь с языковой моделью о ваших загруженных данных",
|
"subtitle": "Общайтесь с языковой моделью о ваших загруженных данных",
|
||||||
"successfully_deleted": "Успешно удалено",
|
"successfully_deleted": "Успешно удалено",
|
||||||
"thinking": "Думаю...",
|
"thinking": "Думаю...",
|
||||||
|
"threads": "Обсуждения",
|
||||||
"title": "Чат с {{brain}}",
|
"title": "Чат с {{brain}}",
|
||||||
"today": "Сегодня",
|
"today": "Сегодня",
|
||||||
"tooManyRequests": "Вы превысили количество запросов в день. Чтобы продолжить чат, введите ключ OpenAI API в вашем профиле или в использованном мозге.",
|
"tooManyRequests": "Вы превысили количество запросов в день. Чтобы продолжить чат, введите ключ OpenAI API в вашем профиле или в использованном мозге.",
|
||||||
|
@ -58,6 +58,7 @@
|
|||||||
"subtitle": "与语言模型讨论您上传的数据",
|
"subtitle": "与语言模型讨论您上传的数据",
|
||||||
"successfully_deleted": "成功删除",
|
"successfully_deleted": "成功删除",
|
||||||
"thinking": "思考中…",
|
"thinking": "思考中…",
|
||||||
|
"threads": "讨论",
|
||||||
"title": "与 {{brain}} 聊天",
|
"title": "与 {{brain}} 聊天",
|
||||||
"today": "今天",
|
"today": "今天",
|
||||||
"tooManyRequests": "您已超过每天的请求次数。想要继续聊天,请在您的个人资料中或为当前大脑配置 OpenAI API 密钥。",
|
"tooManyRequests": "您已超过每天的请求次数。想要继续聊天,请在您的个人资料中或为当前大脑配置 OpenAI API 密钥。",
|
||||||
|
Loading…
Reference in New Issue
Block a user