File size: 7,335 Bytes
4f163ba
 
e9db41c
 
590f245
 
 
e9db41c
 
9a0dd41
 
 
 
e9db41c
 
 
 
 
 
 
 
 
 
 
 
590f245
4f163ba
e9db41c
4f163ba
 
 
 
 
 
e9db41c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9a0dd41
 
 
e9db41c
9a0dd41
e9db41c
9a0dd41
 
 
 
e9db41c
 
4f163ba
590f245
4f163ba
 
 
 
e9db41c
590f245
4f163ba
 
 
 
 
 
 
 
 
 
 
970723c
4f163ba
 
 
970723c
 
 
4f163ba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e9db41c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9a0dd41
4f163ba
 
 
 
 
 
 
 
 
 
 
 
 
 
8a65b6e
 
 
 
 
 
 
 
 
4f163ba
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
import axios from 'axios';

// Create axios instance with dynamic base configuration and auto-discovery
// 1) Resolve env REACT_APP_API_URL (strip trailing /api if present)
const envUrlRaw = (process.env.REACT_APP_API_URL || '').trim();
const envUrlNoTrailingSlash = envUrlRaw.replace(/\/$/, '');
const envBaseWithoutApi = envUrlNoTrailingSlash.replace(/\/api$/, '');
// 2) Prefer localhost if the app runs on localhost
const isLocalhost = typeof window !== 'undefined' && /^(localhost|127\.0\.0\.1)$/i.test(window.location.hostname);
// Prefer 5000 first when running locally; 7860 as secondary
const localCandidates = isLocalhost
  ? ['http://localhost:5000', 'http://127.0.0.1:5000', 'http://localhost:7860', 'http://127.0.0.1:7860']
  : [];
// 3) HF backend as final fallback
const hfBackend = 'https://linguabot-transhub-backend.hf.space';
// 4) Build candidate list (deduped, truthy)
const candidateSet = new Set<string>([
  envBaseWithoutApi,
  ...localCandidates,
  hfBackend
].filter(Boolean) as string[]);
const candidates = Array.from(candidateSet);
// 5) Use persisted base if any
const storedBase = typeof window !== 'undefined' ? (localStorage.getItem('refinity_api_base') || '') : '';
let activeBase = storedBase && candidateSet.has(storedBase) ? storedBase : (candidates[0] || hfBackend);

const api = axios.create({
  baseURL: activeBase,
  headers: {
    'Content-Type': 'application/json',
  },
  timeout: 10000, // 10 second timeout
});

// Helper to set and persist base URL used by both axios and fetch-derived code
function setActiveBase(nextBase: string) {
  if (!nextBase) return;
  activeBase = nextBase.replace(/\/$/, '');
  api.defaults.baseURL = activeBase;
  (api.defaults as any).baseURL = activeBase; // used by components to build fetch URLs
  try { localStorage.setItem('refinity_api_base', activeBase); } catch {}
  console.log('[Refinity] API base updated:', activeBase);
}

// Background probe to auto-select a reachable backend
async function probeBases() {
  for (const base of candidates) {
    try {
      const controller = new AbortController();
      const timer = setTimeout(() => controller.abort(), 2500);
      const resp = await fetch(`${base.replace(/\/$/,'')}/api/health`, { signal: controller.signal });
      clearTimeout(timer);
      if (resp && resp.ok) {
        if (activeBase !== base) setActiveBase(base);
        return;
      }
    } catch {
      // try next
    }
  }
  // nothing reachable - keep current activeBase (likely HF)
}
// Kick off probe without blocking module init.
// In development we skip the active probing to avoid dev-tool extensions turning
// harmless network probe failures into hard errors during bundle evaluation.
if (typeof window !== 'undefined') {
  // initialize from stored or first candidate
  setActiveBase(activeBase);
  // Only auto-probe in production / deployed environments
  if (process.env.NODE_ENV === 'production') {
    probeBases();
  }
}

// Debug: Log the API URL being used
console.log('πŸ”§ API CONFIGURATION DEBUG');
console.log('Environment variables:', {
  REACT_APP_API_URL: process.env.REACT_APP_API_URL,
  NODE_ENV: process.env.NODE_ENV
});
console.log('Initial API Base URL:', activeBase);
console.log('Build timestamp:', new Date().toISOString());

// Request interceptor to add auth token and user role
api.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    
    // Add user role and info to headers
    const user = localStorage.getItem('user');
    const viewMode = (localStorage.getItem('viewMode') || 'auto');
    if (user) {
      try {
        const userData = JSON.parse(user);
        // Respect viewMode: when switched to student, force requests to be treated as student
        const effectiveRole = viewMode === 'student' ? 'student' : (userData.role || 'visitor');
        config.headers['user-role'] = effectiveRole;
        const derivedUsername = userData.username || userData.name || userData.displayName || (userData.email ? String(userData.email).split('@')[0] : undefined);
        config.headers['user-info'] = JSON.stringify({
          _id: userData._id || userData.id,
          username: derivedUsername,
          name: userData.name,
          displayName: userData.displayName,
          email: userData.email,
          role: userData.role
        });
      } catch (error) {
        config.headers['user-role'] = 'visitor';
      }
    }
    
    // Debug: Log the actual request URL
    console.log('πŸš€ Making API request to:', (config.baseURL || '') + (config.url || ''));
    console.log('πŸ”‘ Auth token:', token ? 'Present' : 'Missing');
    
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// Response interceptor to handle errors
api.interceptors.response.use(
  (response) => {
    console.log('βœ… API response received:', response.config.url);
    return response;
  },
  (error) => {
    console.error('❌ API request failed:', error.config?.url, error.message);
    
    // If the current base fails with a network/cors error, rotate to next candidate and retry once
    const isNetworkOrCORS = !error.response || error.message?.includes('Network') || error.message?.includes('Failed to fetch');
    if (isNetworkOrCORS && typeof window !== 'undefined') {
      const idx = candidates.indexOf(activeBase);
      const nextIdx = (idx + 1) % candidates.length;
      const nextBase = candidates[nextIdx];
      if (nextBase && nextBase !== activeBase) {
        setActiveBase(nextBase);
        const cfg: any = error.config || {};
        if (!cfg.__retriedWithNextBase) {
          cfg.__retriedWithNextBase = true;
          cfg.baseURL = activeBase;
          console.warn('[Refinity] Retrying request with base:', activeBase);
          return api.request(cfg);
        }
      }
    }
    
    // Don't auto-redirect for admin operations - let the component handle it
    if (error.response?.status === 401 && !error.config?.url?.includes('/subtitles/update/')) {
      // Token expired or invalid - only redirect for non-admin operations
      localStorage.removeItem('token');
      localStorage.removeItem('user');
      window.location.href = '/login';
    } else if (error.response?.status === 429) {
      // Rate limit exceeded - retry after delay
      console.warn('Rate limit exceeded, retrying after delay...');
      return new Promise(resolve => {
        setTimeout(() => {
          resolve(api.request(error.config));
        }, 2000); // Wait 2 seconds before retry
      });
    } else if (error.response?.status === 503) {
      // Service unavailable - quick exponential backoff retry (max 2 tries)
      const cfg: any = error.config || {};
      cfg.__retryCount = cfg.__retryCount || 0;
      if (cfg.__retryCount < 2) {
        cfg.__retryCount += 1;
        const delay = 500 * Math.pow(2, cfg.__retryCount); // 1000ms, 2000ms
        return new Promise(resolve => setTimeout(() => resolve(api.request(cfg)), delay));
      }
    } else if (error.response?.status === 500) {
      console.error('Server error:', error.response.data);
    } else if (error.code === 'ECONNABORTED') {
      console.error('Request timeout');
    }
    return Promise.reject(error);
  }
);

export { api };