feat(panel): added (#2088)

# 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-01-25 12:44:24 -08:00 committed by GitHub
parent 63c27975f5
commit 66ffc6b9aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 422 additions and 87 deletions

View File

@ -11,7 +11,7 @@ const BrainsManagement = (): JSX.Element => {
const { t } = useTranslation("chat");
return (
<div className="flex flex-col flex-1 bg-highlight">
<div className="flex flex-col flex-1 bg-white">
<div className="w-full h-full p-6 flex flex-col flex-1 overflow-auto">
<div className="w-full mb-10">
<div className="flex flex-row justify-center items-center gap-2">

View File

@ -0,0 +1,7 @@
@use "@/styles/Spacings.module.scss";
.data_panel_wrapper {
display: flex;
flex-direction: column;
row-gap: Spacings.$spacing05;
}

View File

@ -0,0 +1,33 @@
"use client";
import { useEffect, useState } from "react";
import { useChatContext } from "@/lib/context";
import { CloseBrain } from "@/lib/types/MessageMetadata";
import styles from "./DataPanel.module.scss";
import RelatedBrains from "./components/RelatedBrains/RelatedBrains";
const DataPanel = (): JSX.Element => {
const { messages } = useChatContext();
const [lastMessageRelatedBrain, setLastMessageRelatedBrain] = useState<
CloseBrain[]
>([]);
useEffect(() => {
if (messages.length > 0) {
const lastMessage = messages[messages.length - 1];
if (lastMessage?.metadata?.close_brains) {
setLastMessageRelatedBrain(lastMessage.metadata.close_brains);
}
}
}, [lastMessageRelatedBrain, messages]);
return (
<div className={styles.data_panel_wrapper}>
<RelatedBrains closeBrains={lastMessageRelatedBrain} />
</div>
);
};
export default DataPanel;

View File

@ -0,0 +1,50 @@
@use "@/styles/Colors.module.scss";
@use "@/styles/Spacings.module.scss";
.close_brains_wrapper {
display: flex;
flex-direction: column;
padding-bottom: Spacings.$spacing03;
gap: Spacings.$spacing01;
.brain_line {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding-left: Spacings.$spacing03;
padding-right: Spacings.$spacing05;
overflow: hidden;
.left {
display: flex;
align-items: center;
gap: Spacings.$spacing03;
.copy_icon {
visibility: hidden;
}
.brain_name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&.current {
color: Colors.$primary;
}
}
}
.similarity_score {
cursor: help;
}
&:hover {
.left .copy_icon {
visibility: visible;
cursor: pointer;
}
}
}
}

View File

@ -0,0 +1,94 @@
import { useEffect, useState } from "react";
import { FoldableSection } from "@/lib/components/ui/FoldableSection/FoldableSection";
import Icon from "@/lib/components/ui/Icon/Icon";
import { useChatContext } from "@/lib/context";
import { CloseBrain } from "@/lib/types/MessageMetadata";
import styles from "./RelatedBrains.module.scss";
interface RelatedBrainsProps {
closeBrains: CloseBrain[];
}
interface CloseBrainProps {
color: string;
isCurrentBrain: boolean;
}
const RelatedBrains = ({ closeBrains }: RelatedBrainsProps): JSX.Element => {
const [closeBrainsProps, setCloseBrainProps] = useState<CloseBrainProps[]>(
[]
);
const { messages } = useChatContext();
const lerp = (start: number, end: number, t: number): number => {
return start * (1 - t) + end * t;
};
useEffect(() => {
const newProps = closeBrains.map((brain) => {
const t = Math.pow(brain.similarity, 2);
const r = Math.round(lerp(211, 138, t));
const g = Math.round(lerp(211, 43, t));
const b = Math.round(lerp(211, 226, t));
const isCurrentBrain =
brain.name === messages[messages.length - 1].brain_name;
return { color: `rgb(${r}, ${g}, ${b})`, isCurrentBrain: isCurrentBrain };
});
setCloseBrainProps(newProps);
}, [closeBrains, messages]);
if (closeBrains.length === 0) {
return <></>;
}
return (
<FoldableSection
label="Related Brains (Beta)"
icon="brain"
foldedByDefault={true}
>
<div className={styles.close_brains_wrapper}>
{closeBrains.map((brain, index) => (
<div className={styles.brain_line} key={index}>
<div className={styles.left}>
<div className={styles.copy_icon}>
<Icon
name="copy"
size="normal"
color="black"
handleHover={true}
onClick={() =>
void navigator.clipboard.writeText("@" + brain.name)
}
></Icon>
</div>
<p
className={`
${styles.brain_name ?? ""}
${
closeBrainsProps[index]?.isCurrentBrain
? styles.current ?? ""
: ""
}
`}
>
@{brain.name}
</p>
</div>
<div
className={styles.similarity_score}
title="Similarity score"
style={{ color: closeBrainsProps[index]?.color }}
>
{Math.round(brain.similarity * 100)}
</div>
</div>
))}
</div>
</FoldableSection>
);
};
export default RelatedBrains;

View File

@ -83,7 +83,7 @@ export const useChat = () => {
}
const chatQuestion: ChatQuestion = {
model,
model, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
question,
temperature: temperature,
max_tokens: maxTokens,
@ -93,7 +93,6 @@ export const useChat = () => {
callback?.();
await addStreamQuestion(currentChatId, chatQuestion);
} catch (error) {
console.error({ error });

View File

@ -0,0 +1,20 @@
@use "@/styles/Colors.module.scss";
@use "@/styles/Spacings.module.scss";
.chat_page_container {
display: flex;
flex: 1 1 0%;
background-color: Colors.$white;
padding: Spacings.$spacing06;
display: flex;
gap: Spacings.$spacing09;
&.feeding {
background-color: Colors.$chat-bg-gray;
}
.data_panel_wrapper {
height: 100%;
width: 30%;
}
}

View File

@ -1,41 +1,54 @@
"use client";
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
import { useDevice } from "@/lib/hooks/useDevice";
import { useCustomDropzone } from "@/lib/hooks/useDropzone";
import { cn } from "@/lib/utils";
import { ActionsBar } from "./components/ActionsBar";
import { ChatDialogueArea } from "./components/ChatDialogueArea/ChatDialogue";
import DataPanel from "./components/DataPanel/DataPanel";
import { useChatNotificationsSync } from "./hooks/useChatNotificationsSync";
import styles from "./page.module.scss";
const SelectedChatPage = (): JSX.Element => {
const { getRootProps } = useCustomDropzone();
const { shouldDisplayFeedCard } = useKnowledgeToFeedContext();
const { getRootProps } = useCustomDropzone();
const { shouldDisplayFeedCard } = useKnowledgeToFeedContext();
const { isMobile } = useDevice();
useChatNotificationsSync();
useChatNotificationsSync();
return (
<div className="flex flex-1">
<div
className={cn(
"flex flex-col flex-1 items-center justify-stretch w-full h-full overflow-hidden",
shouldDisplayFeedCard ? "bg-chat-bg-gray" : "bg-ivory",
"dark:bg-black transition-colors ease-out duration-500"
)}
data-testid="chat-page"
{...getRootProps()}
>
<div
className={`flex flex-col flex-1 w-full max-w-4xl h-full dark:shadow-primary/25 overflow-hidden p-2 sm:p-4 md:p-6 lg:p-8`}
>
<div className="flex flex-1 flex-col overflow-y-auto">
<ChatDialogueArea />
</div>
<ActionsBar />
</div>
</div>
return (
<div
className={`
${styles.chat_page_container ?? ""}
${shouldDisplayFeedCard ? styles.feeding ?? "" : ""}
`}
data-testid="chat-page"
{...getRootProps()}
>
<div
className={cn(
"flex flex-col flex-1 items-center justify-stretch w-full h-full overflow-hidden",
"dark:bg-black transition-colors ease-out duration-500"
)}
>
<div
className={`flex flex-col flex-1 w-full max-w-4xl h-full dark:shadow-primary/25 overflow-hidden`}
>
<div className="flex flex-1 flex-col overflow-y-auto">
<ChatDialogueArea />
</div>
<ActionsBar />
</div>
);
</div>
{!isMobile && (
<div className={styles.data_panel_wrapper}>
<DataPanel />
</div>
)}
</div>
);
};
export default SelectedChatPage;

View File

@ -1,5 +1,7 @@
import { UUID } from "crypto";
import { CloseBrain } from "@/lib/types/MessageMetadata";
export type ChatQuestion = {
model?: string;
question?: string;
@ -18,6 +20,7 @@ export type ChatMessage = {
brain_name?: string;
metadata?: {
sources?: [string];
close_brains?: CloseBrain[];
};
};

View File

@ -6,7 +6,7 @@
@use "@/styles/Variables.module.scss";
.search_page_container {
background-color: Colors.$ivory;
background-color: Colors.$white;
width: 100%;
height: 100%;
display: flex;

View File

@ -5,7 +5,7 @@ import { MenuControlButton } from "@/app/chat/[chatId]/components/ActionsBar/com
import { nonProtectedPaths } from "@/lib/config/routesConfig";
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 { BrainsManagementButton } from "./components/BrainsManagementButton";
import { DiscussionButton } from "./components/DiscussionButton";
@ -16,51 +16,58 @@ import { ProfileButton } from "./components/ProfileButton";
import { UpgradeToPlus } from "./components/UpgradeToPlus";
export const Menu = (): JSX.Element => {
const { isOpened } = useMenuContext();
const pathname = usePathname() ?? "";
const { isOpened } = useMenuContext();
const pathname = usePathname() ?? "";
if (nonProtectedPaths.includes(pathname)) {
return <></>;
}
if (nonProtectedPaths.includes(pathname)) {
return <></>;
}
const displayedOnPages = ["/chat", "/library", "/brains-management", "/search"];
const displayedOnPages = [
"/chat",
"/library",
"/brains-management",
"/search",
];
const isMenuDisplayed = displayedOnPages.some((page) =>
pathname.includes(page)
);
const isMenuDisplayed = displayedOnPages.some((page) =>
pathname.includes(page)
);
if (!isMenuDisplayed) {
return <></>;
}
if (!isMenuDisplayed) {
return <></>;
}
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
return (
<MotionConfig transition={{ mass: 1, damping: 10, duration: 0.2 }}>
<div
className="flex flex-col fixed sm:sticky top-0 left-0 h-full overflow-visible z-[1000] border-r border-black/10 dark:border-white/25 bg-highlight"
>
<AnimatedDiv>
<div className="flex flex-col flex-1 p-4 gap-4 h-full">
<MenuHeader />
<div className="flex flex-1 w-full">
<div className="w-full gap-2 flex flex-col">
<DiscussionButton />
<ExplorerButton />
<BrainsManagementButton />
<ParametersButton />
</div>
</div>
<div>
<UpgradeToPlus />
<ProfileButton />
</div>
</div>
</AnimatedDiv>
return (
<MotionConfig transition={{ mass: 1, damping: 10, duration: 0.2 }}>
<div className="flex flex-col fixed sm:sticky top-0 left-0 h-full overflow-visible z-[1000] border-r border-black/10 dark:border-white/25 bg-highlight">
<AnimatedDiv>
<div className="flex flex-col flex-1 p-4 gap-4 h-full">
<MenuHeader />
<div className="flex flex-1 w-full">
<div className="w-full gap-2 flex flex-col">
<DiscussionButton />
<ExplorerButton />
<BrainsManagementButton />
<ParametersButton />
</div>
</div>
<div className={`${styles.menu_control_button_wrapper} ${isOpened ? styles.shifted : ''}`}>
<MenuControlButton />
<div>
<UpgradeToPlus />
<ProfileButton />
</div>
</MotionConfig>
);
</div>
</AnimatedDiv>
</div>
<div
className={`${styles.menu_control_button_wrapper} ${
isOpened ? styles.shifted : ""
}`}
>
<MenuControlButton />
</div>
</MotionConfig>
);
};

View File

@ -21,7 +21,7 @@ export const AnimatedDiv = ({ children }: AnimatedDivProps): JSX.Element => {
? "10px 10px 16px rgba(0, 0, 0, 0)"
: "10px 10px 16px rgba(0, 0, 0, 0.5)",
}}
className={"overflow-hidden flex flex-col flex-1 bg-white"}
className={"overflow-hidden flex flex-col flex-1 bg-grey"}
>
{children}
</motion.div>

View File

@ -0,0 +1,36 @@
@use "@/styles/Colors.module.scss";
@use "@/styles/Spacings.module.scss";
.foldable_section_wrapper {
display: flex;
flex-direction: column;
border-radius: 5px;
border: 1px dashed Colors.$light-grey;
overflow: hidden;
.header_wrapper {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
padding: Spacings.$spacing03;
.header_left {
display: flex;
align-items: center;
gap: Spacings.$spacing03;
overflow: hidden;
.header_title {
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
&:hover {
background-color: Colors.$lightest-grey;
}
}
}

View File

@ -0,0 +1,39 @@
import { useEffect, useState } from "react";
import { iconList } from "@/lib/helpers/iconList";
import styles from "./FoldableSection.module.scss";
import { Icon } from "../Icon/Icon";
interface FoldableSectionProps {
label: string;
icon: keyof typeof iconList;
children: React.ReactNode;
foldedByDefault?: boolean;
}
export const FoldableSection = (props: FoldableSectionProps): JSX.Element => {
const [folded, setFolded] = useState<boolean>(false);
useEffect(() => {
setFolded(props.foldedByDefault ?? false);
}, [props.foldedByDefault]);
return (
<div className={styles.foldable_section_wrapper}>
<div className={styles.header_wrapper} onClick={() => setFolded(!folded)}>
<div className={styles.header_left}>
<Icon name={props.icon} size="normal" color="black" />
<p className={styles.header_title}>{props.label}</p>
</div>
<Icon
name={folded ? "chevronDown" : "chevronRight"}
size="large"
color="accent"
/>
</div>
{!folded && <div>{props.children}</div>}
</div>
);
};

View File

@ -25,7 +25,7 @@
color: Colors.$black;
&.hovered {
color: Colors.$accent;
color: Colors.$primary;
}
}

View File

@ -1,3 +1,4 @@
import { useEffect, useState } from "react";
import { IconType } from "react-icons/lib";
import { iconList } from "@/lib/helpers/iconList";
@ -13,6 +14,8 @@ interface IconProps {
disabled?: boolean;
classname?: string;
hovered?: boolean;
handleHover?: boolean;
onClick?: () => void;
}
export const Icon = ({
@ -22,9 +25,18 @@ export const Icon = ({
disabled,
classname,
hovered,
handleHover,
onClick,
}: IconProps): JSX.Element => {
const [iconHovered, setIconHovered] = useState(false);
const IconComponent: IconType = iconList[name];
useEffect(() => {
if (!handleHover) {
setIconHovered(!!hovered);
}
}, [hovered, handleHover]);
return (
<IconComponent
className={`
@ -32,8 +44,11 @@ export const Icon = ({
${styles[size] ?? ""}
${styles[color] ?? ""}
${disabled ? styles.disabled ?? "" : ""}
${hovered ? styles.hovered ?? "" : ""}
${iconHovered ? styles.hovered ?? "" : ""}
`}
onMouseEnter={() => handleHover && setIconHovered(true)}
onMouseLeave={() => handleHover && setIconHovered(false)}
onClick={onClick}
/>
);
};

View File

@ -10,12 +10,7 @@
background-color: Colors.$white;
padding: Spacings.$spacing05;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
&:hover {
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1),
0 8px 10px -6px rgb(0 0 0 / 0.1);
}
border: 1px solid Colors.$light-grey;
.search_icon {
width: IconSizes.$big;

View File

@ -13,6 +13,6 @@
color: Colors.$black;
&.hovered {
color: Colors.$accent;
color: Colors.$primary;
}
}

View File

@ -1,9 +1,20 @@
import { AiOutlineLoading3Quarters } from "react-icons/ai";
import { IconType } from "react-icons/lib";
import { LuPlusCircle, LuSearch } from "react-icons/lu";
import {
LuBrain,
LuChevronDown,
LuChevronRight,
LuCopy,
LuPlusCircle,
LuSearch,
} from "react-icons/lu";
export const iconList: { [name: string]: IconType } = {
add: LuPlusCircle,
brain: LuBrain,
chevronDown: LuChevronDown,
chevronRight: LuChevronRight,
copy: LuCopy,
loader: AiOutlineLoading3Quarters,
search: LuSearch,
};

View File

@ -0,0 +1,9 @@
export interface CloseBrain {
id: string;
similarity: number;
name: string;
}
export interface MessageMetadata {
closeBrains: CloseBrain[];
}

View File

@ -1,8 +1,12 @@
$white: #FFFFFF;
$black: #11243E;
$primary: #6142D4;
$secondary: #F3ECFF;
$tertiary: #F6F4FF;
$accent: #13ABBA;
$highlight: #FAFAFA;
$ivory: #FCFAF6,
$white: #ffffff;
$lightest-grey: #f5f5f5;
$light-grey: #d3d3d3;
$normal-grey: #c8c8c8;
$black: #11243e;
$primary: #6142d4;
$secondary: #f3ecff;
$tertiary: #f6f4ff;
$accent: #13abba;
$highlight: #fafafa;
$ivory: #fcfaf6;
$chat-bg-gray: #d9d9d9;