diff --git a/eduaid_web/src/App.css b/eduaid_web/src/App.css
index 74b5e053..977b3483 100644
--- a/eduaid_web/src/App.css
+++ b/eduaid_web/src/App.css
@@ -36,3 +36,37 @@
transform: rotate(360deg);
}
}
+
+.connection-banner {
+ padding: 8px 12px;
+ text-align: center;
+ font-size: 0.9rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+}
+
+.connection-banner.down {
+ background: #b91c1c;
+ color: white;
+}
+
+.connection-banner.error {
+ background: #facc15;
+ color: #111827;
+}
+
+.connection-banner-retry {
+ background: #ffffff;
+ color: #b91c1c;
+ border: none;
+ padding: 4px 8px;
+ cursor: pointer;
+ border-radius: 4px;
+ font-size: 0.85rem;
+}
+
+.connection-banner-retry:hover {
+ filter: brightness(0.95);
+}
diff --git a/eduaid_web/src/App.js b/eduaid_web/src/App.js
index e9eef0a0..f3a8fefc 100644
--- a/eduaid_web/src/App.js
+++ b/eduaid_web/src/App.js
@@ -1,4 +1,5 @@
import "./App.css";
+import { useEffect, useState } from "react";
import { Routes, Route, HashRouter } from "react-router-dom";
import Home from "./pages/Home";
import Question_Type from "./pages/Question_Type";
@@ -6,19 +7,60 @@ import Text_Input from "./pages/Text_Input";
import Output from "./pages/Output";
import Previous from "./pages/Previous";
import NotFound from "./pages/PageNotFound";
+import apiClient from "./utils/apiClient";
+
+const initialConnectionState = { status: "unknown", detail: "" };
+
+function ConnectionBanner({ connection }) {
+ if (connection.status === "down") {
+ return (
+
+
+ Backend disconnected.{" "}
+ {connection.detail || "Please check backend server and API URL."}
+
+
+
+ );
+ }
+
+ if (connection.status === "error") {
+ return (
+
+ Backend responded with an error.{" "}
+ {connection.detail || "Please try again shortly."}
+
+ );
+ }
+
+ return null;
+}
function App() {
+ const [connection, setConnection] = useState(initialConnectionState);
+
+ useEffect(() => apiClient.subscribeConnectionStatus(setConnection), []);
+
return (
-
-
- } />
- } />
- } />
- } />
- } />
- } />
-
-
+ <>
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+ >
);
}
diff --git a/eduaid_web/src/utils/apiClient.js b/eduaid_web/src/utils/apiClient.js
index 0b9c2576..edff0599 100644
--- a/eduaid_web/src/utils/apiClient.js
+++ b/eduaid_web/src/utils/apiClient.js
@@ -1,89 +1,179 @@
// API client that works both in web and Electron environments
class ApiClient {
constructor() {
- this.isElectron = typeof window !== 'undefined' && window.electronAPI;
- this.baseUrl = this.isElectron
- ? window.electronAPI.getApiConfig().baseUrl
- : process.env.REACT_APP_BASE_URL || 'http://localhost:5000';
+ this.isElectron = typeof window !== "undefined" && window.electronAPI;
+ this.baseUrl = this.isElectron
+ ? window.electronAPI.getApiConfig().baseUrl
+ : process.env.REACT_APP_BASE_URL || "http://localhost:5000";
+
+ this.listeners = new Set();
+ this.currentStatus = "unknown"; // unknown | up | down | error
+ this.currentDetail = "";
+ }
+
+ subscribeConnectionStatus(listener) {
+ this.listeners.add(listener);
+ try {
+ listener({ status: this.currentStatus, detail: this.currentDetail });
+ } catch (error) {
+ console.error("Connection listener failed:", error);
+ }
+ return () => this.listeners.delete(listener);
+ }
+
+ notifyConnectionStatus(status, detail = "") {
+ this.listeners.forEach((listener) => {
+ try {
+ listener({ status, detail });
+ } catch (error) {
+ console.error("Connection listener failed:", error);
+ }
+ });
+ }
+
+ setConnectionStatus(status, detail = "") {
+ if (this.currentStatus === status && this.currentDetail === detail) return;
+ this.currentStatus = status;
+ this.currentDetail = detail;
+ this.notifyConnectionStatus(status, detail);
+ }
+
+ getFallbackBaseUrls() {
+ if (this.isElectron || process.env.REACT_APP_BASE_URL) return [];
+ if (typeof window === "undefined") return [];
+
+ const host = window.location.hostname;
+ if (host !== "localhost" && host !== "127.0.0.1") return [];
+
+ if (this.baseUrl.includes(":5000")) return ["http://localhost:5001"];
+ if (this.baseUrl.includes(":5001")) return ["http://localhost:5000"];
+ return [];
+ }
+
+ async parseJsonResponse(response) {
+ if (!response.ok) {
+ const text = await response.text().catch(() => "");
+ throw new Error(`HTTP ${response.status}: ${text || "Request Failed"}`);
+ }
+
+ if (response.status === 204 || response.status === 205) return null;
+
+ const contentLength = response.headers.get("Content-Length");
+ if (contentLength === "0") return null;
+ return response.json();
+ }
+
+ async fetchJson(url, options = {}) {
+ const response = await fetch(url, options);
+ return this.parseJsonResponse(response);
+ }
+
+ async requestWithFallback(endpoint, options = {}) {
+ const primaryUrl = `${this.baseUrl}${endpoint}`;
+
+ try {
+ const data = await this.fetchJson(primaryUrl, options);
+ this.setConnectionStatus("up");
+ return data;
+ } catch (error) {
+ const isNetworkError = error instanceof TypeError;
+ if (!isNetworkError) {
+ this.setConnectionStatus("error", error.message);
+ throw error;
+ }
+
+ const fallbackBaseUrls = this.getFallbackBaseUrls().filter(
+ (url) => url !== this.baseUrl
+ );
+
+ for (const fallbackBaseUrl of fallbackBaseUrls) {
+ try {
+ const data = await this.fetchJson(`${fallbackBaseUrl}${endpoint}`, options);
+ this.baseUrl = fallbackBaseUrl;
+ this.setConnectionStatus("up");
+ return data;
+ } catch (fallbackError) {
+ if (!(fallbackError instanceof TypeError)) {
+ this.setConnectionStatus("error", fallbackError.message);
+ throw fallbackError;
+ }
+ }
+ }
+
+ this.setConnectionStatus(
+ "down",
+ `Cannot reach backend at ${this.baseUrl}. Check server and REACT_APP_BASE_URL.`
+ );
+ throw error;
+ }
}
async makeRequest(endpoint, options = {}) {
if (this.isElectron) {
- // Use Electron's IPC for API requests
try {
const response = await window.electronAPI.makeApiRequest(endpoint, options);
- if (response.ok) {
- return response.data;
- } else {
- throw new Error(`API request failed with status ${response.status}`);
+ if (!response.ok) {
+ const err = new Error(`API request failed with status ${response.status}`);
+ err.isHttpError = true;
+ this.setConnectionStatus("error", err.message);
+ throw err;
}
+ this.setConnectionStatus("up");
+ return response.data;
} catch (error) {
- console.error('Electron API request failed:', error);
+ if (error?.isHttpError) {
+ throw error;
+ }
+ this.setConnectionStatus("down", error?.message || "API request failed");
throw error;
}
- } else {
- // Use regular fetch for web environment
- const url = `${this.baseUrl}${endpoint}`;
- const response = await fetch(url, options);
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- return await response.json();
}
+
+ return this.requestWithFallback(endpoint, options);
}
// Convenience methods for common HTTP verbs
async get(endpoint, options = {}) {
- return this.makeRequest(endpoint, { ...options, method: 'GET' });
+ return this.makeRequest(endpoint, { ...options, method: "GET" });
}
async post(endpoint, data, options = {}) {
return this.makeRequest(endpoint, {
...options,
- method: 'POST',
+ method: "POST",
headers: {
- 'Content-Type': 'application/json',
- ...options.headers
+ "Content-Type": "application/json",
+ ...options.headers,
},
- body: JSON.stringify(data)
+ body: JSON.stringify(data),
});
}
async postFormData(endpoint, formData, options = {}) {
if (this.isElectron) {
- // For Electron, we need to handle file uploads differently
- // Since we can't easily pass files through IPC, we'll fall back to fetch
- const url = `${this.baseUrl}${endpoint}`;
- const response = await fetch(url, {
- ...options,
- method: 'POST',
- body: formData
- });
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- return await response.json();
- } else {
- // For web, use FormData directly
- const url = `${this.baseUrl}${endpoint}`;
- const response = await fetch(url, {
- ...options,
- method: 'POST',
- body: formData
- });
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
+ try {
+ const response = await fetch(`${this.baseUrl}${endpoint}`, {
+ ...options,
+ method: "POST",
+ body: formData,
+ });
+ const data = await this.parseJsonResponse(response);
+ this.setConnectionStatus("up");
+ return data;
+ } catch (error) {
+ const status = error instanceof TypeError ? "down" : "error";
+ this.setConnectionStatus(status, error?.message || "API request failed");
+ throw error;
}
-
- return await response.json();
}
+
+ return this.requestWithFallback(endpoint, {
+ ...options,
+ method: "POST",
+ body: formData,
+ });
}
}
-// Export a singleton instance
-export default new ApiClient();
+const apiClient = new ApiClient();
+export default apiClient;