mirror of
https://github.com/StanGirard/quivr.git
synced 2024-12-23 19:32:30 +03:00
feat: allow user to control left panel from Chat input (#1880)
Issue: https://github.com/StanGirard/quivr/issues/1866 - Use context to control sidebar open status - Control sidebar through chat bar - Persist sidebar status after page change Demo: https://github.com/StanGirard/quivr/assets/63923024/b9750198-e68d-47a7-b266-627a01586512
This commit is contained in:
parent
5f114c26d6
commit
3ed0ea15bb
@ -5,6 +5,7 @@ import { PropsWithChildren, useEffect } from "react";
|
|||||||
|
|
||||||
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";
|
||||||
|
import { SideBarProvider } from "@/lib/context/SidebarProvider/sidebar-provider";
|
||||||
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
||||||
import { UpdateMetadata } from "@/lib/helpers/updateMetadata";
|
import { UpdateMetadata } from "@/lib/helpers/updateMetadata";
|
||||||
import { usePageTracking } from "@/services/analytics/june/usePageTracking";
|
import { usePageTracking } from "@/services/analytics/june/usePageTracking";
|
||||||
@ -40,7 +41,9 @@ const AppWithQueryClient = ({ children }: PropsWithChildren): JSX.Element => {
|
|||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<BrainProvider>
|
<BrainProvider>
|
||||||
<App>{children}</App>
|
<SideBarProvider>
|
||||||
|
<App>{children}</App>
|
||||||
|
</SideBarProvider>
|
||||||
</BrainProvider>
|
</BrainProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
ChatProviderMock,
|
ChatProviderMock,
|
||||||
} from "@/lib/context/ChatProvider/mocks/ChatProviderMock";
|
} from "@/lib/context/ChatProvider/mocks/ChatProviderMock";
|
||||||
import { KnowledgeToFeedProvider } from "@/lib/context/KnowledgeToFeedProvider";
|
import { KnowledgeToFeedProvider } from "@/lib/context/KnowledgeToFeedProvider";
|
||||||
|
import { SideBarProvider } from "@/lib/context/SidebarProvider/sidebar-provider";
|
||||||
import {
|
import {
|
||||||
SupabaseContextMock,
|
SupabaseContextMock,
|
||||||
SupabaseProviderMock,
|
SupabaseProviderMock,
|
||||||
@ -87,7 +88,9 @@ describe("Chat page", () => {
|
|||||||
<ChatProviderMock>
|
<ChatProviderMock>
|
||||||
<SupabaseProviderMock>
|
<SupabaseProviderMock>
|
||||||
<BrainProviderMock>
|
<BrainProviderMock>
|
||||||
<SelectedChatPage />,
|
<SideBarProvider>
|
||||||
|
<SelectedChatPage />,
|
||||||
|
</SideBarProvider>
|
||||||
</BrainProviderMock>
|
</BrainProviderMock>
|
||||||
</SupabaseProviderMock>
|
</SupabaseProviderMock>
|
||||||
</ChatProviderMock>
|
</ChatProviderMock>
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { LuPanelLeftClose, LuPanelRightClose } from "react-icons/lu";
|
||||||
|
|
||||||
|
import Button from "@/lib/components/ui/Button";
|
||||||
|
import { useSideBarContext } from "@/lib/context/SidebarProvider/hooks/useSideBarContext";
|
||||||
|
|
||||||
|
export const MenuControlButton = (): JSX.Element => {
|
||||||
|
const { isOpened, setIsOpened } = useSideBarContext();
|
||||||
|
const Icon = isOpened ? LuPanelLeftClose : LuPanelRightClose;
|
||||||
|
const { t } = useTranslation("chat");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="tertiary"
|
||||||
|
className="px-2 py-0"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsOpened(!isOpened)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center justify-center gap-1">
|
||||||
|
<Icon className="text-2xl md:text-3xl self-center text-accent" />
|
||||||
|
<span className="text-xs">{t("menu")}</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
@ -9,6 +9,7 @@ import { getBrainIconFromBrainType } from "@/lib/helpers/getBrainIconFromBrainTy
|
|||||||
import { OnboardingQuestions } from "./components";
|
import { OnboardingQuestions } from "./components";
|
||||||
import { ActionsModal } from "./components/ActionsModal/ActionsModal";
|
import { ActionsModal } from "./components/ActionsModal/ActionsModal";
|
||||||
import { ChatEditor } from "./components/ChatEditor/ChatEditor";
|
import { ChatEditor } from "./components/ChatEditor/ChatEditor";
|
||||||
|
import { MenuControlButton } from "./components/MenuControlButton";
|
||||||
import { useChatInput } from "./hooks/useChatInput";
|
import { useChatInput } from "./hooks/useChatInput";
|
||||||
|
|
||||||
type ChatInputProps = {
|
type ChatInputProps = {
|
||||||
@ -37,6 +38,7 @@ export const ChatInput = ({
|
|||||||
}}
|
}}
|
||||||
className="sticky bottom-0 bg-white dark:bg-black w-full flex items-center gap-2 z-20 p-2"
|
className="sticky bottom-0 bg-white dark:bg-black w-full flex items-center gap-2 z-20 p-2"
|
||||||
>
|
>
|
||||||
|
<MenuControlButton />
|
||||||
{!shouldDisplayFeedOrSecretsCard && (
|
{!shouldDisplayFeedOrSecretsCard && (
|
||||||
<Button
|
<Button
|
||||||
className="p-0"
|
className="p-0"
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
ChatProviderMock,
|
ChatProviderMock,
|
||||||
} from "@/lib/context/ChatProvider/mocks/ChatProviderMock";
|
} from "@/lib/context/ChatProvider/mocks/ChatProviderMock";
|
||||||
import { KnowledgeToFeedProvider } from "@/lib/context/KnowledgeToFeedProvider";
|
import { KnowledgeToFeedProvider } from "@/lib/context/KnowledgeToFeedProvider";
|
||||||
|
import { SideBarProvider } from "@/lib/context/SidebarProvider/sidebar-provider";
|
||||||
import { SupabaseContextMock } from "@/lib/context/SupabaseProvider/mocks/SupabaseProviderMock";
|
import { SupabaseContextMock } from "@/lib/context/SupabaseProvider/mocks/SupabaseProviderMock";
|
||||||
|
|
||||||
vi.mock("@/lib/context/SupabaseProvider/supabase-provider", () => ({
|
vi.mock("@/lib/context/SupabaseProvider/supabase-provider", () => ({
|
||||||
@ -91,7 +92,9 @@ describe("ChatsList", () => {
|
|||||||
<KnowledgeToFeedProvider>
|
<KnowledgeToFeedProvider>
|
||||||
<ChatProviderMock>
|
<ChatProviderMock>
|
||||||
<BrainProviderMock>
|
<BrainProviderMock>
|
||||||
<ChatsList />
|
<SideBarProvider>
|
||||||
|
<ChatsList />
|
||||||
|
</SideBarProvider>
|
||||||
</BrainProviderMock>
|
</BrainProviderMock>
|
||||||
</ChatProviderMock>
|
</ChatProviderMock>
|
||||||
</KnowledgeToFeedProvider>
|
</KnowledgeToFeedProvider>
|
||||||
@ -110,7 +113,9 @@ describe("ChatsList", () => {
|
|||||||
<KnowledgeToFeedProvider>
|
<KnowledgeToFeedProvider>
|
||||||
<ChatProviderMock>
|
<ChatProviderMock>
|
||||||
<BrainProviderMock>
|
<BrainProviderMock>
|
||||||
<ChatsList />
|
<SideBarProvider>
|
||||||
|
<ChatsList />
|
||||||
|
</SideBarProvider>
|
||||||
</BrainProviderMock>
|
</BrainProviderMock>
|
||||||
</ChatProviderMock>
|
</ChatProviderMock>
|
||||||
</KnowledgeToFeedProvider>
|
</KnowledgeToFeedProvider>
|
||||||
@ -133,7 +138,9 @@ describe("ChatsList", () => {
|
|||||||
<KnowledgeToFeedProvider>
|
<KnowledgeToFeedProvider>
|
||||||
<ChatProviderMock>
|
<ChatProviderMock>
|
||||||
<BrainProviderMock>
|
<BrainProviderMock>
|
||||||
<ChatsList />
|
<SideBarProvider>
|
||||||
|
<ChatsList />
|
||||||
|
</SideBarProvider>
|
||||||
</BrainProviderMock>
|
</BrainProviderMock>
|
||||||
</ChatProviderMock>
|
</ChatProviderMock>
|
||||||
</KnowledgeToFeedProvider>
|
</KnowledgeToFeedProvider>
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import { motion, MotionConfig } from "framer-motion";
|
import { motion, MotionConfig } from "framer-motion";
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { LuPanelLeftOpen } from "react-icons/lu";
|
import { LuPanelLeftOpen } from "react-icons/lu";
|
||||||
|
|
||||||
import { SidebarHeader } from "@/lib/components/Sidebar/components/SidebarHeader";
|
import { SidebarHeader } from "@/lib/components/Sidebar/components/SidebarHeader";
|
||||||
import { useDevice } from "@/lib/hooks/useDevice";
|
import { useSideBarContext } from "@/lib/context/SidebarProvider/hooks/useSideBarContext";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -21,13 +19,7 @@ export const Sidebar = ({
|
|||||||
children,
|
children,
|
||||||
showButtons,
|
showButtons,
|
||||||
}: SidebarProps): JSX.Element => {
|
}: SidebarProps): JSX.Element => {
|
||||||
const { isMobile } = useDevice();
|
const { isOpened, setIsOpened } = useSideBarContext();
|
||||||
const pathname = usePathname();
|
|
||||||
const [open, setOpen] = useState(!isMobile);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setOpen(!isMobile);
|
|
||||||
}, [isMobile, pathname]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MotionConfig transition={{ mass: 1, damping: 10, duration: 0.2 }}>
|
<MotionConfig transition={{ mass: 1, damping: 10, duration: 0.2 }}>
|
||||||
@ -36,40 +28,40 @@ export const Sidebar = ({
|
|||||||
dragConstraints={{ right: 0, left: 0 }}
|
dragConstraints={{ right: 0, left: 0 }}
|
||||||
dragElastic={0.15}
|
dragElastic={0.15}
|
||||||
onDragEnd={(event, info) => {
|
onDragEnd={(event, info) => {
|
||||||
if (info.offset.x > 100 && !open) {
|
if (info.offset.x > 100 && !isOpened) {
|
||||||
setOpen(true);
|
setIsOpened(true);
|
||||||
} else if (info.offset.x < -100 && open) {
|
} else if (info.offset.x < -100 && isOpened) {
|
||||||
setOpen(false);
|
setIsOpened(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
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"
|
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"
|
||||||
>
|
>
|
||||||
{!open && (
|
{!isOpened && (
|
||||||
<button
|
<button
|
||||||
title="Open Sidebar"
|
title="Open Sidebar"
|
||||||
type="button"
|
type="button"
|
||||||
className="absolute p-3 text-2xl bg-red top-5 -right-20 hover:text-primary dark:hover:text-gray-200 transition-colors"
|
className="absolute p-3 text-2xl bg-red top-5 -right-20 hover:text-primary dark:hover:text-gray-200 transition-colors"
|
||||||
data-testid="open-sidebar-button"
|
data-testid="open-sidebar-button"
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setIsOpened(true)}
|
||||||
>
|
>
|
||||||
<LuPanelLeftOpen />
|
<LuPanelLeftOpen />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{
|
initial={{
|
||||||
width: open ? "18rem" : "0px",
|
width: isOpened ? "18rem" : "0px",
|
||||||
}}
|
}}
|
||||||
animate={{
|
animate={{
|
||||||
width: open ? "18rem" : "0px",
|
width: isOpened ? "18rem" : "0px",
|
||||||
opacity: open ? 1 : 0.5,
|
opacity: isOpened ? 1 : 0.5,
|
||||||
boxShadow: open
|
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={cn("overflow-hidden flex flex-col flex-1 max-w-xs")}
|
className={cn("overflow-hidden flex flex-col flex-1 max-w-xs")}
|
||||||
data-testid="sidebar"
|
data-testid="sidebar"
|
||||||
>
|
>
|
||||||
<SidebarHeader setOpen={setOpen} />
|
<SidebarHeader />
|
||||||
<div className="overflow-auto flex flex-col flex-1">{children}</div>
|
<div className="overflow-auto flex flex-col flex-1">{children}</div>
|
||||||
{showButtons && <SidebarFooter showButtons={showButtons} />}
|
{showButtons && <SidebarFooter showButtons={showButtons} />}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { Sidebar } from "@/lib/components/Sidebar/Sidebar";
|
import { Sidebar } from "@/lib/components/Sidebar/Sidebar";
|
||||||
|
import { SideBarProvider } from "@/lib/context/SidebarProvider/sidebar-provider";
|
||||||
import { useDevice } from "@/lib/hooks/useDevice";
|
import { useDevice } from "@/lib/hooks/useDevice";
|
||||||
|
|
||||||
vi.mock("@/lib/hooks/useDevice");
|
vi.mock("@/lib/hooks/useDevice");
|
||||||
@ -16,9 +17,11 @@ vi.mock("@/lib/hooks/useDevice");
|
|||||||
const renderSidebar = async () => {
|
const renderSidebar = async () => {
|
||||||
await act(() =>
|
await act(() =>
|
||||||
render(
|
render(
|
||||||
<Sidebar>
|
<SideBarProvider>
|
||||||
<div data-testid="sidebar-test-content">📦</div>
|
<Sidebar>
|
||||||
</Sidebar>
|
<div data-testid="sidebar-test-content">📦</div>
|
||||||
|
</Sidebar>
|
||||||
|
</SideBarProvider>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
import { Dispatch, SetStateAction } from "react";
|
import { LuPanelLeftClose } from "react-icons/lu";
|
||||||
import { LuPanelLeft } from "react-icons/lu";
|
|
||||||
|
|
||||||
import { Logo } from "@/lib/components/Logo/Logo";
|
import { Logo } from "@/lib/components/Logo/Logo";
|
||||||
|
import { useSideBarContext } from "@/lib/context/SidebarProvider/hooks/useSideBarContext";
|
||||||
|
|
||||||
type SidebarProps = {
|
export const SidebarHeader = (): JSX.Element => {
|
||||||
setOpen: Dispatch<SetStateAction<boolean>>;
|
const { setIsOpened } = useSideBarContext();
|
||||||
};
|
|
||||||
|
|
||||||
export const SidebarHeader = ({ setOpen }: SidebarProps): JSX.Element => {
|
|
||||||
return (
|
return (
|
||||||
<div className="p-2 border-b relative">
|
<div className="p-2 border-b relative">
|
||||||
<div className="max-w-screen-xl flex justify-between items-center pt-3 pl-3">
|
<div className="max-w-screen-xl flex justify-between items-center pt-3 pl-3">
|
||||||
@ -17,9 +15,9 @@ export const SidebarHeader = ({ setOpen }: SidebarProps): JSX.Element => {
|
|||||||
className="p-3 text-2xl bg:white dark:bg-black text-black dark:text-white hover:text-primary dark:hover:text-gray-200 transition-colors"
|
className="p-3 text-2xl bg:white dark:bg-black text-black dark:text-white hover:text-primary dark:hover:text-gray-200 transition-colors"
|
||||||
type="button"
|
type="button"
|
||||||
data-testid="close-sidebar-button"
|
data-testid="close-sidebar-button"
|
||||||
onClick={() => setOpen(false)}
|
onClick={() => setIsOpened(false)}
|
||||||
>
|
>
|
||||||
<LuPanelLeft />
|
<LuPanelLeftClose />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
|
import { SideBarContext } from "../sidebar-provider";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
|
export const useSideBarContext = () => {
|
||||||
|
const context = useContext(SideBarContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error("useSideBarContext must be used within a SideBarProvider");
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
36
frontend/lib/context/SidebarProvider/sidebar-provider.tsx
Normal file
36
frontend/lib/context/SidebarProvider/sidebar-provider.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { createContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { useDevice } from "@/lib/hooks/useDevice";
|
||||||
|
|
||||||
|
type SideBarContextType = {
|
||||||
|
isOpened: boolean;
|
||||||
|
setIsOpened: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SideBarContext = createContext<SideBarContextType | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
export const SideBarProvider = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}): JSX.Element => {
|
||||||
|
const { isMobile } = useDevice();
|
||||||
|
const [isOpened, setIsOpened] = useState(!isMobile);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsOpened(!isMobile);
|
||||||
|
}, [isMobile]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SideBarContext.Provider
|
||||||
|
value={{
|
||||||
|
isOpened,
|
||||||
|
setIsOpened,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SideBarContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
@ -21,6 +21,7 @@
|
|||||||
"last30Days": "Previous 30 days",
|
"last30Days": "Previous 30 days",
|
||||||
"last7Days": "Previous 7 days",
|
"last7Days": "Previous 7 days",
|
||||||
"limit_reached": "You have reached the limit of requests, please try again later",
|
"limit_reached": "You have reached the limit of requests, please try again later",
|
||||||
|
"menu": "Menu",
|
||||||
"missing_brain": "Please select a brain to chat with",
|
"missing_brain": "Please select a brain to chat with",
|
||||||
"new_discussion": "New discussion",
|
"new_discussion": "New discussion",
|
||||||
"new_prompt": "Create new prompt",
|
"new_prompt": "Create new prompt",
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
"last30Days": "Últimos 30 días",
|
"last30Days": "Últimos 30 días",
|
||||||
"last7Days": "Últimos 7 días",
|
"last7Days": "Últimos 7 días",
|
||||||
"limit_reached": "Has alcanzado el límite de peticiones, intente de nuevo más tarde",
|
"limit_reached": "Has alcanzado el límite de peticiones, intente de nuevo más tarde",
|
||||||
|
"menu": "Menú",
|
||||||
"missing_brain": "No hay cerebro seleccionado",
|
"missing_brain": "No hay cerebro seleccionado",
|
||||||
"new_discussion": "Nueva discusión",
|
"new_discussion": "Nueva discusión",
|
||||||
"new_prompt": "Crear nueva instrucción",
|
"new_prompt": "Crear nueva instrucción",
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
"last30Days": "30 derniers jours",
|
"last30Days": "30 derniers jours",
|
||||||
"last7Days": "7 derniers jours",
|
"last7Days": "7 derniers jours",
|
||||||
"limit_reached": "Vous avez atteint la limite de requêtes, veuillez réessayer plus tard",
|
"limit_reached": "Vous avez atteint la limite de requêtes, veuillez réessayer plus tard",
|
||||||
|
"menu": "Menu",
|
||||||
"missing_brain": "Veuillez selectionner un cerveau pour discuter",
|
"missing_brain": "Veuillez selectionner un cerveau pour discuter",
|
||||||
"new_discussion": "Nouvelle discussion",
|
"new_discussion": "Nouvelle discussion",
|
||||||
"new_prompt": "Créer un nouveau prompt",
|
"new_prompt": "Créer un nouveau prompt",
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
"last30Days": "Últimos 30 dias",
|
"last30Days": "Últimos 30 dias",
|
||||||
"last7Days": "Últimos 7 dias",
|
"last7Days": "Últimos 7 dias",
|
||||||
"limit_reached": "Você atingiu o limite de solicitações, por favor, tente novamente mais tarde",
|
"limit_reached": "Você atingiu o limite de solicitações, por favor, tente novamente mais tarde",
|
||||||
|
"menu": "Menu",
|
||||||
"missing_brain": "Cérebro não encontrado",
|
"missing_brain": "Cérebro não encontrado",
|
||||||
"new_discussion": "Nova discussão",
|
"new_discussion": "Nova discussão",
|
||||||
"new_prompt": "Criar novo prompt",
|
"new_prompt": "Criar novo prompt",
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
"last30Days": "Последние 30 дней",
|
"last30Days": "Последние 30 дней",
|
||||||
"last7Days": "Последние 7 дней",
|
"last7Days": "Последние 7 дней",
|
||||||
"limit_reached": "Вы достигли лимита запросов, пожалуйста, попробуйте позже",
|
"limit_reached": "Вы достигли лимита запросов, пожалуйста, попробуйте позже",
|
||||||
|
"menu": "Меню",
|
||||||
"missing_brain": "Мозг не найден",
|
"missing_brain": "Мозг не найден",
|
||||||
"new_discussion": "Новое обсуждение",
|
"new_discussion": "Новое обсуждение",
|
||||||
"new_prompt": "Создать новый запрос",
|
"new_prompt": "Создать новый запрос",
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
"last30Days": "过去30天",
|
"last30Days": "过去30天",
|
||||||
"last7Days": "过去7天",
|
"last7Days": "过去7天",
|
||||||
"limit_reached": "您已达到请求限制,请稍后再试",
|
"limit_reached": "您已达到请求限制,请稍后再试",
|
||||||
|
"menu": "菜单",
|
||||||
"missing_brain": "请选择一个大脑进行聊天",
|
"missing_brain": "请选择一个大脑进行聊天",
|
||||||
"new_discussion": "新讨论",
|
"new_discussion": "新讨论",
|
||||||
"new_prompt": "新提示",
|
"new_prompt": "新提示",
|
||||||
|
Loading…
Reference in New Issue
Block a user