diff --git a/frontend/app/studio/BrainsTabs/components/ManageBrains/BrainsList/BrainItem/BrainItem.module.scss b/frontend/app/studio/BrainsTabs/components/ManageBrains/BrainsList/BrainItem/BrainItem.module.scss index e236ba367..c4e30ebad 100644 --- a/frontend/app/studio/BrainsTabs/components/ManageBrains/BrainsList/BrainItem/BrainItem.module.scss +++ b/frontend/app/studio/BrainsTabs/components/ManageBrains/BrainsList/BrainItem/BrainItem.module.scss @@ -1,3 +1,4 @@ +@use "styles/BoxShadow.module.scss"; @use "styles/Radius.module.scss"; @use "styles/ScreenSizes.module.scss"; @use "styles/Spacings.module.scss"; @@ -19,6 +20,7 @@ overflow: visible; overflow: hidden; height: 100%; + box-shadow: BoxShadow.$small; &:hover { border-color: var(--primary-0); diff --git a/frontend/app/studio/[brainId]/BrainManagementTabs/components/KnowledgeTab/KnowledgeTab.module.scss b/frontend/app/studio/[brainId]/BrainManagementTabs/components/KnowledgeTab/KnowledgeTab.module.scss index 7f00e2342..3afe7cd99 100644 --- a/frontend/app/studio/[brainId]/BrainManagementTabs/components/KnowledgeTab/KnowledgeTab.module.scss +++ b/frontend/app/studio/[brainId]/BrainManagementTabs/components/KnowledgeTab/KnowledgeTab.module.scss @@ -3,5 +3,5 @@ .knowledge_tab_wrapper { display: flex; justify-content: center; - padding: Spacings.$spacing05; -} \ No newline at end of file + padding-block: Spacings.$spacing05; +} diff --git a/frontend/app/studio/[brainId]/BrainManagementTabs/components/KnowledgeTab/KnowledgeTable/KnowledgeItem/KnowledgeItem.module.scss b/frontend/app/studio/[brainId]/BrainManagementTabs/components/KnowledgeTab/KnowledgeTable/KnowledgeItem/KnowledgeItem.module.scss index 1e65d94d1..3d249381c 100644 --- a/frontend/app/studio/[brainId]/BrainManagementTabs/components/KnowledgeTab/KnowledgeTable/KnowledgeItem/KnowledgeItem.module.scss +++ b/frontend/app/studio/[brainId]/BrainManagementTabs/components/KnowledgeTab/KnowledgeTable/KnowledgeItem/KnowledgeItem.module.scss @@ -5,22 +5,27 @@ @use "styles/ZIndexes.module.scss"; .knowledge_item_wrapper { - padding-inline: Spacings.$spacing05; + padding-inline: Spacings.$spacing06; overflow: hidden; display: flex; gap: Spacings.$spacing02; justify-content: space-between; align-items: center; - margin-block: Spacings.$spacing03; - border: 1px solid var(--border-1); - border-radius: Radius.$normal; + border: 1px solid var(--border-0); padding-block: Spacings.$spacing03; position: relative; overflow: visible; + font-size: Typography.$small; + border-bottom: none; + + &.last { + border-radius: 0 0 Radius.$normal Radius.$normal; + border-bottom: 1px solid var(--border-0); + } &:hover { - border-color: var(--primary-0); - background-color: var(--background-special-0); + background-color: var(--background-1); + cursor: pointer; } .left { @@ -29,10 +34,6 @@ gap: Spacings.$spacing03; overflow: hidden; - .icon { - min-width: 16px; - } - .file_name { @include Typography.EllipsisOverflow; } @@ -45,4 +46,4 @@ z-index: ZIndexes.$modal; padding-bottom: Spacings.$spacing01; } -} \ No newline at end of file +} diff --git a/frontend/app/studio/[brainId]/BrainManagementTabs/components/KnowledgeTab/KnowledgeTable/KnowledgeItem/KnowledgeItem.tsx b/frontend/app/studio/[brainId]/BrainManagementTabs/components/KnowledgeTab/KnowledgeTable/KnowledgeItem/KnowledgeItem.tsx index cae43f363..fe97a5440 100644 --- a/frontend/app/studio/[brainId]/BrainManagementTabs/components/KnowledgeTab/KnowledgeTable/KnowledgeItem/KnowledgeItem.tsx +++ b/frontend/app/studio/[brainId]/BrainManagementTabs/components/KnowledgeTab/KnowledgeTable/KnowledgeItem/KnowledgeItem.tsx @@ -1,11 +1,11 @@ -"use client"; import axios from "axios"; -import { useEffect, useRef, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { useKnowledgeApi } from "@/lib/api/knowledge/useKnowledgeApi"; +import { Checkbox } from "@/lib/components/ui/Checkbox/Checkbox"; import Icon from "@/lib/components/ui/Icon/Icon"; import { OptionsModal } from "@/lib/components/ui/OptionsModal/OptionsModal"; -import { getFileIcon } from "@/lib/helpers/getFileIcon"; +import { iconList } from "@/lib/helpers/iconList"; import { useUrlBrain } from "@/lib/hooks/useBrainIdFromUrl"; import { isUploadedKnowledge, Knowledge } from "@/lib/types/Knowledge"; import { Option } from "@/lib/types/Options"; @@ -16,8 +16,14 @@ import styles from "./KnowledgeItem.module.scss"; const KnowledgeItem = ({ knowledge, + selected, + setSelected, + lastChild, }: { knowledge: Knowledge; + selected: boolean; + setSelected: (selected: boolean, event: React.MouseEvent) => void; + lastChild?: boolean; }): JSX.Element => { const [optionsOpened, setOptionsOpened] = useState(false); const iconRef = useRef(null); @@ -89,13 +95,25 @@ const KnowledgeItem = ({ }, []); return ( -
+
+ setSelected(checked, event)} + />
{isUploadedKnowledge(knowledge) ? ( - getFileIcon(knowledge.fileName) + ) : ( - + )}
{isUploadedKnowledge(knowledge) ? ( @@ -109,11 +127,11 @@ const KnowledgeItem = ({
) => { - event.nativeEvent.stopImmediatePropagation(); + event.stopPropagation(); setOptionsOpened(!optionsOpened); }} > - +
{optionsOpened && } diff --git a/frontend/app/studio/[brainId]/BrainManagementTabs/components/KnowledgeTab/KnowledgeTable/KnowledgeItem/hooks/useKnowledgeItem.ts b/frontend/app/studio/[brainId]/BrainManagementTabs/components/KnowledgeTab/KnowledgeTable/KnowledgeItem/hooks/useKnowledgeItem.ts index 368c3ed85..0492b53cd 100644 --- a/frontend/app/studio/[brainId]/BrainManagementTabs/components/KnowledgeTab/KnowledgeTable/KnowledgeItem/hooks/useKnowledgeItem.ts +++ b/frontend/app/studio/[brainId]/BrainManagementTabs/components/KnowledgeTab/KnowledgeTable/KnowledgeItem/hooks/useKnowledgeItem.ts @@ -16,7 +16,7 @@ export const useKnowledgeItem = () => { const [isDeleting, setIsDeleting] = useState(false); const { publish } = useToast(); const { track } = useEventTracking(); - const { brainId, brain } = useUrlBrain(); + const { brainId } = useUrlBrain(); const { invalidateKnowledgeDataKey } = useKnowledge({ brainId, }); @@ -38,15 +38,6 @@ export const useKnowledgeItem = () => { }); invalidateKnowledgeDataKey(); - - publish({ - variant: "success", - text: t("deleted", { - fileName: knowledge_name, - brain: brain?.name, - ns: "explore", - }), - }); } catch (error) { publish({ variant: "warning", diff --git a/frontend/app/studio/[brainId]/BrainManagementTabs/components/KnowledgeTab/KnowledgeTable/KnowledgeTable.module.scss b/frontend/app/studio/[brainId]/BrainManagementTabs/components/KnowledgeTab/KnowledgeTable/KnowledgeTable.module.scss index e4e32d8a0..59223eed8 100644 --- a/frontend/app/studio/[brainId]/BrainManagementTabs/components/KnowledgeTab/KnowledgeTable/KnowledgeTable.module.scss +++ b/frontend/app/studio/[brainId]/BrainManagementTabs/components/KnowledgeTab/KnowledgeTable/KnowledgeTable.module.scss @@ -6,8 +6,46 @@ display: flex; flex-direction: column; gap: Spacings.$spacing05; + padding-bottom: Spacings.$spacing10; + border-radius: Radius.$normal; .title { @include Typography.H3; } -} \ No newline at end of file + + .table_header { + display: flex; + justify-content: space-between; + align-items: center; + gap: Spacings.$spacing03; + + .search { + width: 250px; + } + } + + .first_line { + display: flex; + justify-content: space-between; + padding-left: calc(Spacings.$spacing06); + padding-right: Spacings.$spacing04; + padding-block: Spacings.$spacing02; + font-weight: 500; + background-color: var(--background-1); + font-size: Typography.$small; + border: 1px solid var(--border-0); + border-radius: Radius.$normal Radius.$normal 0 0; + border-bottom: none; + + &.empty { + border: 1px solid var(--border-0); + border-radius: Radius.$normal; + } + + .left { + display: flex; + align-items: center; + gap: Spacings.$spacing06; + } + } +} diff --git a/frontend/app/studio/[brainId]/BrainManagementTabs/components/KnowledgeTab/KnowledgeTable/KnowledgeTable.tsx b/frontend/app/studio/[brainId]/BrainManagementTabs/components/KnowledgeTab/KnowledgeTable/KnowledgeTable.tsx index ba142f80d..158d2ee1c 100644 --- a/frontend/app/studio/[brainId]/BrainManagementTabs/components/KnowledgeTab/KnowledgeTable/KnowledgeTable.tsx +++ b/frontend/app/studio/[brainId]/BrainManagementTabs/components/KnowledgeTab/KnowledgeTable/KnowledgeTable.tsx @@ -1,7 +1,12 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; -import { Knowledge } from "@/lib/types/Knowledge"; +import { Checkbox } from "@/lib/components/ui/Checkbox/Checkbox"; +import QuivrButton from "@/lib/components/ui/QuivrButton/QuivrButton"; +import { TextInput } from "@/lib/components/ui/TextInput/TextInput"; +import { isUploadedKnowledge, Knowledge } from "@/lib/types/Knowledge"; +import { useKnowledgeItem } from "./KnowledgeItem/hooks/useKnowledgeItem"; +// eslint-disable-next-line import/order import KnowledgeItem from "./KnowledgeItem/KnowledgeItem"; import styles from "./KnowledgeTable.module.scss"; @@ -11,12 +16,133 @@ interface KnowledgeTableProps { const KnowledgeTable = React.forwardRef( ({ knowledgeList }, ref) => { + const [selectedKnowledge, setSelectedKnowledge] = useState([]); + const [lastSelectedIndex, setLastSelectedIndex] = useState( + null + ); + const { onDeleteKnowledge } = useKnowledgeItem(); + const [allChecked, setAllChecked] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const [filteredKnowledgeList, setFilteredKnowledgeList] = + useState(knowledgeList); + + useEffect(() => { + setFilteredKnowledgeList( + knowledgeList.filter((knowledge) => + isUploadedKnowledge(knowledge) + ? knowledge.fileName + .toLowerCase() + .includes(searchQuery.toLowerCase()) + : knowledge.url.toLowerCase().includes(searchQuery.toLowerCase()) + ) + ); + }, [searchQuery, knowledgeList]); + + const handleSelect = ( + knowledge: Knowledge, + index: number, + event: React.MouseEvent + ) => { + if (event.shiftKey && lastSelectedIndex !== null) { + const start = Math.min(lastSelectedIndex, index); + const end = Math.max(lastSelectedIndex, index); + const range = filteredKnowledgeList.slice(start, end + 1); + + setSelectedKnowledge((prevSelected) => { + const newSelected = [...prevSelected]; + range.forEach((item) => { + if ( + !newSelected.some((selectedItem) => selectedItem.id === item.id) + ) { + newSelected.push(item); + } + }); + + return newSelected; + }); + } else { + const isSelected = selectedKnowledge.some( + (item) => item.id === knowledge.id + ); + setSelectedKnowledge((prevSelected) => + isSelected + ? prevSelected.filter( + (selectedItem) => selectedItem.id !== knowledge.id + ) + : [...prevSelected, knowledge] + ); + setLastSelectedIndex( + isSelected && lastSelectedIndex === index ? null : index + ); + } + }; + + const handleDelete = () => { + const toDelete = selectedKnowledge.filter((knowledge) => + filteredKnowledgeList.some((item) => item.id === knowledge.id) + ); + toDelete.forEach((knowledge) => { + void onDeleteKnowledge(knowledge); + }); + setSelectedKnowledge([]); + }; + return (
Uploaded Knowledge +
+
+ +
+ +
- {knowledgeList.map((knowledge) => ( - +
+
+ { + setAllChecked(checked); + checked + ? setSelectedKnowledge(filteredKnowledgeList) + : setSelectedKnowledge([]); + }} + /> + Name +
+ Actions +
+ {filteredKnowledgeList.map((knowledge, index) => ( +
handleSelect(knowledge, index, event)} + > + item.id === knowledge.id + )} + setSelected={(_selected, event) => + handleSelect(knowledge, index, event) + } + lastChild={index === filteredKnowledgeList.length - 1} + /> +
))}
diff --git a/frontend/lib/components/ui/Checkbox/Checkbox.module.scss b/frontend/lib/components/ui/Checkbox/Checkbox.module.scss index 1df30363b..022043221 100644 --- a/frontend/lib/components/ui/Checkbox/Checkbox.module.scss +++ b/frontend/lib/components/ui/Checkbox/Checkbox.module.scss @@ -6,18 +6,20 @@ align-items: center; gap: Spacings.$spacing03; cursor: pointer; + border-radius: Radius.$normal; .checkbox { width: 16px; height: 16px; border: 1px solid var(--border-2); - border-radius: Radius.$small; display: flex; justify-content: center; align-items: center; + border-radius: Radius.$normal; &.filled { background-color: var(--primary-0); + border-color: var(--primary-0); } } @@ -30,4 +32,4 @@ &:hover { background-color: var(--background-3); } -} \ No newline at end of file +} diff --git a/frontend/lib/components/ui/Checkbox/Checkbox.tsx b/frontend/lib/components/ui/Checkbox/Checkbox.tsx index 5a29f502c..31f5e7d97 100644 --- a/frontend/lib/components/ui/Checkbox/Checkbox.tsx +++ b/frontend/lib/components/ui/Checkbox/Checkbox.tsx @@ -7,7 +7,7 @@ import { Icon } from "../Icon/Icon"; interface CheckboxProps { label?: string; checked: boolean; - setChecked: (value: boolean) => void; + setChecked: (value: boolean, event: React.MouseEvent) => void; disabled?: boolean; } @@ -31,7 +31,7 @@ export const Checkbox = ({ onClick={(event) => { event.stopPropagation(); if (!disabled) { - setChecked(!currentChecked); + setChecked(!currentChecked, event); setCurrentChecked(!currentChecked); } }} diff --git a/frontend/lib/helpers/getFileIcon.tsx b/frontend/lib/helpers/getFileIcon.tsx index 3cec9f5cf..e44606657 100644 --- a/frontend/lib/helpers/getFileIcon.tsx +++ b/frontend/lib/helpers/getFileIcon.tsx @@ -43,7 +43,7 @@ const fileTypeIcons: Record = { ipynb: BsFiletypePy, py: BsFiletypePy, telegram: BsFiletypeDocx, - bib: FaFile + bib: FaFile, }; export const getFileIcon = (fileName: string): JSX.Element => { @@ -51,5 +51,5 @@ export const getFileIcon = (fileName: string): JSX.Element => { const Icon = fileType !== undefined ? fileTypeIcons[fileType] : FaFile; - return ; + return ; }; diff --git a/frontend/lib/helpers/iconList.ts b/frontend/lib/helpers/iconList.ts index 06f7eca85..035e70cf9 100644 --- a/frontend/lib/helpers/iconList.ts +++ b/frontend/lib/helpers/iconList.ts @@ -3,6 +3,18 @@ import { BiCoin } from "react-icons/bi"; import { BsArrowRightShort, BsChatLeftText, + BsFiletypeCsv, + BsFiletypeDocx, + BsFiletypeHtml, + BsFiletypeMd, + BsFiletypeMp3, + BsFiletypeMp4, + BsFiletypePdf, + BsFiletypePptx, + BsFiletypePy, + BsFiletypeTxt, + BsFiletypeXls, + BsFiletypeXlsx, BsTextParagraph, } from "react-icons/bs"; import { CgSoftwareDownload } from "react-icons/cg"; @@ -12,6 +24,7 @@ import { FaCheck, FaCheckCircle, FaDiscord, + FaFile, FaFileAlt, FaFolder, FaGithub, @@ -19,6 +32,7 @@ import { FaLinkedin, FaQuestionCircle, FaRegFileAlt, + FaRegFileAudio, FaRegKeyboard, FaRegStar, FaRegThumbsDown, @@ -50,7 +64,7 @@ import { IoShareSocial, IoWarningOutline, } from "react-icons/io5"; -import { LiaRobotSolid } from "react-icons/lia"; +import { LiaFileVideo, LiaRobotSolid } from "react-icons/lia"; import { IconType } from "react-icons/lib"; import { LuBrain, @@ -91,6 +105,7 @@ export const iconList: { [name: string]: IconType } = { addWithoutCircle: IoIosAdd, assistant: TbRobot, back: RiDeleteBackLine, + bib: FaFile, brain: LuBrain, brainCircuit: LuBrainCircuit, calendar: FaCalendar, @@ -107,9 +122,11 @@ export const iconList: { [name: string]: IconType } = { custom: MdDashboardCustomize, delete: MdDeleteOutline, discord: FaDiscord, + docx: BsFiletypeDocx, download: IoCloudDownloadOutline, edit: MdOutlineModeEditOutline, email: MdAlternateEmail, + epub: FaFile, eureka: GoLightBulb, externLink: LuExternalLink, feed: MdDynamicFeed, @@ -126,22 +143,34 @@ export const iconList: { [name: string]: IconType } = { help: IoIosHelpCircleOutline, history: MdHistory, home: IoHomeOutline, + html: BsFiletypeHtml, info: FaInfo, + ipynb: BsFiletypePy, key: FaKey, link: MdLink, linkedin: FaLinkedin, loader: AiOutlineLoading3Quarters, logout: IoMdLogOut, + m4a: LiaFileVideo, + markdown: BsFiletypeMd, + md: BsFiletypeMd, moon: MdDarkMode, + mp3: BsFiletypeMp3, + mp4: BsFiletypeMp4, + mpga: FaRegFileAudio, + mpeg: LiaFileVideo, notifications: IoIosNotifications, office: HiBuildingOffice, + odt: BsFiletypeDocx, options: SlOptions, paragraph: BsTextParagraph, + pptx: BsFiletypePptx, prompt: FaRegKeyboard, + py: BsFiletypePy, question: FaQuestionCircle, - redirection: BsArrowRightShort, radio: IoIosRadio, read: MdMarkEmailRead, + redirection: BsArrowRightShort, robot: LiaRobotSolid, search: LuSearch, settings: IoMdSettings, @@ -152,6 +181,7 @@ export const iconList: { [name: string]: IconType } = { step: IoFootsteps, sun: MdOutlineLightMode, sync: IoMdSync, + telegram: BsFiletypeDocx, thumbsDown: FaRegThumbsDown, thumbsUp: FaRegThumbsUp, twitter: FaTwitter, @@ -162,5 +192,12 @@ export const iconList: { [name: string]: IconType } = { uploadFile: MdUploadFile, user: FaRegUserCircle, warning: IoWarningOutline, + wav: FaRegFileAudio, + webm: LiaFileVideo, website: TbNetwork, + xls: BsFiletypeXls, + xlsx: BsFiletypeXlsx, + txt: BsFiletypeTxt, + csv: BsFiletypeCsv, + pdf: BsFiletypePdf, };