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:
Antoine Dewez 2024-02-16 16:28:27 -08:00 committed by GitHub
parent ef6ee14440
commit 6ae529f614
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 322 additions and 192 deletions

View File

@ -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

View File

@ -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;
}
}
} }
} }

View File

@ -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>
); );
}; };

View File

@ -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 />

View File

@ -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>
);
};

View File

@ -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%);
}
} }

View File

@ -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>
); );

View File

@ -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 {

View File

@ -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>
</>
);
};

View File

@ -8,4 +8,5 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: Spacings.$spacing02; gap: Spacings.$spacing02;
position: relative;
} }

View File

@ -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>

View File

@ -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)}>

View File

@ -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;
}
}
}

View 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>
);
};

View 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;
};

View File

@ -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.",

View File

@ -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.",

View File

@ -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é.",

View File

@ -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.",

View File

@ -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 в вашем профиле или в использованном мозге.",

View File

@ -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 密钥。",