feat: add chat view new design (#1897)

Issue: https://github.com/StanGirard/quivr/issues/1888

- Add Spinner when history is loading
- Change chat messages fetching logic
- Add cha view new design

Demo:



https://github.com/StanGirard/quivr/assets/63923024/c4341ccf-bacd-4720-9aa1-127dd557a75c
This commit is contained in:
Mamadou DICKO 2023-12-14 16:22:09 +01:00 committed by GitHub
parent 992c67a2b9
commit e2c1a027b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 140 additions and 68 deletions

View File

@ -4,6 +4,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { PropsWithChildren, useEffect } from "react"; import { PropsWithChildren, useEffect } from "react";
import { Menu } from "@/lib/components/Menu/Menu"; import { Menu } from "@/lib/components/Menu/Menu";
import { useOutsideClickListener } from "@/lib/components/Menu/hooks/useOutsideClickListener";
import { NotificationBanner } from "@/lib/components/NotificationBanner"; import { NotificationBanner } from "@/lib/components/NotificationBanner";
import { BrainProvider } from "@/lib/context"; import { BrainProvider } from "@/lib/context";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext"; import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
@ -18,6 +19,7 @@ import "../lib/config/LocaleConfig/i18n";
const App = ({ children }: PropsWithChildren): JSX.Element => { const App = ({ children }: PropsWithChildren): JSX.Element => {
const { fetchAllBrains, fetchDefaultBrain, fetchPublicPrompts } = const { fetchAllBrains, fetchDefaultBrain, fetchPublicPrompts } =
useBrainContext(); useBrainContext();
const { onClickOutside } = useOutsideClickListener();
const { session } = useSupabase(); const { session } = useSupabase();
usePageTracking(); usePageTracking();
@ -35,7 +37,9 @@ const App = ({ children }: PropsWithChildren): JSX.Element => {
<NotificationBanner /> <NotificationBanner />
<div className="relative h-full w-full flex justify-stretch items-stretch overflow-auto"> <div className="relative h-full w-full flex justify-stretch items-stretch overflow-auto">
<Menu /> <Menu />
<div className="flex-1">{children}</div> <div onClick={onClickOutside} className="flex-1">
{children}
</div>
<UpdateMetadata /> <UpdateMetadata />
</div> </div>
</div> </div>

View File

@ -20,17 +20,34 @@ import {
import SelectedChatPage from "../page"; import SelectedChatPage from "../page";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
vi.mock("@/lib/context/ChatProvider/ChatProvider", () => ({ vi.mock("@/lib/context/ChatProvider/ChatProvider", () => ({
ChatContext: ChatContextMock, ChatContext: ChatContextMock,
ChatProvider: ChatProviderMock, ChatProvider: ChatProviderMock,
})); }));
vi.mock("@/lib/context/ChatsProvider/hooks/useChatsContext", () => ({
useChatsContext: () => ({
allChats: [
{
chat_id: 1,
name: "Chat 1",
creation_time: new Date().toISOString(),
},
{
chat_id: 2,
name: "Chat 2",
creation_time: new Date().toISOString(),
},
],
deleteChat: vi.fn(),
setAllChats: vi.fn(),
setIsLoading: vi.fn(),
}),
}));
vi.mock("next/navigation", () => ({ vi.mock("next/navigation", () => ({
useRouter: () => ({ replace: vi.fn() }), useRouter: () => ({ replace: vi.fn() }),
useParams: () => ({ chatId: "1" }), useParams: () => ({ chatId: "1" }),
usePathname: () => "/chat/1",
})); }));
vi.mock("@/lib/context/SupabaseProvider/supabase-provider", () => ({ vi.mock("@/lib/context/SupabaseProvider/supabase-provider", () => ({
SupabaseContext: SupabaseContextMock, SupabaseContext: SupabaseContextMock,
})); }));
@ -38,13 +55,11 @@ vi.mock("@/lib/context/SupabaseProvider/supabase-provider", () => ({
vi.mock("@/lib/context/BrainProvider/brain-provider", () => ({ vi.mock("@/lib/context/BrainProvider/brain-provider", () => ({
BrainContext: BrainContextMock, BrainContext: BrainContextMock,
})); }));
vi.mock("@/lib/api/chat/useChatApi", () => ({ vi.mock("@/lib/api/chat/useChatApi", () => ({
useChatApi: () => ({ useChatApi: () => ({
getHistory: () => [], getHistory: () => [],
}), }),
})); }));
vi.mock("@/lib/hooks", async () => { vi.mock("@/lib/hooks", async () => {
const actual = await vi.importActual<typeof import("@/lib/hooks")>( const actual = await vi.importActual<typeof import("@/lib/hooks")>(
"@/lib/hooks" "@/lib/hooks"
@ -59,7 +74,6 @@ vi.mock("@/lib/hooks", async () => {
}), }),
}; };
}); });
vi.mock("@tanstack/react-query", async () => { vi.mock("@tanstack/react-query", async () => {
const actual = await vi.importActual<typeof import("@tanstack/react-query")>( const actual = await vi.importActual<typeof import("@tanstack/react-query")>(
"@tanstack/react-query" "@tanstack/react-query"
@ -72,7 +86,6 @@ vi.mock("@tanstack/react-query", async () => {
}), }),
}; };
}); });
vi.mock( vi.mock(
"../components/ActionsBar/components/ChatInput/components/ChatEditor/ChatEditor", "../components/ActionsBar/components/ChatInput/components/ChatEditor/ChatEditor",
() => ({ () => ({
@ -80,6 +93,9 @@ vi.mock(
}) })
); );
vi.mock("../hooks/useChatNotificationsSync", () => ({
useChatNotificationsSync: vi.fn(),
}));
describe("Chat page", () => { describe("Chat page", () => {
it("should render chat page correctly", () => { it("should render chat page correctly", () => {
const { getByTestId } = render( const { getByTestId } = render(

View File

@ -30,7 +30,7 @@ export const Button = forwardRef(
<div className="flex flex-row justify-between w-full items-center"> <div className="flex flex-row justify-between w-full items-center">
<div className="flex flex-row gap-2 items-center"> <div className="flex flex-row gap-2 items-center">
{startIcon} {startIcon}
<span className="hidden sm:block">{label}</span> <span>{label}</span>
</div> </div>
{endIcon} {endIcon}
</div> </div>

View File

@ -1,6 +1,6 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { act, render, screen } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import { import {
@ -21,7 +21,6 @@ vi.mock("@/lib/context/SupabaseProvider/supabase-provider", () => ({
import { ChatsList } from "../index"; import { ChatsList } from "../index";
const getChatsMock = vi.fn(() => []);
const queryClient = new QueryClient(); const queryClient = new QueryClient();
vi.mock("next/navigation", async () => { vi.mock("next/navigation", async () => {
@ -121,30 +120,4 @@ describe("ChatsList", () => {
const chatItems = screen.getAllByTestId("chats-list-item"); const chatItems = screen.getAllByTestId("chats-list-item");
expect(chatItems).toHaveLength(2); expect(chatItems).toHaveLength(2);
}); });
it("should call getChats when the component mounts", async () => {
vi.mock("@/lib/api/chat/useChatApi", () => ({
useChatApi: () => ({
getChats: () => getChatsMock(),
}),
}));
await act(() =>
render(
<QueryClientProvider client={queryClient}>
<KnowledgeToFeedProvider>
<ChatProviderMock>
<BrainProviderMock>
<SideBarProvider>
<ChatsList />
</SideBarProvider>
</BrainProviderMock>
</ChatProviderMock>
</KnowledgeToFeedProvider>
</QueryClientProvider>
)
);
expect(getChatsMock).toHaveBeenCalledTimes(1);
});
}); });

View File

@ -4,12 +4,8 @@ import { ChatHistory } from "@/lib/components/ChatHistory/ChatHistory";
import { useOnboarding } from "@/lib/hooks/useOnboarding"; import { useOnboarding } from "@/lib/hooks/useOnboarding";
import { WelcomeChat } from "./components/WelcomeChat"; import { WelcomeChat } from "./components/WelcomeChat";
import { useChatNotificationsSync } from "./hooks/useChatNotificationsSync";
import { useChatsList } from "./hooks/useChatsList";
export const ChatsList = (): JSX.Element => { export const ChatsList = (): JSX.Element => {
useChatsList();
useChatNotificationsSync();
const { shouldDisplayWelcomeChat } = useOnboarding(); const { shouldDisplayWelcomeChat } = useOnboarding();
return ( return (

View File

@ -7,9 +7,9 @@ import { useNotificationApi } from "@/lib/api/notification/useNotificationApi";
import { useChatContext } from "@/lib/context"; import { useChatContext } from "@/lib/context";
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext"; import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
import { getChatNotificationsQueryKey } from "../../../../../../../../../../../utils/getChatNotificationsQueryKey"; import { getChatNotificationsQueryKey } from "../utils/getChatNotificationsQueryKey";
import { getMessagesFromChatItems } from "../../../../../../../../../../../utils/getMessagesFromChatItems"; import { getMessagesFromChatItems } from "../utils/getMessagesFromChatItems";
import { getNotificationsFromChatItems } from "../../../../../../../../../../../utils/getNotificationsFromChatItems"; import { getNotificationsFromChatItems } from "../utils/getNotificationsFromChatItems";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useChatNotificationsSync = () => { export const useChatNotificationsSync = () => {

View File

@ -11,7 +11,7 @@ import { useToast } from "@/lib/hooks";
export const useChatsList = () => { export const useChatsList = () => {
const { t } = useTranslation(["chat"]); const { t } = useTranslation(["chat"]);
const { setAllChats } = useChatsContext(); const { setAllChats, setIsLoading } = useChatsContext();
const { publish } = useToast(); const { publish } = useToast();
const { getChats } = useChatApi(); const { getChats } = useChatApi();
@ -29,7 +29,7 @@ export const useChatsList = () => {
} }
}; };
const { data: chats } = useQuery({ const { data: chats, isLoading } = useQuery({
queryKey: [CHATS_DATA_KEY], queryKey: [CHATS_DATA_KEY],
queryFn: fetchAllChats, queryFn: fetchAllChats,
}); });
@ -37,4 +37,8 @@ export const useChatsList = () => {
useEffect(() => { useEffect(() => {
setAllChats(chats ?? []); setAllChats(chats ?? []);
}, [chats]); }, [chats]);
useEffect(() => {
setIsLoading(isLoading);
}, [isLoading]);
}; };

View File

@ -1,19 +1,31 @@
"use client"; "use client";
import { useMenuWidth } from "@/lib/components/Menu/hooks/useMenuWidth";
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext"; import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
import { useCustomDropzone } from "@/lib/hooks/useDropzone"; import { useCustomDropzone } from "@/lib/hooks/useDropzone";
import { cn } from "@/lib/utils";
import { ActionsBar } from "./components/ActionsBar"; import { ActionsBar } from "./components/ActionsBar";
import { ChatDialogueArea } from "./components/ChatDialogueArea/ChatDialogue"; import { ChatDialogueArea } from "./components/ChatDialogueArea/ChatDialogue";
import { useChatNotificationsSync } from "./hooks/useChatNotificationsSync";
import { useChatsList } from "./hooks/useChatsList";
const SelectedChatPage = (): JSX.Element => { const SelectedChatPage = (): JSX.Element => {
const { getRootProps } = useCustomDropzone(); const { getRootProps } = useCustomDropzone();
const { shouldDisplayFeedCard } = useKnowledgeToFeedContext(); const { shouldDisplayFeedCard } = useKnowledgeToFeedContext();
const { staticMenuWidth } = useMenuWidth();
useChatsList();
useChatNotificationsSync();
return ( return (
<div className="flex flex-1">
<div <div
className={`flex flex-col flex-1 items-center justify-stretch w-full h-full overflow-hidden ${shouldDisplayFeedCard ? "bg-chat-bg-gray" : "bg-white" className={cn(
} dark:bg-black transition-colors ease-out duration-500`} "flex flex-col flex-1 items-center justify-stretch w-full h-full overflow-hidden",
shouldDisplayFeedCard ? "bg-chat-bg-gray" : "bg-tertiary",
"dark:bg-black transition-colors ease-out duration-500"
)}
data-testid="chat-page" data-testid="chat-page"
{...getRootProps()} {...getRootProps()}
> >
@ -26,6 +38,8 @@ const SelectedChatPage = (): JSX.Element => {
<ActionsBar /> <ActionsBar />
</div> </div>
</div> </div>
<div className="h-full bg-highlight" style={{ width: staticMenuWidth }} />
</div>
); );
}; };

View File

@ -9,9 +9,10 @@ import {
isWithinLast7Days, isWithinLast7Days,
isYesterday, isYesterday,
} from "./utils"; } from "./utils";
import Spinner from "../ui/Spinner";
export const ChatHistory = (): JSX.Element => { export const ChatHistory = (): JSX.Element => {
const { allChats } = useChatsContext(); const { allChats, isLoading } = useChatsContext();
const { t } = useTranslation("chat"); const { t } = useTranslation("chat");
const todayChats = allChats.filter((chat) => const todayChats = allChats.filter((chat) =>
isToday(new Date(chat.creation_time)) isToday(new Date(chat.creation_time))
@ -26,6 +27,14 @@ export const ChatHistory = (): JSX.Element => {
isWithinLast30Days(new Date(chat.creation_time)) isWithinLast30Days(new Date(chat.creation_time))
); );
if (isLoading) {
return (
<div className="flex justify-center align-center">
<Spinner />
</div>
);
}
return ( return (
<div <div
data-testid="chats-list-items" data-testid="chats-list-items"

View File

@ -11,10 +11,12 @@ import { MenuHeader } from "./components/MenuHeader";
import { ParametersButton } from "./components/ParametersButton"; import { ParametersButton } from "./components/ParametersButton";
import { ProfileButton } from "./components/ProfileButton"; import { ProfileButton } from "./components/ProfileButton";
import { UpgradeToPlus } from "./components/UpgradeToPlus"; import { UpgradeToPlus } from "./components/UpgradeToPlus";
import { useMenuWidth } from "./hooks/useMenuWidth";
export const Menu = (): JSX.Element => { export const Menu = (): JSX.Element => {
const pathname = usePathname() ?? ""; const pathname = usePathname() ?? "";
const { staticMenuWidth } = useMenuWidth();
if (nonProtectedPaths.includes(pathname)) { if (nonProtectedPaths.includes(pathname)) {
return <></>; return <></>;
} }
@ -27,7 +29,10 @@ export const Menu = (): JSX.Element => {
return ( return (
<MotionConfig transition={{ mass: 1, damping: 10, duration: 0.2 }}> <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-30 border-r border-black/10 dark:border-white/25 bg-white dark:bg-black"> <div
className="flex flex-col fixed sm:sticky top-0 left-0 h-full overflow-visible z-30 border-r border-black/10 dark:border-white/25 bg-highlight bg-highlight"
style={{ width: staticMenuWidth }}
>
<AnimatedDiv> <AnimatedDiv>
<div className="flex flex-col flex-1 p-4 gap-4"> <div className="flex flex-col flex-1 p-4 gap-4">
<MenuHeader /> <MenuHeader />

View File

@ -2,25 +2,28 @@ import { motion } from "framer-motion";
import { useSideBarContext } from "@/lib/context/SidebarProvider/hooks/useSideBarContext"; import { useSideBarContext } from "@/lib/context/SidebarProvider/hooks/useSideBarContext";
import { useMenuWidth } from "../hooks/useMenuWidth";
type AnimatedDivProps = { type AnimatedDivProps = {
children: React.ReactNode; children: React.ReactNode;
}; };
export const AnimatedDiv = ({ children }: AnimatedDivProps): JSX.Element => { export const AnimatedDiv = ({ children }: AnimatedDivProps): JSX.Element => {
const { isOpened } = useSideBarContext(); const { isOpened } = useSideBarContext();
const { OPENED_MENU_WIDTH } = useMenuWidth();
return ( return (
<motion.div <motion.div
initial={{ initial={{
width: isOpened ? "260px" : "0px", width: isOpened ? OPENED_MENU_WIDTH : "0px",
}} }}
animate={{ animate={{
width: isOpened ? "260px" : "0px", width: isOpened ? OPENED_MENU_WIDTH : 0,
opacity: isOpened ? 1 : 0.5, opacity: isOpened ? 1 : 0.5,
boxShadow: isOpened boxShadow: isOpened
? "10px 10px 16px rgba(0, 0, 0, 0)" ? "10px 10px 16px rgba(0, 0, 0, 0)"
: "10px 10px 16px rgba(0, 0, 0, 0.5)", : "10px 10px 16px rgba(0, 0, 0, 0.5)",
}} }}
className={"overflow-hidden flex flex-col flex-1"} className={"overflow-hidden flex flex-col flex-1 bg-white"}
> >
{children} {children}
</motion.div> </motion.div>

View File

@ -0,0 +1,23 @@
import { usePathname } from "next/navigation";
import { useDevice } from "@/lib/hooks/useDevice";
const OPENED_MENU_WIDTH = 260;
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useMenuWidth = () => {
const pathname = usePathname() ?? "";
const { isMobile } = useDevice();
const isStaticSideBarActivated = !isMobile;
const shouldAddFixedPadding = pathname.startsWith("/chat");
const staticMenuWidth =
shouldAddFixedPadding && isStaticSideBarActivated ? OPENED_MENU_WIDTH : 0;
return {
OPENED_MENU_WIDTH,
staticMenuWidth,
};
};

View File

@ -0,0 +1,18 @@
import { useSideBarContext } from "@/lib/context/SidebarProvider/hooks/useSideBarContext";
import { useDevice } from "@/lib/hooks/useDevice";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useOutsideClickListener = () => {
const { isOpened, setIsOpened } = useSideBarContext();
const { isMobile } = useDevice();
const onClickOutside = () => {
if (isOpened && isMobile) {
setIsOpened(false);
}
};
return {
onClickOutside,
};
};

View File

@ -8,6 +8,8 @@ type ChatsContextType = {
allChats: ChatEntity[]; allChats: ChatEntity[];
//set setAllChats is from the useState hook so it can take a function as params //set setAllChats is from the useState hook so it can take a function as params
setAllChats: React.Dispatch<React.SetStateAction<ChatEntity[]>>; setAllChats: React.Dispatch<React.SetStateAction<ChatEntity[]>>;
isLoading: boolean;
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
}; };
export const ChatsContext = createContext<ChatsContextType | undefined>( export const ChatsContext = createContext<ChatsContextType | undefined>(
@ -20,12 +22,15 @@ export const ChatsProvider = ({
children: React.ReactNode; children: React.ReactNode;
}): JSX.Element => { }): JSX.Element => {
const [allChats, setAllChats] = useState<ChatEntity[]>([]); const [allChats, setAllChats] = useState<ChatEntity[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
return ( return (
<ChatsContext.Provider <ChatsContext.Provider
value={{ value={{
allChats, allChats,
setAllChats, setAllChats,
isLoading,
setIsLoading,
}} }}
> >
{children} {children}

View File

@ -7,7 +7,7 @@ export const useChatsContext = () => {
const context = useContext(ChatsContext); const context = useContext(ChatsContext);
if (context === undefined) { if (context === undefined) {
throw new Error("useChatsStore must be used inside ChatsProvider"); throw new Error("useChatsContext must be used inside ChatsProvider");
} }
return context; return context;

View File

@ -51,7 +51,7 @@
"shortcut_go_to_shortcuts": "CMDK: Go to shortcuts", "shortcut_go_to_shortcuts": "CMDK: Go to shortcuts",
"shortcut_go_to_user_page": "CMDU: Go to user page", "shortcut_go_to_user_page": "CMDU: Go to user page",
"shortcut_manage_brains": "CMDB: Manage your brains", "shortcut_manage_brains": "CMDB: Manage your brains",
"shortcut_select_brain": "@: Select a brain to talk", "shortcut_select_brain": "@: Select a brain",
"shortcut_select_file": "/: Select a file to talk to", "shortcut_select_file": "/: Select a file to talk to",
"subtitle": "Talk to a language model about your uploaded data", "subtitle": "Talk to a language model about your uploaded data",
"thinking": "Thinking...", "thinking": "Thinking...",

View File

@ -51,7 +51,7 @@
"shortcut_go_to_shortcuts": "CMDK: Ir a los atajos", "shortcut_go_to_shortcuts": "CMDK: Ir a los atajos",
"shortcut_go_to_user_page": "CMDU: Ir a la página de usuario", "shortcut_go_to_user_page": "CMDU: Ir a la página de usuario",
"shortcut_manage_brains": "CMDB: Administrar tus cerebros", "shortcut_manage_brains": "CMDB: Administrar tus cerebros",
"shortcut_select_brain": "@: Seleccionar un cerebro para hablar", "shortcut_select_brain": "@: Seleccionar un cerebro",
"shortcut_select_file": "/: Seleccionar un archivo para hablar", "shortcut_select_file": "/: Seleccionar un archivo para hablar",
"subtitle": "Habla con un modelo de lenguaje acerca de tus datos subidos", "subtitle": "Habla con un modelo de lenguaje acerca de tus datos subidos",
"thinking": "Pensando...", "thinking": "Pensando...",

View File

@ -51,7 +51,7 @@
"shortcut_go_to_shortcuts": "CMDK: Accéder aux raccourcis", "shortcut_go_to_shortcuts": "CMDK: Accéder aux raccourcis",
"shortcut_go_to_user_page": "CMDU: Accéder à la page utilisateur", "shortcut_go_to_user_page": "CMDU: Accéder à la page utilisateur",
"shortcut_manage_brains": "CMDB: Gérer vos cerveaux", "shortcut_manage_brains": "CMDB: Gérer vos cerveaux",
"shortcut_select_brain": "@: Sélectionner un cerveau pour discuter", "shortcut_select_brain": "@: Sélectionnez un cerveau",
"shortcut_select_file": "/: Sélectionner un fichier pour discuter", "shortcut_select_file": "/: Sélectionner un fichier pour discuter",
"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",
"thinking": "Réflexion...", "thinking": "Réflexion...",

View File

@ -51,7 +51,7 @@
"shortcut_go_to_shortcuts": "CMDA: Acesse os atalhos", "shortcut_go_to_shortcuts": "CMDA: Acesse os atalhos",
"shortcut_go_to_user_page": "CMDU: Acesse a página do usuário", "shortcut_go_to_user_page": "CMDU: Acesse a página do usuário",
"shortcut_manage_brains": "CMGC: Gerencie seus cérebros", "shortcut_manage_brains": "CMGC: Gerencie seus cérebros",
"shortcut_select_brain": "@: Selecione um cérebro para conversar", "shortcut_select_brain": "@: Selecione um cérebro",
"shortcut_select_file": "/: Selecione um arquivo para conversar", "shortcut_select_file": "/: Selecione um arquivo para conversar",
"subtitle": "Converse com um modelo de linguagem sobre seus dados enviados", "subtitle": "Converse com um modelo de linguagem sobre seus dados enviados",
"thinking": "Pensando...", "thinking": "Pensando...",

View File

@ -51,7 +51,7 @@
"shortcut_go_to_shortcuts": "CMDK: Перейти к ярлыкам", "shortcut_go_to_shortcuts": "CMDK: Перейти к ярлыкам",
"shortcut_go_to_user_page": "CMDU: Перейти на страницу пользователя", "shortcut_go_to_user_page": "CMDU: Перейти на страницу пользователя",
"shortcut_manage_brains": "CMDB: Управление вашими мозгами", "shortcut_manage_brains": "CMDB: Управление вашими мозгами",
"shortcut_select_brain": "@: Выберите мозг для общения", "shortcut_select_brain": "@: Выберите мозг",
"shortcut_select_file": "/: Выберите файл для общения", "shortcut_select_file": "/: Выберите файл для общения",
"subtitle": "Общайтесь с языковой моделью о ваших загруженных данных", "subtitle": "Общайтесь с языковой моделью о ваших загруженных данных",
"thinking": "Думаю...", "thinking": "Думаю...",

View File

@ -52,7 +52,7 @@
"shortcut_go_to_shortcuts": "CMDK: 前往快捷方式", "shortcut_go_to_shortcuts": "CMDK: 前往快捷方式",
"shortcut_go_to_user_page": "CMDU: 进入用户页面", "shortcut_go_to_user_page": "CMDU: 进入用户页面",
"shortcut_manage_brains": "CMDB: 管理大脑", "shortcut_manage_brains": "CMDB: 管理大脑",
"shortcut_select_brain": "@: 选择一个大脑进行交流", "shortcut_select_brain": "@: 选择一个大脑",
"shortcut_select_file": "/: 选择一个文件进行对话", "shortcut_select_file": "/: 选择一个文件进行对话",
"subtitle": "与语言模型讨论您上传的数据", "subtitle": "与语言模型讨论您上传的数据",
"thinking": "思考中…", "thinking": "思考中…",

View File

@ -18,7 +18,9 @@ module.exports = {
black: "#11243E", black: "#11243E",
primary: "#6142D4", primary: "#6142D4",
secondary: "#F3ECFF", secondary: "#F3ECFF",
tertiary: "#F6F4FF",
accent: "#13ABBA", accent: "#13ABBA",
highlight: "#FAFAFA",
"accent-hover": "#008491", "accent-hover": "#008491",
"chat-bg-gray": "#D9D9D9", "chat-bg-gray": "#D9D9D9",
"msg-gray": "#9B9B9B", "msg-gray": "#9B9B9B",