Skip to content
Merged
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
40 changes: 33 additions & 7 deletions frontend/src/pages/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { apiService } from "../services/api";
const POLL_INTERVAL = 600; // 0.6 seconds
const INITIAL_ERROR_STATE = { visible: false, message: '' };
const DEBOUNCE_DELAY = 300; // 300ms debounce for user input
const CONVERSATION_FETCH_ERROR_DELAY_MS = 10000; // wait 10s before showing fetch errors
const CONVERSATION_FETCH_ERROR_THRESHOLD = Math.ceil(
CONVERSATION_FETCH_ERROR_DELAY_MS / POLL_INTERVAL
);

function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
Expand Down Expand Up @@ -39,28 +43,49 @@ export default function App() {
const debouncedUserInput = useDebounce(userInput, DEBOUNCE_DELAY);

const errorTimerRef = useRef(null);
const conversationFetchErrorCountRef = useRef(0);

const handleError = useCallback((error, context) => {
console.error(`${context}:`, error);

const isConversationFetchError = error.status === 404;
const errorMessage = isConversationFetchError
? "Error fetching conversation. Retrying..." // Updated message

const isConversationFetchError =
context === "fetching conversation" && (error.status === 404 || error.status === 408);

if (isConversationFetchError) {
if (error.status === 404) {
conversationFetchErrorCountRef.current += 1;

const hasExceededThreshold =
conversationFetchErrorCountRef.current >= CONVERSATION_FETCH_ERROR_THRESHOLD;

if (!hasExceededThreshold) {
return;
}
} else {
// For timeouts or other connectivity errors surface immediately
conversationFetchErrorCountRef.current = CONVERSATION_FETCH_ERROR_THRESHOLD;
}
} else {
conversationFetchErrorCountRef.current = 0;
}

const errorMessage = isConversationFetchError
? "Error fetching conversation. Retrying..."
: `Error ${context.toLowerCase()}. Please try again.`;

setError(prevError => {
// If the same 404 error is already being displayed, don't reset state (prevents flickering)
if (prevError.visible && prevError.message === errorMessage) {
return prevError;
}
return { visible: true, message: errorMessage };
});

// Clear any existing timeout
if (errorTimerRef.current) {
clearTimeout(errorTimerRef.current);
}

// Only auto-dismiss non-404 errors after 3 seconds
if (!isConversationFetchError) {
errorTimerRef.current = setTimeout(() => setError(INITIAL_ERROR_STATE), 3000);
Expand All @@ -72,6 +97,7 @@ export default function App() {
if (errorTimerRef.current) {
clearTimeout(errorTimerRef.current);
}
conversationFetchErrorCountRef.current = 0;
setError(INITIAL_ERROR_STATE);
}, []);

Expand Down
50 changes: 45 additions & 5 deletions frontend/src/services/api.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
const API_BASE_URL = 'http://127.0.0.1:8000';

const resolveRequestTimeout = () => {
const env = typeof import.meta !== 'undefined' ? import.meta.env : undefined;
const configured = env?.VITE_API_TIMEOUT_MS;
const parsed = Number.parseInt(configured, 10);
if (Number.isFinite(parsed) && parsed > 0) {
return parsed;
}
return 15000;
};

const REQUEST_TIMEOUT_MS = resolveRequestTimeout(); // default to 15s, overridable via Vite env

class ApiError extends Error {
constructor(message, status) {
super(message);
Expand All @@ -19,12 +31,31 @@ async function handleResponse(response) {
return response.json();
}

async function fetchWithTimeout(url, options = {}, timeout = REQUEST_TIMEOUT_MS) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);

try {
return await fetch(url, { ...options, signal: controller.signal });
} catch (error) {
if (error.name === 'AbortError') {
throw new ApiError('Request timed out', 408);
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}

export const apiService = {
async getConversationHistory() {
try {
const res = await fetch(`${API_BASE_URL}/get-conversation-history`);
const res = await fetchWithTimeout(`${API_BASE_URL}/get-conversation-history`);
return handleResponse(res);
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
throw new ApiError(
'Failed to fetch conversation history',
error.status || 500
Expand All @@ -38,7 +69,7 @@ export const apiService = {
}

try {
const res = await fetch(
const res = await fetchWithTimeout(
`${API_BASE_URL}/send-prompt?prompt=${encodeURIComponent(message)}`,
{
method: 'POST',
Expand All @@ -49,6 +80,9 @@ export const apiService = {
);
return handleResponse(res);
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
throw new ApiError(
'Failed to send message',
error.status || 500
Expand All @@ -58,7 +92,7 @@ export const apiService = {

async startWorkflow() {
try {
const res = await fetch(
const res = await fetchWithTimeout(
`${API_BASE_URL}/start-workflow`,
{
method: 'POST',
Expand All @@ -69,6 +103,9 @@ export const apiService = {
);
return handleResponse(res);
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
throw new ApiError(
'Failed to start workflow',
error.status || 500
Expand All @@ -78,18 +115,21 @@ export const apiService = {

async confirm() {
try {
const res = await fetch(`${API_BASE_URL}/confirm`, {
const res = await fetchWithTimeout(`${API_BASE_URL}/confirm`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
return handleResponse(res);
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
throw new ApiError(
'Failed to confirm action',
error.status || 500
);
}
}
};
};