Skip to content
Open
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
34 changes: 34 additions & 0 deletions eduaid_web/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
62 changes: 52 additions & 10 deletions eduaid_web/src/App.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,66 @@
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";
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 (
<div className="connection-banner down">
<span>
Backend disconnected.{" "}
{connection.detail || "Please check backend server and API URL."}
</span>
<button
type="button"
className="connection-banner-retry"
onClick={() => window.location.reload()}
>
Retry
</button>
</div>
);
}

if (connection.status === "error") {
return (
<div className="connection-banner error">
Backend responded with an error.{" "}
{connection.detail || "Please try again shortly."}
</div>
);
}

return null;
}

function App() {
const [connection, setConnection] = useState(initialConnectionState);

useEffect(() => apiClient.subscribeConnectionStatus(setConnection), []);

return (
<HashRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/question-type" element={<Question_Type />} />
<Route path="/input" element={<Text_Input />} />
<Route path="/output" element={<Output />} />
<Route path="/history" element={<Previous />} />
<Route path="*" element={<NotFound />} />
</Routes>
</HashRouter>
<>
<ConnectionBanner connection={connection} />
<HashRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/question-type" element={<Question_Type />} />
<Route path="/input" element={<Text_Input />} />
<Route path="/output" element={<Output />} />
<Route path="/history" element={<Previous />} />
<Route path="*" element={<NotFound />} />
</Routes>
</HashRouter>
</>
);
}

Expand Down
198 changes: 144 additions & 54 deletions eduaid_web/src/utils/apiClient.js
Original file line number Diff line number Diff line change
@@ -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,
});
}
Comment on lines 152 to 175
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# First, find the apiClient.js file and understand the repository structure
find . -name "apiClient.js" -o -name "*apiClient*" -o -name "*electron*" | head -20

Repository: AOSSIE-Org/EduAid

Length of output: 96


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for makeApiRequest usage and electronAPI references
rg -n 'makeApiRequest|electronAPI|contextBridge|ipcRenderer' --type js --type ts --type jsx --type tsx -B 2 -A 2

Repository: AOSSIE-Org/EduAid

Length of output: 88


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find preload scripts and main process files
fd -HI 'preload|main\.js|ipc' | grep -E '\.(js|ts)$'

Repository: AOSSIE-Org/EduAid

Length of output: 109


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Examine the full apiClient.js file to understand all request methods
cat -n eduaid_web/src/utils/apiClient.js | head -200

Repository: AOSSIE-Org/EduAid

Length of output: 6336


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Examine preload.js to see how makeApiRequest is exposed
cat -n eduaid_desktop/preload.js

Repository: AOSSIE-Org/EduAid

Length of output: 2395


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Examine main.js to see the IPC handler for makeApiRequest
cat -n eduaid_desktop/main.js | head -300

Repository: AOSSIE-Org/EduAid

Length of output: 8621


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for auth, token, session, headers handling in main.js and config
rg -n 'auth|token|session|Authorization|headers' eduaid_desktop/ -A 2 -B 1

Repository: AOSSIE-Org/EduAid

Length of output: 1286


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check config.js to understand the setup
find eduaid_desktop -name "config.js" -o -name "config.ts" -exec cat -n {} \;

Repository: AOSSIE-Org/EduAid

Length of output: 43


🏁 Script executed:

#!/bin/bash
set -eulo pipefail

# Search for any FormData usage in the codebase
rg -n 'FormData|multipart' --type js --type ts -A 3 -B 1

Repository: AOSSIE-Org/EduAid

Length of output: 1554


Enhance IPC handler to support FormData and unify with direct fetch path.

postFormData() uses direct fetch in Electron while other requests use window.electronAPI.makeApiRequest. This split exists because the current IPC handler hardcodes 'Content-Type': 'application/json' and cannot serialize FormData objects.

The workaround causes inconsistencies:

  • Error handling differs: postFormData checks error instanceof TypeError, while makeRequest uses an isHttpError flag
  • postFormData lacks the normalized error response structure
  • postFormData has no fallback mechanism (unlike makeRequest)
  • Potential missing auth/session handling if added to IPC layer later

Extend the IPC handler to support multipart/form-data with proper boundary generation, then route postFormData through the unified makeApiRequest channel.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eduaid_web/src/utils/apiClient.js` around lines 143 - 166, postFormData
currently bypasses the unified IPC path; update the IPC handler (the channel
used by window.electronAPI.makeApiRequest) to detect when body is a FormData,
serialize it into a multipart/form-data payload with a generated boundary (or
encode parts as ArrayBuffers/Blobs and send boundary/value metadata so the main
process can reconstruct the multipart body), and accept a marker indicating
"isFormData" so the handler sets the proper Content-Type with boundary and
forwards the raw multipart bytes to fetch; then change postFormData to call the
same IPC method (or requestWithFallback) as other requests
(window.electronAPI.makeApiRequest / requestWithFallback) so it receives the
normalized error shape (including isHttpError), uses the same connection-status
logic (setConnectionStatus with normalized error), and benefits from the
fallback logic in requestWithFallback. Ensure you reference and preserve symbols
postFormData, requestWithFallback, window.electronAPI.makeApiRequest, and any
normalized error fields (isHttpError) when implementing.

}

// Export a singleton instance
export default new ApiClient();
const apiClient = new ApiClient();
export default apiClient;