Compare commits
	
		
			2 Commits
		
	
	
		
			00c356268d
			...
			a2f422f67d
		
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								
								 | 
						a2f422f67d | |
| 
							
							
								 | 
						3e1b5d1ba3 | 
| 
						 | 
					@ -28,4 +28,6 @@ yarn-error.log*
 | 
				
			||||||
/src/server/model_datasets
 | 
					/src/server/model_datasets
 | 
				
			||||||
*.log
 | 
					*.log
 | 
				
			||||||
src/server/venv/*
 | 
					src/server/venv/*
 | 
				
			||||||
__pycache__/
 | 
					__pycache__/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					my_database.db
 | 
				
			||||||
| 
						 | 
					@ -1,21 +1,18 @@
 | 
				
			||||||
<!DOCTYPE html>
 | 
					<!DOCTYPE html>
 | 
				
			||||||
<html lang="en">
 | 
					<html lang="en">
 | 
				
			||||||
  <head>
 | 
						<head>
 | 
				
			||||||
    <meta charset="utf-8" />
 | 
							<meta charset="utf-8" />
 | 
				
			||||||
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
 | 
							<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
 | 
				
			||||||
    <meta name="viewport" content="width=device-width, initial-scale=1" />
 | 
							<meta name="viewport" content="width=device-width, initial-scale=1" />
 | 
				
			||||||
    <meta name="theme-color" content="#000000" />
 | 
							<meta name="theme-color" content="#000000" />
 | 
				
			||||||
    <meta
 | 
							<meta name="description" content="Web site created using create-react-app" />
 | 
				
			||||||
      name="description"
 | 
							<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
 | 
				
			||||||
      content="Web site created using create-react-app"
 | 
							<!--
 | 
				
			||||||
    />
 | 
					 | 
				
			||||||
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
 | 
					 | 
				
			||||||
    <!--
 | 
					 | 
				
			||||||
      manifest.json provides metadata used when your web app is installed on a
 | 
					      manifest.json provides metadata used when your web app is installed on a
 | 
				
			||||||
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
 | 
					      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
 | 
				
			||||||
    -->
 | 
					    -->
 | 
				
			||||||
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
 | 
							<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
 | 
				
			||||||
    <!--
 | 
							<!--
 | 
				
			||||||
      Notice the use of %PUBLIC_URL% in the tags above.
 | 
					      Notice the use of %PUBLIC_URL% in the tags above.
 | 
				
			||||||
      It will be replaced with the URL of the `public` folder during the build.
 | 
					      It will be replaced with the URL of the `public` folder during the build.
 | 
				
			||||||
      Only files inside the `public` folder can be referenced from the HTML.
 | 
					      Only files inside the `public` folder can be referenced from the HTML.
 | 
				
			||||||
| 
						 | 
					@ -24,12 +21,12 @@
 | 
				
			||||||
      work correctly both with client-side routing and a non-root public URL.
 | 
					      work correctly both with client-side routing and a non-root public URL.
 | 
				
			||||||
      Learn how to configure a non-root public URL by running `npm run build`.
 | 
					      Learn how to configure a non-root public URL by running `npm run build`.
 | 
				
			||||||
    -->
 | 
					    -->
 | 
				
			||||||
    <title>React App</title>
 | 
							<title>AI Tool</title>
 | 
				
			||||||
  </head>
 | 
						</head>
 | 
				
			||||||
  <body>
 | 
						<body>
 | 
				
			||||||
    <noscript>You need to enable JavaScript to run this app.</noscript>
 | 
							<noscript>You need to enable JavaScript to run this app.</noscript>
 | 
				
			||||||
    <div id="root"></div>
 | 
							<div id="root"></div>
 | 
				
			||||||
    <!--
 | 
							<!--
 | 
				
			||||||
      This HTML file is a template.
 | 
					      This HTML file is a template.
 | 
				
			||||||
      If you open it directly in the browser, you will see an empty page.
 | 
					      If you open it directly in the browser, you will see an empty page.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -39,5 +36,5 @@
 | 
				
			||||||
      To begin the development, run `npm start` or `yarn start`.
 | 
					      To begin the development, run `npm start` or `yarn start`.
 | 
				
			||||||
      To create a production bundle, use `npm run build` or `yarn build`.
 | 
					      To create a production bundle, use `npm run build` or `yarn build`.
 | 
				
			||||||
    -->
 | 
					    -->
 | 
				
			||||||
  </body>
 | 
						</body>
 | 
				
			||||||
</html>
 | 
					</html>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										454
									
								
								src/App.js
								
								
								
								
							
							
						
						
									
										454
									
								
								src/App.js
								
								
								
								
							| 
						 | 
					@ -1,24 +1,39 @@
 | 
				
			||||||
import '@mantine/core/styles.css';
 | 
					import "@mantine/core/styles.css";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import './App.css';
 | 
					import "./App.css";
 | 
				
			||||||
import { Dropzone } from './pages/components/Dropzone';
 | 
					import { Dropzone } from "./pages/components/Dropzone";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { ActionIcon, AppShell, Box, Burger, Button, Group, LoadingOverlay, Progress, ScrollArea, Text, Tooltip } from '@mantine/core';
 | 
					import { ActionIcon, AppShell, Box, Burger, Button, Group, LoadingOverlay, Modal, Progress, ScrollArea, Tabs, Text, Tooltip } from "@mantine/core";
 | 
				
			||||||
import { useDisclosure, useHotkeys } from '@mantine/hooks';
 | 
					import { useDisclosure, useHotkeys } from "@mantine/hooks";
 | 
				
			||||||
import { notifications } from '@mantine/notifications';
 | 
					import { notifications } from "@mantine/notifications";
 | 
				
			||||||
import { IconChevronLeft, IconChevronRight, IconImageInPicture, IconRefreshDot, IconTrash } from '@tabler/icons-react';
 | 
					import {
 | 
				
			||||||
import { useEffect, useMemo, useRef, useState } from 'react';
 | 
						IconChevronLeft,
 | 
				
			||||||
import { ImageDetect } from './pages/components/ImageDetect';
 | 
						IconChevronRight,
 | 
				
			||||||
import ImageLabel from './pages/components/ImageLabel';
 | 
						IconEdit,
 | 
				
			||||||
import { useImagesDetected } from './stores/use-images-detected';
 | 
						IconImageInPicture,
 | 
				
			||||||
import { SaveButton } from './pages/components/SaveButton';
 | 
						IconPlus,
 | 
				
			||||||
import { generateClientID } from './ultils';
 | 
						IconRefreshDot,
 | 
				
			||||||
 | 
						IconReportAnalytics,
 | 
				
			||||||
 | 
						IconTrash,
 | 
				
			||||||
 | 
					} from "@tabler/icons-react";
 | 
				
			||||||
 | 
					import { useEffect, useMemo, useRef, useState } from "react";
 | 
				
			||||||
 | 
					import { ImageDetect } from "./pages/components/ImageDetect";
 | 
				
			||||||
 | 
					import ImageLabel from "./pages/components/ImageLabel";
 | 
				
			||||||
 | 
					import { useImagesDetected } from "./stores/use-images-detected";
 | 
				
			||||||
 | 
					import { SaveButton } from "./pages/components/SaveButton";
 | 
				
			||||||
 | 
					import { generateClientID } from "./ultils";
 | 
				
			||||||
 | 
					import ModalLabel from "./pages/components/ModalLabel";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function App() {
 | 
					function App() {
 | 
				
			||||||
	const [mobileOpened, { toggle: toggleMobile }] = useDisclosure();
 | 
						const [mobileOpened, { toggle: toggleMobile }] = useDisclosure();
 | 
				
			||||||
	const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
 | 
						const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const [clickData, setClickData] = useState(null);
 | 
						const [clickData, setClickData] = useState(null);
 | 
				
			||||||
 | 
						const [listLabel, setListLabel] = useState([]);
 | 
				
			||||||
 | 
						const [isLoading, setIsLoading] = useState(false);
 | 
				
			||||||
 | 
						const [opened, { open, close }] = useDisclosure(false);
 | 
				
			||||||
 | 
						const [dataLabel, setDataLabel] = useState({});
 | 
				
			||||||
 | 
						const [openConfirm, setOpenConfirm] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const { images, clearImagesDetected, appendImageDetect, setImageDetected } = useImagesDetected();
 | 
						const { images, clearImagesDetected, appendImageDetect, setImageDetected } = useImagesDetected();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,13 +42,28 @@ function App() {
 | 
				
			||||||
	const imageLabelRef = useRef();
 | 
						const imageLabelRef = useRef();
 | 
				
			||||||
	const dropzoneRef = useRef();
 | 
						const dropzoneRef = useRef();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const fetchLabels = async () => {
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								const response = await fetch(`${process.env.REACT_APP_API_URL}/labels`);
 | 
				
			||||||
 | 
								// const response = await fetch(`http://127.0.0.1:5000/labels`);
 | 
				
			||||||
 | 
								const result = await response.json();
 | 
				
			||||||
 | 
								setListLabel(result);
 | 
				
			||||||
 | 
							} catch (error) {
 | 
				
			||||||
 | 
								console.log(error);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						useEffect(() => {
 | 
				
			||||||
 | 
							fetchLabels();
 | 
				
			||||||
 | 
						}, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const handleClear = () => {
 | 
						const handleClear = () => {
 | 
				
			||||||
		// const result = window.confirm('Are you want to clear ?');
 | 
							// const result = window.confirm('Are you want to clear ?');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const ID_NOTI = 'handle-clear';
 | 
							const ID_NOTI = "handle-clear";
 | 
				
			||||||
		notifications.show({
 | 
							notifications.show({
 | 
				
			||||||
			id: ID_NOTI,
 | 
								id: ID_NOTI,
 | 
				
			||||||
			title: 'Are you want to clear ?',
 | 
								title: "Are you want to clear ?",
 | 
				
			||||||
			message: (
 | 
								message: (
 | 
				
			||||||
				<Box className="flex items-center gap-2 justify-end">
 | 
									<Box className="flex items-center gap-2 justify-end">
 | 
				
			||||||
					<Button
 | 
										<Button
 | 
				
			||||||
| 
						 | 
					@ -55,11 +85,11 @@ function App() {
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const handleDeleteImage = (data, index) => {
 | 
						const handleDeleteImage = (data, index) => {
 | 
				
			||||||
		const ID_NOTI = 'handle-delete-image';
 | 
							const ID_NOTI = "handle-delete-image";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const deleteImage = async () => {
 | 
							const deleteImage = async () => {
 | 
				
			||||||
			try {
 | 
								try {
 | 
				
			||||||
				const response = await fetch(`${process.env.REACT_APP_API_URL}/delete/${data.image_name}`, { method: 'DELETE' });
 | 
									const response = await fetch(`${process.env.REACT_APP_API_URL}/delete/${data.image_name}`, { method: "DELETE" });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const result = await response.json();
 | 
									const result = await response.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -76,7 +106,7 @@ function App() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		notifications.show({
 | 
							notifications.show({
 | 
				
			||||||
			id: ID_NOTI,
 | 
								id: ID_NOTI,
 | 
				
			||||||
			title: 'Are you want to delete ?',
 | 
								title: "Are you want to delete ?",
 | 
				
			||||||
			message: (
 | 
								message: (
 | 
				
			||||||
				<Box className="flex items-center gap-2 justify-end">
 | 
									<Box className="flex items-center gap-2 justify-end">
 | 
				
			||||||
					<Button onClick={deleteImage} color="red" size="xs">
 | 
										<Button onClick={deleteImage} color="red" size="xs">
 | 
				
			||||||
| 
						 | 
					@ -132,10 +162,10 @@ function App() {
 | 
				
			||||||
			processFiles: 0,
 | 
								processFiles: 0,
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const worker = new Worker(new URL('./workers/detect-worker.js', import.meta.url));
 | 
							const worker = new Worker(new URL("./workers/detect-worker.js", import.meta.url));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		worker.onmessage = (event) => {
 | 
							worker.onmessage = (event) => {
 | 
				
			||||||
			if (event.data.status === 'success') {
 | 
								if (event.data.status === "success") {
 | 
				
			||||||
				if (event.data?.result) {
 | 
									if (event.data?.result) {
 | 
				
			||||||
					setImageDetected(event.data.result);
 | 
										setImageDetected(event.data.result);
 | 
				
			||||||
					setProgress({
 | 
										setProgress({
 | 
				
			||||||
| 
						 | 
					@ -151,11 +181,11 @@ function App() {
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				console.error('⚠️ Lỗi:', event.data.error);
 | 
									console.error("⚠️ Lỗi:", event.data.error);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		worker.postMessage({ action: 'processImages', files });
 | 
							worker.postMessage({ action: "processImages", files });
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const handlePrev = () => {
 | 
						const handlePrev = () => {
 | 
				
			||||||
| 
						 | 
					@ -188,132 +218,294 @@ function App() {
 | 
				
			||||||
		return true;
 | 
							return true;
 | 
				
			||||||
	}, [images, clickData]);
 | 
						}, [images, clickData]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const handleTraining = async () => {
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								setIsLoading(true);
 | 
				
			||||||
 | 
								setClickData(null);
 | 
				
			||||||
 | 
								clearImagesDetected();
 | 
				
			||||||
 | 
								const response = await fetch(`${process.env.REACT_APP_API_URL}/train`);
 | 
				
			||||||
 | 
								// const response = await fetch(`http://127.0.0.1:5000/train`);
 | 
				
			||||||
 | 
								const result = await response.json();
 | 
				
			||||||
 | 
								if (result?.error) {
 | 
				
			||||||
 | 
									notifications.show({
 | 
				
			||||||
 | 
										title: "Failed",
 | 
				
			||||||
 | 
										message: result?.error,
 | 
				
			||||||
 | 
										color: "red",
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								} else
 | 
				
			||||||
 | 
									notifications.show({
 | 
				
			||||||
 | 
										title: "Success",
 | 
				
			||||||
 | 
										message: `Training successfully!`,
 | 
				
			||||||
 | 
										color: "green",
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								setIsLoading(false);
 | 
				
			||||||
 | 
							} catch (error) {
 | 
				
			||||||
 | 
								console.log(error);
 | 
				
			||||||
 | 
								setIsLoading(false);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const handleSave = async (value) => {
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								setIsLoading(true);
 | 
				
			||||||
 | 
								const url = `${process.env.REACT_APP_API_URL}/label/${value?.id ? "update" : "add"}`;
 | 
				
			||||||
 | 
								// const url = `http://127.0.0.1:5000/label/${value?.id ? "update" : "add"}`;
 | 
				
			||||||
 | 
								const response = await fetch(url, {
 | 
				
			||||||
 | 
									method: "POST",
 | 
				
			||||||
 | 
									headers: {
 | 
				
			||||||
 | 
										"Content-Type": "application/json",
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									body: JSON.stringify(value),
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								const result = await response.json();
 | 
				
			||||||
 | 
								if (!result?.success) {
 | 
				
			||||||
 | 
									notifications.show({
 | 
				
			||||||
 | 
										title: "Failed",
 | 
				
			||||||
 | 
										message: result?.error,
 | 
				
			||||||
 | 
										color: "red",
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								} else
 | 
				
			||||||
 | 
									notifications.show({
 | 
				
			||||||
 | 
										title: "Success",
 | 
				
			||||||
 | 
										message: value?.id ? `Update label successfully!` : `Add label successfully!`,
 | 
				
			||||||
 | 
										color: "green",
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								fetchLabels();
 | 
				
			||||||
 | 
								setIsLoading(false);
 | 
				
			||||||
 | 
							} catch (error) {
 | 
				
			||||||
 | 
								console.log(error);
 | 
				
			||||||
 | 
								setIsLoading(false);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	useHotkeys([
 | 
						useHotkeys([
 | 
				
			||||||
		['ArrowRight', handleNext],
 | 
							["ArrowRight", handleNext],
 | 
				
			||||||
		['ArrowLeft', handlePrev],
 | 
							["ArrowLeft", handlePrev],
 | 
				
			||||||
	]);
 | 
						]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return (
 | 
						return (
 | 
				
			||||||
		<div style={{ position: 'relative' }}>
 | 
							<div style={{ position: "relative" }}>
 | 
				
			||||||
			<AppShell
 | 
								<AppShell
 | 
				
			||||||
				header={{ height: 60 }}
 | 
									header={{ height: 60 }}
 | 
				
			||||||
				navbar={{
 | 
									navbar={{
 | 
				
			||||||
					width: 300,
 | 
										width: 300,
 | 
				
			||||||
					breakpoint: 'sm',
 | 
										breakpoint: "sm",
 | 
				
			||||||
					collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
 | 
										collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
 | 
				
			||||||
				}}
 | 
									}}
 | 
				
			||||||
				padding="md">
 | 
									padding="md">
 | 
				
			||||||
				<AppShell.Header>
 | 
									<Tabs color="teal" defaultValue="detect">
 | 
				
			||||||
					<Box className="flex items-center justify-between h-full">
 | 
										<AppShell.Header>
 | 
				
			||||||
						<Group h="100%" px="md">
 | 
											<Box className="flex items-center justify-between h-full">
 | 
				
			||||||
							<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
 | 
												<Group h="100%" px="md">
 | 
				
			||||||
							<Burger opened={desktopOpened} onClick={toggleDesktop} visibleFrom="sm" size="sm" />
 | 
													<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
 | 
				
			||||||
						</Group>
 | 
													<Burger opened={desktopOpened} onClick={toggleDesktop} visibleFrom="sm" size="sm" />
 | 
				
			||||||
 | 
												</Group>
 | 
				
			||||||
						<Box className="px-4 flex items-center gap-4 w-fit">
 | 
												<Box className="px-4 flex items-center gap-4 w-fit">
 | 
				
			||||||
							<Button disabled={!clickData} onClick={handleClearSelect} leftSection={<IconRefreshDot size={14} />} color="orange">
 | 
													<Tabs.List>
 | 
				
			||||||
								Reset select
 | 
														<Tabs.Tab value="detect">
 | 
				
			||||||
							</Button>
 | 
															<span style={{ fontSize: 20 }}>Detect</span>
 | 
				
			||||||
							<SaveButton
 | 
														</Tabs.Tab>
 | 
				
			||||||
								onSaved={(data) => {
 | 
														<Tabs.Tab value="config">
 | 
				
			||||||
									handleNext();
 | 
															<span style={{ fontSize: 20 }}>Config label</span>
 | 
				
			||||||
								}}
 | 
														</Tabs.Tab>
 | 
				
			||||||
								currentData={clickData}
 | 
													</Tabs.List>
 | 
				
			||||||
								imageLabelRef={imageLabelRef}
 | 
					 | 
				
			||||||
							/>
 | 
					 | 
				
			||||||
						</Box>
 | 
					 | 
				
			||||||
					</Box>
 | 
					 | 
				
			||||||
				</AppShell.Header>
 | 
					 | 
				
			||||||
				<AppShell.Navbar p="md">
 | 
					 | 
				
			||||||
					<Box className="flex flex-col justify-between gap-2 w-full h-full">
 | 
					 | 
				
			||||||
						<Box className="flex items-center justify-between gap-4 mb-2">
 | 
					 | 
				
			||||||
							<Button fullWidth disabled={!images.length} onClick={handleClear} leftSection={<IconTrash size={14} />} color="red">
 | 
					 | 
				
			||||||
								Clear
 | 
					 | 
				
			||||||
							</Button>
 | 
					 | 
				
			||||||
							<Button fullWidth disabled={images.length} onClick={handleAddImages} leftSection={<IconImageInPicture size={14} />}>
 | 
					 | 
				
			||||||
								Add images
 | 
					 | 
				
			||||||
							</Button>
 | 
					 | 
				
			||||||
						</Box>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
						<ScrollArea h={780} className="pb-5 flex-1">
 | 
					 | 
				
			||||||
							<Box className="flex flex-col gap-3 w-full">
 | 
					 | 
				
			||||||
								{images.length > 0 &&
 | 
					 | 
				
			||||||
									images.map((item, index) => {
 | 
					 | 
				
			||||||
										return (
 | 
					 | 
				
			||||||
											<ImageDetect
 | 
					 | 
				
			||||||
												currentData={clickData}
 | 
					 | 
				
			||||||
												onClick={(data) => {
 | 
					 | 
				
			||||||
													console.log({ data });
 | 
					 | 
				
			||||||
													setClickData(data);
 | 
					 | 
				
			||||||
												}}
 | 
					 | 
				
			||||||
												key={`${item?.name || item?.image_name}_${index}`}
 | 
					 | 
				
			||||||
												file={item}
 | 
					 | 
				
			||||||
												onDelete={(data) => {
 | 
					 | 
				
			||||||
													handleDeleteImage(data, index);
 | 
					 | 
				
			||||||
												}}
 | 
					 | 
				
			||||||
											/>
 | 
					 | 
				
			||||||
										);
 | 
					 | 
				
			||||||
									})}
 | 
					 | 
				
			||||||
							</Box>
 | 
												</Box>
 | 
				
			||||||
 | 
												<Box className="px-4 flex items-center gap-4 w-fit">
 | 
				
			||||||
							{images.length <= 0 && (
 | 
													<Button disabled={isLoading} onClick={() => setOpenConfirm(true)} leftSection={<IconReportAnalytics size={14} />} color="#caa32c">
 | 
				
			||||||
								<Box className="flex items-center justify-center">
 | 
														Training
 | 
				
			||||||
									<span>No images to process</span>
 | 
													</Button>
 | 
				
			||||||
 | 
													<Button disabled={!clickData || isLoading} onClick={handleClearSelect} leftSection={<IconRefreshDot size={14} />} color="orange">
 | 
				
			||||||
 | 
														Reset select
 | 
				
			||||||
 | 
													</Button>
 | 
				
			||||||
 | 
													<SaveButton
 | 
				
			||||||
 | 
														disabled={isLoading}
 | 
				
			||||||
 | 
														onSaved={(data) => {
 | 
				
			||||||
 | 
															handleNext();
 | 
				
			||||||
 | 
														}}
 | 
				
			||||||
 | 
														currentData={clickData}
 | 
				
			||||||
 | 
														imageLabelRef={imageLabelRef}
 | 
				
			||||||
 | 
													/>
 | 
				
			||||||
 | 
												</Box>
 | 
				
			||||||
 | 
											</Box>
 | 
				
			||||||
 | 
										</AppShell.Header>
 | 
				
			||||||
 | 
										<Tabs.Panel value="detect">
 | 
				
			||||||
 | 
											<AppShell.Navbar p="md">
 | 
				
			||||||
 | 
												<Box className="flex flex-col justify-between gap-2 w-full h-full">
 | 
				
			||||||
 | 
													<Box className="flex items-center justify-between gap-4 mb-2">
 | 
				
			||||||
 | 
														<Button fullWidth disabled={!images.length || isLoading} onClick={handleClear} leftSection={<IconTrash size={14} />} color="red">
 | 
				
			||||||
 | 
															Clear
 | 
				
			||||||
 | 
														</Button>
 | 
				
			||||||
 | 
														<Button fullWidth disabled={images.length || isLoading} onClick={handleAddImages} leftSection={<IconImageInPicture size={14} />}>
 | 
				
			||||||
 | 
															Add images
 | 
				
			||||||
 | 
														</Button>
 | 
				
			||||||
								</Box>
 | 
													</Box>
 | 
				
			||||||
							)}
 | 
					 | 
				
			||||||
						</ScrollArea>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
						{progress.total > 0 && (
 | 
													<ScrollArea h={780} className="pb-5 flex-1">
 | 
				
			||||||
							<Box className="flex flex-col items-center justify-end w-full">
 | 
														<Box className="flex flex-col gap-3 w-full">
 | 
				
			||||||
								<Text>
 | 
															{images.length > 0 &&
 | 
				
			||||||
									{progress.processFiles} / {progress.total}
 | 
																images.map((item, index) => {
 | 
				
			||||||
								</Text>
 | 
																	return (
 | 
				
			||||||
								<Progress className="w-full" value={(progress.processFiles * 100) / progress.total} striped animated />
 | 
																		<ImageDetect
 | 
				
			||||||
 | 
																			currentData={clickData}
 | 
				
			||||||
 | 
																			onClick={(data) => {
 | 
				
			||||||
 | 
																				console.log({ data });
 | 
				
			||||||
 | 
																				setClickData(data);
 | 
				
			||||||
 | 
																			}}
 | 
				
			||||||
 | 
																			key={`${item?.name || item?.image_name}_${index}`}
 | 
				
			||||||
 | 
																			file={item}
 | 
				
			||||||
 | 
																			onDelete={(data) => {
 | 
				
			||||||
 | 
																				handleDeleteImage(data, index);
 | 
				
			||||||
 | 
																			}}
 | 
				
			||||||
 | 
																		/>
 | 
				
			||||||
 | 
																	);
 | 
				
			||||||
 | 
																})}
 | 
				
			||||||
 | 
														</Box>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
														{images.length <= 0 && (
 | 
				
			||||||
 | 
															<Box className="flex items-center justify-center">
 | 
				
			||||||
 | 
																<span>No images to process</span>
 | 
				
			||||||
 | 
															</Box>
 | 
				
			||||||
 | 
														)}
 | 
				
			||||||
 | 
													</ScrollArea>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
													{progress.total > 0 && (
 | 
				
			||||||
 | 
														<Box className="flex flex-col items-center justify-end w-full">
 | 
				
			||||||
 | 
															<Text>
 | 
				
			||||||
 | 
																{progress.processFiles} / {progress.total}
 | 
				
			||||||
 | 
															</Text>
 | 
				
			||||||
 | 
															<Progress className="w-full" value={(progress.processFiles * 100) / progress.total} striped animated />
 | 
				
			||||||
 | 
														</Box>
 | 
				
			||||||
 | 
													)}
 | 
				
			||||||
							</Box>
 | 
												</Box>
 | 
				
			||||||
						)}
 | 
											</AppShell.Navbar>
 | 
				
			||||||
					</Box>
 | 
											<AppShell.Main style={{ position: "relative" }}>
 | 
				
			||||||
				</AppShell.Navbar>
 | 
												{clickData && <ImageLabel ref={imageLabelRef} data={clickData} />}
 | 
				
			||||||
				<AppShell.Main style={{ position: 'relative' }}>
 | 
					 | 
				
			||||||
					{clickData && <ImageLabel ref={imageLabelRef} data={clickData} />}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
					<Box
 | 
												<Box
 | 
				
			||||||
						style={{
 | 
													style={{
 | 
				
			||||||
							display: 'flex',
 | 
														display: "flex",
 | 
				
			||||||
							alignItems: 'center',
 | 
														alignItems: "center",
 | 
				
			||||||
							justifyContent: 'center',
 | 
														justifyContent: "center",
 | 
				
			||||||
							gap: '10px',
 | 
														gap: "10px",
 | 
				
			||||||
							width: '100%',
 | 
														width: "100%",
 | 
				
			||||||
							flexDirection: 'column',
 | 
														flexDirection: "column",
 | 
				
			||||||
						}}>
 | 
													}}>
 | 
				
			||||||
						<Dropzone
 | 
													<Dropzone
 | 
				
			||||||
							openRef={dropzoneRef}
 | 
														openRef={dropzoneRef}
 | 
				
			||||||
							hidden={!clickData}
 | 
														hidden={!clickData}
 | 
				
			||||||
							onErorrFiles={(errors) => {
 | 
														onErorrFiles={(errors) => {
 | 
				
			||||||
								notifications.show({
 | 
															notifications.show({
 | 
				
			||||||
									title: 'Invalid Images',
 | 
																title: "Invalid Images",
 | 
				
			||||||
									message: `There ${errors.length === 1 ? 'is' : 'are'} ${errors.length} invalid image${
 | 
																message: `There ${errors.length === 1 ? "is" : "are"} ${errors.length} invalid image${
 | 
				
			||||||
										errors.length > 1 ? 's' : ''
 | 
																	errors.length > 1 ? "s" : ""
 | 
				
			||||||
									}. Please re-check!`,
 | 
																}. Please re-check!`,
 | 
				
			||||||
									color: 'red',
 | 
																color: "red",
 | 
				
			||||||
								});
 | 
															});
 | 
				
			||||||
 | 
														}}
 | 
				
			||||||
 | 
														onFilesChange={handleUpload}
 | 
				
			||||||
 | 
													/>
 | 
				
			||||||
 | 
												</Box>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
												<Box className="fixed bottom-5 right-5 flex items-center gap-4">
 | 
				
			||||||
 | 
													<Tooltip label={"Left (<)"}>
 | 
				
			||||||
 | 
														<ActionIcon disabled={!showPrev} onClick={handlePrev} size={"lg"}>
 | 
				
			||||||
 | 
															<IconChevronLeft size={20} />
 | 
				
			||||||
 | 
														</ActionIcon>
 | 
				
			||||||
 | 
													</Tooltip>
 | 
				
			||||||
 | 
													<Tooltip label={"Right (>)"}>
 | 
				
			||||||
 | 
														<ActionIcon disabled={!showNext} onClick={handleNext} size={"lg"}>
 | 
				
			||||||
 | 
															<IconChevronRight size={20} />
 | 
				
			||||||
 | 
														</ActionIcon>
 | 
				
			||||||
 | 
													</Tooltip>
 | 
				
			||||||
 | 
												</Box>
 | 
				
			||||||
 | 
											</AppShell.Main>
 | 
				
			||||||
 | 
										</Tabs.Panel>
 | 
				
			||||||
 | 
										<Tabs.Panel value="config">
 | 
				
			||||||
 | 
											<AppShell.Main style={{ position: "relative", paddingLeft: 0 }}>
 | 
				
			||||||
 | 
												<ScrollArea h={780} className="pb-5 flex-1">
 | 
				
			||||||
 | 
													<Box className="flex justify-end pe-4">
 | 
				
			||||||
 | 
														<Button disabled={isLoading} leftSection={<IconPlus size={14} />} color="#1a9b20" onClick={open}>
 | 
				
			||||||
 | 
															Add
 | 
				
			||||||
 | 
														</Button>
 | 
				
			||||||
 | 
													</Box>
 | 
				
			||||||
 | 
													<div className="flex justify-center items-center">
 | 
				
			||||||
 | 
														<Box w={600} className="mt-4">
 | 
				
			||||||
 | 
															{listLabel.length > 0 &&
 | 
				
			||||||
 | 
																listLabel?.map((item, index) => {
 | 
				
			||||||
 | 
																	return (
 | 
				
			||||||
 | 
																		<Box w={600} key={index} className="flex items-center justify-between mb-4">
 | 
				
			||||||
 | 
																			<Text>{item?.name}</Text>
 | 
				
			||||||
 | 
																			<div className="flex items-center justify-between gap-4">
 | 
				
			||||||
 | 
																				<Button
 | 
				
			||||||
 | 
																					disabled={isLoading}
 | 
				
			||||||
 | 
																					leftSection={<IconEdit size={14} />}
 | 
				
			||||||
 | 
																					color="#0659a5"
 | 
				
			||||||
 | 
																					onClick={() => {
 | 
				
			||||||
 | 
																						open();
 | 
				
			||||||
 | 
																						setDataLabel(item);
 | 
				
			||||||
 | 
																					}}>
 | 
				
			||||||
 | 
																					Edit
 | 
				
			||||||
 | 
																				</Button>
 | 
				
			||||||
 | 
																			</div>
 | 
				
			||||||
 | 
																		</Box>
 | 
				
			||||||
 | 
																	);
 | 
				
			||||||
 | 
																})}
 | 
				
			||||||
 | 
														</Box>
 | 
				
			||||||
 | 
													</div>
 | 
				
			||||||
 | 
												</ScrollArea>
 | 
				
			||||||
 | 
											</AppShell.Main>
 | 
				
			||||||
 | 
										</Tabs.Panel>
 | 
				
			||||||
 | 
									</Tabs>
 | 
				
			||||||
 | 
									<LoadingOverlay
 | 
				
			||||||
 | 
										visible={isLoading}
 | 
				
			||||||
 | 
										zIndex={10}
 | 
				
			||||||
 | 
										overlayProps={{ radius: "sm", blur: 2 }}
 | 
				
			||||||
 | 
										loaderProps={{
 | 
				
			||||||
 | 
											size: "lg",
 | 
				
			||||||
 | 
										}}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
									<ModalLabel
 | 
				
			||||||
 | 
										opened={opened}
 | 
				
			||||||
 | 
										open={open}
 | 
				
			||||||
 | 
										close={() => {
 | 
				
			||||||
 | 
											close();
 | 
				
			||||||
 | 
											setDataLabel({});
 | 
				
			||||||
 | 
										}}
 | 
				
			||||||
 | 
										data={dataLabel}
 | 
				
			||||||
 | 
										onSave={() => {
 | 
				
			||||||
 | 
											close();
 | 
				
			||||||
 | 
											setDataLabel({});
 | 
				
			||||||
 | 
											setIsLoading(true);
 | 
				
			||||||
 | 
											handleSave(dataLabel);
 | 
				
			||||||
 | 
											setTimeout(() => {
 | 
				
			||||||
 | 
												setIsLoading(false);
 | 
				
			||||||
 | 
											}, 2000);
 | 
				
			||||||
 | 
										}}
 | 
				
			||||||
 | 
										setData={setDataLabel}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<Modal opened={openConfirm} onClose={() => setOpenConfirm(false)} title={"Confirm"}>
 | 
				
			||||||
 | 
										<div style={{ padding: 20 }} className="flex flex-col gap-4 justify-center items-center">
 | 
				
			||||||
 | 
											<div>Do you want to run training?</div>
 | 
				
			||||||
 | 
											<div>Maybe you need to save some data to train!</div>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
										<div style={{ padding: 20 }} className="flex justify-center gap-4">
 | 
				
			||||||
 | 
											<Button
 | 
				
			||||||
 | 
												onClick={() => {
 | 
				
			||||||
 | 
													setOpenConfirm(false);
 | 
				
			||||||
 | 
												}}>
 | 
				
			||||||
 | 
												Cancel
 | 
				
			||||||
 | 
											</Button>
 | 
				
			||||||
 | 
											<Button
 | 
				
			||||||
 | 
												onClick={() => {
 | 
				
			||||||
 | 
													setOpenConfirm(false);
 | 
				
			||||||
 | 
													handleTraining();
 | 
				
			||||||
							}}
 | 
												}}
 | 
				
			||||||
							onFilesChange={handleUpload}
 | 
												color="#1a9b20">
 | 
				
			||||||
						/>
 | 
												Confirm
 | 
				
			||||||
					</Box>
 | 
											</Button>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
					<Box className="fixed bottom-5 right-5 flex items-center gap-4">
 | 
									</Modal>
 | 
				
			||||||
						<Tooltip label={'Left (<)'}>
 | 
					 | 
				
			||||||
							<ActionIcon disabled={!showPrev} onClick={handlePrev} size={'lg'}>
 | 
					 | 
				
			||||||
								<IconChevronLeft size={20} />
 | 
					 | 
				
			||||||
							</ActionIcon>
 | 
					 | 
				
			||||||
						</Tooltip>
 | 
					 | 
				
			||||||
						<Tooltip label={'Right (>)'}>
 | 
					 | 
				
			||||||
							<ActionIcon disabled={!showNext} onClick={handleNext} size={'lg'}>
 | 
					 | 
				
			||||||
								<IconChevronRight size={20} />
 | 
					 | 
				
			||||||
							</ActionIcon>
 | 
					 | 
				
			||||||
						</Tooltip>
 | 
					 | 
				
			||||||
					</Box>
 | 
					 | 
				
			||||||
				</AppShell.Main>
 | 
					 | 
				
			||||||
			</AppShell>
 | 
								</AppShell>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
	);
 | 
						);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
import React, { useEffect, useRef, useState, forwardRef, useImperativeHandle, memo } from 'react';
 | 
					import React, { useEffect, useRef, useState, forwardRef, useImperativeHandle, memo } from "react";
 | 
				
			||||||
import { Box, LoadingOverlay } from '@mantine/core';
 | 
					import { Box, LoadingOverlay } from "@mantine/core";
 | 
				
			||||||
import PointsBlur from './PointsBlur';
 | 
					import PointsBlur from "./PointsBlur";
 | 
				
			||||||
import { generateImageUrl } from '../../ultils';
 | 
					import { generateImageUrl } from "../../ultils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ImageLabel = forwardRef(({ data }, ref) => {
 | 
					const ImageLabel = forwardRef(({ data }, ref) => {
 | 
				
			||||||
	const [imageSrc, setImageSrc] = useState(null);
 | 
						const [imageSrc, setImageSrc] = useState(null);
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,7 @@ const ImageLabel = forwardRef(({ data }, ref) => {
 | 
				
			||||||
	const [reloading, setReloading] = useState(false);
 | 
						const [reloading, setReloading] = useState(false);
 | 
				
			||||||
	const [listLabel, setListLabel] = useState([]);
 | 
						const [listLabel, setListLabel] = useState([]);
 | 
				
			||||||
	const [infoImage, setInfoImage] = useState({});
 | 
						const [infoImage, setInfoImage] = useState({});
 | 
				
			||||||
	const [imageName, setImageName] = useState('');
 | 
						const [imageName, setImageName] = useState("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Expose blurredRegions cho component cha
 | 
						// Expose blurredRegions cho component cha
 | 
				
			||||||
	useImperativeHandle(ref, () => ({
 | 
						useImperativeHandle(ref, () => ({
 | 
				
			||||||
| 
						 | 
					@ -33,18 +33,23 @@ const ImageLabel = forwardRef(({ data }, ref) => {
 | 
				
			||||||
			alert(data.error);
 | 
								alert(data.error);
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			if (data?.points) {
 | 
								if (data?.points) {
 | 
				
			||||||
				blurredRegions.current = data?.points?.map((pre) => [
 | 
									blurredRegions.current = data?.points?.map((pre) => {
 | 
				
			||||||
					{ x: pre.x1, y: pre.y1, label: pre.label },
 | 
										const arrLabel = pre?.label?.split(".");
 | 
				
			||||||
					{ x: pre.x2, y: pre.y1, label: pre.label },
 | 
										const label = arrLabel?.length > 1 ? arrLabel[1] : arrLabel[0];
 | 
				
			||||||
					{ x: pre.x2, y: pre.y2, label: pre.label },
 | 
										const subLabel = arrLabel?.length > 1 ? arrLabel[0] : null;
 | 
				
			||||||
					{ x: pre.x1, y: pre.y2, label: pre.label },
 | 
										return [
 | 
				
			||||||
				]);
 | 
											{ x: pre.x1, y: pre.y1, label: label, subLabel: subLabel },
 | 
				
			||||||
 | 
											{ x: pre.x2, y: pre.y1, label: label, subLabel: subLabel },
 | 
				
			||||||
 | 
											{ x: pre.x2, y: pre.y2, label: label, subLabel: subLabel },
 | 
				
			||||||
 | 
											{ x: pre.x1, y: pre.y2, label: label, subLabel: subLabel },
 | 
				
			||||||
 | 
										];
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			setListLabel(
 | 
								setListLabel(
 | 
				
			||||||
				Object.entries(data?.labels).map(([id, name]) => ({
 | 
									Object.entries(data?.labels).map(([id, name]) => ({
 | 
				
			||||||
					id: Number(id),
 | 
										id: Number(id),
 | 
				
			||||||
					name,
 | 
										name,
 | 
				
			||||||
				})),
 | 
									}))
 | 
				
			||||||
			);
 | 
								);
 | 
				
			||||||
			setImageSrc(generateImageUrl(data?.image_path));
 | 
								setImageSrc(generateImageUrl(data?.image_path));
 | 
				
			||||||
			setImageName(data?.image_name);
 | 
								setImageName(data?.image_name);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,72 @@
 | 
				
			||||||
 | 
					import { Modal, Button, TextInput } from "@mantine/core";
 | 
				
			||||||
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ModalLabel = ({ opened, open, close, data, onSave, setData }) => {
 | 
				
			||||||
 | 
						const [openConfirm, setOpenConfirm] = useState(false);
 | 
				
			||||||
 | 
						const [isError, setIsError] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						useEffect(() => {
 | 
				
			||||||
 | 
							if (!opened) {
 | 
				
			||||||
 | 
								setIsError(false);
 | 
				
			||||||
 | 
								setOpenConfirm(false);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}, [opened]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return (
 | 
				
			||||||
 | 
							<>
 | 
				
			||||||
 | 
								<Modal opened={opened} onClose={close} title={data?.id ? "Edit Label" : "Add Label"} centered>
 | 
				
			||||||
 | 
									<div style={{ padding: 20 }} className="flex justify-center">
 | 
				
			||||||
 | 
										<TextInput
 | 
				
			||||||
 | 
											label="Input label"
 | 
				
			||||||
 | 
											withAsterisk
 | 
				
			||||||
 | 
											placeholder="Input placeholder"
 | 
				
			||||||
 | 
											value={data?.name || ""}
 | 
				
			||||||
 | 
											onChange={(event) => {
 | 
				
			||||||
 | 
												if (event.currentTarget.value) setIsError(false);
 | 
				
			||||||
 | 
												setData((pre) => ({ ...pre, name: event.currentTarget.value }));
 | 
				
			||||||
 | 
											}}
 | 
				
			||||||
 | 
											style={{ width: 300 }}
 | 
				
			||||||
 | 
											error={isError ? "Invalid label" : ""}
 | 
				
			||||||
 | 
										/>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
									<div style={{ padding: 20 }} className="flex justify-center gap-4">
 | 
				
			||||||
 | 
										<Button onClick={close}>Close</Button>
 | 
				
			||||||
 | 
										<Button
 | 
				
			||||||
 | 
											onClick={() => {
 | 
				
			||||||
 | 
												if (!data?.name) {
 | 
				
			||||||
 | 
													setIsError(true);
 | 
				
			||||||
 | 
													return;
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
												if (data?.id) {
 | 
				
			||||||
 | 
													onSave(data);
 | 
				
			||||||
 | 
													close();
 | 
				
			||||||
 | 
												} else setOpenConfirm(true);
 | 
				
			||||||
 | 
											}}
 | 
				
			||||||
 | 
											color="#1a9b20">
 | 
				
			||||||
 | 
											Save
 | 
				
			||||||
 | 
										</Button>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</Modal>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<Modal opened={openConfirm} onClose={() => setOpenConfirm(false)} title={"Confirm"}>
 | 
				
			||||||
 | 
									<div style={{ padding: 20 }} className="flex flex-col gap-4 justify-center items-center">
 | 
				
			||||||
 | 
										<div>Do you want to add new label?</div>
 | 
				
			||||||
 | 
										<div style={{ color: "#f54e4e" }}>Label cannot delete after add!</div>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
									<div style={{ padding: 20 }} className="flex justify-center gap-4">
 | 
				
			||||||
 | 
										<Button
 | 
				
			||||||
 | 
											onClick={() => {
 | 
				
			||||||
 | 
												setOpenConfirm(false);
 | 
				
			||||||
 | 
											}}>
 | 
				
			||||||
 | 
											Cancel
 | 
				
			||||||
 | 
										</Button>
 | 
				
			||||||
 | 
										<Button onClick={onSave} color="#1a9b20">
 | 
				
			||||||
 | 
											Confirm
 | 
				
			||||||
 | 
										</Button>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</Modal>
 | 
				
			||||||
 | 
							</>
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default ModalLabel;
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
import React, { useEffect, useRef, useState } from 'react';
 | 
					import React, { useEffect, useRef, useState } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded, reloading, setReloading, setInfoImage, listLabel = [] }) => {
 | 
					const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded, reloading, setReloading, setInfoImage, listLabel = [] }) => {
 | 
				
			||||||
	const points = useRef([]);
 | 
						const points = useRef([]);
 | 
				
			||||||
| 
						 | 
					@ -12,7 +12,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	useEffect(() => {
 | 
						useEffect(() => {
 | 
				
			||||||
		const canvas = canvasRef.current;
 | 
							const canvas = canvasRef.current;
 | 
				
			||||||
		const ctx = canvas.getContext('2d');
 | 
							const ctx = canvas.getContext("2d");
 | 
				
			||||||
		const image = new Image();
 | 
							const image = new Image();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		image.onload = () => {
 | 
							image.onload = () => {
 | 
				
			||||||
| 
						 | 
					@ -23,8 +23,12 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
 | 
				
			||||||
			setInfoImage({ width: image.width, height: image.height });
 | 
								setInfoImage({ width: image.width, height: image.height });
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		image.crossOrigin = 'anonymous';
 | 
							image.crossOrigin = "anonymous";
 | 
				
			||||||
		image.src = imageSrc;
 | 
							image.src = imageSrc;
 | 
				
			||||||
 | 
							setInfo({});
 | 
				
			||||||
 | 
							setIsOpen(false);
 | 
				
			||||||
 | 
							points.current = []; // Clear points after blurring
 | 
				
			||||||
 | 
							setReloading((pre) => !pre);
 | 
				
			||||||
	}, [imageSrc]);
 | 
						}, [imageSrc]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	useEffect(() => {
 | 
						useEffect(() => {
 | 
				
			||||||
| 
						 | 
					@ -39,7 +43,6 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const handleCanvasClick = (event) => {
 | 
							const handleCanvasClick = (event) => {
 | 
				
			||||||
			if (!isImageLoaded.current) return;
 | 
								if (!isImageLoaded.current) return;
 | 
				
			||||||
			if (isOpen) return;
 | 
					 | 
				
			||||||
			const { x, y } = getMousePos(canvas, event);
 | 
								const { x, y } = getMousePos(canvas, event);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// Check if a delete button is clicked
 | 
								// Check if a delete button is clicked
 | 
				
			||||||
| 
						 | 
					@ -49,14 +52,22 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
 | 
				
			||||||
				const buttonY = Math.min(region[0].y, region[1].y, region[2].y, region[3].y) - 20;
 | 
									const buttonY = Math.min(region[0].y, region[1].y, region[2].y, region[3].y) - 20;
 | 
				
			||||||
				if (Math.abs(buttonX - x) < 30 && Math.abs(buttonY - y) < 10) {
 | 
									if (Math.abs(buttonX - x) < 30 && Math.abs(buttonY - y) < 10) {
 | 
				
			||||||
					// blurredRegions.current.splice(i, 1);
 | 
										// blurredRegions.current.splice(i, 1);
 | 
				
			||||||
 | 
										if (info?.index === i) {
 | 
				
			||||||
 | 
											setInfo({});
 | 
				
			||||||
 | 
											setIsOpen(false);
 | 
				
			||||||
 | 
											return;
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
					setInfo({ index: i, value: blurredRegions.current[i] });
 | 
										setInfo({ index: i, value: blurredRegions.current[i] });
 | 
				
			||||||
					setIsOpen(true);
 | 
										setIsOpen(true);
 | 
				
			||||||
					redrawCanvas(i);
 | 
										redrawCanvas(i);
 | 
				
			||||||
					return;
 | 
										return;
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
								if (isOpen) return;
 | 
				
			||||||
			points.current.push({ x, y, label: listLabel[0]?.name || 'cisco' });
 | 
								const arrLabel = (listLabel[0]?.name || ["logo.cisco"])?.split(".");
 | 
				
			||||||
 | 
								const label = arrLabel?.length > 1 ? arrLabel[1] : arrLabel[0];
 | 
				
			||||||
 | 
								const subLabel = arrLabel?.length > 1 ? arrLabel[0] : null;
 | 
				
			||||||
 | 
								points.current.push({ x, y, label: label, subLabel: subLabel });
 | 
				
			||||||
			drawRedPoint(canvas, x, y);
 | 
								drawRedPoint(canvas, x, y);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (points.current.length === 4) {
 | 
								if (points.current.length === 4) {
 | 
				
			||||||
| 
						 | 
					@ -67,11 +78,10 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const handleCanvasMouseDown = (event) => {
 | 
							const handleCanvasMouseDown = (event) => {
 | 
				
			||||||
			if (isOpen) return;
 | 
					 | 
				
			||||||
			const { x, y } = getMousePos(canvas, event);
 | 
								const { x, y } = getMousePos(canvas, event);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// Check if we clicked on an existing point
 | 
					 | 
				
			||||||
			for (let i = 0; i < blurredRegions.current.length; i++) {
 | 
								for (let i = 0; i < blurredRegions.current.length; i++) {
 | 
				
			||||||
 | 
									// Check if we clicked on an existing point
 | 
				
			||||||
				for (let j = 0; j < blurredRegions.current[i].length; j++) {
 | 
									for (let j = 0; j < blurredRegions.current[i].length; j++) {
 | 
				
			||||||
					const point = blurredRegions.current[i][j];
 | 
										const point = blurredRegions.current[i][j];
 | 
				
			||||||
					if (Math.abs(point.x - x) < 5 && Math.abs(point.y - y) < 5) {
 | 
										if (Math.abs(point.x - x) < 5 && Math.abs(point.y - y) < 5) {
 | 
				
			||||||
| 
						 | 
					@ -84,6 +94,25 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
 | 
				
			||||||
				// Check if we clicked inside a region
 | 
									// Check if we clicked inside a region
 | 
				
			||||||
				const region = blurredRegions.current[i];
 | 
									const region = blurredRegions.current[i];
 | 
				
			||||||
				if (isPointInRegion(x, y, region)) {
 | 
									if (isPointInRegion(x, y, region)) {
 | 
				
			||||||
 | 
										// Check if a delete button is clicked
 | 
				
			||||||
 | 
										for (let i = 0; i < blurredRegions.current.length; i++) {
 | 
				
			||||||
 | 
											const region = blurredRegions.current[i];
 | 
				
			||||||
 | 
											const buttonX = (region[0].x + region[1].x) / 2;
 | 
				
			||||||
 | 
											const buttonY = Math.min(region[0].y, region[1].y, region[2].y, region[3].y) - 20;
 | 
				
			||||||
 | 
											if (Math.abs(buttonX - x) < 30 && Math.abs(buttonY - y) < 10) {
 | 
				
			||||||
 | 
												// blurredRegions.current.splice(i, 1);
 | 
				
			||||||
 | 
												if (info?.index === i) {
 | 
				
			||||||
 | 
													setInfo({});
 | 
				
			||||||
 | 
													setIsOpen(false);
 | 
				
			||||||
 | 
													return;
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
												setInfo({ index: i, value: blurredRegions.current[i] });
 | 
				
			||||||
 | 
												setIsOpen(true);
 | 
				
			||||||
 | 
												redrawCanvas(i);
 | 
				
			||||||
 | 
												return;
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										if (isOpen) return;
 | 
				
			||||||
					setMovingRegionIndex(i);
 | 
										setMovingRegionIndex(i);
 | 
				
			||||||
					setOffset({ x: region[0].x - x, y: region[0].y - y });
 | 
										setOffset({ x: region[0].x - x, y: region[0].y - y });
 | 
				
			||||||
					return;
 | 
										return;
 | 
				
			||||||
| 
						 | 
					@ -110,6 +139,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
 | 
				
			||||||
					x: x + offset.x,
 | 
										x: x + offset.x,
 | 
				
			||||||
					y: y + offset.y,
 | 
										y: y + offset.y,
 | 
				
			||||||
					label: blurredRegions.current[regionIndex][pointIndex]?.label,
 | 
										label: blurredRegions.current[regionIndex][pointIndex]?.label,
 | 
				
			||||||
 | 
										subLabel: blurredRegions.current[regionIndex][pointIndex]?.subLabel,
 | 
				
			||||||
				};
 | 
									};
 | 
				
			||||||
				redrawCanvas();
 | 
									redrawCanvas();
 | 
				
			||||||
			} else if (movingRegionIndex !== null) {
 | 
								} else if (movingRegionIndex !== null) {
 | 
				
			||||||
| 
						 | 
					@ -125,14 +155,14 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		canvas.addEventListener('mousedown', handleCanvasMouseDown);
 | 
							canvas.addEventListener("mousedown", handleCanvasMouseDown);
 | 
				
			||||||
		canvas.addEventListener('mouseup', handleCanvasMouseUp);
 | 
							canvas.addEventListener("mouseup", handleCanvasMouseUp);
 | 
				
			||||||
		canvas.addEventListener('mousemove', handleCanvasMouseMove);
 | 
							canvas.addEventListener("mousemove", handleCanvasMouseMove);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return () => {
 | 
							return () => {
 | 
				
			||||||
			canvas.removeEventListener('mousedown', handleCanvasMouseDown);
 | 
								canvas.removeEventListener("mousedown", handleCanvasMouseDown);
 | 
				
			||||||
			canvas.removeEventListener('mouseup', handleCanvasMouseUp);
 | 
								canvas.removeEventListener("mouseup", handleCanvasMouseUp);
 | 
				
			||||||
			canvas.removeEventListener('mousemove', handleCanvasMouseMove);
 | 
								canvas.removeEventListener("mousemove", handleCanvasMouseMove);
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	}, [draggingPointIndex, movingRegionIndex, offset, isOpen, info]);
 | 
						}, [draggingPointIndex, movingRegionIndex, offset, isOpen, info]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -160,17 +190,17 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const drawRedPoint = (canvas, x, y) => {
 | 
						const drawRedPoint = (canvas, x, y) => {
 | 
				
			||||||
		const ctx = canvas.getContext('2d');
 | 
							const ctx = canvas.getContext("2d");
 | 
				
			||||||
		ctx.save();
 | 
							ctx.save();
 | 
				
			||||||
		ctx.beginPath();
 | 
							ctx.beginPath();
 | 
				
			||||||
		ctx.arc(x, y, 5, 0, 2 * Math.PI);
 | 
							ctx.arc(x, y, 5, 0, 2 * Math.PI);
 | 
				
			||||||
		ctx.fillStyle = 'red';
 | 
							ctx.fillStyle = "red";
 | 
				
			||||||
		ctx.fill();
 | 
							ctx.fill();
 | 
				
			||||||
		ctx.restore();
 | 
							ctx.restore();
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const drawBorder = (canvas, points) => {
 | 
						const drawBorder = (canvas, points) => {
 | 
				
			||||||
		const ctx = canvas.getContext('2d');
 | 
							const ctx = canvas.getContext("2d");
 | 
				
			||||||
		ctx.save();
 | 
							ctx.save();
 | 
				
			||||||
		ctx.beginPath();
 | 
							ctx.beginPath();
 | 
				
			||||||
		points.forEach((point, index) => {
 | 
							points.forEach((point, index) => {
 | 
				
			||||||
| 
						 | 
					@ -182,7 +212,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
		ctx.closePath();
 | 
							ctx.closePath();
 | 
				
			||||||
		ctx.lineWidth = 2;
 | 
							ctx.lineWidth = 2;
 | 
				
			||||||
		ctx.strokeStyle = 'blue';
 | 
							ctx.strokeStyle = "blue";
 | 
				
			||||||
		ctx.stroke();
 | 
							ctx.stroke();
 | 
				
			||||||
		ctx.restore();
 | 
							ctx.restore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -190,23 +220,26 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const drawDeleteButton = (canvas, region, isSelected) => {
 | 
						const drawDeleteButton = (canvas, region, isSelected) => {
 | 
				
			||||||
		const ctx = canvas.getContext('2d');
 | 
							const ctx = canvas.getContext("2d");
 | 
				
			||||||
		const buttonX = (region[0].x + region[1].x) / 2;
 | 
							const buttonX = (region[0].x + region[1].x) / 2;
 | 
				
			||||||
		const buttonY = Math.min(region[0].y, region[1].y, region[2].y, region[3].y) - 20;
 | 
							const buttonY = Math.min(region[0].y, region[1].y, region[2].y, region[3].y) - 20;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ctx.save();
 | 
							ctx.save();
 | 
				
			||||||
		ctx.fillStyle = isSelected ? 'rgb(196, 194, 61)' : 'rgb(2, 101, 182)';
 | 
							ctx.fillStyle = isSelected ? "rgb(196, 194, 61)" : "rgb(2, 101, 182)";
 | 
				
			||||||
		ctx.fillRect(buttonX - 30, buttonY - 10, 60, 20);
 | 
							ctx.fillRect(buttonX - 30, buttonY - 10, Math.max(region[0]?.label?.length * 6 + 20, region[0]?.subLabel?.length * 5 + 10), 25);
 | 
				
			||||||
		ctx.fillStyle = 'rgb(255, 255, 255)';
 | 
							ctx.fillStyle = "rgb(255, 255, 255)";
 | 
				
			||||||
		ctx.font = '13px Arial';
 | 
							ctx.font = "9px Arial";
 | 
				
			||||||
		ctx.textAlign = 'center';
 | 
							ctx.textAlign = "center";
 | 
				
			||||||
		ctx.fillText(region[0]?.label, buttonX, buttonY + 5);
 | 
							ctx.fillText(region[0]?.subLabel, buttonX - 10, buttonY);
 | 
				
			||||||
 | 
							ctx.font = "11px Arial";
 | 
				
			||||||
 | 
							ctx.textAlign = "center";
 | 
				
			||||||
 | 
							ctx.fillText(region[0]?.label, buttonX, buttonY + 10);
 | 
				
			||||||
		ctx.restore();
 | 
							ctx.restore();
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const redrawCanvas = (index) => {
 | 
						const redrawCanvas = (index) => {
 | 
				
			||||||
		const canvas = canvasRef.current;
 | 
							const canvas = canvasRef.current;
 | 
				
			||||||
		const ctx = canvas.getContext('2d');
 | 
							const ctx = canvas.getContext("2d");
 | 
				
			||||||
		const image = new Image();
 | 
							const image = new Image();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		image.onload = () => {
 | 
							image.onload = () => {
 | 
				
			||||||
| 
						 | 
					@ -223,29 +256,39 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		image.crossOrigin = 'anonymous';
 | 
							image.crossOrigin = "anonymous";
 | 
				
			||||||
		image.src = imageSrc;
 | 
							image.src = imageSrc;
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return (
 | 
						return (
 | 
				
			||||||
		<div style={{ position: 'relative' }}>
 | 
							<div style={{ position: "relative" }}>
 | 
				
			||||||
			<canvas ref={canvasRef} />
 | 
								<canvas ref={canvasRef} />
 | 
				
			||||||
			{isOpen && (
 | 
								{isOpen && (
 | 
				
			||||||
				<div style={{ backgroundColor: '#FFF', position: 'absolute', top: info?.value[0].y - 200, left: info?.value[0].x, zIndex: 9999 }}>
 | 
									<div
 | 
				
			||||||
 | 
										style={{
 | 
				
			||||||
 | 
											backgroundColor: "#FFF",
 | 
				
			||||||
 | 
											position: "absolute",
 | 
				
			||||||
 | 
											top: info?.value[0].y - 30,
 | 
				
			||||||
 | 
											left: info?.value[0].x < 200 ? info?.value[0].x + 50 : info?.value[0].x - 170,
 | 
				
			||||||
 | 
											zIndex: 9999,
 | 
				
			||||||
 | 
										}}>
 | 
				
			||||||
					<div>
 | 
										<div>
 | 
				
			||||||
						<span style={{ color: '#000', fontSize: '18px', fontWeight: 'bold' }}>Select label</span>
 | 
											<span style={{ color: "#000", fontSize: "18px", fontWeight: "bold" }}>Select label</span>
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
					{listLabel?.map((el, i) => (
 | 
										{listLabel?.map((el, i) => (
 | 
				
			||||||
						<div key={i}>
 | 
											<div key={i}>
 | 
				
			||||||
							<button
 | 
												<button
 | 
				
			||||||
								onClick={() => {
 | 
													onClick={() => {
 | 
				
			||||||
									const newList = info?.value?.map((pre) => ({ ...pre, label: el?.name }));
 | 
														const arrLabel = el?.name?.split(".");
 | 
				
			||||||
 | 
														const label = arrLabel?.length > 1 ? arrLabel[1] : arrLabel[0];
 | 
				
			||||||
 | 
														const subLabel = arrLabel?.length > 1 ? arrLabel[0] : null;
 | 
				
			||||||
 | 
														const newList = info?.value?.map((pre) => ({ ...pre, label: label, subLabel: subLabel }));
 | 
				
			||||||
									blurredRegions.current[info?.index] = newList;
 | 
														blurredRegions.current[info?.index] = newList;
 | 
				
			||||||
									setInfo({});
 | 
														setInfo({});
 | 
				
			||||||
									setIsOpen(false);
 | 
														setIsOpen(false);
 | 
				
			||||||
									redrawCanvas();
 | 
														redrawCanvas();
 | 
				
			||||||
								}}
 | 
													}}
 | 
				
			||||||
								style={{ backgroundColor: '#ccc', width: '150px', color: '#000' }}>
 | 
													style={{ backgroundColor: "#ccc", width: "150px", color: "#000" }}>
 | 
				
			||||||
								{el?.name}
 | 
													{el?.name}
 | 
				
			||||||
							</button>
 | 
												</button>
 | 
				
			||||||
						</div>
 | 
											</div>
 | 
				
			||||||
| 
						 | 
					@ -258,7 +301,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
 | 
				
			||||||
								setIsOpen(false);
 | 
													setIsOpen(false);
 | 
				
			||||||
								redrawCanvas();
 | 
													redrawCanvas();
 | 
				
			||||||
							}}
 | 
												}}
 | 
				
			||||||
							style={{ backgroundColor: '#ff0000', marginBottom: '10px', width: '150px', color: '#fff' }}>
 | 
												style={{ backgroundColor: "#ff0000", marginBottom: "10px", width: "150px", color: "#fff" }}>
 | 
				
			||||||
							Delete
 | 
												Delete
 | 
				
			||||||
						</button>
 | 
											</button>
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,12 +1,12 @@
 | 
				
			||||||
import { Button, Tooltip, LoadingOverlay } from '@mantine/core';
 | 
					import { Button, Tooltip, LoadingOverlay } from "@mantine/core";
 | 
				
			||||||
import { notifications } from '@mantine/notifications';
 | 
					import { notifications } from "@mantine/notifications";
 | 
				
			||||||
import { IconImageInPicture } from '@tabler/icons-react';
 | 
					import { IconImageInPicture } from "@tabler/icons-react";
 | 
				
			||||||
import { useState } from 'react';
 | 
					import { useState } from "react";
 | 
				
			||||||
import { useImagesDetected } from '../../stores/use-images-detected';
 | 
					import { useImagesDetected } from "../../stores/use-images-detected";
 | 
				
			||||||
import { convertToBoundingBox } from '../../ultils';
 | 
					import { convertToBoundingBox } from "../../ultils";
 | 
				
			||||||
import { useHotkeys } from '@mantine/hooks';
 | 
					import { useHotkeys } from "@mantine/hooks";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
 | 
					export const SaveButton = ({ currentData, imageLabelRef, onSaved, disabled = false }) => {
 | 
				
			||||||
	const [loading, setLoading] = useState(false);
 | 
						const [loading, setLoading] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const { appendImageDetect, images } = useImagesDetected();
 | 
						const { appendImageDetect, images } = useImagesDetected();
 | 
				
			||||||
| 
						 | 
					@ -14,7 +14,7 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
 | 
				
			||||||
	const handleSave = async ({ blurredRegions, infoImage, imageName, listLabel }) => {
 | 
						const handleSave = async ({ blurredRegions, infoImage, imageName, listLabel }) => {
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			if (blurredRegions?.length > 0) {
 | 
								if (blurredRegions?.length > 0) {
 | 
				
			||||||
				let arrPoints = '';
 | 
									let arrPoints = "";
 | 
				
			||||||
				blurredRegions?.forEach((points) => {
 | 
									blurredRegions?.forEach((points) => {
 | 
				
			||||||
					const img_w = infoImage?.width;
 | 
										const img_w = infoImage?.width;
 | 
				
			||||||
					const img_h = infoImage?.height;
 | 
										const img_h = infoImage?.height;
 | 
				
			||||||
| 
						 | 
					@ -31,19 +31,21 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
 | 
				
			||||||
					const height = (y_max - y_min) / img_h;
 | 
										const height = (y_max - y_min) / img_h;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					// Get class ID
 | 
										// Get class ID
 | 
				
			||||||
					const class_id = listLabel.find((label) => label?.name === points[0].label)?.id || '0';
 | 
										const class_id = listLabel.find((label) => label?.name === points[0].label)?.id || "0";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					const yolo_label = `${class_id} ${x_center.toFixed(6)} ${y_center.toFixed(6)} ${width.toFixed(6)} ${height.toFixed(6)}`;
 | 
										const yolo_label = `${class_id} ${x_center.toFixed(6)} ${y_center.toFixed(6)} ${width.toFixed(6)} ${height.toFixed(6)}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					arrPoints += yolo_label + '\n';
 | 
										arrPoints += yolo_label + "\n";
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const url = `${process.env.REACT_APP_API_URL}/save`;
 | 
									const url = `${process.env.REACT_APP_API_URL}/save`;
 | 
				
			||||||
 | 
									// const url = `http://127.0.0.1:5000/save`;
 | 
				
			||||||
				const formData = new FormData();
 | 
									const formData = new FormData();
 | 
				
			||||||
				formData.append('list', arrPoints);
 | 
									formData.append("list", arrPoints);
 | 
				
			||||||
				formData.append('imageName', imageName);
 | 
									formData.append("imageName", imageName);
 | 
				
			||||||
				setLoading(true);
 | 
									setLoading(true);
 | 
				
			||||||
				const response = await fetch(url, {
 | 
									const response = await fetch(url, {
 | 
				
			||||||
					method: 'POST',
 | 
										method: "POST",
 | 
				
			||||||
					body: formData,
 | 
										body: formData,
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -55,7 +57,7 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
 | 
				
			||||||
					});
 | 
										});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					currentData.points = newPoints;
 | 
										currentData.points = newPoints;
 | 
				
			||||||
					currentData['isSave'] = true;
 | 
										currentData["isSave"] = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					const newImages = images.map((image) => {
 | 
										const newImages = images.map((image) => {
 | 
				
			||||||
						if (image.image_name === imageName) {
 | 
											if (image.image_name === imageName) {
 | 
				
			||||||
| 
						 | 
					@ -72,9 +74,9 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
 | 
				
			||||||
					appendImageDetect(newImages);
 | 
										appendImageDetect(newImages);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					notifications.show({
 | 
										notifications.show({
 | 
				
			||||||
						title: 'Save Success',
 | 
											title: "Save Success",
 | 
				
			||||||
						message: `${imageName} save success`,
 | 
											message: `${imageName} save success`,
 | 
				
			||||||
						color: 'green',
 | 
											color: "green",
 | 
				
			||||||
					});
 | 
										});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					if (onSaved) {
 | 
										if (onSaved) {
 | 
				
			||||||
| 
						 | 
					@ -84,9 +86,9 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		} catch (error) {
 | 
							} catch (error) {
 | 
				
			||||||
			notifications.show({
 | 
								notifications.show({
 | 
				
			||||||
				title: 'Save Error',
 | 
									title: "Save Error",
 | 
				
			||||||
				message: error?.message || 'Internal Server Error',
 | 
									message: error?.message || "Internal Server Error",
 | 
				
			||||||
				color: 'red',
 | 
									color: "red",
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		} finally {
 | 
							} finally {
 | 
				
			||||||
			setLoading(false);
 | 
								setLoading(false);
 | 
				
			||||||
| 
						 | 
					@ -101,11 +103,11 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
 | 
				
			||||||
		handleSave(saveData);
 | 
							handleSave(saveData);
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	useHotkeys([['ctrl+S', handleSubmit]]);
 | 
						useHotkeys([["ctrl+S", handleSubmit]]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return (
 | 
						return (
 | 
				
			||||||
		<Tooltip label={'Ctrl + S'}>
 | 
							<Tooltip label={"Ctrl + S"}>
 | 
				
			||||||
			<Button className="relative ư-f" disabled={!currentData} onClick={handleSubmit} leftSection={<IconImageInPicture size={14} />}>
 | 
								<Button className="relative ư-f" disabled={!currentData || disabled} onClick={handleSubmit} leftSection={<IconImageInPicture size={14} />}>
 | 
				
			||||||
				Save
 | 
									Save
 | 
				
			||||||
				<LoadingOverlay visible={loading} loaderProps={{ size: 14 }} />
 | 
									<LoadingOverlay visible={loading} loaderProps={{ size: 14 }} />
 | 
				
			||||||
			</Button>
 | 
								</Button>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,7 @@ from routes.save import save_bp
 | 
				
			||||||
from routes.show import show_bp
 | 
					from routes.show import show_bp
 | 
				
			||||||
from routes.delete import delete_bp
 | 
					from routes.delete import delete_bp
 | 
				
			||||||
from routes.upload import upload_bp
 | 
					from routes.upload import upload_bp
 | 
				
			||||||
 | 
					from routes.label import config_label
 | 
				
			||||||
 | 
					
 | 
				
			||||||
app = Flask(__name__)
 | 
					app = Flask(__name__)
 | 
				
			||||||
CORS(app)
 | 
					CORS(app)
 | 
				
			||||||
| 
						 | 
					@ -17,6 +18,7 @@ app.register_blueprint(save_bp)
 | 
				
			||||||
app.register_blueprint(show_bp)
 | 
					app.register_blueprint(show_bp)
 | 
				
			||||||
app.register_blueprint(delete_bp)
 | 
					app.register_blueprint(delete_bp)
 | 
				
			||||||
app.register_blueprint(upload_bp)
 | 
					app.register_blueprint(upload_bp)
 | 
				
			||||||
 | 
					app.register_blueprint(config_label)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if __name__ == '__main__':
 | 
					if __name__ == '__main__':
 | 
				
			||||||
    app.run(debug=True)
 | 
					    app.run(debug=True)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,6 +9,7 @@ import random
 | 
				
			||||||
import cv2
 | 
					import cv2
 | 
				
			||||||
import json
 | 
					import json
 | 
				
			||||||
import time
 | 
					import time
 | 
				
			||||||
 | 
					import subprocess
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Load mô hình YOLO
 | 
					# Load mô hình YOLO
 | 
				
			||||||
model = YOLO(get_latest_model_including_today(trained_model_folder=TRAINED_MODEL_FOLDER))
 | 
					model = YOLO(get_latest_model_including_today(trained_model_folder=TRAINED_MODEL_FOLDER))
 | 
				
			||||||
| 
						 | 
					@ -139,6 +140,19 @@ def reload_model():
 | 
				
			||||||
    model = YOLO(new_model_path)  
 | 
					    model = YOLO(new_model_path)  
 | 
				
			||||||
    return jsonify({"message": f"Model reloaded from {new_model_path}", "status": "true"}), 200
 | 
					    return jsonify({"message": f"Model reloaded from {new_model_path}", "status": "true"}), 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@detect_bp.route('/train', methods=['GET'])
 | 
				
			||||||
 | 
					def run_training():
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        # Run train.py as a subprocess
 | 
				
			||||||
 | 
					        process = subprocess.Popen(["python", "train.py"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 | 
				
			||||||
 | 
					        stdout, stderr = process.communicate()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if process.returncode == 0:
 | 
				
			||||||
 | 
					            return jsonify({"message": "Training started successfully", "output": stdout.decode()}), 200
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return jsonify({"error": "Training failed", "details": stderr.decode()}), 500
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    except Exception as e:
 | 
				
			||||||
 | 
					        return jsonify({"error": str(e)}), 500
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,141 @@
 | 
				
			||||||
 | 
					import datetime
 | 
				
			||||||
 | 
					from flask import Blueprint, request, jsonify, send_from_directory
 | 
				
			||||||
 | 
					import sqlite3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					config_label = Blueprint('label', __name__)
 | 
				
			||||||
 | 
					# Connect to database (creates a new one if it doesn’t exist)
 | 
				
			||||||
 | 
					conn = sqlite3.connect("my_database.db")
 | 
				
			||||||
 | 
					# Create a cursor object to execute SQL commands
 | 
				
			||||||
 | 
					cursor = conn.cursor()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Connect to database
 | 
				
			||||||
 | 
					def get_db_connection():
 | 
				
			||||||
 | 
					    conn = sqlite3.connect("my_database.db")
 | 
				
			||||||
 | 
					    conn.row_factory = sqlite3.Row  # Enables dictionary-like access to rows
 | 
				
			||||||
 | 
					    return conn
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Create table (only call once at startup)
 | 
				
			||||||
 | 
					def create_table():
 | 
				
			||||||
 | 
					    conn = get_db_connection()
 | 
				
			||||||
 | 
					    cursor = conn.cursor()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Check if the table exists
 | 
				
			||||||
 | 
					    cursor.execute("""
 | 
				
			||||||
 | 
					        SELECT name FROM sqlite_master WHERE type='table' AND name='labels'
 | 
				
			||||||
 | 
					    """)
 | 
				
			||||||
 | 
					    table_exists = cursor.fetchone()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not table_exists:
 | 
				
			||||||
 | 
					        # Create table if it doesn't exist
 | 
				
			||||||
 | 
					        cursor.execute("""
 | 
				
			||||||
 | 
					            CREATE TABLE labels (
 | 
				
			||||||
 | 
					                id INTEGER PRIMARY KEY AUTOINCREMENT,
 | 
				
			||||||
 | 
					                name TEXT NOT NULL,
 | 
				
			||||||
 | 
					                code INTEGER NOT NULL,
 | 
				
			||||||
 | 
					                status TEXT DEFAULT 'active',
 | 
				
			||||||
 | 
					                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 | 
				
			||||||
 | 
					                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        """)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Insert default values
 | 
				
			||||||
 | 
					        default_values = [
 | 
				
			||||||
 | 
					            ("logo.cisco", 0),
 | 
				
			||||||
 | 
					            ("barcode.SN", 1)
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					        cursor.executemany("INSERT INTO labels (name, code) VALUES (?, ?)", default_values)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        print("Table created and default values inserted.")
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        print("Table already exists.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    conn.commit()
 | 
				
			||||||
 | 
					    conn.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Call create_table() when the script runs
 | 
				
			||||||
 | 
					create_table()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Route to add a new label
 | 
				
			||||||
 | 
					@config_label.route('/label/add', methods=['POST'])
 | 
				
			||||||
 | 
					def add_label():
 | 
				
			||||||
 | 
					    data = request.get_json()  # Get JSON data from request
 | 
				
			||||||
 | 
					    name = data.get("name")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not name:
 | 
				
			||||||
 | 
					        return jsonify({"error": "Name is required"}), 400
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    conn = get_db_connection()
 | 
				
			||||||
 | 
					    cursor = conn.cursor()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Get the last code value
 | 
				
			||||||
 | 
					    cursor.execute("SELECT MAX(code) FROM labels")
 | 
				
			||||||
 | 
					    last_code = cursor.fetchone()[0]
 | 
				
			||||||
 | 
					    new_code = (last_code + 1) if last_code is not None else 1  # Start from 1 if table is empty
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Insert new label
 | 
				
			||||||
 | 
					    cursor.execute(
 | 
				
			||||||
 | 
					        "INSERT INTO labels (name, code) VALUES (?, ?)", 
 | 
				
			||||||
 | 
					        (name, new_code)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    conn.commit()
 | 
				
			||||||
 | 
					    conn.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return jsonify({"message": "Label added successfully", "name": name, "code": new_code, "success" : True}), 201
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Route to update a label
 | 
				
			||||||
 | 
					@config_label.route('/label/update', methods=['POST'])
 | 
				
			||||||
 | 
					def update_label():
 | 
				
			||||||
 | 
					    data = request.get_json()
 | 
				
			||||||
 | 
					    name = data.get("name")
 | 
				
			||||||
 | 
					    label_id = data.get("id")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not name or not label_id:
 | 
				
			||||||
 | 
					        return jsonify({"error": "Name and id are required"}), 400
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    conn = get_db_connection()
 | 
				
			||||||
 | 
					    cursor = conn.cursor()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    cursor.execute(
 | 
				
			||||||
 | 
					        "UPDATE labels SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", 
 | 
				
			||||||
 | 
					        (name, label_id)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    conn.commit()
 | 
				
			||||||
 | 
					    conn.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return jsonify({"message": "Label updated successfully", "success" : True}), 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@config_label.route('/labels', methods=['GET'])
 | 
				
			||||||
 | 
					def get_labels():
 | 
				
			||||||
 | 
					    conn = get_db_connection()
 | 
				
			||||||
 | 
					    cursor = conn.cursor()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    cursor.execute("SELECT * FROM labels WHERE status = 'active'")
 | 
				
			||||||
 | 
					    labels = cursor.fetchall()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    conn.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Convert rows to list of dictionaries
 | 
				
			||||||
 | 
					    label_list = [dict(row) for row in labels]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return jsonify(label_list), 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Route to delete a label
 | 
				
			||||||
 | 
					@config_label.route('/delete/<int:label_id>', methods=['GET'])
 | 
				
			||||||
 | 
					def delete_label(label_id):
 | 
				
			||||||
 | 
					    if not label_id is None:
 | 
				
			||||||
 | 
					        return jsonify({"error": "ID label are required"}), 400
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    conn = get_db_connection()
 | 
				
			||||||
 | 
					    cursor = conn.cursor()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    cursor.execute(
 | 
				
			||||||
 | 
					        "UPDATE labels SET status = 'delete' WHERE id = ?", 
 | 
				
			||||||
 | 
					        (label_id)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    conn.commit()
 | 
				
			||||||
 | 
					    conn.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return jsonify({"message": "Label delete successfully"}), 200
 | 
				
			||||||
| 
						 | 
					@ -7,13 +7,31 @@ from ultralytics import YOLO
 | 
				
			||||||
import yaml
 | 
					import yaml
 | 
				
			||||||
import requests
 | 
					import requests
 | 
				
			||||||
import glob
 | 
					import glob
 | 
				
			||||||
 | 
					import sqlite3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# 🗂️ Configure paths
 | 
					# 🗂️ Configure paths
 | 
				
			||||||
DATA_SPLIT_FOLDER = "model_datasets"
 | 
					DATA_SPLIT_FOLDER = "model_datasets"
 | 
				
			||||||
IMAGE_EXTENSION = ".png"  
 | 
					IMAGE_EXTENSION = ".png"  
 | 
				
			||||||
LOG_FILE = "training_logs.log"
 | 
					LOG_FILE = "training_logs.log"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class_names = ["cisco", "barcode"]
 | 
					def get_label_names():
 | 
				
			||||||
 | 
					    conn = sqlite3.connect("my_database.db")
 | 
				
			||||||
 | 
					    conn.row_factory = sqlite3.Row  # Enables dictionary-like access to rows
 | 
				
			||||||
 | 
					    # Create a cursor object to execute SQL commands
 | 
				
			||||||
 | 
					    cursor = conn.cursor()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Fetch all names from the labels table
 | 
				
			||||||
 | 
					    cursor.execute("SELECT name FROM labels")
 | 
				
			||||||
 | 
					    labels = cursor.fetchall()
 | 
				
			||||||
 | 
					    conn.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Convert fetched rows into a simple list of strings
 | 
				
			||||||
 | 
					    label_names = [row["name"] for row in labels]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return label_names
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class_names = get_label_names()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# 📅 Get today's and yesterday's date
 | 
					# 📅 Get today's and yesterday's date
 | 
				
			||||||
today = datetime.date.today()
 | 
					today = datetime.date.today()
 | 
				
			||||||
yesterday = today - datetime.timedelta(days=1)
 | 
					yesterday = today - datetime.timedelta(days=1)
 | 
				
			||||||
| 
						 | 
					@ -37,9 +55,9 @@ elif os.path.exists(image_folder_yesterday) and os.path.exists(label_folder_yest
 | 
				
			||||||
    label_folder = label_folder_yesterday
 | 
					    label_folder = label_folder_yesterday
 | 
				
			||||||
    date_str = yesterday_str
 | 
					    date_str = yesterday_str
 | 
				
			||||||
else:
 | 
					else:
 | 
				
			||||||
    print(image_folder)
 | 
					    # print(image_folder)
 | 
				
			||||||
    print(label_folder)
 | 
					    # print(label_folder)
 | 
				
			||||||
    raise Exception("⚠️ No valid image & label folder found in the last two days!")
 | 
					    raise Exception("  No valid image & label folder found in the last two days!")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# 🏗️ Create dataset folders
 | 
					# 🏗️ Create dataset folders
 | 
				
			||||||
dataset_folder = os.path.join(DATA_SPLIT_FOLDER, date_str)
 | 
					dataset_folder = os.path.join(DATA_SPLIT_FOLDER, date_str)
 | 
				
			||||||
| 
						 | 
					@ -78,7 +96,7 @@ def log_message(message: str):
 | 
				
			||||||
def get_latest_model(trained_model_folder: str, default_model: str = "train5/weights/best.pt"):
 | 
					def get_latest_model(trained_model_folder: str, default_model: str = "train5/weights/best.pt"):
 | 
				
			||||||
    """Tìm model gần nhất (trước ngày hôm nay), nếu không có thì dùng model mặc định."""
 | 
					    """Tìm model gần nhất (trước ngày hôm nay), nếu không có thì dùng model mặc định."""
 | 
				
			||||||
    if not os.path.exists(trained_model_folder):
 | 
					    if not os.path.exists(trained_model_folder):
 | 
				
			||||||
        log_message(f"⚠️ Folder {trained_model_folder} does not exist. Using default model: {default_model}")
 | 
					        log_message(f"  Folder {trained_model_folder} does not exist. Using default model: {default_model}")
 | 
				
			||||||
        return default_model
 | 
					        return default_model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    today_str = datetime.datetime.now().strftime("%Y-%m-%d")
 | 
					    today_str = datetime.datetime.now().strftime("%Y-%m-%d")
 | 
				
			||||||
| 
						 | 
					@ -93,10 +111,10 @@ def get_latest_model(trained_model_folder: str, default_model: str = "train5/wei
 | 
				
			||||||
    for folder in subfolders:
 | 
					    for folder in subfolders:
 | 
				
			||||||
        model_path = os.path.join(trained_model_folder, folder, "weights", "best.pt")
 | 
					        model_path = os.path.join(trained_model_folder, folder, "weights", "best.pt")
 | 
				
			||||||
        if os.path.exists(model_path):
 | 
					        if os.path.exists(model_path):
 | 
				
			||||||
            log_message(f"✅ Found latest model: {model_path}")
 | 
					            log_message(f" Found latest model: {model_path}")
 | 
				
			||||||
            return model_path
 | 
					            return model_path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    log_message(f"⚠️ No valid models found in {trained_model_folder}. Using default model: {default_model}")
 | 
					    log_message(f"  No valid models found in {trained_model_folder}. Using default model: {default_model}")
 | 
				
			||||||
    return default_model
 | 
					    return default_model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def create_dataset_yaml(dataset_folder: str, classes: list):
 | 
					def create_dataset_yaml(dataset_folder: str, classes: list):
 | 
				
			||||||
| 
						 | 
					@ -122,10 +140,10 @@ def call_reload_model_api(base_url="http://localhost:5000"):
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        response = requests.post(url, timeout=10)
 | 
					        response = requests.post(url, timeout=10)
 | 
				
			||||||
        response_data = response.json()
 | 
					        response_data = response.json()
 | 
				
			||||||
        log_message(f"✅ API Response: {response_data} \n")
 | 
					        log_message(f" API Response: {response_data} \n")
 | 
				
			||||||
        return response_data
 | 
					        return response_data
 | 
				
			||||||
    except requests.RequestException as e:
 | 
					    except requests.RequestException as e:
 | 
				
			||||||
        log_message(f"❌ Error calling reload model API: {e} \n")
 | 
					        log_message(f" Error calling reload model API: {e} \n")
 | 
				
			||||||
        return {"error": str(e)}
 | 
					        return {"error": str(e)}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def clear_images_source():
 | 
					def clear_images_source():
 | 
				
			||||||
| 
						 | 
					@ -159,8 +177,8 @@ def move_files(file_list, dest_img_folder, dest_lbl_folder):
 | 
				
			||||||
                copied_images += 1
 | 
					                copied_images += 1
 | 
				
			||||||
                copied_labels += 1
 | 
					                copied_labels += 1
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                log.write(f"[{datetime.datetime.now()}] ⚠️ Missing label for image: {img_file}\n")
 | 
					                log.write(f"[{datetime.datetime.now()}]   Missing label for image: {img_file}\n")
 | 
				
			||||||
                shutil.copy(img_path, os.path.join(nolabel_img_folder, img_file))
 | 
					                shutil.copy(img_path, os.path.join(nolable_img_folder, img_file))
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
| 
						 | 
					@ -171,10 +189,10 @@ def train_yolo_model(pretrained_model: str, dataset_folder: str,project_name: st
 | 
				
			||||||
    dataset_yaml = os.path.join(dataset_folder, "data.yaml")
 | 
					    dataset_yaml = os.path.join(dataset_folder, "data.yaml")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if not os.path.exists(dataset_yaml):
 | 
					    if not os.path.exists(dataset_yaml):
 | 
				
			||||||
        raise FileNotFoundError(f"⚠️ Not found file {dataset_yaml}. Plases check datasets!")
 | 
					        raise FileNotFoundError(f"  Not found file {dataset_yaml}. Plases check datasets!")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if not os.path.exists(pretrained_model):
 | 
					    if not os.path.exists(pretrained_model):
 | 
				
			||||||
        log_message(f"⚠️ Model not found {pretrained_model}. Start train with 'yolov8n.pt'.")
 | 
					        log_message(f"  Model not found {pretrained_model}. Start train with 'yolov8n.pt'.")
 | 
				
			||||||
        pretrained_model = "yolov8n.pt"
 | 
					        pretrained_model = "yolov8n.pt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # 🚀 Tạo model YOLOv8 và load model đã train trước đó
 | 
					    # 🚀 Tạo model YOLOv8 và load model đã train trước đó
 | 
				
			||||||
| 
						 | 
					@ -196,7 +214,7 @@ def train_yolo_model(pretrained_model: str, dataset_folder: str,project_name: st
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with open(LOG_FILE, "a") as log:
 | 
					    with open(LOG_FILE, "a") as log:
 | 
				
			||||||
        log.write(f"\n[{datetime.datetime.now()}] ✅ Train completed\n")
 | 
					        log.write(f"\n[{datetime.datetime.now()}]  Train completed\n")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    call_reload_model_api()
 | 
					    call_reload_model_api()
 | 
				
			||||||
    clear_images_source()
 | 
					    clear_images_source()
 | 
				
			||||||
| 
						 | 
					@ -214,7 +232,7 @@ create_dataset_yaml(dataset_folder, class_names)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# 🏁 Log summary
 | 
					# 🏁 Log summary
 | 
				
			||||||
with open(LOG_FILE, "a") as log:
 | 
					with open(LOG_FILE, "a") as log:
 | 
				
			||||||
    log.write(f"\n[{datetime.datetime.now()}] ✅ Dataset split completed\n")
 | 
					    log.write(f"\n[{datetime.datetime.now()}]  Dataset split completed\n")
 | 
				
			||||||
    log.write(f"Source folder: {image_folder}\n")
 | 
					    log.write(f"Source folder: {image_folder}\n")
 | 
				
			||||||
    log.write(f"Dataset saved in: {dataset_folder}\n")
 | 
					    log.write(f"Dataset saved in: {dataset_folder}\n")
 | 
				
			||||||
    log.write(f"Planned Train: {len(train_files)} images, Val: {len(val_files)} images\n")
 | 
					    log.write(f"Planned Train: {len(train_files)} images, Val: {len(val_files)} images\n")
 | 
				
			||||||
| 
						 | 
					@ -224,7 +242,7 @@ with open(LOG_FILE, "a") as log:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if(train_copied_imgs <=0 or train_copied_lbls <= 0 or val_copied_imgs <=0 or val_copied_lbls <=0):
 | 
					if(train_copied_imgs <=0 or train_copied_lbls <= 0 or val_copied_imgs <=0 or val_copied_lbls <=0):
 | 
				
			||||||
    with open(LOG_FILE, "a") as log:
 | 
					    with open(LOG_FILE, "a") as log:
 | 
				
			||||||
        log.write(f"\n[{datetime.datetime.now()}] ❌ Data not qualified\n")
 | 
					        log.write(f"\n[{datetime.datetime.now()}]  Data not qualified\n")
 | 
				
			||||||
        log.write("=" * 50 + "\n")
 | 
					        log.write("=" * 50 + "\n")
 | 
				
			||||||
else:
 | 
					else:
 | 
				
			||||||
    train_yolo_model(pretrained_model = get_latest_model(
 | 
					    train_yolo_model(pretrained_model = get_latest_model(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,6 @@
 | 
				
			||||||
export const generateImageUrl = (image_path) => {
 | 
					export const generateImageUrl = (image_path) => {
 | 
				
			||||||
	return process.env.REACT_APP_API_URL + `/${image_path}`;
 | 
						return process.env.REACT_APP_API_URL + `/${image_path}`;
 | 
				
			||||||
 | 
						// return `http://127.0.0.1:5000/${image_path}`;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function convertToBoundingBox(points) {
 | 
					export function convertToBoundingBox(points) {
 | 
				
			||||||
| 
						 | 
					@ -19,6 +20,6 @@ export const randomDelay = () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function generateClientID() {
 | 
					export function generateClientID() {
 | 
				
			||||||
	const id = Math.random().toString(36).substr(2, 9);
 | 
						const id = Math.random().toString(36).substr(2, 9);
 | 
				
			||||||
	localStorage.setItem('client_id', id);
 | 
						localStorage.setItem("client_id", id);
 | 
				
			||||||
	return id;
 | 
						return id;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,21 +4,21 @@ self.onmessage = async function (e) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let processFiles = 0;
 | 
						let processFiles = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (action === 'processImages') {
 | 
						if (action === "processImages") {
 | 
				
			||||||
		for (const file of files) {
 | 
							for (const file of files) {
 | 
				
			||||||
			try {
 | 
								try {
 | 
				
			||||||
				const formData = new FormData();
 | 
									const formData = new FormData();
 | 
				
			||||||
				formData.append('image', file, file.name);
 | 
									formData.append("image", file, file.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const response = await fetch('/api/detect_image', {
 | 
									const response = await fetch(`${process.env.REACT_APP_API_URL}/detect_image`, {
 | 
				
			||||||
					method: 'POST',
 | 
										method: "POST",
 | 
				
			||||||
					body: formData,
 | 
										body: formData,
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const result = await response.json();
 | 
									const result = await response.json();
 | 
				
			||||||
				self.postMessage({ status: 'success', result, total: files.length, processFiles: ++processFiles });
 | 
									self.postMessage({ status: "success", result, total: files.length, processFiles: ++processFiles });
 | 
				
			||||||
			} catch (error) {
 | 
								} catch (error) {
 | 
				
			||||||
				self.postMessage({ status: 'error', error: error.message });
 | 
									self.postMessage({ status: "error", error: error.message });
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue