+
+ {flagEnabled &&
}
diff --git a/frontend/lib/components/Menu/components/QualityAssistantButton/QualityAssistantButton.module.scss b/frontend/lib/components/Menu/components/QualityAssistantButton/QualityAssistantButton.module.scss
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/frontend/lib/components/Menu/components/QualityAssistantButton/QualityAssistantButton.tsx b/frontend/lib/components/Menu/components/QualityAssistantButton/QualityAssistantButton.tsx
new file mode 100644
index 000000000000..45a58f005bbb
--- /dev/null
+++ b/frontend/lib/components/Menu/components/QualityAssistantButton/QualityAssistantButton.tsx
@@ -0,0 +1,21 @@
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+
+import { MenuButton } from "@/lib/components/Menu/components/MenuButton/MenuButton";
+
+export const QualityAssistantButton = (): JSX.Element => {
+ const pathname = usePathname() ?? "";
+ const isSelected = pathname.includes("/quality-assistant");
+
+ return (
+
+
+
+ );
+};
diff --git a/frontend/lib/components/ui/FileInput/FileInput.module.scss b/frontend/lib/components/ui/FileInput/FileInput.module.scss
index 030051960717..93d8a34b01d4 100644
--- a/frontend/lib/components/ui/FileInput/FileInput.module.scss
+++ b/frontend/lib/components/ui/FileInput/FileInput.module.scss
@@ -1,24 +1,62 @@
@use "styles/Radius.module.scss";
+@use "styles/ScreenSizes.module.scss";
@use "styles/Spacings.module.scss";
@use "styles/Typography.module.scss";
-.header_wrapper {
- display: flex;
- justify-content: space-between;
- align-items: center;
- border: 1px solid var(--border-2);
- border-radius: Radius.$big;
- padding: Spacings.$spacing03;
- cursor: pointer;
-
- &:hover {
- background-color: var(--background-2);
- border-color: var(--accent);
+.file_input_wrapper {
+ width: 100%;
+ height: 200px;
+
+ &.drag_active {
+ .header_wrapper {
+ border: 3px dashed var(--accent);
+ background-color: var(--background-3);
+ }
}
- .placeholder {
- color: var(--text-2);
- font-size: Typography.$small;
+ .header_wrapper {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border: 1px solid var(--border-2);
+ border-radius: Radius.$big;
+ padding: Spacings.$spacing03;
+ cursor: pointer;
+ height: 100%;
+ width: 100%;
+
+ &:hover {
+ border: 3px dashed var(--accent);
+ }
+
+ &.drag_active {
+ border: 3px dashed var(--accent);
+ background-color: var(--background-3);
+ }
+
+ .box_content {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ column-gap: Spacings.$spacing05;
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+
+ .input {
+ display: flex;
+ gap: Spacings.$spacing02;
+ padding: Spacings.$spacing05;
+
+ @media (max-width: ScreenSizes.$small) {
+ flex-direction: column;
+ }
+
+ .clickable {
+ font-weight: bold;
+ }
+ }
+ }
}
}
@@ -29,4 +67,4 @@
.error_message {
font-size: Typography.$tiny;
color: var(--dangerous);
-}
\ No newline at end of file
+}
diff --git a/frontend/lib/components/ui/FileInput/FileInput.tsx b/frontend/lib/components/ui/FileInput/FileInput.tsx
index 650a742b25cf..69543b15ad96 100644
--- a/frontend/lib/components/ui/FileInput/FileInput.tsx
+++ b/frontend/lib/components/ui/FileInput/FileInput.tsx
@@ -1,35 +1,36 @@
import { useRef, useState } from "react";
-
-import { iconList } from "@/lib/helpers/iconList";
+import { Accept, useDropzone } from "react-dropzone";
import styles from "./FileInput.module.scss";
-import { FieldHeader } from "../FieldHeader/FieldHeader";
import { Icon } from "../Icon/Icon";
interface FileInputProps {
label: string;
- icon: keyof typeof iconList;
onFileChange: (file: File) => void;
acceptedFileTypes?: string[];
}
export const FileInput = (props: FileInputProps): JSX.Element => {
- const [currentFile, setcurrentFile] = useState
(null);
+ const [currentFile, setCurrentFile] = useState(null);
const [errorMessage, setErrorMessage] = useState("");
const fileInputRef = useRef(null);
- const handleFileChange = (event: React.ChangeEvent) => {
+ const handleFileChange = (file: File) => {
+ const fileExtension = file.name.split(".").pop();
+ if (props.acceptedFileTypes?.includes(fileExtension || "")) {
+ props.onFileChange(file);
+ setCurrentFile(file);
+ setErrorMessage("");
+ } else {
+ setErrorMessage("Wrong extension");
+ }
+ };
+
+ const handleInputChange = (event: React.ChangeEvent) => {
const file = event.target.files?.[0];
if (file) {
- const fileExtension = file.name.split(".").pop();
- if (props.acceptedFileTypes?.includes(fileExtension || "")) {
- props.onFileChange(file);
- setcurrentFile(file);
- setErrorMessage("");
- } else {
- setErrorMessage("Wrong extension");
- }
+ handleFileChange(file);
}
};
@@ -37,30 +38,70 @@ export const FileInput = (props: FileInputProps): JSX.Element => {
fileInputRef.current?.click();
};
+ const mimeTypes: { [key: string]: string } = {
+ pdf: "application/pdf",
+ doc: "application/msword",
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ xls: "application/vnd.ms-excel",
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ csv: "text/csv",
+ txt: "text/plain",
+ jpg: "image/jpeg",
+ jpeg: "image/jpeg",
+ png: "image/png",
+ };
+
+ const accept: Accept | undefined = props.acceptedFileTypes?.reduce(
+ (acc, type) => {
+ const mimeType = mimeTypes[type];
+ if (mimeType) {
+ acc[mimeType] = [];
+ }
+
+ return acc;
+ },
+ {} as Accept
+ );
+
+ const { getRootProps, getInputProps, isDragActive } = useDropzone({
+ onDrop: (acceptedFiles) => {
+ const file = acceptedFiles[0];
+ if (file) {
+ handleFileChange(file);
+ }
+ },
+ accept,
+ });
+
return (
-
-
-
-
-
- Click here to {currentFile ? "change your" : "upload a"} file
-
-
+
+
+
+
+
+
+ Choose file
+
+
or drag it here
+
+ {currentFile && (
+
{currentFile.name}
+ )}
-
`application/${type}`)
- .join(",")}
- style={{ display: "none" }}
- />
- {currentFile && (
-
{currentFile.name}
- )}
+
{errorMessage !== "" && (
{errorMessage}
)}
diff --git a/frontend/lib/components/ui/Tag/Tag.module.scss b/frontend/lib/components/ui/Tag/Tag.module.scss
index 6a7caa788b08..ca4c6ed34b53 100644
--- a/frontend/lib/components/ui/Tag/Tag.module.scss
+++ b/frontend/lib/components/ui/Tag/Tag.module.scss
@@ -28,4 +28,9 @@
color: var(--success);
background-color: var(--background-success);
}
+
+ &.grey {
+ color: var(--text-4);
+ background-color: var(--background-pending);
+ }
}
diff --git a/frontend/lib/helpers/table.ts b/frontend/lib/helpers/table.ts
new file mode 100644
index 000000000000..65367c2146b3
--- /dev/null
+++ b/frontend/lib/helpers/table.ts
@@ -0,0 +1,117 @@
+import { UUID } from "crypto";
+
+interface SortConfig
{
+ key: keyof T;
+ direction: "ascending" | "descending";
+}
+
+interface HasId {
+ id: number | UUID;
+}
+
+const getAllValues = (obj: T): string[] => {
+ let values: string[] = [];
+ for (const key in obj) {
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
+ const value = (obj as Record)[key];
+ if (typeof value === "string" || typeof value === "number") {
+ values.push(value.toString());
+ } else if (Array.isArray(value)) {
+ values = values.concat(value.map((v: string) => v.toString()));
+ } else if (typeof value === "object" && value !== null) {
+ values = values.concat(getAllValues(value));
+ }
+ }
+ }
+
+ return values;
+};
+
+export const filterAndSort = (
+ dataList: T[],
+ searchQuery: string,
+ sortConfig: SortConfig,
+ getComparableValue: (item: T) => unknown
+): T[] => {
+ let filteredList = dataList.filter((item) =>
+ getAllValues(item).some((value) =>
+ value.toLowerCase().includes(searchQuery.toLowerCase())
+ )
+ );
+
+ const compareValues = (
+ a: string | number,
+ b: string | number,
+ direction: "ascending" | "descending"
+ ) => {
+ if (a < b) {
+ return direction === "ascending" ? -1 : 1;
+ }
+ if (a > b) {
+ return direction === "ascending" ? 1 : -1;
+ }
+
+ return 0;
+ };
+
+ // Appliquer les configurations de tri
+ if (sortConfig.key) {
+ filteredList = filteredList.sort((a, b) => {
+ const aValue = getComparableValue(a);
+ const bValue = getComparableValue(b);
+
+ // Vérifier que les valeurs sont des chaînes ou des nombres
+ if (
+ (typeof aValue === "string" || typeof aValue === "number") &&
+ (typeof bValue === "string" || typeof bValue === "number")
+ ) {
+ return compareValues(aValue, bValue, sortConfig.direction);
+ }
+
+ return 0;
+ });
+ }
+
+ return filteredList;
+};
+
+export const updateSelectedItems = (params: {
+ item: T;
+ index: number;
+ event: React.MouseEvent;
+ lastSelectedIndex: number | null;
+ filteredList: T[];
+ selectedItems: T[];
+}): { selectedItems: T[]; lastSelectedIndex: number | null } => {
+ const { item, index, event, lastSelectedIndex, filteredList, selectedItems } =
+ params;
+
+ if (event.shiftKey && lastSelectedIndex !== null) {
+ const start = Math.min(lastSelectedIndex, index);
+ const end = Math.max(lastSelectedIndex, index);
+ const range = filteredList.slice(start, end + 1);
+
+ const newSelected = [...selectedItems];
+ range.forEach((rangeItem) => {
+ if (
+ !newSelected.some((selectedItem) => selectedItem.id === rangeItem.id)
+ ) {
+ newSelected.push(rangeItem);
+ }
+ });
+
+ return { selectedItems: newSelected, lastSelectedIndex: index };
+ } else {
+ const isSelected = selectedItems.some(
+ (selectedItem) => selectedItem.id === item.id
+ );
+ const newSelectedItems = isSelected
+ ? selectedItems.filter((selectedItem) => selectedItem.id !== item.id)
+ : [...selectedItems, item];
+
+ return {
+ selectedItems: newSelectedItems,
+ lastSelectedIndex: isSelected ? null : index,
+ };
+ }
+};
diff --git a/frontend/next.config.js b/frontend/next.config.js
index a6dc189b5d11..f0f920171feb 100644
--- a/frontend/next.config.js
+++ b/frontend/next.config.js
@@ -56,6 +56,7 @@ const ContentSecurityPolicy = {
"*.intercomcdn.com",
"https://*.vercel.app",
process.env.NEXT_PUBLIC_FRONTEND_URL,
+ "http://host.docker.internal:54321",
],
"connect-src": [
"'self'",
@@ -72,7 +73,8 @@ const ContentSecurityPolicy = {
"https://vitals.vercel-insights.com/v1/vitals",
"https://us.posthog.com",
"*.posthog.com",
- "https://us.i.posthog.com"
+ "https://us.i.posthog.com",
+ "http://host.docker.internal:54321",
],
"img-src": [
"'self'",
diff --git a/frontend/package.json b/frontend/package.json
index 17c035a961bb..6eb729f700e2 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -72,6 +72,7 @@
"eslint": "8.46.0",
"eslint-config-next": "^14.1.0",
"eslint-plugin-prefer-arrow": "1.2.3",
+ "file-saver": "^2.0.5",
"framer-motion": "10.15.0",
"front-matter": "4.0.2",
"headlessui": "0.0.0",
@@ -117,6 +118,7 @@
"@tailwindcss/typography": "0.5.9",
"@testing-library/jest-dom": "6.1.3",
"@testing-library/react": "14.0.0",
+ "@types/file-saver": "^2.0.7",
"@types/prismjs": "^1.26.4",
"@types/react-katex": "^3.0.4",
"@types/uuid": "^10.0.0",
diff --git a/frontend/styles/_Variables.module.scss b/frontend/styles/_Variables.module.scss
index b9f1881fb841..075cc8c8ed76 100644
--- a/frontend/styles/_Variables.module.scss
+++ b/frontend/styles/_Variables.module.scss
@@ -2,3 +2,5 @@ $searchBarHeight: 62px;
$pageHeaderHeight: 48px;
$menuWidth: 230px;
$brainButtonHeight: 105px;
+$menuSectionWidth: 175px;
+$assistantInputWidth: 300px;
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 5f15c8a5d3c2..b993d6c9f699 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -2750,6 +2750,11 @@
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz"
integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==
+"@types/file-saver@^2.0.7":
+ version "2.0.7"
+ resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.7.tgz#8dbb2f24bdc7486c54aa854eb414940bbd056f7d"
+ integrity sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==
+
"@types/hast@^2.0.0":
version "2.3.5"
resolved "https://registry.npmjs.org/@types/hast/-/hast-2.3.5.tgz"
@@ -4760,6 +4765,11 @@ file-entry-cache@^6.0.1:
dependencies:
flat-cache "^3.0.4"
+file-saver@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz"
+ integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==
+
file-selector@^0.6.0:
version "0.6.0"
resolved "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz"