mirror of
https://github.com/QuivrHQ/quivr.git
synced 2024-12-14 17:03:29 +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 \
|
||||
curl \
|
||||
git \
|
||||
poppler-utils \
|
||||
tesseract-ocr \
|
||||
autoconf \
|
||||
automake \
|
||||
build-essential \
|
||||
libtool \
|
||||
python-dev \
|
||||
build-essential \
|
||||
@ -23,6 +26,8 @@ RUN apt-get clean && apt-get update && apt-get install -y \
|
||||
poppler-utils \
|
||||
tesseract-ocr \
|
||||
libreoffice \
|
||||
libpq-dev \
|
||||
gcc \
|
||||
pandoc && \
|
||||
rm -rf /var/lib/apt/lists/* && apt-get clean
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
@use "@/styles/ScreenSizes.module.scss";
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
@use "@/styles/ZIndexes.module.scss";
|
||||
|
||||
.brain_item_wrapper {
|
||||
padding-inline: Spacings.$spacing05;
|
||||
@ -16,6 +17,8 @@
|
||||
border: 1px solid Colors.$lightest-black;
|
||||
border-radius: Radius.$normal;
|
||||
padding-block: Spacings.$spacing03;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
|
||||
&:hover {
|
||||
border-color: Colors.$primary;
|
||||
@ -53,31 +56,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.options_menu {
|
||||
.options_modal {
|
||||
position: absolute;
|
||||
background-color: Colors.$highlight;
|
||||
border-radius: Radius.$normal;
|
||||
right: Spacings.$spacing07;
|
||||
border-radius: Radius.$normal;
|
||||
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;
|
||||
}
|
||||
}
|
||||
right: Spacings.$spacing02;
|
||||
top: Spacings.$spacing08;
|
||||
z-index: ZIndexes.$modal;
|
||||
padding-bottom: Spacings.$spacing01;
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,10 @@ import { DeleteOrUnsubscribeConfirmationModal } from "@/app/studio/[brainId]/com
|
||||
import { useBrainManagementTabs } from "@/app/studio/[brainId]/components/BrainManagementTabs/hooks/useBrainManagementTabs";
|
||||
import { getBrainPermissions } from "@/app/studio/[brainId]/components/BrainManagementTabs/utils/getBrainPermissions";
|
||||
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 { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";
|
||||
import { Option } from "@/lib/types/Options";
|
||||
|
||||
import styles from "./BrainItem.module.scss";
|
||||
|
||||
@ -17,34 +19,49 @@ type BrainItemProps = {
|
||||
|
||||
export const BrainItem = ({ brain, even }: BrainItemProps): JSX.Element => {
|
||||
const [optionsOpened, setOptionsOpened] = useState<boolean>(false);
|
||||
const [deleteHovered, setDeleteHovered] = useState<boolean>(false);
|
||||
const [editHovered, setEditHovered] = useState<boolean>(false);
|
||||
|
||||
const {
|
||||
handleUnsubscribeOrDeleteBrain,
|
||||
isDeleteOrUnsubscribeModalOpened,
|
||||
setIsDeleteOrUnsubscribeModalOpened,
|
||||
isDeleteOrUnsubscribeRequestPending,
|
||||
} = useBrainManagementTabs(brain.id);
|
||||
|
||||
const { allBrains } = useBrainContext();
|
||||
|
||||
const { isOwnedByCurrentUser } = getBrainPermissions({
|
||||
brainId: brain.id,
|
||||
userAccessibleBrains: allBrains,
|
||||
});
|
||||
|
||||
const iconRef = 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(() => {
|
||||
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 () => {
|
||||
@ -53,68 +70,50 @@ export const BrainItem = ({ brain, even }: BrainItemProps): JSX.Element => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
<>
|
||||
<div
|
||||
className={`
|
||||
${even ? styles.even : styles.odd}
|
||||
${styles.brain_item_wrapper}
|
||||
`}
|
||||
>
|
||||
<Link className={styles.brain_info_wrapper} href={`/studio/${brain.id}`}>
|
||||
<span className={styles.name}>{brain.name}</span>
|
||||
<span className={styles.description}>{brain.description}</span>
|
||||
</Link>
|
||||
|
||||
<div>
|
||||
<div
|
||||
onClick={(event: React.MouseEvent<HTMLElement>) => {
|
||||
event.stopPropagation();
|
||||
setOptionsOpened(!optionsOpened);
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
className={styles.brain_info_wrapper}
|
||||
href={`/studio/${brain.id}`}
|
||||
>
|
||||
<Icon name="options" size="normal" color="black" handleHover={true} />
|
||||
</div>
|
||||
{optionsOpened && (
|
||||
<div className={styles.options_menu} ref={optionsRef}>
|
||||
<div
|
||||
className={styles.option}
|
||||
onClick={() => setIsDeleteOrUnsubscribeModalOpened(true)}
|
||||
onMouseEnter={() => setDeleteHovered(true)}
|
||||
onMouseLeave={() => setDeleteHovered(false)}
|
||||
>
|
||||
<span>Delete</span>
|
||||
<Icon
|
||||
name="delete"
|
||||
size="normal"
|
||||
color="dangerous"
|
||||
hovered={deleteHovered}
|
||||
/>
|
||||
</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>
|
||||
<span className={styles.name}>{brain.name}</span>
|
||||
<span className={styles.description}>{brain.description}</span>
|
||||
</Link>
|
||||
|
||||
<div>
|
||||
<div
|
||||
ref={iconRef}
|
||||
onClick={(event: React.MouseEvent<HTMLElement>) => {
|
||||
event.nativeEvent.stopImmediatePropagation();
|
||||
setOptionsOpened(!optionsOpened);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="options"
|
||||
size="normal"
|
||||
color="black"
|
||||
handleHover={true}
|
||||
/>
|
||||
</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>
|
||||
<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 { AnimatedDiv } from "./components/AnimationDiv";
|
||||
import { DiscussionButton } from "./components/DiscussionButton/DiscussionButton";
|
||||
import { HistoryButton } from "./components/HistoryButton/HistoryButton";
|
||||
import { HomeButton } from "./components/HomeButton/HomeButton";
|
||||
import { ProfileButton } from "./components/ProfileButton/ProfileButton";
|
||||
import { StudioButton } from "./components/StudioButton/StudioButton";
|
||||
import { ThreadsButton } from "./components/ThreadsButton/ThreadsButton";
|
||||
import { UpgradeToPlusButton } from "./components/UpgradeToPlusButton/UpgradeToPlusButton";
|
||||
|
||||
export const Menu = (): JSX.Element => {
|
||||
@ -61,7 +61,7 @@ export const Menu = (): JSX.Element => {
|
||||
<DiscussionButton />
|
||||
<HomeButton />
|
||||
<StudioButton />
|
||||
<HistoryButton />
|
||||
<ThreadsButton />
|
||||
</div>
|
||||
<div className={styles.block}>
|
||||
<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;
|
||||
color: Colors.$dark-grey;
|
||||
font-size: Typography.$small;
|
||||
max-height: 200px;
|
||||
max-height: 300px;
|
||||
overflow-y: scroll;
|
||||
display: flex;
|
||||
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 { useChatsContext } from "@/lib/context/ChatsProvider/hooks/useChatsContext";
|
||||
|
||||
import { ChatsSection } from "./ChatsSection/ChatsSection";
|
||||
import styles from "./HistoryButton.module.scss";
|
||||
import styles from "./ThreadsButton.module.scss";
|
||||
import { ThreadsSection } from "./ThreadsSection/ThreadsSection";
|
||||
import { isWithinLast30Days, isWithinLast7Days, isYesterday } from "./utils";
|
||||
|
||||
export const HistoryButton = (): JSX.Element => {
|
||||
export const ThreadsButton = (): JSX.Element => {
|
||||
const [canScrollDown, setCanScrollDown] = useState<boolean>(false);
|
||||
const { allChats } = useChatsContext();
|
||||
const { t } = useTranslation("chat");
|
||||
@ -49,10 +49,10 @@ export const HistoryButton = (): JSX.Element => {
|
||||
|
||||
return (
|
||||
<FoldableSection
|
||||
label={t("history")}
|
||||
label={t("threads")}
|
||||
icon="history"
|
||||
foldedByDefault={true}
|
||||
hideBorderIfUnfolded={true}
|
||||
hideBorder={true}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
@ -60,10 +60,10 @@ export const HistoryButton = (): JSX.Element => {
|
||||
${canScrollDown ? styles.fade_out : ""}
|
||||
`}
|
||||
>
|
||||
<ChatsSection chats={todayChats} title={t("today")} />
|
||||
<ChatsSection chats={yesterdayChats} title={t("yesterday")} />
|
||||
<ChatsSection chats={last7DaysChats} title={t("last7Days")} />
|
||||
<ChatsSection chats={last30DaysChats} title={t("last30Days")} />
|
||||
<ThreadsSection chats={todayChats} title={t("today")} />
|
||||
<ThreadsSection chats={yesterdayChats} title={t("yesterday")} />
|
||||
<ThreadsSection chats={last7DaysChats} title={t("last7Days")} />
|
||||
<ThreadsSection chats={last30DaysChats} title={t("last30Days")} />
|
||||
</div>
|
||||
</FoldableSection>
|
||||
);
|
@ -2,8 +2,9 @@
|
||||
@use "@/styles/Radius.module.scss";
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
@use "@/styles/ZIndexes.module.scss";
|
||||
|
||||
.chat_item_wrapper {
|
||||
.thread_item_wrapper {
|
||||
color: Colors.$black;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@ -11,7 +12,7 @@
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
.edit_chat_name {
|
||||
.edit_thread_name {
|
||||
@include Typography.EllipsisOverflow;
|
||||
|
||||
color: Colors.$black;
|
||||
@ -28,7 +29,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.chat_item_name {
|
||||
.thread_item_name {
|
||||
@include Typography.EllipsisOverflow;
|
||||
|
||||
&:hover {
|
||||
@ -36,10 +37,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.icons_wrapper {
|
||||
visibility: hidden;
|
||||
display: flex;
|
||||
gap: Spacings.$spacing02;
|
||||
.options_modal {
|
||||
position: absolute;
|
||||
right: Spacings.$spacing02;
|
||||
z-index: ZIndexes.$modal;
|
||||
}
|
||||
|
||||
&: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;
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing02;
|
||||
position: relative;
|
||||
}
|
@ -1,14 +1,14 @@
|
||||
import { ChatEntity } from "@/app/chat/[chatId]/types";
|
||||
|
||||
import { ChatHistoryItem } from "./ChatHistoryItem/ChatHistoryItem";
|
||||
import styles from "./ChatsSection.module.scss";
|
||||
import { ThreadItem } from "./ThreadItem/ThreadItem";
|
||||
import styles from "./ThreadsSection.module.scss";
|
||||
|
||||
type ChatSectionProps = {
|
||||
chats: ChatEntity[];
|
||||
title: string;
|
||||
};
|
||||
|
||||
export const ChatsSection = (props: ChatSectionProps): JSX.Element => {
|
||||
export const ThreadsSection = (props: ChatSectionProps): JSX.Element => {
|
||||
if (props.chats.length === 0) {
|
||||
return <></>;
|
||||
}
|
||||
@ -18,7 +18,7 @@ export const ChatsSection = (props: ChatSectionProps): JSX.Element => {
|
||||
<div>{props.title}</div>
|
||||
<div className={styles.chats_wrapper}>
|
||||
{props.chats.map((chat) => (
|
||||
<ChatHistoryItem key={chat.chat_id} chatHistoryItem={chat} />
|
||||
<ThreadItem key={chat.chat_id} chatHistoryItem={chat} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
@ -11,7 +11,7 @@ interface FoldableSectionProps {
|
||||
icon: keyof typeof iconList;
|
||||
children: React.ReactNode;
|
||||
foldedByDefault?: boolean;
|
||||
hideBorderIfUnfolded?: boolean;
|
||||
hideBorder?: boolean;
|
||||
}
|
||||
|
||||
export const FoldableSection = (props: FoldableSectionProps): JSX.Element => {
|
||||
@ -26,7 +26,7 @@ export const FoldableSection = (props: FoldableSectionProps): JSX.Element => {
|
||||
className={`
|
||||
${styles.foldable_section_wrapper}
|
||||
${!folded ? styles.unfolded : ""}
|
||||
${props.hideBorderIfUnfolded && folded ? styles.hide_border : ""}
|
||||
${props.hideBorder ? styles.hide_border : ""}
|
||||
`}
|
||||
>
|
||||
<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",
|
||||
"successfully_deleted": "Successfully deleted",
|
||||
"thinking": "Thinking...",
|
||||
"threads": "Threads",
|
||||
"title": "Chat with {{brain}}",
|
||||
"today": "Today",
|
||||
"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",
|
||||
"successfully_deleted": "Chat borrado correctamente",
|
||||
"thinking": "Pensando...",
|
||||
"threads": "Hilos",
|
||||
"title": "Conversa con {{brain}}",
|
||||
"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.",
|
||||
|
@ -58,6 +58,7 @@
|
||||
"subtitle": "Parlez à un modèle linguistique de vos données téléchargées",
|
||||
"successfully_deleted": "Chat supprimé avec succès",
|
||||
"thinking": "Réflexion...",
|
||||
"threads": "Fils",
|
||||
"title": "Discuter avec {{brain}}",
|
||||
"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é.",
|
||||
|
@ -58,6 +58,7 @@
|
||||
"subtitle": "Converse com um modelo de linguagem sobre seus dados enviados",
|
||||
"successfully_deleted": "Conversa excluída com sucesso",
|
||||
"thinking": "Pensando...",
|
||||
"threads": "Tópicos",
|
||||
"title": "Converse com {{brain}}",
|
||||
"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.",
|
||||
|
@ -58,6 +58,7 @@
|
||||
"subtitle": "Общайтесь с языковой моделью о ваших загруженных данных",
|
||||
"successfully_deleted": "Успешно удалено",
|
||||
"thinking": "Думаю...",
|
||||
"threads": "Обсуждения",
|
||||
"title": "Чат с {{brain}}",
|
||||
"today": "Сегодня",
|
||||
"tooManyRequests": "Вы превысили количество запросов в день. Чтобы продолжить чат, введите ключ OpenAI API в вашем профиле или в использованном мозге.",
|
||||
|
@ -58,6 +58,7 @@
|
||||
"subtitle": "与语言模型讨论您上传的数据",
|
||||
"successfully_deleted": "成功删除",
|
||||
"thinking": "思考中…",
|
||||
"threads": "讨论",
|
||||
"title": "与 {{brain}} 聊天",
|
||||
"today": "今天",
|
||||
"tooManyRequests": "您已超过每天的请求次数。想要继续聊天,请在您的个人资料中或为当前大脑配置 OpenAI API 密钥。",
|
||||
|
Loading…
Reference in New Issue
Block a user