Skip to content

Commit 2e608b4

Browse files
Merge pull request #233 from StephanAkkerman/feat/api-status-upgrade
Feat/api status upgrade
2 parents ba6e0c9 + a4bc571 commit 2e608b4

File tree

6 files changed

+364
-83
lines changed

6 files changed

+364
-83
lines changed

backend/fluentai/utils/load_models.py

+3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ def get_model_dir_name(model: str) -> str:
3232

3333
def download_all_models():
3434
"""Download all models from the Hugging Face Hub and clean them up after loading."""
35+
# Make the models dir if it does not exist yet
36+
os.makedirs("models", exist_ok=True)
37+
3538
# Get the directory names in /models
3639
downloaded_models = [
3740
str(entry)

frontend/src/app/layout.tsx

+11-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import type { Metadata } from "next";
21
import "./globals.css";
3-
import Header from "../components/Header";
2+
import Header from "@/components/Header";
3+
import StatusChecker from "@/components/StatusChecker";
4+
import { ToastProvider } from "@/contexts/ToastContext";
5+
import type { Metadata } from "next";
46

57
const isGithubPages = process.env.NODE_ENV === "production" && process.env.GITHUB_PAGES === "true";
68

@@ -20,10 +22,13 @@ export default function RootLayout({
2022
return (
2123
<html lang="en">
2224
<body className="bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 text-gray-800 dark:text-gray-200 font-sans min-h-screen flex flex-col">
23-
<Header />
24-
<main className="flex-grow max-w-6xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-12 space-y-8">
25-
{children}
26-
</main>
25+
<ToastProvider>
26+
<Header />
27+
<main className="flex-grow max-w-6xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-12 space-y-8">
28+
{children}
29+
</main>
30+
<StatusChecker />
31+
</ToastProvider>
2732
</body>
2833
</html>
2934
);

frontend/src/app/page.tsx

-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { useState } from "react";
44
import CardGenerator from "../components/CardGenerator";
55
import Flashcard from "../components/Flashcard";
66
import { Card } from "@/interfaces/CardInterfaces";
7-
import StatusChecker from "@/components/StatusChecker";
87

98
export default function Home() {
109
const [card, setCard] = useState<Card | null>(null);
@@ -24,7 +23,6 @@ export default function Home() {
2423

2524
return (
2625
<div className="flex flex-col gap-12">
27-
<StatusChecker />
2826
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-start">
2927
<div className="flex gap-12 flex-col">
3028
<CardGenerator
+153-75
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,174 @@
1-
import React, { useState, useEffect, useRef } from "react";
1+
"use client";
2+
3+
import { useEffect, useRef } from "react";
24
import { ANKI_CONFIG } from "@/config/constants";
5+
import { useToast } from "@/contexts/ToastContext";
36

47
interface APIStatus {
58
available: boolean | null;
69
name: string;
710
description: string;
8-
show: boolean;
911
}
1012

1113
export default function StatusChecker() {
12-
const [statuses, setStatuses] = useState<APIStatus[]>([
13-
{ name: "AnkiConnect", description: "Anki synchronization", available: null, show: true },
14-
{ name: "Card Generator", description: "Card creation API", available: null, show: true },
14+
const { showToast, hideAllToasts } = useToast();
15+
const statusesRef = useRef<APIStatus[]>([
16+
{ name: "AnkiConnect", description: "Anki synchronization", available: null },
17+
{ name: "Card Generator", description: "Card creation API", available: null },
1518
]);
1619

17-
// Use a ref to store the initial statuses
18-
const initialStatuses = useRef(statuses);
20+
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
21+
const isMounted = useRef(true);
22+
23+
// Define the API status check function
24+
const checkAPIStatus = async (name: string): Promise<boolean> => {
25+
try {
26+
if (name === "AnkiConnect") {
27+
const response = await fetch(ANKI_CONFIG.API_URL, {
28+
method: "POST",
29+
headers: { "Content-Type": "application/json" },
30+
body: JSON.stringify({ action: "version", version: 6 }),
31+
});
32+
if (response.ok) {
33+
const data = await response.json();
34+
return !!data.result;
35+
}
36+
}
37+
if (name === "Card Generator") {
38+
const response = await fetch("http://localhost:8000/create_card/supported_languages");
39+
if (response.ok) {
40+
return true;
41+
}
42+
}
43+
return false;
44+
} catch (error) {
45+
console.error(`Error checking ${name}:`, error);
46+
return false;
47+
}
48+
};
49+
50+
// Cleanup when component unmounts
51+
useEffect(() => {
52+
return () => {
53+
isMounted.current = false;
54+
if (pollingIntervalRef.current) {
55+
clearInterval(pollingIntervalRef.current);
56+
pollingIntervalRef.current = null;
57+
}
58+
};
59+
}, []);
60+
61+
// Set up the polling mechanism
62+
const POLLING_INTERVAL = 5000; // 5 seconds between checks
63+
const SUCCESS_DISPLAY_TIME = 3000; // 3 seconds to display success
1964

2065
useEffect(() => {
21-
const checkAPIStatuses = async () => {
22-
const newStatuses = await Promise.all(
23-
initialStatuses.current.map(async (status) => {
24-
try {
25-
if (status.name === "AnkiConnect") {
26-
const response = await fetch(ANKI_CONFIG.API_URL, {
27-
method: "POST",
28-
headers: { "Content-Type": "application/json" },
29-
body: JSON.stringify({ action: "version", version: 6 }),
30-
});
31-
if (response.ok) {
32-
const data = await response.json();
33-
return { ...status, available: !!data.result, show: true };
34-
}
35-
}
36-
if (status.name === "Card Generator") {
37-
const response = await fetch("http://localhost:8000/create_card/supported_languages");
38-
if (response.ok) {
39-
return { ...status, available: true, show: true };
40-
}
41-
}
42-
return { ...status, available: false, show: true };
43-
} catch (error) {
44-
console.error(`Error checking ${status.name}:`, error);
45-
return { ...status, available: false, show: true };
66+
// Function to check all statuses at once
67+
const checkAllStatuses = async () => {
68+
if (!isMounted.current) return;
69+
70+
// Create a copy to work with
71+
const currentStatuses = [...statusesRef.current];
72+
const updatedStatuses = [...currentStatuses];
73+
const recoveredServices: string[] = [];
74+
let hasServiceDown = false;
75+
let statusChanged = false;
76+
77+
// Check each service
78+
for (let i = 0; i < updatedStatuses.length; i++) {
79+
const status = updatedStatuses[i];
80+
const isAvailable = await checkAPIStatus(status.name);
81+
82+
// If status changed from not available to available (true transition)
83+
if (status.available === false && isAvailable) {
84+
updatedStatuses[i] = { ...status, available: true };
85+
recoveredServices.push(status.name);
86+
statusChanged = true;
87+
}
88+
// If checking for the first time and it's available
89+
else if (status.available === null && isAvailable) {
90+
updatedStatuses[i] = { ...status, available: true };
91+
}
92+
// If service is unavailable
93+
else if (!isAvailable) {
94+
if (status.available !== false) {
95+
statusChanged = true;
4696
}
47-
})
48-
);
49-
setStatuses(newStatuses);
97+
updatedStatuses[i] = { ...status, available: false };
98+
hasServiceDown = true;
99+
}
100+
}
50101

51-
newStatuses.forEach((status, index) => {
52-
if (status.available === true) {
102+
// Update statuses ref
103+
statusesRef.current = updatedStatuses;
104+
105+
// Show appropriate toast notifications
106+
if (statusChanged) {
107+
// Clear existing toasts when status changes
108+
hideAllToasts();
109+
110+
// If any service was recovered, show a success toast for it
111+
if (recoveredServices.length > 0) {
112+
// Show success toast for specifically recovered services
113+
showToast({
114+
type: 'success',
115+
title: `Service${recoveredServices.length > 1 ? 's' : ''} Recovered`,
116+
message: `${recoveredServices.join(", ")} ${recoveredServices.length > 1 ? 'are' : 'is'} now available.`,
117+
duration: SUCCESS_DISPLAY_TIME // Show longer so users notice it
118+
});
119+
}
120+
121+
// If we still have services down, show an error toast after a brief delay
122+
// This prevents the toasts from appearing simultaneously
123+
if (hasServiceDown) {
53124
setTimeout(() => {
54-
setStatuses((prev) => {
55-
const updated = [...prev];
56-
updated[index] = { ...status, show: false };
57-
return updated;
125+
if (!isMounted.current) return;
126+
127+
const unavailableServices = statusesRef.current
128+
.filter(s => s.available === false)
129+
.map(s => s.name)
130+
.join(", ");
131+
132+
showToast({
133+
type: 'error',
134+
title: 'Service Interruption',
135+
message: `${unavailableServices} ${statusesRef.current.filter(s => s.available === false).length > 1 ? 'are' : 'is'} unavailable. Please check your connections.`,
136+
duration: 0 // Keep until resolved
58137
});
59-
}, 3000);
138+
}, recoveredServices.length > 0 ? 300 : 0); // Small delay if we just showed a recovery toast
139+
} else if (recoveredServices.length > 0) {
140+
// If all services are up after some were down, show an additional "all clear" notification
141+
setTimeout(() => {
142+
if (!isMounted.current) return;
143+
144+
showToast({
145+
type: 'info',
146+
title: 'All Systems Operational',
147+
message: 'All services are now running properly.',
148+
duration: SUCCESS_DISPLAY_TIME
149+
});
150+
}, 300); // Small delay after the recovery toast
60151
}
61-
});
152+
}
153+
};
154+
155+
// Run initial check
156+
checkAllStatuses();
157+
158+
// Set up single polling interval (only if not already set)
159+
if (!pollingIntervalRef.current) {
160+
pollingIntervalRef.current = setInterval(checkAllStatuses, POLLING_INTERVAL);
161+
}
162+
163+
// Cleanup on effect change
164+
return () => {
165+
if (pollingIntervalRef.current) {
166+
clearInterval(pollingIntervalRef.current);
167+
pollingIntervalRef.current = null;
168+
}
62169
};
170+
}, [showToast, hideAllToasts]); // Include toast functions as dependencies
63171

64-
checkAPIStatuses();
65-
}, []); // Empty dependency array since we use initialStatuses.current
66-
67-
return (
68-
<div className="space-y-0">
69-
{statuses.map((status, index) => (
70-
<div
71-
key={index}
72-
className={`transform transition-all duration-500 ease-in-out overflow-hidden
73-
${status.show ? "max-h-24 mb-4 opacity-100 translate-y-0" : "max-h-0 mb-0 opacity-0 -translate-y-4"}
74-
${status.available === null
75-
? "bg-yellow-100 text-yellow-800"
76-
: status.available
77-
? "bg-green-100 text-green-800"
78-
: "bg-red-100 text-red-800"
79-
}`}
80-
>
81-
<div className="p-4 text-center rounded">
82-
{status.available === null && <p>Checking {status.name} availability...</p>}
83-
{status.available === true && (
84-
<p>{status.name} is available! ({status.description})</p>
85-
)}
86-
{status.available === false && (
87-
<p>
88-
<strong>{status.name} is unavailable.</strong> Ensure the {status.description} is running and accessible.
89-
</p>
90-
)}
91-
</div>
92-
</div>
93-
))}
94-
</div>
95-
);
172+
// This component doesn't render anything visible on its own now
173+
return null;
96174
}

0 commit comments

Comments
 (0)