diff --git a/src/frontend/package.json b/src/frontend/package.json index c0aecc38..e7a9c3ff 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -10,6 +10,8 @@ "preview": "vite preview" }, "dependencies": { + "@azure/msal-browser": "^4.2.0", + "@azure/msal-react": "^3.0.4", "@fluentui/react": "^8.122.9", "@fluentui/react-components": "^9.56.7", "@fluentui/react-file-type-icons": "^8.12.7", @@ -34,12 +36,11 @@ "sql-formatter": "^15.4.11", "tailwind-merge": "^2.6.0", "tailwindcss": "^3.4.17", - "uuid": "^11.0.5", - "@azure/msal-browser": "^4.2.0", - "@azure/msal-react": "^3.0.4" + "uuid": "^11.0.5" }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/node": "^22.14.0", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "@vitejs/plugin-react": "^4.3.4", @@ -50,6 +51,7 @@ "globals": "^15.14.0", "rollup": "^4.34.4", "rollup-plugin-dts": "^6.1.1", + "sass-embedded": "^1.86.3", "vite": "^6.0.5", "vite-plugin-svgr": "^4.3.0" } diff --git a/src/frontend/src/App.css b/src/frontend/src/App.css index 7baf2ad9..72d1c89f 100644 --- a/src/frontend/src/App.css +++ b/src/frontend/src/App.css @@ -49,20 +49,18 @@ } -.landing-page p { - font-size: 1.2rem; - font-weight: bold; -} - -.landing-page main { - padding-top: 16rem; /* Adjust the value as needed */ -} - -.landing-page p { - margin-bottom: 8rem; /* Space between text and upload button */ -} - .sidebar { height: 100vh; /* Make it full viewport height */ background-color: white; +} + +.panelRight{ + position: "fixed"; + top:'60px'; + right: 0; + height: "calc(100vh - 60px)"; + width: "300px"; + z-index: 1050; + background: "white"; + overflow-y: "auto"; } \ No newline at end of file diff --git a/src/frontend/src/App.jsx b/src/frontend/src/App.jsx index 5ee27f24..d6c9ae0c 100644 --- a/src/frontend/src/App.jsx +++ b/src/frontend/src/App.jsx @@ -1,26 +1,89 @@ - -import './App.css' -import { BrowserRouter as Router, Routes, Route } from 'react-router-dom' -import LandingPage from './pages/landingPage'; -import ModernizationPage from './pages/modernizationPage'; -import BatchViewPage from './pages/batchView'; -import { initializeIcons } from '@fluentui/react'; +import "./App.css"; +import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; +import LandingPage from "./pages/landingPage/landingPage"; +import ModernizationPage from "./pages/modernizationPage/modernizationPage"; +import BatchViewPage from "./pages/batchViewPage/batchView"; +import { initializeIcons } from "@fluentui/react"; +import Header from "./components/Header/Header"; +import HeaderTools from "./components/Header/HeaderTools"; +import PanelRightToggles from "./components/Header/PanelRightToggles"; +import { Button, Tooltip } from "@fluentui/react-components"; +import { + HistoryRegular, + HistoryFilled, + bundleIcon, +} from "@fluentui/react-icons"; +export const History = bundleIcon(HistoryFilled, HistoryRegular); +//import RootState from "./store/store"; +import { useSelector, useDispatch } from "react-redux"; +import { togglePanel, closePanel } from "./store/historyPanelSlice"; +import PanelRightToolbar from "./components/Panels/PanelRightToolbar"; +import PanelRight from "./components/Panels/PanelRight"; +import BatchHistoryPanel from "./components/batchHistoryPanel/batchHistoryPanel"; initializeIcons(); function App() { + const dispatch = useDispatch(); + const isPanelOpen = useSelector((state) => state.historyPanel.isOpen); + const handleLeave = () => { + if (window.cancelLogoUploads) { + window.cancelLogoUploads(); + } + }; + + const handleTogglePanel = () => { + dispatch(togglePanel()); + }; return (
+
+
+ + + +
+
} /> - } /> + } + /> } /> + {isPanelOpen && ( +
+ + } + handleDismiss={handleTogglePanel} + /> + dispatch(closePanel())} + /> + +
+ )}
); } -export default App \ No newline at end of file +export default App; diff --git a/src/frontend/src/commonComponents/ConfirmationDialog/confirmationDialogue.scss b/src/frontend/src/commonComponents/ConfirmationDialog/confirmationDialogue.scss new file mode 100644 index 00000000..5ccec5e9 --- /dev/null +++ b/src/frontend/src/commonComponents/ConfirmationDialog/confirmationDialogue.scss @@ -0,0 +1,34 @@ +.dialogBody{ + display: flex; + justify-content: space-between; + align-items: right +} + +.dismissButton{ + position: absolute; + top: 8px; + right: 8px; + width: 32px; + height: 32px; +} + +.dialogActionContainer{ + display: flex; + justify-content: space-between; + gap: 8px; + flex-wrap: nowrap; +} + +.actionButton{ + flex-grow: 1; + min-width: 120px; + max-width: 175px; + white-space: nowrap; +} + +.secondaryButton{ + flex-grow: 1; + min-width: 100px; + max-width: auto; + white-space: nowrap; +} \ No newline at end of file diff --git a/src/frontend/src/commonComponents/ConfirmationDialog/confirmationDialogue.tsx b/src/frontend/src/commonComponents/ConfirmationDialog/confirmationDialogue.tsx index cd65ae3e..095730fb 100644 --- a/src/frontend/src/commonComponents/ConfirmationDialog/confirmationDialogue.tsx +++ b/src/frontend/src/commonComponents/ConfirmationDialog/confirmationDialogue.tsx @@ -2,53 +2,33 @@ import React from "react"; import { Dialog, DialogSurface, DialogBody, DialogTitle, DialogContent, DialogActions } from "@fluentui/react-components"; import { Button } from "@fluentui/react-components"; import { Dismiss24Regular } from "@fluentui/react-icons"; +import "./confirmationDialogue.scss"; const ConfirmationDialog = ({ open, setOpen, title, message, confirmText, cancelText, onConfirm, onCancel }) => { return ( setOpen(data.open)}> -
+
{title}
{message} + className="dialogActionContainer"> {cancelText && cancelText.trim() !== "" && ( diff --git a/src/frontend/src/commonComponents/errorContent/errorContent.scss b/src/frontend/src/commonComponents/errorContent/errorContent.scss new file mode 100644 index 00000000..1ddf7996 --- /dev/null +++ b/src/frontend/src/commonComponents/errorContent/errorContent.scss @@ -0,0 +1,10 @@ + + .errorItem { + margin-top: 16px; + padding-left: 20px; + padding-bottom: 16px; + } + .tbSql{ + font-size: 16px; + color: #519ABA; + } diff --git a/src/frontend/src/commonComponents/errorContent/errorContent.tsx b/src/frontend/src/commonComponents/errorContent/errorContent.tsx new file mode 100644 index 00000000..7906c615 --- /dev/null +++ b/src/frontend/src/commonComponents/errorContent/errorContent.tsx @@ -0,0 +1,37 @@ +import { Accordion, AccordionItem, AccordionHeader, AccordionPanel,Text } from "@fluentui/react-components"; +import React from "react"; +import { TbSql } from "react-icons/tb"; +import './errorContent.scss'; +import ErrorComponent from "../errorsComponent/errorComponent"; + +export const ErrorContent = (props) => { + + const errorFiles = props?.batchSummary.files.filter(file => file.error_count && file.error_count); + if (errorFiles.length === 0) { + return ( +
+ No errors found. +
+ ); + } + + return ( +
+ file.file_id)}> + {errorFiles.map((file, idx) =>{ + return ( + + + + + {file.name} ({file.error_count}) + + + + + + )})} + +
+ ); +}; \ No newline at end of file diff --git a/src/frontend/src/commonComponents/errorsComponent/errorComponent.scss b/src/frontend/src/commonComponents/errorsComponent/errorComponent.scss new file mode 100644 index 00000000..15f90e04 --- /dev/null +++ b/src/frontend/src/commonComponents/errorsComponent/errorComponent.scss @@ -0,0 +1,43 @@ +.pl_16{ + padding-left: 16px; +} + +.ml_8{ + margin-left: 8px; +} + +.infoText{ + display: flex !important; + align-items: flex-start +} + +.iconContainer{ + flex-shrink: 0; + display: inline-block; + width: 16px; + height: 16px; + margin-right: 8px; + margin-top: 3px; +} + +.errorIcon{ + color: var(--colorStatusDangerForeground1); + width: 16px; + height: 16px; +} + +.warningIcon{ + color: #B89500; + width: 16px; + height: 16px; +} + +.infoIcon{ + color: #007ACC; + width: 16px; + height: 16px; +} + +.pl_24{ + padding-left: 24px; +} diff --git a/src/frontend/src/commonComponents/errorsComponent/errorComponent.tsx b/src/frontend/src/commonComponents/errorsComponent/errorComponent.tsx new file mode 100644 index 00000000..ccdb42d9 --- /dev/null +++ b/src/frontend/src/commonComponents/errorsComponent/errorComponent.tsx @@ -0,0 +1,36 @@ +import { List,ListItem, tokens,Text } from "@fluentui/react-components"; +import { DismissCircle24Regular, Warning24Regular, InfoRegular } from "@fluentui/react-icons"; +import React from "react"; +import "./errorComponent.scss"; + +const ErrorComponent = (props) => { + const {file} = props; + return ( + <> + {file?.file_logs?.length > 0 ? ( + + {file.file_logs?.map((log, logIdx) => ( + + + + {log.logType === "error" ? ( + + ) : log.logType === "warning" ? ( + + ) : ( + + )} + + {log.agentType}: {log.description} + + + ))} + + ) : ( +

No detailed logs available.

+ )} + + ); + }; + + export default ErrorComponent; \ No newline at end of file diff --git a/src/frontend/src/commonComponents/fileError/fileError.scss b/src/frontend/src/commonComponents/fileError/fileError.scss new file mode 100644 index 00000000..a5936692 --- /dev/null +++ b/src/frontend/src/commonComponents/fileError/fileError.scss @@ -0,0 +1,23 @@ +.errorSection{ + background-color: #F8DADB !important; + margin-bottom: 8px; + height: 40px; + box-shadow: none +} +.sectionHeader{ + display: flex; + height: 40px; + align-items: center; + justify-content: space-between; + cursor: pointer; + box-sizing: border-box; + padding: 0px; + text-align: left +} + +.errorContentScrollable{ + max-height: 450px; + overflow-y: auto; + padding-right: 8px; + border-bottom: 1px solid #eaeaea +} \ No newline at end of file diff --git a/src/frontend/src/commonComponents/fileError/fileError.tsx b/src/frontend/src/commonComponents/fileError/fileError.tsx new file mode 100644 index 00000000..eb4cd3bf --- /dev/null +++ b/src/frontend/src/commonComponents/fileError/fileError.tsx @@ -0,0 +1,30 @@ +import { Card,Text } from "@fluentui/react-components"; +import { ChevronDown, ChevronRight } from "lucide-react"; +import React from "react"; +import { ErrorContent } from "../errorContent/errorContent"; +import "./fileError.scss"; + +export const FileError = (props) => { + const {batchSummary, expandedSections, setExpandedSections, styles}=props; + const isExpanded = expandedSections?.includes("errors"); + + return ( + <> + +
setExpandedSections((prev) => + prev.includes("errors") ? prev.filter((id) => id !== "errors") : [...prev, "errors"] + )} + > + Errors ({batchSummary.error_count || 0}) + {isExpanded ? : } +
+
+ + {isExpanded &&
+ {} +
} + + ); +}; \ No newline at end of file diff --git a/src/frontend/src/components/Content/Content.tsx b/src/frontend/src/components/Content/Content.tsx index ce95679c..c6a5803d 100644 --- a/src/frontend/src/components/Content/Content.tsx +++ b/src/frontend/src/components/Content/Content.tsx @@ -1,5 +1,6 @@ import React, { ReactNode, ReactElement } from "react"; import PanelToolbar from "../Panels/PanelLeftToolbar.js"; // Import to identify toolbar +import "./content.scss"; // Import for styling interface ContentProps { children?: ReactNode; @@ -17,25 +18,11 @@ const Content: React.FC = ({ children }) => { return (
- {toolbar &&
{toolbar}
} + {toolbar &&
{toolbar}
}
{content}
diff --git a/src/frontend/src/components/Content/ContentToolbar.tsx b/src/frontend/src/components/Content/ContentToolbar.tsx index ec5d75f4..37fb741d 100644 --- a/src/frontend/src/components/Content/ContentToolbar.tsx +++ b/src/frontend/src/components/Content/ContentToolbar.tsx @@ -1,5 +1,6 @@ import React, { ReactNode } from "react"; import { Body1Strong } from "@fluentui/react-components"; +import "./Content.scss"; // Import for styling interface ContentToolbarProps { panelIcon?: ReactNode; @@ -15,44 +16,21 @@ const ContentToolbar: React.FC = ({ return (
{(panelIcon || panelTitle) && (
{panelIcon && (
{panelIcon}
)} {panelTitle && ( {panelTitle} @@ -61,11 +39,6 @@ const ContentToolbar: React.FC = ({ )}
{children}
diff --git a/src/frontend/src/components/Content/content.scss b/src/frontend/src/components/Content/content.scss new file mode 100644 index 00000000..ba2582ce --- /dev/null +++ b/src/frontend/src/components/Content/content.scss @@ -0,0 +1,54 @@ +.content{ + display: flex; + flex: 1; + flex-direction: column; + height: 100%; + box-sizing: border-box; + position: relative; + min-width: 320px; + top: 60px; +} + +.fs_0{ + flex-shrink: 0; +} + +.panelContent{ + flex: 1; + overflow-y: auto; +} + +.panelToolbar{ + display: flex; + align-items: center; + gap: 8px; + padding: 16px; + box-sizing: border-box; + height: 56px; +} + +.penelTitle{ + display: flex; + align-items: center; + gap: 6px; + flex: 1 1 auto; + overflow: hidden; +} + +.penlIcon{ + flex-shrink: 0; // Prevent the icon from shrinking + display: flex; + align-items: center; +} + +.panelLabel{ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.panelTools{ + display: flex; + align-items: center; + gap: 0; +} \ No newline at end of file diff --git a/src/frontend/src/components/Header/Header.scss b/src/frontend/src/components/Header/Header.scss new file mode 100644 index 00000000..a001bcda --- /dev/null +++ b/src/frontend/src/components/Header/Header.scss @@ -0,0 +1,49 @@ +.header{ + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + background-color: #fafafa; + border-bottom: 1px solid var(--colorNeutralStroke2); + padding: 16px; + height: 64px; + box-sizing: border-box; + gap: 12px; + position: fixed; + z-index: 1000; +} + +.headerContainer{ + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; +} + +.headerImage{ + width: 25px; + height: 25px +} + +.subTitle{ + white-space: nowrap; + margin-top: -2px +} + +.fw_400{ + font-weight: 400 +} + +.toolbar{ + display: flex; + flex: 0; + align-items: center; + flex-direction: row-reverse; + padding: 4px 0px; +} + +.toolbar2{ + padding: 4px 0px; + display: flex; + flex-direction: row-reverse +} \ No newline at end of file diff --git a/src/frontend/src/components/Header/Header.tsx b/src/frontend/src/components/Header/Header.tsx index baccc3bc..afd5bbce 100644 --- a/src/frontend/src/components/Header/Header.tsx +++ b/src/frontend/src/components/Header/Header.tsx @@ -1,5 +1,7 @@ import React from "react"; import { Subtitle2 } from "@fluentui/react-components"; +import "./Header.scss"; // Import for styling +import { useNavigate } from "react-router-dom"; /** * @component * @name Header @@ -17,41 +19,28 @@ type HeaderProps = { }; const Header: React.FC = ({ title = "Contoso", subtitle, children }) => { + const navigate = useNavigate(); return (
{ + navigate("/"); // Redirect to home page on logo click + } + } > {/* Render custom logo or default MsftColor logo */} {/* } /> */} - Contoso + Contoso {/* Render title and optional subtitle */} - + {title} {subtitle && ( - | {subtitle} + | {subtitle} )}
diff --git a/src/frontend/src/components/Header/HeaderTools.tsx b/src/frontend/src/components/Header/HeaderTools.tsx index 6561bd48..01173882 100644 --- a/src/frontend/src/components/Header/HeaderTools.tsx +++ b/src/frontend/src/components/Header/HeaderTools.tsx @@ -1,5 +1,6 @@ import React from "react"; import { Toolbar } from "@fluentui/react-components"; +import "./Header.scss" interface HeaderToolsProps { @@ -11,13 +12,7 @@ const HeaderTools: React.FC = ({ children }) => { return ( {children} diff --git a/src/frontend/src/components/Header/PanelRightToggles.tsx b/src/frontend/src/components/Header/PanelRightToggles.tsx index cdcac8f5..790bdfff 100644 --- a/src/frontend/src/components/Header/PanelRightToggles.tsx +++ b/src/frontend/src/components/Header/PanelRightToggles.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, ReactElement } from "react"; import { Toolbar, ToggleButton, ToggleButtonProps } from "@fluentui/react-components"; -import eventBus from "../eventbus.js"; +import eventBus from "../common/eventbus.js"; type PanelRightTogglesProps = { children: React.ReactNode; @@ -37,7 +37,7 @@ const PanelRightToggles: React.FC = ({ children }) => { }; return ( - + {React.Children.map(children, (child, index) => { const panelType = panelTypes[index]; // Dynamically assign panelType based on index if (isToggleButton(child) && panelType) { diff --git a/src/frontend/src/components/Panels/PanelLeft.tsx b/src/frontend/src/components/Panels/PanelLeft.tsx index 981b27f9..3b05c43f 100644 --- a/src/frontend/src/components/Panels/PanelLeft.tsx +++ b/src/frontend/src/components/Panels/PanelLeft.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect, ReactNode, ReactElement } from "react"; import PanelToolbar from "./PanelLeftToolbar.js"; // Import to identify toolbar +import "./Panels.scss"; // Import for styling interface PanelLeftProps { panelWidth?: number; @@ -61,12 +62,6 @@ const PanelLeft: React.FC = ({ className="panelLeft" style={{ width: `${width}px`, - display: "flex", - flexDirection: "column", - backgroundColor: "var(--colorNeutralBackground2)", - height: "100%", - boxSizing: "border-box", - position: "relative", borderRight: panelResize ? isHandleHovered ? "2px solid var(--colorNeutralStroke2)" @@ -74,16 +69,10 @@ const PanelLeft: React.FC = ({ : "none", }} > - {toolbar &&
{toolbar}
} + {toolbar &&
{toolbar}
}
{content}
@@ -95,13 +84,6 @@ const PanelLeft: React.FC = ({ onMouseEnter={() => setIsHandleHovered(true)} onMouseLeave={() => setIsHandleHovered(false)} style={{ - position: "absolute", - top: 0, - right: 0, - width: "2px", - height: "100%", - cursor: "ew-resize", - zIndex: 1, backgroundColor: isHandleHovered ? "var(--colorNeutralStroke2)" : "transparent", diff --git a/src/frontend/src/components/Panels/PanelLeftToolbar.tsx b/src/frontend/src/components/Panels/PanelLeftToolbar.tsx index 4886896f..16d77694 100644 --- a/src/frontend/src/components/Panels/PanelLeftToolbar.tsx +++ b/src/frontend/src/components/Panels/PanelLeftToolbar.tsx @@ -1,5 +1,6 @@ import React, { ReactNode } from "react"; import { Body1Strong } from "@fluentui/react-components"; +import "./Panels.scss"; // Import for styling interface PanelLeftToolbarProps { panelIcon?: ReactNode; @@ -15,44 +16,21 @@ const PanelLeftToolbar: React.FC = ({ return (
{(panelIcon || panelTitle) && (
{panelIcon && (
{panelIcon}
)} {panelTitle && ( {panelTitle} @@ -61,11 +39,6 @@ const PanelLeftToolbar: React.FC = ({ )}
{children}
diff --git a/src/frontend/src/components/Panels/PanelRight.tsx b/src/frontend/src/components/Panels/PanelRight.tsx index e98d9162..836509c7 100644 --- a/src/frontend/src/components/Panels/PanelRight.tsx +++ b/src/frontend/src/components/Panels/PanelRight.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect, ReactNode, ReactElement } from "react"; -import eventBus from "../eventbus.js"; +import eventBus from "../common/eventbus.js"; import PanelRightToolbar from "./PanelRightToolbar"; // Import to identify toolbar +import "./Panels.scss" interface PanelRightProps { panelWidth?: number; // Optional width of the panel @@ -82,17 +83,9 @@ const PanelRight: React.FC = ({ return (
= ({ : "none", }} > - {toolbar &&
{toolbar}
} + {toolbar &&
{toolbar}
}
{content}
@@ -120,13 +108,6 @@ const PanelRight: React.FC = ({ onMouseEnter={() => setIsHandleHovered(true)} onMouseLeave={() => setIsHandleHovered(false)} style={{ - position: "absolute", - top: 0, - right: 0, - width: "2px", - height: "100%", - cursor: "ew-resize", - zIndex: 1, backgroundColor: isHandleHovered ? "var(--colorNeutralStroke2)" : "transparent", diff --git a/src/frontend/src/components/Panels/PanelRightToolbar.tsx b/src/frontend/src/components/Panels/PanelRightToolbar.tsx index 8c2b92a6..f86c05ba 100644 --- a/src/frontend/src/components/Panels/PanelRightToolbar.tsx +++ b/src/frontend/src/components/Panels/PanelRightToolbar.tsx @@ -1,6 +1,7 @@ import React, { ReactNode } from "react"; import { Body1Strong, Button } from "@fluentui/react-components"; import { DismissRegular } from '@fluentui/react-icons'; +import "./Panels.scss"; // Import for styling interface PanelRightToolbarProps { panelTitle?: string | null; @@ -20,44 +21,20 @@ const PanelRightToolbar: React.FC = ({ return (
{panelIcon && (
{panelIcon}
)} {panelTitle && ( {panelTitle} @@ -65,11 +42,6 @@ const PanelRightToolbar: React.FC = ({
{children}
*/} + + {loading ? ( +
+ +
+ ) : error ? ( +

{error}

+ ) : batchHistory.length === 0 ? ( +

No batch history available.

+ ) : ( +
+ {batchHistory.map(batch => ( + setHoveredBatchId(batch.batch_id)} + onMouseLeave={() => setHoveredBatchId(null)} + style={{ + backgroundColor: hoveredBatchId === batch.batch_id ? "#e1e1e1" : "transparent", + }} + > +
+ handleBatchNavigation(batch)}> + {(() => { + const date = new Date(batch.created_at); + const userLocale = navigator.language; + const dateFormatter = new Intl.DateTimeFormat(userLocale, { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }); + return dateFormatter.format(date); + })()} ({batch.file_count} {batch.file_count === 1 ? "file" : "files"}) + + {hoveredBatchId === batch.batch_id && batch.status === "completed" ? ( + + + + ) : ( + + {formatToLocaleTime(batch.updated_at + '+00:00')} + + )} +
+
+ ))} +
+ )} + + {/* Confirmation dialog for deleting all batches */} + setShowDeleteAllDialog(false)} + confirmText="Delete All" + cancelText="Cancel" + /> + + {/* Confirmation dialog for deleting a single batch */} + deleteBatchFromHistory(selectedBatchId)} + onCancel={() => setShowDeleteDialog(false)} + confirmText="Delete" + cancelText="Cancel" + /> +
+ ); +}; + +export default HistoryPanel; \ No newline at end of file diff --git a/src/frontend/src/components/bottomBar/bottomBar.scss b/src/frontend/src/components/bottomBar/bottomBar.scss new file mode 100644 index 00000000..e3ae4c54 --- /dev/null +++ b/src/frontend/src/components/bottomBar/bottomBar.scss @@ -0,0 +1,49 @@ +.cardContainer{ + background-color: #fafafa !important; + padding: 1rem; + border-radius: 0; + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + z-index:1000; +} + +.cardContainer2{ + display: flex; + align-items: center; + width: 55%; + justify-content: space-between; + gap: 2rem; +} + +.cardHeader{ + display: flex; + align-items: center; + gap: 2rem; + flex-grow: 1; +} + +.cardLabel{ + display: flex; + align-items: center; + gap: 0.75rem +} + +.width_150{ + width: 150px; +} + +.buttonContainer{ + display: flex; + align-items: center; + gap: 1rem; +} + +.minWidth_120{ + min-width: 120px; +} + +.minWidth_80{ + min-width: 80px; +} \ No newline at end of file diff --git a/src/frontend/src/components/bottomBar/bottomBar.tsx b/src/frontend/src/components/bottomBar/bottomBar.tsx new file mode 100644 index 00000000..73813f55 --- /dev/null +++ b/src/frontend/src/components/bottomBar/bottomBar.tsx @@ -0,0 +1,142 @@ +import { Button, Card, Dropdown, DropdownProps, Option } from "@fluentui/react-components" +import React, { useState } from "react" +import { useNavigate } from "react-router-dom" +import { updateBatchSummary } from "../../store/modernizationSlice" +import { useDispatch } from "react-redux" +import "./bottomBar.scss" + +// Define possible upload states +const UploadState = { + IDLE: "IDLE", + UPLOADING: "UPLOADING", + COMPLETED: "COMPLETED", +} + +type UploadStateType = keyof typeof UploadState + +interface BottomBarProps { + uploadState: UploadStateType + onCancel: () => void + onStartTranslating: () => void + selectedTargetLanguage: string[]; + selectedCurrentLanguage: string[]; + onTargetLanguageChange: (targetLanguage: string[]) => void; + onCurrentLanguageChange: (currentLanguage: string[]) => void; +} + +const BottomBar: React.FC = ({ uploadState = UploadState.IDLE, onCancel, onStartTranslating, selectedTargetLanguage, selectedCurrentLanguage, onTargetLanguageChange, onCurrentLanguageChange }) => { + const dispatch = useDispatch() + const handleCancel = () => { + if (onCancel) { + onCancel() + } + } + + const handleCurrentLanguageChange: DropdownProps["onOptionSelect"] = (ev, data) => { + if (data.optionValue) { + onCurrentLanguageChange([data.optionValue]); + } + }; + + const handleTargetLanguageChange: DropdownProps["onOptionSelect"] = (ev, data) => { + if (data.optionValue) { + onTargetLanguageChange([data.optionValue]); + } + }; + + const handleStartTranslating = () => { + if (uploadState === UploadState.COMPLETED) { + dispatch(updateBatchSummary({ + batch_id: "", + upload_id: "", + date_created: "", + total_files: 0, + completed_files: 0, + error_count: 0, + status: "", + warning_count: 0, + hasFiles: 0, + files: [] as { + file_id: string; + name: string; + status: string; + error_count: number; + warning_count: number; + file_logs: any[]; + content?: string; + translated_content?: string; + }[], + })); + onStartTranslating() + } + } + + return ( +
+ +
+
+
+ + + + +
+
+ + + + + +
+
+
+ + +
+
+
+
+ ) +} + +export default BottomBar; \ No newline at end of file diff --git a/src/frontend/src/components/common/MsftColor.tsx b/src/frontend/src/components/common/MsftColor.tsx new file mode 100644 index 00000000..a9669001 --- /dev/null +++ b/src/frontend/src/components/common/MsftColor.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const MicrosoftLogo: React.FC = () => { + return ( + + + + + + + ); +}; + +export default MicrosoftLogo; \ No newline at end of file diff --git a/src/frontend/src/components/common/bundleIcons.tsx b/src/frontend/src/components/common/bundleIcons.tsx new file mode 100644 index 00000000..4d033d1f --- /dev/null +++ b/src/frontend/src/components/common/bundleIcons.tsx @@ -0,0 +1,88 @@ +import { + ArrowExitRegular, + ArrowExitFilled, + PersonRegular, + PersonFilled, + PersonFeedbackRegular, + PersonFeedbackFilled, + LeafOneRegular, + LeafOneFilled, + FlowRegular, + FlowFilled, + BeakerRegular, + BeakerFilled, + WeatherSunnyRegular, + WeatherSunnyFilled, + WeatherMoonRegular, + WeatherMoonFilled, + PanelLeftContractFilled, + PanelLeftContractRegular, + PanelLeftExpandFilled, + PanelLeftExpandRegular, + PanelRightContractFilled, + PanelRightContractRegular, + PanelRightExpandFilled, + PanelRightExpandRegular, + ShareFilled, + ShareRegular, + SearchFilled, + SearchRegular, + CodeRegular, + CodeFilled, + DesignIdeasFilled, + DesignIdeasRegular, + bundleIcon, + FolderOpenFilled, + FolderOpenRegular, + OpenFilled, + OpenRegular, + TreeDeciduousFilled, + TreeDeciduousRegular, + TreeEvergreenFilled, + TreeEvergreenRegular, + LinkFilled, + LinkRegular, + OrganizationHorizontalFilled, + OrganizationHorizontalRegular +} from "@fluentui/react-icons"; + +export const Link = bundleIcon(LinkFilled, LinkRegular); +export const OrganizationHorizontal = bundleIcon(OrganizationHorizontalFilled, OrganizationHorizontalRegular); +export const TreeDeciduous = bundleIcon(TreeDeciduousFilled, TreeDeciduousRegular); +export const TreeEvergreen = bundleIcon(TreeEvergreenFilled, TreeEvergreenRegular); +export const FolderOpen = bundleIcon(FolderOpenFilled, FolderOpenRegular); +export const Open = bundleIcon(OpenFilled, OpenRegular); +export const ArrowExit = bundleIcon(ArrowExitFilled, ArrowExitRegular); +export const Code = bundleIcon(CodeFilled, CodeRegular); +export const DesignIdeas = bundleIcon(DesignIdeasFilled, DesignIdeasRegular); +export const Person = bundleIcon(PersonFilled, PersonRegular); +export const PersonFeedback = bundleIcon( + PersonFeedbackFilled, + PersonFeedbackRegular +); +export const LeafOne = bundleIcon(LeafOneFilled, LeafOneRegular); +export const Share = bundleIcon(ShareFilled, ShareRegular); +export const Flow = bundleIcon(FlowFilled, FlowRegular); +export const Beaker = bundleIcon(BeakerFilled, BeakerRegular); +export const WeatherSunny = bundleIcon(WeatherSunnyFilled, WeatherSunnyRegular); +export const WeatherMoon = bundleIcon(WeatherMoonFilled, WeatherMoonRegular); +export const PanelLeftContract = bundleIcon( + PanelLeftContractFilled, + PanelLeftContractRegular +); +export const PanelLeftExpand = bundleIcon( + PanelLeftExpandFilled, + PanelLeftExpandRegular +); +export const PanelRightContract = bundleIcon( + PanelRightContractFilled, + PanelRightContractRegular +); +export const PanelRightExpand = bundleIcon( + PanelRightExpandFilled, + PanelRightExpandRegular +); +export const Search = bundleIcon(SearchFilled, SearchRegular); + +//DEV NOTES: +// 1) It's recommended to create bundleIcons for icons that nest within Fluent components and then import them into your project. diff --git a/src/frontend/src/components/common/errorWarningSection.tsx b/src/frontend/src/components/common/errorWarningSection.tsx new file mode 100644 index 00000000..777a6f31 --- /dev/null +++ b/src/frontend/src/components/common/errorWarningSection.tsx @@ -0,0 +1,58 @@ +import * as React from "react"; +import { ErrorWarningProps } from "../../types/types"; + +export const ErrorWarningSection: React.FC = ({ title, count, type, items }) => { + return ( +
+
+
+
+ {title} ({count}) +
+
+
+ +
+
+
+
+
+ {items.map((item, index) => ( +
+
+
+
+ +
+
+
+
+ {item.fileName} ({item.count}) +
+
+
source
+
+
+
+ {item.messages.map((message, msgIndex) => ( +
+
+ +
+
+
+
+ {message.message} +
+
+
{message.location}
+
+
+
+ ))} +
+ ))} +
+
+ ); +}; \ No newline at end of file diff --git a/src/frontend/src/components/common/eventbus.tsx b/src/frontend/src/components/common/eventbus.tsx new file mode 100644 index 00000000..89fe0ff2 --- /dev/null +++ b/src/frontend/src/components/common/eventbus.tsx @@ -0,0 +1,39 @@ +type EventCallback = (...args: any[]) => void; + +class EventBus { + private events: { [key: string]: EventCallback[] } = {}; + private panelWidth: number = 400; // Shared default width for panels + + // Register an event listener + on(event: string, callback: EventCallback) { + if (!this.events[event]) { + this.events[event] = []; + } + this.events[event].push(callback); + } + + // Remove an event listener + off(event: string, callback: EventCallback) { + if (!this.events[event]) return; + this.events[event] = this.events[event].filter((cb) => cb !== callback); + } + + // Emit an event + emit(event: string, ...args: any[]) { + if (!this.events[event]) return; + this.events[event].forEach((callback) => callback(...args)); + } + + // Manage shared panel width + setPanelWidth(width: number) { + this.panelWidth = width; + this.emit("panelWidthChanged", width); + } + + getPanelWidth(): number { + return this.panelWidth; + } +} + +const eventBus = new EventBus(); +export default eventBus; diff --git a/src/frontend/src/components/common/fileItem.tsx b/src/frontend/src/components/common/fileItem.tsx new file mode 100644 index 00000000..b1e69e12 --- /dev/null +++ b/src/frontend/src/components/common/fileItem.tsx @@ -0,0 +1,27 @@ +import * as React from "react"; +import { FileItemProps } from "../../types/types"; + +export const FileItem: React.FC = ({ name, count, type, icon, details }) => { + return ( +
+
+
+ +
+ {name} +
+
+
+ {count !== undefined && ( +
+
+ {count} +
+
+
+
+
+ )} +
+ ); +}; \ No newline at end of file diff --git a/src/frontend/src/components/common/steps.tsx b/src/frontend/src/components/common/steps.tsx new file mode 100644 index 00000000..c8f86c84 --- /dev/null +++ b/src/frontend/src/components/common/steps.tsx @@ -0,0 +1,34 @@ +import * as React from "react"; + +import { StepProps } from "../../types/types"; + +export const Step: React.FC = ({ icon, title, status, isLast }) => { + return ( +
+
+ + {isLast && ( + + )} +
+
+
+ {title} +
+
+ {status} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/frontend/src/components/uploadButton/uploadButton.tsx b/src/frontend/src/components/uploadButton/uploadButton.tsx new file mode 100644 index 00000000..03a82f42 --- /dev/null +++ b/src/frontend/src/components/uploadButton/uploadButton.tsx @@ -0,0 +1,566 @@ +import React, { useCallback, useState, useEffect } from 'react'; +import { useDropzone, FileRejection, DropzoneOptions } from 'react-dropzone'; +import { CircleCheck, X } from 'lucide-react'; +import { + Button, + Toast, + ToastTitle, + useToastController, + Tooltip, +} from "@fluentui/react-components"; +import { MessageBar, MessageBarType } from "@fluentui/react"; +import { deleteBatch, startProcessing } from '../../store/batchSlice'; +import {deleteFileFromBatch, uploadFile} from '../../store/uploadFileSlice'; +import { useDispatch } from 'react-redux'; +import ConfirmationDialog from '../../commonComponents/ConfirmationDialog/confirmationDialogue'; +import { AppDispatch } from '../../store/store' +import { v4 as uuidv4 } from 'uuid'; +import "./uploadStyles.scss"; +import { useNavigate } from "react-router-dom"; + +interface FileUploadZoneProps { + onFileUpload?: (acceptedFiles: File[]) => void; + onFileReject?: (fileRejections: FileRejection[]) => void; + onUploadStateChange?: (state: 'IDLE' | 'UPLOADING' | 'COMPLETED') => void; + maxSize?: number; + acceptedFileTypes?: Record; + selectedCurrentLanguage: string[]; + selectedTargetLanguage: string[]; +} + +interface UploadingFile { + file: File; + progress: number; + status: 'uploading' | 'completed' | 'error'; + id: string; + batchId: string; +} + +const FileUploadZone: React.FC = ({ + onFileUpload, + onFileReject, + onUploadStateChange, + maxSize = 200 * 1024 * 1024, + acceptedFileTypes = { 'application/sql': ['.sql'] }, + selectedCurrentLanguage, + selectedTargetLanguage +}) => { + const [uploadingFiles, setUploadingFiles] = useState([]); + const [uploadIntervals, setUploadIntervals] = useState<{ [key: string]: ReturnType }>({}); + const [showCancelDialog, setShowCancelDialog] = useState(false); + const [showLogoCancelDialog, setShowLogoCancelDialog] = useState(false); + const [uploadState, setUploadState] = useState<'IDLE' | 'UPLOADING' | 'COMPLETED'>('IDLE'); + const [batchId, setBatchId] = useState(uuidv4()); + const [allUploadsComplete, setAllUploadsComplete] = useState(false); + const [fileLimitExceeded, setFileLimitExceeded] = useState(false); + const [showFileLimitDialog, setShowFileLimitDialog] = useState(false); + const navigate = useNavigate(); + + const MAX_FILES = 20; + const dispatch = useDispatch(); + + useEffect(() => { + if (uploadingFiles.length === 0) { + setAllUploadsComplete(false); + } + }); + + useEffect(() => { + let newState: 'IDLE' | 'UPLOADING' | 'COMPLETED' = 'IDLE'; + + if (uploadingFiles.length > 0) { + const activeFiles = uploadingFiles.filter(f => f.status !== 'error'); + if (activeFiles.length > 0 && activeFiles.every(f => f.status === 'completed')) { + newState = 'COMPLETED'; + setAllUploadsComplete(true); + } else { + newState = 'UPLOADING'; + } + } + + setUploadState(newState); + onUploadStateChange?.(newState); + }, [uploadingFiles, onUploadStateChange]); + + const startNewBatch = () => { + setBatchId(uuidv4()); // Generate a new batchId for each new batch of uploads + }; + + const simulateFileUpload = (file: File) => { + if (batchId == "") { + startNewBatch(); // Ensure batchId is set before starting any upload + } + + const frontendFileId = uuidv4(); + const newFile: UploadingFile = { + file, + progress: 0, + status: 'uploading', + id: frontendFileId, + batchId: batchId + }; + + setUploadingFiles(prev => [...prev, newFile]); + + const duration = 6000 + Math.random() * 2000;; + const steps = 50; + const increment = 100 / steps; + const stepDuration = duration / steps; + + let currentProgress = 0; + let hasStartedUpload = false; // To ensure dispatch is called once + const intervalId = setInterval(() => { + currentProgress += increment; + + setUploadingFiles(prev => + prev.map(f => + f.id === frontendFileId + ? { + ...f, + progress: Math.min(currentProgress, 99), + status: 'uploading' + } + : f + ) + ); + + if (currentProgress >= 1 && !hasStartedUpload) { + hasStartedUpload = true; + + dispatch(uploadFile({ batchId, file })) + .unwrap() + .then((response) => { + if (response?.file.file_id) { + // Update the file list with the correct fileId from backend + setUploadingFiles((prev) => + prev.map((f) => + f.id === frontendFileId ? { ...f, id: response.file.file_id, progress: 100, status: 'completed' } : f + ) + ); + } + clearInterval(intervalId); + }) + .catch((error) => { + console.error("Upload failed:", error); + + // Mark the file upload as failed + setUploadingFiles((prev) => + prev.map((f) => + f.id === frontendFileId ? { ...f, status: 'error' } : f + ) + ); + clearInterval(intervalId); + }); + + setUploadIntervals(prev => { + const next = { ...prev }; + delete next[frontendFileId]; + return next; + }); + } + }, stepDuration); + }; + + const onDrop = useCallback( + (acceptedFiles: File[], fileRejections: FileRejection[]) => { + // Check current files count and determine how many more can be added + const remainingSlots = MAX_FILES - uploadingFiles.length; + + if (remainingSlots <= 0) { + // Already at max files, show dialog + setShowFileLimitDialog(true); + return; + } + + // If more files are dropped than slots available + if (acceptedFiles.length > remainingSlots) { + // Take only the first `remainingSlots` files + const filesToUpload = acceptedFiles.slice(0, remainingSlots); + filesToUpload.forEach(file => simulateFileUpload(file)); + + if (onFileUpload) onFileUpload(filesToUpload); + + // Show dialog about exceeding limit + setShowFileLimitDialog(true); + } else { + // Normal case, upload all files + acceptedFiles.forEach(file => simulateFileUpload(file)); + if (onFileUpload) onFileUpload(acceptedFiles); + } + + if (onFileReject && fileRejections.length > 0) { + onFileReject(fileRejections); + } + }, + [onFileUpload, onFileReject, uploadingFiles.length] + ); + + const dropzoneOptions: DropzoneOptions = { + onDrop, + noClick: true, + maxSize, + accept: acceptedFileTypes, + //maxFiles: MAX_FILES, + }; + + const { getRootProps, getInputProps, open } = useDropzone(dropzoneOptions); + + const removeFile = (fileId: string) => { + setUploadingFiles((prev) => { + const updatedFiles = prev.filter((f) => f.id !== fileId); + console.log("Updated uploadingFiles:", updatedFiles); + return updatedFiles; + }); + + // Clear any running upload interval + if (uploadIntervals[fileId]) { + clearInterval(uploadIntervals[fileId]); + setUploadIntervals((prev) => { + const { [fileId]: _, ...rest } = prev; + return rest; + }); + } + + // Backend deletion only if file was uploaded successfully + const fileToRemove = uploadingFiles.find((f) => f.id === fileId); + if (fileToRemove && fileToRemove.status !== "error") { + dispatch(deleteFileFromBatch(fileToRemove.id)) + .unwrap() + .catch((error) => console.error("Failed to delete file:", error)); + } + }; + + const cancelAllUploads = useCallback(() => { + // Clear all upload intervals + dispatch(deleteBatch({ batchId, headers: null })); + + Object.values(uploadIntervals).forEach(interval => clearInterval(interval)); + setUploadIntervals({}); + setUploadingFiles([]); + setUploadState('IDLE'); + onUploadStateChange?.('IDLE'); + setShowCancelDialog(false); + setShowLogoCancelDialog(false); + //setBatchId(); + startNewBatch(); + }, [uploadIntervals, onUploadStateChange]); + + useEffect(() => { + if (typeof window !== 'undefined') { + // Store the original function if it exists + const originalCancelLogoUploads = (window as any).cancelLogoUploads; + + // Override with our new function that shows the dialog + (window as any).cancelLogoUploads = () => { + // Show dialog regardless of upload state + if (uploadingFiles.length > 0) { // Only show if there are files + setShowLogoCancelDialog(true); + } + }; + // Cleanup: Restore original function on unmount + return () => { + (window as any).cancelLogoUploads = originalCancelLogoUploads; + }; + } + }, [uploadingFiles.length]); // Runs when uploadingFiles.length changes + + useEffect(() => { + if (typeof window !== 'undefined') { + // Store the original function if it exists + const originalCancelUploads = (window as any).cancelUploads; + + // Override with our new function that shows the dialog + (window as any).cancelUploads = () => { + // Show dialog regardless of upload state + if (uploadingFiles.length > 0) { // Only show if there are files + setShowCancelDialog(true); + } + }; + // Cleanup + return () => { + (window as any).cancelUploads = originalCancelUploads; + }; + } + }, [uploadingFiles.length]); + + useEffect(() => { + if (typeof window !== 'undefined') { + const originalStartTranslating = (window as any).startTranslating; + + (window as any).startTranslating = async () => { + const payload = { + batchId: batchId, + translateFrom: selectedCurrentLanguage[0], + translateTo: selectedTargetLanguage[0], + }; + + if (uploadingFiles.length > 0) { + // First navigate to loading page before starting processing + navigate(`/batch-process/${batchId}`); + + // Then dispatch the action and wait for it to complete + try { + dispatch(startProcessing(payload)); + return batchId; // Return the batchId after processing completes + } catch (error) { + console.error('Processing failed:', error); + // Still return the batchId even if processing failed + return batchId; + } + } + return null; + }; + + // Cleanup + return () => { + (window as any).startTranslating = originalStartTranslating; + }; + } + }, [uploadingFiles.length, selectedTargetLanguage, selectedCurrentLanguage, batchId, dispatch, navigate]); + + const toasterId = "uploader-toast"; + const { dispatchToast } = useToastController(toasterId); + + useEffect(() => { + if (allUploadsComplete) { + // Show success toast when uploads are complete + dispatchToast( + + + All files uploaded successfully! + + , + { intent: "success" } + ); + } + }, [allUploadsComplete, dispatchToast]); + + // Auto-hide file limit exceeded alert after 5 seconds + useEffect(() => { + if (fileLimitExceeded) { + const timer = setTimeout(() => { + setFileLimitExceeded(false); + }, 5000); + + return () => clearTimeout(timer); + } + }, [fileLimitExceeded]); + + return ( +
+ setShowCancelDialog(false)} + confirmText="Cancel upload" + cancelText="Continue upload" + /> + + setShowLogoCancelDialog(false)} + confirmText="Leave and lose progress" + cancelText="Stay here" + /> + setShowFileLimitDialog(false)} + onCancel={() => setShowFileLimitDialog(false)} + confirmText="OK" + cancelText="" + /> + + {uploadingFiles.length === 0 && ( +
+

+ Modernize your code +

+

+ Modernize your code by updating the language with AI +

+
+ )} + +
+

+ {uploadingFiles.length > 0 + ? `Uploading (${uploadingFiles.filter(f => f.status === 'completed').length}/${uploadingFiles.length})` + : 'Upload files in batch' + } +

+
+ +
0 ? "16px" : "0px", + flexDirection: uploadingFiles.length > 0 ? 'row' : 'column', + justifyContent: uploadingFiles.length > 0 ? 'space-between' : 'center', + height: uploadingFiles.length > 0 ? '80px' : '251px', + }} + > + + + {uploadingFiles.length > 0 ? ( + <> +
+ Upload Icon +
+

+ Drag and drop files here +

+

+ Limit {Math.floor(maxSize / (1024 * 1024))}MB per file • SQL Only • {uploadingFiles.length}/{MAX_FILES} files +

+
+
+ + + ) : ( + <> + Upload Icon +

+ Drag and drop files here +

+

+ or +

+ +

+ Limit {Math.floor(maxSize / (1024 * 1024))}MB per file • SQL Only • {MAX_FILES} files max +

+ + )} +
+ +
+ {allUploadsComplete && ( + +
+ + All valid files uploaded successfully! +
+
+ )} + + {fileLimitExceeded && ( + setFileLimitExceeded(false)} + dismissButtonAriaLabel="Close" + styles={{ + root: { display: "flex", alignItems: "center" }, + }} + > + + Maximum of {MAX_FILES} files allowed. Some files were not uploaded. + + )} +
+ + {uploadingFiles.length > 0 && ( +
+ {uploadingFiles.map((file) => ( +
+
+ 📄 +
+ +
+ {file.file.name} +
+
+
+
+
+ + + +
+ ))} +
+ )} +
+ ); +}; + +export default FileUploadZone; \ No newline at end of file diff --git a/src/frontend/src/components/uploadButton/uploadStyles.scss b/src/frontend/src/components/uploadButton/uploadStyles.scss new file mode 100644 index 00000000..028c63b4 --- /dev/null +++ b/src/frontend/src/components/uploadButton/uploadStyles.scss @@ -0,0 +1,216 @@ +.dismissal-120 .ms-Button-icon { + /* display: inline-block; */ + width: 16px; + height: 16px; + } + + .uploadFile-message-bar .ms-MessageBar-icon { + color: #37a04c !important; + font-size: 14px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + font-weight: bolder !important; + } + + .uploadFile-message-bar .ms-MessageBar-icon svg { + stroke-width: 8.5px; + width: 14px !important; + height: 14px !important; + } + + .containerClass{ + width: 100%; + min-width: 620px; + max-width: 700px; + margin: 0 auto; + margin-top: 0px; + padding: 16px; + padding-bottom: 60px + } + + .uploadContainer{ + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + margin-bottom: 90px; + text-align: center + } + + .title{ + font-size: 24px; + font-weight: bold; + margin: 0px + } + + .label{ + font-size: 16px; + font-weight: 600; + margin: 0px + } + + .informationContainer{ + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px + } + + .infoMessage{ + font-size: 16px; + margin: 0px + } + + .uploadingFilesInfo{ + width: 100%; + border: 2px dashed #ccc; + border-radius: 4px; + background-color: #FAFAFA; + display: flex; + align-items: center; + margin-bottom: 16px; + } + + .fileUploadContainer{ + display: flex; + align-items: center; + gap: 12px + } + + .uploadImage{ + width: 32px; + height: 32px + } + + .uploadInfoMsg{ + margin: 0px; + font-size: 16px; + color: #333 + } + + .uploadLimitInfoMsg{ + margin: 4px 0px 0px 0px; + font-size: 12px; + color: #666 + } + + .uploadLimitInfoMsg2{ + margin: 8px 0px 0px 0px; + font-size: 12px; + color: #666 + } + + .browseBtn{ + min-width: 120px !important; + background-color: white !important; + border: 1px solid grey !important; + border-radius: 4px !important; + height: 32px !important + } + + .browseBtn2{ + min-width: 120px !important; + background-color: white !important; + border: 1px solid grey !important; + border-radius: 4px !important; + } + + .uploadIcon{ + width: 64px; + height: 64px + } + + .dragdropMsg{ + margin: 16px 0px 0px 0px; + font-size: 18px; + color: #333; + font-weight: 600 + } + + .dragdropMsg2{ + margin: 8px 0px; + font-size: 14px; + color: #666 + } + + .messagebarContainer{ + display: flex; + flex-direction: column; + gap: 13px; + width: 737px; + padding-bottom: 10px; + border-radius: 4px + } + + .iconContainer2{ + display: flex; + align-items: left + } + + .mr_8{ + margin-right: 8px + } + + .xBtn{ + margin-right: 12px; + padding-top: 3px + } + + .uploadedFilesContainer{ + display: flex; + flex-direction: column; + gap: 8px; + width: 737px; + max-height: 300px; + overflow-y: auto; + scrollbar-width: thin + } + + .fileMainContainer{ + display: flex; + align-items: center; + gap: 12px; + padding: 8px 12px; + background-color: white; + border-radius: 4px; + border: 1px solid #eee; + } + + .fileImageContainer{ + display: flex; + align-items: center; + width: 24px + } + + .tooltipClass{ + width: 80px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 14px; + align-items: left; + cursor: pointer; + text-align: left; + } + + .statusContainer{ + flex: 1; + height: 4px; + background-color: #eee; + border-radius: 2px; + overflow: hidden + } + + .removeBtn{ + border: none; + background: none; + cursor: pointer; + padding: 4px; + color: #666; + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px + } \ No newline at end of file diff --git a/src/frontend/src/main.jsx b/src/frontend/src/main.jsx index 7ae29774..3f52b4b5 100644 --- a/src/frontend/src/main.jsx +++ b/src/frontend/src/main.jsx @@ -6,8 +6,9 @@ import { FluentProvider, webLightTheme } from '@fluentui/react-components'; import { Provider } from 'react-redux'; import { store } from './store/store'; import AuthProvider from './msal-auth/AuthProvider'; -import { setEnvData, setApiUrl, config as defaultConfig } from './api/config'; +import { setEnvData, setApiUrl, config as defaultConfig } from './utils/config.js'; import { initializeMsalInstance } from './msal-auth/msalInstance'; +import {updateUrl} from './utils/httpUtil.js'; const Main = () => { const [isConfigLoaded, setIsConfigLoaded] = useState(false); @@ -37,6 +38,7 @@ const Main = () => { const instance = config.ENABLE_AUTH ? await initializeMsalInstance(config) : {}; setMsalInstance(instance); setIsConfigLoaded(true); + updateUrl(); } catch (error) { console.error("Error fetching config:", error); } diff --git a/src/frontend/src/pages/batchViewPage/batchView.styles.ts b/src/frontend/src/pages/batchViewPage/batchView.styles.ts new file mode 100644 index 00000000..6b1cb824 --- /dev/null +++ b/src/frontend/src/pages/batchViewPage/batchView.styles.ts @@ -0,0 +1,294 @@ +import { makeStyles, tokens } from "@fluentui/react-components"; + +export const useStyles = makeStyles({ + root: { + display: "flex", + flexDirection: "column", + height: "100vh", + }, + content: { + display: "flex", + flex: 1, + overflow: "hidden", + }, + fileIcon: { + color: tokens.colorNeutralForeground1, + marginRight: "12px", + flexShrink: 0, + fontSize: "20px", + height: "20px", + width: "20px", + }, + statusContainer: { + display: "flex", + alignItems: "center", + gap: "8px", + marginLeft: "auto", + }, + fileName: { + flex: 1, + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + fontWeight: "600", + }, + fileList: { + display: "flex", + flexDirection: "column", + gap: "4px", + padding: "16px", + flex: 1, + overflow: "auto", + paddingBottom: "70px", + }, + panelHeader: { + padding: "16px 20px", + display: "flex", + justifyContent: "space-between", + alignItems: "center", + }, + fileCard: { + backgroundColor: tokens.colorNeutralBackground1, + border: `1px solid ${tokens.colorNeutralStroke1}`, + borderRadius: "4px", + padding: "12px", + display: "flex", + alignItems: "center", + cursor: "pointer", + "&:hover": { + backgroundColor: tokens.colorNeutralBackground3, + border: tokens.colorBrandBackground, + }, + }, + selectedCard: { + border: "var(--NeutralStroke2.Rest)", + backgroundColor: "#EBEBEB", + }, + mainContent: { + flex: 1, + backgroundColor: tokens.colorNeutralBackground1, + }, + codeCard: { + backgroundColor: tokens.colorNeutralBackground1, + boxShadow: tokens.shadow4, + overflow: "hidden", + maxHeight: "87vh", + overflowY: "auto", + transition: "width 0.3s ease-in-out", + }, + codeHeader: { + padding: "12px 16px", + display: "flex", + justifyContent: "space-between", + alignItems: "center", + }, + summaryContent: { + padding: "24px", + width: "100%", // Add this + maxWidth: "100%", // Add this + overflowX: "hidden", // Add this + }, + summaryCard: { + height: "40px", + width: "100%", + maxWidth: "100%", // Add this + padding: "2px", + backgroundColor: "#F2FBF2", + marginBottom: "16px", + marginLeft: "0", // Change from marginleft + marginRight: "0", + boxShadow: "none", + overflowX: "hidden", // Add this + boxSizing: "border-box", // Add this + }, + errorSection: { + backgroundColor: "#F8DADB", + marginBottom: "8px", + height: "40px", + boxShadow: "none" + }, + warningSection: { + backgroundColor: tokens.colorStatusWarningBackground1, + marginBottom: "16px", + boxShadow: "none" + }, + sectionHeader: { + display: "flex", + height: "40px", + alignItems: "center", + justifyContent: "space-between", + cursor: "pointer", + boxSizing: "border-box", + padding: "0", + textAlign: "left" + }, + errorItem: { + marginTop: "16px", + paddingLeft: "20px", + paddingBottom: "16px", + }, + errorTitle: { + display: "flex", + alignItems: "center", + gap: "8px", + marginBottom: "8px", + }, + errorDetails: { + marginTop: "4px", + color: tokens.colorNeutralForeground2, + paddingLeft: "20px", + }, + errorSource: { + color: tokens.colorNeutralForeground2, + fontSize: "12px", + }, + loadingContainer: { + display: "flex", + flexDirection: "column", + justifyContent: "center", + alignItems: "center", + height: "100%", + gap: "16px", + }, + loadingContainer_flex1: { + display: "flex", + flexDirection: "column", + justifyContent: "center", + alignItems: "center", + height: "100%", + gap: "16px", + flex: 1, + }, + buttonContainer: { + display: "flex", + justifyContent: "flex-end", + gap: "8px", + position: "absolute", + bottom: 0, + left: 0, + right: 0, + backgroundColor: tokens.colorNeutralBackground2, + borderTop: "1px solid #e5e7eb", /* Optional: adds a separator line */ + padding: "16px 20px", + zIndex: "10", + }, + downloadButton: { + marginLeft: "auto", + display: "flex", + alignItems: "center", + gap: "4px", + }, + summaryHeader: { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + padding: "12px 24px", + transition: "width 0.3s ease-in-out" + }, + + summaryTitle: { + fontSize: "12px", + }, + + aiGeneratedTag: { + color: "#6b7280", // Replacing theme.palette.text.secondary with a neutral gray + fontSize: "0.875rem", + backgroundColor: "#f3f4f6", // Replacing theme.palette.background.default with a light gray + padding: "4px 8px", // Replacing theme.spacing(0.5, 1) + borderRadius: "4px", // Replacing theme.shape.borderRadius with a standard value + }, + + noContentAvailable: { + fontSize: "20px", + padding: "16px", + color: "inherit", + textAlign: "center", + }, + errorContent: { + backgroundColor: "#F8DADB", + marginBottom: "16px", + boxShadow: "none" + }, + warningContent: { + backgroundColor: tokens.colorStatusWarningBackground1, + marginBottom: "16px", + paddingBottom: "22px", + paddingTop: "8px", + boxShadow: "none" + }, + p_8:{ + padding: "8px" + }, + p_20:{ + padding: "20px" + }, + spinnerContainer:{ + padding: "20px", + textAlign: "center" + }, + aiInfoText:{ + color: tokens.colorNeutralForeground3, + paddingRight: "20px", + }, + errorText:{ + color: tokens.colorStatusDangerForeground1, + }, + errorIcon:{ + color: tokens.colorStatusDangerForeground1, + width: "16px", + height: "16px" + }, + warningIcon:{ + color: "#B89500", + width: "16px", + height: "16px" + }, + successIcon:{ + color: "#0B6A0B", + width: "16px", + height: "16px" + }, + fileContent:{ + textAlign: 'center', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + marginTop: '60px', + height: '70vh', + width: "100%", + maxWidth: "800px", + margin: "auto", + transition: "width 0.3s ease-in-out", + }, + checkMark:{ + width: '100px', + height: '100px', + marginBottom: '24px' + }, + mb_16:{ + marginBottom: "16px" + }, + mb_24:{ + marginBottom: "24px" + }, + mt_16:{ + marginTop: "16px" + }, + cursorPointer:{ + cursor: "pointer", + }, + flex_1:{ + flex: 1 + }, + panelRight:{ + position: "fixed", + top: "60px", // Adjust based on your header height + right: 0, + height: "calc(100vh - 60px)", // Ensure it does not cover the header + width: "300px", // Set an appropriate width + zIndex: 1050, + background: "white", + overflowY: "auto", + } +}); \ No newline at end of file diff --git a/src/frontend/src/pages/batchViewPage/batchView.tsx b/src/frontend/src/pages/batchViewPage/batchView.tsx new file mode 100644 index 00000000..76110c53 --- /dev/null +++ b/src/frontend/src/pages/batchViewPage/batchView.tsx @@ -0,0 +1,562 @@ +import * as React from "react"; +import { useParams } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; +import { useState, useEffect } from "react"; +import Content from "../../components/Content/Content"; +import Header from "../../components/Header/Header"; +import HeaderTools from "../../components/Header/HeaderTools"; +import PanelLeft from "../../components/Panels/PanelLeft"; +import { + Button, + Text, + Card, + tokens, + Spinner, + Tooltip, +} from "@fluentui/react-components"; +import { + DismissCircle24Regular, + CheckmarkCircle24Regular, + DocumentRegular, + ArrowDownload24Regular, + bundleIcon, + HistoryFilled, + HistoryRegular, + Warning24Regular, +} from "@fluentui/react-icons"; +import { Light as SyntaxHighlighter } from "react-syntax-highlighter"; +import sql from "react-syntax-highlighter/dist/esm/languages/hljs/sql"; +import { vs } from "react-syntax-highlighter/dist/esm/styles/hljs"; +import PanelRightToggles from "../../components/Header/PanelRightToggles"; +import PanelRight from "../../components/Panels/PanelRight"; +import PanelRightToolbar from "../../components/Panels/PanelRightToolbar"; +import BatchHistoryPanel from "../../components/batchHistoryPanel/batchHistoryPanel"; +import ConfirmationDialog from "../../commonComponents/ConfirmationDialog/confirmationDialogue"; +import { + determineFileStatus, + filesLogsBuilder, + filesErrorCounter, + completedFiles, + hasFiles, + fileErrorCounter, + fileWarningCounter, +} from "../../utils/utils"; +import { useStyles } from "./batchView.styles"; +export const History = bundleIcon(HistoryFilled, HistoryRegular); +import { format } from "sql-formatter"; +import { FileItem, BatchSummary } from "../../types/types"; +import { FileError } from "../../components/../commonComponents/fileError/fileError"; +import ErrorComponent from "../../commonComponents/errorsComponent/errorComponent"; +import { useDispatch, useSelector } from "react-redux"; +import { AppDispatch, RootState } from "../../store/store"; +import { + fetchBatchSummary, + fetchFileFromAPI, + handleDownloadZip, +} from "../../store/modernizationSlice"; + +SyntaxHighlighter.registerLanguage("sql", sql); + +const BatchStoryPage = () => { + const { batchId } = useParams<{ batchId: string }>(); + const navigate = useNavigate(); + const dispatch = useDispatch(); + const [showLeaveDialog, setShowLeaveDialog] = useState(false); + const styles = useStyles(); + const [batchTitle, setBatchTitle] = useState(""); + const [loading, setLoading] = useState(true); + const [fileLoading, setFileLoading] = useState(false); + const [error, setError] = useState(null); + const [dataLoaded, setDataLoaded] = useState(false); + const [uploadId, setUploadId] = useState(""); + const isPanelOpen = useSelector( + (state: RootState) => state.historyPanel.isOpen + ); + + // Files state with a summary file + const [files, setFiles] = useState([]); + + const [selectedFileId, setSelectedFileId] = useState(""); + const [expandedSections, setExpandedSections] = useState(["errors"]); + const [localBatchSummary, setLocalBatchSummary] = + useState(null); + const [selectedFileContent, setSelectedFileContent] = useState(""); + const [selectedFileTranslatedContent, setSelectedFileTranslatedContent] = + useState(""); + const { fileContent, batchSummary } = useSelector( + (state: RootState) => state.modernizationReducer + ); + + // Fetch batch data from API + useEffect(() => { + if (!batchId || !(batchId.length === 36)) { + setError("Invalid batch ID provided"); + setLoading(false); + return; + } + setLoading(true); + setDataLoaded(false); + dispatch(fetchBatchSummary(batchId)); // Fetch batch summary from Redux store + }, [batchId]); + + useEffect(() => { + if(batchSummary && batchSummary.batch_id !=='' && batchSummary.upload_id !==''){ + setLocalBatchSummary(batchSummary); + setUploadId(batchSummary.upload_id); + + // Set batch title with date and file count + const formattedDate = new Date( + batchSummary.date_created + ).toLocaleDateString(); + setBatchTitle( + `${formattedDate} (${batchSummary.total_files} file${ + batchSummary.total_files === 1 ? "" : "s" + })` + ); + // Create file list from API response + const fileItems: FileItem[] = batchSummary.files.map((file) => ({ + id: file.file_id, + name: file.name, // This is now the original_name from API + type: "code", + status: determineFileStatus(file), + code: "", // Don't store content here, will fetch on demand + translatedCode: "", // Don't store content here, will fetch on demand + errorCount: file.error_count, + file_logs: file.file_logs, + warningCount: file.warning_count, + })); + + // Add summary file + const updatedFiles: FileItem[] = [ + { + id: "summary", + name: "Summary", + type: "summary", + status: "completed", + errorCount: batchSummary.error_count, + warningCount: batchSummary.warning_count, + file_logs: [], + }, + ...fileItems, + ]; + + setFiles(updatedFiles as FileItem[]); + setSelectedFileId("summary"); // Default to summary view + setDataLoaded(true); + setLoading(false); + } + + }, [batchSummary]); + + // Fetch file content when a file is selected + useEffect(() => { + if (selectedFileId === "summary" || !selectedFileId || fileLoading) { + return; + } + setFileLoading(true); + dispatch(fetchFileFromAPI(selectedFileId)); + }, [selectedFileId]); + + useEffect(() => { + if (fileContent) { + setSelectedFileContent(fileContent.content || ""); + setSelectedFileTranslatedContent(fileContent.translated_content || ""); + } + + setFileLoading(false); + }, [fileContent]); + + const renderWarningContent = () => { + if (!expandedSections.includes("warnings")) return null; + + if (!localBatchSummary) return null; + + // Group warnings by file + const warningFiles = files.filter( + (file) => + file.warningCount && file.warningCount > 0 && file.id !== "summary" + ); + + if (warningFiles.length === 0) { + return ( +
+ No warnings found. +
+ ); + } + + return ( +
+ {warningFiles.map((file, fileIndex) => ( +
+
+ + {file.name} ({file.warningCount}) + + source +
+
+ Warning in file processing. See file for details. +
+
+ ))} +
+ ); + }; + + const renderContent = () => { + // Define header content based on selected file + const renderHeader = () => { + const selectedFile = files.find((f) => f.id === selectedFileId); + + if (!selectedFile) return null; + + const title = selectedFile.id === "summary" ? "Summary" : "T-SQL"; + + return ( +
+ + {title} + + + AI-generated content may be incorrect + +
+ ); + }; + + if (loading) { + return ( + <> + {renderHeader()} +
+ + Loading batch data... +
+ + ); + } + + if (error) { + return ( + <> + {renderHeader()} +
+ + Error: {error} + + +
+ + ); + } + + if (!dataLoaded || !localBatchSummary) { + return ( + <> + {renderHeader()} +
+ No data available + +
+ + ); + } + + const selectedFile = files.find((f) => f.id === selectedFileId); + if (!selectedFile) { + return ( + <> + {renderHeader()} +
+ No file selected +
+ + ); + } + + // If a specific file is selected (not summary), show the file content + if (selectedFile.id !== "summary") { + return ( + <> + {renderHeader()} + +
+ + {selectedFile.name}{" "} + {selectedFileTranslatedContent ? "(Translated)" : ""} + +
+ {fileLoading ? ( +
+ + Loading file content... +
+ ) : ( + <> + {!selectedFile.errorCount && selectedFile.warningCount ? ( + <> + + + File processed with warnings + + + + + + + ) : null} + {selectedFileTranslatedContent ? ( + + {format(selectedFileTranslatedContent, { + language: "tsql", + })} + + ) : ( + <> + + Unable to process the file + + + + + + )} + + )} +
+ + ); + } + + // Show the summary page when summary is selected + if (selectedFile.id === "summary" && localBatchSummary) { + // Check if there are no errors and all files are processed successfully + const noErrors = localBatchSummary.error_count === 0; + const allFilesProcessed = + localBatchSummary.completed_files === localBatchSummary.total_files; + if (noErrors && allFilesProcessed) { + // Show the success message UI with the green banner and checkmark + return ( + <> + {renderHeader()} +
+ {/* Green success banner */} + +
+ + {localBatchSummary.total_files}{" "} + {localBatchSummary.total_files === 1 ? "file" : "files"}{" "} + processed successfully + +
+
+ + {/* Success checkmark and message */} +
+ Success checkmark + + No errors! Your files are ready to download. + + + Your code has been successfully translated with no errors. All + files are now ready for download. Click 'Download' to save + them to your local drive. + +
+
+ + ); + } + + // Otherwise show the regular summary view with errors/warnings + return ( + <> + {renderHeader()} +
+ {/* Only show success card if at least one file was successfully completed */} + {localBatchSummary.completed_files > 0 && ( + +
+ + {localBatchSummary.completed_files}{" "} + {localBatchSummary.completed_files === 1 ? "file" : "files"}{" "} + processed successfully + +
+
+ )} + + {/* Add margin/spacing between cards */} +
+ +
+
+ + ); + } + + return null; + }; + + const handleLeave = () => { + setShowLeaveDialog(false); + navigate("/"); + }; + const downloadZip = async () => { + if (batchId) { + dispatch(handleDownloadZip(batchId)); + } + }; + + if (!dataLoaded && loading) { + return ( +
+
+ + Loading batch data... +
+
+ ); + } + + return ( +
+
+ +
+ {batchTitle} +
+ +
+ {files.map((file) => ( +
setSelectedFileId(file.id)} + > + {file.id === "summary" ? ( + // If you have a custom icon, use it here + Summary icon + ) : ( + + )} + {file.name} +
+ {file.id === "summary" && file.errorCount ? ( + <> + + {file.errorCount}{" "} + {file.errorCount === 1 ? "error" : "errors"} + + + ) : file.status?.toLowerCase() === "error" ? ( + <> + {file.errorCount} + + + ) : file.id !== "summary" && + file.status === "completed" && + file.warningCount ? ( + <> + {file.warningCount} + + + ) : file.status?.toLowerCase() === "completed" ? ( + + ) : // No icon for other statuses + null} +
+
+ ))} +
+ +
+ + + +
+
+ +
{renderContent()}
+
+
+ setShowLeaveDialog(false)} + confirmText="Return to home and lose progress" + cancelText="Stay here" + /> +
+ ); +}; + +export default BatchStoryPage; diff --git a/src/frontend/src/pages/landingPage/landingPage.scss b/src/frontend/src/pages/landingPage/landingPage.scss new file mode 100644 index 00000000..a5f2493a --- /dev/null +++ b/src/frontend/src/pages/landingPage/landingPage.scss @@ -0,0 +1,79 @@ +main { + padding-top: 8rem !important; +} +.main-content { + transition: margin-right 0.3s ease-in-out; /* Smooth transition */ + margin-right: 0px; /* Default margin */ +} + +.main-content.shifted { + margin-right: 300px; /* Adjust to panel width */ +} + +/* html, body { + overflow: auto; + scrollbar-width: none; +} */ + +html, body { +overflow: auto; +scrollbar-width: none; /* Ensures scrollbar is visible */ +} + +/* For WebKit-based browsers (Chrome, Safari) */ +html::-webkit-scrollbar { +width: 8px; /* Adjust width as needed */ +} + +html::-webkit-scrollbar-thumb { +background: #888; /* Scrollbar color */ +border-radius: 4px; +} + +html::-webkit-scrollbar-track { +background: #f1f1f1; /* Track color */ +} + +.main-content { + // overflow: hidden; + // overflow-y: auto; + flex: 1; + /* //margin-top: 60px; Same as header height */ + margin-bottom: 50px; /* Adjust based on bottom bar height */ + scrollbar-width: none; +} + +.pointerCursor { + cursor: pointer; +} + +.zI-950{ + z-index:950; + background:'red' +} + + + +// .landing-page p { +// font-size: 1.2rem; +// font-weight: bold; +// } + +.landing-page main { + //padding-top: 16rem; /* Adjust the value as needed */ +} + +.landing-page p { + //margin-bottom: 8rem; /* Space between text and upload button */ +} + +.panelRight{ + position: "fixed"; + top:'60px'; + right: 0; + height: "calc(100vh - 60px)"; + width: "300px"; + z-index: 1050; + background: "white"; + overflow-y: "auto"; +} \ No newline at end of file diff --git a/src/frontend/src/pages/landingPage/landingPage.tsx b/src/frontend/src/pages/landingPage/landingPage.tsx new file mode 100644 index 00000000..f7fbf634 --- /dev/null +++ b/src/frontend/src/pages/landingPage/landingPage.tsx @@ -0,0 +1,126 @@ +import React, { useEffect, useRef, useState } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { RootState } from "../../store/store"; +declare global { + interface Window { + cancelUploads?: () => void; + cancelLogoUploads?: () => void; + startTranslating?: () => Promise; + } +} +import Content from "../../components/Content/Content"; +import "./landingPage.scss"; + +import UploadButton from "../../components/uploadButton/uploadButton"; +import BottomBar from "../../components/bottomBar/bottomBar"; +import { useNavigate } from "react-router-dom"; +import { resetState } from "../../store/uploadFileSlice"; + +export const LandingPage = (): JSX.Element => { + const dispatch = useDispatch(); // Add dispatch hook + const [selectedTargetLanguage, setSelectedTargetLanguage] = useState< + string[] + >(["T-SQL"]); + const [selectedCurrentLanguage, setSelectedCurrentLanguage] = useState< + string[] + >(["Informix"]); + const batchHistoryRef = useRef<{ triggerDeleteAll: () => void } | null>(null); + const isPanelOpen = useSelector( + (state: RootState) => state.historyPanel.isOpen + ); + const navigate = useNavigate(); + + useEffect(() => { + dispatch(resetState()); + }, [dispatch]); + + const [uploadState, setUploadState] = useState< + "IDLE" | "UPLOADING" | "COMPLETED" + >("IDLE"); + + const handleUploadStateChange = (state) => { + setUploadState(state); + }; + + const handleCancelUploads = () => { + // This function will be called from BottomBar + if (window.cancelUploads) { + window.cancelUploads(); + } + setUploadState("IDLE"); + }; + + const handleStartTranslating = async () => { + try { + if (window.startTranslating) { + // Get the batchId from startTranslating first + const resultBatchId = await window.startTranslating(); + + if (resultBatchId) { + // Once processing is complete, navigate to the modern page + navigate(`/batch-process/${resultBatchId}`); + } else { + // If no batchId returned, just go to modern + navigate("/batch-process"); + } + } else { + // If startTranslating is not available, just navigate to modern + navigate("/batch-process"); + } + } catch (error) { + console.error("Error in handleStartTranslating:", error); + navigate("/batch-process"); + } + }; + + const handleCurrentLanguageChange = (currentlanguage: string[]) => { + setSelectedCurrentLanguage(currentlanguage); + }; + + const handleTargetLanguageChange = (targetLanguage: string[]) => { + setSelectedTargetLanguage(targetLanguage); + }; + + return ( +
+ {/* Main Content */} +
+
+ {/*
+

+ Modernize your code +

+

+ Modernize your code by updating the language with AI +

+
*/} + + +
+ +
+
+
+
+ +
+ ); +}; + +export default LandingPage; diff --git a/src/frontend/src/pages/modernizationPage/modernizationPage.styles.ts b/src/frontend/src/pages/modernizationPage/modernizationPage.styles.ts new file mode 100644 index 00000000..94e41e77 --- /dev/null +++ b/src/frontend/src/pages/modernizationPage/modernizationPage.styles.ts @@ -0,0 +1,382 @@ +import { makeStyles, tokens } from "@fluentui/react-components"; + +export const useStyles = makeStyles({ + root: { + display: "flex", + flexDirection: "column", + height: "100vh", + // backgroundColor: tokens.colorNeutralBackground2, + }, + content: { + display: "flex", + flex: 1, + overflow: "hidden", + }, + fileIcon: { + color: tokens.colorNeutralForeground1, + marginRight: "12px", + flexShrink: 0, + fontSize: "20px", + height: "20px", + width: "20px", + }, + statusContainer: { + display: "flex", + alignItems: "center", + gap: "8px", + marginLeft: "auto", + }, + fileName: { + flex: 1, + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + fontWeight: "600", + }, + fileList: { + display: "flex", + flexDirection: "column", + gap: "4px", + padding: "16px", + flex: 1, + overflow: "auto", + }, + panelHeader: { + padding: "16px 20px", + display: "flex", + justifyContent: "space-between", + alignItems: "center", + }, + fileCard: { + backgroundColor: tokens.colorNeutralBackground1, + border: `1px solid ${tokens.colorNeutralStroke1}`, + borderRadius: "4px", + padding: "12px", + display: "flex", + alignItems: "center", + cursor: "pointer", + "&:hover": { + backgroundColor: tokens.colorNeutralBackground3, + border: tokens.colorBrandBackground, + }, + }, + selectedCard: { + border: "var(--NeutralStroke2.Rest)", + backgroundColor: "#EBEBEB", + }, + progressFill: { + height: "100%", + backgroundColor: "#2563EB", + transition: "width 0.3s ease", + }, + imageContainer: { + display: "flex", + justifyContent: "center", + marginTop: "24px", + marginBottom: "24px", + }, + stepList: { + marginTop: "48px", + }, + step: { + fontSize: "16px", // Increase font size + fontWeight: "400", // Make text bold (optional) + marginBottom: "48px", // Add spacing between steps + }, + codeCard: { + backgroundColor: tokens.colorNeutralBackground1, + boxShadow: tokens.shadow4, + overflow: "hidden", + maxHeight: "87vh", + overflowY: "auto", + }, + codeHeader: { + padding: "12px 16px", + }, + summaryContent: { + padding: "24px", + }, + summaryCard: { + backgroundColor: "#F2FBF2", + marginBottom: "16px", + boxShadow: "none" + }, + errorContent: { + backgroundColor: "#F8DADB", + marginBottom: "16px", + boxShadow: "none" + }, + errorSection: { + backgroundColor: "#F8DADB", + marginBottom: "8px", + boxShadow: "none" + }, + warningSection: { + backgroundColor: tokens.colorStatusWarningBackground1, + marginBottom: "16px", + boxShadow: "none" + }, + warningContent: { + backgroundColor: tokens.colorStatusWarningBackground1, + marginBottom: "16px", + paddingBottom: "22px", + paddingTop: "8px", + boxShadow: "none" + }, + sectionHeader: { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + cursor: "pointer", + }, + errorItem: { + marginTop: "16px", + paddingLeft: "20px", + }, + errorTitle: { + display: "flex", + alignItems: "center", + gap: "8px", + marginBottom: "8px", + }, + errorDetails: { + marginTop: "4px", + color: tokens.colorNeutralForeground2, + paddingLeft: "20px", + }, + errorSource: { + color: tokens.colorNeutralForeground2, + fontSize: "12px", + }, + // Styles for the loading overlay + loadingOverlay: { + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: tokens.colorNeutralBackground1, + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + zIndex: 1000, + }, + loadingCard: { + width: "100%", + maxWidth: "500px", + padding: "32px", + textAlign: "center", + boxShadow: tokens.shadow16, + borderRadius: "8px", + }, + loadingProgressBar: { + width: "100%", + height: "8px", + backgroundColor: tokens.colorNeutralBackground3, + borderRadius: "4px", + marginTop: "24px", + marginBottom: "8px", + overflow: "hidden", + }, + loadingProgressFill: { + height: "100%", + backgroundColor: tokens.colorBrandBackground, + transition: "width 0.5s ease-out", + }, + mainContent: { + flex: 1, + top: "60", + backgroundColor: "white", // Change from tokens.colorNeutralBackground1 to white + overflow: "auto", + }, + progressSection: { + maxWidth: "800px", + margin: "20px auto 0", // Add top margin to move it lower in the page + display: "flex", + flexDirection: "column", + paddingTop: "20px", // Add padding at the top + }, + progressBar: { + width: "100%", + height: "4px", + backgroundColor: "#E5E7EB", + borderRadius: "2px", + marginTop: "32px", + marginBottom: "16px", + overflow: "hidden", + }, + buttonContainer: { + padding: "16px", + display: "flex", + justifyContent: "flex-end", + gap: "8px", + }, + summaryHeader: { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + padding: "12px 24px", // Replacing theme.spacing(2) with a fixed value + }, + summaryTitle: { + fontSize: "12px", + }, + aiGeneratedTag: { + color: "#6b7280", // Replacing theme.palette.text.secondary with a neutral gray + fontSize: "0.875rem", + backgroundColor: "#f3f4f6", // Replacing theme.palette.background.default with a light gray + padding: "4px 8px", // Replacing theme.spacing(0.5, 1) + borderRadius: "4px", // Replacing theme.shape.borderRadius with a standard value + }, + queuedFile: { + borderRadius: "4px", + backgroundColor: "var(--NeutralBackgroundInvertedDisabled-Rest)", // Correct background color + opacity: 0.5, // Disabled effect + pointerEvents: "none", // Prevents clicks + }, + summaryDisabled: { + borderRadius: "4px", + backgroundColor: "var(--NeutralBackgroundInvertedDisabled-Rest)", // Correct background color + opacity: 0.5, // Disabled effect + pointerEvents: "none", // Prevents clicks + }, + inProgressFile: { + borderRadius: "4px", + backgroundColor: "var(--NeutralBackground1.Rest)", // Correct background color + opacity: 0.5, // Disabled effect + }, + completedFile: { + borderRadius: "4px", + backgroundColor: "var(--NeutralBackground1-Rest)", // Correct background color + }, + downloadButton: { + marginLeft: "auto", + display: "flex", + alignItems: "center", + gap: "4px", + }, + errorBanner: { + backgroundColor: "#F8DADB", + marginBottom: "16px", + boxShadow: "none" + }, + fixedButtonContainer: { + position: "absolute", + bottom: 0, + left: 0, + right: 0, /* Match your panel background color */ + backgroundColor: tokens.colorNeutralBackground2, + borderTop: "1px solid #e5e7eb", /* Optional: adds a separator line */ + padding: "0px 16px", + zIndex: "10", + }, + panelContainer: { + display: "flex", + flexDirection: "column", + height: "100%", + position: "relative", + }, + fileListContainer: { + flex: 1, + overflowY: "auto", + paddingBottom: "60px", /* Add padding to prevent content from being hidden behind the fixed buttons */ + }, + textColor:{ + color: tokens.colorNeutralForeground3, + }, + loadingContainer:{ + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + height: "50vh", + }, + gettingRedayInfo:{ + marginTop: "16px", + fontSize: "24px", + fontWeight: "600" + }, + p_20:{ + padding: "20px", + }, + spinnerCon:{ + padding: "20px", + textAlign: "center" + }, + progressText:{ + marginBottom: "20px", + marginTop: "40px" + }, + percentageTextContainer:{ + display: "flex", + justifyContent: "flex-end" + }, + percentageText:{ + fontWeight: "bold", + color: "#333" + }, + progressIcon:{ + width: "160px", + height: "160px" + }, + fileLog:{ + display: "flex", + alignItems: "center" + }, + fileLogText1:{ + fontSize: "16px", + marginRight: "4px", + alignSelf: "flex-start", + }, + fileLogText2:{ + fontSize: "16px", + color: "#333", + marginLeft: "4px", + }, + loadingText:{ + padding: "20px", + textAlign: "center" + }, + successContainer:{ + textAlign: "center", + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + marginTop: "60px", + height: "50vh", + + img:{ + width: "100px", + height: "100px", + marginBottom: "24px", + } + }, + mb_24:{ + marginBottom: "24px" + }, + mb_16:{ + marginBottom: "16px" + }, + checkMarkIcon:{ + color: "#0B6A0B", + width: "16px", + height: "16px", + }, + dismissIcon:{ + color: "#BE1100", + width: "16px", + height: "16px", + }, + warningIcon:{ + color: "#B89500", + width: "16px", + height: "16px", + }, + completedIcon:{ + color: "#0B6A0B", + width: "16px", + height: "16px", + } + +}); \ No newline at end of file diff --git a/src/frontend/src/pages/modernizationPage/modernizationPage.tsx b/src/frontend/src/pages/modernizationPage/modernizationPage.tsx new file mode 100644 index 00000000..ca14c5ec --- /dev/null +++ b/src/frontend/src/pages/modernizationPage/modernizationPage.tsx @@ -0,0 +1,1058 @@ +import * as React from "react"; +import Content from "../../components/Content/Content"; +import PanelLeft from "../../components/Panels/PanelLeft"; +import webSocketService from "../../utils/WebSocketService"; +import { useStyles } from "./modernizationPage.styles"; +import { + Button, + Text, + Card, + tokens, + Spinner, +} from "@fluentui/react-components"; +import { + DismissCircle24Regular, + Warning24Regular, + CheckmarkCircle24Regular, + DocumentRegular, + ChevronDown16Filled, + ChevronRight16Regular, + HistoryFilled, + bundleIcon, + HistoryRegular, + ArrowSyncRegular, + ArrowDownload24Regular, +} from "@fluentui/react-icons"; +import { Light as SyntaxHighlighter } from "react-syntax-highlighter"; +import { vs } from "react-syntax-highlighter/dist/esm/styles/hljs"; +import sql from "react-syntax-highlighter/dist/cjs/languages/hljs/sql"; +import { useNavigate, useParams } from "react-router-dom"; +import { useState, useEffect, useCallback, useRef } from "react"; +import { + filesLogsBuilder, + fileErrorCounter, + formatAgent, + formatDescription, + fileWarningCounter, +} from "../../utils/utils"; +import { format } from "sql-formatter"; +import { FileItem, WebSocketMessage } from "../../types/types"; +import { FileError } from "../../components/../commonComponents/fileError/fileError"; +import ErrorComponent from "../../commonComponents/errorsComponent/errorComponent"; +import { Agents, ProcessingStage } from "../../utils/constants"; +import { useDispatch, useSelector } from "react-redux"; +import { AppDispatch, RootState } from "../../store/store"; +import { + fetchBatchSummary, + fetchFileFromAPI, + handleDownloadZip, +} from "../../store/modernizationSlice"; + +export const History = bundleIcon(HistoryFilled, HistoryRegular); + +SyntaxHighlighter.registerLanguage("sql", sql); + +const getTrackPercentage = ( + status: string, + fileTrackLog: WebSocketMessage[] +): number => { + switch (status?.toLowerCase()) { + case "completed": + return ProcessingStage.Completed; + case "in_process": + if (fileTrackLog && fileTrackLog.length > 0) { + if (fileTrackLog.some((entry) => entry.agent_type === Agents.Checker)) { + return ProcessingStage.FinalChecks; + } else if ( + fileTrackLog.some((entry) => entry.agent_type === Agents.Picker) + ) { + return ProcessingStage.Processing; + } else if ( + fileTrackLog.some((entry) => entry.agent_type === Agents.Migrator) + ) { + return ProcessingStage.Parsing; + } + return ProcessingStage.Starting; + } + return ProcessingStage.Queued; + case "ready_to_process": + return ProcessingStage.Queued; + default: + return ProcessingStage.NotStarted; + } +}; + +const getPrintFileStatus = (status: string): string => { + switch (status) { + case "completed": + return "Completed"; + case "in_process": + return "Processing"; + case "Processing": + return "Pending"; + case "Pending": + return "Pending"; + default: + return "Queued"; + } +}; + +const ModernizationPage = () => { + const { batchId } = useParams<{ batchId: string }>(); + const navigate = useNavigate(); + const dispatch = useDispatch(); + const [selectedFilebg, setSelectedFile] = useState(null); + + const handleClick = (file: string) => { + setSelectedFile(file === selectedFilebg ? null : file); + }; + + //const [batchSummary, setBatchSummary] = useState(null); + const styles = useStyles(); + const [text, setText] = useState(""); + const isPanelOpen = useSelector( + (state: RootState) => state.historyPanel.isOpen + ); + + // Get batchId and fileList from Redux + const [reduxFileList, setReduxFileList] = useState([]); + + // State for the loading component + const [showLoading, setShowLoading] = useState(true); + const [loadingError, setLoadingError] = useState(null); + + const [selectedFileId, setSelectedFileId] = React.useState(""); + const [fileId, setFileId] = React.useState(""); + const [expandedSections, setExpandedSections] = useState(["errors"]); + const [progressPercentage, setProgressPercentage] = useState(0); + const [allFilesCompleted, setAllFilesCompleted] = useState(false); + const [isZipButtonDisabled, setIsZipButtonDisabled] = useState(true); + const [fileLoading, setFileLoading] = useState(false); + const [selectedFileTranslatedContent, setSelectedFileTranslatedContent] = + useState(""); + + const { fileContent, batchSummary } = useSelector( + (state: RootState) => state.modernizationReducer + ); + + const fetchBatchData = async (batchId) => { + try { + setShowLoading(true); + dispatch(fetchBatchSummary(batchId)); + } catch (err) { + console.error("Error fetching batch data:", err); + setLoadingError( + err instanceof Error ? err.message : "An unknown error occurred" + ); + setShowLoading(false); + } + }; + + useEffect(() => { + if (batchSummary && batchSummary.batch_id !=='' && batchSummary.upload_id !=='') { + const batchCompleted = + batchSummary?.status?.toLowerCase() === "completed" || + batchSummary?.status === "failed"; + if (batchCompleted) { + setAllFilesCompleted(true); + if (batchSummary?.hasFiles > 0) { + setIsZipButtonDisabled(false); + } + } + // Transform the server response to an array of your FileItem objects + const fileItems: FileItem[] = batchSummary?.files.map( + (file: any, index: number) => ({ + id: `file${index}`, + name: file.name, + type: "code", + status: file.status?.toLowerCase(), + file_result: file.file_result, + errorCount: + file.status.toLowerCase() === "completed" ? file.error_count : 0, + warningCount: file.warning_count || 0, + code: "", + translatedCode: file.translated_content || "", + file_logs: file.file_logs, + fileId: file.file_id, + batchId: file.batch_id, + }) + ); + const updatedFiles: FileItem[] = [ + { + id: "summary", + name: "Summary", + type: "summary", + status: + batchSummary?.status?.toLowerCase() === "in_process" + ? "Pending" + : batchSummary?.status, + errorCount: batchCompleted ? batchSummary?.error_count : 0, + file_track_percentage: 0, + warningCount: 0, + }, + ...fileItems, + ]; + + // Store it in local state, not Redux + setReduxFileList(updatedFiles); + } else { + setLoadingError("No data received from server"); + } + setShowLoading(false); + }, [batchSummary]); + + useEffect(() => { + if (!batchId || batchId.length !== 36) { + setLoadingError("No valid batch ID provided"); + setShowLoading(false); + return; + } + + fetchBatchData(batchId); + }, [batchId]); + + const downloadZip = async () => { + if (batchId) { + dispatch(handleDownloadZip(batchId)); + } + }; + + // Initialize files state with a summary file + const [files, setFiles] = useState([ + { + id: "summary", + name: "Summary", + type: "summary", + status: "Pending", + errorCount: 0, + warningCount: 0, + file_track_percentage: 0, + }, + ]); + + useEffect(() => { + // This handles the browser's refresh button and keyboard shortcuts + const handleBeforeUnload = (e) => { + e.preventDefault(); + e.returnValue = ""; + + // You could store a flag in sessionStorage here + sessionStorage.setItem("refreshAttempt", "true"); + }; + + // This will execute when the page loads + const checkForRefresh = () => { + if (sessionStorage.getItem("refreshAttempt") === "true") { + // Clear the flag + sessionStorage.removeItem("refreshAttempt"); + // Handle the "after refresh" behavior here + console.log("Page was refreshed, restore state..."); + // You could restore form data or UI state here + } + }; + + window.addEventListener("beforeunload", handleBeforeUnload); + checkForRefresh(); // Check on component mount + + return () => { + window.removeEventListener("beforeunload", handleBeforeUnload); + }; + }, []); + + useEffect(() => { + const handleBeforeUnload = (event) => { + // Completely prevent browser's default dialog + event.preventDefault(); + event.stopPropagation(); + + // Show your custom dialog + //setShowLeaveDialog(true); + + // Modern browsers require this to suppress their own dialog + event.returnValue = + "You have unsaved changes. Are you sure you want to leave?"; + return ""; + }; + + // Add event listeners for maximum coverage + window.addEventListener("beforeunload", handleBeforeUnload); + + // Cleanup event listener on component unmount + return () => { + window.removeEventListener("beforeunload", handleBeforeUnload); + }; + }, []); // Empty dependency array means this runs once on component mount + + useEffect(() => { + // Prevent default refresh behavior + const handleKeyDown = (event) => { + // Prevent Ctrl+R, Cmd+R, and F5 refresh + if ( + ((event.ctrlKey || event.metaKey) && event.key === "r") || + event.key === "F5" + ) { + event.preventDefault(); + + // Optional: Show a dialog or toast to inform user + event.returnValue = + "You have unsaved changes. Are you sure you want to leave?"; + return ""; + } + }; + + // Prevent accidental page unload + const handleBeforeUnload = (event) => { + event.preventDefault(); + event.returnValue = ""; // Required for Chrome + }; + + // Add event listeners + window.addEventListener("keydown", handleKeyDown); + window.addEventListener("beforeunload", handleBeforeUnload); + + // Cleanup event listeners on component unmount + return () => { + window.removeEventListener("keydown", handleKeyDown); + window.removeEventListener("beforeunload", handleBeforeUnload); + }; + }, []); + + // Update files state when Redux fileList changes + useEffect(() => { + if (reduxFileList && reduxFileList.length > 0) { + // Map the Redux fileList to our FileItem format + const fileItems: FileItem[] = reduxFileList + .filter((file) => file.type !== "summary") + .map((file: any, index: number) => ({ + id: file.id, + name: file.name, + type: "code", + status: file.status, // Initial status + file_result: file.file_result, + fileId: file.fileId, + batchId: file.batchId, + file_logs: file.file_logs, + file_track_percentage: file.status === "completed" ? 100 : 0, + code: "", + translatedCode: file.translatedCode || "", + errorCount: file.errorCount || 0, + warningCount: file.warningCount || 0, + })); + + // Add summary file at the beginning + const summaryFile = reduxFileList.find((file) => file.type === "summary"); + setFiles([ + summaryFile || { + id: "summary", + name: "Summary", + type: "summary", + status: "Pending", + errorCount: 0, + warningCount: 0, + file_track_percentage: 0, + }, + ...fileItems, + ]); + + // If no file is selected, select the first file + if (!selectedFileId && fileItems.length > 0) { + if (summaryFile && summaryFile.status === "completed") { + setSelectedFileId(summaryFile.id); + } else { + setSelectedFileId(fileItems[0].id); + } + } + + // Update text with file count + setText(`${new Date().toLocaleDateString()} (${fileItems.length} files)`); + } + }, [reduxFileList, batchId]); + + // Set up WebSocket connection using the WebSocketService + useEffect(() => { + if (batchId?.length === 36) { + console.log(`Connecting to WebSocket with batchId: ${batchId}`); + webSocketService.connect(batchId); + + const onOpen = () => console.log("WebSocket connection established"); + webSocketService.on("open", onOpen); + + return () => { + console.log("Cleaning up WebSocket connection"); + webSocketService.off("open", onOpen); + webSocketService.disconnect(); // Uncomment this! + }; + } else { + console.log( + "The page you are looking for does not exist. Redirected to Home" + ); + navigate("/"); + } + }, [batchId]); + + const highestProgressRef = useRef(0); + const currentProcessingFileRef = useRef(null); + + //new PT FR ends + // Handle WebSocket messages + const handleWebSocketMessage = useCallback( + async (data: WebSocketMessage) => { + console.log("Received WebSocket message:", data); + + if (!data || !data.file_id) { + console.warn("Received invalid WebSocket message:", data); + return; + } + + if (data.file_id) { + currentProcessingFileRef.current = data.file_id; + } + // Update process steps dynamically from agent_type + const agent = formatAgent(data.agent_type); + const message = formatDescription(data.agent_message); + setFileId(data.file_id); + + // Update file status based on the message + setFiles((prevFiles) => { + const fileIndex = prevFiles.findIndex( + (file) => file.fileId === data.file_id + ); + + if (fileIndex === -1) { + console.warn( + `File with ID ${data.file_id} not found in the file list` + ); + return prevFiles; + } + data.agent_message = message; + data.agent_type = agent; + const updatedFiles = [...prevFiles]; + const newTrackLog = updatedFiles[fileIndex].file_track_log?.some( + (entry) => + entry.agent_type === data.agent_type && + entry.agent_message === data.agent_message + ) + ? updatedFiles[fileIndex].file_track_log + : [data, ...(updatedFiles[fileIndex].file_track_log || [])]; + updatedFiles[fileIndex] = { + ...updatedFiles[fileIndex], + status: data.process_status, + file_track_log: newTrackLog, + file_track_percentage: getTrackPercentage( + data.process_status, + newTrackLog + ), + }; + + // Update summary status + const summaryIndex = updatedFiles.findIndex( + (file) => file.id === "summary" + ); + if (summaryIndex !== -1) { + const totalFiles = updatedFiles.filter( + (file) => file.id !== "summary" + ).length; + const completedFiles = updatedFiles.filter( + (file) => file.status === "completed" && file.id !== "summary" + ).length; + const newAllFilesCompleted = + completedFiles === totalFiles && totalFiles > 0; + setAllFilesCompleted(newAllFilesCompleted); + + updatedFiles[summaryIndex] = { + ...updatedFiles[summaryIndex], + status: newAllFilesCompleted ? "completed" : "Processing", + }; + } + + return updatedFiles; + }); + + // Fetch file content if processing is completed + if (data.process_status === "completed") { + try { + dispatch(fetchFileFromAPI(data.file_id || "")); + //const newFileUpdate = await fetchFileFromAPI123(data.file_id); + //const batchSumamry = await fetchBatchSummary123(data.batch_id); + dispatch(fetchBatchSummary(data.batch_id)); + //setBatchSummary(batchSumamry); + setFiles((currentFiles) => { + const c = currentFiles.map((f) => + f.fileId === data.file_id + ? { + ...f, + code: fileContent?.content, + status: data.process_status, + translatedCode: fileContent?.translated_content, + errorCount: fileErrorCounter(fileContent), + warningCount: fileWarningCounter(fileContent), + file_result: fileContent.file_result || undefined, + file_logs: filesLogsBuilder(fileContent), + } + : f + ); + // Update summary status + const summaryIndex = c.findIndex((file) => file.id === "summary"); + if (summaryIndex !== -1) { + setAllFilesCompleted(batchSummary?.status === "completed"); + if ( + batchSummary?.status === "completed" && + batchSummary?.hasFiles > 0 + ) { + setIsZipButtonDisabled(false); + } + + c[summaryIndex] = { + ...c[summaryIndex], + errorCount: batchSummary?.error_count, + warningCount: batchSummary?.warning_count, + status: + batchSummary?.status === "completed" + ? batchSummary?.status + : "Processing", + }; + } + return c; + }); + // updateProgressPercentage(); + } catch (error) { + console.error("Error fetching completed file:", error); + } + } else { + // updateProgressPercentage(); + } + }, + [files, fileId] + ); + + // Listen for WebSocket messages using the WebSocketService + useEffect(() => { + webSocketService.on("message", handleWebSocketMessage); + + webSocketService.on("error", (error) => { + console.error("WebSocket error:", error); + setLoadingError("Connection error occurred. Please try again."); + }); + + webSocketService.on("close", () => { + console.log("WebSocket connection closed"); + }); + + return () => { + webSocketService.off("message", handleWebSocketMessage); + }; + }, [handleWebSocketMessage]); + + useEffect(() => { + const messageHandler = (data: WebSocketMessage) => { + console.log("WebSocket message received:", data); + handleWebSocketMessage(data); + }; + + webSocketService.on("message", messageHandler); + + return () => { + webSocketService.off("message", messageHandler); + }; + }, [handleWebSocketMessage]); + + // Set a timeout for initial loading - if no progress after 30 seconds, show error + useEffect(() => { + const loadingTimeout = setTimeout(() => { + if (progressPercentage < 5 && showLoading) { + setLoadingError( + "Processing is taking longer than expected. You can continue waiting or try again later." + ); + } + }, 30000); + + return () => clearTimeout(loadingTimeout); + }, [progressPercentage, showLoading]); + + useEffect(() => {}, [files, selectedFileId, allFilesCompleted]); + + // Auto-select next processing file + useEffect(() => { + // If no file is selected, try to select one + if (!selectedFileId && files.length > 1) { + const processingFile = files.find((f) => f.status === "in_process"); + if (processingFile) { + setSelectedFileId(processingFile.id); + } else { + // Select first non-summary file + const firstFile = files.find((f) => f.id !== "summary"); + if (firstFile) { + setSelectedFileId(firstFile.id); + } + } + } + }, [files, selectedFileId, allFilesCompleted]); + + const renderBottomButtons = () => { + return ( +
+ + +
+ ); + }; + + const selectedFile = files.find((f) => f.id === selectedFileId); + + // Fix for the Progress tracker title, positioning and background color + const renderContent = () => { + const renderHeader = () => { + const selectedFile = files.find((f) => f.id === selectedFileId); + + if (!selectedFile) return null; + + const title = selectedFile.id === "summary" ? "Summary" : "T-SQL"; + + return ( +
+ + {title} + + + AI-generated content may be incorrect + +
+ ); + }; + const processingStarted = files.some( + (file) => + file.id !== "summary" && + (file.status === "in_process" || file.status === "completed") + ); + + // Show spinner if processing hasn't started yet + if (!processingStarted) { + return ( +
+ + + Getting things ready + +
+ ); + } + // Always show the progress bar until all files are completed + if (!allFilesCompleted || selectedFile?.id !== "summary") { + // If a specific file is selected (not summary) and it's completed, show the file content + if ( + selectedFile && + selectedFile.id !== "summary" && + selectedFile.status === "completed" + ) { + return ( + <> + {renderHeader()} + +
+ + {selectedFile.name}{" "} + {selectedFile.translatedCode ? "(Translated)" : ""} + +
+ {!selectedFile.errorCount && selectedFile.warningCount ? ( + <> + + File processed with warnings + + + + + + ) : null} + {selectedFile.translatedCode ? ( + + {format(selectedFile.translatedCode, { language: "tsql" })} + + ) : selectedFile.status === "completed" && + !selectedFile.translatedCode && + !selectedFile.errorCount ? ( +
+ + Loading file content... +
+ ) : null} + {selectedFile.errorCount ? ( + <> + + Unable to process the file + + + + + + ) : null} +
+ + ); + } + // Otherwise, show the progress view with summary information + const fileIndex = files.findIndex((file) => file.fileId === fileId); + const currentFile = files[fileIndex]; + return ( + <> + {currentFile?.file_track_percentage ? ( +
+ + Progress tracker + +
+
+
+
+ + {Math.floor(currentFile?.file_track_percentage ?? 0)}% + +
+ +
+ Progress illustration +
+ +
+ {currentFile?.file_track_log?.map((step, index) => ( +
+ + • + + + {step.agent_type}: {step.agent_message} + +
+ ))} +
+
+ ) : ( +
+ + Loading file status... +
+ )} + + ); + } + + // Show the full summary page only when all files are completed and summary is selected + if (allFilesCompleted && selectedFile?.id === "summary") { + const completedCount = files.filter( + (file) => + file.status === "completed" && + file.file_result !== "error" && + file.id !== "summary" + ).length; + const totalCount = files.filter((file) => file.id !== "summary").length; + const errorCount = selectedFile.errorCount || 0; + + // Check if there are no errors and all files are processed successfully + const noErrors = errorCount === 0; + const allFilesProcessed = completedCount === totalCount; + if (noErrors && allFilesProcessed) { + // Show the success message UI with the green banner and checkmark + return ( + <> + {renderHeader()} +
+ {/* Green success banner */} + + + {totalCount} {totalCount === 1 ? "file" : "files"} processed + successfully + + + + {/* Success checkmark and message */} +
+ Success checkmark + + No errors! Your files are ready to download. + + + Your code has been successfully translated with no errors. All + files are now ready for download. Click 'Download' to save + them to your local drive. + +
+
+ + ); + } + + // Otherwise show the regular summary view with errors/warnings + if (noErrors && allFilesProcessed) { + return ( + <> + {renderHeader()} +
+ + + {completedCount} of {totalCount}{" "} + {totalCount === 1 ? "file" : "files"} processed successfully + + + +
+ setExpandedSections((prev) => + prev.includes("errors") + ? prev.filter((id) => id !== "errors") + : [...prev, "errors"] + ) + } + > + Errors ({errorCount}) + {expandedSections.includes("errors") ? ( + + ) : ( + + )} +
+
+
+ + ); + } else { + return ( + <> + {renderHeader()} +
+ {batchSummary && batchSummary.completed_files > 0 ? ( + + + {batchSummary.completed_files} of {batchSummary.total_files}{" "} + {batchSummary.total_files === 1 ? "file" : "files"}{" "} + processed successfully + + + ) : null} + + +
+ + ); + } + } + + return null; + }; + + return ( +
+
+ +
+
+ {text} +
+
+
+ {files.map((file, index) => { + // Determine styling classes dynamically + const isQueued = + file.status === "Pending" || + file.status === "Queued" || + file.status === "ready_to_process"; + const isInProgress = file.status === "in_process"; + const isCompleted = file.status === "completed"; + const isSummary = file.id === "summary"; + const isSummaryDisabled = + isSummary && file.status !== "completed"; + const displayStatus = getPrintFileStatus(file.status); + const isProcessing = displayStatus === "Processing"; + const fileClass = `${styles.fileCard} + ${ + selectedFileId === file.id + ? styles.selectedCard + : "" + } + ${isQueued ? styles.queuedFile : ""} + ${ + isInProgress + ? styles.completedFile + : "" + } + ${ + isCompleted ? styles.completedFile : "" + } + ${ + isSummaryDisabled + ? styles.summaryDisabled + : "" + } + `; + return ( +
{ + // Only allow selecting summary if all files are completed + if ( + file.id === "summary" && + file.status !== "completed" + ) + return; + // Don't allow selecting queued files + if (file.status === "ready_to_process") return; + setSelectedFileId(file.id); + handleClick(file.id); + }} + style={{ + backgroundColor: selectedFilebg === file.id ? "#EBEBEB" : "var(--NeutralBackground1-Rest)", + }} + > + {isSummary ? ( + + ) : isInProgress ? ( + // Use the Fluent arrow sync icon for processing files + + ) : ( + + )} + {file.name} +
+ {file.id === "summary" && + allFilesCompleted && + file.errorCount === 0 ? ( + <> + + + ) : file.id === "summary" && + file.errorCount && + file.errorCount > 0 && + allFilesCompleted ? ( + <> + + {file.errorCount.toLocaleString()}{" "} + {file.errorCount === 1 ? "error" : "errors"} + + + ) : file.status === "completed" && file.errorCount ? ( + <> + {file.errorCount} + + + ) : file.status === "completed" && file.warningCount ? ( + <> + {file.warningCount} + + + ) : file.status === "completed" ? ( + + ) : ( + + {displayStatus} + + )} +
+
+ ); + })} +
+
+
+ {renderBottomButtons()} +
+
+
+ + +
+ {renderContent()} +
+
+
+
+ ); +}; + +export default ModernizationPage; diff --git a/src/frontend/src/store/batchSlice.ts b/src/frontend/src/store/batchSlice.ts new file mode 100644 index 00000000..f68c22a4 --- /dev/null +++ b/src/frontend/src/store/batchSlice.ts @@ -0,0 +1,196 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import httpUtility from '../utils/httpUtil'; + +// Dummy API call for batch deletion +export const deleteBatch = createAsyncThunk< + any, // The type of returned response data (can be updated to match API response) + { batchId: string; headers?: Record | null }, // Payload type + { rejectValue: string } // Type for rejectWithValue +>( + 'batch/deleteBatch', + async ({ batchId, headers }: { batchId: string; headers?: Record | null }, { rejectWithValue }) => { + try { + + const response:any = await httpUtility.delete(`/delete-batch/${batchId}`); + return response; + } catch (error: any) { + return rejectWithValue(error.response?.data || 'Failed to delete batch'); + } + } +); + + +//API call for Batch Start Processing +export const startProcessing = createAsyncThunk( + "batch/startProcessing", + async (payload: { batchId: string; translateFrom: string; translateTo: string }, { rejectWithValue }) => { + try { + // Constructing the request payload + const requestData = { + batch_id: payload.batchId, + translate_from: payload.translateFrom, // Empty for now + translate_to: payload.translateTo, // Either "sql" or "postgress" + }; + + const response:any = await httpUtility.post(`/start-processing`, requestData); + + const data = response + + return await data; + } catch (error) { + return rejectWithValue(error.response?.data || "Failed to start processing"); + } + } +); + +interface FetchBatchHistoryPayload { + headers?: Record; +} + +// Async thunk to fetch batch history with headers +export const fetchBatchHistory = createAsyncThunk( + "batch/fetchBatchHistory", + async ({ headers }: FetchBatchHistoryPayload, { rejectWithValue }) => { + try { + + + const response:any = await httpUtility.get(`/batch-history`); + return response; + } catch (error) { + if (error.response && error.response.status === 404) { + return []; + } + return rejectWithValue(error.response?.data || "Failed to load batch history"); + } + } +); + +export const deleteAllBatches = createAsyncThunk( + "batch/deleteAllBatches", + async ({ headers }: { headers: Record }, { rejectWithValue }) => { + try { + + const response:any = await httpUtility.delete(`/delete_all`); + return response; + } catch (error) { + return rejectWithValue(error.response?.data || "Failed to delete all batch history"); + } + } +); + +// + +// Initial state for the batch slice +const initialState: { + batches: string[], + batchId: string | null; + fileId: string | null; + message: string; + loading: boolean; + error: string | null; + uploadingFiles: boolean; + files: { + file_id: string; + batch_id: string; + original_name: string; + blob_path: string; + translated_path: string; + status: string; + error_count: number; + created_at: string; + updated_at: string; + }[]; +} = { + batchId: null, + fileId: null, + message: '', + loading: false, + error: null, + uploadingFiles: false, + files: [], + batches: [] +}; + +export const batchSlice = createSlice({ + name: 'batch', + initialState, + reducers: { + resetBatch: (state) => { + state.batchId = null; + state.fileId = null + state.message = ''; + state.error = null; + }, + }, + extraReducers: (builder) => { + // Handle the deleteBatch action + builder + .addCase(deleteBatch.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase(deleteBatch.fulfilled, (state, action) => { + state.loading = false; + if (action.payload) { + state.batchId = action.payload.batch_id; + state.message = action.payload.message; + } else { + state.error = "Unexpected response: Payload is undefined."; + } + }) + .addCase(deleteBatch.rejected, (state, action) => { + state.loading = false; + state.error = action.payload as string; + }); + + builder + .addCase(startProcessing.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase(startProcessing.fulfilled, (state, action) => { + state.loading = false; + if (action.payload) { + console.log("Action Payload", action.payload); + state.batchId = action.payload.batch_id; + state.message = "Processing started successfully"; + } else { + state.error = "Unexpected response: Payload is undefined."; + } + }) + .addCase(startProcessing.rejected, (state, action) => { + state.loading = false; + state.error = action.payload as string; + }); + //Fetch Batch History Action Handle + builder + .addCase(fetchBatchHistory.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase(fetchBatchHistory.fulfilled, (state, action) => { + state.loading = false; + state.batches = action.payload; + }) + .addCase(fetchBatchHistory.rejected, (state, action) => { + state.loading = false; + state.error = action.payload as string | null; + }); + builder + .addCase(deleteAllBatches.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase(deleteAllBatches.fulfilled, (state) => { + state.loading = false; + state.batches = []; + }) + .addCase(deleteAllBatches.rejected, (state, action) => { + state.loading = false; + state.error = action.payload as string | null; + }); + }, +}); + +export const { } = batchSlice.actions; +export default batchSlice.reducer; \ No newline at end of file diff --git a/src/frontend/src/store/historyPanelSlice.ts b/src/frontend/src/store/historyPanelSlice.ts new file mode 100644 index 00000000..7492b47d --- /dev/null +++ b/src/frontend/src/store/historyPanelSlice.ts @@ -0,0 +1,20 @@ +import { createSlice } from "@reduxjs/toolkit"; + +const historyPanelSlice = createSlice({ + name: "historyPanel", + initialState: { isOpen: false }, + reducers: { + togglePanel: (state) => { + state.isOpen = !state.isOpen; + }, + openPanel: (state) => { + state.isOpen = true; + }, + closePanel: (state) => { + state.isOpen = false; + }, + }, +}); + +export const { togglePanel, openPanel, closePanel } = historyPanelSlice.actions; +export default historyPanelSlice.reducer; diff --git a/src/frontend/src/store/modernizationSlice.ts b/src/frontend/src/store/modernizationSlice.ts new file mode 100644 index 00000000..f0951ef7 --- /dev/null +++ b/src/frontend/src/store/modernizationSlice.ts @@ -0,0 +1,141 @@ +import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; +import httpUtility from "../utils/httpUtil"; +import { BatchSummary } from "../types/types"; +import { completedFiles, filesFinalErrorCounter, filesErrorCounter, hasFiles, fileWarningCounter, fileErrorCounter, filesLogsBuilder } from "../utils/utils"; + +export const fetchFileFromAPI = createAsyncThunk( + "modernization/fetchFileFromAPI", + async (fileId: string, { rejectWithValue }) => { + try { + const response:any = await httpUtility.get(`/file/${fileId}`); + return response; // Assuming the API returns the file content directly + } catch (error) { + console.error("Error fetching file from API:", error); + return rejectWithValue({ content: "", translatedContent: "" }); + } + } +); + +export const fetchBatchSummary = createAsyncThunk( + "modernization/fetchBatchSummary", + async (batchId: string, { rejectWithValue }) => { + try { + const responseData:any = await httpUtility.get(`/batch-summary/${batchId}`); + const data: BatchSummary = { + batch_id: responseData.batch.batch_id, + upload_id: responseData.batch.id, // Use id as upload_id + date_created: responseData.batch.created_at, + total_files: responseData.batch.file_count, + completed_files: completedFiles(responseData.files), + error_count: responseData.batch.status === "completed" ? filesFinalErrorCounter(responseData.files) : filesErrorCounter(responseData.files), + status: responseData.batch.status, + warning_count: responseData.files.reduce((count, file) => count + (file.syntax_count || 0), 0), + hasFiles: hasFiles(responseData), + files: responseData.files.map(file => ({ + file_id: file.file_id, + name: file.original_name, // Use original_name here + status: file.status, + file_result: file.file_result, + warning_count: fileWarningCounter(file), + error_count: fileErrorCounter(file), + translated_content: file.translated_content, + file_logs: filesLogsBuilder(file), + })) + }; + return data; + } catch (error) { + console.error("Error fetchBatchSummary:", error); + return rejectWithValue({ content: "", translatedContent: "" }); + } + } +); + +export const handleDownloadZip = createAsyncThunk( + "modernization/handleDownloadZip", + async (batchId: string, { rejectWithValue }) => { + if (batchId) { + try { + const response:any = await httpUtility.get(`/download/${batchId}?batch_id=${batchId}`); + + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + + // Create a temporary element and trigger download + const link = document.createElement("a"); + link.href = url; + link.setAttribute("download", "download.zip"); // Specify a filename + document.body.appendChild(link); + link.click(); + + // Cleanup + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + } catch (error) { + console.error("Download failed:", error); + } + } + } + ); + +const modernizationSlice = createSlice({ + name: "modernizationSlice", + initialState: { + isOpen: false, + loading: false, + fileContent: {content: "", translated_content: "",file_result: null, file_track_log: [], file_track_percentage: 0}, + batchSummary: { + batch_id: "", + upload_id: "", + date_created: "", + total_files: 0, + completed_files: 0, + error_count: 0, + status: "", + warning_count: 0, + hasFiles: 0, + files: [] as { + file_id: string; + name: string; + status: string; + error_count: number; + warning_count: number; + file_logs: any[]; + content?: string; + translated_content?: string; + }[], + }, + }, + reducers: { + updateBatchSummary: (state, action) => { + state.batchSummary = action.payload; + }, + }, + extraReducers: (builder) => { + // Handle the deleteBatch action + builder + .addCase(fetchFileFromAPI.pending, (state) => { + state.loading = true; + }) + .addCase(fetchFileFromAPI.fulfilled, (state, action) => { + state.fileContent = action.payload; + state.loading = false; + }) + .addCase(fetchFileFromAPI.rejected, (state, action) => { + state.loading = false; + }) + .addCase(fetchBatchSummary.pending, (state) => { + state.loading = true; + }) + .addCase(fetchBatchSummary.fulfilled, (state, action) => { + state.batchSummary = action.payload; + state.loading = false; + }) + .addCase(fetchBatchSummary.rejected, (state, action) => { + state.loading = false; + }) + } +}); + +export const { updateBatchSummary} = modernizationSlice.actions; +export default modernizationSlice.reducer; \ No newline at end of file diff --git a/src/frontend/src/store/store.ts b/src/frontend/src/store/store.ts index 73b2a041..52f32f65 100644 --- a/src/frontend/src/store/store.ts +++ b/src/frontend/src/store/store.ts @@ -1,12 +1,15 @@ import { configureStore } from '@reduxjs/toolkit'; -import { batchSlice, fileReducer } from '../slices/batchSlice'; -import historyPanelReducer from '../slices/historyPanelSlice'; +import batchReducer from './batchSlice'; +import historyPanelReducer from './historyPanelSlice'; +import modernizationReducer from './modernizationSlice'; +import fileReducer from './uploadFileSlice'; export const store = configureStore({ reducer: { - batch: batchSlice.reducer, + batch: batchReducer, fileUpload: fileReducer, historyPanel: historyPanelReducer, + modernizationReducer: modernizationReducer, }, }) diff --git a/src/frontend/src/store/uploadFileSlice.ts b/src/frontend/src/store/uploadFileSlice.ts new file mode 100644 index 00000000..91dd8fa9 --- /dev/null +++ b/src/frontend/src/store/uploadFileSlice.ts @@ -0,0 +1,88 @@ +import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"; +import httpUtility from "../utils/httpUtil"; + +export const deleteFileFromBatch = createAsyncThunk( + 'batch/deleteFileFromBatch', + async (fileId: string, { rejectWithValue }) => { + try { + + const response:any = await httpUtility.delete(`/delete-file/${fileId}`); + + // Return the response data + return response; + } catch (error) { + // Handle the error + return rejectWithValue(error.response?.data || 'Failed to delete batch'); + } + } +); + +// API call for uploading single file in batch +export const uploadFile = createAsyncThunk('/upload', // Updated action name + async (payload: { file: File; batchId: string }, { rejectWithValue }) => { + try { + const formData = new FormData(); + + // Append batch_id + formData.append("batch_id", payload.batchId); + + // Append the single file + formData.append("file", payload.file); + //formData.append("file_uuid", payload.uuid); + + const response:any = await httpUtility.post(`/upload`, formData); + return response; + } catch (error) { + return rejectWithValue(error.response?.data || 'Failed to upload file'); + } + } +); + +interface FileState { + batchId: string | null; + fileList: { fileId: string; originalName: string }[]; // Store file_id & name + status: 'idle' | 'loading' | 'succeeded' | 'failed'; + error: string | null; +} + +// Initial state +const initialFileState: FileState = { + batchId: null, + fileList: [], + status: 'idle', + error: null, +}; + +const fileSlice = createSlice({ + name: 'fileUpload', + initialState: initialFileState, + reducers: { + resetState: (state) => { + state.batchId = null; + state.fileList = []; + state.status = 'idle'; + state.error = null; + }, + }, + extraReducers: (builder) => { + builder + .addCase(uploadFile.fulfilled, (state, action: PayloadAction<{ batch: { batch_id: string }; file: { file_id: string; original_name: string } }>) => { + state.batchId = action.payload.batch.batch_id; // Store batch ID + state.fileList.push({ + fileId: action.payload.file.file_id, // Store file ID + originalName: action.payload.file.original_name, // Store file name + }); + state.status = 'succeeded'; + }) + .addCase(uploadFile.rejected, (state, action: PayloadAction) => { + state.error = action.payload; + state.status = 'failed'; + }) + .addCase(deleteFileFromBatch.fulfilled, (state, action) => { + state.fileList = state.fileList.filter(file => file.fileId !== action.meta.arg); + }); + }, +}); + +export const { resetState } = fileSlice.actions; +export default fileSlice.reducer; diff --git a/src/frontend/src/types/types.ts b/src/frontend/src/types/types.ts index ac284af9..cf68dd45 100644 --- a/src/frontend/src/types/types.ts +++ b/src/frontend/src/types/types.ts @@ -37,4 +37,55 @@ export interface StepProps { location: string; }>; }>; + } + type FileType = "summary" | "code" + type FileResult = "info" | "warning" | "error" | null + + export interface WebSocketMessage { + batch_id: string; + file_id: string; + agent_type: string; + agent_message: string; + process_status: string; + file_result: FileResult; + } + + export interface FileItem { + id: string + name: string + type: FileType; + status: string + code?: string + translatedCode?: string + errorCount?: number + warningCount?: number + file_logs?: any[]; + file_result?: string + file_track_log?: WebSocketMessage[] + file_track_percentage?: number + fileId?: string + batchId?: string + order?: number + } + + export interface BatchSummary { + batch_id: string; + upload_id: string; // Added upload_id to the interface + date_created: string; + total_files: number; + status: string; + completed_files: number; + error_count: number; + warning_count: number; + hasFiles: number; + files: { + file_id: string; + name: string; + status: string; + error_count: number; + warning_count: number; + file_logs: any[]; + content?: string; + translated_content?: string; + }[]; } \ No newline at end of file diff --git a/src/frontend/src/utils/WebSocketService.tsx b/src/frontend/src/utils/WebSocketService.tsx new file mode 100644 index 00000000..dcddd970 --- /dev/null +++ b/src/frontend/src/utils/WebSocketService.tsx @@ -0,0 +1,85 @@ +import { getApiUrl } from "./config"; + +// WebSocketService.ts +type EventHandler = (data: any) => void; + +class WebSocketService { + private socket: WebSocket | null = null; + private eventHandlers: Record = {}; + + connect(batch_id: string): void { + let apiUrl = getApiUrl() as string | null; + + console.log('API URL: websocket', apiUrl); + if (apiUrl) { + apiUrl = apiUrl.replace(/^https?/, match => match === "https" ? "wss" : "ws"); + } else { + throw new Error('API URL is null'); + } + console.log('Connecting to WebSocket:', apiUrl); + if (this.socket) return; // Prevent duplicate connections + this.socket = new WebSocket(`${apiUrl}/socket/${batch_id}`); + + this.socket.onopen = () => { + console.log('WebSocket connection opened.'); + this._emit('open', undefined); + }; + + this.socket.onmessage = (event: MessageEvent) => { + try { + const data = JSON.parse(event.data); + this._emit('message', data); + } catch (err) { + console.error('Error parsing message:', err); + } + }; + + this.socket.onerror = (error: Event) => { + console.error('WebSocket error:', error); + this._emit('error', error); + }; + + this.socket.onclose = (event: CloseEvent) => { + console.log('WebSocket closed:', event); + this._emit('close', event); + this.socket = null; + }; + } + + disconnect(): void { + if (this.socket) { + this.socket.close(); + this.socket = null; + console.log('WebSocket connection closed manually.'); + } + } + + send(data: any): void { + if (this.socket && this.socket.readyState === WebSocket.OPEN) { + this.socket.send(JSON.stringify(data)); + } else { + console.error('WebSocket is not open. Cannot send:', data); + } + } + + on(event: string, handler: EventHandler): void { + if (!this.eventHandlers[event]) { + this.eventHandlers[event] = []; + } + this.eventHandlers[event].push(handler); + } + + off(event: string, handler: EventHandler): void { + if (!this.eventHandlers[event]) return; + this.eventHandlers[event] = this.eventHandlers[event].filter(h => h !== handler); + } + + private _emit(event: string, data: any): void { + if (this.eventHandlers[event]) { + this.eventHandlers[event].forEach(handler => handler(data)); + } + } +} + +const webSocketService = new WebSocketService(); +export default webSocketService; diff --git a/src/frontend/src/utils/config.js b/src/frontend/src/utils/config.js new file mode 100644 index 00000000..2e687054 --- /dev/null +++ b/src/frontend/src/utils/config.js @@ -0,0 +1,82 @@ +export let API_URL= null; +export let USER_ID= null; + +export let config = { + API_URL: "http://localhost:8000", + REACT_APP_MSAL_AUTH_CLIENTID: "", + REACT_APP_MSAL_AUTH_AUTHORITY: "", + REACT_APP_MSAL_REDIRECT_URL: "", + REACT_APP_MSAL_POST_REDIRECT_URL: "", + ENABLE_AUTH: false, +}; + +export function setApiUrl(url) { + if (url) { + API_URL = `${url}/api`; + } +} + +export function setEnvData(configData) { + if (configData) { + config.API_URL = configData.API_URL || ""; + config.REACT_APP_MSAL_AUTH_CLIENTID = configData.REACT_APP_MSAL_AUTH_CLIENTID || ""; + config.REACT_APP_MSAL_AUTH_AUTHORITY = configData.REACT_APP_MSAL_AUTH_AUTHORITY || ""; + config.REACT_APP_MSAL_REDIRECT_URL = configData.REACT_APP_MSAL_REDIRECT_URL || ""; + config.REACT_APP_MSAL_POST_REDIRECT_URL = configData.REACT_APP_MSAL_POST_REDIRECT_URL || ""; + config.ENABLE_AUTH = configData.ENABLE_AUTH || false; + } +} + +export function getConfigData() { + if (!config.REACT_APP_MSAL_AUTH_CLIENTID || !config.REACT_APP_MSAL_AUTH_AUTHORITY || !config.REACT_APP_MSAL_REDIRECT_URL || !config.REACT_APP_MSAL_POST_REDIRECT_URL) { + // Check if window.appConfig exists + if (window.appConfig) { + setEnvData(window.appConfig); + } + } + + return { ...config }; +} + +export function getApiUrl() { + if (!API_URL) { + // Check if window.appConfig exists + if (window.appConfig && window.appConfig.API_URL) { + setApiUrl(window.appConfig.API_URL); + } + } + + if (!API_URL) { + console.warn('API URL not yet configured'); + return null; + } + + return API_URL; +} + +export function getUserId() { + USER_ID = window.activeUserId ?? null; + const userId = USER_ID ?? "00000000-0000-0000-0000-000000000000"; + return userId; +} + +export function headerBuilder(headers) { + let userId = getUserId(); + let defaultHeaders = { + "x-ms-client-principal-id": String(userId) || "", // Custom header + }; + return { + ...defaultHeaders, ...(headers ? headers : {}) + }; +} + +export default { + setApiUrl, + getApiUrl, + getUserId, + getConfigData, + setEnvData, + config, + USER_ID, + API_URL +}; \ No newline at end of file diff --git a/src/frontend/src/utils/constants.tsx b/src/frontend/src/utils/constants.tsx new file mode 100644 index 00000000..5236e23b --- /dev/null +++ b/src/frontend/src/utils/constants.tsx @@ -0,0 +1,17 @@ +export enum ProcessingStage { + NotStarted = 1, + Queued = 10, + Starting = 20, + Parsing = 40, + Processing = 60, + FinalChecks = 95, + Completed = 100 + } + +export enum Agents { + Verifier = "Semantic Verifier agent", + Checker = "Syntax Checker agent", + Picker = "Picker agent", + Migrator = "Migrator agent", + Agents = "Agent" + } \ No newline at end of file diff --git a/src/frontend/src/utils/httpUtil.ts b/src/frontend/src/utils/httpUtil.ts new file mode 100644 index 00000000..cf98affc --- /dev/null +++ b/src/frontend/src/utils/httpUtil.ts @@ -0,0 +1,150 @@ +// AuthService.ts + +import { getApiUrl } from "./config"; + +let api:string;//import.meta.env.VITE_API_URL; // Replace with your API URL +// Define the types for the response +interface ApiResponse { + data: T; +} + +interface FetchOptions { + method: string; + headers: HeadersInit; + body?: string | FormData | null; +} + +export const updateUrl=()=>{ + api = getApiUrl() ?? ''; // Provide a fallback empty string if getApiUrl() returns null +} + +// Fetch with JWT Authentication +const fetchWithAuth = async (url: string, method: string = 'GET', body: any): Promise => { + const token = localStorage.getItem('token'); // Get the token from localStorage + + const headers: HeadersInit = { + 'Authorization': `Bearer ${token}`, // Add the token to the Authorization header + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cache-Control': 'no-cache', + }; + + // If body is FormData, do not set Content-Type header + if (body instanceof FormData) { + delete headers['Content-Type']; + } else { + headers['Content-Type'] = 'application/json'; + body = body ? JSON.stringify(body) : null; + } + + const options: FetchOptions = { + method, + headers, + }; + + if (body) { + options.body = body; // Add the body only if it exists (for POST, PUT) + } + + try { + const response = await fetch(`${api}${url}`, options); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(errorText || 'Something went wrong'); + } + + const isJson = response.headers.get('content-type')?.includes('application/json'); + return isJson ? (await response.json()) as T : null; + } catch (error: any) { + //console.error('API Error:', error.message); + throw error; + } +}; + +// Fetch with JWT Authentication +const fetchHeadersWithAuth = async (url: string, method: string = 'GET', body: any = null): Promise => { + const token = localStorage.getItem('token'); // Get the token from localStorage + + const headers: HeadersInit = { + 'Authorization': `Bearer ${token}`, // Add the token to the Authorization header + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cache-Control': 'no-cache', + }; + + // If body is FormData, do not set Content-Type header + if (body instanceof FormData) { + delete headers['Content-Type']; + } else { + headers['Content-Type'] = 'application/json'; + body = body ? JSON.stringify(body) : null; + } + + const options: FetchOptions = { + method, + headers, + }; + + if (body) { + options.body = body; // Add the body only if it exists (for POST, PUT) + } + + try { + const response = await fetch(`${api}${url}`, options); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(errorText || 'Something went wrong'); + } + return response; + + } catch (error: any) { + //console.error('API Error:', error.message); + throw error; + } +}; + +// Vanilla Fetch without Auth for Login +const fetchWithoutAuth = async (url: string, method: string = 'POST', body: any = null): Promise => { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + }; + + const options: FetchOptions = { + method, + headers, + }; + + if (body) { + options.body = JSON.stringify(body); // Add the body for POST + } + + try { + const response = await fetch(`${api}${url}`, options); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(errorText || 'Login failed'); + } + + const isJson = response.headers.get('content-type')?.includes('application/json'); + return isJson ? (await response.json()) as T : null; + } catch (error: any) { + //console.error('Login Error:', error.message); + throw error; + } +}; + +// Authenticated requests (with token) and login (without token) +export const httpUtility = { + get: (url: string): Promise => fetchWithAuth(url, 'GET', null), + post: (url: string, body: any): Promise => fetchWithAuth(url, 'POST', body), + put: (url: string, body: any): Promise => fetchWithAuth(url, 'PUT', body), + delete: (url: string): Promise => fetchWithAuth(url, 'DELETE', null), + upload: (url: string, formData: FormData): Promise => fetchWithAuth(url, 'POST', formData), + login: (url: string, body: any): Promise => fetchWithoutAuth(url, 'POST', body), // For login without auth + headers : (url: string): Promise => fetchHeadersWithAuth(url, 'GET'), +}; + +export default httpUtility; diff --git a/src/frontend/src/utils/utils.tsx b/src/frontend/src/utils/utils.tsx new file mode 100644 index 00000000..7f9d89a5 --- /dev/null +++ b/src/frontend/src/utils/utils.tsx @@ -0,0 +1,138 @@ +export const filesErrorCounter = (files) => { + return files.reduce((count, file) => { + const logs = filesLogsBuilder(file); + const errorCount = logs.filter(log => log.logType === "error").length; + return count + errorCount; + }, 0); +}; + +export const filesFinalErrorCounter = (files) => { + return files.reduce((count, file) => { + const logs = filesLogsBuilder(file); + const errorCount = logs.filter(log => log.logType === "error").length; + if (file.status !== "completed") { // unfinished or failed file without error entry + return count + (errorCount > 0 ? errorCount : 1); + } + return count + errorCount; + }, 0); +}; +export const completedFiles = (files) => { + return files.filter(f => f.status?.toLowerCase() === "completed" && f.file_result !== "error").length; +}; + +export const hasFiles = (responseData) => { + return completedFiles(responseData.files) +}; + +export const fileErrorCounter = (file) => { + const logs = filesLogsBuilder(file); + return logs.filter(log => log.logType === "error").length; +}; + +export const fileWarningCounter = (file) => { + const logs = filesLogsBuilder(file); + const value = logs.filter(log => log.logType === "warning").length + return value; +}; + +export const determineFileStatus = (file) => { + // If file.status is not "completed", it's an error. + if (file.status?.toLowerCase() !== "completed") return "error"; + // If file.status is "completed" but file_result is "error", it's an error. + if (file.file_result === "error") return "error"; + // If file.status is "completed" and file_result is "success", it's completed. + if (file.file_result === "success") return "completed"; + // Fallback to error if none of the above conditions are met. + return "error"; +}; +// Function to format agent type strings +export const formatAgent = (str = "Agent") => { + if (!str) return "agent"; + + const cleaned = str + .replace(/[^a-zA-Z\s]/g, " ") + .replace(/\s+/g, " ") + .trim() + .replace(/\bAgents\b/i, "Agent"); + + const words = cleaned + .split(" ") + .filter(Boolean) + .map(w => w.toLowerCase()); + + const hasAgent = words.includes("agent"); + + // Capitalize all except "agent" (unless it's the only word) + const result = words.map((word, index) => { + if (word === "agent") { + return words.length === 1 ? "Agent" : "agent"; // Capitalize if it's the only word + } + return word.charAt(0).toUpperCase() + word.slice(1); + }); + + if (!hasAgent) { + result.push("agent"); + } + + return result.join(" "); +}; + +// Function to handle rate limit errors and ensure descriptions end with a dot +export const formatDescription = (description) => { + if (!description) return "No description provided."; + + let sanitizedDescription = description.includes("RateLimitError") + ? "Rate limit error." + : description; + + // Ensure it ends with a dot + if (!sanitizedDescription.endsWith(".")) { + sanitizedDescription += "."; + } + + return sanitizedDescription.replace(/_/g, ' '); +}; + +// Function to build log entries from file logs +export const filesLogsBuilder = (file) => { + if (!file || !file.logs || file.logs.length === 0) { + return []; + } + + return file.logs + .filter(log => log.agent_type !== "human") // Exclude human logs + .map((log, index) => { + let parsedDescription; + const description = log.description; + + try { + const json_desc = typeof description === "object" ? description : JSON.parse(description); + try { + if (json_desc.differences && Array.isArray(json_desc.differences)) { + parsedDescription = json_desc.differences.toString(); + }else { + if(Array.isArray(json_desc.content)){ + parsedDescription = json_desc.content.toString(); // Fallback to json_desc content + }else{ + const json_desc2 = typeof json_desc.content === "object" ? json_desc.content : JSON.parse(json_desc.content); + parsedDescription = json_desc2.source_summary ?? json_desc2.input_summary?? json_desc2.thought ?? json_desc2.toString(); // Fallback to json_desc content + } + + } + } catch { + parsedDescription = json_desc.content; // Fallback to json_desc content + } + } catch { + parsedDescription = description; // Fallback to raw description + } + + return { + id: index, + agentType: formatAgent(log.agent_type), // Apply improved formatSentence function + description: formatDescription(parsedDescription), // Apply sanitizeRateLimitError function + logType: log.log_type, + timestamp: log.timestamp, + }; + }); +}; +