mirror of
https://github.com/QuivrHQ/quivr.git
synced 2024-08-17 02:10:31 +03:00
feat(frontend): new ui ux for knowledge (#2732)
# 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:
parent
cb2442d76b
commit
fe41b36788
@ -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);
|
||||
|
@ -3,5 +3,5 @@
|
||||
.knowledge_tab_wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: Spacings.$spacing05;
|
||||
}
|
||||
padding-block: Spacings.$spacing05;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<boolean>(false);
|
||||
const iconRef = useRef<HTMLDivElement | null>(null);
|
||||
@ -89,13 +95,25 @@ const KnowledgeItem = ({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.knowledge_item_wrapper}>
|
||||
<div
|
||||
className={`${styles.knowledge_item_wrapper} ${
|
||||
lastChild ? styles.last : ""
|
||||
}`}
|
||||
>
|
||||
<div className={styles.left}>
|
||||
<Checkbox
|
||||
checked={selected}
|
||||
setChecked={(checked, event) => setSelected(checked, event)}
|
||||
/>
|
||||
<div className={styles.icon}>
|
||||
{isUploadedKnowledge(knowledge) ? (
|
||||
getFileIcon(knowledge.fileName)
|
||||
<Icon
|
||||
name={knowledge.extension.slice(1) as keyof typeof iconList}
|
||||
size="small"
|
||||
color="black"
|
||||
/>
|
||||
) : (
|
||||
<Icon name="link" size="normal" color="black" />
|
||||
<Icon name="link" size="small" color="black" />
|
||||
)}
|
||||
</div>
|
||||
{isUploadedKnowledge(knowledge) ? (
|
||||
@ -109,11 +127,11 @@ const KnowledgeItem = ({
|
||||
<div
|
||||
ref={iconRef}
|
||||
onClick={(event: React.MouseEvent<HTMLElement>) => {
|
||||
event.nativeEvent.stopImmediatePropagation();
|
||||
event.stopPropagation();
|
||||
setOptionsOpened(!optionsOpened);
|
||||
}}
|
||||
>
|
||||
<Icon name="options" size="normal" color="black" handleHover={true} />
|
||||
<Icon name="options" size="small" color="black" handleHover={true} />
|
||||
</div>
|
||||
<div ref={optionsRef} className={styles.options_modal}>
|
||||
{optionsOpened && <OptionsModal options={options} />}
|
||||
|
@ -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",
|
||||
|
@ -6,8 +6,46 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing05;
|
||||
padding-bottom: Spacings.$spacing10;
|
||||
border-radius: Radius.$normal;
|
||||
|
||||
.title {
|
||||
@include Typography.H3;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<HTMLDivElement, KnowledgeTableProps>(
|
||||
({ knowledgeList }, ref) => {
|
||||
const [selectedKnowledge, setSelectedKnowledge] = useState<Knowledge[]>([]);
|
||||
const [lastSelectedIndex, setLastSelectedIndex] = useState<number | null>(
|
||||
null
|
||||
);
|
||||
const { onDeleteKnowledge } = useKnowledgeItem();
|
||||
const [allChecked, setAllChecked] = useState<boolean>(false);
|
||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||
const [filteredKnowledgeList, setFilteredKnowledgeList] =
|
||||
useState<Knowledge[]>(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 (
|
||||
<div ref={ref} className={styles.knowledge_table_wrapper}>
|
||||
<span className={styles.title}>Uploaded Knowledge</span>
|
||||
<div className={styles.table_header}>
|
||||
<div className={styles.search}>
|
||||
<TextInput
|
||||
iconName="search"
|
||||
label="Search"
|
||||
inputValue={searchQuery}
|
||||
setInputValue={setSearchQuery}
|
||||
/>
|
||||
</div>
|
||||
<QuivrButton
|
||||
label="Delete"
|
||||
iconName="delete"
|
||||
color="dangerous"
|
||||
disabled={selectedKnowledge.length === 0}
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{knowledgeList.map((knowledge) => (
|
||||
<KnowledgeItem knowledge={knowledge} key={knowledge.id} />
|
||||
<div
|
||||
className={`${styles.first_line} ${
|
||||
filteredKnowledgeList.length === 0 ? styles.empty : ""
|
||||
}`}
|
||||
>
|
||||
<div className={styles.left}>
|
||||
<Checkbox
|
||||
checked={allChecked}
|
||||
setChecked={(checked) => {
|
||||
setAllChecked(checked);
|
||||
checked
|
||||
? setSelectedKnowledge(filteredKnowledgeList)
|
||||
: setSelectedKnowledge([]);
|
||||
}}
|
||||
/>
|
||||
<span className={styles.name}>Name</span>
|
||||
</div>
|
||||
<span className={styles.actions}>Actions</span>
|
||||
</div>
|
||||
{filteredKnowledgeList.map((knowledge, index) => (
|
||||
<div
|
||||
key={knowledge.id}
|
||||
onClick={(event) => handleSelect(knowledge, index, event)}
|
||||
>
|
||||
<KnowledgeItem
|
||||
knowledge={knowledge}
|
||||
selected={selectedKnowledge.some(
|
||||
(item) => item.id === knowledge.id
|
||||
)}
|
||||
setSelected={(_selected, event) =>
|
||||
handleSelect(knowledge, index, event)
|
||||
}
|
||||
lastChild={index === filteredKnowledgeList.length - 1}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}}
|
||||
|
@ -43,7 +43,7 @@ const fileTypeIcons: Record<SupportedFileExtensions, IconType> = {
|
||||
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 <Icon width={16} />;
|
||||
return <Icon width={24} height={24} />;
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user