feat(chatPage): update ui add new feed component (#1275)

* feat(brains): remove unrelevent buttons on management page

* feat: improve feed input ux

* feat: update brain knowledge context add knowledge to feed logic

* feat: allow to drop files everywhere in the chat section

* style: add slide in effect when opening feed card

* test(chatPage): fix failing tests
This commit is contained in:
Mamadou DICKO 2023-09-28 11:29:55 +02:00 committed by GitHub
parent b6f38f7aff
commit 2226eef06b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 272 additions and 246 deletions

View File

@ -98,15 +98,16 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
{t("defaultBrain", { ns: "brain" })}
</div>
) : (
<Button
variant={"secondary"}
isLoading={isSettingAsDefault}
onClick={() => void setAsDefaultBrainHandler()}
type="button"
disabled={!hasEditRights}
>
{t("setDefaultBrain", { ns: "brain" })}
</Button>
hasEditRights && (
<Button
variant={"secondary"}
isLoading={isSettingAsDefault}
onClick={() => void setAsDefaultBrainHandler()}
type="button"
>
{t("setDefaultBrain", { ns: "brain" })}
</Button>
)
)}
</div>
</div>
@ -187,9 +188,11 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
{...register("maxTokens")}
/>
</fieldset>
<div className="flex w-full justify-end py-4">
<SaveButton disabled={!hasEditRights} handleSubmit={handleSubmit} />
</div>
{hasEditRights && (
<div className="flex w-full justify-end py-4">
<SaveButton handleSubmit={handleSubmit} />
</div>
)}
<Divider text={t("customPromptSection", { ns: "config" })} />
{hasEditRights && <PublicPrompts onSelect={pickPublicPrompt} />}
<Field
@ -208,12 +211,14 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
disabled={!hasEditRights}
{...register("prompt.content")}
/>
<div className="flex w-full justify-end py-4">
<SaveButton disabled={!hasEditRights} handleSubmit={handleSubmit} />
</div>
{promptId !== "" && (
{hasEditRights && (
<div className="flex w-full justify-end py-4">
<SaveButton handleSubmit={handleSubmit} />
</div>
)}
{hasEditRights && promptId !== "" && (
<Button
disabled={isUpdating || !hasEditRights}
disabled={isUpdating}
onClick={() => void removeBrainPrompt()}
>
{t("removePrompt", { ns: "config" })}

View File

@ -10,6 +10,7 @@ import {
ChatContextMock,
ChatProviderMock,
} from "@/lib/context/ChatProvider/mocks/ChatProviderMock";
import { KnowledgeProvider } from "@/lib/context/KnowledgeProvider";
import {
SupabaseContextMock,
SupabaseProviderMock,
@ -74,15 +75,17 @@ vi.mock("@tanstack/react-query", async () => {
describe("Chat page", () => {
it("should render chat page correctly", () => {
const { getByTestId } = render(
<QueryClientProvider client={queryClient}>
<ChatProviderMock>
<SupabaseProviderMock>
<BrainProviderMock>
<SelectedChatPage />,
</BrainProviderMock>
</SupabaseProviderMock>
</ChatProviderMock>
</QueryClientProvider>
<KnowledgeProvider>
<QueryClientProvider client={queryClient}>
<ChatProviderMock>
<SupabaseProviderMock>
<BrainProviderMock>
<SelectedChatPage />,
</BrainProviderMock>
</SupabaseProviderMock>
</ChatProviderMock>
</QueryClientProvider>
</KnowledgeProvider>
);
expect(getByTestId("chat-page")).toBeDefined();

View File

@ -1,8 +1,10 @@
import { AnimatePresence, motion } from "framer-motion";
import { useTranslation } from "react-i18next";
import { AiOutlineLoading3Quarters } from "react-icons/ai";
import { ChatInput, KnowledgeToFeed } from "./components";
import { useActionBar } from "./hooks/useActionBar";
type ActionBarProps = {
setShouldDisplayUploadCard: (shouldDisplay: boolean) => void;
shouldDisplayUploadCard: boolean;
@ -29,12 +31,21 @@ export const ActionsBar = ({
<div>
{shouldDisplayUploadCard && (
<div className="flex flex-1 overflow-y-auto shadow-md dark:shadow-primary/25 hover:shadow-xl transition-shadow rounded-xl bg-white dark:bg-black border border-black/10 dark:border-white/25 p-4 md:p-6 mt-5">
<KnowledgeToFeed
closeFeedInput={() => setShouldDisplayUploadCard(false)}
dispatchHasPendingRequests={() => setHasPendingRequests(true)}
/>
</div>
<AnimatePresence>
<motion.div
key="slide"
initial={{ y: "100%", opacity: 0 }}
animate={{ y: 0, opacity: 1, transition: { duration: 0.2 } }}
exit={{ y: "100%", opacity: 0 }}
>
<div className="flex flex-1 overflow-y-auto shadow-md dark:shadow-primary/25 hover:shadow-xl transition-shadow rounded-xl bg-white dark:bg-black border border-black/10 dark:border-white/25 p-4 md:p-6 mt-5">
<KnowledgeToFeed
closeFeedInput={() => setShouldDisplayUploadCard(false)}
dispatchHasPendingRequests={() => setHasPendingRequests(true)}
/>
</div>
</motion.div>
</AnimatePresence>
)}
{!shouldDisplayUploadCard && (
<div className="flex mt-1 flex-col w-full shadow-md dark:shadow-primary/25 hover:shadow-xl transition-shadow rounded-xl bg-white dark:bg-black border border-black/10 dark:border-white/25 md:mb-4 lg:mb-[-20px] p-2">

View File

@ -1,4 +1,3 @@
/* eslint-disable */
"use client";
import { useTranslation } from "react-i18next";
import { PiPaperclipFill } from "react-icons/pi";

View File

@ -49,6 +49,7 @@ export const KnowledgeToFeed = ({
onChange={(newSelectedBrainId) =>
setCurrentBrainId(newSelectedBrainId)
}
className="flex flex-row items-center"
/>
</div>
<KnowledgeToFeedInput

View File

@ -1,8 +1,17 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { useKnowledgeContext } from "@/lib/context/KnowledgeProvider/hooks/useKnowledgeContext";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useSelectedChatPage = () => {
const [shouldDisplayUploadCard, setShouldDisplayUploadCard] = useState(false);
const { knowledgeToFeed } = useKnowledgeContext();
useEffect(() => {
if (knowledgeToFeed.length > 0 && !shouldDisplayUploadCard) {
setShouldDisplayUploadCard(true);
}
}, [knowledgeToFeed, setShouldDisplayUploadCard]);
return {
shouldDisplayUploadCard,

View File

@ -1,5 +1,7 @@
"use client";
import { useCustomDropzone } from "@/lib/hooks/useDropzone";
import { ActionsBar } from "./components/ActionsBar";
import { ChatDialogueArea } from "./components/ChatDialogueArea/ChatDialogue";
import { ChatHeader } from "./components/ChatHeader";
@ -8,11 +10,13 @@ import { useSelectedChatPage } from "./hooks/useSelectedChatPage";
const SelectedChatPage = (): JSX.Element => {
const { setShouldDisplayUploadCard, shouldDisplayUploadCard } =
useSelectedChatPage();
const { getRootProps } = useCustomDropzone();
return (
<main
className="flex flex-col w-full h-[calc(100vh-61px)] overflow-hidden"
data-testid="chat-page"
{...getRootProps()}
>
<section className="flex flex-col flex-1 items-center w-full h-full overflow-y-auto">
<ChatHeader />

View File

@ -39,8 +39,7 @@ export const useChatNotificationsSync = () => {
);
if (hasAPendingNotification) {
//30 seconds
return 2_000;
return 2_000; // in ms
}
return false;

View File

@ -1,7 +1,7 @@
"use client";
import { ReactNode } from "react";
import { ChatProvider } from "@/lib/context";
import { ChatProvider, KnowledgeProvider } from "@/lib/context";
import { ChatsProvider } from "@/lib/context/ChatsProvider/chats-provider";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import { redirectToLogin } from "@/lib/router/redirectToLogin";
@ -20,14 +20,16 @@ const Layout = ({ children }: LayoutProps): JSX.Element => {
}
return (
<ChatsProvider>
<ChatProvider>
<div className="relative h-full w-full flex justify-stretch items-stretch">
<ChatsList />
{children}
</div>
</ChatProvider>
</ChatsProvider>
<KnowledgeProvider>
<ChatsProvider>
<ChatProvider>
<div className="relative h-full w-full flex justify-stretch items-stretch">
<ChatsList />
{children}
</div>
</ChatProvider>
</ChatsProvider>
</KnowledgeProvider>
);
};

View File

@ -1,13 +1,12 @@
import { useTranslation } from "react-i18next";
import Button from "@/lib/components/ui/Button";
import { Divider } from "@/lib/components/ui/Divider";
import { useKnowledgeContext } from "@/lib/context/KnowledgeProvider/hooks/useKnowledgeContext";
import { FeedItems } from "./components";
import { Crawler } from "./components/Crawler";
import { FileUploader } from "./components/FileUploader";
import { useKnowledgeToFeedInput } from "./hooks/useKnowledgeToFeedInput.ts";
import { FeedItemUploadType } from "../../../app/chat/[chatId]/components/ActionsBar/types";
type KnowledgeToFeedInputProps = {
dispatchHasPendingRequests?: () => void;
@ -19,31 +18,33 @@ export const KnowledgeToFeedInput = ({
closeFeedInput,
}: KnowledgeToFeedInputProps): JSX.Element => {
const { t } = useTranslation(["translation", "upload"]);
const { addContent, contents, feedBrain, removeContent } =
useKnowledgeToFeedInput({
dispatchHasPendingRequests,
closeFeedInput,
});
const files: File[] = (
contents.filter((c) => c.source === "upload") as FeedItemUploadType[]
).map((c) => c.file);
const { feedBrain } = useKnowledgeToFeedInput({
dispatchHasPendingRequests,
closeFeedInput,
});
const { knowledgeToFeed } = useKnowledgeContext();
return (
<>
<FileUploader addContent={addContent} files={files} />
<Divider text={t("or", { ns: "translation" })} className="m-5" />
<Crawler addContent={addContent} />
<FeedItems contents={contents} removeContent={removeContent} />
<div className="px-20">
<div className="flex flex-row gap-10 justify-between items-center mt-5">
<FileUploader />
<span className="whitespace-nowrap ">
{`${t("and", { ns: "translation" })} / ${t("or", {
ns: "translation",
})}`}
</span>
<Crawler />
</div>
<FeedItems />
<div className="flex justify-center mt-5">
<Button
disabled={contents.length === 0}
disabled={knowledgeToFeed.length === 0}
className="rounded-xl bg-purple-600 border-white"
onClick={() => void feedBrain()}
>
{t("feed_form_submit_button", { ns: "upload" })}
</Button>
</div>
</>
</div>
);
};

View File

@ -2,20 +2,17 @@
import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useKnowledgeContext } from "@/lib/context/KnowledgeProvider/hooks/useKnowledgeContext";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import { useToast } from "@/lib/hooks";
import { redirectToLogin } from "@/lib/router/redirectToLogin";
import { useEventTracking } from "@/services/analytics/june/useEventTracking";
import { FeedItemType } from "../../../../../../app/chat/[chatId]/components/ActionsBar/types";
import { isValidUrl } from "../helpers/isValidUrl";
type UseCrawlerProps = {
addContent: (content: FeedItemType) => void;
};
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useCrawler = ({ addContent }: UseCrawlerProps) => {
export const useCrawler = () => {
const { addKnowledgeToFeed } = useKnowledgeContext();
const urlInputRef = useRef<HTMLInputElement | null>(null);
const { session } = useSupabase();
const { publish } = useToast();
@ -41,7 +38,7 @@ export const useCrawler = ({ addContent }: UseCrawlerProps) => {
return;
}
void track("URL_CRAWLED");
addContent({
addKnowledgeToFeed({
source: "crawl",
url: urlToCrawl,
});

View File

@ -6,16 +6,9 @@ import Button from "@/lib/components/ui/Button";
import Field from "@/lib/components/ui/Field";
import { useCrawler } from "./hooks/useCrawler";
import { FeedItemType } from "../../../../../app/chat/[chatId]/components/ActionsBar/types";
type CrawlerProps = {
addContent: (content: FeedItemType) => void;
};
export const Crawler = ({ addContent }: CrawlerProps): JSX.Element => {
const { urlInputRef, urlToCrawl, handleSubmit, setUrlToCrawl } = useCrawler({
addContent,
});
export const Crawler = (): JSX.Element => {
const { urlInputRef, urlToCrawl, handleSubmit, setUrlToCrawl } = useCrawler();
const { t } = useTranslation(["translation", "upload"]);
return (

View File

@ -1,36 +1,30 @@
import { Fragment } from "react";
import { useKnowledgeContext } from "@/lib/context/KnowledgeProvider/hooks/useKnowledgeContext";
import { CrawlFeedItem } from "./components/CrawlFeedItem";
import { FileFeedItem } from "./components/FileFeedItem/FileFeedItem";
import { FeedItemType } from "../../../../../app/chat/[chatId]/components/ActionsBar/types";
type FeedItemsProps = {
contents: FeedItemType[];
removeContent: (index: number) => void;
};
export const FeedItems = ({
contents,
removeContent,
}: FeedItemsProps): JSX.Element => {
if (contents.length === 0) {
export const FeedItems = (): JSX.Element => {
const { knowledgeToFeed, removeKnowledgeToFeed } = useKnowledgeContext();
if (knowledgeToFeed.length === 0) {
return <Fragment />;
}
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 mt-1 bg-white p-6 overflow-scroll">
{contents.map((item, index) =>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 mt-1 bg-white py-6 overflow-scroll">
{knowledgeToFeed.map((item, index) =>
item.source === "crawl" ? (
<CrawlFeedItem
key={item.url}
url={item.url}
onRemove={() => removeContent(index)}
onRemove={() => removeKnowledgeToFeed(index)}
/>
) : (
<FileFeedItem
key={item.file.name}
file={item.file}
onRemove={() => removeContent(index)}
onRemove={() => removeKnowledgeToFeed(index)}
/>
)
)}

View File

@ -1,108 +0,0 @@
/* eslint-disable max-lines */
import { FileRejection, useDropzone } from "react-dropzone";
import { useTranslation } from "react-i18next";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import { useToast } from "@/lib/hooks";
import { redirectToLogin } from "@/lib/router/redirectToLogin";
import { SupportedFileExtensionsWithDot } from "@/lib/types/SupportedFileExtensions";
import { useEventTracking } from "@/services/analytics/june/useEventTracking";
import { FeedItemType } from "../../../../../../app/chat/[chatId]/components/ActionsBar/types";
type UseFileUploaderProps = {
addContent: (content: FeedItemType) => void;
files: File[];
};
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useFileUploader = ({
addContent,
files,
}: UseFileUploaderProps) => {
const { publish } = useToast();
const { session } = useSupabase();
const { track } = useEventTracking();
if (session === null) {
redirectToLogin();
}
const { t } = useTranslation(["upload"]);
const onDrop = (acceptedFiles: File[], fileRejections: FileRejection[]) => {
if (fileRejections.length > 0) {
const firstRejection = fileRejections[0];
if (firstRejection.errors[0].code === "file-invalid-type") {
publish({ variant: "danger", text: t("invalidFileType") });
} else {
publish({
variant: "danger",
text: t("maxSizeError", { ns: "upload" }),
});
}
return;
}
for (const file of acceptedFiles) {
const isAlreadyInFiles =
files.filter((f) => f.name === file.name && f.size === file.size)
.length > 0;
if (isAlreadyInFiles) {
publish({
variant: "warning",
text: t("alreadyAdded", { fileName: file.name, ns: "upload" }),
});
} else {
void track("FILE_UPLOADED");
addContent({
source: "upload",
file: file,
});
}
}
};
const accept: Record<string, SupportedFileExtensionsWithDot[]> = {
"text/plain": [".txt"],
"text/csv": [".csv"],
"text/markdown": [".md", ".markdown"],
"audio/x-m4a": [".m4a"],
"audio/mpeg": [".mp3", ".mpga", ".mpeg"],
"audio/webm": [".webm"],
"video/mp4": [".mp4"],
"audio/wav": [".wav"],
"application/pdf": [".pdf"],
"text/html": [".html"],
"application/vnd.openxmlformats-officedocument.presentationml.presentation":
[".pptx"],
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": [
".docx",
],
"application/vnd.oasis.opendocument.text": [".odt"],
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [
".xlsx",
".xls",
],
"application/epub+zip": [".epub"],
"application/x-ipynb+json": [".ipynb"],
"text/x-python": [".py"],
};
const { getInputProps, getRootProps, isDragActive, open } = useDropzone({
onDrop,
noClick: true,
maxSize: 100000000, // 1 MB
accept,
});
return {
getInputProps,
getRootProps,
isDragActive,
open,
};
};

View File

@ -3,34 +3,26 @@ import { useTranslation } from "react-i18next";
import { IoCloudUploadOutline } from "react-icons/io5";
import Card from "@/lib/components/ui/Card";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import { useCustomDropzone } from "@/lib/hooks/useDropzone";
import { redirectToLogin } from "@/lib/router/redirectToLogin";
import { useFileUploader } from "./hooks/useFileUploader";
import { FeedItemType } from "../../../../../app/chat/[chatId]/components/ActionsBar/types";
export const FileUploader = (): JSX.Element => {
const { session } = useSupabase();
const { getInputProps, isDragActive, open } = useCustomDropzone();
type FileUploaderProps = {
addContent: (content: FeedItemType) => void;
files: File[];
};
export const FileUploader = ({
addContent,
files,
}: FileUploaderProps): JSX.Element => {
const { getInputProps, getRootProps, isDragActive, open } = useFileUploader({
addContent,
files,
});
if (session === null) {
redirectToLogin();
}
const { t } = useTranslation(["translation", "upload"]);
return (
<section
{...getRootProps()}
className="w-full outline-none flex flex-col gap-10 items-center justify-center px-6 py-3"
>
<div className="flex flex-col sm:flex-row max-w-3xl w-full items-center gap-5 mt-5 cursor-pointer">
<section className="w-full outline-none flex flex-col gap-10 items-center justify-center px-0">
<div className="flex flex-col sm:flex-row max-w-3xl w-full items-center gap-5 cursor-pointer">
<div className="flex-1 w-full">
<Card
className="h-24 flex justify-center items-center"
className="h-20 flex justify-center items-center"
onClick={open}
>
<IoCloudUploadOutline className="text-5xl" />

View File

@ -10,12 +10,12 @@ import { useNotificationApi } from "@/lib/api/notification/useNotificationApi";
import { useUploadApi } from "@/lib/api/upload/useUploadApi";
import { useChatContext } from "@/lib/context";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { useKnowledgeContext } from "@/lib/context/KnowledgeProvider/hooks/useKnowledgeContext";
import { getAxiosErrorParams } from "@/lib/helpers/getAxiosErrorParams";
import { useToast } from "@/lib/hooks";
import {
FeedItemCrawlType,
FeedItemType,
FeedItemUploadType,
} from "../../../../app/chat/[chatId]/components/ActionsBar/types";
@ -28,7 +28,6 @@ export const useKnowledgeToFeedInput = ({
dispatchHasPendingRequests,
closeFeedInput,
}: UseKnowledgeToFeedInput) => {
const [contents, setContents] = useState<FeedItemType[]>([]);
const { publish } = useToast();
const { uploadFile } = useUploadApi();
const { t } = useTranslation(["upload"]);
@ -41,14 +40,7 @@ export const useKnowledgeToFeedInput = ({
const params = useParams();
const chatId = params?.chatId as UUID | undefined;
const [hasPendingRequests, setHasPendingRequests] = useState(false);
const addContent = (content: FeedItemType) => {
setContents((prevContents) => [...prevContents, content]);
};
const removeContent = (index: number) => {
setContents((prevContents) => prevContents.filter((_, i) => i !== index));
};
const { setKnowledgeToFeed, knowledgeToFeed } = useKnowledgeContext();
const fetchNotifications = async (currentChatId: UUID): Promise<void> => {
const fetchedNotifications = await getChatNotifications(currentChatId);
setNotifications(fetchedNotifications);
@ -127,11 +119,11 @@ export const useKnowledgeToFeedInput = ({
);
const files: File[] = (
contents.filter((c) => c.source === "upload") as FeedItemUploadType[]
knowledgeToFeed.filter((c) => c.source === "upload") as FeedItemUploadType[]
).map((c) => c.file);
const urls: string[] = (
contents.filter((c) => c.source === "crawl") as FeedItemCrawlType[]
knowledgeToFeed.filter((c) => c.source === "crawl") as FeedItemCrawlType[]
).map((c) => c.url);
const feedBrain = async (): Promise<void> => {
@ -144,7 +136,7 @@ export const useKnowledgeToFeedInput = ({
return;
}
if (contents.length === 0) {
if (knowledgeToFeed.length === 0) {
publish({
variant: "danger",
text: t("addFiles"),
@ -166,7 +158,7 @@ export const useKnowledgeToFeedInput = ({
await Promise.all([...uploadPromises, ...crawlPromises]);
setContents([]);
setKnowledgeToFeed([]);
if (chatId === undefined) {
void router.push(`/chat/${currentChatId}`);
@ -184,9 +176,6 @@ export const useKnowledgeToFeedInput = ({
};
return {
addContent,
contents,
removeContent,
feedBrain,
hasPendingRequests,
setHasPendingRequests,

View File

@ -2,6 +2,7 @@
import { BsCheckCircleFill } from "react-icons/bs";
import Popover from "@/lib/components/ui/Popover";
import { cn } from "@/lib/utils";
export type SelectOptionProps<T> = {
label: string;
@ -14,6 +15,8 @@ type SelectProps<T> = {
onChange: (option: T) => void;
label?: string;
readOnly?: boolean;
className?: string;
emptyLabel?: string;
};
const selectedStyle = "rounded-lg bg-black text-white";
@ -24,6 +27,8 @@ export const Select = <T extends string | number>({
value,
label,
readOnly = false,
className,
emptyLabel,
}: SelectProps<T>): JSX.Element => {
const selectedValueLabel = options.find(
(option) => option.value === value
@ -31,11 +36,11 @@ export const Select = <T extends string | number>({
if (readOnly) {
return (
<div>
<div className={cn("gap-2", className)}>
{label !== undefined && (
<label
id="listbox-label"
className="block text-sm font-medium leading-6 text-gray-900 mb-2"
className="block text-sm font-medium leading-6 text-gray-900"
>
{label}
</label>
@ -49,7 +54,7 @@ export const Select = <T extends string | number>({
>
<span className="flex items-center">
<span className="mx-4 block truncate">
{selectedValueLabel ?? label ?? "Select"}
{selectedValueLabel ?? emptyLabel ?? "-"}
</span>
</span>
</button>
@ -59,11 +64,11 @@ export const Select = <T extends string | number>({
}
return (
<div>
<div className={cn("gap-2", className)}>
{label !== undefined && (
<label
id="listbox-label"
className="block text-sm font-medium leading-6 text-gray-900 mb-2"
className="block text-sm font-medium leading-6 text-gray-900"
>
{label}
</label>
@ -78,7 +83,7 @@ export const Select = <T extends string | number>({
>
<span className="flex items-center">
<span className="ml-3 block truncate">
{selectedValueLabel ?? label ?? "Select"}
{selectedValueLabel ?? emptyLabel ?? "-"}
</span>
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 ml-3 flex items-center pr-2">

View File

@ -1,14 +1,32 @@
import { useContext } from "react";
import { FeedItemType } from "@/app/chat/[chatId]/components/ActionsBar/types";
import { KnowledgeContext } from "../knowledge-provider";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useKnowledgeContext = () => {
const context = useContext(KnowledgeContext);
const addKnowledgeToFeed = (knowledge: FeedItemType) => {
context?.setKnowledgeToFeed((prevKnowledge) => [
...prevKnowledge,
knowledge,
]);
};
const removeKnowledgeToFeed = (index: number) => {
context?.setKnowledgeToFeed((prevKnowledge) => {
const newKnowledge = [...prevKnowledge];
newKnowledge.splice(index, 1);
return newKnowledge;
});
};
if (context === undefined) {
throw new Error("useKnowledge must be used inside KnowledgeProvider");
}
return context;
return { ...context, addKnowledgeToFeed, removeKnowledgeToFeed };
};

View File

@ -2,11 +2,14 @@
import { createContext, useState } from "react";
import { FeedItemType } from "@/app/chat/[chatId]/components/ActionsBar/types";
import { Knowledge } from "@/lib/types/Knowledge";
type KnowledgeContextType = {
allKnowledge: Knowledge[];
setAllKnowledge: React.Dispatch<React.SetStateAction<Knowledge[]>>;
knowledgeToFeed: FeedItemType[];
setKnowledgeToFeed: React.Dispatch<React.SetStateAction<FeedItemType[]>>;
};
export const KnowledgeContext = createContext<KnowledgeContextType | undefined>(
@ -19,12 +22,15 @@ export const KnowledgeProvider = ({
children: React.ReactNode;
}): JSX.Element => {
const [allKnowledge, setAllKnowledge] = useState<Knowledge[]>([]);
const [knowledgeToFeed, setKnowledgeToFeed] = useState<FeedItemType[]>([]);
return (
<KnowledgeContext.Provider
value={{
allKnowledge,
setAllKnowledge,
knowledgeToFeed,
setKnowledgeToFeed,
}}
>
{children}

View File

@ -0,0 +1,28 @@
import { SupportedFileExtensionsWithDot } from "@/lib/types/SupportedFileExtensions";
export const acceptedFormats: Record<string, SupportedFileExtensionsWithDot[]> =
{
"text/plain": [".txt"],
"text/csv": [".csv"],
"text/markdown": [".md", ".markdown"],
"audio/x-m4a": [".m4a"],
"audio/mpeg": [".mp3", ".mpga", ".mpeg"],
"audio/webm": [".webm"],
"video/mp4": [".mp4"],
"audio/wav": [".wav"],
"application/pdf": [".pdf"],
"text/html": [".html"],
"application/vnd.openxmlformats-officedocument.presentationml.presentation":
[".pptx"],
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": [
".docx",
],
"application/vnd.oasis.opendocument.text": [".odt"],
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [
".xlsx",
".xls",
],
"application/epub+zip": [".epub"],
"application/x-ipynb+json": [".ipynb"],
"text/x-python": [".py"],
};

View File

@ -0,0 +1,72 @@
import { FileRejection, useDropzone } from "react-dropzone";
import { useTranslation } from "react-i18next";
import { FeedItemUploadType } from "@/app/chat/[chatId]/components/ActionsBar/types";
import { useEventTracking } from "@/services/analytics/june/useEventTracking";
import { useToast } from "./useToast";
import { useKnowledgeContext } from "../context/KnowledgeProvider/hooks/useKnowledgeContext";
import { acceptedFormats } from "../helpers/acceptedFormats";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useCustomDropzone = () => {
const { knowledgeToFeed, addKnowledgeToFeed } = useKnowledgeContext();
const files: File[] = (
knowledgeToFeed.filter((c) => c.source === "upload") as FeedItemUploadType[]
).map((c) => c.file);
const { publish } = useToast();
const { track } = useEventTracking();
const { t } = useTranslation(["upload"]);
const onDrop = (acceptedFiles: File[], fileRejections: FileRejection[]) => {
if (fileRejections.length > 0) {
const firstRejection = fileRejections[0];
if (firstRejection.errors[0].code === "file-invalid-type") {
publish({ variant: "danger", text: t("invalidFileType") });
} else {
publish({
variant: "danger",
text: t("maxSizeError", { ns: "upload" }),
});
}
return;
}
for (const file of acceptedFiles) {
const isAlreadyInFiles =
files.filter((f) => f.name === file.name && f.size === file.size)
.length > 0;
if (isAlreadyInFiles) {
publish({
variant: "warning",
text: t("alreadyAdded", { fileName: file.name, ns: "upload" }),
});
} else {
void track("FILE_UPLOADED");
addKnowledgeToFeed({
source: "upload",
file: file,
});
}
}
};
const { getInputProps, getRootProps, isDragActive, open } = useDropzone({
onDrop,
noClick: true,
maxSize: 100000000, // 1 MB
accept: acceptedFormats,
});
return {
getInputProps,
getRootProps,
isDragActive,
open,
};
};

View File

@ -21,6 +21,7 @@
"email": "Email",
"password": "Password",
"or": "or",
"and": "and",
"loginButton": "Login",
"signUpButton": "Sign up",
"logoutButton": "Logout",

View File

@ -26,6 +26,7 @@
"open_source_desc": "La libertad es hermosa, al igual que Quivr. Código abierto y de uso gratuito.",
"open_source_title": "Open source",
"or": "o",
"and": "y",
"Owner": "Propietario",
"password": "Contraseña",
"resetButton": "Restaurar",

View File

@ -21,6 +21,7 @@
"email": "Email",
"password": "Mot de passe",
"or": "ou",
"and": "et",
"loginButton": "Connexion",
"signUpButton": "S'inscrire",
"logoutButton": "Déconnexion",

View File

@ -21,6 +21,7 @@
"email": "Email",
"password": "Senha",
"or": "ou",
"and": "e",
"loginButton": "Entrar",
"signUpButton": "Cadastre-se",
"logoutButton": "Sair",

View File

@ -21,6 +21,7 @@
"email": "Email",
"password": "Пароль",
"or": "или",
"and": "и",
"loginButton": "Войти",
"signUpButton": "Зарегистрироваться",
"logoutButton": "Выйти",

View File

@ -21,6 +21,7 @@
"email": "Email",
"password": "密码",
"or": "或",
"and": "和",
"loginButton": "登录",
"signUpButton": "注册",
"logoutButton": "注销",