Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions oobe/src/components/AIErrorModal.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.modal.show .modal-dialog.custom-modal {
max-width: 70vw !important;
width: 90vw !important;
margin: 0 auto !important;
}

.custom-modal {
max-width: 70vw !important;
width: 90vw !important;
margin: 0 auto !important;
}

.custom-modal .modal-content {
min-height: 500px;
height: auto;
border: 4px solid rgba(114, 114, 114, 0.2) !important;
border-radius: 24px !important;
}

.modal-body .text-bold {
font-weight: 700 !important;
font-weight: bold !important;
}

.modal-backdrop.show {
background-color: rgba(0, 0, 0, 0.4) !important;
background-image: radial-gradient(
circle,
rgba(255, 255, 255, 0.1) 1px,
transparent 1px
);
background-size: 3px 3px;
opacity: 1 !important;
}

.modal-backdrop.fade {
transition: none !important;
-webkit-transition: none !important;
}
135 changes: 135 additions & 0 deletions oobe/src/components/AIErrorModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { Modal, Button } from "react-bootstrap";
import { useState, type ReactNode } from "react";
import "./AIErrorModal.scss";

interface AIErrorModalProps {
show: boolean;
onHide: () => void;
onContinue: () => Promise<boolean>;
onExit?: () => void;
message?: ReactNode;
}

const AIErrorModal = ({
show,
onHide,
onContinue,
onExit,
message = (
<>
<strong>No AI model</strong> is currently deployed on this device.
<br />
To run this application, deploy a model from your <strong>
Clea
</strong>{" "}
account.
<br />
If you have already deployed the model click '<strong>Check again</strong>
' to verify.
</>
),
}: AIErrorModalProps) => {
const [loading, setLoading] = useState(false);
const [errorMsg, setErrorMsg] = useState<string | null>(null);

const handleContinue = async () => {
setErrorMsg(null);
setLoading(true);
try {
const ok = await onContinue();
setLoading(false);
if (ok) {
onHide();
} else {
setErrorMsg("No model detected. Please deploy a model and try again");
}
} catch {
setLoading(false);
setErrorMsg("No model detected. Please deploy a model and try again");
}
};

return (
<Modal
show={show}
onHide={onHide}
centered
backdrop="static"
keyboard={false}
dialogClassName="custom-modal"
contentClassName="border border-grey rounded-4 overflow-hidden"
>
<Modal.Header
className="border-0"
style={{ backgroundColor: "#000", paddingBottom: "0.5rem" }}
></Modal.Header>

<Modal.Body
className="px-5 py-4 text-center border-0 d-flex flex-column justify-content-center"
style={{ backgroundColor: "#000", color: "#fff" }}
>
<h2 className="fw-bold mb-3" style={{ fontSize: "2.5rem" }}>
Attention
</h2>

<p
className="mb-4"
style={{ fontSize: "2rem", opacity: 0.9, lineHeight: "1.6" }}
>
{message}
</p>

{errorMsg && (
<div
className="mb-4 fw-bold"
style={{
color: "#ff0000",
fontSize: "1.4rem",
textShadow: "0 0 10px rgba(255,0,0,0.3)",
}}
>
{errorMsg}
</div>
)}

<div className="d-flex justify-content-center gap-3">
<Button
variant="primary"
onClick={handleContinue}
disabled={loading}
className="px-5 py-3 fw-bold"
style={{
borderRadius: "15px",
backgroundColor: "#fff",
color: "#000",
border: "none",
}}
>
<h2
className="mb-1"
style={{ fontSize: "1.7rem", fontWeight: "800" }}
>
Check again
</h2>
</Button>
<Button
variant="outline-light"
onClick={onExit || onHide}
disabled={loading}
className="px-5 py-3 fw-bold"
style={{ borderRadius: "15px", border: "2px solid #fff" }}
>
<h2
className="mb-1"
style={{ fontSize: "1.7rem", fontWeight: "800", color: "#fff" }}
>
Exit
</h2>
</Button>
</div>
</Modal.Body>
</Modal>
);
};

export default AIErrorModal;
3 changes: 0 additions & 3 deletions oobe/src/i18n/langs/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,6 @@
"faceRecognitionModal.tenantRecognized": {
"defaultMessage": "Tenant successfully recognized."
},
"imageFormatError": {
"defaultMessage": "Backend rejected the image format."
},
"industrialAlertManagement.dischargePressure": {
"defaultMessage": "DISCHARGE PRESSURE"
},
Expand Down
3 changes: 0 additions & 3 deletions oobe/src/i18n/langs/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -257,9 +257,6 @@
"electricityMeter.title": {
"defaultMessage": "Electricity Meter"
},
"imageFormatError": {
"defaultMessage": "Backend rejected the image format."
},
"faceRecognitionModal.authenticatedUser": {
"defaultMessage": "Authorization confirmed"
},
Expand Down
44 changes: 29 additions & 15 deletions oobe/src/pages/CrowdAndFallDetection.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Container, Image, Button, Alert } from "react-bootstrap";
import { Container, Image, Button } from "react-bootstrap";
import { useState, useEffect, useRef, useLayoutEffect } from "react";
import { useNavigate } from "react-router-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faX, faExclamationTriangle } from "@fortawesome/free-solid-svg-icons";
import { FormattedMessage, defineMessages } from "react-intl";
import ImageCarousel from "./ImageCarousel";
import AIErrorModal from "../components/AIErrorModal";
import { APIClient } from "../api/APIClient";
import {
logo,
Expand Down Expand Up @@ -126,7 +127,6 @@ const urlToFile = async (url: string): Promise<File> => {

const CrowdAndFallDetection = ({ apiClient }: CrowdAndFallDetectionProps) => {
const [results, setResults] = useState<PersonResult[]>([]);
const [error, setError] = useState<string | null>(null);
const [status, setStatus] = useState<"greeting" | "analysis" | "result">(
"greeting",
);
Expand All @@ -139,6 +139,7 @@ const CrowdAndFallDetection = ({ apiClient }: CrowdAndFallDetectionProps) => {
left: 0,
top: 0,
});
const [showAIErrorModal, setShowAIErrorModal] = useState(false);

const imageRef = useRef<HTMLImageElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -198,8 +199,8 @@ const CrowdAndFallDetection = ({ apiClient }: CrowdAndFallDetectionProps) => {
setAnalysisTime(new Date());
setStatus("result");
} catch {
setError("Error");
setStatus("greeting");
setShowAIErrorModal(true);
// keep status as 'analysis' so the modal overlays the spinner and user can retry
}
};
processImage();
Expand Down Expand Up @@ -324,7 +325,7 @@ const CrowdAndFallDetection = ({ apiClient }: CrowdAndFallDetectionProps) => {
</div>

<div className="flex-grow-1 overflow-auto pe-2 custom-scrollbar mb-3">
{status === "analysis" ? (
{status === "analysis" && !showAIErrorModal ? (
<div className="d-flex align-items-center gap-3 text-info p-4 bg-dark bg-opacity-50 rounded-4 border border-info border-opacity-25">
<div className="spinner-border spinner-border-sm" />
<FormattedMessage {...messages.scanningMessage} />
Expand Down Expand Up @@ -401,16 +402,29 @@ const CrowdAndFallDetection = ({ apiClient }: CrowdAndFallDetectionProps) => {
</div>
</div>
)}
{error && (
<Alert
variant="danger"
className="position-fixed bottom-0 start-0 m-3 z-3 shadow-lg border-0 rounded-3"
onClose={() => setError(null)}
dismissible
>
<FormattedMessage {...messages.errorMsg} />
</Alert>
)}
<AIErrorModal
show={showAIErrorModal}
onHide={() => {
setShowAIErrorModal(false);
}}
onExit={() => {
setShowAIErrorModal(false);
navigate("/smart-building");
}}
onContinue={async () => {
try {
setResults([]);
const file = await urlToFile(currentImage);
const data = await apiClient.getPersonResult(file);
setResults(data || []);
setAnalysisTime(new Date());
setStatus("result");
return true;
} catch {
return false;
}
}}
/>
</Container>
);
};
Expand Down
4 changes: 2 additions & 2 deletions oobe/src/pages/ImageCarousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ const ImageCarousel = ({
: "border-secondary opacity-50"
}`}
style={{
width: "110px",
height: "70px",
width: "150px",
height: "150px",
transition: "all 0.2s ease-in-out",
}}
onClick={() => onSelect(img)}
Expand Down
Loading
Loading